#region CPL License /* Nuclex Framework Copyright (C) 2002-2019 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.ComponentModel; using System.Threading; using NUnit.Framework; namespace Nuclex.Windows.Forms.ViewModels { /// Unit test for the threaded action class [TestFixture] public class ThreadedActionTest { #region class DummyContext /// Synchronization context that does absolutely nothing private class DummyContext : ISynchronizeInvoke { #region class SimpleAsyncResult /// Barebones implementation of an asynchronous result private class SimpleAsyncResult : IAsyncResult { /// Ehether the asynchronous operation is complete /// /// Always true because it completes synchronously /// public bool IsCompleted { get { return true; } } /// /// Wait handle that can be used to wait for the asynchronous operation /// public WaitHandle AsyncWaitHandle { get { throw new NotImplementedException("Not implemented"); } } /// Custom state that can be used to pass information around public object AsyncState { get { throw new NotImplementedException("Not implemented"); } } /// Whether the asynchronous operation completed synchronously public bool CompletedSynchronously { get { return true; } } /// The value returned from the asynchronous operation public object ReturnedValue; } #endregion // class SimpleAsyncResult /// Whether the calling thread needs to use Invoke() public bool InvokeRequired { get { return true; } } /// Schedules the specified method for execution in the target thread /// Method the target thread will execute when it is idle /// Arguments that will be passed to the method /// /// An asynchronous result handle that can be used to check on the status of /// the call and wait for its completion /// public IAsyncResult BeginInvoke(Delegate method, object[] arguments) { var asyncResult = new SimpleAsyncResult(); asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments); return asyncResult; } /// Waits for the asychronous call to complete /// /// Asynchronous result handle returned by the method /// /// The original result returned by the asychronously called method public object EndInvoke(IAsyncResult result) { return ((SimpleAsyncResult)result).ReturnedValue; } /// /// Schedules the specified method for execution in the target thread and waits /// for it to complete /// /// Method that will be executed by the target thread /// Arguments that will be passed to the method /// The result returned by the specified method public object Invoke(Delegate method, object[] arguments) { return method.Method.Invoke(method.Target, arguments); } } #endregion // class DummyContext #region class DummyThreadedAction /// Implementation of a threaded action for the unit test private class DummyThreadedAction : ThreadedAction { /// /// Initializes a new threaded action, letting the base class figure out the UI thread /// public DummyThreadedAction() : base() { this.finishedGate = new ManualResetEvent(initialState: false); } /// /// Initializes a new view model using the specified UI context explicitly /// public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) { this.finishedGate = new ManualResetEvent(initialState: false); } /// Immediately releases all resources owned by the instance public override void Dispose() { base.Dispose(); if(this.finishedGate != null) { this.finishedGate.Dispose(); this.finishedGate = null; } } /// Waits until the first background operation is finished /// /// True if the background operation is finished, false if it is ongoing /// public bool WaitUntilFinished() { return this.finishedGate.WaitOne(100); } /// Selects the value that will be assigned when the action runs /// Value the action will assigned when it runs public void SetValueToAssign(int valueToAssign) { this.valueToAssign = valueToAssign; } /// Sets up an error the action will fail with when run /// Error the action will fail with public void SetErrorToFailWith(Exception errorToFailWith) { this.errorToFailWith = errorToFailWith; } /// Last error that was reported by the threaded view model public Exception ReportedError { get { return this.reportedError; } } /// Value that has been assigned from the background thread public int AssignedValue { get { return this.assignedValue; } } /// Executes the threaded action from the background thread /// Token by which execution can be canceled protected override void Run(CancellationToken cancellationToken) { if(this.errorToFailWith != null) { throw this.errorToFailWith; } this.assignedValue = this.valueToAssign; this.finishedGate.Set(); } /// Called when an error occurs in the background thread /// Exception that was thrown in the background thread protected override void ReportError(Exception exception) { this.reportedError = exception; this.finishedGate.Set(); } /// Error the action will fail with, if set private Exception errorToFailWith; /// Value the action will assign to its same-named field private int valueToAssign; /// Last error that was reported by the threaded view model private volatile Exception reportedError; /// Triggered when the private ManualResetEvent finishedGate; /// Value that is assigned through the background thread private volatile int assignedValue; } #endregion // class DummyThreadedAction /// Verifies that the threaded action has a default constructor [Test, Explicit] public void HasDefaultConstructor() { using(var mainForm = new System.Windows.Forms.Form()) { mainForm.Show(); try { mainForm.Visible = false; using(new DummyThreadedAction()) { } } finally { mainForm.Close(); } } } /// /// Verifies that the threaded action can be constructed with a custom UI context /// [Test] public void HasCustomSychronizationContextConstructor() { using(new DummyThreadedAction(new DummyContext())) { } } /// Checks that a new threadd action starts out idle and not busy [Test] public void NewInstanceIsNotBusy() { using(var action = new DummyThreadedAction(new DummyContext())) { Assert.IsFalse(action.IsBusy); } } /// /// Verifies that errors happening in the background processing threads are /// reported to the main thread /// [Test] public void ErrorsInBackgroundThreadAreReported() { using(var action = new DummyThreadedAction(new DummyContext())) { var testError = new ArgumentException("Mooh"); action.SetErrorToFailWith(testError); action.Start(); action.WaitUntilFinished(); Assert.AreSame(testError, action.ReportedError); } } /// /// Verifies that the background thread actually executes and can do work /// [Test] public void BackgroundThreadExecutesTasks() { using(var action = new DummyThreadedAction(new DummyContext())) { action.SetValueToAssign(42001); action.Start(); action.WaitUntilFinished(); Assert.AreEqual(42001, action.AssignedValue); } } } } // namespace Nuclex.Windows.Forms.ViewModels #endif // UNITTEST