Time-of-day and DST (testing) — PrimeTime (NodaTime) stack
Deterministic DST tests under a virtual PrimeTestClock anchored to a fixed NodaTime DateTimeZone (US Eastern in the example fixture). Because the zone is part of the clock construction, the tests do not depend on the host's TimeZoneInfo.Local and produce the same results everywhere.
/// <summary>
/// Verifies advancing virtual time across a US Eastern spring-forward transition changes the
/// resolved UTC offset for local zoned "now".
/// </summary>
[Fact]
public void DstSpring_Advance_ChangesLocalUtcOffset ()
{
Instant before = Instant.FromUtc(2024, 3, 10, 6, 30, 0);
IPrimeTestClock clock = NodaDstScenarioFixture.CreateClock(before);
Offset offsetBefore = clock.LocalZonedNowInstant.Offset;
clock.Advance(Duration.FromHours(2));
Offset offsetAfter = clock.LocalZonedNowInstant.Offset;
offsetAfter.Should().NotBe(offsetBefore);
}
/// <summary>
/// Verifies a fall-back ambiguous local wall-clock time maps leniently to a single virtual instant
/// on the clock's configured zone.
/// </summary>
[Fact]
public void DstFallAmbiguous_SetLocalTime_LenientMappingIsStable ()
{
DateTimeZone eastern = NodaDstScenarioFixture.UsEastern;
LocalDateTime ambiguousWall = new(2024, 11, 3, 1, 30);
ZonedDateTime lenient = eastern.AtLeniently(ambiguousWall);
IPrimeTestClock clock = new PrimeTestClock(lenient.ToInstant(), eastern);
clock.SetLocalTime(ambiguousWall);
clock.LocalZonedNowInstant.LocalDateTime.Should().Be(lenient.LocalDateTime);
}
/// <summary>
/// When the machine exposes a spring-forward gap example, documents that the BCL marks that
/// wall-clock value as invalid for <see cref="TimeZoneInfo.Local"/>.
/// </summary>
[Fact]
public void MachineProbe_InvalidExample_WhenPresent_IsInvalidLocalTime ()
{
TestEnvironmentDescriptor descriptor = TestEnvironmentDescriptor.FromLocalMachine();
if (descriptor.InvalidLocalWallClockExample is not DateTime invalid)
{
return;
}
TimeZoneInfo.Local.IsInvalidTime(invalid).Should().BeTrue();
}
What to notice
NodaDstScenarioFixture.CreateClockconstructs aPrimeTestClockwith a fixed TZDB zone (UsEastern), so spring-forward and fall-back transitions are reproducible.- Crossing a spring-forward boundary changes
LocalZonedNowInstant.Offsetbecause the clock's local view jumps from EST (-05:00) to EDT (-04:00). - For ambiguous fall-back wall-clock times,
SetLocalTimeuses lenient mapping: the wall clock is resolved deterministically to the earlier of the two valid instants. - The third test demonstrates a host-environment probe: when
TimeZoneInfo.Localexposes a known invalid sample, the test asserts the BCL marks it invalid; otherwise it short-circuits.
Related
- Time-of-day and DST (production) — wall-clock scheduling and environment probes in the example app.
- Test-clock control APIs —
Set*,Advance,RunFor,Start/Stopreference for both stacks. - API:
IPrimeTestClock·PrimeTestClock