Skip to content

FakeTimeProvider.Advance/SetUtcNow does not behave as expected #3995

Description

@egil

The current implementation of FakeTimeProviders Advance(TimeSpan) and SetUtcNow(DateTimeOffset) does not behave as expected.

There are a few different scenarios, so let me take them one at a time:

Scenario 1: A timer with dueTime = 5 second and period = 5 seconds.

Calling Advance(TimeSpan.FromSeconds(5)) at time 0 should invoke the timer callback 1 time (works).
Calling Advance(TimeSpan.FromSeconds(10)) at time 0 should invoke the timer callback 2 time (does not work).

Here are two tests that cover this:

[Fact]
public void Timer_callback_invoked_multiple_times_single_advance()
{
   var sut = new FakeTimeProvider();
   var callbackCount = 0;
   var dueTime = TimeSpan.FromSeconds(3);
   var period = TimeSpan.FromSeconds(5);
   using var timer = sut.CreateTimer(_ => callbackCount++, null, dueTime, period);

   sut.Advance(TimeSpan.FromSeconds(13));

   callbackCount.Should().Be(3);
}

[Fact]
public void GetUtcNow_matches_time_at_callback_time()
{
   var sut = new FakeTimeProvider();
   var startTime = sut.GetUtcNow();
   var callbackTimes = new List<DateTimeOffset>();
   var interval = TimeSpan.FromSeconds(3);
   using var timer = sut.CreateTimer(_ => callbackTimes.Add(sut.GetUtcNow()), null, interval, interval);

   sut.Advance(interval + interval + interval);

   callbackTimes.Should().ContainInOrder(
       startTime + interval,
       startTime + interval + interval,
       startTime + interval + interval + interval);
}

Scenario 2: A periodic timer with a period = 10 seconds and a delay of 3 seconds inserted after each WaitForNextTickAsync completes.

The point of this scenario is that during a timer callback, another thing can be scheduled.

Calling Advance(TimeSpan.FromSeconds(23)) at time 0 should result in callback invocations at times 10, 13, 20, and 23.

Here is a test that covers this:

[Fact]
public async Task Callbacks_happens_in_schedule_order()
{
    var sut = new FakeTimeProvider();
    var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromSeconds(10));
    var startTime = sut.GetUtcNow();
    var callbacks = new List<DateTimeOffset>();
    var callbacksTask = AsyncCallbacks(periodicTimer);

    sut.Advance(TimeSpan.FromSeconds(23));

    callbacks.Should().ContainInOrder(
        startTime + TimeSpan.FromSeconds(10),
        startTime + TimeSpan.FromSeconds(13),
        startTime + TimeSpan.FromSeconds(20),
        startTime + TimeSpan.FromSeconds(23));

    periodicTimer.Dispose();
    await callbacksTask;

    async Task AsyncCallbacks(PeriodicTimer periodicTimer)
    {
        while (await periodicTimer.WaitForNextTickAsync().ConfigureAwait(false))
        {
            callbacks.Add(sut.GetUtcNow());
            await sut.Delay(TimeSpan.FromSeconds(3));
            callbacks.Add(sut.GetUtcNow());
        }
    }
}

I could see the benefit in a SkipAhead/BendSpaceAndTime or similar method, that allows the test user to skip forward in time from t0 to tN and explicitly not invoke any callbacks between t0 and tN, and only invoke callbacks set to trigger at tN. However, while using my own version of FakeTimeProvider in production for the last six months I have yet to have a need. I have however needed both of the scenarios described above in my testing.

Ps. If you want to compare notes, here is my implementation, ManualTimeProvider.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions