#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#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