Nuclex.Support/Source/Threading/ThreadRunner.Test.cs

461 lines
16 KiB
C#

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