Skip to content

Commit 15a9751

Browse files
committed
fix: ITimer returned by ManualTestProvider such that timers created with a due time equal to zero will fire the timer callback immediately.
1 parent b8a52ca commit 15a9751

File tree

4 files changed

+596
-9
lines changed

4 files changed

+596
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ All notable changes to TimeScheduler will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
9-
## [1.0.0-preview.2]
8+
## [1.0.0-preview.3]
109

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

1412
- Changed `ManualTestProvider.ToString()` method to return current date time.
13+
14+
- Fixed `ITimer` returned by `ManualTestProvider` such that timers created with a due time equal to zero will fire the timer callback immediately.
15+
1516
## [1.0.0-preview.1]
1617

1718
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.

src/TimeProviderExtensions/ManualTimeProvider.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics.CodeAnalysis;
4+
using System.Globalization;
45
using System.Linq;
56
using System.Runtime.CompilerServices;
67
using System.Text;
@@ -21,6 +22,7 @@ public class ManualTimeProvider : TimeProvider
2122
{
2223
internal const uint MaxSupportedTimeout = 0xfffffffe;
2324
internal const uint UnsignedInfinite = unchecked((uint)-1);
25+
internal static readonly DateTimeOffset Epoch = new(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
2426

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

4143
/// <summary>
4244
/// Creates an instance of the <see cref="ManualTimeProvider"/> with
43-
/// <see cref="DateTimeOffset.UtcNow"/> being the initial value returned by <see cref="GetUtcNow()"/>.
45+
/// <c>UtcNow</c> set to <c>2000-01-01 00:00:00.000</c>.
4446
/// </summary>
45-
public ManualTimeProvider()
46-
: this(System.GetUtcNow())
4747
/// <param name="localTimeZone">Optional local time zone to use during testing. Defaults to <see cref="TimeZoneInfo.Utc"/>.</param>
4848
public ManualTimeProvider(TimeZoneInfo? localTimeZone = null)
4949
: this(Epoch)
5050
{
51-
5251
this.localTimeZone = localTimeZone ?? TimeZoneInfo.Utc;
5352
}
5453

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

227226
if (insertPosition == -1)
227+
{
228228
futureCallbacks.Add(mtsc);
229+
}
229230
else
230231
{
231232
futureCallbacks.Insert(insertPosition, mtsc);
@@ -341,7 +342,7 @@ internal void TimerElapsed()
341342

342343
callback?.Invoke(state);
343344

344-
if (currentPeriod != Timeout.InfiniteTimeSpan)
345+
if (currentPeriod != Timeout.InfiniteTimeSpan && currentPeriod != TimeSpan.Zero)
345346
ScheduleCallback(currentPeriod);
346347
}
347348

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

353354
running = true;
354-
owner.ScheduleCallback(this, waitTime);
355+
356+
if (waitTime == TimeSpan.Zero)
357+
{
358+
TimerElapsed();
359+
}
360+
else
361+
{
362+
owner.ScheduleCallback(this, waitTime);
363+
}
355364
}
356365

357366
private static void ValidateTimeSpanRange(TimeSpan time, [CallerArgumentExpression("time")] string? parameter = null)
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
#if TargetMicrosoftTestTimeProvider
2+
using SutTimeProvider = Microsoft.Extensions.Time.Testing.FakeTimeProvider;
3+
#else
4+
using SutTimeProvider = TimeProviderExtensions.ManualTimeProvider;
5+
#endif
6+
7+
// Licensed to the .NET Foundation under one or more agreements.
8+
// The .NET Foundation licenses this file to you under the MIT license.
9+
10+
using System;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Microsoft.Extensions.Time.Testing;
14+
using Xunit;
15+
16+
namespace Microsoft.Extensions.Time.Testing.Test;
17+
18+
public class FakeTimeProviderTests
19+
{
20+
[Fact]
21+
public void DefaultCtor()
22+
{
23+
var timeProvider = new SutTimeProvider();
24+
25+
var now = timeProvider.GetUtcNow();
26+
var timestamp = timeProvider.GetTimestamp();
27+
var frequency = timeProvider.TimestampFrequency;
28+
29+
Assert.Equal(2000, now.Year);
30+
Assert.Equal(1, now.Month);
31+
Assert.Equal(1, now.Day);
32+
Assert.Equal(0, now.Hour);
33+
Assert.Equal(0, now.Minute);
34+
Assert.Equal(0, now.Second);
35+
Assert.Equal(0, now.Millisecond);
36+
Assert.Equal(TimeSpan.Zero, now.Offset);
37+
Assert.Equal(10_000_000, frequency);
38+
39+
var timestamp2 = timeProvider.GetTimestamp();
40+
var frequency2 = timeProvider.TimestampFrequency;
41+
now = timeProvider.GetUtcNow();
42+
43+
Assert.Equal(2000, now.Year);
44+
Assert.Equal(1, now.Month);
45+
Assert.Equal(1, now.Day);
46+
Assert.Equal(0, now.Hour);
47+
Assert.Equal(0, now.Minute);
48+
Assert.Equal(0, now.Second);
49+
Assert.Equal(0, now.Millisecond);
50+
Assert.Equal(10_000_000, frequency2);
51+
Assert.Equal(timestamp2, timestamp);
52+
}
53+
54+
[Fact]
55+
public void RichCtor()
56+
{
57+
var timeProvider = new SutTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero));
58+
59+
timeProvider.Advance(TimeSpan.FromMilliseconds(8));
60+
var pnow = timeProvider.GetTimestamp();
61+
var frequency = timeProvider.TimestampFrequency;
62+
var now = timeProvider.GetUtcNow();
63+
64+
Assert.Equal(2001, now.Year);
65+
Assert.Equal(2, now.Month);
66+
Assert.Equal(3, now.Day);
67+
Assert.Equal(4, now.Hour);
68+
Assert.Equal(5, now.Minute);
69+
Assert.Equal(6, now.Second);
70+
Assert.Equal(TimeSpan.Zero, now.Offset);
71+
Assert.Equal(8, now.Millisecond);
72+
Assert.Equal(10_000_000, frequency);
73+
74+
timeProvider.Advance(TimeSpan.FromMilliseconds(8));
75+
var pnow2 = timeProvider.GetTimestamp();
76+
var frequency2 = timeProvider.TimestampFrequency;
77+
now = timeProvider.GetUtcNow();
78+
79+
Assert.Equal(2001, now.Year);
80+
Assert.Equal(2, now.Month);
81+
Assert.Equal(3, now.Day);
82+
Assert.Equal(4, now.Hour);
83+
Assert.Equal(5, now.Minute);
84+
Assert.Equal(6, now.Second);
85+
Assert.Equal(16, now.Millisecond);
86+
Assert.Equal(10_000_000, frequency2);
87+
Assert.True(pnow2 > pnow);
88+
}
89+
90+
[Fact]
91+
public void LocalTimeZoneIsUtc()
92+
{
93+
var timeProvider = new SutTimeProvider();
94+
var localTimeZone = timeProvider.LocalTimeZone;
95+
96+
Assert.Equal(TimeZoneInfo.Utc, localTimeZone);
97+
}
98+
99+
[Fact]
100+
public void GetTimestampSyncWithUtcNow()
101+
{
102+
var timeProvider = new SutTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero));
103+
104+
var initialTimeUtcNow = timeProvider.GetUtcNow();
105+
var initialTimestamp = timeProvider.GetTimestamp();
106+
107+
timeProvider.SetUtcNow(timeProvider.GetUtcNow().AddMilliseconds(1234));
108+
109+
var finalTimeUtcNow = timeProvider.GetUtcNow();
110+
var finalTimeTimestamp = timeProvider.GetTimestamp();
111+
112+
var utcDelta = finalTimeUtcNow - initialTimeUtcNow;
113+
var perfDelta = finalTimeTimestamp - initialTimestamp;
114+
var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp);
115+
116+
Assert.Equal(1, utcDelta.Seconds);
117+
Assert.Equal(234, utcDelta.Milliseconds);
118+
Assert.Equal(1234D, utcDelta.TotalMilliseconds);
119+
Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3);
120+
Assert.Equal(1234, elapsedTime.TotalMilliseconds);
121+
}
122+
123+
[Fact]
124+
public void AdvanceGoesForward()
125+
{
126+
var timeProvider = new SutTimeProvider(new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.Zero));
127+
128+
var initialTimeUtcNow = timeProvider.GetUtcNow();
129+
var initialTimestamp = timeProvider.GetTimestamp();
130+
131+
timeProvider.Advance(TimeSpan.FromMilliseconds(1234));
132+
133+
var finalTimeUtcNow = timeProvider.GetUtcNow();
134+
var finalTimeTimestamp = timeProvider.GetTimestamp();
135+
136+
var utcDelta = finalTimeUtcNow - initialTimeUtcNow;
137+
var perfDelta = finalTimeTimestamp - initialTimestamp;
138+
var elapsedTime = timeProvider.GetElapsedTime(initialTimestamp, finalTimeTimestamp);
139+
140+
Assert.Equal(1, utcDelta.Seconds);
141+
Assert.Equal(234, utcDelta.Milliseconds);
142+
Assert.Equal(1234D, utcDelta.TotalMilliseconds);
143+
Assert.Equal(1.234D, (double)perfDelta / timeProvider.TimestampFrequency, 3);
144+
Assert.Equal(1234, elapsedTime.TotalMilliseconds);
145+
}
146+
147+
[Fact]
148+
public void ToStr()
149+
{
150+
var dto = new DateTimeOffset(new DateTime(2022, 1, 2, 3, 4, 5, 6), TimeSpan.Zero);
151+
152+
var timeProvider = new SutTimeProvider(dto);
153+
Assert.Equal("2022-01-02T03:04:05.006", timeProvider.ToString());
154+
}
155+
156+
private readonly TimeSpan _infiniteTimeout = TimeSpan.FromMilliseconds(-1);
157+
158+
[Fact]
159+
public void Delay_InvalidArgs()
160+
{
161+
var timeProvider = new SutTimeProvider();
162+
_ = Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => timeProvider.Delay(TimeSpan.FromTicks(-1), CancellationToken.None));
163+
_ = Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => timeProvider.Delay(_infiniteTimeout, CancellationToken.None));
164+
}
165+
166+
[Fact]
167+
public async Task Delay_Zero()
168+
{
169+
var timeProvider = new SutTimeProvider();
170+
var t = timeProvider.Delay(TimeSpan.Zero, CancellationToken.None);
171+
await t;
172+
173+
Assert.True(t.IsCompleted && !t.IsFaulted);
174+
}
175+
176+
[Fact]
177+
public async Task Delay_Timeout()
178+
{
179+
var timeProvider = new SutTimeProvider();
180+
181+
var delay = timeProvider.Delay(TimeSpan.FromMilliseconds(1), CancellationToken.None);
182+
timeProvider.Advance();
183+
await delay;
184+
185+
Assert.True(delay.IsCompleted);
186+
Assert.False(delay.IsFaulted);
187+
Assert.False(delay.IsCanceled);
188+
}
189+
190+
[Fact]
191+
public async Task Delay_Cancelled()
192+
{
193+
var timeProvider = new SutTimeProvider();
194+
195+
using var cs = new CancellationTokenSource();
196+
var delay = timeProvider.Delay(_infiniteTimeout, cs.Token);
197+
Assert.False(delay.IsCompleted);
198+
199+
cs.Cancel();
200+
201+
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
202+
await Assert.ThrowsAsync<TaskCanceledException>(async () => await delay);
203+
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
204+
}
205+
206+
[Fact]
207+
public async Task CreateSource()
208+
{
209+
var timeProvider = new SutTimeProvider();
210+
211+
using var cts = timeProvider.CreateCancellationTokenSource(TimeSpan.FromMilliseconds(1));
212+
timeProvider.Advance();
213+
214+
await Assert.ThrowsAsync<TaskCanceledException>(() => timeProvider.Delay(TimeSpan.FromTicks(1), cts.Token));
215+
}
216+
217+
[Fact]
218+
public async Task WaitAsync()
219+
{
220+
var timeProvider = new SutTimeProvider();
221+
var source = new TaskCompletionSource<bool>();
222+
223+
#if NET8_0_OR_GREATER
224+
await Assert.ThrowsAsync<TimeoutException>(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None));
225+
#else
226+
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => source.Task.WaitAsync(TimeSpan.FromTicks(-1), timeProvider, CancellationToken.None));
227+
#endif
228+
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => source.Task.WaitAsync(TimeSpan.FromMilliseconds(-2), timeProvider, CancellationToken.None));
229+
230+
var t = source.Task.WaitAsync(TimeSpan.FromSeconds(100000), timeProvider, CancellationToken.None);
231+
while (!t.IsCompleted)
232+
{
233+
timeProvider.Advance();
234+
await Task.Delay(1);
235+
_ = source.TrySetResult(true);
236+
}
237+
238+
Assert.True(t.IsCompleted);
239+
Assert.False(t.IsFaulted);
240+
Assert.False(t.IsCanceled);
241+
}
242+
243+
[Fact]
244+
public async Task WaitAsync_InfiniteTimeout()
245+
{
246+
var timeProvider = new SutTimeProvider();
247+
var source = new TaskCompletionSource<bool>();
248+
249+
var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, CancellationToken.None);
250+
while (!t.IsCompleted)
251+
{
252+
timeProvider.Advance();
253+
await Task.Delay(1);
254+
_ = source.TrySetResult(true);
255+
}
256+
257+
Assert.True(t.IsCompleted);
258+
Assert.False(t.IsFaulted);
259+
Assert.False(t.IsCanceled);
260+
}
261+
262+
[Fact]
263+
public async Task WaitAsync_Timeout()
264+
{
265+
var timeProvider = new SutTimeProvider();
266+
var source = new TaskCompletionSource<bool>();
267+
268+
var t = source.Task.WaitAsync(TimeSpan.FromMilliseconds(1), timeProvider, CancellationToken.None);
269+
while (!t.IsCompleted)
270+
{
271+
timeProvider.Advance();
272+
await Task.Delay(1);
273+
}
274+
275+
Assert.True(t.IsCompleted);
276+
Assert.True(t.IsFaulted);
277+
Assert.False(t.IsCanceled);
278+
}
279+
280+
[Fact]
281+
public async Task WaitAsync_Cancel()
282+
{
283+
var timeProvider = new SutTimeProvider();
284+
var source = new TaskCompletionSource<bool>();
285+
using var cts = new CancellationTokenSource();
286+
287+
var t = source.Task.WaitAsync(_infiniteTimeout, timeProvider, cts.Token);
288+
cts.Cancel();
289+
290+
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
291+
await Assert.ThrowsAsync<TaskCanceledException>(() => t).ConfigureAwait(false);
292+
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
293+
}
294+
}

0 commit comments

Comments
 (0)