#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if !NO_CONCURRENT_COLLECTIONS
using System;
using System.Threading;
using System.Collections.Generic;
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 // !NO_CONCURRENT_COLLECTIONS