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