SetProgression partially working; wrote several unit tests using the NMock library; lots of smaller improvements to the progression classes

git-svn-id: file:///srv/devel/repo-conversion/nusu@12 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2007-04-19 18:18:34 +00:00
parent cefbc78868
commit 71b6dc90a2
8 changed files with 452 additions and 36 deletions

View File

@ -164,6 +164,11 @@
<XNAUseContentPipeline>false</XNAUseContentPipeline> <XNAUseContentPipeline>false</XNAUseContentPipeline>
<Name>ThreadedMethodOperation</Name> <Name>ThreadedMethodOperation</Name>
</Compile> </Compile>
<Compile Include="Source\Tracking\SetProgression.Test.cs">
<XNAUseContentPipeline>false</XNAUseContentPipeline>
<Name>SetProgression.Test</Name>
<DependentUpon>SetProgression.cs</DependentUpon>
</Compile>
<Compile Include="Source\Tracking\WeightedProgression.cs"> <Compile Include="Source\Tracking\WeightedProgression.cs">
<XNAUseContentPipeline>false</XNAUseContentPipeline> <XNAUseContentPipeline>false</XNAUseContentPipeline>
<Name>WeightedProgression</Name> <Name>WeightedProgression</Name>

View File

@ -49,35 +49,36 @@ namespace Nuclex.Support.Collections {
new EventHandler<ObservableCollection<int>.ItemEventArgs>( new EventHandler<ObservableCollection<int>.ItemEventArgs>(
this.mockedSubscriber.ItemRemoved this.mockedSubscriber.ItemRemoved
); );
this.mockery.VerifyAllExpectationsHaveBeenMet();
} }
/// <summary>Tests whether the Clearing event is fired</summary> /// <summary>Tests whether the Clearing event is fired</summary>
[Test] [Test]
public void TestClearingEvent() { public void TestClearingEvent() {
Expect.Once.On(this.mockedSubscriber). Expect.Once.On(this.mockedSubscriber).
Method("Clearing"); Method("Clearing");
this.observedCollection.Clear(); this.observedCollection.Clear();
this.mockery.VerifyAllExpectationsHaveBeenMet();
} }
/// <summary>Tests whether the ItemAdded event is fired</summary> /// <summary>Tests whether the ItemAdded event is fired</summary>
[Test] [Test]
public void TestItemAddedEvent() { public void TestItemAddedEvent() {
Expect.Once.On(this.mockedSubscriber). Expect.Once.On(this.mockedSubscriber).
Method("ItemAdded"). Method("ItemAdded").
WithAnyArguments(); WithAnyArguments();
this.observedCollection.Add(123); this.observedCollection.Add(123);
this.mockery.VerifyAllExpectationsHaveBeenMet();
} }
/// <summary>Tests whether the ItemRemoved event is fired</summary> /// <summary>Tests whether the ItemRemoved event is fired</summary>
[Test] [Test]
public void TestItemRemovedEvent() { public void TestItemRemovedEvent() {
Expect.Once.On(this.mockedSubscriber). Expect.Once.On(this.mockedSubscriber).
Method("ItemAdded"). Method("ItemAdded").
WithAnyArguments(); WithAnyArguments();
@ -89,6 +90,7 @@ namespace Nuclex.Support.Collections {
this.observedCollection.Add(123); this.observedCollection.Add(123);
this.observedCollection.Remove(123); this.observedCollection.Remove(123);
this.mockery.VerifyAllExpectationsHaveBeenMet();
} }
/// <summary>Mock object factory</summary> /// <summary>Mock object factory</summary>

View File

@ -8,34 +8,105 @@ namespace Nuclex.Support.Tracking {
/// <typeparam name="ProgressionType"> /// <typeparam name="ProgressionType">
/// Type of the progression that is being observed /// Type of the progression that is being observed
/// </typeparam> /// </typeparam>
internal class ObservedProgression<ProgressionType> internal class ObservedProgression<ProgressionType> : IDisposable
where ProgressionType : Progression { where ProgressionType : Progression {
/// <summary>Delegate for reporting progress updates</summary>
public delegate void ReportDelegate();
/// <summary>Initializes a new observed progression</summary> /// <summary>Initializes a new observed progression</summary>
/// <param name="weightedProgression">Weighted progression being observed</param> /// <param name="weightedProgression">Weighted progression being observed</param>
/// <param name="progressUpdateCallback">
/// Callback to invoke when the progression's progress changes
/// </param>
/// <param name="endedCallback">
/// Callback to invoke when the progression has ended
/// </param>
internal ObservedProgression( internal ObservedProgression(
WeightedProgression<ProgressionType> weightedProgression WeightedProgression<ProgressionType> weightedProgression,
ReportDelegate progressUpdateCallback,
ReportDelegate endedCallback
) { ) {
this.weightedProgression = weightedProgression; this.weightedProgression = weightedProgression;
this.endedCallback = endedCallback;
this.progressUpdateCallback = progressUpdateCallback;
this.weightedProgression.Progression.AsyncEnded +=
new EventHandler(asyncEnded);
this.weightedProgression.Progression.AsyncProgressUpdated +=
new EventHandler<ProgressUpdateEventArgs>(asyncProgressUpdated);
}
/// <summary>Immediately releases all resources owned by the object</summary>
public void Dispose() {
disconnectEvents();
} }
/// <summary>Weighted progression being observed</summary> /// <summary>Weighted progression being observed</summary>
public WeightedProgression<ProgressionType> WeightedProgression { public WeightedProgression<ProgressionType> WeightedProgression {
get { return this.weightedProgression; } get { return this.weightedProgression; }
} }
/*
internal void AsyncProgressUpdated(object sender, ProgressUpdateEventArgs e) { /// <summary>Amount of progress this progression has achieved so far</summary>
this.Progress = e.Progress; public float Progress {
get { return this.progress; }
} }
internal void AsyncEnded(object sender, EventArgs e) { }
*/ /// <summary>Called when the observed progression has ended</summary>
/// <param name="sender">Progression that has ended</param>
/// <param name="e">Not used</param>
private void asyncEnded(object sender, EventArgs e) {
this.progress = 1.0f;
disconnectEvents(); // We don't need those anymore!
this.progressUpdateCallback();
}
/// <summary>Called when the progress of the observed progression changes</summary>
/// <param name="sender">Progression whose progress has changed</param>
/// <param name="e">Contains the updated progress</param>
private void asyncProgressUpdated(object sender, ProgressUpdateEventArgs e) {
this.progress = e.Progress;
this.progressUpdateCallback();
}
/// <summary>Unscribes from all events of the observed progression</summary>
private void disconnectEvents() {
// Make use of the double check locking idiom to avoid the costly lock when
// the events have already been unsubscribed
if(this.endedCallback != null) {
// This is an internal class with special knowledge that there
// is no risk of deadlock involved, so we don't need a fancy syncRoot!
lock(this) {
if(this.endedCallback != null) {
this.weightedProgression.Progression.AsyncEnded -=
new EventHandler(asyncEnded);
this.weightedProgression.Progression.AsyncProgressUpdated -=
new EventHandler<ProgressUpdateEventArgs>(asyncProgressUpdated);
this.endedCallback = null;
this.progressUpdateCallback = null;
}
}
} // endedCallback != null
}
/// <summary>The weighted progression that is being observed</summary> /// <summary>The weighted progression that is being observed</summary>
private WeightedProgression<ProgressionType> weightedProgression; private WeightedProgression<ProgressionType> weightedProgression;
/* /// <summary>Callback to invoke when the progress updates</summary>
/// <summary>Amount of progress this progression has achieved so far</summary> private volatile ReportDelegate progressUpdateCallback;
/// <summary>Callback to invoke when the progression ends</summary>
private volatile ReportDelegate endedCallback;
/// <summary>Progress achieved so far</summary>
private volatile float progress; private volatile float progress;
*/
} }
} // namespace Nuclex.Support.Tracking } // namespace Nuclex.Support.Tracking

View File

@ -27,7 +27,7 @@ namespace Nuclex.Support.Tracking {
/// SetProgression or QueueOperation classes was created. /// SetProgression or QueueOperation classes was created.
/// </para> /// </para>
/// </remarks> /// </remarks>
internal class WeightedProgressionCollection<ProgressionType> : internal class WeightedProgressionWrapperCollection<ProgressionType> :
TransformingReadOnlyCollection< TransformingReadOnlyCollection<
ObservedProgression<ProgressionType>, WeightedProgression<ProgressionType> ObservedProgression<ProgressionType>, WeightedProgression<ProgressionType>
> >
@ -35,7 +35,7 @@ namespace Nuclex.Support.Tracking {
/// <summary>Initializes a new weighted progression collection wrapper</summary> /// <summary>Initializes a new weighted progression collection wrapper</summary>
/// <param name="items">Items to be exposed as weighted progressions</param> /// <param name="items">Items to be exposed as weighted progressions</param>
internal WeightedProgressionCollection( internal WeightedProgressionWrapperCollection(
IList<ObservedProgression<ProgressionType>> items IList<ObservedProgression<ProgressionType>> items
) )
: base(items) { } : base(items) { }

View File

@ -19,7 +19,7 @@ namespace Nuclex.Support.Tracking {
} }
/// <summary>Achieved progress</summary> /// <summary>Achieved progress</summary>
protected float progress; private float progress;
} }

