Skip to content

Commit 42f0628

Browse files
committed
feat: implement time jumping with Jump
1 parent 66c4c54 commit 42f0628

File tree

9 files changed

+365
-29
lines changed

9 files changed

+365
-29
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ During testing, you can move time forward by calling `Advance(TimeSpan)` or `Set
1010

1111
The .NET team has published a similar test-specific time provider, the [`Microsoft.Extensions.Time.Testing.FakeTimeProvider`](https://www.nuget.org/packages/Microsoft.Extensions.Time.Testing.FakeTimeProvider/).
1212

13-
The public API of both `FakeTimeProvider` and `ManualTimeProvider` are identical, but there are some differences in when time is set before timer callbacks. Let's illustrate this with an example:
13+
The public API of both `FakeTimeProvider` and `ManualTimeProvider` are compatible, but there are some differences in when time is set before timer callbacks. Let's illustrate this with an example:
1414

1515
For example, if we create an `ITimer` with a *due time* and *period* set to **1 second**, the `DateTimeOffset` returned from `GetUtcNow()` during the timer callback may be different depending on the amount passed to `Advance()` (or `SetUtcNow()`).
1616

1717
If we call `Advance(TimeSpan.FromSeconds(1))` three times, effectively moving time forward by three seconds, the timer callback will be invoked once at times `00:01`, `00:02`, and `00:03`, as illustrated in the drawing below. Both `FakeTimeProvider` and `ManualTimeProvider` behaves like this:
1818

1919
![Advancing time by three seconds in one-second increments.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/advance-1-second.svg)
2020

21-
However, if we instead call `Advance(TimeSpan.FromSeconds(3))` once, the two implementations behave differently. `ManualTimeProvider` will invoke the timer callback at the same time (`00:01`, `00:02`, and `00:03`) as if we had called `Advance(TimeSpan.FromSeconds(1))` three times, as illustrated in the drawing below:
21+
If we instead call `Advance(TimeSpan.FromSeconds(3))` once, the two implementations behave differently. `ManualTimeProvider` will invoke the timer callback at the same time (`00:01`, `00:02`, and `00:03`) as if we had called `Advance(TimeSpan.FromSeconds(1))` three times, as illustrated in the drawing below:
2222

2323
![Advancing time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/ManualTimeProvider-advance-3-seconds.svg)
2424

@@ -30,6 +30,16 @@ Technically, both implementations are correct since the `ITimer` abstractions on
3030

3131
However, I strongly prefer the `ManualTimeProvider` approach since it behaves consistently independent of how time is moved forward. It seems much more in the spirit of how a deterministic time provider should behave and avoids users being surprised when writing tests. I imagine users may get stuck for a while trying to debug why the time reported by `GetUtcNow()` is not set as expected due to the subtle difference in behavior of `FakeTimeProvider`.
3232

33+
That said, it can be useful to test that your code behaves correctly if a timer isn't allocated processor time immidiately when it's callback should fire, and for that, `ManualTimeProvider` includes a different method, `Jump`.
34+
35+
### Jumping to a point in time
36+
37+
A real `ITimer`'s callback may not be allocated processor time and be able to fire at the moment it has been scheduled, e.g. if the processor is busy doing other things. The callback will eventaully fire (unless the timer is disposed).
38+
39+
To support testing this scenario, `ManualtTimeProvider` includes a method that will jump time to a specific point, and then invoke all scheduled timer callbacks between the start and end of the jump. This behavior is similar to how `FakeTimeProvider`s `Advance` method works, as described in the previous sectoin.
40+
41+
![Jumping ahead in time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/jump-3-seconds.svg).
42+
3343
## Known limitations and issues:
3444

3545
- If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking `CancellationTokenSource.CancelAfter(TimeSpan)` on the `CancellationTokenSource` object returned by `CreateCancellationTokenSource(TimeSpan delay)`. This action will not terminate the initial timer indicated by the `delay` argument initially passed the `CreateCancellationTokenSource` method. However, this restriction does not apply to .NET 8.0 and later versions.

TimeProviderExtensions.lutconfig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<LUTConfig Version="1.0">
2+
<Repository />
3+
<ParallelBuilds>true</ParallelBuilds>
4+
<ParallelTestRuns>true</ParallelTestRuns>
5+
<TestCaseTimeout>180000</TestCaseTimeout>
6+
</LUTConfig>

docs/jump-3-seconds.svg

Lines changed: 17 additions & 0 deletions
Loading

src/TimeProviderExtensions/ManualTimeProvider.ManualTimerScheduledCallback.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ private sealed class ManualTimerScheduler : IComparable<ManualTimerScheduler>
1919
private readonly TimerCallback callback;
2020
private readonly object? state;
2121
private readonly ManualTimeProvider timeProvider;
22-
private TimeSpan period;
2322
private bool running;
2423

2524
public DateTimeOffset CallbackTime { get; set; }
2625

26+
public TimeSpan Period { get; private set; }
27+
2728
public ManualTimerScheduler(ManualTimeProvider timeProvider, TimerCallback callback, object? state)
2829
{
2930
this.timeProvider = timeProvider;
@@ -53,7 +54,7 @@ internal void Change(TimeSpan dueTime, TimeSpan period)
5354
{
5455
Cancel();
5556

56-
this.period = period;
57+
Period = period;
5758

5859
if (dueTime != Timeout.InfiniteTimeSpan)
5960
{
@@ -67,9 +68,9 @@ internal void TimerElapsed()
6768

6869
callback.Invoke(state);
6970

70-
if (period != Timeout.InfiniteTimeSpan && period != TimeSpan.Zero)
71+
if (Period != Timeout.InfiniteTimeSpan && Period != TimeSpan.Zero)
7172
{
72-
ScheduleCallback(period);
73+
ScheduleCallback(Period);
7374
}
7475
}
7576

0 commit comments

Comments
 (0)