question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

FakeTimeProvider.Advance/SetUtcNow does not behave as expected

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 4 months ago
  • Comments:21 (21 by maintainers)

github_iconTop GitHub Comments

2reactions
geeknoidcommented, May 26, 2023

BTW, I do appreciate you’re looking at all these fake time providers. As noted in that issue, I hope we can converge these a bit.

Likewise. It’s an interesting problem, and I need one too 😃

I do think the entire motivation for creating the TimeProvider was to give the ecosystem a single abstraction and ideally, also a single “fake” version that we could all lean on instead of all creating our own.

The motivation for TimeProvider/FakeTimeProvider came from needing to write tests for all the code in this repo. Then we started to look around and found literally hundreds of variations of IClock types within Microsoft services. It became very clear that this needed a universal abstraction.

2reactions
sebastienroscommented, May 25, 2023

That’s actually how I thought it should have worked initially, but AFAIR the existing implementation and tests were not behaving this way. I’ll talk with internal stakeholders and comment back here.

Read more comments on GitHub >

github_iconTop Results From Across the Web

No results found

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found