#region CPL License /* Nuclex Framework Copyright (C) 2002-2013 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 using System; using System.Threading; using System.Collections.Generic; #if UNITTEST using NUnit.Framework; namespace Nuclex.Support { /// 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 #endif // UNITTEST