Fixed a critical threading bug in the weighted transaction observation wrapper; ProgressTracker not has 100% test coverage; wrote even more unit tests for the ProgressTracker

git-svn-id: file:///srv/devel/repo-conversion/nusu@101 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2008-12-05 19:28:39 +00:00
parent 8c5f2d45f7
commit e785fdf57d
3 changed files with 172 additions and 27 deletions

View File

@ -126,10 +126,14 @@ namespace Nuclex.Support.Tracking {
/// <summary>Called when the progress of the observed transaction changes</summary>
/// <param name="sender">Transaction whose progress has changed</param>
/// <param name="e">Contains the updated progress</param>
private void asyncProgressChanged(object sender, ProgressReportEventArgs e) {
this.progress = e.Progress;
this.progressUpdateCallback();
/// <param name="arguments">Contains the updated progress</param>
private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) {
this.progress = arguments.Progress;
ReportDelegate savedProgressUpdateCallback = this.progressUpdateCallback;
if(savedProgressUpdateCallback != null) {
savedProgressUpdateCallback();
}
}
/// <summary>Unsubscribes from all events of the observed transaction</summary>

View File

@ -135,7 +135,28 @@ namespace Nuclex.Support.Tracking {
}
#endregion // class TestWiatable
#endregion // class TestTransaction
#region class EvilTransaction
/// <summary>
/// Transaction that tries to emulate a thread given a progress report at
/// a very inconvenient time ;)
/// </summary>
private class EvilTransaction : Transaction, IProgressReporter {
/// <summary>will be triggered to report when progress has been achieved</summary>
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged {
add {}
remove {
// Send a progress update right when the subscriber is trying to unsubscribe
value(this, new ProgressReportEventArgs(0.5f));
}
}
}
#endregion // class EvilTransaction
/// <summary>Initialization routine executed before each test is run</summary>
[SetUp]
@ -380,6 +401,115 @@ namespace Nuclex.Support.Tracking {
}
}
/// <summary>
/// Tests whether the progress tracker enters and leaves the idle state correctly
/// when a transaction is removed via Untrack()
/// </summary>
[Test]
public void TestIdleWithUntrack() {
using(ProgressTracker tracker = new ProgressTracker()) {
TestTransaction test1 = new TestTransaction();
Assert.IsTrue(tracker.Idle);
tracker.Track(test1);
Assert.IsFalse(tracker.Idle);
tracker.Untrack(test1);
Assert.IsTrue(tracker.Idle);
}
}
/// <summary>
/// Tests whether the progress tracker enters and leaves the idle state correctly
/// when a transaction is removed the transaction finishing
/// </summary>
[Test]
public void TestIdleWithAutoRemoval() {
using(ProgressTracker tracker = new ProgressTracker()) {
TestTransaction test1 = new TestTransaction();
Assert.IsTrue(tracker.Idle);
tracker.Track(test1);
Assert.IsFalse(tracker.Idle);
test1.End();
Assert.IsTrue(tracker.Idle);
}
}
/// <summary>
/// Tests whether the progress tracker enters and leaves the idle state correctly
/// when a transaction is removed via Untrack()
/// </summary>
[Test]
public void TestProgressWithUntrack() {
using(ProgressTracker tracker = new ProgressTracker()) {
TestTransaction test1 = new TestTransaction();
TestTransaction test2 = new TestTransaction();
tracker.Track(test1);
tracker.Track(test2);
Assert.AreEqual(0.0f, tracker.Progress);
test1.ChangeProgress(0.5f);
Assert.AreEqual(0.25f, tracker.Progress);
tracker.Untrack(test2);
Assert.AreEqual(0.5f, tracker.Progress);
}
}
/// <summary>
/// Verifies that the progress tracker throws an exception if it is instructed
/// to untrack a transaction it doesn't know about
/// </summary>
[Test, ExpectedException(typeof(ArgumentException))]
public void TestThrowOnUntrackNonTrackedTransaction() {
using(ProgressTracker tracker = new ProgressTracker()) {
TestTransaction test1 = new TestTransaction();
tracker.Untrack(test1);
}
}
/// <summary>
/// Verifies that the progress tracker throws an exception if it is instructed
/// to untrack a transaction it doesn't know about
/// </summary>
[Test]
public void TestProgressReportDuringUnsubscribe() {
using(ProgressTracker tracker = new ProgressTracker()) {
EvilTransaction evil = new EvilTransaction();
tracker.Track(evil);
tracker.Untrack(evil);
}
}
/// <summary>
/// Verifies that the progress tracker doesn't choke on a transaction being tracked
/// multiple times.
/// </summary>
[Test]
public void TestMultiTrackedTransaction() {
using(ProgressTracker tracker = new ProgressTracker()) {
TestTransaction test = new TestTransaction();
tracker.Track(test);
tracker.Track(test);
tracker.Track(test);
tracker.Untrack(test);
tracker.Untrack(test);
tracker.Untrack(test);
}
}
/// <summary>Mocks a subscriber for the events of a tracker</summary>
/// <param name="tracker">Tracker to mock an event subscriber for</param>
/// <returns>The mocked event subscriber</returns>

