Skip to content

Commit 05f53e6

Browse files
committed
feat: add Microsofts FakeTimeProvider implementation
1 parent c85b8a8 commit 05f53e6

12 files changed

+554
-137
lines changed

src/TimeProviderExtensions/ManualTimeProvider.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Linq;
55
using System.Runtime.CompilerServices;
66
using System.Text;
7+
using Microsoft.Extensions.Time.Testing;
8+
using static Microsoft.Extensions.Time.Testing.FakeTimeProviderTimer;
79

810
namespace TimeProviderExtensions;
911

@@ -39,7 +41,7 @@ public class ManualTimeProvider : TimeProvider
3941
public ManualTimeProvider()
4042
: this(System.GetUtcNow())
4143
{
42-
44+
4345
}
4446

4547
/// <summary>
@@ -113,19 +115,37 @@ public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSp
113115

114116
/// <summary>
115117
/// Forward the date and time represented by <see cref="GetUtcNow()"/>
116-
/// by the specified <paramref name="time"/>, and triggers any
118+
/// by the specified <paramref name="delta"/>, and triggers any
117119
/// scheduled items that are waiting for time to be forwarded.
118120
/// </summary>
119-
/// <param name="time">The span of time to forward <see cref="GetUtcNow()"/> with.</param>
120-
/// <exception cref="ArgumentException">If <paramref name="time"/> is negative or zero.</exception>
121-
public void ForwardTime(TimeSpan time)
121+
/// <param name="delta">The span of time to forward <see cref="GetUtcNow()"/> with.</param>
122+
/// <exception cref="ArgumentException">If <paramref name="delta"/> is negative or zero.</exception>
123+
public void ForwardTime(TimeSpan delta)
122124
{
123-
if (time <= TimeSpan.Zero)
124-
throw new ArgumentException("The timespan to forward time by must be positive.", nameof(time));
125+
if (delta <= TimeSpan.Zero)
126+
throw new ArgumentException("The timespan to forward time by must be positive.", nameof(delta));
127+
128+
SetUtcNow(utcNow + delta);
129+
}
125130

126-
SetUtcNow(utcNow + time);
131+
/// <summary>
132+
/// Advance the date and time represented by <see cref="GetUtcNow()"/>
133+
/// by the specified <paramref name="delta"/>, and triggers any
134+
/// scheduled items that are waiting for time to be forwarded.
135+
/// </summary>
136+
/// <param name="delta">The amount of time to advance the clock by.</param>
137+
public void Advance(TimeSpan delta)
138+
{
139+
SetUtcNow(utcNow + delta);
127140
}
128141

