Table of Contents

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.CreateClock constructs a PrimeTestClock with a fixed TZDB zone (UsEastern), so spring-forward and fall-back transitions are reproducible.
  • Crossing a spring-forward boundary changes LocalZonedNowInstant.Offset because the clock's local view jumps from EST (-05:00) to EDT (-04:00).
  • For ambiguous fall-back wall-clock times, SetLocalTime uses 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.Local exposes a known invalid sample, the test asserts the BCL marks it invalid; otherwise it short-circuits.