View File

@ -156,7 +156,7 @@ namespace Nuclex.Support.Tracking {
}
} else { // Not ended -- Transation is still running
} else { // Not ended -- Transaction is still running
// Construct a new transation observer and add the transaction to our
// list of tracked transactions.
@ -171,8 +171,9 @@ namespace Nuclex.Support.Tracking {
// If this is the first transaction to be added to the list, tell our
// owner that we're idle no longer!
if(wasEmpty)
if(wasEmpty) {
setIdle(false);
}
} // if transaction ended
@ -200,8 +201,9 @@ namespace Nuclex.Support.Tracking {
new TransactionMatcher(transaction).Matches
)
);
if(removeIndex == -1)
throw new InvalidOperationException("Item is not being tracked");
if(removeIndex == -1) {
throw new ArgumentException("Specified transaction is not being tracked");
}
// Remove and dispose the transaction the user wants to untrack
{
@ -225,9 +227,13 @@ namespace Nuclex.Support.Tracking {
// Rebuild the total weight from scratch. Subtracting the removed transaction's
// weight would work, too, but we might accumulate rounding errors making the sum
// drift slowly away from the actual value.
this.totalWeight = 0.0f;
float newTotalWeight = 0.0f;
for(int index = 0; index < this.trackedTransactions.Count; ++index)
this.totalWeight += this.trackedTransactions[index].WeightedTransaction.Weight;
newTotalWeight += this.trackedTransactions[index].WeightedTransaction.Weight;
this.totalWeight = newTotalWeight;
recalculateProgress();
}
@ -262,7 +268,7 @@ namespace Nuclex.Support.Tracking {
/// <summary>Recalculates the total progress of the tracker</summary>
private void recalculateProgress() {
float totalProgress = 0.0f;
bool progressChanged = false;
// Lock the collection to avoid trouble when someone tries to remove one
// of our tracked transactions while we're just doing a progress update
@ -272,27 +278,32 @@ namespace Nuclex.Support.Tracking {
// ended and the collection of tracked transactions is cleared, a waiting
// thread might deliver another progress update causing this method to
// be entered. In this case, the right thing is to do nothing at all.
if(this.totalWeight == 0.0f)
return;
if(this.totalWeight != 0.0f) {
float totalProgress = 0.0f;
// Sum up the total progress
for(int index = 0; index < this.trackedTransactions.Count; ++index) {
float weight = this.trackedTransactions[index].WeightedTransaction.Weight;
totalProgress += this.trackedTransactions[index].Progress * weight;
}
// Sum up the total progress
for(int index = 0; index < this.trackedTransactions.Count; ++index) {
float weight = this.trackedTransactions[index].WeightedTransaction.Weight;
totalProgress += this.trackedTransactions[index].Progress * weight;
}
// This also needs to be in the lock to guarantee that the totalWeight is
// the one for the number of transactions we just summed -- by design,
// the total weight always has to be updated at the same time as the collection.
totalProgress /= this.totalWeight;
// This also needs to be in the lock to guarantee that the total weight
// corresponds to the number of transactions we just summed -- by design,
// the total weight always has to be updated at the same time as the collection.
totalProgress /= this.totalWeight;
// Finally, trigger the event if the progress has changed
if(totalProgress != this.progress) {
this.progress = totalProgress;
OnAsyncProgressUpdated(totalProgress);
if(totalProgress != this.progress) {
this.progress = totalProgress;
progressChanged = true;
}
}
} // lock
// Finally, trigger the event if the progress has changed
if(progressChanged) {
OnAsyncProgressUpdated(this.progress);
}
}
/// <summary>Called when one of the tracked transactions has ended</summary>