-
Notifications
You must be signed in to change notification settings - Fork 757
/
FakeTimeProvider.cs
277 lines (243 loc) · 9.2 KB
/
FakeTimeProvider.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// 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.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.Time.Testing;
/// <summary>
/// Represents a synthetic time provider that can be used to enable deterministic behavior in tests.
/// </summary>
public class FakeTimeProvider : TimeProvider
{
internal readonly HashSet<Waiter> Waiters = [];
private DateTimeOffset _now = new(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero);
private TimeZoneInfo _localTimeZone = TimeZoneInfo.Utc;
private volatile int _wakeWaitersGate;
private TimeSpan _autoAdvanceAmount;
/// <summary>
/// Initializes a new instance of the <see cref="FakeTimeProvider"/> class.
/// </summary>
/// <remarks>
/// This creates a provider whose time is initially set to midnight January 1st 2000.
/// The provider is set to not automatically advance time each time it is read.
/// </remarks>
public FakeTimeProvider()
{
Start = _now;
}
/// <summary>
/// Initializes a new instance of the <see cref="FakeTimeProvider"/> class.
/// </summary>
/// <param name="startDateTime">The initial time and date reported by the provider.</param>
/// <remarks>
/// The provider is set to not automatically advance time each time it is read.
/// </remarks>
public FakeTimeProvider(DateTimeOffset startDateTime)
{
_ = Throw.IfLessThan(startDateTime.Ticks, 0);
_now = startDateTime;
Start = _now;
}
/// <summary>
/// Gets the starting date and time for this provider.
/// </summary>
public DateTimeOffset Start { get; }
/// <summary>
/// Gets or sets the amount of time by which time advances whenever the clock is read.
/// </summary>
/// <remarks>
/// This defaults to <see cref="TimeSpan.Zero"/>.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">if the time value is set to less than <see cref="TimeSpan.Zero"/>.</exception>
public TimeSpan AutoAdvanceAmount
{
get => _autoAdvanceAmount;
set
{
_ = Throw.IfLessThan(value.Ticks, 0);
_autoAdvanceAmount = value;
}
}
/// <inheritdoc />
public override DateTimeOffset GetUtcNow()
{
DateTimeOffset result;
lock (Waiters)
{
result = _now;
_now += _autoAdvanceAmount;
}
WakeWaiters();
return result;
}
/// <summary>
/// Sets the date and time in the UTC time zone.
/// </summary>
/// <param name="value">The date and time in the UTC time zone.</param>
/// <exception cref="ArgumentOutOfRangeException">if the supplied time value is before the current time.</exception>
public void SetUtcNow(DateTimeOffset value)
{
lock (Waiters)
{
if (value < _now)
{
Throw.ArgumentOutOfRangeException(nameof(value), $"Cannot go back in time. Current time is {_now}.");
}
_now = value;
}
WakeWaiters();
}
/// <summary>
/// Advances time by a specific amount.
/// </summary>
/// <param name="delta">The amount of time to advance the clock by.</param>
/// <remarks>
/// Advancing time affects the timers created from this provider, and all other operations that are directly or
/// indirectly using this provider as a time source. Whereas when using <see cref="TimeProvider.System"/>, time
/// marches forward automatically in hardware, for the fake time provider the application is responsible for
/// doing this explicitly by calling this method.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">if the time value is less than <see cref="TimeSpan.Zero"/>.</exception>
public void Advance(TimeSpan delta)
{
_ = Throw.IfLessThan(delta.Ticks, 0);
lock (Waiters)
{
_now += delta;
}
WakeWaiters();
}
/// <inheritdoc />
public override long GetTimestamp()
{
// Notionally we're multiplying by frequency and dividing by ticks per second,
// which are the same value for us. Don't actually do the math as the full
// precision of ticks (a long) cannot be represented in a double during division.
// For test stability we want a reproducible result.
//
// The same issue could occur converting back, in GetElapsedTime(). Unfortunately
// that isn't virtual so we can't do the same trick. However, if tests advance
// the clock in multiples of 1ms or so this loss of precision will not be visible.
Debug.Assert(TimestampFrequency == TimeSpan.TicksPerSecond, "Assuming frequency equals ticks per second");
return _now.Ticks;
}
/// <inheritdoc />
public override TimeZoneInfo LocalTimeZone => _localTimeZone;
/// <summary>
/// Sets the local time zone.
/// </summary>
/// <param name="localTimeZone">The local time zone.</param>
public void SetLocalTimeZone(TimeZoneInfo localTimeZone) => _localTimeZone = Throw.IfNull(localTimeZone);
/// <summary>
/// Gets the amount by which the value from <see cref="GetTimestamp"/> increments per second.
/// </summary>
/// <remarks>
/// This is fixed to the value of <see cref="TimeSpan.TicksPerSecond"/>.
/// </remarks>
public override long TimestampFrequency => TimeSpan.TicksPerSecond;
/// <summary>
/// Returns a string representation this provider's idea of current time.
/// </summary>
/// <returns>A string representing the provider's current time.</returns>
public override string ToString() => _now.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture);
/// <inheritdoc />
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
var timer = new Timer(this, Throw.IfNull(callback), state);
_ = timer.Change(dueTime, period);
return timer;
}
internal void RemoveWaiter(Waiter waiter)
{
lock (Waiters)
{
_ = Waiters.Remove(waiter);
}
}
internal void AddWaiter(Waiter waiter, long dueTime)
{
lock (Waiters)
{
waiter.ScheduledOn = _now.Ticks;
waiter.WakeupTime = _now.Ticks + dueTime;
_ = Waiters.Add(waiter);
}
WakeWaiters();
}
private void WakeWaiters()
{
if (Interlocked.CompareExchange(ref _wakeWaitersGate, 1, 0) == 1)
{
// some other thread is already in here, so let it take care of things
return;
}
while (true)
{
Waiter? candidate = null;
lock (Waiters)
{
// find an expired waiter
foreach (var waiter in Waiters)
{
if (waiter.WakeupTime > _now.Ticks)
{
// not expired yet
}
else if (candidate is null)
{
// our first candidate
candidate = waiter;
}
else if (waiter.WakeupTime < candidate.WakeupTime)
{
// found a waiter with an earlier wake time, it's our new candidate
candidate = waiter;
}
else if (waiter.WakeupTime > candidate.WakeupTime)
{
// the waiter has a later wake time, so keep the current candidate
}
else if (waiter.ScheduledOn < candidate.ScheduledOn)
{
// the new waiter has the same wake time aa the candidate, pick whichever was scheduled earliest to maintain order
candidate = waiter;
}
}
}
if (candidate == null)
{
// didn't find a candidate to wake, we're done
_wakeWaitersGate = 0;
return;
}
var oldTicks = _now.Ticks;
// invoke the callback
candidate.InvokeCallback();
var newTicks = _now.Ticks;
// see if we need to reschedule the waiter
if (candidate.Period > 0)
{
// update the waiter's state
candidate.ScheduledOn = newTicks;
if (oldTicks != newTicks)
{
// time changed while in the callback, readjust the wake time accordingly
candidate.WakeupTime = newTicks + candidate.Period;
}
else
{
// move on to the next period
candidate.WakeupTime += candidate.Period;
}
}
else
{
// this waiter is never running again, so remove from the set.
RemoveWaiter(candidate);
}
}
}
}