Skip to content

Commit

Permalink
fix: ITimer returned by ManualTestProvider such that timers creat…
Browse files Browse the repository at this point in the history
…ed with a due time equal to zero will fire the timer callback immediately.
  • Loading branch information
egil committed May 24, 2023
1 parent b8a52ca commit 15a9751
Show file tree
Hide file tree
Showing 4 changed files with 596 additions and 9 deletions.
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ All notable changes to TimeScheduler will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.0.0-preview.2]
## [1.0.0-preview.3]

- Changed ManualTestProvider sets the local time zone to UTC by default, provides method for overriding during testing.
- Changed `ManualTestProvider` sets the local time zone to UTC by default, provides method for overriding during testing.

- Changed `ManualTestProvider.ToString()` method to return current date time.

- Fixed `ITimer` returned by `ManualTestProvider` such that timers created with a due time equal to zero will fire the timer callback immediately.

## [1.0.0-preview.1]

This release adds a dependency on [Microsoft.Bcl.TimeProvider](https://www.nuget.org/packages/Microsoft.Bcl.TimeProvider) and utilizes the types built-in to that to do much of the work.
Expand Down
21 changes: 15 additions & 6 deletions src/TimeProviderExtensions/ManualTimeProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
Expand All @@ -21,6 +22,7 @@ public class ManualTimeProvider : TimeProvider
{
internal const uint MaxSupportedTimeout = 0xfffffffe;
internal const uint UnsignedInfinite = unchecked((uint)-1);
internal static readonly DateTimeOffset Epoch = new(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);

private readonly List<ManualTimerScheduledCallback> futureCallbacks = new();
private DateTimeOffset utcNow;
Expand All @@ -40,15 +42,12 @@ public class ManualTimeProvider : TimeProvider

/// <summary>
/// Creates an instance of the <see cref="ManualTimeProvider"/> with
/// <see cref="DateTimeOffset.UtcNow"/> being the initial value returned by <see cref="GetUtcNow()"/>.
/// <c>UtcNow</c> set to <c>2000-01-01 00:00:00.000</c>.
/// </summary>
public ManualTimeProvider()
: this(System.GetUtcNow())
/// <param name="localTimeZone">Optional local time zone to use during testing. Defaults to <see cref="TimeZoneInfo.Utc"/>.</param>
public ManualTimeProvider(TimeZoneInfo? localTimeZone = null)
: this(Epoch)
{

this.localTimeZone = localTimeZone ?? TimeZoneInfo.Utc;
}

Expand Down Expand Up @@ -225,7 +224,9 @@ private void ScheduleCallback(ManualTimer timer, TimeSpan waitTime)
var insertPosition = futureCallbacks.FindIndex(x => x.CallbackTime > mtsc.CallbackTime);

if (insertPosition == -1)
{
futureCallbacks.Add(mtsc);
}
else
{
futureCallbacks.Insert(insertPosition, mtsc);
Expand Down Expand Up @@ -341,7 +342,7 @@ internal void TimerElapsed()

callback?.Invoke(state);

if (currentPeriod != Timeout.InfiniteTimeSpan)
if (currentPeriod != Timeout.InfiniteTimeSpan && currentPeriod != TimeSpan.Zero)
ScheduleCallback(currentPeriod);
}

Expand All @@ -351,7 +352,15 @@ private void ScheduleCallback(TimeSpan waitTime)
return;

running = true;
owner.ScheduleCallback(this, waitTime);

if (waitTime == TimeSpan.Zero)
{
TimerElapsed();
}
else
{
owner.ScheduleCallback(this, waitTime);
}
}

private static void ValidateTimeSpanRange(TimeSpan time, [CallerArgumentExpression("time")] string? parameter = null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
#if TargetMicrosoftTestTimeProvider
using SutTimeProvider = Microsoft.Extensions.Time.Testing.FakeTimeProvider;
#else
using SutTimeProvider = TimeProviderExtensions.ManualTimeProvider;
#endif

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Time.Testing;
using Xunit;

namespace Microsoft.Extensions.Time.Testing.Test;

public class FakeTimeProviderTests
{
[Fact]
public void DefaultCtor()
{
var timeProvider = new SutTimeProvider();

var now = timeProvider.GetUtcNow();
var timestamp = timeProvider.GetTimestamp();
var frequency = timeProvider.TimestampFrequency;

Assert.Equal(2000, now.Year);
Assert.Equal(1, now.Month);
Assert.Equal(1, now.Day);
Assert.Equal(0, now.Hour);
Assert.Equal(0, now.Minute);
Assert.Equal(0, now.Second);
Assert.Equal(0, now.Millisecond);
Assert.Equal(TimeSpan.Zero, now.Offset);
Assert.Equal(10_000_000, frequency);

var timestamp2 = timeProvider.GetTimestamp();
var frequency2 = timeProvider.TimestampFrequency;
now = timeProvider.GetUtcNow();

Assert.Equal(2000, now.Year);
Assert.Equal(1, now.Month);
Assert.Equal(1, now.Day);
Assert.Equal(0, now.Hour);
Assert.Equal(0, now.Minute);
Assert.Equal(0, now.Second);
Assert.Equal(0, now.Millisecond);
Assert.Equal(10_000_000, frequency2);
Assert.Equal(timestamp2, timestamp);
}

[Fact]
public void RichCtor()
{
var timeProvider = new SutTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero));

timeProvider.Advance(TimeSpan.FromMilliseconds(8));
var pnow = timeProvider.GetTimestamp();
var frequency = timeProvider.TimestampFrequency;
var now = timeProvider.GetUtcNow();

Assert.Equal(2001, now.Year);
Assert.Equal(2, now.Month);
Assert.Equal(3, now.Day);
Assert.Equal(4, now.Hour);
Assert.Equal(5, now.Minute);
Assert.Equal(6, now.Second);
Assert.Equal(TimeSpan.Zero, now.Offset);
Assert.Equal(8, now.Millisecond);
Assert.Equal(10_000_000, frequency);

timeProvider.Advance(TimeSpan.FromMilliseconds(8));
var pnow2 = timeProvider.GetTimestamp();
var frequency2 = timeProvider.TimestampFrequency;
now = timeProvider.GetUtcNow();

Assert.Equal(2001, now.Year);
Assert.Equal(2, now.Month);
Assert.Equal(3, now.Day);
Assert.Equal(4, now.Hour);
Assert.Equal(5, now.Minute);
Assert.Equal(6, now.Second);
Assert.Equal(16, now.Millisecond);
Assert.Equal(10_000_000, frequency2);
Assert.True(pnow2 > pnow);
}

[Fact]
public void LocalTimeZoneIsUtc()
{
var timeProvider = new SutTimeProvider();
var localTimeZone = timeProvider.LocalTimeZone;

Assert.Equal(TimeZoneInfo.Utc, localTimeZone);
}

[Fact]
public void GetTimestampSyncWithUtcNow()
{
var timeProvider = new SutTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero));

