#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 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.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32;
using NUnit.Framework;
namespace Nuclex.Support.Scheduling {
/// Unit Test for the scheduler
[TestFixture]
public class SchedulerTest {
#region class MockTimeSource
/// Mocked time source
private class MockTimeSource : ITimeSource {
/// Called when the system date/time are adjusted
public event EventHandler DateTimeAdjusted;
/// Initializes a new mocked time source
/// Start time in UTC format
public MockTimeSource(DateTime utcStartTime) {
this.currentTime = utcStartTime;
this.currentTicks = 1000000000;
}
/// Waits for an AutoResetEvent to become signalled
/// WaitHandle the method will wait for
/// Number of ticks to wait
///
/// True if the WaitHandle was signalled, false if the timeout was reached
/// or the time source thinks its time to recheck the system date/time.
///
public bool WaitOne(AutoResetEvent waitHandle, long ticks) {
long currentTicks;
long eventDueTicks;
lock(this) {
this.autoResetEvent = waitHandle;
this.eventDueTicks += ticks;
currentTicks = this.currentTicks;
eventDueTicks = this.eventDueTicks;
}
// If we need to wait, use the wait handle. We do not use the wait handle's
// return value (or even its timeout) because we might trigger it ourselves
// to simulate the passing of time.
if(eventDueTicks > 0) {
this.autoResetEvent = waitHandle;
waitHandle.WaitOne();
this.autoResetEvent = null;
}
// Do not use the cached values here -- we might have used the WaitHandle and
// the simulation time could have been advanced while we were waiting.
lock(this) {
return (this.eventDueTicks > 0); // True = signalled, false = timed out
}
}
/// Current system time in UTC format
public DateTime CurrentUtcTime {
get { lock(this) { return this.currentTime; } }
}
/// How long the time source has been running
public long Ticks {
get {
lock(this) {
this.eventDueTicks = 0;
return this.currentTicks;
}
}
}
/// Advances the time of the time source
///
/// Time span by which to advance the time source's time
///
public void AdvanceTime(TimeSpan timeSpan) {
lock(this) {
this.currentTicks += timeSpan.Ticks;
this.currentTime += timeSpan;
// Problem: The Scheduler has just calculated the remaining ticks until
// notification occurs. Next, another thread advances simulation time
// and then the scheduler calls this. It will wait, even though
// the simultion time has progressed.
// To compensate this, we track the remaining time until the event is due
// and allow for a negative time budget if AdvanceTime() is called after
// the scheduler has just queried the current tick count.
this.eventDueTicks -= timeSpan.Ticks;
if(this.eventDueTicks <= 0) {
AutoResetEvent copy = this.autoResetEvent;
if(copy != null) {
copy.Set();
}
}
}
}
/// Manually triggers the date time adjusted event
/// New simulation time to jump to
public void AdjustTime(DateTime newUtcTime) {
lock(this) {
this.currentTime = newUtcTime;
}
EventHandler copy = DateTimeAdjusted;
if(copy != null) {
copy(this, EventArgs.Empty);
}
}
/// Auto reset event the time source is currently waiting on
private volatile AutoResetEvent autoResetEvent;
/// Ticks at which the auto reset event will be due
private long eventDueTicks;
/// Current time source tick counter
private long currentTicks;
/// Current system time and date
private DateTime currentTime;
}
#endregion // class MockTimeSource
#region class TestSubscriber
/// Subscriber used to test the scheduler notifications
private class TestSubscriber : IDisposable {
/// Initializes a new test subscriber
public TestSubscriber() {
this.waitHandle = new AutoResetEvent(false);
}
/// Immediately releases all resources owned by the instance
public void Dispose() {
if(this.waitHandle != null) {
this.waitHandle.Close();
this.waitHandle = null;
}
}
/// Callback method that can be subscribed to the scheduler
/// Not used
public void Callback(object state) {
Interlocked.Increment(ref this.callbackCount);
this.waitHandle.Set();
}
/// Blocks ther caller until the callback is invoked
///
/// Maximum number of milliseconds to wait for the callback
///
/// True if the callback was invoked, false if the call timed out
public bool WaitForCallback(int milliseconds) {
return this.waitHandle.WaitOne(milliseconds);
}
/// Number of times the callback has been invoked
public int CallbackCount {
get { return Thread.VolatileRead(ref this.callbackCount); }
}
/// Callback invocation count
private int callbackCount;
/// WaitHandle used to wait for the callback
private AutoResetEvent waitHandle;
}
#endregion // class TestSubscriber
///
/// Test whether the Scheduler can explicitely create a windows time source
///
[Test]
public void TestCreateWindowsTimeSource() {
ITimeSource timeSource = Scheduler.CreateTimeSource(true);
try {
Assert.That(timeSource is WindowsTimeSource);
}
finally {
IDisposable disposableTimeSource = timeSource as IDisposable;
if(disposableTimeSource != null) {
disposableTimeSource.Dispose();
}
}
}
///
/// Test whether the Scheduler can explicitely create a generic time source
///
[Test]
public void TestCreateGenericTimeSource() {
ITimeSource timeSource = Scheduler.CreateTimeSource(false);
try {
Assert.That(timeSource is GenericTimeSource);
}
finally {
IDisposable disposableTimeSource = timeSource as IDisposable;
if(disposableTimeSource != null) {
disposableTimeSource.Dispose();
}
}
}
///
/// Test whether the Scheduler can automatically choose the right time source
///
[Test]
public void TestCreateDefaultTimeSource() {
ITimeSource timeSource = Scheduler.CreateDefaultTimeSource();
try {
Assert.IsNotNull(timeSource);
}
finally {
IDisposable disposableTimeSource = timeSource as IDisposable;
if(disposableTimeSource != null) {
disposableTimeSource.Dispose();
}
}
}
///
/// Verifies that the default constructor of the scheduler is working
///
[Test]
public void TestDefaultConstructor() {
using(Scheduler scheduler = new Scheduler()) { }
}
///
/// Verifies that the default constructor of the scheduler is working
///
[Test]
public void TestThrowOnNotifyAtWithUnspecifiedDateTimeKind() {
using(TestSubscriber subscriber = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler()) {
Assert.Throws(
delegate() {
scheduler.NotifyAt(new DateTime(2000, 1, 1), subscriber.Callback);
}
);
}
}
}
///
/// Tests whether the NotifyAt() method invokes the callback at the right time
///
[Test]
public void TestNotifyAt() {
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
using(TestSubscriber subscriber = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
scheduler.NotifyAt(makeUtc(new DateTime(2010, 1, 2)), subscriber.Callback);
mockTimeSource.AdvanceTime(TimeSpan.FromDays(1));
Assert.IsTrue(subscriber.WaitForCallback(1000));
}
}
}
///
/// Verifies that a notification at an absolute time is processed correctly
/// if a time synchronization occurs during the wait.
///
[Test]
public void TestNotifyAtWithDateTimeAdjustment() {
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
using(TestSubscriber subscriber = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
scheduler.NotifyAt(makeUtc(new DateTime(2010, 1, 2)), subscriber.Callback);
// Let 12 hours pass, after that, we simulate a time synchronization
// that puts the system 12 hours ahead of the original time.
mockTimeSource.AdvanceTime(TimeSpan.FromHours(12));
mockTimeSource.AdjustTime(new DateTime(2010, 1, 2));
Assert.IsTrue(subscriber.WaitForCallback(1000));
}
}
}
/// Tests whether the scheduler's Cancel() method is working
[Test]
public void TestCancelNotification() {
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
using(TestSubscriber subscriber1 = new TestSubscriber()) {
using(TestSubscriber subscriber2 = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
object handle = scheduler.NotifyIn(
TimeSpan.FromHours(24), subscriber1.Callback
);
scheduler.NotifyIn(TimeSpan.FromHours(36), subscriber2.Callback);
mockTimeSource.AdvanceTime(TimeSpan.FromHours(12));
scheduler.Cancel(handle);
mockTimeSource.AdvanceTime(TimeSpan.FromHours(24));
// Wait for the second subscriber to be notified. This is still a race
// condition (there's no guarantee the thread pool will run tasks in
// the order they were added), but it's the best we can do without
// relying on an ugly Thread.Sleep() in this test.
Assert.IsTrue(subscriber2.WaitForCallback(1000));
Assert.AreEqual(0, subscriber1.CallbackCount);
}
}
}
}
///
/// Tests the scheduler with two notifications that are scheduled in inverse
/// order of their due time.
///
[Test]
public void TestInverseOrderNotification() {
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
using(TestSubscriber subscriber1 = new TestSubscriber()) {
using(TestSubscriber subscriber2 = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
scheduler.NotifyIn(TimeSpan.FromHours(24), subscriber1.Callback);
scheduler.NotifyIn(TimeSpan.FromHours(12), subscriber2.Callback);
mockTimeSource.AdvanceTime(TimeSpan.FromHours(18));
Assert.IsTrue(subscriber2.WaitForCallback(1000));
Assert.AreEqual(0, subscriber1.CallbackCount);
mockTimeSource.AdvanceTime(TimeSpan.FromHours(18));
Assert.IsTrue(subscriber1.WaitForCallback(1000));
}
}
}
}
///
/// Tests the scheduler with two notifications that are scheduled to
/// occur at the exact same time
///
[Test]
public void TestTwoNotificationsAtSameTime() {
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
using(TestSubscriber subscriber1 = new TestSubscriber()) {
using(TestSubscriber subscriber2 = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
scheduler.NotifyIn(60000, subscriber1.Callback);
scheduler.NotifyIn(60000, subscriber2.Callback);
mockTimeSource.AdvanceTime(TimeSpan.FromMilliseconds(60000));
Assert.IsTrue(subscriber1.WaitForCallback(1000));
Assert.IsTrue(subscriber2.WaitForCallback(1000));
}
}
}
}
///
/// Verifies that the scheduler's NotifyEach() method is working correctly
///
[Test]
public void TestNotifyEachWithMilliseconds() {
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
using(TestSubscriber subscriber = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
scheduler.NotifyEach(1000, 1000, subscriber.Callback);
mockTimeSource.AdvanceTime(TimeSpan.FromMilliseconds(4000));
// Wait for 4 invocations of the callback. We might not catch each trigger
// of the AutoResetEvent, but we know that it will be 4 at most.
for(int invocation = 0; invocation < 4; ++invocation) {
Assert.IsTrue(subscriber.WaitForCallback(1000));
if(subscriber.CallbackCount == 4) {
break;
}
}
}
}
}
///
/// Verifies that the scheduler's NotifyEach() method is working correctly
///
[Test]
public void TestNotifyEachWithTimespan() {
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
using(TestSubscriber subscriber = new TestSubscriber()) {
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
scheduler.NotifyEach(
TimeSpan.FromHours(12), TimeSpan.FromHours(1), subscriber.Callback
);
mockTimeSource.AdvanceTime(TimeSpan.FromHours(14));
// Wait for 3 invocations of the callback. We might not catch each trigger
// of the AutoResetEvent, but we know that it will be 3 at most.
for(int invocation = 0; invocation < 3; ++invocation) {
Assert.IsTrue(subscriber.WaitForCallback(1000));
if(subscriber.CallbackCount == 3) {
break;
}
}
}
}
}
/// Returns the provided date/time value as a utc time value
/// Date/time value that will be returned as UTC
/// The provided date/time value as UTC
///
/// This doesn't convert the time, it just returns the exact same date and time
/// but tags it as UTC by setting the DateTimeKind to UTC.
///
private static DateTime makeUtc(DateTime dateTime) {
return new DateTime(dateTime.Ticks, DateTimeKind.Utc);
}
}
} // namespace Nuclex.Support.Scheduling
#endif // UNITTEST