From 8c5f2d45f76d2fe8c076de06d4255a4c2356305f Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Wed, 3 Dec 2008 18:58:20 +0000 Subject: [PATCH] Renamed Waitable to Transaction (Progression -> Waitable -> Transaction -- might it be that this concept is no crispy enough and should be revisited? I think so!); increased test coverage for lots of classes in the tracking namespace, all but 3 are now 100% covered; eliminated redundant ProgressUpdate events from the ProgressTracker and adjusted unit tests so progress update events that re-report the current progress are optional; added a new idea for a replacement of the broken (quality-wise, at least) command line parser git-svn-id: file:///srv/devel/repo-conversion/nusu@100 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support.csproj | 36 +- Source/Parsing/CommandLine.cs | 46 +++ Source/Plugins/PluginRepository.Test.cs | 4 +- Source/Scheduling/OperationQueue.Test.cs | 32 +- Source/Scheduling/OperationQueue.cs | 34 +- Source/Tracking/IdleStateEventArgs.Test.cs | 59 ++++ .../ObservedWeightedTransaction.Test.cs | 158 +++++++++ .../Internal/ObservedWeightedTransaction.cs | 177 ++++++++++ .../Internal/ObservedWeightedWaitable.cs | 177 ---------- ...ightedTransactionWrapperCollection.Test.cs | 70 ++++ ...> WeightedTransactionWrapperCollection.cs} | 46 +-- .../Tracking/ProgressReportEventArgs.Test.cs | 59 ++++ Source/Tracking/ProgressTracker.Test.cs | 322 ++++++++++-------- Source/Tracking/ProgressTracker.cs | 291 ++++++++-------- Source/Tracking/Request.cs | 4 +- Source/Tracking/StatusReportEventArgs.Test.cs | 60 ++++ Source/Tracking/Transaction.Test.cs | 189 ++++++++++ .../Tracking/{Waitable.cs => Transaction.cs} | 42 +-- Source/Tracking/TransactionGroup.Test.cs | 263 ++++++++++++++ Source/Tracking/TransactionGroup.cs | 210 ++++++++++++ Source/Tracking/WaitableGroup.Test.cs | 257 -------------- Source/Tracking/WaitableGroup.cs | 202 ----------- Source/Tracking/WeightedTransaction.Test.cs | 88 +++++ Source/Tracking/WeightedTransaction.cs | 60 ++++ Source/Tracking/WeightedWaitable.cs | 60 ---- 25 files changed, 1867 insertions(+), 1079 deletions(-) create mode 100644 Source/Parsing/CommandLine.cs create mode 100644 Source/Tracking/IdleStateEventArgs.Test.cs create mode 100644 Source/Tracking/Internal/ObservedWeightedTransaction.Test.cs create mode 100644 Source/Tracking/Internal/ObservedWeightedTransaction.cs delete mode 100644 Source/Tracking/Internal/ObservedWeightedWaitable.cs create mode 100644 Source/Tracking/Internal/WeightedTransactionWrapperCollection.Test.cs rename Source/Tracking/Internal/{WeightedWaitableWrapperCollection.cs => WeightedTransactionWrapperCollection.cs} (50%) create mode 100644 Source/Tracking/ProgressReportEventArgs.Test.cs create mode 100644 Source/Tracking/StatusReportEventArgs.Test.cs create mode 100644 Source/Tracking/Transaction.Test.cs rename Source/Tracking/{Waitable.cs => Transaction.cs} (83%) create mode 100644 Source/Tracking/TransactionGroup.Test.cs create mode 100644 Source/Tracking/TransactionGroup.cs delete mode 100644 Source/Tracking/WaitableGroup.Test.cs delete mode 100644 Source/Tracking/WaitableGroup.cs create mode 100644 Source/Tracking/WeightedTransaction.Test.cs create mode 100644 Source/Tracking/WeightedTransaction.cs delete mode 100644 Source/Tracking/WeightedWaitable.cs diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj index 6957b75..23bfbb8 100644 --- a/Nuclex.Support.csproj +++ b/Nuclex.Support.csproj @@ -118,6 +118,7 @@ CommandLineParser.cs + PathHelper.cs @@ -176,23 +177,44 @@ StringSegment.cs - - + + IdleStateEventArgs.cs + + + + ObservedWeightedTransaction.cs + + + + WeightedTransactionWrapperCollection.cs + + + ProgressReportEventArgs.cs + ProgressTracker.cs - - - WaitableGroup.cs + + StatusReportEventArgs.cs + + + Transaction.cs + + + + TransactionGroup.cs - - + + + + WeightedTransaction.cs + WeakReference.cs diff --git a/Source/Parsing/CommandLine.cs b/Source/Parsing/CommandLine.cs new file mode 100644 index 0000000..76df0d7 --- /dev/null +++ b/Source/Parsing/CommandLine.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Nuclex.Support.Source.Parsing { + +#if false + /// + /// Parses an application's command line parameters for easier consumption + /// + /// + /// + /// At the time of the creation of this parser, there are already several command line + /// parsing libraries out there. Most of them, however, do way too much at once or at + /// the very least use one huge, untested clutter of classes and methods to arrive + /// at their results. + /// + /// + /// This parser does nothing more than parse the command line arguments. It doesn't + /// interpret them and it doesn't check them for validity. Due to this, it can easily + /// be unit-tested and is an ideal building block to create actual command line + /// interpreters that connect the parameters to program instructions and or fill + /// structures in code. + /// + /// + public class CommandLine { + public static CommandLine Parse(string commandLine) {} + } + + public struct CommandLineOption { + + /// Contains the raw string the command line argument was parsed from + public string Raw; + /// Method used to specify the argument (either '-', '--' or '/') + public string Method; + /// Name of the command line argument + public string Name; + /// Value that has been assigned to the command line argument + public string Value; + /// Method used to assign the value (either '=', ':' or ' ') + public string Assignment; + + } +#endif + +} diff --git a/Source/Plugins/PluginRepository.Test.cs b/Source/Plugins/PluginRepository.Test.cs index 4de2c19..56910f1 100644 --- a/Source/Plugins/PluginRepository.Test.cs +++ b/Source/Plugins/PluginRepository.Test.cs @@ -77,9 +77,9 @@ namespace Nuclex.Support.Plugins { /// [Test] public void TestAddFilesWithOwnAssembly() { - Assembly self = Assembly.GetAssembly(GetType()); - PluginRepository testRepository = new PluginRepository(); + + Assembly self = Assembly.GetAssembly(GetType()); testRepository.AddFiles(self.Location); } diff --git a/Source/Scheduling/OperationQueue.Test.cs b/Source/Scheduling/OperationQueue.Test.cs index df1e49a..01a4a61 100644 --- a/Source/Scheduling/OperationQueue.Test.cs +++ b/Source/Scheduling/OperationQueue.Test.cs @@ -35,24 +35,24 @@ namespace Nuclex.Support.Scheduling { [TestFixture] public class OperationQueueTest { - #region interface IQueueOperationSubscriber + #region interface IOperationQueueSubscriber - /// Interface used to test the set waitable. - public interface IQueueOperationSubscriber { + /// Interface used to test the operation queue + public interface IOperationQueueSubscriber { - /// Called when the queue operations's progress changes - /// Queue operation whose progress has changed - /// Contains the new progress achieved - void ProgressChanged(object sender, ProgressReportEventArgs e); + /// Called when the operations queue's progress changes + /// Operation queue whose progress has changed + /// Contains the new progress achieved + void ProgressChanged(object sender, ProgressReportEventArgs arguments); - /// Called when the queue operation has ended - /// Queue operation that as ended - /// Not used - void Ended(object sender, EventArgs e); + /// Called when the operation queue has ended + /// Operation queue that as ended + /// Not used + void Ended(object sender, EventArgs arguments); } - #endregion // interface IQueueOperationSubscriber + #endregion // interface IOperationQueueSubscriber #region class ProgressUpdateEventArgsMatcher @@ -180,7 +180,7 @@ namespace Nuclex.Support.Scheduling { new TestOperation[] { operation1, operation2 } ); - IQueueOperationSubscriber mockedSubscriber = mockSubscriber(testQueueOperation); + IOperationQueueSubscriber mockedSubscriber = mockSubscriber(testQueueOperation); testQueueOperation.Start(); @@ -212,9 +212,9 @@ namespace Nuclex.Support.Scheduling { /// 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(); + private IOperationQueueSubscriber mockSubscriber(Operation operation) { + IOperationQueueSubscriber mockedSubscriber = + this.mockery.NewMock(); operation.AsyncEnded += new EventHandler(mockedSubscriber.Ended); (operation as IProgressReporter).AsyncProgressChanged += diff --git a/Source/Scheduling/OperationQueue.cs b/Source/Scheduling/OperationQueue.cs index bfd441c..816524b 100644 --- a/Source/Scheduling/OperationQueue.cs +++ b/Source/Scheduling/OperationQueue.cs @@ -42,23 +42,23 @@ namespace Nuclex.Support.Scheduling { /// public OperationQueue(IEnumerable childs) : this() { - // Construct a WeightedWaitable with the default weight for each - // waitable and wrap it in an ObservedWaitable + // Construct a WeightedTransaction with the default weight for each + // transaction and wrap it in an ObservedTransaction foreach(OperationType operation in childs) - this.children.Add(new WeightedWaitable(operation)); + this.children.Add(new WeightedTransaction(operation)); - // Since all waitables have a weight of 1.0, the total weight is - // equal to the number of waitables in our list + // Since all transactions have a weight of 1.0, the total weight is + // equal to the number of transactions in our list this.totalWeight = (float)this.children.Count; } /// Initializes a new queue operation with custom weights /// Child operations to execute in this operation - public OperationQueue(IEnumerable> childs) : this() { + public OperationQueue(IEnumerable> childs) : this() { - // Construct an ObservedWaitablen around each of the WeightedWaitables - foreach(WeightedWaitable operation in childs) { + // Construct an ObservedTransactionn around each of the WeightedTransactions + foreach(WeightedTransaction operation in childs) { this.children.Add(operation); // Sum up the total weight @@ -74,11 +74,11 @@ namespace Nuclex.Support.Scheduling { asyncOperationProgressChanged ); - this.children = new List>(); + this.children = new List>(); } /// Provides access to the child operations of this queue - public IList> Children { + public IList> Children { get { return this.children; } } @@ -99,7 +99,7 @@ namespace Nuclex.Support.Scheduling { /// Fires the progress update event /// Progress to report (ranging from 0.0 to 1.0) /// - /// Informs the observers of this waitable about the achieved progress. + /// Informs the observers of this transaction about the achieved progress. /// protected virtual void OnAsyncProgressChanged(float progress) { OnAsyncProgressChanged(new ProgressReportEventArgs(progress)); @@ -108,10 +108,10 @@ namespace Nuclex.Support.Scheduling { /// Fires the progress update event /// Progress to report (ranging from 0.0 to 1.0) /// - /// Informs the observers of this waitable about the achieved progress. - /// Allows for classes derived from the Waitable class to easily provide + /// Informs the observers of this transaction about the achieved progress. + /// Allows for classes derived from the transaction class to easily provide /// a custom event arguments class that has been derived from the - /// waitable's ProgressUpdateEventArgs class. + /// transaction's ProgressUpdateEventArgs class. /// protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) { EventHandler copy = AsyncProgressChanged; @@ -125,7 +125,7 @@ namespace Nuclex.Support.Scheduling { /// and launches the operation by calling its Begin() method. /// private void startCurrentOperation() { - OperationType operation = this.children[this.currentOperationIndex].Waitable; + OperationType operation = this.children[this.currentOperationIndex].Transaction; operation.AsyncEnded += this.asyncOperationEndedDelegate; @@ -143,7 +143,7 @@ namespace Nuclex.Support.Scheduling { /// counts up the accumulated progress of the queue. /// private void endCurrentOperation() { - OperationType operation = this.children[this.currentOperationIndex].Waitable; + OperationType operation = this.children[this.currentOperationIndex].Transaction; // Disconnect from the operation's events operation.AsyncEnded -= this.asyncOperationEndedDelegate; @@ -217,7 +217,7 @@ namespace Nuclex.Support.Scheduling { /// Delegate to the asyncOperationProgressUpdated() method private EventHandler asyncOperationProgressChangedDelegate; /// Operations being managed in the queue - private List> children; + private List> children; /// Summed weight of all operations in the queue private float totalWeight; /// Accumulated weight of the operations already completed diff --git a/Source/Tracking/IdleStateEventArgs.Test.cs b/Source/Tracking/IdleStateEventArgs.Test.cs new file mode 100644 index 0000000..7b673d4 --- /dev/null +++ b/Source/Tracking/IdleStateEventArgs.Test.cs @@ -0,0 +1,59 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.IO; + +#if UNITTEST + +using NUnit.Framework; +using NUnit.Framework.SyntaxHelpers; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the "idle state" event argument container + [TestFixture] + public class IdleStateEventArgsTest { + + /// + /// Tests whether the idle state event arguments correctly report a non-idle state + /// + [Test] + public void TestIdleStateChangedToFalse() { + IdleStateEventArgs idleStateFalse = new IdleStateEventArgs(false); + + Assert.IsFalse(idleStateFalse.Idle); + } + + /// + /// Tests whether the idle state event arguments correctly report an idle state + /// + [Test] + public void TestIdleStateChangedToTrue() { + IdleStateEventArgs idleStateFalse = new IdleStateEventArgs(true); + + Assert.IsTrue(idleStateFalse.Idle); + } + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/Internal/ObservedWeightedTransaction.Test.cs b/Source/Tracking/Internal/ObservedWeightedTransaction.Test.cs new file mode 100644 index 0000000..077f828 --- /dev/null +++ b/Source/Tracking/Internal/ObservedWeightedTransaction.Test.cs @@ -0,0 +1,158 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.IO; +using System.Threading; + +#if UNITTEST + +using NUnit.Framework; +using NUnit.Framework.SyntaxHelpers; +using NMock2; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the observation wrapper of weighted transactions + [TestFixture] + public class ObservedWeightedTransactionTest { + + #region interface IObservationSubscriber + + /// + /// Interface used to test the observation wrapper of weighted transactions + /// + public interface IObservationSubscriber { + + /// Will be invoked when an observed transaction's progress changes + void ProgressUpdated(); + + /// Will be invoked when an observed transaction completes + void Ended(); + + } + + #endregion // interface IObservationSubscriber + + #region class FunkyTransaction + + /// + /// Transaction that goes into the 'ended' state as soon as someone registers for + /// state change notifications + /// + private class FunkyTransaction : Transaction { + + /// Manages registrations to the AsyncEnded event + public override event EventHandler AsyncEnded { + add { + base.AsyncEnded += value; + + // To deterministically provoke an 'Ended' event just after registration we + // will switch the transaction into the 'ended' state right here + int oldValue = Interlocked.Exchange(ref this.alreadyEnded, 1); + if(oldValue != 1) { + OnAsyncEnded(); + } + } + remove { + base.AsyncEnded -= value; + } + } + + /// Whether the transaction has already been ended + private int alreadyEnded; + + } + + #endregion // class FunkyTransaction + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new Mockery(); + } + + /// Verifies that the constructor of the observation wrapper works + [Test] + public void TestConstructorWithAlreadyEndedTransaction() { + WeightedTransaction testTransaction = new WeightedTransaction( + Transaction.EndedDummy + ); + + IObservationSubscriber subscriber = this.mockery.NewMock(); + + Expect.AtLeast(0).On(subscriber).Method("ProgressUpdated"); + Expect.Once.On(subscriber).Method("Ended"); + + using( + ObservedWeightedTransaction test = + new ObservedWeightedTransaction( + testTransaction, + new ObservedWeightedTransaction.ReportDelegate( + subscriber.ProgressUpdated + ), + new ObservedWeightedTransaction.ReportDelegate( + subscriber.Ended + ) + ) + ) { + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + } + + /// + /// Verifies that the constructor of the observation wrapper can handle a transaction + /// entering the 'ended' state right on subscription + /// + [Test] + public void TestConstructorWithEndingTransaction() { + WeightedTransaction testTransaction = new WeightedTransaction( + new FunkyTransaction() + ); + + IObservationSubscriber subscriber = this.mockery.NewMock(); + + Expect.AtLeast(0).On(subscriber).Method("ProgressUpdated"); + Expect.Once.On(subscriber).Method("Ended"); + + using( + ObservedWeightedTransaction test = + new ObservedWeightedTransaction( + testTransaction, + new ObservedWeightedTransaction.ReportDelegate( + subscriber.ProgressUpdated + ), + new ObservedWeightedTransaction.ReportDelegate( + subscriber.Ended + ) + ) + ) { + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + } + + /// Mock object factory + private Mockery mockery; + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/Internal/ObservedWeightedTransaction.cs b/Source/Tracking/Internal/ObservedWeightedTransaction.cs new file mode 100644 index 0000000..32ae33f --- /dev/null +++ b/Source/Tracking/Internal/ObservedWeightedTransaction.cs @@ -0,0 +1,177 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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; + +namespace Nuclex.Support.Tracking { + + /// Transaction being observed by another object + /// + /// Type of the transaction that is being observed + /// + internal class ObservedWeightedTransaction : IDisposable + where TransactionType : Transaction { + + /// Delegate for reporting progress updates + public delegate void ReportDelegate(); + + /// Initializes a new observed transaction + /// Weighted transaction being observed + /// + /// Callback to invoke when the transaction's progress changes + /// + /// + /// Callback to invoke when the transaction has ended + /// + internal ObservedWeightedTransaction( + WeightedTransaction weightedTransaction, + ReportDelegate progressUpdateCallback, + ReportDelegate endedCallback + ) { + this.weightedTransaction = weightedTransaction; + + // See if this transaction has already ended (initial check for performance) + if(weightedTransaction.Transaction.Ended) { + + // Since we don't subscribe to the .Ended event (which would be fired immediately on + // subscription if the transaction was already finished), we will emulate this + // behavior here. There is no race condition here: The transition to .Ended occurs + // only once and will never happen in reverse. This is just a minor optimization to + // prevent object coupling where none is neccessary and to save some processing time. + this.progress = 1.0f; + progressUpdateCallback(); + endedCallback(); + + return; + + } + + this.endedCallback = endedCallback; + this.progressUpdateCallback = progressUpdateCallback; + + // This might trigger the event handler to be invoked right here if the transaction + // ended between our initial optimization attempt and this line. It's unlikely, + // however, so we'll not waste time with another optimization attempt. + this.weightedTransaction.Transaction.AsyncEnded += new EventHandler(asyncEnded); + + // See whether this transaction implements the IProgressReporter interface and if + // so, connect to its progress report event in order to pass these reports on + // to whomever created ourselfes. + this.progressReporter = this.weightedTransaction.Transaction as IProgressReporter; + if(this.progressReporter != null) { + this.asyncProgressChangedEventHandler = new EventHandler( + asyncProgressChanged + ); + this.progressReporter.AsyncProgressChanged += this.asyncProgressChangedEventHandler; + } + } + + /// Immediately releases all resources owned by the object + public void Dispose() { + asyncDisconnectEvents(); + } + + /// Weighted transaction being observed + public WeightedTransaction WeightedTransaction { + get { return this.weightedTransaction; } + } + + /// Amount of progress this transaction has achieved so far + public float Progress { + get { return this.progress; } + } + + /// Called when the observed transaction has ended + /// Transaction that has ended + /// Not used + private void asyncEnded(object sender, EventArgs e) { + ReportDelegate savedEndedCallback = this.endedCallback; + ReportDelegate savedProgressUpdateCallback = this.progressUpdateCallback; + + asyncDisconnectEvents(); // We don't need those anymore! + + // If the progress hasn't reached 1.0 yet, make a fake report so that even + // when a transaction doesn't report any progress at all, the set or queue + // owning us will have a percentage of transactions completed. + // + // There is the possibility of a race condition here, as a final progress + // report could have been generated by a thread running the transaction + // that was preempted by this thread. This would cause the progress to + // jump to 1.0 and then back to whatever the waiting thread will report. + if(this.progress != 1.0f) { + this.progress = 1.0f; + savedProgressUpdateCallback(); + } + + savedEndedCallback(); + } + + /// Called when the progress of the observed transaction changes + /// Transaction whose progress has changed + /// Contains the updated progress + private void asyncProgressChanged(object sender, ProgressReportEventArgs e) { + this.progress = e.Progress; + this.progressUpdateCallback(); + } + + /// Unsubscribes from all events of the observed transaction + private void asyncDisconnectEvents() { + + // 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.weightedTransaction.Transaction.AsyncEnded -= new EventHandler(asyncEnded); + + if(this.progressReporter != null) { + this.progressReporter.AsyncProgressChanged -= + this.asyncProgressChangedEventHandler; + + this.asyncProgressChangedEventHandler = null; + } + + this.endedCallback = null; + this.progressUpdateCallback = null; + } + } + + } // endedCallback != null + + } + + private EventHandler asyncProgressChangedEventHandler; + /// The observed transaction's progress reporting interface + private IProgressReporter progressReporter; + /// The weighted wable that is being observed + private WeightedTransaction weightedTransaction; + /// Callback to invoke when the progress updates + private volatile ReportDelegate progressUpdateCallback; + /// Callback to invoke when the transaction ends + private volatile ReportDelegate endedCallback; + /// Progress achieved so far + private volatile float progress; + } + +} // namespace Nuclex.Support.Tracking diff --git a/Source/Tracking/Internal/ObservedWeightedWaitable.cs b/Source/Tracking/Internal/ObservedWeightedWaitable.cs deleted file mode 100644 index 1fa6c76..0000000 --- a/Source/Tracking/Internal/ObservedWeightedWaitable.cs +++ /dev/null @@ -1,177 +0,0 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2008 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; - -namespace Nuclex.Support.Tracking { - - /// Waitable being observed by another object - /// - /// Type of the waitable that is being observed - /// - internal class ObservedWeightedWaitable : IDisposable - where WaitableType : Waitable { - - /// Delegate for reporting progress updates - public delegate void ReportDelegate(); - - /// Initializes a new observed waitable - /// Weighted waitable being observed - /// - /// Callback to invoke when the waitable's progress changes - /// - /// - /// Callback to invoke when the waitable has ended - /// - internal ObservedWeightedWaitable( - WeightedWaitable weightedWaitable, - ReportDelegate progressUpdateCallback, - ReportDelegate endedCallback - ) { - this.weightedWaitable = weightedWaitable; - - // See if this waitable has already ended (initial check for performance) - if(weightedWaitable.Waitable.Ended) { - - this.progress = 1.0f; - - } else { - - this.endedCallback = endedCallback; - this.progressUpdateCallback = progressUpdateCallback; - - this.weightedWaitable.Waitable.AsyncEnded += - new EventHandler(asyncEnded); - - // Check whether this waitable might have ended before we were able to - // attach ourselfes to its event. If so, don't bother registering to the - // other event and (important) set our progress to 1.0 because, since we - // might not have gotten the 'Ended' event, it might otherwise stay at 0.0 - // even though the waitable is in the 'Ended' state. - if(weightedWaitable.Waitable.Ended) { - this.progress = 1.0f; - } else { - this.progressReporter = this.weightedWaitable.Waitable as IProgressReporter; - - if(this.progressReporter != null) { - this.asyncProgressChangedEventHandler = new EventHandler( - asyncProgressChanged - ); - - this.progressReporter.AsyncProgressChanged += - this.asyncProgressChangedEventHandler; - } - } - - } - } - - /// Immediately releases all resources owned by the object - public void Dispose() { - asyncDisconnectEvents(); - } - - /// Weighted waitable being observed - public WeightedWaitable WeightedWaitable { - get { return this.weightedWaitable; } - } - - /// Amount of progress this waitable has achieved so far - public float Progress { - get { return this.progress; } - } - - /// Called when the observed waitable has ended - /// Waitable that has ended - /// Not used - private void asyncEnded(object sender, EventArgs e) { - ReportDelegate endedCallback = this.endedCallback; - ReportDelegate progressUpdateCallback = this.progressUpdateCallback; - - asyncDisconnectEvents(); // We don't need those anymore! - - // If the progress hasn't reached 1.0 yet, make a fake report so that even - // when a waitable doesn't report any progress at all, the set or queue - // owning us will have a percentage of waitables completed. - // - // There is the possibility of a race condition here, as a final progress - // report could have been generated by a thread running the waitable - // that was preempted by this thread. This would cause the progress to - // jump to 1.0 and then back to whatever the waiting thread will report. - if(this.progress != 1.0f) { - this.progress = 1.0f; - progressUpdateCallback(); - } - - endedCallback(); - } - - /// Called when the progress of the observed waitable changes - /// Waitable whose progress has changed - /// Contains the updated progress - private void asyncProgressChanged(object sender, ProgressReportEventArgs e) { - this.progress = e.Progress; - this.progressUpdateCallback(); - } - - /// Unsubscribes from all events of the observed waitable - private void asyncDisconnectEvents() { - - // 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.weightedWaitable.Waitable.AsyncEnded -= new EventHandler(asyncEnded); - - if(this.progressReporter != null) { - this.progressReporter.AsyncProgressChanged -= - this.asyncProgressChangedEventHandler; - - this.asyncProgressChangedEventHandler = null; - } - - this.endedCallback = null; - this.progressUpdateCallback = null; - } - } - - } // endedCallback != null - - } - - private EventHandler asyncProgressChangedEventHandler; - /// The observed waitable's progress reporting interface - private IProgressReporter progressReporter; - /// The weighted wable that is being observed - private WeightedWaitable weightedWaitable; - /// Callback to invoke when the progress updates - private volatile ReportDelegate progressUpdateCallback; - /// Callback to invoke when the waitable ends - private volatile ReportDelegate endedCallback; - /// Progress achieved so far - private volatile float progress; - } - -} // namespace Nuclex.Support.Tracking diff --git a/Source/Tracking/Internal/WeightedTransactionWrapperCollection.Test.cs b/Source/Tracking/Internal/WeightedTransactionWrapperCollection.Test.cs new file mode 100644 index 0000000..b87b7f1 --- /dev/null +++ b/Source/Tracking/Internal/WeightedTransactionWrapperCollection.Test.cs @@ -0,0 +1,70 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.IO; +using System.Threading; + +#if UNITTEST + +using NUnit.Framework; +using NUnit.Framework.SyntaxHelpers; +using NMock2; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the observation wrapper collection of weighted transactions + [TestFixture] + public class WeightedTransactionWrapperCollectionTest { + + /// + /// Tests whether the wrapper collection is handing out the unwrapped transactions + /// + [Test] + public void TestWrapperCollection() { + WeightedTransaction transaction = new WeightedTransaction( + Transaction.EndedDummy + ); + + ObservedWeightedTransaction observed = new ObservedWeightedTransaction( + transaction, + endedCallback, + progressUpdatedCallback + ); + + WeightedTransactionWrapperCollection wrapper = + new WeightedTransactionWrapperCollection( + new ObservedWeightedTransaction[] { observed } + ); + + Assert.AreSame(transaction, wrapper[0]); + } + + /// Dummy callback used as event subscriber in the tests + private void endedCallback() { } + + /// Dummy callback used as event subscriber in the tests + private void progressUpdatedCallback() { } + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/Internal/WeightedWaitableWrapperCollection.cs b/Source/Tracking/Internal/WeightedTransactionWrapperCollection.cs similarity index 50% rename from Source/Tracking/Internal/WeightedWaitableWrapperCollection.cs rename to Source/Tracking/Internal/WeightedTransactionWrapperCollection.cs index 8ffb4ef..0b90517 100644 --- a/Source/Tracking/Internal/WeightedWaitableWrapperCollection.cs +++ b/Source/Tracking/Internal/WeightedTransactionWrapperCollection.cs @@ -27,36 +27,36 @@ using Nuclex.Support.Collections; namespace Nuclex.Support.Tracking { - /// Collection of waitables with a weighting value - /// Type of waitables to manage + /// Collection of transactions with a weighting value + /// Type of transactions to manage /// /// /// This collection is exposed as a read-only collection to the user that - /// stores WeightedWaitables. Internally, it merely wraps a collection of - /// an internal type used to keep track of the individual waitable's - /// progress in the WaitableSet and OperationQueue classes. + /// stores WeightedTransactions. Internally, it merely wraps a collection of + /// an internal type used to keep track of the individual transaction's + /// progress in the TransactionGroup and OperationQueue classes. /// /// - /// It is read-only because the design requires a waitable to only ever - /// finish once. If it was possible eg. to add items after a WaitableSet - /// had signalled itself as being finished, it would be moved into an - /// unfinished state again. Also, an empty WaitableSet is, by definition, - /// finished (simply because there is no work to do) - unless the contents - /// of set are passed to the WaitableSet's constructor and never modified - /// at all, the design would be violated as soon as ab instance of the - /// WaitableSet or OperationQueue classes was created. + /// It is read-only because the design requires a transaction to only ever finish + /// once. If it was possible eg. to add items after a TransactionGroup had signalled + /// itself as being finished, it would be moved into an unfinished state again. + /// Also, an empty TransactionGroup is, by definition, finished (simply because + /// there is no work to do) - unless the contents of the group are passed to the + /// TransactionGroup's constructor and never modified at all, the design would be + /// violated as soon as an instance of the TransactionGroup or OperationQueue + /// classes was created. /// /// - internal class WeightedWaitableWrapperCollection : + internal class WeightedTransactionWrapperCollection : TransformingReadOnlyCollection< - ObservedWeightedWaitable, WeightedWaitable + ObservedWeightedTransaction, WeightedTransaction > - where WaitableType : Waitable { + where TransactionType : Transaction { - /// Initializes a new weighted waitable collection wrapper - /// Items to be exposed as weighted waitables - internal WeightedWaitableWrapperCollection( - IList> items + /// Initializes a new weighted transaction collection wrapper + /// Items to be exposed as weighted transactions + internal WeightedTransactionWrapperCollection( + IList> items ) : base(items) { } @@ -69,10 +69,10 @@ namespace Nuclex.Support.Tracking { /// be called frequently, because the TransformingReadOnlyCollection does /// not cache otherwise store the transformed items. /// - protected override WeightedWaitable Transform( - ObservedWeightedWaitable item + protected override WeightedTransaction Transform( + ObservedWeightedTransaction item ) { - return item.WeightedWaitable; + return item.WeightedTransaction; } } diff --git a/Source/Tracking/ProgressReportEventArgs.Test.cs b/Source/Tracking/ProgressReportEventArgs.Test.cs new file mode 100644 index 0000000..84cdc9e --- /dev/null +++ b/Source/Tracking/ProgressReportEventArgs.Test.cs @@ -0,0 +1,59 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.IO; + +#if UNITTEST + +using NUnit.Framework; +using NUnit.Framework.SyntaxHelpers; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the progress report event argument container + [TestFixture] + public class ProgressReportEventArgsTest { + + /// + /// Tests whether the progress report event arguments correctly report zero progress + /// + [Test] + public void TestZeroProgress() { + ProgressReportEventArgs zeroProgress = new ProgressReportEventArgs(0.0f); + + Assert.AreEqual(0.0f, zeroProgress.Progress); + } + + /// + /// Tests whether the progress report event arguments correctly report complete progress + /// + [Test] + public void TestCompleteProgress() { + ProgressReportEventArgs zeroProgress = new ProgressReportEventArgs(1.0f); + + Assert.AreEqual(1.0f, zeroProgress.Progress); + } + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/ProgressTracker.Test.cs b/Source/Tracking/ProgressTracker.Test.cs index 248b125..c778ebc 100644 --- a/Source/Tracking/ProgressTracker.Test.cs +++ b/Source/Tracking/ProgressTracker.Test.cs @@ -91,21 +91,21 @@ namespace Nuclex.Support.Tracking { #endregion // class ProgressUpdateEventArgsMatcher - #region class TestWaitable + #region class TestTransaction - /// Waitable used for testing in this unit test - private class TestWaitable : Waitable, IProgressReporter { + /// Transaction used for testing in this unit test + private class TestTransaction : Transaction, IProgressReporter { /// will be triggered to report when progress has been achieved public event EventHandler AsyncProgressChanged; - /// Changes the testing waitable's indicated progress - /// New progress to be reported by the testing waitable + /// Changes the testing transaction's indicated progress + /// New progress to be reported by the testing transaction public void ChangeProgress(float progress) { OnAsyncProgressChanged(progress); } - /// Transitions the waitable into the ended state + /// Transitions the transaction into the ended state public void End() { OnAsyncEnded(); } @@ -113,7 +113,7 @@ namespace Nuclex.Support.Tracking { /// Fires the progress update event /// Progress to report (ranging from 0.0 to 1.0) /// - /// Informs the observers of this waitable about the achieved progress. + /// Informs the observers of this transaction about the achieved progress. /// protected virtual void OnAsyncProgressChanged(float progress) { OnAsyncProgressChanged(new ProgressReportEventArgs(progress)); @@ -122,10 +122,10 @@ namespace Nuclex.Support.Tracking { /// Fires the progress update event /// Progress to report (ranging from 0.0 to 1.0) /// - /// Informs the observers of this waitable about the achieved progress. - /// Allows for classes derived from the Waitable class to easily provide + /// Informs the observers of this transaction about the achieved progress. + /// Allows for classes derived from the transaction class to easily provide /// a custom event arguments class that has been derived from the - /// waitable's ProgressUpdateEventArgs class. + /// transaction's ProgressUpdateEventArgs class. /// protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) { EventHandler copy = AsyncProgressChanged; @@ -146,204 +146,238 @@ namespace Nuclex.Support.Tracking { /// Validates that the tracker properly sums the progress [Test] public void TestSummedProgress() { - ProgressTracker tracker = new ProgressTracker(); + using(ProgressTracker tracker = new ProgressTracker()) { + IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); - IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); + TestTransaction test1 = new TestTransaction(); + TestTransaction test2 = new TestTransaction(); - Expect.Once.On(mockedSubscriber). - Method("IdleStateChanged"). - WithAnyArguments(); + // Step 1 + { + Expect.Once.On(mockedSubscriber). + Method("IdleStateChanged"). + WithAnyArguments(); - Expect.Exactly(2).On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f)) - } - ); + // Since the progress is already at 0, these redundant reports are optional + Expect.Between(0, 2).On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f)) + } + ); - TestWaitable test1 = new TestWaitable(); - tracker.Track(test1); - TestWaitable test2 = new TestWaitable(); - tracker.Track(test2); + tracker.Track(test1); + tracker.Track(test2); + } - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.25f)) - } - ); + // Step 2 + { + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.25f)) + } + ); - test1.ChangeProgress(0.5f); + test1.ChangeProgress(0.5f); + } + } this.mockery.VerifyAllExpectationsHaveBeenMet(); } /// - /// Validates that the tracker only removes waitables when the whole + /// Validates that the tracker only removes transactions when the whole /// tracking list has reached the 'ended' state. /// /// - /// If the tracker would remove ended waitables right when they finished, + /// If the tracker would remove ended transactions right when they finished, /// the total progress would jump back each time. This is unwanted, of course. /// [Test] public void TestDelayedRemoval() { - ProgressTracker tracker = new ProgressTracker(); + using(ProgressTracker tracker = new ProgressTracker()) { + IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); - IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); + TestTransaction test1 = new TestTransaction(); + TestTransaction test2 = new TestTransaction(); - Expect.Once.On(mockedSubscriber). - Method("IdleStateChanged"). - WithAnyArguments(); + // Step 1 + { + Expect.Once.On(mockedSubscriber). + Method("IdleStateChanged"). + WithAnyArguments(); - Expect.Exactly(2).On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f)) - } - ); + // This is optional. The tracker's progress is currently 0, so there's no need + // to send out superfluous progress reports. + Expect.Between(0, 2).On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f)) + } + ); - TestWaitable test1 = new TestWaitable(); - tracker.Track(test1); - TestWaitable test2 = new TestWaitable(); - tracker.Track(test2); + tracker.Track(test1); + tracker.Track(test2); + } - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.25f)) - } - ); + // Step 2 + { + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.25f)) + } + ); - test1.ChangeProgress(0.5f); + // Total progress should be 0.25 after this call (two transactions, one with + // 0% progress and one with 50% progress) + test1.ChangeProgress(0.5f); + } - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.75f)) - } - ); + // Step 3 + { + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.75f)) + } + ); - // Total progress should be 0.75 after this call (one waitable at 1.0, - // the other one at 0.5). If the second waitable would be removed, - // the progress would jump to 0.5 instead. - test2.End(); + // Total progress should be 0.75 after this call (one transaction at 100%, + // the other one at 50%). If the second transaction would be removed by the tracker, + // (which would be inappropriate!) the progress would falsely jump to 0.5 instead. + test2.End(); + } - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(1.0f)) - } - ); + // Step 4 + { + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(1.0f)) + } + ); - Expect.Once.On(mockedSubscriber). - Method("IdleStateChanged"). - WithAnyArguments(); + Expect.Once.On(mockedSubscriber). + Method("IdleStateChanged"). + WithAnyArguments(); - test1.End(); + test1.End(); + } + } this.mockery.VerifyAllExpectationsHaveBeenMet(); } /// - /// Validates that the tracker behaves correctly if it is fed with waitables + /// Validates that the tracker behaves correctly if it is fed with transactions /// that have already ended. /// [Test] - public void TestSoleEndedWaitable() { - ProgressTracker tracker = new ProgressTracker(); + public void TestSoleEndedTransaction() { + using(ProgressTracker tracker = new ProgressTracker()) { + IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); - IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); - - tracker.Track(Waitable.EndedDummy); + tracker.Track(Transaction.EndedDummy); + } this.mockery.VerifyAllExpectationsHaveBeenMet(); } /// - /// Validates that the tracker behaves correctly if it is fed with waitables - /// that have already ended in addition to waitables that are actively executing. + /// Validates that the tracker behaves correctly if it is fed with transactions + /// that have already ended in addition to transactions that are actively executing. /// [Test] - public void TestEndedWaitable() { - ProgressTracker tracker = new ProgressTracker(); + public void TestEndedTransaction() { + using(ProgressTracker tracker = new ProgressTracker()) { + IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); - IProgressTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); + TestTransaction test1 = new TestTransaction(); - Expect.Once.On(mockedSubscriber). - Method("IdleStateChanged"). - WithAnyArguments(); + // Step 1 + { + Expect.Once.On(mockedSubscriber). + Method("IdleStateChanged"). + WithAnyArguments(); - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f)) - } - ); + Expect.Between(0, 1).On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f)) + } + ); - TestWaitable test1 = new TestWaitable(); - tracker.Track(test1); + tracker.Track(test1); + } - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.5f)) - } - ); + // Step 2 + { + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.5f)) + } + ); - tracker.Track(Waitable.EndedDummy); + tracker.Track(Transaction.EndedDummy); + } - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), - new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(1.0f)) - } - ); + // Step 3 + { + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), + new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(1.0f)) + } + ); - Expect.Once.On(mockedSubscriber). - Method("IdleStateChanged"). - WithAnyArguments(); + Expect.Once.On(mockedSubscriber). + Method("IdleStateChanged"). + WithAnyArguments(); - test1.End(); + test1.End(); + } + } this.mockery.VerifyAllExpectationsHaveBeenMet(); } /// - /// Tries to provoke a deadlock by re-entering the tracker from one of - /// its own events. + /// Tries to provoke a deadlock by re-entering the tracker from one of its own events /// [Test] public void TestProvokedDeadlock() { - ProgressTracker tracker = new ProgressTracker(); + using(ProgressTracker tracker = new ProgressTracker()) { + TestTransaction test1 = new TestTransaction(); + tracker.Track(test1); - TestWaitable test1 = new TestWaitable(); - tracker.Track(test1); + tracker.AsyncIdleStateChanged += + (EventHandler)delegate(object sender, IdleStateEventArgs arguments) { + tracker.Track(Transaction.EndedDummy); + }; - tracker.AsyncIdleStateChanged += - (EventHandler)delegate(object sender, IdleStateEventArgs arguments) { - tracker.Track(Waitable.EndedDummy); - }; - - test1.End(); + test1.End(); + } } /// Mocks a subscriber for the events of a tracker diff --git a/Source/Tracking/ProgressTracker.cs b/Source/Tracking/ProgressTracker.cs index b28a917..5e8a025 100644 --- a/Source/Tracking/ProgressTracker.cs +++ b/Source/Tracking/ProgressTracker.cs @@ -25,208 +25,195 @@ using System.Threading; namespace Nuclex.Support.Tracking { /// - /// Helps tracking the progress of one or more waitable background processes + /// Helps tracking the progress of one or more background transactions /// /// /// /// This is useful if you want to display a progress bar for multiple - /// Waitables but can not guarantee that no additional Waitables + /// transactions but can not guarantee that no additional transactions /// will appear inmidst of execution. /// /// - /// This class does not implement the interface itself - /// in order to not violate the design principles of Waitables which - /// guarantee that a will only finish once (whereas the + /// This class does not implement the interface itself + /// in order to not violate the design principles of transactions which + /// guarantee that a will only finish once (whereas the /// progress tracker might 'finish' any number of times). /// /// public class ProgressTracker : IDisposable, IProgressReporter { - #region class WaitableMatcher + #region class TransactionMatcher - /// Matches a direct Waitable to a fully wrapped one - private class WaitableMatcher { + /// Matches a direct transaction to a fully wrapped one + private class TransactionMatcher { /// - /// Initializes a new Waitable matcher that matches against - /// the specified Waitable + /// Initializes a new transaction matcher that matches against + /// the specified transaction /// - /// Waitable to match against - public WaitableMatcher(Waitable toMatch) { + /// Transaction to match against + public TransactionMatcher(Transaction toMatch) { this.toMatch = toMatch; } /// - /// Checks whether the provided Waitable matches the comparison - /// Waitable of the instance + /// Checks whether the provided transaction matches the comparison + /// transaction of the instance /// - /// Waitable to match to the comparison Waitable - public bool Matches(ObservedWeightedWaitable other) { - return ReferenceEquals(other.WeightedWaitable.Waitable, this.toMatch); + /// Transaction to match to the comparison transaction + public bool Matches(ObservedWeightedTransaction other) { + return ReferenceEquals(other.WeightedTransaction.Transaction, this.toMatch); } - /// Waitable this instance compares against - private Waitable toMatch; + /// Transaction this instance compares against + private Transaction toMatch; } - #endregion // class WaitableMatcher + #endregion // class TransactionMatcher /// Triggered when the idle state of the tracker changes /// - /// The tracker is idle when no Waitables are being tracked in it. If you're + /// The tracker is idle when no transactions are being tracked in it. If you're /// using this class to feed a progress bar, this would be the event to use for /// showing or hiding the progress bar. The tracker starts off as idle because, - /// upon construction, its list of Waitables will be empty. + /// upon construction, its list of transactions will be empty. /// public event EventHandler AsyncIdleStateChanged; /// Triggered when the total progress has changed public event EventHandler AsyncProgressChanged; - /// Initializes a new Waitable tracker + /// Initializes a new transaction tracker public ProgressTracker() { - this.trackedWaitables = new List>(); + this.trackedTransactions = new List>(); this.idle = true; this.asyncEndedDelegate = - new ObservedWeightedWaitable.ReportDelegate(asyncEnded); + new ObservedWeightedTransaction.ReportDelegate(asyncEnded); this.asyncProgressUpdatedDelegate = - new ObservedWeightedWaitable.ReportDelegate(asyncProgressChanged); + new ObservedWeightedTransaction.ReportDelegate(asyncProgressChanged); } /// Immediately releases all resources owned by the instance public void Dispose() { - lock(this.trackedWaitables) { + lock(this.trackedTransactions) { - // Get rid of all Waitables we're tracking. This unsubscribes the - // observers from the events of the Waitables and stops us from + // Get rid of all transactions we're tracking. This unsubscribes the + // observers from the events of the transactions and stops us from // being kept alive and receiving any further events if some of the - // tracked Waitables are still executing. - for(int index = 0; index < this.trackedWaitables.Count; ++index) - this.trackedWaitables[index].Dispose(); + // tracked transactions are still executing. + for(int index = 0; index < this.trackedTransactions.Count; ++index) + this.trackedTransactions[index].Dispose(); // Help the GC a bit by untangling the references :) - this.trackedWaitables.Clear(); - this.trackedWaitables = null; + this.trackedTransactions.Clear(); + this.trackedTransactions = null; } // lock } - /// Begins tracking the specified waitable background process - /// Waitable background process to be tracked - public void Track(Waitable waitable) { - Track(waitable, 1.0f); + /// Begins tracking the specified background transactions + /// Background transaction to be tracked + public void Track(Transaction transaction) { + Track(transaction, 1.0f); } - /// Begins tracking the specified waitable background process - /// Waitable background process to be tracked - /// Weight to assign to this waitable background process - public void Track(Waitable waitable, float weight) { + /// Begins tracking the specified background transaction + /// Background transaction to be tracked + /// Weight to assign to this background transaction + public void Track(Transaction transaction, float weight) { - // Add the new Waitable into the tracking list. This has to be done + // Add the new transaction into the tracking list. This has to be done // inside a lock to prevent issues with the progressUpdate callback, which could // access the totalWeight field before it has been updated to reflect the - // new Waitable added to the collection. - lock(this.trackedWaitables) { + // new transaction added to the collection. + lock(this.trackedTransactions) { - bool wasEmpty = (this.trackedWaitables.Count == 0); + bool wasEmpty = (this.trackedTransactions.Count == 0); + + if(transaction.Ended) { + + // If the ended transaction would become the only transaction in the list, + // there's no sense in doing anything at all because it would have to be + // thrown right out again. Only add the transaction when there are other + // running transactions to properly sum total progress for consistency. + if(!wasEmpty) { + + // Construct a new observation wrapper. This is done inside the lock + // because as soon as we are subscribed to the events, we can potentially + // receive them. The lock eliminates the risk of processing a progress update + // before the transaction has been added to the tracked transactions list. + this.trackedTransactions.Add( + new ObservedWeightedTransaction( + new WeightedTransaction(transaction, weight), + this.asyncProgressUpdatedDelegate, + this.asyncEndedDelegate + ) + ); + + } + + } else { // Not ended -- Transation is still running + + // Construct a new transation observer and add the transaction to our + // list of tracked transactions. + ObservedWeightedTransaction observedTransaction = + new ObservedWeightedTransaction( + new WeightedTransaction(transaction, weight), + this.asyncProgressUpdatedDelegate, + this.asyncEndedDelegate + ); + + this.trackedTransactions.Add(observedTransaction); + + // If this is the first transaction to be added to the list, tell our + // owner that we're idle no longer! + if(wasEmpty) + setIdle(false); + + } // if transaction ended // This can be done after we registered the wrapper to our delegates because // any incoming progress updates will be stopped from the danger of a // division-by-zero from the potentially still zeroed totalWeight by the lock. this.totalWeight += weight; - if(waitable.Ended) { - - // If the ended Waitable would become the only Waitable in the list, - // there's no sense in doing anything at all because it would have to be - // thrown right out again. Only add the Waitable when there are other - // running Waitables to properly sum total progress for consistency. - if(!wasEmpty) { - - // Construct a new observation wrapper. This is done inside the lock - // because as soon as we are subscribed to the events, we can potentially - // receive them. The lock eliminates the risk of processing a progress update - // before the Waitable has been added to the tracked Waitables list. - this.trackedWaitables.Add( - new ObservedWeightedWaitable( - new WeightedWaitable(waitable, weight), - this.asyncProgressUpdatedDelegate, - this.asyncEndedDelegate - ) - ); - - // All done, the total progress is different now, so force a recalculation and - // send out the AsyncProgressUpdated event. - recalculateProgress(); - - } - - } else { // Not ended -- Waitable is still running - - // Construct a new Waitable observer and add the Waitable to our - // list of tracked Waitables. - ObservedWeightedWaitable observedWaitable = - new ObservedWeightedWaitable( - new WeightedWaitable(waitable, weight), - this.asyncProgressUpdatedDelegate, - this.asyncEndedDelegate - ); - - this.trackedWaitables.Add(observedWaitable); - - // If this is the first Waitable to be added to the list, tell our - // owner that we're idle no longer! - if(wasEmpty) - setIdle(false); - - // All done, the total progress is different now, so force a recalculation and - // send out the AsyncProgressUpdated event. - recalculateProgress(); - - // The Waitable might have ended before we had registered to its AsyncEnded - // event, so we have to do this to be on the safe side. This might cause - // asyncEnded() to be called twice, but that's not a problem in this - // implementation and improves performance and simplicity for the normal path. - if(waitable.Ended) { - asyncEnded(); - observedWaitable.Dispose(); - } - - } // if Waitable ended + // All done, the total progress is different now, so force a recalculation and + // send out the AsyncProgressUpdated event. + recalculateProgress(); } // lock } - /// Stops tracking the specified waitable background process - /// Waitable background process to stop tracking of - public void Untrack(Waitable waitable) { - lock(this.trackedWaitables) { + /// Stops tracking the specified background transaction + /// Background transaction to stop tracking of + public void Untrack(Transaction transaction) { + lock(this.trackedTransactions) { // Locate the object to be untracked in our collection - int removeIndex = this.trackedWaitables.FindIndex( - new Predicate>( - new WaitableMatcher(waitable).Matches + int removeIndex = this.trackedTransactions.FindIndex( + new Predicate>( + new TransactionMatcher(transaction).Matches ) ); if(removeIndex == -1) throw new InvalidOperationException("Item is not being tracked"); - // Remove and dispose the Waitable the user wants to untrack + // Remove and dispose the transaction the user wants to untrack { - ObservedWeightedWaitable wrappedWaitable = - this.trackedWaitables[removeIndex]; + ObservedWeightedTransaction wrappedTransaction = + this.trackedTransactions[removeIndex]; - this.trackedWaitables.RemoveAt(removeIndex); - wrappedWaitable.Dispose(); + this.trackedTransactions.RemoveAt(removeIndex); + wrappedTransaction.Dispose(); } // If the list is empty, then we're back in the idle state - if(this.trackedWaitables.Count == 0) { + if(this.trackedTransactions.Count == 0) { this.totalWeight = 0.0f; @@ -235,12 +222,12 @@ namespace Nuclex.Support.Tracking { } else { - // Rebuild the total weight from scratch. Subtracting the removed Waitable's + // 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; - for(int index = 0; index < this.trackedWaitables.Count; ++index) - this.totalWeight += this.trackedWaitables[index].WeightedWaitable.Weight; + for(int index = 0; index < this.trackedTransactions.Count; ++index) + this.totalWeight += this.trackedTransactions[index].WeightedTransaction.Weight; } @@ -252,7 +239,7 @@ namespace Nuclex.Support.Tracking { get { return this.idle; } } - /// Current summed progress of the tracked Waitables + /// Current summed progress of the tracked transactions public float Progress { get { return this.progress; } } @@ -278,59 +265,61 @@ namespace Nuclex.Support.Tracking { float totalProgress = 0.0f; // Lock the collection to avoid trouble when someone tries to remove one - // of our tracked Waitables while we're just doing a progress update - lock(this.trackedWaitables) { + // of our tracked transactions while we're just doing a progress update + lock(this.trackedTransactions) { - // This is a safety measure. In theory, even after all Waitables have - // ended and the collection of tracked Waitables is cleared, a waiting + // This is a safety measure. In theory, even after all transactions have + // 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; // Sum up the total progress - for(int index = 0; index < this.trackedWaitables.Count; ++index) { - float weight = this.trackedWaitables[index].WeightedWaitable.Weight; - totalProgress += this.trackedWaitables[index].Progress * weight; + 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 Waitables we just summed -- by design, + // 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; - // Finally, trigger the event - this.progress = totalProgress; - OnAsyncProgressUpdated(totalProgress); + // Finally, trigger the event if the progress has changed + if(totalProgress != this.progress) { + this.progress = totalProgress; + OnAsyncProgressUpdated(totalProgress); + } } // lock } - /// Called when one of the tracked Waitables has ended + /// Called when one of the tracked transactions has ended private void asyncEnded() { - lock(this.trackedWaitables) { + lock(this.trackedTransactions) { - // If any Waitables in the list are still going, keep the entire list. + // If any transactions in the list are still going, keep the entire list. // This behavior is intentional in order to prevent the tracker's progress from - // jumping back repeatedly when multiple tracked Waitables come to an end. - for(int index = 0; index < this.trackedWaitables.Count; ++index) - if(!this.trackedWaitables[index].WeightedWaitable.Waitable.Ended) + // jumping back repeatedly when multiple tracked transactions come to an end. + for(int index = 0; index < this.trackedTransactions.Count; ++index) + if(!this.trackedTransactions[index].WeightedTransaction.Transaction.Ended) return; - // All Waitables have finished, get rid of the wrappers and make a - // fresh start for future Waitables to be tracked. No need to call - // Dispose() since, as a matter of fact, when the Waitable - this.trackedWaitables.Clear(); + // All transactions have finished, get rid of the wrappers and make a + // fresh start for future transactions to be tracked. No need to call + // Dispose() since, as a matter of fact, when the transaction + this.trackedTransactions.Clear(); this.totalWeight = 0.0f; // Notify our owner that we're idle now. This line is only reached when all - // Waitables were finished, so it's safe to trigger this here. + // transactions were finished, so it's safe to trigger this here. setIdle(true); } // lock } - /// Called when one of the tracked Waitables has achieved progress + /// Called when one of the tracked transactions has achieved progress private void asyncProgressChanged() { recalculateProgress(); } @@ -338,7 +327,7 @@ namespace Nuclex.Support.Tracking { /// Changes the idle state /// Whether or not the tracker is currently idle /// - /// This method expects to be called during a lock() on trackedWaitables! + /// This method expects to be called during a lock() on trackedTransactions! /// private void setIdle(bool idle) { this.idle = idle; @@ -348,16 +337,16 @@ namespace Nuclex.Support.Tracking { /// Whether the tracker is currently idle private volatile bool idle; - /// Current summed progress of the tracked Waitables + /// Current summed progress of the tracked transactions private volatile float progress; - /// Total weight of all Waitables being tracked + /// Total weight of all transactions being tracked private volatile float totalWeight; - /// Waitables being tracked by this tracker - private List> trackedWaitables; + /// Transactions being tracked by this tracker + private List> trackedTransactions; /// Delegate for the asyncEnded() method - private ObservedWeightedWaitable.ReportDelegate asyncEndedDelegate; + private ObservedWeightedTransaction.ReportDelegate asyncEndedDelegate; /// Delegate for the asyncProgressUpdated() method - private ObservedWeightedWaitable.ReportDelegate asyncProgressUpdatedDelegate; + private ObservedWeightedTransaction.ReportDelegate asyncProgressUpdatedDelegate; } diff --git a/Source/Tracking/Request.cs b/Source/Tracking/Request.cs index d3380eb..c7be01e 100644 --- a/Source/Tracking/Request.cs +++ b/Source/Tracking/Request.cs @@ -33,11 +33,11 @@ namespace Nuclex.Support.Tracking { /// store a new exception) and re-throw them when in ReraiseExceptions() /// /// - /// Like in the Waitable class, the contract requires you to always call + /// Like in the transaction class, the contract requires you to always call /// OnAsyncEnded(), no matter what the outcome of your operation is. /// /// - public abstract class Request : Waitable { + public abstract class Request : Transaction { #region class EndedDummyRequest diff --git a/Source/Tracking/StatusReportEventArgs.Test.cs b/Source/Tracking/StatusReportEventArgs.Test.cs new file mode 100644 index 0000000..57b55a6 --- /dev/null +++ b/Source/Tracking/StatusReportEventArgs.Test.cs @@ -0,0 +1,60 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.IO; + +#if UNITTEST + +using NUnit.Framework; +using NUnit.Framework.SyntaxHelpers; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the status report event argument container + [TestFixture] + public class StatusReportEventArgsTest { + + /// + /// Tests whether the status report event arguments correctly reports an empty status + /// + [Test] + public void TestEmptyStatus() { + StatusReportEventArgs emptyStatus = new StatusReportEventArgs(string.Empty); + + Assert.AreEqual(string.Empty, emptyStatus.Status); + } + + /// + /// Tests whether the status report event arguments correctly reports simple + /// status indications + /// + [Test] + public void TestSimpleStatus() { + StatusReportEventArgs emptyStatus = new StatusReportEventArgs("hello world"); + + Assert.AreEqual("hello world", emptyStatus.Status); + } + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/Transaction.Test.cs b/Source/Tracking/Transaction.Test.cs new file mode 100644 index 0000000..35e7515 --- /dev/null +++ b/Source/Tracking/Transaction.Test.cs @@ -0,0 +1,189 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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; +using System.Threading; + +#if UNITTEST + +using NUnit.Framework; +using NMock2; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the transaction class + [TestFixture] + public class TransactionTest { + + #region interface ITransactionSubscriber + + /// Interface used to test the transaction + public interface ITransactionSubscriber { + + /// Called when the set transaction has ended + /// Transaction group that as ended + /// Not used + void Ended(object sender, EventArgs arguments); + + } + + #endregion // interface ITransactionGroupSubscriber + + #region class TestTransaction + + /// Transaction used for testing in this unit test + private class TestTransaction : Transaction { + + /// Transitions the transaction into the ended state + public void End() { + OnAsyncEnded(); + } + + } + + #endregion // class TestWiatable + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new Mockery(); + } + + /// + /// Verifies that the transaction throws an exception when it is ended multiple times + /// + [Test, ExpectedException(typeof(InvalidOperationException))] + public void TestThrowOnRepeatedlyEndedTransaction() { + TestTransaction test = new TestTransaction(); + test.End(); + test.End(); + } + + /// + /// Tests whether the Ended event of the transaction is correctly delivered if + /// the transaction ends after the subscription already took place + /// + [Test] + public void TestEndedEventAfterSubscription() { + TestTransaction test = new TestTransaction(); + + ITransactionSubscriber mockedSubscriber = mockSubscriber(test); + Expect.Once.On(mockedSubscriber). + Method("Ended"). + WithAnyArguments(); + + test.End(); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// + /// Tests whether the Ended event of the transaction is correctly delivered if + /// the transaction is already done when the subscription takes place + /// + [Test] + public void TestEndedEventDuingSubscription() { + TestTransaction test = new TestTransaction(); + test.End(); + + ITransactionSubscriber mockedSubscriber = + this.mockery.NewMock(); + + Expect.Once.On(mockedSubscriber). + Method("Ended"). + WithAnyArguments(); + + test.AsyncEnded += new EventHandler(mockedSubscriber.Ended); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// + /// Verifies that the Wait() method of the transaction works as expected + /// + [Test] + public void TestWaitUnlimited() { + TestTransaction test = new TestTransaction(); + + // We can only do a positive test here without slowing down the unit test + test.End(); + test.Wait(); + } + + /// + /// Verifies that the Wait() method of the transaction works as expected using + /// a millisecond count as its argument + /// + [Test] + public void TestWaitMilliseconds() { + TestTransaction test = new TestTransaction(); + + // Wait 0 milliseconds for the transaction to end. Of course, this will not happen, + // so a timeout occurs and false is returned + Assert.IsFalse(test.Wait(0)); + + test.End(); + + // Wait another 0 milliseconds for the transaction to end. Now it has already ended + // and no timeout will occur, even with a wait time of 0 milliseconds. + Assert.IsTrue(test.Wait(0)); + } + + /// + /// Verifies that the Wait() method of the transaction works as expected using + /// a TimeSpan as its argument + /// + [Test] + public void TestWaitTimeSpan() { + TestTransaction test = new TestTransaction(); + + // Wait 0 milliseconds for the transaction to end. Of course, this will not happen, + // so a timeout occurs and false is returned + Assert.IsFalse(test.Wait(TimeSpan.Zero)); + + test.End(); + + // Wait another 0 milliseconds for the transaction to end. Now it has already ended + // and no timeout will occur, even with a wait time of 0 milliseconds. + Assert.IsTrue(test.Wait(TimeSpan.Zero)); + } + + /// Mocks a subscriber for the events of a transaction + /// Transaction to mock an event subscriber for + /// The mocked event subscriber + private ITransactionSubscriber mockSubscriber(Transaction transaction) { + ITransactionSubscriber mockedSubscriber = + this.mockery.NewMock(); + + transaction.AsyncEnded += new EventHandler(mockedSubscriber.Ended); + + return mockedSubscriber; + } + + /// Mock object factory + private Mockery mockery; + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/Waitable.cs b/Source/Tracking/Transaction.cs similarity index 83% rename from Source/Tracking/Waitable.cs rename to Source/Tracking/Transaction.cs index a981523..f5d5992 100644 --- a/Source/Tracking/Waitable.cs +++ b/Source/Tracking/Transaction.cs @@ -28,46 +28,46 @@ namespace Nuclex.Support.Tracking { /// /// /// By encapsulating long-running operations which will ideally be running in - /// a background thread in a class that's derived from + /// a background thread in a class that's derived from /// you can wait for the completion of the operation and optionally even receive /// feedback on the achieved progress. This is useful for displaying a progress /// bar, loading screen or some other means of entertaining the user while he /// waits for the task to complete. /// /// - /// You can register callbacks which will be fired once the + /// You can register callbacks which will be fired once the /// task has completed. This class deliberately does not provide an Execute() /// method or anything similar to clearly seperate the initiation of an operation /// from just monitoring it. By omitting an Execute() method, it also becomes - /// possible to construct a Waitable just-in-time when it is explicitely being + /// possible to construct a transaction just-in-time when it is explicitely being /// asked for. /// /// - public abstract class Waitable { + public abstract class Transaction { - #region class EndedDummyWaitable + #region class EndedDummyTransaction - /// Dummy waitable which always is in the 'ended' state - private class EndedDummyWaitable : Waitable { + /// Dummy transaction which always is in the 'ended' state + private class EndedDummyTransaction : Transaction { - /// Initializes a new ended dummy waitable - public EndedDummyWaitable() { + /// Initializes a new ended dummy transaction + public EndedDummyTransaction() { OnAsyncEnded(); } } - #endregion // class EndedDummyWaitable + #endregion // class EndedDummyTransaction - /// A dummy waitable that's always in the 'ended' state + /// A dummy transaction that's always in the 'ended' state /// /// Useful if an operation is already complete when it's being asked for or - /// when a waitable that's lazily created is accessed after the original + /// when a transaction that's lazily created is accessed after the original /// operation has ended already. /// - public static readonly Waitable EndedDummy = new EndedDummyWaitable(); + public static readonly Transaction EndedDummy = new EndedDummyTransaction(); - /// Will be triggered when the Waitable has ended + /// Will be triggered when the transaction has ended /// /// If the process is already finished when a client registers to this event, /// the registered callback will be invoked synchronously right when the @@ -149,12 +149,12 @@ namespace Nuclex.Support.Tracking { return WaitHandle.WaitOne(timeoutMilliseconds, false); } - /// Whether the Waitable has ended already + /// Whether the transaction has ended already public virtual bool Ended { get { return this.ended; } } - /// WaitHandle that can be used to wait for the Waitable to end + /// WaitHandle that can be used to wait for the transaction to end public virtual WaitHandle WaitHandle { get { @@ -186,14 +186,14 @@ namespace Nuclex.Support.Tracking { /// /// /// Calling this method is mandatory. Implementers need to take care that - /// the OnAsyncEnded() method is called on any instance of Waitable that's + /// the OnAsyncEnded() method is called on any instance of transaction that's /// being created. This method also must not be called more than once. /// /// protected virtual void OnAsyncEnded() { - // Make sure the waitable is not ended more than once. By guaranteeing that - // a waitable can only be ended once, we allow users of this class to + // Make sure the transaction is not ended more than once. By guaranteeing that + // a transaction can only be ended once, we allow users of this class to // skip some safeguards against notifications arriving twice. lock(this) { @@ -202,7 +202,7 @@ namespace Nuclex.Support.Tracking { // to waste any effort optimizing the speed at which an implementation fault // will be noticed. if(this.ended) - throw new InvalidOperationException("The Waitable has already been ended"); + throw new InvalidOperationException("The transaction has already been ended"); this.ended = true; @@ -236,7 +236,7 @@ namespace Nuclex.Support.Tracking { protected volatile List endedEventSubscribers; /// Whether the operation has completed yet protected volatile bool ended; - /// Event that will be set when the waitable is completed + /// Event that will be set when the transaction is completed /// /// This event is will only be created when it is specifically asked for using /// the WaitHandle property. diff --git a/Source/Tracking/TransactionGroup.Test.cs b/Source/Tracking/TransactionGroup.Test.cs new file mode 100644 index 0000000..3c548f7 --- /dev/null +++ b/Source/Tracking/TransactionGroup.Test.cs @@ -0,0 +1,263 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the transaction group class + [TestFixture] + public class TransactionGroupTest { + + #region interface ITransactionGroupSubscriber + + /// Interface used to test the transaction group + public interface ITransactionGroupSubscriber { + + /// Called when the transaction group's progress changes + /// Transaction group whose progress has changed + /// Contains the new progress achieved + void ProgressChanged(object sender, ProgressReportEventArgs arguments); + + /// Called when the transaction group has ended + /// Transaction group that as ended + /// Not used + void Ended(object sender, EventArgs arguments); + + } + + #endregion // interface ITransactionGroupSubscriber + + #region class ProgressUpdateEventArgsMatcher + + /// Compares two ProgressUpdateEventArgsInstances for NMock validation + private class ProgressUpdateEventArgsMatcher : Matcher { + + /// Initializes a new ProgressUpdateEventArgsMatcher + /// Expected progress update event arguments + public ProgressUpdateEventArgsMatcher(ProgressReportEventArgs 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) { + ProgressReportEventArgs actual = (actualAsObject as ProgressReportEventArgs); + 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 ProgressReportEventArgs expected; + + } + + #endregion // class ProgressUpdateEventArgsMatcher + + #region class TestTransaction + + /// Transaction used for testing in this unit test + private class TestTransaction : Transaction, IProgressReporter { + + /// will be triggered to report when progress has been achieved + public event EventHandler AsyncProgressChanged; + + /// Changes the testing transaction's indicated progress + /// + /// New progress to be reported by the testing transaction + /// + public void ChangeProgress(float progress) { + OnAsyncProgressChanged(progress); + } + + /// Transitions the transaction into the ended state + public void End() { + OnAsyncEnded(); + } + + /// Fires the progress update event + /// Progress to report (ranging from 0.0 to 1.0) + /// + /// Informs the observers of this transaction about the achieved progress. + /// + protected virtual void OnAsyncProgressChanged(float progress) { + OnAsyncProgressChanged(new ProgressReportEventArgs(progress)); + } + + /// Fires the progress update event + /// Progress to report (ranging from 0.0 to 1.0) + /// + /// Informs the observers of this transaction about the achieved progress. + /// Allows for classes derived from the transaction class to easily provide + /// a custom event arguments class that has been derived from the + /// transaction's ProgressUpdateEventArgs class. + /// + protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) { + EventHandler copy = AsyncProgressChanged; + if(copy != null) + copy(this, eventArguments); + } + + } + + #endregion // class TestTransaction + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new Mockery(); + } + + /// Validates that the set transaction properly sums the progress + [Test] + public void TestSummedProgress() { + using( + TransactionGroup testTransactionGroup = + new TransactionGroup( + new TestTransaction[] { new TestTransaction(), new TestTransaction() } + ) + ) { + ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup); + + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(TransactionGroup)), + new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.25f)) + } + ); + + testTransactionGroup.Children[0].Transaction.ChangeProgress(0.5f); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + } + + /// Validates that the transaction group respects the weights + [Test] + public void TestWeightedSummedProgress() { + using( + TransactionGroup testTransactionGroup = + new TransactionGroup( + new WeightedTransaction[] { + new WeightedTransaction(new TestTransaction(), 1.0f), + new WeightedTransaction(new TestTransaction(), 2.0f) + } + ) + ) { + ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup); + + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(TransactionGroup)), + new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f / 3.0f)) + } + ); + + testTransactionGroup.Children[0].Transaction.ChangeProgress(0.5f); + + Expect.Once.On(mockedSubscriber). + Method("ProgressChanged"). + With( + new Matcher[] { + new NMock2.Matchers.TypeMatcher(typeof(TransactionGroup)), + new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f)) + } + ); + + testTransactionGroup.Children[1].Transaction.ChangeProgress(0.5f); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + } + + /// + /// Validates that the ended event is triggered when the last transaction ends + /// + [Test] + public void TestEndedEvent() { + using( + TransactionGroup testTransactionGroup = + new TransactionGroup( + new TestTransaction[] { new TestTransaction(), new TestTransaction() } + ) + ) { + ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup); + + Expect.Exactly(2).On(mockedSubscriber). + Method("ProgressChanged"). + WithAnyArguments(); + + Expect.Once.On(mockedSubscriber). + Method("Ended"). + WithAnyArguments(); + + testTransactionGroup.Children[0].Transaction.End(); + testTransactionGroup.Children[1].Transaction.End(); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + } + + /// Mocks a subscriber for the events of a transaction + /// Transaction to mock an event subscriber for + /// The mocked event subscriber + private ITransactionGroupSubscriber mockSubscriber(Transaction transaction) { + ITransactionGroupSubscriber mockedSubscriber = + this.mockery.NewMock(); + + transaction.AsyncEnded += new EventHandler(mockedSubscriber.Ended); + (transaction as IProgressReporter).AsyncProgressChanged += + new EventHandler(mockedSubscriber.ProgressChanged); + + return mockedSubscriber; + } + + /// Mock object factory + private Mockery mockery; + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/TransactionGroup.cs b/Source/Tracking/TransactionGroup.cs new file mode 100644 index 0000000..ab23a55 --- /dev/null +++ b/Source/Tracking/TransactionGroup.cs @@ -0,0 +1,210 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.Collections.ObjectModel; + +using Nuclex.Support.Collections; + +namespace Nuclex.Support.Tracking { + + /// Forms a single transaction from a group of transactions + /// Type of transactions to manage as a set + public class TransactionGroup : Transaction, IDisposable, IProgressReporter + where TransactionType : Transaction { + + /// will be triggered to report when progress has been achieved + public event EventHandler AsyncProgressChanged; + + /// Initializes a new transaction group + /// Transactions to track with this group + /// + /// Uses a default weighting factor of 1.0 for all transactions. + /// + public TransactionGroup(IEnumerable childs) + : this() { + + // Construct a WeightedTransaction with the default weight for each + // transaction and wrap it in an ObservedTransaction + foreach(TransactionType transaction in childs) { + this.children.Add( + new ObservedWeightedTransaction( + new WeightedTransaction(transaction), + new ObservedWeightedTransaction.ReportDelegate( + asyncProgressUpdated + ), + new ObservedWeightedTransaction.ReportDelegate( + asyncChildEnded + ) + ) + ); + } + + // Since all transactions have a weight of 1.0, the total weight is + // equal to the number of transactions in our list + this.totalWeight = (float)this.children.Count; + + } + + /// Initializes a new transaction group + /// Transactions to track with this group + public TransactionGroup( + IEnumerable> childs + ) + : this() { + + // Construct an ObservedTransaction around each of the WeightedTransactions + foreach(WeightedTransaction transaction in childs) { + this.children.Add( + new ObservedWeightedTransaction( + transaction, + new ObservedWeightedTransaction.ReportDelegate( + asyncProgressUpdated + ), + new ObservedWeightedTransaction.ReportDelegate( + asyncChildEnded + ) + ) + ); + + // Sum up the total weight + this.totalWeight += transaction.Weight; + } + + } + + /// Performs common initialization for the public constructors + private TransactionGroup() { + this.children = new List>(); + } + + /// Immediately releases all resources owned by the object + public void Dispose() { + + if(this.children != null) { + + // Dispose all the observed transactions, disconnecting the events from the + // actual transactions so the GC can more easily collect this class + for(int index = 0; index < this.children.Count; ++index) + this.children[index].Dispose(); + + this.children = null; + this.wrapper = null; + + } + + } + + /// Childs contained in the transaction set + public IList> Children { + get { + + // The wrapper is constructed only when needed. Most of the time, users will + // just create a transaction group and monitor its progress without ever using + // the Childs collection. + if(this.wrapper == null) { + + // This doesn't need a lock because it's a stateless wrapper. + // If it is constructed twice, then so be it, no problem at all. + this.wrapper = new WeightedTransactionWrapperCollection( + this.children + ); + + } + + return this.wrapper; + + } + } + + /// Fires the progress update event + /// Progress to report (ranging from 0.0 to 1.0) + /// + /// Informs the observers of this transactions about the achieved progress. + /// + protected virtual void OnAsyncProgressChanged(float progress) { + OnAsyncProgressChanged(new ProgressReportEventArgs(progress)); + } + + /// Fires the progress update event + /// Progress to report (ranging from 0.0 to 1.0) + /// + /// Informs the observers of this transaction about the achieved progress. + /// Allows for classes derived from the transaction class to easily provide + /// a custom event arguments class that has been derived from the + /// transaction's ProgressUpdateEventArgs class. + /// + protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) { + EventHandler copy = AsyncProgressChanged; + if(copy != null) + copy(this, eventArguments); + } + + /// + /// Called when the progress of one of the observed transactions changes + /// + private void asyncProgressUpdated() { + float totalProgress = 0.0f; + + // Calculate the sum of the progress reported by our child transactions, + // scaled to the weight each transaction has assigned to it. + for(int index = 0; index < this.children.Count; ++index) { + totalProgress += + this.children[index].Progress * this.children[index].WeightedTransaction.Weight; + } + + // Calculate the actual combined progress + if(this.totalWeight > 0.0f) + totalProgress /= this.totalWeight; + + // Send out the progress update + OnAsyncProgressChanged(totalProgress); + } + + /// + /// Called when an observed transaction ends + /// + private void asyncChildEnded() { + + // If there's still at least one transaction going, don't report that + // the transaction group has finished yet. + for(int index = 0; index < this.children.Count; ++index) + if(!this.children[index].WeightedTransaction.Transaction.Ended) + return; + + // All child transactions have ended, so the set has now ended as well + OnAsyncEnded(); + + } + + /// Transactions being managed in the set + private List> children; + /// + /// Wrapper collection for exposing the child transactions under the + /// WeightedTransaction interface + /// + private volatile WeightedTransactionWrapperCollection wrapper; + /// Summed weight of all transactions in the set + private float totalWeight; + + } + +} // namespace Nuclex.Support.Tracking diff --git a/Source/Tracking/WaitableGroup.Test.cs b/Source/Tracking/WaitableGroup.Test.cs deleted file mode 100644 index 9fcd81b..0000000 --- a/Source/Tracking/WaitableGroup.Test.cs +++ /dev/null @@ -1,257 +0,0 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2008 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; - -namespace Nuclex.Support.Tracking { - - /// Unit Test for the waitable group class - [TestFixture] - public class WaitableGroupTest { - - #region interface IWaitableGroupSubscriber - - /// Interface used to test the set waitable. - public interface IWaitableGroupSubscriber { - - /// Called when the set waitable's progress changes - /// Waitable group whose progress has changed - /// Contains the new progress achieved - void ProgressChanged(object sender, ProgressReportEventArgs e); - - /// Called when the set waitable has ended - /// Waitable group that as ended - /// Not used - void Ended(object sender, EventArgs e); - - } - - #endregion // interface IWaitableGroupSubscriber - - #region class ProgressUpdateEventArgsMatcher - - /// Compares two ProgressUpdateEventArgsInstances for NMock validation - private class ProgressUpdateEventArgsMatcher : Matcher { - - /// Initializes a new ProgressUpdateEventArgsMatcher - /// Expected progress update event arguments - public ProgressUpdateEventArgsMatcher(ProgressReportEventArgs 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) { - ProgressReportEventArgs actual = (actualAsObject as ProgressReportEventArgs); - 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 ProgressReportEventArgs expected; - - } - - #endregion // class ProgressUpdateEventArgsMatcher - - #region class TestWaitable - - /// Waitable used for testing in this unit test - private class TestWaitable : Waitable, IProgressReporter { - - /// will be triggered to report when progress has been achieved - public event EventHandler AsyncProgressChanged; - - /// Changes the testing waitable's indicated progress - /// - /// New progress to be reported by the testing waitable - /// - public void ChangeProgress(float progress) { - OnAsyncProgressChanged(progress); - } - - /// Transitions the waitable into the ended state - public void End() { - OnAsyncEnded(); - } - - /// Fires the progress update event - /// Progress to report (ranging from 0.0 to 1.0) - /// - /// Informs the observers of this waitable about the achieved progress. - /// - protected virtual void OnAsyncProgressChanged(float progress) { - OnAsyncProgressChanged(new ProgressReportEventArgs(progress)); - } - - /// Fires the progress update event - /// Progress to report (ranging from 0.0 to 1.0) - /// - /// Informs the observers of this waitable about the achieved progress. - /// Allows for classes derived from the Waitable class to easily provide - /// a custom event arguments class that has been derived from the - /// Waitable's ProgressUpdateEventArgs class. - /// - protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) { - EventHandler copy = AsyncProgressChanged; - if(copy != null) - copy(this, eventArguments); - } - - } - - #endregion // class TestWaitable - - /// Initialization routine executed before each test is run - [SetUp] - public void Setup() { - this.mockery = new Mockery(); - } - - /// Validates that the set waitable properly sums the progress - [Test] - public void TestSummedProgress() { - WaitableGroup testWaitableGroup = - new WaitableGroup( - new TestWaitable[] { new TestWaitable(), new TestWaitable() } - ); - - IWaitableGroupSubscriber mockedSubscriber = mockSubscriber(testWaitableGroup); - - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(WaitableGroup)), - new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.25f)) - } - ); - - testWaitableGroup.Children[0].Waitable.ChangeProgress(0.5f); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Validates that the waitable group respects the weights - [Test] - public void TestWeightedSummedProgress() { - WaitableGroup testWaitableGroup = - new WaitableGroup( - new WeightedWaitable[] { - new WeightedWaitable(new TestWaitable(), 1.0f), - new WeightedWaitable(new TestWaitable(), 2.0f) - } - ); - - IWaitableGroupSubscriber mockedSubscriber = mockSubscriber(testWaitableGroup); - - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(WaitableGroup)), - new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f / 3.0f)) - } - ); - - testWaitableGroup.Children[0].Waitable.ChangeProgress(0.5f); - - Expect.Once.On(mockedSubscriber). - Method("ProgressChanged"). - With( - new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(WaitableGroup)), - new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f)) - } - ); - - testWaitableGroup.Children[1].Waitable.ChangeProgress(0.5f); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// - /// Validates that the ended event is triggered when the last waitable ends - /// - [Test] - public void TestEndedEvent() { - WaitableGroup testWaitableGroup = - new WaitableGroup( - new TestWaitable[] { new TestWaitable(), new TestWaitable() } - ); - - IWaitableGroupSubscriber mockedSubscriber = mockSubscriber(testWaitableGroup); - - Expect.Exactly(2).On(mockedSubscriber). - Method("ProgressChanged"). - WithAnyArguments(); - - Expect.Once.On(mockedSubscriber). - Method("Ended"). - WithAnyArguments(); - - testWaitableGroup.Children[0].Waitable.End(); - testWaitableGroup.Children[1].Waitable.End(); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Mocks a subscriber for the events of a waitable - /// Waitable to mock an event subscriber for - /// The mocked event subscriber - private IWaitableGroupSubscriber mockSubscriber(Waitable waitable) { - IWaitableGroupSubscriber mockedSubscriber = - this.mockery.NewMock(); - - waitable.AsyncEnded += new EventHandler(mockedSubscriber.Ended); - (waitable as IProgressReporter).AsyncProgressChanged += - new EventHandler(mockedSubscriber.ProgressChanged); - - return mockedSubscriber; - } - - /// Mock object factory - private Mockery mockery; - - } - -} // namespace Nuclex.Support.Tracking - -#endif // UNITTEST diff --git a/Source/Tracking/WaitableGroup.cs b/Source/Tracking/WaitableGroup.cs deleted file mode 100644 index ff24fb7..0000000 --- a/Source/Tracking/WaitableGroup.cs +++ /dev/null @@ -1,202 +0,0 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2008 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.Collections.ObjectModel; - -using Nuclex.Support.Collections; - -namespace Nuclex.Support.Tracking { - - /// Forms a single waitable from a group of waitables - /// Type of waitables to manage as a set - public class WaitableGroup : Waitable, IDisposable, IProgressReporter - where WaitableType : Waitable { - - /// will be triggered to report when progress has been achieved - public event EventHandler AsyncProgressChanged; - - /// Initializes a new waitable group - /// Waitables to track with this group - /// - /// Uses a default weighting factor of 1.0 for all waitables. - /// - public WaitableGroup(IEnumerable childs) - : this() { - - // Construct a WeightedWaitable with the default weight for each - // waitable and wrap it in an ObservedWaitable - foreach(WaitableType waitable in childs) { - this.children.Add( - new ObservedWeightedWaitable( - new WeightedWaitable(waitable), - new ObservedWeightedWaitable.ReportDelegate(asyncProgressUpdated), - new ObservedWeightedWaitable.ReportDelegate(asyncChildEnded) - ) - ); - } - - // Since all waitables have a weight of 1.0, the total weight is - // equal to the number of waitables in our list - this.totalWeight = (float)this.children.Count; - - } - - /// Initializes a new waitable group - /// Waitables to track with this group - public WaitableGroup( - IEnumerable> childs - ) - : this() { - - // Construct an ObservedWaitable around each of the WeightedWaitables - foreach(WeightedWaitable waitable in childs) { - this.children.Add( - new ObservedWeightedWaitable( - waitable, - new ObservedWeightedWaitable.ReportDelegate(asyncProgressUpdated), - new ObservedWeightedWaitable.ReportDelegate(asyncChildEnded) - ) - ); - - // Sum up the total weight - this.totalWeight += waitable.Weight; - } - - } - - /// Performs common initialization for the public constructors - private WaitableGroup() { - this.children = new List>(); - } - - /// Immediately releases all resources owned by the object - public void Dispose() { - - if(this.children != null) { - - // Dispose all the observed waitables, disconnecting the events from the - // actual waitables so the GC can more easily collect this class - for(int index = 0; index < this.children.Count; ++index) - this.children[index].Dispose(); - - this.children = null; - this.wrapper = null; - - } - - } - - /// Childs contained in the waitable set - public IList> Children { - get { - - // The wrapper is constructed only when needed. Most of the time, users will - // just create a waitable group and monitor its progress without ever using - // the Childs collection. - if(this.wrapper == null) { - - // This doesn't need a lock because it's a stateless wrapper. - // If it is constructed twice, then so be it, no problem at all. - this.wrapper = new WeightedWaitableWrapperCollection( - this.children - ); - - } - - return this.wrapper; - - } - } - - /// Fires the progress update event - /// Progress to report (ranging from 0.0 to 1.0) - /// - /// Informs the observers of this waitables about the achieved progress. - /// - protected virtual void OnAsyncProgressChanged(float progress) { - OnAsyncProgressChanged(new ProgressReportEventArgs(progress)); - } - - /// Fires the progress update event - /// Progress to report (ranging from 0.0 to 1.0) - /// - /// Informs the observers of this waitable about the achieved progress. - /// Allows for classes derived from the Waitable class to easily provide - /// a custom event arguments class that has been derived from the - /// waitable's ProgressUpdateEventArgs class. - /// - protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) { - EventHandler copy = AsyncProgressChanged; - if(copy != null) - copy(this, eventArguments); - } - - /// - /// Called when the progress of one of the observed waitables changes - /// - private void asyncProgressUpdated() { - float totalProgress = 0.0f; - - // Calculate the sum of the progress reported by our child waitables, - // scaled to the weight each waitable has assigned to it. - for(int index = 0; index < this.children.Count; ++index) { - totalProgress += - this.children[index].Progress * this.children[index].WeightedWaitable.Weight; - } - - // Calculate the actual combined progress - if(this.totalWeight > 0.0f) - totalProgress /= this.totalWeight; - - // Send out the progress update - OnAsyncProgressChanged(totalProgress); - } - - /// - /// Called when an observed waitable ends - /// - private void asyncChildEnded() { - - // If there's still at least one waitable going, don't report that - // the waitable group has finished yet. - for(int index = 0; index < this.children.Count; ++index) - if(!this.children[index].WeightedWaitable.Waitable.Ended) - return; - - // All child waitables have ended, so the set has now ended as well - OnAsyncEnded(); - - } - - /// Waitables being managed in the set - private List> children; - /// - /// Wrapper collection for exposing the child waitables under the - /// WeightedWaitable interface - /// - private volatile WeightedWaitableWrapperCollection wrapper; - /// Summed weight of all waitables in the set - private float totalWeight; - - } - -} // namespace Nuclex.Support.Tracking diff --git a/Source/Tracking/WeightedTransaction.Test.cs b/Source/Tracking/WeightedTransaction.Test.cs new file mode 100644 index 0000000..ea914e2 --- /dev/null +++ b/Source/Tracking/WeightedTransaction.Test.cs @@ -0,0 +1,88 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.IO; + +#if UNITTEST + +using NUnit.Framework; +using NUnit.Framework.SyntaxHelpers; + +namespace Nuclex.Support.Tracking { + + /// Unit Test for the weighted transaction wrapper + [TestFixture] + public class WeightedTransactionTest { + + #region class TestTransaction + + /// Transaction used for testing in this unit test + private class TestTransaction : Transaction { } + + #endregion // class TestTransaction + + /// + /// Tests whether the weighted transaction wrapper correctly stores the transaction + /// it was given in the constructor + /// + [Test] + public void TestTransactionStorage() { + TestTransaction transaction = new TestTransaction(); + WeightedTransaction testWrapper = new WeightedTransaction( + transaction + ); + + Assert.AreSame(transaction, testWrapper.Transaction); + } + + /// + /// Tests whether the weighted transaction wrapper correctly applies the default + /// unit weight to the transaction if no explicit weight was specified + /// + [Test] + public void TestDefaultWeight() { + TestTransaction transaction = new TestTransaction(); + WeightedTransaction testWrapper = new WeightedTransaction( + transaction + ); + + Assert.AreEqual(1.0f, testWrapper.Weight); + } + + /// + /// Tests whether the weighted transaction wrapper correctly stores the weight + /// it was given in the constructor + /// + [Test] + public void TestWeightStorage() { + TestTransaction transaction = new TestTransaction(); + WeightedTransaction testWrapper = new WeightedTransaction( + transaction, 12.0f + ); + + Assert.AreEqual(12.0f, testWrapper.Weight); + } + + } + +} // namespace Nuclex.Support.Tracking + +#endif // UNITTEST diff --git a/Source/Tracking/WeightedTransaction.cs b/Source/Tracking/WeightedTransaction.cs new file mode 100644 index 0000000..464f564 --- /dev/null +++ b/Source/Tracking/WeightedTransaction.cs @@ -0,0 +1,60 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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; + +namespace Nuclex.Support.Tracking { + + /// Transaction with an associated weight for the total progress + public class WeightedTransaction where TransactionType : Transaction { + + /// + /// Initializes a new weighted transaction with a default weight of 1.0 + /// + /// Transaction whose progress to monitor + public WeightedTransaction(TransactionType transaction) : this(transaction, 1.0f) { } + + /// Initializes a new weighted transaction + /// transaction whose progress to monitor + /// Weighting of the transaction's progress + public WeightedTransaction(TransactionType transaction, float weight) { + this.transaction = transaction; + this.weight = weight; + } + + /// Transaction being wrapped by this weighted transaction + public TransactionType Transaction { + get { return this.transaction; } + } + + /// The contribution of this transaction to the total progress + public float Weight { + get { return this.weight; } + } + + /// Transaction whose progress we're tracking + private TransactionType transaction; + /// Weighting of this transaction in the total progress + private float weight; + + } + +} // namespace Nuclex.Support.Tracking diff --git a/Source/Tracking/WeightedWaitable.cs b/Source/Tracking/WeightedWaitable.cs deleted file mode 100644 index 3a0ed56..0000000 --- a/Source/Tracking/WeightedWaitable.cs +++ /dev/null @@ -1,60 +0,0 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2008 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; - -namespace Nuclex.Support.Tracking { - - /// Waitable with an associated weight for the total progress - public class WeightedWaitable where WaitableType : Waitable { - - /// - /// Initializes a new weighted waitable with a default weight of 1.0 - /// - /// Waitable whose progress to monitor - public WeightedWaitable(WaitableType waitable) : this(waitable, 1.0f) { } - - /// Initializes a new weighted waitable - /// Waitable whose progress to monitor - /// Weighting of the waitable's progress - public WeightedWaitable(WaitableType waitable, float weight) { - this.waitable = waitable; - this.weight = weight; - } - - /// Waitable being wrapped by this weighted waitable - public WaitableType Waitable { - get { return this.waitable; } - } - - /// The contribution of this waitable to the total progress - public float Weight { - get { return this.weight; } - } - - /// Waitable whose progress we're tracking - private WaitableType waitable; - /// Weighting of this waitable in the total progress - private float weight; - - } - -} // namespace Nuclex.Support.Tracking