var initialTimeUtcNow = timeProvider.GetUtcNow();
var initialTimestamp = timeProvider.GetTimestamp();

timeProvider.SetUtcNow(timeProvider.GetUtcNow().AddMilliseconds(1234));

var finalTimeUtcNow = timeProvider.GetUtcNow();
var finalTimeTimestamp = timeProvider.GetTimestamp();

var utcDelta = finalTimeUtcNow - initialTimeUtcNow;
var perfDelta = finalTimeTimestamp - initialTimestamp;
var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp);

Assert.Equal(1, utcDelta.Seconds);
Assert.Equal(234, utcDelta.Milliseconds);
Assert.Equal(1234D, utcDelta.TotalMilliseconds);
Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3);
Assert.Equal(1234, elapsedTime.TotalMilliseconds);
}

[Fact]
public void AdvanceGoesForward()
{
var timeProvider = new SutTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero));

var initialTimeUtcNow = timeProvider.GetUtcNow();
var initialTimestamp = timeProvider.GetTimestamp();

timeProvider.Advance(TimeSpan.FromMilliseconds(1234));

var finalTimeUtcNow = timeProvider.GetUtcNow();
var finalTimeTimestamp = timeProvider.GetTimestamp();

var utcDelta = finalTimeUtcNow - initialTimeUtcNow;
var perfDelta = finalTimeTimestamp - initialTimestamp;
var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp);

Assert.Equal(1, utcDelta.Seconds);
Assert.Equal(234, utcDelta.Milliseconds);
Assert.Equal(1234D, utcDelta.TotalMilliseconds);
Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3);
Assert.Equal(1234, elapsedTime.TotalMilliseconds);
}

[Fact]
public void ToStr()
{
var dto = new DateTimeOffset(new DateTime(2022, 1, 2, 3, 4, 5, 6), TimeSpan.Zero);

var timeProvider = new SutTimeProvider(dto);
Assert.Equal("2022-01-02T03:04:05.006", timeProvider.ToString());
}

private readonly TimeSpan _infiniteTimeout = TimeSpan.FromMilliseconds(-1);

[Fact]
public void Delay_InvalidArgs()
{
var timeProvider = new SutTimeProvider();
_ = Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => timeProvider.Delay(TimeSpan.FromTicks(-1), CancellationToken.None));
_ = Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => timeProvider.Delay(_infiniteTimeout, CancellationToken.None));
}

[Fact]
public async Task Delay_Zero()
{
var timeProvider = new SutTimeProvider();
var t = timeProvider.Delay(TimeSpan.Zero, CancellationToken.None);
await t;

Assert.True(t.IsCompleted && !t.IsFaulted);
}

[Fact]
public async Task Delay_Timeout()
{
var timeProvider = new SutTimeProvider();

var delay = timeProvider.Delay(TimeSpan.FromMilliseconds(1), CancellationToken.None);
timeProvider.Advance();
await delay;

Assert.True(delay.IsCompleted);
Assert.False(delay.IsFaulted);
Assert.False(delay.IsCanceled);
}

[Fact]
public async Task Delay_Cancelled()
{
var timeProvider = new SutTimeProvider();

using var cs = new CancellationTokenSource();
var delay = timeProvider.Delay(_infiniteTimeout, cs.Token);
Assert.False(delay.IsCompleted);

cs.Cancel();

#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
await Assert.ThrowsAsync<TaskCanceledException>(async () => await delay);
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
}

[Fact]
public async Task CreateSource()
{
var timeProvider = new SutTimeProvider();

using var cts = timeProvider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1));
timeProvider.Advance();

await Assert.ThrowsAsync<TaskCanceledException>(() => timeProvider.Delay(TimeSpan.FromTicks(1), cts.Token));
}

[Fact]
public async Task WaitAsync()
{
var timeProvider = new SutTimeProvider();
var source = new TaskCompletionSource<bool>();

#if NET8_0_OR_GREATER
await Assert.ThrowsAsync<TimeoutException>(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None));
#else
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None));
#endif
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => source.Task.WaitAsync(TimeSpan.FromMilliseconds(-2), timeProvider, CancellationToken.None));

var t = source.Task.WaitAsync(TimeSpan.FromSeconds(100000), timeProvider, CancellationToken.None);
while (!t.IsCompleted)
{
timeProvider.Advance();
await Task.Delay(1);
_ = source.TrySetResult(true);
}

Assert.True(t.IsCompleted);
Assert.False(t.IsFaulted);
Assert.False(t.IsCanceled);
}

[Fact]
public async Task WaitAsync_InfiniteTimeout()
{
var timeProvider = new SutTimeProvider();
var source = new TaskCompletionSource<bool>();

var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, CancellationToken.None);
while (!t.IsCompleted)
{
timeProvider.Advance();
await Task.Delay(1);
_ = source.TrySetResult(true);
}

Assert.True(t.IsCompleted);
Assert.False(t.IsFaulted);
Assert.False(t.IsCanceled);
}

[Fact]
public async Task WaitAsync_Timeout()
{
var timeProvider = new SutTimeProvider();
var source = new TaskCompletionSource<bool>();

var t = source.Task.WaitAsync(TimeSpan.FromMilliseconds(1), timeProvider, CancellationToken.None);
while (!t.IsCompleted)
{
timeProvider.Advance();
await Task.Delay(1);
}

Assert.True(t.IsCompleted);
Assert.True(t.IsFaulted);
Assert.False(t.IsCanceled);
}

[Fact]
public async Task WaitAsync_Cancel()
{
var timeProvider = new SutTimeProvider();
var source = new TaskCompletionSource<bool>();
using var cts = new CancellationTokenSource();

var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, cts.Token);
cts.Cancel();

#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
await Assert.ThrowsAsync<TaskCanceledException>(() => t).ConfigureAwait(false);
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
}
}
Loading

0 comments on commit 15a9751

Please sign in to comment.