#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 {
/// Unit Test for the parallel background worker class
[TestFixture]
internal class ParallelBackgroundWorkerTest {
#region class TestWorker
/// Implementation of a background worker used for unit testing
private class TestWorker : ParallelBackgroundWorker {
/// Initializes a new parallel background worker with unlimited threads
public TestWorker() : base() { }
///
/// Initializes a new parallel background worker running the specified number
/// of tasks in parallel
///
///
/// Number of tasks to run in parallel (if positive) or number of CPU cores to leave
/// unused (if negative).
///
///
/// 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.
///
public TestWorker(int threadCount) : base(threadCount) { }
///
/// Initializes a new parallel background worker that uses the specified name for
/// its worker threads.
///
/// Name that will be assigned to the worker threads
public TestWorker(string name) : base(name) { }
///
/// Initializes a new parallel background worker that uses the specified name for
/// its worker threads and running the specified number of tasks in parallel.
///
/// Name that will be assigned to the worker threads
///
/// Number of tasks to run in parallel (if positive) or number of CPU cores to leave
/// unused (if negative).
///
///
/// 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.
///
public TestWorker(string name, int threadCount) : base(name, threadCount) { }
/// Called in a thread to execute a single task
/// Task that should be executed
///
/// Cancellation token through which the method can be signalled to cancel
///
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);
}
}
}
/// Whether the work tasks should throw exceptions
public bool ThrowException;
/// Event that can be used to stop work tasks from completing
public ManualResetEvent WaitEvent;
/// Set by work tasks if they have been cancelled
public bool WasCancelled;
/// Work tasks that have reached execution
public ICollection Tasks;
}
#endregion // class TestWorker
/// Verifies that the background worker has a default constructor
[Test]
public void CanBeDefaultConstructed() {
using(new TestWorker()) { }
}
///
/// Verifies that a background worker can be constructed that uses a fixed number
/// of threads
///
[Test]
public void CanUseFixedNumberOfThreads() {
using(new TestWorker(4)) { }
}
///
/// Verifies that a background worker can be constructed that leaves free a fixed
/// number of CPU cores
///
[Test]
public void CanPreserveFixedNumberOfCores() {
using(new TestWorker(-2)) { }
}
///
/// Verifies that a background worker can be constructed using a specific name
/// for its worker threads
///
[Test]
public void CanUseNamedThreads() {
using(new TestWorker("Test Task Thread")) { }
}
///
/// Verifies that a background worker can be constructed that uses a fixed number
/// of threads using a specific name
///
[Test]
public void CanUseFixedNumberOfNamedThreads() {
using(new TestWorker("Test Task Thread", 4)) { }
}
///
/// 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.
///
[Test]
public void CanPreserveFixedNumberOfCoresAndUseNamedThreads() {
using(new TestWorker("Test Task Thread", -2)) { }
}
///
/// Verifies that exceptions happening inside the tasks are collected and re-thrown
/// in the Join() method.
///
[Test]
public void ExceptionsAreReThrownInJoin() {
using(var testWorker = new TestWorker()) {
testWorker.ThrowException = true;
testWorker.AddTask(new object());
testWorker.AddTask(new object());
Assert.Throws(
() => {
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);
}
}
}
///
/// Verifies that tasks can be cancelled while they are running
///
[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
}
/// Verifies that calling Join() waits for all queued tasks
[Test]
public void JoinWaitsForQueuedTasks() {
var tasks = new List(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();
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