#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