142+
/// <summary>
143+
/// Advance the date and time represented by <see cref="GetUtcNow()"/>
144+
/// by one millisecond, and triggers any scheduled items that are waiting for time to be forwarded.
145+
/// </summary>
146+
public void Advance()
147+
=> Advance(TimeSpan.FromMilliseconds(1));
148+
129149
/// <summary>
130150
/// Sets the date and time returned by <see cref="GetUtcNow()"/> to <paramref name="newUtcNew"/> and triggers any
131151
/// scheduled items that are waiting for time to be forwarded.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Globalization;
8+
using System.Threading;
9+
10+
namespace Microsoft.Extensions.Time.Testing;
11+
12+
/// <summary>
13+
/// A synthetic clock used to provide deterministic behavior in tests.
14+
/// </summary>
15+
public class FakeTimeProvider : TimeProvider
16+
{
17+
internal static readonly DateTimeOffset Epoch = new(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
18+
19+
internal readonly List<FakeTimeProviderTimer.Waiter> Waiters = new();
20+
21+
private DateTimeOffset _now = Epoch;
22+
private TimeZoneInfo _localTimeZone;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="FakeTimeProvider"/> class.
26+
/// </summary>
27+
/// <remarks>
28+
/// This creates a clock whose time is set to midnight January 1st 2000.
29+
/// The clock is set to not automatically advance every time it is read.
30+
/// </remarks>
31+
public FakeTimeProvider()
32+
{
33+
_localTimeZone = TimeZoneInfo.Utc;
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="FakeTimeProvider"/> class.
38+
/// </summary>
39+
/// <param name="startTime">The initial time reported by the clock.</param>
40+
public FakeTimeProvider(DateTimeOffset startTime)
41+
: this()
42+
{
43+
_now = startTime;
44+
}
45+
46+
/// <inheritdoc />
47+
public override DateTimeOffset GetUtcNow()
48+
{
49+
return _now;
50+
}
51+
52+
/// <summary>
53+
/// Sets the date and time in the UTC timezone.
54+
/// </summary>
55+
/// <param name="value">The date and time in the UTC timezone.</param>
56+
public void SetUtcNow(DateTimeOffset value)
57+
{
58+
List<FakeTimeProviderTimer.Waiter> waiters;
59+
lock (Waiters)
60+
{
61+
_now = value;
62+
waiters = GetWaitersToWake();
63+
}
64+
65+
WakeWaiters(waiters);
66+
}
67+
68+
/// <summary>
69+
/// Advances the clock's time by a specific amount.
70+
/// </summary>
71+
/// <param name="delta">The amount of time to advance the clock by.</param>
72+
public void Advance(TimeSpan delta)
73+
{
74+
List<FakeTimeProviderTimer.Waiter> waiters;
75+
lock (Waiters)
76+
{
77+
_now += delta;
78+
waiters = GetWaitersToWake();
79+
}
80+
81+
WakeWaiters(waiters);
82+
}
83+
84+
/// <summary>
85+
/// Advances the clock's time by one millisecond.
86+
/// </summary>
87+
public void Advance() => Advance(TimeSpan.FromMilliseconds(1));
88+
89+
/// <inheritdoc />
90+
public override long GetTimestamp()
91+
{
92+
// Notionally we're multiplying by frequency and dividing by ticks per second,
93+
// which are the same value for us. Don't actually do the math as the full
94+
// precision of ticks (a long) cannot be represented in a double during division.
95+
// For test stability we want a reproducible result.
96+
//
97+
// The same issue could occur converting back, in GetElapsedTime(). Unfortunately
98+
// that isn't virtual so we can't do the same trick. However, if tests advance
99+
// the clock in multiples of 1ms or so this loss of precision will not be visible.
100+
Debug.Assert(TimestampFrequency == TimeSpan.TicksPerSecond, "Assuming frequency equals ticks per second");
101+
return _now.Ticks;
102+
}
103+
104+
/// <inheritdoc />
105+
public override TimeZoneInfo LocalTimeZone => _localTimeZone;
106+
107+
/// <summary>
108+
/// Sets the local timezone.
109+
/// </summary>
110+
/// <param name="localTimeZone">The local timezone.</param>
111+
public void SetLocalTimeZone(TimeZoneInfo localTimeZone)
112+
{
113+
_localTimeZone = localTimeZone;
114+
}
115+
116+
/// <summary>
117+
/// Gets the amount that the value from <see cref="GetTimestamp"/> increments per second.
118+
/// </summary>
119+
/// <remarks>
120+
/// We fix it here for test instability which would otherwise occur within
121+
/// <see cref="GetTimestamp"/> when the result of multiplying underlying ticks
122+
/// by frequency and dividing by ticks per second is truncated to long.
123+
///
124+
/// Similarly truncation could occur when reversing this calculation to figure a time
125+
/// interval from the difference between two timestamps.
126+
///
127+
/// As ticks per second is always 10^7, setting frequency to 10^7 is convenient.
128+
/// It happens that the system usually uses 10^9 or 10^6 so this could expose
129+
/// any assumption made that it is one of those values.
130+
/// </remarks>
131+
public override long TimestampFrequency => TimeSpan.TicksPerSecond;
132+
133+
/// <summary>
134+
/// Returns a string representation this clock's current time.
135+
/// </summary>
136+
/// <returns>A string representing the clock's current time.</returns>
137+
public override string ToString() => GetUtcNow().ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture);
138+
139+
/// <inheritdoc />
140+
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
141+
{
142+
return new FakeTimeProviderTimer(this, dueTime, period, callback, state);
143+
}
144+
145+
internal void AddWaiter(FakeTimeProviderTimer.Waiter waiter)
146+
{
147+
lock (Waiters)
148+
{
149+
Waiters.Add(waiter);
150+
}
151+
}
152+
153+
internal void RemoveWaiter(FakeTimeProviderTimer.Waiter waiter)
154+
{
155+
lock (Waiters)
156+
{
157+
_ = Waiters.Remove(waiter);
158+
}
159+
}
160+
161+
private List<FakeTimeProviderTimer.Waiter> GetWaitersToWake()
162+
{
163+
var l = new List<FakeTimeProviderTimer.Waiter>(Waiters.Count);
164+
foreach (var w in Waiters)
165+
{
166+
if (_now >= w.WakeupTime)
167+
{
168+
l.Add(w);
169+
}
170+
}
171+
172+
return l;
173+
}
174+
175+
private void WakeWaiters(List<FakeTimeProviderTimer.Waiter> waiters)
176+
{
177+
foreach (var w in waiters)
178+
{
179+
if (_now >= w.WakeupTime)
180+
{
181+
w.TriggerAndSchedule(false);
182+
}
183+
}
184+
}
185+
}

0 commit comments

Comments
 (0)