-
Notifications
You must be signed in to change notification settings - Fork 763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve FakeTimeProvider into a canonical fake #3996
Comments
Here's some extension methods they used in Kestrel public static long ToTicks(this TimeSpan timeSpan, TimeProvider timeProvider)
=> timeSpan.ToTicks(timeProvider.TimestampFrequency);
public static long ToTicks(this TimeSpan timeSpan, long tickFrequency)
{
if (timeSpan == TimeSpan.MaxValue)
{
return long.MaxValue;
}
if (timeSpan == TimeSpan.MinValue)
{
return long.MinValue;
}
return timeSpan.Ticks * (tickFrequency / TimeSpan.TicksPerSecond);
}
public static long GetTimestamp(this TimeProvider timeProvider, TimeSpan timeSpan)
{
return timeProvider.GetTimestamp(timeProvider.GetTimestamp(), timeSpan);
}
public static long GetTimestamp(this TimeProvider timeProvider, long timeStamp, TimeSpan timeSpan)
{
return timeStamp + timeSpan.ToTicks(timeProvider);
} are any of these useful to add here, or to the base TimeProvider? Note, some will have the truncation problem. |
I am also pushing for other changes that have a positive impact on testing: dotnet/runtime#85326 |
I agree, it's possible. Feel free to open an issue. cc @tarekgh |
I am not seeing making GetElapsedTime virtual is right thing. CC @stephentoub |
I generally think it is important that the FakeTimeProvider behaves exactly like the real timer, besides actually having time pass. Another example where the current implementation (and my own) fail is how timer callbacks are invoked. Consider this test: // This fails since the Change call blocks until the callback completes
[Fact]
public void Async_timer_callback_with_duetime_zero()
{
// Arrange
var sut = new FakeTimeProvider();
var callbacks = new List<DateTimeOffset>();
// Act
using var timer = sut.CreateTimer(
_ =>
{
Thread.Sleep(20);
callbacks.Add(sut.GetUtcNow());
},
null,
TimeSpan.Zero,
Timeout.InfiniteTimeSpan);
timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
// Assert
callbacks.Should().BeEmpty();
}
// This succeeds since the Change call does not block until the callback completes
[Fact]
public void Async_timer_callback_with_duetime_zero()
{
// Arrange
var sut = TimeProvider.System;
var callbacks = new List<DateTimeOffset>();
// Act
using var timer = sut.CreateTimer(
_ =>
{
Thread.Sleep(20);
callbacks.Add(sut.GetUtcNow());
},
null,
TimeSpan.Zero,
Timeout.InfiniteTimeSpan);
timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
// Assert
callbacks.Should().BeEmpty();
} This particular issue bid me today in a scenario where |
The FakeTimeProvider in this repo is the one we're shipping to customers. It has a robust feature set, so I'm closing this for now. We can create new issues when specific customer requests come in. |
The one in this repo has some issues/limitations,
Currently it is set to 10^7 == TimeSpan.TicksPerSecond. The actual TimeProvider.System uses TimespanFrequency == Stopwatch.Frequency == typically 10^6 or 10^9 (and does not change once the system is started). A different TimespanFrequency could help spot bugs where code assumes TimeSpan.TicksPerSecond.
Note that while GetElapsedTime() abstracts Frequency, Kestrel generally does math like
Expiration = currentTimestamp + timeoutTimeSpan.TotalSeconds * timeProvider.TimespanFrequency
, and then laterif (currentTimestamp > Expiration) ...
. See here for some examplesIf TimespanFrequency changes we would need to take care about truncation. We don't do math here in order to keep stable test results - essentially long has more digits than double. GetElapsedTime() is not virtual, so can't be fixed; one option is possibly to ensure that the base ticks are rounded up front to the nearest 1ms or so.
Consider allowing an arbitrary base offset. Eg., one of the Kestrel ones randomizes the base offset. Many of the tests in this repo use UtcNow, which actually adds non reproducibility, example.
Look at the TimeProvider's in dotnet/aspnetcore and dotnet/runtime possibly elsewhere, and consider whether we can make one with the combined features that we can share via dotnet/arcade instead of each having our own. Ideally we have a single test TimeProvider that can be configured however needed. Right now we have numerous ones.
The text was updated successfully, but these errors were encountered: