#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