From fa6a406d818dfacf63e6812b2729c0e3823c5467 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 13 Jul 2026 09:16:56 +0200 Subject: [PATCH 1/2] chore(tests): stop asserting exact timer tick counts in JS timer tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Timer.Elapsed.Subscribe works` and `Timer with AutoReset = true works` both started a 50ms timer, slept 125ms, and asserted the handler had run exactly twice. How many ticks land inside a fixed sleep is a wall-clock race: the second tick is due at 100ms, leaving 25ms of slack, and on a loaded CI runner — where timer resolution is coarse — it slips past the window and only one arrives. `Timer.Elapsed.Subscribe works` failed this way on the windows-latest JS leg. Assert the semantics the tests are named for instead: the handler fires repeatedly, and stops firing once the timer is stopped or the subscription is disposed. The counter is read after Stop/Dispose so a tick cannot land between the read and the unsubscribe. This is the same tolerance the neighbouring `Assigning an event to a variable works` already uses (200ms sleep, at least two ticks), which has been stable. `Timer with AutoReset = false works` is left alone: exactly one tick is the invariant it exists to check, not an artifact of timing. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/Js/Main/DateTimeTests.fs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/Js/Main/DateTimeTests.fs b/tests/Js/Main/DateTimeTests.fs index f137d6c22..71911f87f 100644 --- a/tests/Js/Main/DateTimeTests.fs +++ b/tests/Js/Main/DateTimeTests.fs @@ -1189,10 +1189,17 @@ let tests = let t = new Timers.Timer(50.) t.Elapsed.Add(fun ev -> res := !res + 5) t.Start() - do! Async.Sleep 125 + do! Async.Sleep 200 t.Stop() - do! Async.Sleep 50 - equal 10 !res + // Snapshot after Stop: a tick landing between the read and the stop is a legal + // interleaving, and would be indistinguishable from Stop failing to halt the timer. + let afterStop = !res + do! Async.Sleep 100 + // AutoReset keeps the timer firing, so more than one tick lands. The exact number is + // timing-dependent — how many 50ms ticks fit in the sleep varies with runner load. + afterStop > 5 |> equal true + // Stop halts it: no further ticks after the snapshot. + !res |> equal afterStop } testCaseAsync "Timer.Elapsed.Subscribe works" <| fun () -> @@ -1201,10 +1208,17 @@ let tests = let t = new Timers.Timer(50.) let disp = t.Elapsed.Subscribe(fun ev -> res := !res + 5) t.Start() - do! Async.Sleep 125 + do! Async.Sleep 200 disp.Dispose() - do! Async.Sleep 50 - equal 10 !res + // Snapshot after Dispose: a tick landing between the read and the unsubscribe is a + // legal interleaving, and would be indistinguishable from Dispose failing to detach. + let afterDispose = !res + do! Async.Sleep 100 + // The subscriber receives Elapsed events. The exact number is timing-dependent — how + // many 50ms ticks fit in the sleep varies with runner load. + afterDispose > 5 |> equal true + // Dispose detaches it: no further ticks after the snapshot. + !res |> equal afterDispose t.Stop() } From 29a2a2a4993748f35e27d3b6f0da757340def138 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 19 Jul 2026 15:39:06 +0200 Subject: [PATCH 2/2] chore(tests): fix swapped equal arguments in JS timer tests `equal` is `equal expected actual`, so passing the observed counter first made failure messages print the two sides reversed. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/Js/Main/DateTimeTests.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Js/Main/DateTimeTests.fs b/tests/Js/Main/DateTimeTests.fs index 71911f87f..83ad64acc 100644 --- a/tests/Js/Main/DateTimeTests.fs +++ b/tests/Js/Main/DateTimeTests.fs @@ -1199,7 +1199,7 @@ let tests = // timing-dependent — how many 50ms ticks fit in the sleep varies with runner load. afterStop > 5 |> equal true // Stop halts it: no further ticks after the snapshot. - !res |> equal afterStop + equal afterStop !res } testCaseAsync "Timer.Elapsed.Subscribe works" <| fun () -> @@ -1218,7 +1218,7 @@ let tests = // many 50ms ticks fit in the sleep varies with runner load. afterDispose > 5 |> equal true // Dispose detaches it: no further ticks after the snapshot. - !res |> equal afterDispose + equal afterDispose !res t.Stop() }