Moved all unit test files into a separate directory in preparation for splitting the project

This commit is contained in:
Markus Ewald 2024-07-24 13:27:29 +02:00 committed by cygon
parent 28b96fd557
commit ba5234f701
58 changed files with 0 additions and 853 deletions

View file

@ -0,0 +1,355 @@
#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 UNITTEST
using System;
using System.Collections.Generic;
using System.Threading;
using NUnit.Framework;
namespace Nuclex.Support.Threading {
/// <summary>Unit Test for the CPU core-affine thread pool</summary>
[TestFixture]
internal class AffineThreadPoolTest {
#region class TestTask
/// <summary>ThreadPool task that can be used for testing</summary>
private class TestTask : IDisposable {
/// <summary>Initializes a new test task</summary>
public TestTask() {
this.callbackEvent = new ManualResetEvent(false);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public void Dispose() {
if(this.callbackEvent != null) {
this.callbackEvent.Close();
this.callbackEvent = null;
}
}
/// <summary>Callback that can be added to the thread pool as a task</summary>
/// <param name="state">User defined state</param>
public void Callback(object state) {
this.LastCallbackState = state;
this.callbackEvent.Set();
}
/// <summary>Event that will be set when the callback is executed</summary>
public ManualResetEvent CallbackEvent {
get { return this.callbackEvent; }
}
/// <summary>
/// State parameter that was provide when the callback was called
/// </summary>
public volatile object LastCallbackState;
/// <summary>Event that will be set when the callback is invoked</summary>
private ManualResetEvent callbackEvent;
}
#endregion // class TestTask
#region class WaitTask
/// <summary>ThreadPool task that can be used for testing</summary>
private class WaitTask : IDisposable {
/// <summary>Initializes a new test task</summary>
public WaitTask() {
this.startEvent = new ManualResetEvent(false);
this.finishEvent = new ManualResetEvent(false);
this.waitEvent = new ManualResetEvent(false);
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public void Dispose() {
if(this.waitEvent != null) {
this.waitEvent.Close();
this.waitEvent = null;
}
if(this.finishEvent != null) {
this.finishEvent.Close();
this.finishEvent = null;
}
if(this.startEvent != null) {
this.startEvent.Close();
this.startEvent = null;
}
}
/// <summary>Callback that can be added to the thread pool as a task</summary>
/// <param name="state">User defined state</param>
public void Callback(object state) {
this.LastCallbackState = state;
this.startEvent.Set();
this.waitEvent.WaitOne();
this.finishEvent.Set();
}
/// <summary>Event that will be set when the callback has started</summary>
public ManualResetEvent StartEvent {
get { return this.startEvent; }
}
/// <summary>Event that will be set when the callback has finished</summary>
public ManualResetEvent FinishEvent {
get { return this.finishEvent; }
}
/// <summary>Event that blocks the callback</summary>
public ManualResetEvent WaitEvent {
get { return this.waitEvent; }
}
/// <summary>
/// State parameter that was provide when the callback was called
/// </summary>
public volatile object LastCallbackState;
/// <summary>Event that will be set when the callback has started</summary>
private ManualResetEvent startEvent;
/// <summary>Event that will be set when the callback has finished</summary>
private ManualResetEvent finishEvent;
/// <summary>Event used to block the callback</summary>
private ManualResetEvent waitEvent;
}
#endregion // class WaitTask
#if false
#region class ThrowingDisposable
/// <summary>Throws an exception when it is disposed</summary>
private class ThrowingDisposable : IDisposable {
/// <summary>Immediately releases all resources owned by the instance</summary>
public void Dispose() {
throw new ArithmeticException("Simulated exception for unit testing");
}
}
#endregion // class ThrowingDisposable
/// <summary>
/// Verifies that the Thread Pool's default assertion handler is working
/// </summary>
[Test]
public void TestDefaultAssertionHandler() {
// We can't test a failing assertion because our tests need to run
// unattended on a build server without blocking for user input.
AffineThreadPool.DefaultAssertionHandler(
true, "Unit test", "This should not fail"
);
}
#endif
/// <summary>Tests whether the QueueUserWorkItem() method is working</summary>
[Test]
public void TestQueueUserWorkItem() {
using(TestTask task = new TestTask()) {
AffineThreadPool.QueueUserWorkItem(task.Callback);
Assert.IsTrue(task.CallbackEvent.WaitOne(1000));
}
}
/// <summary>
/// Verifies that the QueueUserWorkItem() method is passing the state parameter
/// on to the callback
/// </summary>
[Test]
public void TestQueueUserWorkItemWithState() {
using(TestTask task = new TestTask()) {
object state = new object();
AffineThreadPool.QueueUserWorkItem(task.Callback, state);
Assert.IsTrue(task.CallbackEvent.WaitOne(1000));
Assert.AreSame(state, task.LastCallbackState);
}
}
/// <summary>
/// Tests whether the thread pool can handle an exception from a user work item
/// </summary>
[Test]
public void TestExceptionFromUserWorkItem() {
using(ManualResetEvent exceptionEvent = new ManualResetEvent(false)) {
AffineThreadPool.ExceptionDelegate oldExceptionHandler =
AffineThreadPool.ExceptionHandler;
AffineThreadPool.ExceptionHandler = delegate(Exception exception) {
exceptionEvent.Set();
};
try {
AffineThreadPool.QueueUserWorkItem(
delegate(object state) { throw new KeyNotFoundException(); }
);
Assert.IsTrue(exceptionEvent.WaitOne(1000));
}
finally {
AffineThreadPool.ExceptionHandler = oldExceptionHandler;
}
}
}
/// <summary>
/// Verifies that the affine thread pool's maximum thread count equals
/// the number of logical processors in the system
/// </summary>
[Test]
public void TestMaxThreadsProperty() {
Assert.AreEqual(Environment.ProcessorCount, AffineThreadPool.MaxThreads);
}
#if WINDOWS
/// <summary>
/// Verifies that the ProcessThread instance for a system thread id can
/// be determined using the GetProcessThread() method
/// </summary>
[Test]
public void CanGetProcessThreadForManagedThread() {
if(Environment.OSVersion.Platform == PlatformID.Win32NT) {
Thread.BeginThreadAffinity();
try {
int threadId = AffineThreadPool.GetCurrentThreadId();
Assert.IsNotNull(AffineThreadPool.GetProcessThread(threadId));
Assert.IsNull(AffineThreadPool.GetProcessThread(0));
}
finally {
Thread.EndThreadAffinity();
}
}
}
#endif // WINDOWS
/// <summary>
/// Tests whether the afine thread pool's default exception handler works
/// as expected
/// </summary>
[Test]
public void TestDefaultExceptionHandler() {
Assert.Throws<ArrayTypeMismatchException>(
delegate() {
AffineThreadPool.ExceptionHandler(new ArrayTypeMismatchException("Test"));
}
);
}
/// <summary>
/// Verifies that the waiting work items count and active thread count are
/// updated by the thread pool.
/// </summary>
[Test]
public void TestWaitingWorkItemsProperty() {
int eventCount = AffineThreadPool.Processors;
WaitTask[] tasks = new WaitTask[eventCount];
int createdTasks = 0;
try {
// CHECK: Is there danger that the thread pool still has not finished
// queued items for other unit tests, thereby failing to meet
// our expected task counts?
// Create the tasks, counting up the created task counter. If an exception
// occurs, we will roll back from there.
for(createdTasks = 0; createdTasks < eventCount; ++createdTasks) {
tasks[createdTasks] = new WaitTask();
}
// Schedule the blocking tasks in the thread pool so it will not be able
// to process the next task we add to the queue
for(int index = 0; index < eventCount; ++index) {
AffineThreadPool.QueueUserWorkItem(tasks[index].Callback);
}
// Wait for the tasks to start so they aren't preempted by the tasks we're
// going to add (which would finish immediately). The affine thread pool
// works on a first come first serve basis, but we don't want to rely on this
// implementation detail in the unit test.
for(int index = 0; index < eventCount; ++index) {
Assert.IsTrue(
tasks[index].StartEvent.WaitOne(10000),
"Task " + index.ToString() + " was started"
);
}
// All Thread should now be active and no work items should be waiting
Assert.AreEqual(
createdTasks, AffineThreadPool.ActiveThreads,
"ActiveThreads property equals number of tasks"
);
Assert.AreEqual(
0, AffineThreadPool.WaitingWorkItems,
"No waiting work items are in the queue"
);
// Add a task to the queue and make sure the waiting work item count goes up
AffineThreadPool.QueueUserWorkItem(delegate(object state) { });
Assert.AreEqual(
1, AffineThreadPool.WaitingWorkItems,
"Added work item is waiting in the queue"
);
// The same again. Now we should have 2 work items sitting in the queue
AffineThreadPool.QueueUserWorkItem(delegate(object state) { });
Assert.AreEqual(
2, AffineThreadPool.WaitingWorkItems,
"Both added work items are waiting in the queue"
);
// Let the WaitTasks finish so we're not blocking the thread pool any longer
for(int index = 0; index < eventCount; ++index) {
tasks[index].WaitEvent.Set();
}
// Wait for the tasks to end before we get rid of them
for(int index = 0; index < eventCount; ++index) {
Assert.IsTrue(
tasks[index].FinishEvent.WaitOne(1000),
"Task " + index.ToString() + " has finished"
);
}
}
finally {
for(--createdTasks; createdTasks >= 0; --createdTasks) {
tasks[createdTasks].Dispose();
}
}
}
}
} // namespace Nuclex.Support.Threading
#endif // UNITTEST

View file

@ -0,0 +1,257 @@
#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;
#if UNITTEST
using NUnit.Framework;
namespace Nuclex.Support.Threading {
/// <summary>Unit Test for the parallel background worker class</summary>
[TestFixture]
internal class ParallelBackgroundWorkerTest {
#region class TestWorker
/// <summary>Implementation of a background worker used for unit testing</summary>
#pragma warning disable 0618
private class TestWorker : ParallelBackgroundWorker<object> {
#pragma warning restore 0618
/// <summary>Initializes a new parallel background worker with unlimited threads</summary>
public TestWorker() : base() { }
/// <summary>
/// Initializes a new parallel background worker running the specified number
/// of tasks in parallel
/// </summary>
/// <param name="threadCount">
/// Number of tasks to run in parallel (if positive) or number of CPU cores to leave
/// unused (if negative).
/// </param>
/// <remarks>
/// If a negative number of threads is used, at least one thread will be always
/// be created, so specifying -2 on a single-core system will still occupy
/// the only core.
/// </remarks>
public TestWorker(int threadCount) : base(threadCount) { }
/// <summary>
/// Initializes a new parallel background worker that uses the specified name for
/// its worker threads.
/// </summary>
/// <param name="name">Name that will be assigned to the worker threads</param>
public TestWorker(string name) : base(name) { }
/// <summary>
/// Initializes a new parallel background worker that uses the specified name for
/// its worker threads and running the specified number of tasks in parallel.
/// </summary>
/// <param name="name">Name that will be assigned to the worker threads</param>
/// <param name="threadCount">
/// Number of tasks to run in parallel (if positive) or number of CPU cores to leave
/// unused (if negative).
/// </param>
/// <remarks>
/// If a negative number of threads is used, at least one thread will be always
/// be created, so specifying -2 on a single-core system will still occupy
/// the only core.
/// </remarks>
public TestWorker(string name, int threadCount) : base(name, threadCount) { }
/// <summary>Called in a thread to execute a single task</summary>
/// <param name="task">Task that should be executed</param>
/// <param name="cancellationToken">
/// Cancellation token through which the method can be signalled to cancel
/// </param>
protected override void Run(object task, CancellationToken cancellationToken) {
if(this.ThrowException) {
throw new Exception("Something went wrong");
}
if(this.WaitEvent != null) {
this.WaitEvent.WaitOne();
}
this.WasCancelled = cancellationToken.IsCancellationRequested;
if(this.Tasks != null) {
lock(this.Tasks) {
this.Tasks.Add(task);
}
}
}
/// <summary>Whether the work tasks should throw exceptions</summary>
public bool ThrowException;
/// <summary>Event that can be used to stop work tasks from completing</summary>
public ManualResetEvent WaitEvent;
/// <summary>Set by work tasks if they have been cancelled</summary>
public bool WasCancelled;
/// <summary>Work tasks that have reached execution</summary>
public ICollection<object> Tasks;
}
#endregion // class TestWorker
/// <summary>Verifies that the background worker has a default constructor</summary>
[Test]
public void CanBeDefaultConstructed() {
using(new TestWorker()) { }
}
/// <summary>
/// Verifies that a background worker can be constructed that uses a fixed number
/// of threads
/// </summary>
[Test]
public void CanUseFixedNumberOfThreads() {
using(new TestWorker(4)) { }
}
/// <summary>
/// Verifies that a background worker can be constructed that leaves free a fixed
/// number of CPU cores
/// </summary>
[Test]
public void CanPreserveFixedNumberOfCores() {
using(new TestWorker(-2)) { }
}
/// <summary>
/// Verifies that a background worker can be constructed using a specific name
/// for its worker threads
/// </summary>
[Test]
public void CanUseNamedThreads() {
using(new TestWorker("Test Task Thread")) { }
}
/// <summary>
/// Verifies that a background worker can be constructed that uses a fixed number
/// of threads using a specific name
/// </summary>
[Test]
public void CanUseFixedNumberOfNamedThreads() {
using(new TestWorker("Test Task Thread", 4)) { }
}
/// <summary>
/// Verifies that a background worker can be constructed that leaves free a fixed
/// number of CPU cores and uses a specific name for its worker threads.
/// </summary>
[Test]
public void CanPreserveFixedNumberOfCoresAndUseNamedThreads() {
using(new TestWorker("Test Task Thread", -2)) { }
}
/// <summary>
/// Verifies that exceptions happening inside the tasks are collected and re-thrown
/// in the Join() method.
/// </summary>
[Test]
public void ExceptionsAreReThrownInJoin() {
using(var testWorker = new TestWorker()) {
testWorker.ThrowException = true;
testWorker.AddTask(new object());
testWorker.AddTask(new object());
Assert.Throws<AggregateException>(
() => {
testWorker.Join();
}
);
try {
testWorker.Join();
Assert.Fail(
"Calling ParallelBackgroundWorker.Join() multiple times should re-throw " +
"exceptions multiple times"
);
}
catch(AggregateException aggregateException) {
Assert.AreEqual(2, aggregateException.InnerExceptions.Count);
}
}
}
/// <summary>
/// Verifies that tasks can be cancelled while they are running
/// </summary>
[Test]
public void TasksCanBeCancelled() {
using(var waitEvent = new ManualResetEvent(false)) {
using(var testWorker = new TestWorker()) {
testWorker.WaitEvent = waitEvent;
testWorker.AddTask(new object());
testWorker.CancelRunningTasks();
waitEvent.Set();
Assert.IsTrue(testWorker.Wait(1000));
Assert.IsTrue(testWorker.WasCancelled);
}
} // disposes waitEvent
}
/// <summary>Verifies that calling Join() waits for all queued tasks</summary>
[Test]
public void JoinWaitsForQueuedTasks() {
var tasks = new List<object>(100);
for(int index = 0; index < 100; ++index) {
tasks.Add(new object());
}
using(var waitEvent = new ManualResetEvent(false)) {
using(var testWorker = new TestWorker(2)) {
testWorker.WaitEvent = waitEvent;
testWorker.Tasks = new List<object>();
for(int index = 0; index < 100; ++index) {
testWorker.AddTask(tasks[index]);
}
CollectionAssert.IsEmpty(testWorker.Tasks);
waitEvent.Set();
testWorker.Join();
lock(testWorker.Tasks) {
CollectionAssert.AreEquivalent(tasks, testWorker.Tasks);
}
}
} // disposes waitEvent
}
}
} // namespace Nuclex.Support.Threading
#endif // UNITTEST
#endif // !NO_CONCURRENT_COLLECTIONS

View file

@ -0,0 +1,459 @@
#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;
#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