#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