Moved all unit test files into a separate directory in preparation for splitting the project
This commit is contained in:
parent
28b96fd557
commit
ba5234f701
58 changed files with 0 additions and 853 deletions
355
Tests/Threading/AffineThreadPoolTest.cs
Normal file
355
Tests/Threading/AffineThreadPoolTest.cs
Normal 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
|
257
Tests/Threading/ParallelBackgroundWorkerTest.cs
Normal file
257
Tests/Threading/ParallelBackgroundWorkerTest.cs
Normal 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
|
459
Tests/Threading/ThreadRunnerTest.cs
Normal file
459
Tests/Threading/ThreadRunnerTest.cs
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue