Fixed a serious bug in the TransactionGroup which would cause the TransactionGroup to crash when all the transactions it gets passed in the constructor were in the ended state; fixed another serious bug that would occur when transactions reached the ended state during the constructor; wrote unit tests that reproduced those bugs
git-svn-id: file:///srv/devel/repo-conversion/nusu@121 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
parent
a2f53639d4
commit
190e32ee56
|
@ -98,7 +98,10 @@ namespace Nuclex.Support.Tracking {
|
||||||
IObservationSubscriber subscriber = this.mockery.NewMock<IObservationSubscriber>();
|
IObservationSubscriber subscriber = this.mockery.NewMock<IObservationSubscriber>();
|
||||||
|
|
||||||
Expect.AtLeast(0).On(subscriber).Method("ProgressUpdated");
|
Expect.AtLeast(0).On(subscriber).Method("ProgressUpdated");
|
||||||
Expect.Once.On(subscriber).Method("Ended");
|
// This should no be called because otherwise, the 'Ended' event would be raised
|
||||||
|
// to the transaction group before all transactions have been added into
|
||||||
|
// the internal list, leading to an early ending or even multiple endings.
|
||||||
|
Expect.Never.On(subscriber).Method("Ended");
|
||||||
|
|
||||||
using(
|
using(
|
||||||
ObservedWeightedTransaction<Transaction> test =
|
ObservedWeightedTransaction<Transaction> test =
|
||||||
|
|
|
@ -58,7 +58,12 @@ namespace Nuclex.Support.Tracking {
|
||||||
// prevent object coupling where none is neccessary and to save some processing time.
|
// prevent object coupling where none is neccessary and to save some processing time.
|
||||||
this.progress = 1.0f;
|
this.progress = 1.0f;
|
||||||
progressUpdateCallback();
|
progressUpdateCallback();
|
||||||
endedCallback();
|
|
||||||
|
// Do not call the ended callback here. This constructor is called when the
|
||||||
|
// TransactionGroup constructs its list of transactions. If this is called and
|
||||||
|
// the first transaction to be added to the group happens to be in the ended
|
||||||
|
// state, the transactionGroup will immediately think it has ended!
|
||||||
|
//!DONT!endedCallback();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,12 @@ License along with this library
|
||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#if UNITTEST
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
#if UNITTEST
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NMock2;
|
using NMock2;
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,13 @@ License along with this library
|
||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#if UNITTEST
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
#if UNITTEST
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NMock2;
|
using NMock2;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ License along with this library
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
#if UNITTEST
|
#if UNITTEST
|
||||||
|
|
||||||
|
@ -139,6 +140,53 @@ namespace Nuclex.Support.Tracking {
|
||||||
|
|
||||||
#endregion // class TestTransaction
|
#endregion // class TestTransaction
|
||||||
|
|
||||||
|
#region class ChainEndingTransaction
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transaction that ends another transaction when its Ended property is called
|
||||||
|
/// </summary>
|
||||||
|
private class ChainEndingTransaction : Transaction {
|
||||||
|
|
||||||
|
/// <summary>Initializes a new chain ending transaction</summary>
|
||||||
|
public ChainEndingTransaction() {
|
||||||
|
this.chainedTransaction = new TestTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Transitions the transaction into the ended state</summary>
|
||||||
|
public void End() {
|
||||||
|
OnAsyncEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transaction that will end when this transaction's ended property is accessed
|
||||||
|
/// </summary>
|
||||||
|
public TestTransaction ChainedTransaction {
|
||||||
|
get { return this.chainedTransaction; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Whether the transaction has ended already</summary>
|
||||||
|
public override bool Ended {
|
||||||
|
get {
|
||||||
|
if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) {
|
||||||
|
this.chainedTransaction.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.Ended;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transaction that will end when this transaction's ended property is accessed
|
||||||
|
/// </summary>
|
||||||
|
private TestTransaction chainedTransaction;
|
||||||
|
|
||||||
|
/// <summary>Whether we already ended the chained transaction and ourselves</summary>
|
||||||
|
private int endedCalled;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion // class ChainEndingTransaction
|
||||||
|
|
||||||
/// <summary>Initialization routine executed before each test is run</summary>
|
/// <summary>Initialization routine executed before each test is run</summary>
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() {
|
public void Setup() {
|
||||||
|
@ -239,6 +287,63 @@ namespace Nuclex.Support.Tracking {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the transaction group immediately enters the ended state when
|
||||||
|
/// the contained transactions have already ended before the constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This was a bug at one time and should prevent a regression
|
||||||
|
/// </remarks>
|
||||||
|
[Test]
|
||||||
|
public void TestAlreadyEndedTransactions() {
|
||||||
|
using(
|
||||||
|
TransactionGroup<Transaction> testTransactionGroup =
|
||||||
|
new TransactionGroup<Transaction>(
|
||||||
|
new Transaction[] { Transaction.EndedDummy, Transaction.EndedDummy }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Assert.IsTrue(testTransactionGroup.Wait(1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the transaction group doesn't think it's already ended when
|
||||||
|
/// the first transaction being added is in the ended state
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This was a bug at one time and should prevent a regression
|
||||||
|
/// </remarks>
|
||||||
|
[Test]
|
||||||
|
public void TestAlreadyEndedTransactionAsFirstTransaction() {
|
||||||
|
using(
|
||||||
|
TransactionGroup<Transaction> testTransactionGroup =
|
||||||
|
new TransactionGroup<Transaction>(
|
||||||
|
new Transaction[] { Transaction.EndedDummy, new TestTransaction() }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Assert.IsFalse(testTransactionGroup.Ended);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a transaction ending while the constructor is running doesn't
|
||||||
|
/// wreak havoc on the transaction group
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestTransactionEndingDuringConstructor() {
|
||||||
|
ChainEndingTransaction chainTransaction = new ChainEndingTransaction();
|
||||||
|
using(
|
||||||
|
TransactionGroup<Transaction> testTransactionGroup =
|
||||||
|
new TransactionGroup<Transaction>(
|
||||||
|
new Transaction[] { chainTransaction.ChainedTransaction, chainTransaction }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Assert.IsFalse(testTransactionGroup.Ended);
|
||||||
|
chainTransaction.End();
|
||||||
|
Assert.IsTrue(testTransactionGroup.Ended);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Mocks a subscriber for the events of a transaction</summary>
|
/// <summary>Mocks a subscriber for the events of a transaction</summary>
|
||||||
/// <param name="transaction">Transaction to mock an event subscriber for</param>
|
/// <param name="transaction">Transaction to mock an event subscriber for</param>
|
||||||
/// <returns>The mocked event subscriber</returns>
|
/// <returns>The mocked event subscriber</returns>
|
||||||
|
|
|
@ -21,6 +21,7 @@ License along with this library
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
using Nuclex.Support.Collections;
|
using Nuclex.Support.Collections;
|
||||||
|
|
||||||
|
@ -35,17 +36,18 @@ namespace Nuclex.Support.Tracking {
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
||||||
|
|
||||||
/// <summary>Initializes a new transaction group</summary>
|
/// <summary>Initializes a new transaction group</summary>
|
||||||
/// <param name="childs">Transactions to track with this group</param>
|
/// <param name="children">Transactions to track with this group</param>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Uses a default weighting factor of 1.0 for all transactions.
|
/// Uses a default weighting factor of 1.0 for all transactions.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public TransactionGroup(IEnumerable<TransactionType> childs)
|
public TransactionGroup(IEnumerable<TransactionType> children) {
|
||||||
: this() {
|
List<ObservedWeightedTransaction<TransactionType>> childrenList =
|
||||||
|
new List<ObservedWeightedTransaction<TransactionType>>();
|
||||||
|
|
||||||
// Construct a WeightedTransaction with the default weight for each
|
// Construct a WeightedTransaction with the default weight for each
|
||||||
// transaction and wrap it in an ObservedTransaction
|
// transaction and wrap it in an ObservedTransaction
|
||||||
foreach(TransactionType transaction in childs) {
|
foreach(TransactionType transaction in children) {
|
||||||
this.children.Add(
|
childrenList.Add(
|
||||||
new ObservedWeightedTransaction<TransactionType>(
|
new ObservedWeightedTransaction<TransactionType>(
|
||||||
new WeightedTransaction<TransactionType>(transaction),
|
new WeightedTransaction<TransactionType>(transaction),
|
||||||
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
||||||
|
@ -60,20 +62,26 @@ namespace Nuclex.Support.Tracking {
|
||||||
|
|
||||||
// Since all transactions have a weight of 1.0, the total weight is
|
// Since all transactions have a weight of 1.0, the total weight is
|
||||||
// equal to the number of transactions in our list
|
// equal to the number of transactions in our list
|
||||||
this.totalWeight = (float)this.children.Count;
|
this.totalWeight = (float)childrenList.Count;
|
||||||
|
// Thread.MemoryBarrier(); // not needed because children is volatile
|
||||||
|
this.children = childrenList;
|
||||||
|
|
||||||
|
// Any asyncEnded events being receiving from the transactions until now
|
||||||
|
// would have been ignored, so we need to check again here
|
||||||
|
asyncChildEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Initializes a new transaction group</summary>
|
/// <summary>Initializes a new transaction group</summary>
|
||||||
/// <param name="childs">Transactions to track with this group</param>
|
/// <param name="children">Transactions to track with this group</param>
|
||||||
public TransactionGroup(
|
public TransactionGroup(
|
||||||
IEnumerable<WeightedTransaction<TransactionType>> childs
|
IEnumerable<WeightedTransaction<TransactionType>> children
|
||||||
)
|
) {
|
||||||
: this() {
|
List<ObservedWeightedTransaction<TransactionType>> childrenList =
|
||||||
|
new List<ObservedWeightedTransaction<TransactionType>>();
|
||||||
|
|
||||||
// Construct an ObservedTransaction around each of the WeightedTransactions
|
// Construct an ObservedTransaction around each of the WeightedTransactions
|
||||||
foreach(WeightedTransaction<TransactionType> transaction in childs) {
|
foreach(WeightedTransaction<TransactionType> transaction in children) {
|
||||||
this.children.Add(
|
childrenList.Add(
|
||||||
new ObservedWeightedTransaction<TransactionType>(
|
new ObservedWeightedTransaction<TransactionType>(
|
||||||
transaction,
|
transaction,
|
||||||
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
||||||
|
@ -89,11 +97,11 @@ namespace Nuclex.Support.Tracking {
|
||||||
this.totalWeight += transaction.Weight;
|
this.totalWeight += transaction.Weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
this.children = childrenList;
|
||||||
|
|
||||||
/// <summary>Performs common initialization for the public constructors</summary>
|
// Any asyncEnded events being receiving from the transactions until now
|
||||||
private TransactionGroup() {
|
// would have been ignored, so we need to check again here
|
||||||
this.children = new List<ObservedWeightedTransaction<TransactionType>>();
|
asyncChildEnded();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Immediately releases all resources owned by the object</summary>
|
/// <summary>Immediately releases all resources owned by the object</summary>
|
||||||
|
@ -162,6 +170,10 @@ namespace Nuclex.Support.Tracking {
|
||||||
/// Called when the progress of one of the observed transactions changes
|
/// Called when the progress of one of the observed transactions changes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void asyncProgressUpdated() {
|
private void asyncProgressUpdated() {
|
||||||
|
if(this.children == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
float totalProgress = 0.0f;
|
float totalProgress = 0.0f;
|
||||||
|
|
||||||
// Calculate the sum of the progress reported by our child transactions,
|
// Calculate the sum of the progress reported by our child transactions,
|
||||||
|
@ -184,6 +196,15 @@ namespace Nuclex.Support.Tracking {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void asyncChildEnded() {
|
private void asyncChildEnded() {
|
||||||
|
|
||||||
|
// If a transaction reports its end durign the constructor, it will end up here
|
||||||
|
// where the collection has not been assigned yet, allowing us to skip the
|
||||||
|
// check until all transactions are there (otherwise, we might invoke
|
||||||
|
// OnAsyncended() early, because all transactions in the list seem to have ended
|
||||||
|
// despite the fact that the constructor hasn't finished adding transactions yet)
|
||||||
|
if(this.children == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If there's still at least one transaction going, don't report that
|
// If there's still at least one transaction going, don't report that
|
||||||
// the transaction group has finished yet.
|
// the transaction group has finished yet.
|
||||||
for(int index = 0; index < this.children.Count; ++index)
|
for(int index = 0; index < this.children.Count; ++index)
|
||||||
|
@ -191,12 +212,14 @@ namespace Nuclex.Support.Tracking {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// All child transactions have ended, so the set has now ended as well
|
// All child transactions have ended, so the set has now ended as well
|
||||||
OnAsyncEnded();
|
if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) {
|
||||||
|
OnAsyncEnded();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Transactions being managed in the set</summary>
|
/// <summary>Transactions being managed in the set</summary>
|
||||||
private List<ObservedWeightedTransaction<TransactionType>> children;
|
private volatile List<ObservedWeightedTransaction<TransactionType>> children;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wrapper collection for exposing the child transactions under the
|
/// Wrapper collection for exposing the child transactions under the
|
||||||
/// WeightedTransaction interface
|
/// WeightedTransaction interface
|
||||||
|
@ -204,6 +227,8 @@ namespace Nuclex.Support.Tracking {
|
||||||
private volatile WeightedTransactionWrapperCollection<TransactionType> wrapper;
|
private volatile WeightedTransactionWrapperCollection<TransactionType> wrapper;
|
||||||
/// <summary>Summed weight of all transactions in the set</summary>
|
/// <summary>Summed weight of all transactions in the set</summary>
|
||||||
private float totalWeight;
|
private float totalWeight;
|
||||||
|
/// <summary>Whether we already called OnAsyncEnded</summary>
|
||||||
|
private int endedCalled;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user