#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 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 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 #region class ChainEndingTransaction /// /// Transaction that ends another transaction when its Ended property is called /// private class ChainEndingTransaction : Transaction { /// Initializes a new chain ending transaction public ChainEndingTransaction() { this.chainedTransaction = new TestTransaction(); } /// Transitions the transaction into the ended state public void End() { OnAsyncEnded(); } /// /// Transaction that will end when this transaction's ended property is accessed /// public TestTransaction ChainedTransaction { get { return this.chainedTransaction; } } /// Whether the transaction has ended already public override bool Ended { get { if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) { this.chainedTransaction.End(); } return base.Ended; } } /// /// Transaction that will end when this transaction's ended property is accessed /// private TestTransaction chainedTransaction; /// Whether we already ended the chained transaction and ourselves private int endedCalled; } #endregion // class ChainEndingTransaction /// 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 out of /// multiple transactions in the group ends. /// [Test] public void TestEndedEventWithTwoTransactions() { 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(); } } /// /// Validates that the ended event is triggered when a single transaction contained /// in the group ends. /// [Test] public void TestEndedEventWithSingleTransaction() { using( TransactionGroup testTransactionGroup = new TransactionGroup( new TestTransaction[] { new TestTransaction() } ) ) { ITransactionGroupSubscriber mockedSubscriber = mockSubscriber(testTransactionGroup); Expect.Once.On(mockedSubscriber). Method("ProgressChanged"). WithAnyArguments(); Expect.Once.On(mockedSubscriber). Method("Ended"). WithAnyArguments(); testTransactionGroup.Children[0].Transaction.End(); this.mockery.VerifyAllExpectationsHaveBeenMet(); } } /// /// Verifies that the transaction group immediately enters the ended state when /// the contained transactions have already ended before the constructor /// /// /// This was a bug at one time and should prevent a regression /// [Test] public void TestAlreadyEndedTransactions() { using( TransactionGroup testTransactionGroup = new TransactionGroup( new Transaction[] { Transaction.EndedDummy, Transaction.EndedDummy } ) ) { Assert.IsTrue(testTransactionGroup.Wait(1000)); } } /// /// Verifies that the transaction group doesn't think it's already ended when /// the first transaction being added is in the ended state /// /// /// This was a bug at one time and should prevent a regression /// [Test] public void TestAlreadyEndedTransactionAsFirstTransaction() { using( TransactionGroup testTransactionGroup = new TransactionGroup( new Transaction[] { Transaction.EndedDummy, new TestTransaction() } ) ) { Assert.IsFalse(testTransactionGroup.Ended); } } /// /// Verifies that a transaction ending while the constructor is running doesn't /// wreak havoc on the transaction group /// [Test] public void TestTransactionEndingDuringConstructor() { ChainEndingTransaction chainTransaction = new ChainEndingTransaction(); using( TransactionGroup testTransactionGroup = new TransactionGroup( new Transaction[] { chainTransaction.ChainedTransaction, chainTransaction } ) ) { Assert.IsFalse(testTransactionGroup.Ended); chainTransaction.End(); Assert.IsTrue(testTransactionGroup.Ended); } } /// 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