View File

@ -52,7 +52,7 @@ namespace Nuclex.Support.Tracking {
public event EventHandler AsyncEnded; public event EventHandler AsyncEnded;
/// <summary>Whether the progression has ended already</summary> /// <summary>Whether the progression has ended already</summary>
public virtual bool Ended { public bool Ended {
get { return this.ended; } get { return this.ended; }
} }
@ -63,6 +63,10 @@ namespace Nuclex.Support.Tracking {
// The WaitHandle will only be created when someone asks for it! // The WaitHandle will only be created when someone asks for it!
// See the Double-Check Locking idiom on why the condition is checked twice // See the Double-Check Locking idiom on why the condition is checked twice
// (primarily, it avoids an expensive lock when it isn't needed) // (primarily, it avoids an expensive lock when it isn't needed)
//
// We can *not* optimize this lock away since we absolutely must not create
// two doneEvents -- someone might call .WaitOne() on the first one when only
// the second one is assigned to this.doneEvent and gets set in the end.
if(this.doneEvent == null) { if(this.doneEvent == null) {
lock(this.syncRoot) { lock(this.syncRoot) {
@ -109,16 +113,27 @@ namespace Nuclex.Support.Tracking {
/// seperately. /// seperately.
/// </remarks> /// </remarks>
protected virtual void OnAsyncEnded() { protected virtual void OnAsyncEnded() {
// TODO: Find a way around this. Interlocked.Exchange would be best!
// We do not lock here since we require this method to be called only once
// in the object's lifetime. If someone really badly wanted to break this
// he'd probably have a one-in-a-million chance of getting through.
if(this.ended)
throw new InvalidOperationException("The progression has already been ended");
this.ended = true; this.ended = true;
lock(this.syncRoot) { // Doesn't need a lock. If another thread wins the race and creates the event
// after we just saw it being null, it would be created in an already set
// state due to the ended flag (see above) being set to true beforehand!
if(this.doneEvent != null) if(this.doneEvent != null)
this.doneEvent.Set(); this.doneEvent.Set();
}
// Finally, fire the AsyncEnded event
EventHandler copy = AsyncEnded; EventHandler copy = AsyncEnded;
if(copy != null) if(copy != null)
copy(this, EventArgs.Empty); copy(this, EventArgs.Empty);
} }
/// <summary>Used to synchronize multithreaded accesses to this object</summary> /// <summary>Used to synchronize multithreaded accesses to this object</summary>

View File

@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.IO;
#if UNITTEST
using NUnit.Framework;
using NMock2;
namespace Nuclex.Support.Tracking {
/// <summary>Unit Test for the progression set class</summary>
[TestFixture]
public class SetProgressionTest {
#region interface ISetProgressionSubscriber
/// <summary>Interface used to test the set progression.</summary>
public interface ISetProgressionSubscriber {
/// <summary>Called when the set progression's progress changes</summary>
/// <param name="sender">Set progression whose progress has changed</param>
/// <param name="e">Contains the new progress achieved</param>
void ProgressUpdated(object sender, ProgressUpdateEventArgs e);
/// <summary>Called when the set progression has ended</summary>
/// <param name="sender">Set progression that as ended</param>
/// <param name="e">Not used</param>
void Ended(object sender, EventArgs e);
}
#endregion // interface ISetProgressionSubscriber
#region class ProgressUpdateEventArgsMatcher
/// <summary>Compares two ProgressUpdateEventArgsInstances for NMock validation</summary>
private class ProgressUpdateEventArgsMatcher : Matcher {
/// <summary>Initializes a new ProgressUpdateEventArgsMatcher </summary>
/// <param name="expected">Expected progress update event arguments</param>
public ProgressUpdateEventArgsMatcher(ProgressUpdateEventArgs expected) {
this.expected = expected;
}
/// <summary>
/// Called by NMock to verfiy the ProgressUpdateEventArgs match the expected value
/// </summary>
/// <param name="actualAsObject">Actual value to compare to the expected value</param>
/// <returns>
/// True if the actual value matches the expected value; otherwise false
/// </returns>
public override bool Matches(object actualAsObject) {
ProgressUpdateEventArgs actual = (actualAsObject as ProgressUpdateEventArgs);
if(actual == null)
return false;
return (actual.Progress == this.expected.Progress);
}
/// <summary>Creates a string representation of the expected value</summary>
/// <param name="writer">Writer to write the string representation into</param>
public override void DescribeTo(TextWriter writer) {
writer.Write(this.expected.Progress.ToString());
}
/// <summary>Expected progress update event args value</summary>
private ProgressUpdateEventArgs expected;
}
#endregion // class ProgressUpdateEventArgsMatcher
#region class TestProgression
/// <summary>Progression used for testing in this unit test</summary>
private class TestProgression : Progression {
/// <summary>Changes the testing progression's indicated progress</summary>
/// <param name="progress">
/// New progress to be reported by the testing progression
/// </param>
public void ChangeProgress(float progress) {
OnAsyncProgressUpdated(progress);
}
/// <summary>Transitions the progression into the ended state</summary>
public void End() {
OnAsyncEnded();
}
}
#endregion // class TestProgression
/// <summary>Initialization routine executed before each test is run</summary>
[SetUp]
public void Setup() {
this.mockery = new Mockery();
}
/// <summary>Validates that the set progression properly sums the progress</summary>
[Test]
public void TestSummedProgress() {
SetProgression<TestProgression> testSetProgression =
new SetProgression<TestProgression>(
new TestProgression[] {
new TestProgression(), new TestProgression()
}
);
ISetProgressionSubscriber mockedSubscriber = mockSubscriber(testSetProgression);
Expect.Once.On(mockedSubscriber).
Method("ProgressUpdated").
With(
new Matcher[] {
new NMock2.Matchers.TypeMatcher(typeof(SetProgression<TestProgression>)),
new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.25f))
}
);
testSetProgression.Childs[0].Progression.ChangeProgress(0.5f);
this.mockery.VerifyAllExpectationsHaveBeenMet();
}
/// <summary>Validates that the set progression respects the weights</summary>
[Test]
public void TestWeightedSummedProgress() {
SetProgression<TestProgression> testSetProgression =
new SetProgression<TestProgression>(
new WeightedProgression<TestProgression>[] {
new WeightedProgression<TestProgression>(new TestProgression(), 1.0f),
new WeightedProgression<TestProgression>(new TestProgression(), 2.0f)
}
);
ISetProgressionSubscriber mockedSubscriber = mockSubscriber(testSetProgression);
Expect.Once.On(mockedSubscriber).
Method("ProgressUpdated").
With(
new Matcher[] {
new NMock2.Matchers.TypeMatcher(typeof(SetProgression<TestProgression>)),
new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.5f / 3.0f))
}
);
testSetProgression.Childs[0].Progression.ChangeProgress(0.5f);
Expect.Once.On(mockedSubscriber).
Method("ProgressUpdated").
With(
new Matcher[] {
new NMock2.Matchers.TypeMatcher(typeof(SetProgression<TestProgression>)),
new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.5f))
}
);
testSetProgression.Childs[1].Progression.ChangeProgress(0.5f);
this.mockery.VerifyAllExpectationsHaveBeenMet();
}
/// <summary>Mocks a subscriber for the events of a progression</summary>
/// <param name="progression">Progression to mock an event subscriber for</param>
/// <returns>The mocked event subscriber</returns>
private ISetProgressionSubscriber mockSubscriber(Progression progression) {
ISetProgressionSubscriber mockedSubscriber =
this.mockery.NewMock<ISetProgressionSubscriber>();
progression.AsyncEnded += new EventHandler(mockedSubscriber.Ended);
progression.AsyncProgressUpdated +=
new EventHandler<ProgressUpdateEventArgs>(mockedSubscriber.ProgressUpdated);
return mockedSubscriber;
}
/// <summary>
/// Validates that the ended event is triggered when the last progression ends
/// </summary>
[Test]
public void TestEndedEvent() {
SetProgression<TestProgression> testSetProgression =
new SetProgression<TestProgression>(
new TestProgression[] {
new TestProgression(), new TestProgression()
}
);
ISetProgressionSubscriber mockedSubscriber = mockSubscriber(testSetProgression);
Expect.Exactly(2).On(mockedSubscriber).
Method("ProgressUpdated").
WithAnyArguments();
Expect.Once.On(mockedSubscriber).
Method("Ended").
WithAnyArguments();
testSetProgression.Childs[0].Progression.End();
testSetProgression.Childs[1].Progression.End();
this.mockery.VerifyAllExpectationsHaveBeenMet();
}
/// <summary>Mock object factory</summary>
private Mockery mockery;
}
} // namespace Nuclex.Support.Tracking
#endif // UNITTEST

View File

@ -11,31 +11,139 @@ namespace Nuclex.Support.Tracking {
public class SetProgression<ProgressionType> : Progression public class SetProgression<ProgressionType> : Progression
where ProgressionType : Progression { where ProgressionType : Progression {
/// <summary>Performs common initialization for the public constructors</summary>
private SetProgression() {
this.childs = new List<ObservedProgression<ProgressionType>>();
this.asyncProgressUpdatedDelegate =
new ObservedProgression<ProgressionType>.ReportDelegate(asyncProgressUpdated);
this.asyncEndedDelegate =
new ObservedProgression<ProgressionType>.ReportDelegate(asyncEnded);
}
/// <summary>Initializes a new set progression</summary> /// <summary>Initializes a new set progression</summary>
/// <param name="progressions">Progressions to track with this set</param> /// <param name="childs">Progressions to track with this set</param>
/// <remarks> /// <remarks>
/// Uses a default weighting factor of 1.0 for all progressions. /// Uses a default weighting factor of 1.0 for all progressions.
/// </remarks> /// </remarks>
public SetProgression(IEnumerable<ProgressionType> progressions) { public SetProgression(IEnumerable<ProgressionType> childs)
: this() {
// Construct a WeightedProgression with the default weight for each
// progression and wrap it in an ObservedProgression
foreach(ProgressionType progression in childs) {
this.childs.Add(
new ObservedProgression<ProgressionType>(
new WeightedProgression<ProgressionType>(progression),
this.asyncProgressUpdatedDelegate, this.asyncEndedDelegate
)
);
}
// Since all progressions have a weight of 1.0, the total weight is
// equal to the number of progressions in our list
this.totalWeight = (float)this.childs.Count;
} }
/// <summary>Initializes a new set progression</summary> /// <summary>Initializes a new set progression</summary>
/// <param name="progressions">Progressions to track with this set</param> /// <param name="childs">Progressions to track with this set</param>
public SetProgression(IEnumerable<WeightedProgression<ProgressionType>> progressions) { public SetProgression(
IEnumerable<WeightedProgression<ProgressionType>> childs
)
: this() {
// Construct an ObservedProgression around each of the WeightedProgressions
float totalWeight = 0.0f;
foreach(WeightedProgression<ProgressionType> progression in childs) {
this.childs.Add(
new ObservedProgression<ProgressionType>(
progression,
this.asyncProgressUpdatedDelegate, this.asyncEndedDelegate
)
);
// Sum up the total weight
totalWeight += progression.Weight;
}
// Take over the summed weight of all progressions we were given
this.totalWeight = totalWeight;
} }
/// <summary>Childs contained in the progression set</summary> /// <summary>Childs contained in the progression set</summary>
public ReadOnlyCollection<WeightedProgression<ProgressionType>> Childs { public IList<WeightedProgression<ProgressionType>> Childs {
get { return null; } get {
// The wrapper is constructed only when needed. Most of the time, users will
// just create a SetProgression and monitor its progress without ever using
// the Childs collection.
if(this.wrapper == null) {
// This doesn't need a lock because it's only a stateless wrapper. If it
// is constructed twice, then so be it.
this.wrapper = new WeightedProgressionWrapperCollection<ProgressionType>(
this.childs
);
} }
/*
return this.wrapper;
}
}
/// <summary>
/// Called when the progress of one of the observed progressions changes
/// </summary>
private void asyncProgressUpdated() {
// Calculate the sum of the progress reported by our child progressions,
// scaled to the weight each progression has assigned to it.
float totalProgress = 0.0f;
for(int index = 0; index < this.childs.Count; ++index) {
totalProgress +=
this.childs[index].Progress * this.childs[index].WeightedProgression.Weight;
}
// Calculate the actual combined progress
if(this.totalWeight > 0.0f)
totalProgress /= this.totalWeight;
// Send out the progress update
OnAsyncProgressUpdated(totalProgress);
}
/// <summary>
/// Called when an observed progressions ends
/// </summary>
private void asyncEnded() {
for(int index = 0; index < this.childs.Count; ++index)
if(!this.childs[index].WeightedProgression.Progression.Ended)
return;
OnAsyncEnded();
}
/// <summary>Progressions being managed in the set</summary> /// <summary>Progressions being managed in the set</summary>
private ReadOnlyCollection<ProgressionType> progressions; private List<ObservedProgression<ProgressionType>> childs;
/// <summary>whether the progress needs to be recalculated</summary> /// <summary>
private volatile bool needProgressRecalculation; /// Wrapper collection for exposing the child progressions under the
/// <summary>Total progress achieved by the progressions in this collection</summary> /// WeightedProgression interface
private volatile float totalProgress; /// </summary>
*/ private volatile WeightedProgressionWrapperCollection<ProgressionType> wrapper;
/// <summary>Summed weight of all progression in the set</summary>
private float totalWeight;
/// <summary>Delegate for the asyncProgressUpdated() method</summary>
private ObservedProgression<ProgressionType>.ReportDelegate asyncProgressUpdatedDelegate;
/// <summary>Delegate for the asyncEnded() method</summary>
private ObservedProgression<ProgressionType>.ReportDelegate asyncEndedDelegate;
} }
} // namespace Nuclex.Support.Tracking } // namespace Nuclex.Support.Tracking