2f82a2fdf9
git-svn-id: file:///srv/devel/repo-conversion/nusu@332 d2e56fa2-650e-0410-a79f-9358c0239efd
461 lines
16 KiB
C#
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
|