#region CPL License /* Nuclex Framework Copyright (C) 2002-2017 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 #if !NO_CONCURRENT_COLLECTIONS using System; using System.Threading; using System.Collections.Generic; #if UNITTEST using NUnit.Framework; namespace Nuclex.Support.Threading { /// Unit Test for the thread runner class [TestFixture] internal class ThreadRunnerTest { #region class DefaultDisposeRunner /// Implementation of a thread runner to check default dispose behavior private class DefaultDisposeRunner : ThreadRunner { /// Reports an error /// Error that will be reported protected override void ReportError(Exception exception) { } /// Called when the status of the busy flag changes protected override void BusyChanged() { } } #endregion // class DefaultDisposeRunner #region class DummyRunner /// Implementation of a thread runner used for unit testing private class DummyRunner : ThreadRunner { /// Initializes a new dummy thread runner public DummyRunner() : base() { this.completionGate = new ManualResetEvent(initialState: false); } /// Immediately frees all resources used by the instance public new void Dispose() { base.Dispose(100); if(this.completionGate != null) { this.completionGate.Dispose(); this.completionGate = null; } } /// Waits for the task for complete (all of 100 milliseconds) /// True if the task completed, false if it continues running public bool WaitForCompletion() { return this.completionGate.WaitOne(100); } /// How often the status of the busy flag has changed public int BusyChangeCount { get { return this.busyChangeCount; } } /// Error that has been reported the last time a task was run public Exception ReportedError { get { return this.reportedError; } } /// Reports an error /// Error that will be reported protected override void ReportError(Exception exception) { this.reportedError = exception; } /// Called when the status of the busy flag changes protected override void BusyChanged() { ++busyChangeCount; if((busyChangeCount >= 2) && (base.IsBusy == false)) { this.completionGate.Set(); } } /// Last error that was reported in the thread private Exception reportedError; /// Number of times the busy state of the runner has changed private int busyChangeCount; /// Triggered when the busy event has performed a double flank private ManualResetEvent completionGate; } #endregion // class DummyRunner #region class DummyTask /// Dummy task that can be executed by a thread runner private class DummyTask : IDisposable { /// Initializes a new dummy task /// How long the task shoudl take to execute public DummyTask(int delayMilliseconds) { this.startGate = new ManualResetEvent(initialState: false); this.delayMilliseconds = delayMilliseconds; } /// Immediately releases all resources owned by the instance public void Dispose() { if(this.startGate != null) { this.startGate.Dispose(); this.startGate = null; } } /// Waits for the task to start (all of 100 milliseconds) /// True if the start started, false if it didn't public bool WaitForStart() { return this.startGate.WaitOne(100); } /// Sets the task up to fail with the specified error /// Error the task will fail with public void FailWith(Exception error) { this.error = error; } /// Runs the task with no arguments public void Run() { this.startGate.Set(); ++this.executionCount; Thread.Sleep(this.delayMilliseconds); if(this.error != null) { throw this.error; } } /// Runs the task with one argument /// First argument passed from the runner public void Run(float firstArgument) { this.startGate.Set(); ++this.executionCount; this.firstArgument = firstArgument; Thread.Sleep(this.delayMilliseconds); if(this.error != null) { throw this.error; } } /// Runs the task with two argument /// First argument passed from the runner /// Second argument passed from the runner public void Run(float firstArgument, string secondArgument) { this.startGate.Set(); ++this.executionCount; this.firstArgument = firstArgument; this.secondArgument = secondArgument; Thread.Sleep(this.delayMilliseconds); if(this.error != null) { throw this.error; } } /// Runs the task with no arguments /// Token by which cancellation can be signalled public void RunCancellable(CancellationToken cancellationToken) { this.startGate.Set(); ++this.executionCount; if(delayMilliseconds == 0) { Thread.Sleep(0); } else { if(cancellationToken.WaitHandle.WaitOne(delayMilliseconds)) { this.wasCancelled = cancellationToken.IsCancellationRequested; cancellationToken.ThrowIfCancellationRequested(); } } if(this.error != null) { throw this.error; } } /// Runs the task with one argument /// First argument passed from the runner /// Token by which cancellation can be signalled public void RunCancellable(float firstArgument, CancellationToken cancellationToken) { this.startGate.Set(); ++this.executionCount; this.firstArgument = firstArgument; if(delayMilliseconds == 0) { Thread.Sleep(0); } else { if(cancellationToken.WaitHandle.WaitOne(delayMilliseconds)) { this.wasCancelled = cancellationToken.IsCancellationRequested; cancellationToken.ThrowIfCancellationRequested(); } } if(this.error != null) { throw this.error; } } /// Runs the task with two argument /// First argument passed from the runner /// Second argument passed from the runner /// Token by which cancellation can be signalled public void RunCancellable( float firstArgument, string secondArgument, CancellationToken cancellationToken ) { this.startGate.Set(); ++this.executionCount; this.firstArgument = firstArgument; this.secondArgument = secondArgument; if(delayMilliseconds == 0) { Thread.Sleep(0); } else { if(cancellationToken.WaitHandle.WaitOne(delayMilliseconds)) { this.wasCancelled = cancellationToken.IsCancellationRequested; cancellationToken.ThrowIfCancellationRequested(); } } if(this.error != null) { throw this.error; } } /// How many times the task was run public int ExecutionCount { get { return this.executionCount; } } /// Whether the task was cancelled by the runner itself public bool WasCancelled { get { return this.wasCancelled; } } /// What the first argument was during the last call public float FirstArgument { get { return this.firstArgument; } } /// What the second argument was during the last call public string SecondArgument { get { return this.secondArgument; } } /// Last error that was reported in the thread private Exception error; /// Triggered when the task has started private ManualResetEvent startGate; /// How long the task should take to execute in milliseconds private int delayMilliseconds; /// How many times the task has been executed private volatile int executionCount; /// Whether the task has been cancelled private volatile bool wasCancelled; /// First argument that was passed to the task private volatile float firstArgument; /// Second argument that was passed to the task private volatile string secondArgument; } #endregion // class DummyRunner /// Verifies that the thread runner has a default constructor [Test] public void CanBeDefaultConstructed() { using(new DummyRunner()) { } } /// Checks that the runner sets and unsets its busy flag [Test] public void BusyFlagIsToggled() { using(var runner = new DummyRunner()) { int busyFlagChangeCount = runner.BusyChangeCount; Assert.IsFalse(runner.IsBusy); runner.RunInBackground((Action)delegate() { }); Assert.IsTrue(runner.WaitForCompletion()); Assert.GreaterOrEqual(busyFlagChangeCount + 2, runner.BusyChangeCount); Assert.IsFalse(runner.IsBusy); } } /// Lets the thread runner run a simple task in the background [Test] public void CanRunSimpleTaskInBackground() { using(var task = new DummyTask(0)) { using(var runner = new DummyRunner()) { runner.RunInBackground(new Action(task.Run)); Assert.IsTrue(runner.WaitForCompletion()); Assert.IsNull(runner.ReportedError); } Assert.AreEqual(1, task.ExecutionCount); Assert.IsFalse(task.WasCancelled); } } /// /// Checks that the thread runner is able to pass a single argument to a task /// [Test] public void CanPassSingleArgumentToSimpleTask() { using(var task = new DummyTask(0)) { using(var runner = new DummyRunner()) { runner.RunInBackground(new Action(task.Run), 12.43f); Assert.IsTrue(runner.WaitForCompletion()); Assert.IsNull(runner.ReportedError); } Assert.AreEqual(1, task.ExecutionCount); Assert.AreEqual(12.43f, task.FirstArgument); Assert.IsFalse(task.WasCancelled); } } /// /// Checks that the thread runner is able to pass two arguments to a task /// [Test] public void CanPassTwoArgumentsToSimpleTask() { using(var task = new DummyTask(0)) { using(var runner = new DummyRunner()) { runner.RunInBackground(new Action(task.Run), 98.67f, "Hello"); Assert.IsTrue(runner.WaitForCompletion()); Assert.IsNull(runner.ReportedError); } Assert.AreEqual(1, task.ExecutionCount); Assert.AreEqual(98.67f, task.FirstArgument); Assert.AreEqual("Hello", task.SecondArgument); Assert.IsFalse(task.WasCancelled); } } /// /// Verifies that an error happening in a simple task is reported correctly /// [Test] public void SimpleTaskErrorIsReported() { using(var task = new DummyTask(0)) { var error = new InvalidOperationException("Mooh!"); task.FailWith(error); using(var runner = new DummyRunner()) { runner.RunInBackground(new Action(task.Run)); Assert.IsTrue(runner.WaitForCompletion()); Assert.AreSame(error, runner.ReportedError); } Assert.AreEqual(1, task.ExecutionCount); Assert.IsFalse(task.WasCancelled); } } /// Lets the thread runner run a cancellable task in the background [Test] public void CanRunCancellableTaskInBackground() { using(var task = new DummyTask(100)) { using(var runner = new DummyRunner()) { runner.RunInBackground(new CancellableAction(task.RunCancellable)); Assert.IsTrue(task.WaitForStart()); runner.CancelAllBackgroundOperations(); Assert.IsTrue(runner.WaitForCompletion()); Assert.IsNull(runner.ReportedError); } Assert.AreEqual(1, task.ExecutionCount); Assert.IsTrue(task.WasCancelled); } } /// /// Checks that the thread runner is able to pass a single argument to a task /// that can be cancelled /// [Test] public void CanPassSingleArgumentToCancellableTask() { using(var task = new DummyTask(100)) { using(var runner = new DummyRunner()) { runner.RunInBackground(new CancellableAction(task.RunCancellable), 12.43f); Assert.IsTrue(task.WaitForStart()); runner.CancelAllBackgroundOperations(); Assert.IsTrue(runner.WaitForCompletion()); Assert.IsNull(runner.ReportedError); } Assert.AreEqual(1, task.ExecutionCount); Assert.AreEqual(12.43f, task.FirstArgument); Assert.IsTrue(task.WasCancelled); } } /// /// Checks that the thread runner is able to pass two arguments to a task /// that can be cancelled /// [Test] public void CanPassTwoArgumentsToCancellableTask() { using(var task = new DummyTask(100)) { using(var runner = new DummyRunner()) { runner.RunInBackground( new CancellableAction(task.RunCancellable), 98.67f, "Hello" ); Assert.IsTrue(task.WaitForStart()); runner.CancelAllBackgroundOperations(); Assert.IsTrue(runner.WaitForCompletion()); Assert.IsNull(runner.ReportedError); } Assert.AreEqual(1, task.ExecutionCount); Assert.AreEqual(98.67f, task.FirstArgument); Assert.AreEqual("Hello", task.SecondArgument); Assert.IsTrue(task.WasCancelled); } } } } // namespace Nuclex.Support.Threading #endif // UNITTEST #endif // !NO_CONCURRENT_COLLECTIONS