#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 view model base class [TestFixture] public class ThreadedViewModelTest { #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 TestViewModel /// View model used to unit test the threaded view model base class private class TestViewModel : ThreadedViewModel { /// /// Initializes a new view model, letting the base class figure out the UI thread /// public TestViewModel() : base() { this.finishedGate = new ManualResetEvent(initialState: false); } /// /// Initializes a new view model, using the specified context for the UI thread /// /// Synchronization context of the UI thread public TestViewModel(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); } /// Runs a background process that causes the specified error /// Error that will be caused in the background process public void CauseErrorInBackgroundThread(Exception error) { RunInBackground(() => throw error); } /// /// Assigns the specified value to the same-named property from a background thread /// /// Value that will be assigned to the same-named property public void AssignValueInBackgroundThread(int value) { RunInBackground( delegate () { this.assignedValue = value; this.finishedGate.Set(); } ); } /// 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; } } /// 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(); } /// 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 TestViewModel /// Verifies that the threaded view model 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 TestViewModel()) { } } finally { mainForm.Close(); } } } /// /// Verifies that the threaded view model can be constructed with a custom UI context /// [Test] public void HasCustomSychronizationContextConstructor() { using(new TestViewModel(new DummyContext())) { } } /// Checks that a new view model starts out idle and not busy [Test] public void NewInstanceIsNotBusy() { using(var viewModel = new TestViewModel(new DummyContext())) { Assert.IsFalse(viewModel.IsBusy); } } /// /// Verifies that errors happening in the background processing threads are /// reported to the main thread /// [Test] public void ErrorsInBackgroundThreadAreReported() { using(var viewModel = new TestViewModel(new DummyContext())) { var testError = new ArgumentException("Mooh"); viewModel.CauseErrorInBackgroundThread(testError); viewModel.WaitUntilFinished(); Assert.AreSame(testError, viewModel.ReportedError); } } /// /// Verifies that the background thread actually executes and can do work /// [Test] public void BackgroundThreadExecutesTasks() { using(var viewModel = new TestViewModel(new DummyContext())) { viewModel.AssignValueInBackgroundThread(10042); viewModel.WaitUntilFinished(); Assert.AreEqual(10042, viewModel.AssignedValue); } } } } // namespace Nuclex.Windows.Forms.ViewModels #endif // UNITTEST