#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 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 UNITTEST using System; using System.Collections.Generic; using System.Threading; using NUnit.Framework; namespace Nuclex.Support { /// Unit Test for the CPU core-affine thread pool [TestFixture] public class AffineThreadPoolTest { #region class TestTask /// ThreadPool task that can be used for testing private class TestTask : IDisposable { /// Initializes a new test task public TestTask() { this.callbackEvent = new ManualResetEvent(false); } /// Immediately releases all resources owned by the instance public void Dispose() { if(this.callbackEvent != null) { this.callbackEvent.Close(); this.callbackEvent = null; } } /// Callback that can be added to the thread pool as a task /// User defined state public void Callback(object state) { this.LastCallbackState = state; this.callbackEvent.Set(); } /// Event that will be set when the callback is executed public ManualResetEvent CallbackEvent { get { return this.callbackEvent; } } /// /// State parameter that was provide when the callback was called /// public volatile object LastCallbackState; /// Event that will be set when the callback is invoked private ManualResetEvent callbackEvent; } #endregion // class TestTask #region class WaitTask /// ThreadPool task that can be used for testing private class WaitTask : IDisposable { /// Initializes a new test task public WaitTask() { this.startEvent = new ManualResetEvent(false); this.finishEvent = new ManualResetEvent(false); this.waitEvent = new ManualResetEvent(false); } /// Immediately releases all resources owned by the instance 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; } } /// Callback that can be added to the thread pool as a task /// User defined state public void Callback(object state) { this.LastCallbackState = state; this.startEvent.Set(); this.waitEvent.WaitOne(); this.finishEvent.Set(); } /// Event that will be set when the callback has started public ManualResetEvent StartEvent { get { return this.startEvent; } } /// Event that will be set when the callback has finished public ManualResetEvent FinishEvent { get { return this.finishEvent; } } /// Event that blocks the callback public ManualResetEvent WaitEvent { get { return this.waitEvent; } } /// /// State parameter that was provide when the callback was called /// public volatile object LastCallbackState; /// Event that will be set when the callback has started private ManualResetEvent startEvent; /// Event that will be set when the callback has finished private ManualResetEvent finishEvent; /// Event used to block the callback private ManualResetEvent waitEvent; } #endregion // class WaitTask #region class ThrowingDisposable /// Throws an exception when it is disposed private class ThrowingDisposable : IDisposable { /// Immediately releases all resources owned by the instance public void Dispose() { throw new ArithmeticException("Simulated exception for unit testing"); } } #endregion // class ThrowingDisposable /// /// Verifies that the Thread Pool's default assertion handler is working /// [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" ); } /// Tests whether the QueueUserWorkItem() method is working [Test] public void TestQueueUserWorkItem() { using(TestTask task = new TestTask()) { AffineThreadPool.QueueUserWorkItem(task.Callback); Assert.IsTrue(task.CallbackEvent.WaitOne(1000)); } } /// /// Verifies that the QueueUserWorkItem() method is passing the state parameter /// on to the callback /// [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); } } /// /// Tests whether the thread pool can handle an exception from a user work item /// [Test] public void TestExceptionFromUserWorkItem() { using(ManualResetEvent assertEvent = new ManualResetEvent(false)) { AffineThreadPool.AssertionDelegate oldAssertionHandler = AffineThreadPool.AssertionHandler; AffineThreadPool.AssertionHandler = delegate( bool condition, string message, string details ) { assertEvent.Set(); }; try { AffineThreadPool.QueueUserWorkItem( delegate(object state) { throw new KeyNotFoundException(); } ); Assert.IsTrue(assertEvent.WaitOne(1000)); } finally { AffineThreadPool.AssertionHandler = oldAssertionHandler; } } } /// /// Verifies that the affine thread pool's maximum thread count equals /// the number of logical processors in the system /// [Test] public void TestMaxThreadsProperty() { Assert.AreEqual(Environment.ProcessorCount, AffineThreadPool.MaxThreads); } /// /// Tests whether the thread pool can handle an exception from a user work item /// [Test] public void TestExceptionFromDisposableState() { using(ManualResetEvent assertEvent = new ManualResetEvent(false)) { AffineThreadPool.AssertionDelegate oldAssertionHandler = AffineThreadPool.AssertionHandler; AffineThreadPool.AssertionHandler = delegate( bool condition, string message, string details ) { assertEvent.Set(); }; try { int eventCount = AffineThreadPool.CpuCores; WaitTask[] tasks = new WaitTask[eventCount]; int createdTasks = 0; try { // 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 aborted by EmptyQueue() for(int index = 0; index < eventCount; ++index) { Assert.IsTrue(tasks[index].StartEvent.WaitOne(1000)); } Assert.AreEqual(createdTasks, AffineThreadPool.ActiveThreads); Assert.AreEqual(0, AffineThreadPool.WaitingCallbacks); // Add a task to the queue whose state implements a faulty IDisposable AffineThreadPool.QueueUserWorkItem( delegate(object state) { }, new ThrowingDisposable() ); Assert.AreEqual(1, AffineThreadPool.WaitingCallbacks); // Now clear the thread pool. This should cause the faulty IDisposable // to be disposed and then throw its exception. AffineThreadPool.EmptyQueue(); // Make sure our custom assertion handler has been triggered Assert.IsTrue(assertEvent.WaitOne(1000)); Assert.AreEqual(createdTasks, AffineThreadPool.ActiveThreads); Assert.AreEqual(0, AffineThreadPool.WaitingCallbacks); // Let the thread pool finish its active tasks for(int index = 0; index < eventCount; ++index) { tasks[index].WaitEvent.Set(); } // Wait for the tasks to end before we dispose them for(int index = 0; index < eventCount; ++index) { Assert.IsTrue(tasks[index].FinishEvent.WaitOne(1000)); } } finally { for(--createdTasks; createdTasks >= 0; --createdTasks) { tasks[createdTasks].Dispose(); } } } finally { AffineThreadPool.AssertionHandler = oldAssertionHandler; } } // using } } } // namespace Nuclex.Support #endif // UNITTEST