diff --git a/Nuclex.Support (PC).csproj b/Nuclex.Support (PC).csproj index ea87d51..6fe0931 100644 --- a/Nuclex.Support (PC).csproj +++ b/Nuclex.Support (PC).csproj @@ -170,6 +170,11 @@ false QueueOperation + + false + QueueOperation.Test + QueueOperation.cs + false ThreadOperation diff --git a/Source/Scheduling/Operation.cs b/Source/Scheduling/Operation.cs index dc031ef..9363474 100644 --- a/Source/Scheduling/Operation.cs +++ b/Source/Scheduling/Operation.cs @@ -33,7 +33,9 @@ namespace Nuclex.Support.Scheduling { /// Waits for the background operation to end /// /// Any exceptions raised in the background operation will be thrown - /// in this method. + /// in this method. If you decide to override this method, you should + /// call End() first (and let any possible exception through to your + /// caller). /// public virtual void End() { @@ -83,7 +85,7 @@ namespace Nuclex.Support.Scheduling { // We allow the caller to set the exception multiple times. While I certainly // can't think of a scenario where this would happen, throwing an exception // in that case seems worse. The caller might just be executing an exception - // handling block and locking + throwing here could cause even more problems. + // handling block and locking + throwing here could cause all kinds of problems. this.occuredException = exception; } diff --git a/Source/Scheduling/QueueOperation.Test.cs b/Source/Scheduling/QueueOperation.Test.cs new file mode 100644 index 0000000..d7893c3 --- /dev/null +++ b/Source/Scheduling/QueueOperation.Test.cs @@ -0,0 +1,193 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2007 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion +using System; +using System.Collections.Generic; +using System.IO; + +#if UNITTEST + +using NUnit.Framework; +using NMock2; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Support.Scheduling { + + /// Unit Test for the queue operation class + [TestFixture] + public class QueueOperationTest { + + #region interface IQueueOperationSubscriber + + /// Interface used to test the set progression. + public interface IQueueOperationSubscriber { + + /// Called when the queue operations's progress changes + /// Queue operation whose progress has changed + /// Contains the new progress achieved + void ProgressUpdated(object sender, ProgressUpdateEventArgs e); + + /// Called when the queue operation has ended + /// Queue operation that as ended + /// Not used + void Ended(object sender, EventArgs e); + + } + + #endregion // interface IQueueOperationSubscriber + + #region class ProgressUpdateEventArgsMatcher + + /// Compares two ProgressUpdateEventArgsInstances for NMock validation + private class ProgressUpdateEventArgsMatcher : Matcher { + + /// Initializes a new ProgressUpdateEventArgsMatcher + /// Expected progress update event arguments + public ProgressUpdateEventArgsMatcher(ProgressUpdateEventArgs expected) { + this.expected = expected; + } + + /// + /// Called by NMock to verfiy the ProgressUpdateEventArgs match the expected value + /// + /// Actual value to compare to the expected value + /// + /// True if the actual value matches the expected value; otherwise false + /// + public override bool Matches(object actualAsObject) { + ProgressUpdateEventArgs actual = (actualAsObject as ProgressUpdateEventArgs); + if(actual == null) + return false; + + return (actual.Progress == this.expected.Progress); + } + + /// Creates a string representation of the expected value + /// Writer to write the string representation into + public override void DescribeTo(TextWriter writer) { + writer.Write(this.expected.Progress.ToString()); + } + + /// Expected progress update event args value + private ProgressUpdateEventArgs expected; + + } + + #endregion // class ProgressUpdateEventArgsMatcher + + #region class TestOperation + + /// Progression used for testing in this unit test + private class TestOperation : Operation { + + /// Begins executing the operation. Yeah, sure :) + public override void Begin() { } + + /// Moves the operation into the ended state + public void SetEnded() { + SetEnded(null); + } + + /// Moves the operation into the ended state with an exception + /// Exception + public void SetEnded(Exception exception) { + SetException(exception); + OnAsyncEnded(); + } + + /// Changes the testing progression's indicated progress + /// + /// New progress to be reported by the testing progression + /// + public void ChangeProgress(float progress) { + OnAsyncProgressUpdated(progress); + } + + } + + #endregion // class TestOperation + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new Mockery(); + } + + /// Validates that the queue executes operations sequentially + [Test] + public void TestSequentialExecution() { + TestOperation operation1 = new TestOperation(); + TestOperation operation2 = new TestOperation(); + + QueueOperation testQueueOperation = + new QueueOperation( + new TestOperation[] { operation1, operation2 } + ); + + IQueueOperationSubscriber mockedSubscriber = mockSubscriber(testQueueOperation); + + testQueueOperation.Begin(); + + Expect.Once.On(mockedSubscriber). + Method("ProgressUpdated"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(QueueOperation)), + new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.25f)) + } + ); + + operation1.ChangeProgress(0.5f); + + Expect.Once.On(mockedSubscriber). + Method("ProgressUpdated"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(QueueOperation)), + new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.5f)) + } + ); + + operation1.SetEnded(); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Mocks a subscriber for the events of an operation + /// Operation to mock an event subscriber for + /// The mocked event subscriber + private IQueueOperationSubscriber mockSubscriber(Operation operation) { + IQueueOperationSubscriber mockedSubscriber = + this.mockery.NewMock(); + + operation.AsyncEnded += new EventHandler(mockedSubscriber.Ended); + operation.AsyncProgressUpdated += + new EventHandler(mockedSubscriber.ProgressUpdated); + + return mockedSubscriber; + } + + /// Mock object factory + private Mockery mockery; + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Scheduling/QueueOperation.cs b/Source/Scheduling/QueueOperation.cs index 0922f14..49389f4 100644 --- a/Source/Scheduling/QueueOperation.cs +++ b/Source/Scheduling/QueueOperation.cs @@ -32,36 +32,156 @@ namespace Nuclex.Support.Scheduling { where OperationType : Operation { /// Initializes a new queue operation + private QueueOperation() { + this.asyncOperationEndedDelegate = new EventHandler(asyncOperationEnded); + this.asyncOperationProgressUpdatedDelegate = new EventHandler( + asyncOperationProgressUpdated + ); + + this.childs = new List>(); + } + + /// Initializes a new queue operation with default weights /// Child operations to execute in this operation /// /// All child operations will have a default weight of 1.0 /// - public QueueOperation(IEnumerable childs) { - this.setProgression = new SetProgression(childs); + public QueueOperation(IEnumerable childs) : this() { + + // Construct a WeightedProgression with the default weight for each + // progression and wrap it in an ObservedProgression + foreach(OperationType operation in childs) + this.childs.Add(new WeightedProgression(operation)); + + // 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; + } /// Initializes a new queue operation with custom weights /// Child operations to execute in this operation - public QueueOperation(IEnumerable> childs) { - this.setProgression = new SetProgression(childs); + public QueueOperation(IEnumerable> childs) : this() { + + // Construct an ObservedProgression around each of the WeightedProgressions + foreach(WeightedProgression operation in childs) { + this.childs.Add(operation); + + // Sum up the total weight + this.totalWeight += operation.Weight; + } + } - + + /// Provides access to the child operations of this queue + public IList> Childs { + get { return this.childs; } + } + /// Launches the background operation public override void Begin() { - this.setProgression.AsyncProgressUpdated += - new EventHandler(setProgressionProgressUpdated); - - - - //this.setProgression.Childs[0].Progression.Begin(); + beginCurrentOperation(); } - private void setProgressionProgressUpdated(object sender, ProgressUpdateEventArgs e) { - throw new Exception("The method or operation is not implemented."); + /// Prepares the current operation and calls its Begin() method + /// + /// This subscribes the queue to the events of to the current operation + /// and launches the operation by calling its Begin() method. + /// + private void beginCurrentOperation() { + OperationType operation = this.childs[this.currentOperationIndex].Progression; + + operation.AsyncEnded += this.asyncOperationEndedDelegate; + operation.AsyncProgressUpdated += this.asyncOperationProgressUpdatedDelegate; + + operation.Begin(); } - /// SetProgression used internally to handle progress reports - private volatile SetProgression setProgression; + /// Disconnects from the current operation and calls its End() method + /// + /// This unsubscribes the queue from the current operation's events, calls End() + /// on the operation and, if the operation didn't have an exception to report, + /// counts up the accumulated progress of the queue. + /// + private void endCurrentOperation() { + OperationType operation = this.childs[this.currentOperationIndex].Progression; + + // Disconnect from the operation's events + operation.AsyncEnded -= this.asyncOperationEndedDelegate; + operation.AsyncProgressUpdated -= this.asyncOperationProgressUpdatedDelegate; + + try { + operation.End(); + + // Add the operations weight to the total amount of completed weight in the queue + this.completedWeight += this.childs[this.currentOperationIndex].Weight; + + // Trigger another progress update + OnAsyncProgressUpdated(this.completedWeight / this.totalWeight); + } + catch(Exception exception) { + SetException(exception); + } + } + + /// Called when the current executing operation ends + /// Operation that ended + /// Not used + private void asyncOperationEnded(object sender, EventArgs e) { + + // Unsubscribe from the current operation's events and update the + // accumulating progress counter + endCurrentOperation(); + + // Only jump to the next operation if no exception occured + if(OccuredException == null) { + + ++this.currentOperationIndex; + + // Execute the next operation unless we reached the end of the queue + if(this.currentOperationIndex < this.childs.Count) { + beginCurrentOperation(); + return; + } + + } + + // Either an exception has occured or we reached the end of the operation + // queue. In any case, we need to report that the operation is over. + OnAsyncEnded(); + + } + + /// Called when currently executing operation makes progress + /// Operation that has achieved progress + /// Not used + private void asyncOperationProgressUpdated(object sender, ProgressUpdateEventArgs e) { + + // Determine the completed weight of the currently executing operation + float currentOperationCompletedWeight = + e.Progress * this.childs[this.currentOperationIndex].Weight; + + // Build the total normalized amount of progress for the queue + float progress = + (this.completedWeight + currentOperationCompletedWeight) / this.totalWeight; + + // Done, we can send the actual progress to any event subscribers + OnAsyncProgressUpdated(progress); + + } + + /// Delegate to the asyncOperationEnded() method + private EventHandler asyncOperationEndedDelegate; + /// Delegate to the asyncOperationProgressUpdated() method + private EventHandler asyncOperationProgressUpdatedDelegate; + /// Operations being managed in the queue + private List> childs; + /// Summed weight of all operations in the queue + private float totalWeight; + /// Accumulated weight of the operations already completed + private float completedWeight; + /// Index of the operation currently executing + private int currentOperationIndex; } diff --git a/Source/Tracking/ProgressionTracker.cs b/Source/Tracking/ProgressionTracker.cs index 25f2bdc..1d82e2e 100644 --- a/Source/Tracking/ProgressionTracker.cs +++ b/Source/Tracking/ProgressionTracker.cs @@ -34,7 +34,7 @@ namespace Nuclex.Support.Tracking { /// This class does not implement the IProgression interface itself in /// order to not violate the design principles of progressions which /// guarantee that a progression will only finish once (whereas the - /// progression tracking might finish any number of times). + /// progression tracker might 'finish' any number of times). /// /// public class ProgressionTracker : IDisposable { @@ -98,6 +98,10 @@ namespace Nuclex.Support.Tracking { public void Dispose() { lock(this.trackedProgressions) { + // Get rid of all progression we're tracking. This unsubscribes the + // observers from the events of the progressions and stops us from + // being kept alive and receiving any further events if some of the + // tracked progressions are still executing. for(int index = 0; index < this.trackedProgressions.Count; ++index) this.trackedProgressions[index].Dispose(); diff --git a/Source/Tracking/SetProgression.Test.cs b/Source/Tracking/SetProgression.Test.cs index e6a6fbf..b64b14b 100644 --- a/Source/Tracking/SetProgression.Test.cs +++ b/Source/Tracking/SetProgression.Test.cs @@ -180,20 +180,6 @@ namespace Nuclex.Support.Tracking { this.mockery.VerifyAllExpectationsHaveBeenMet(); } - /// Mocks a subscriber for the events of a progression - /// Progression to mock an event subscriber for - /// The mocked event subscriber - private ISetProgressionSubscriber mockSubscriber(Progression progression) { - ISetProgressionSubscriber mockedSubscriber = - this.mockery.NewMock(); - - progression.AsyncEnded += new EventHandler(mockedSubscriber.Ended); - progression.AsyncProgressUpdated += - new EventHandler(mockedSubscriber.ProgressUpdated); - - return mockedSubscriber; - } - /// /// Validates that the ended event is triggered when the last progression ends /// @@ -220,6 +206,20 @@ namespace Nuclex.Support.Tracking { this.mockery.VerifyAllExpectationsHaveBeenMet(); } + /// Mocks a subscriber for the events of a progression + /// Progression to mock an event subscriber for + /// The mocked event subscriber + private ISetProgressionSubscriber mockSubscriber(Progression progression) { + ISetProgressionSubscriber mockedSubscriber = + this.mockery.NewMock(); + + progression.AsyncEnded += new EventHandler(mockedSubscriber.Ended); + progression.AsyncProgressUpdated += + new EventHandler(mockedSubscriber.ProgressUpdated); + + return mockedSubscriber; + } + /// Mock object factory private Mockery mockery; diff --git a/Source/Tracking/SetProgression.cs b/Source/Tracking/SetProgression.cs index 09745c2..358fd22 100644 --- a/Source/Tracking/SetProgression.cs +++ b/Source/Tracking/SetProgression.cs @@ -167,7 +167,7 @@ namespace Nuclex.Support.Tracking { /// WeightedProgression interface /// private volatile WeightedProgressionWrapperCollection wrapper; - /// Summed weight of all progression in the set + /// Summed weight of all progressions in the set private float totalWeight; }