From 6366e05c00e4e0883028fe7237595a9f5b29eb8b Mon Sep 17 00:00:00 2001 From: Josh Poole Date: Mon, 31 Aug 2026 17:48:51 +0100 Subject: [PATCH] 20260831 - Fix the flaky capture-loop test at its cause, not its timeout test_refresh_failure_does_not_kill_capture_loop failed roughly 1 run in 20, on any machine, and took main red after an unrelated merge. It is not a slow runner and a longer timeout would not have helped. _run() only re-renders when new_frames_since_render > 0, and the first render resets that counter. The capture thread starts on service.start() and consumes frames every POLL_INTERVAL_S while the main thread is still on its way to attach(). With a fixed two-frame list there is a race: if both frames are gone before a viewer is registered, the first render fires on _refresh_requested, the counter resets, no frame ever arrives again, and the second render can never happen. calls["n"] sticks at 1 and the wait expires however long it is. The test now takes frames from a source that never runs out, so a second render is reachable whatever order the threads happen to run in. The assertion then tests what it means to test, that the loop survives an exception, rather than incidentally testing thread scheduling. FakeBlah2Client is left alone. Other tests assert on a fixed frame count and rely on it draining. Measured: 0 failures in 150 runs, against 2/25 and 2/50 before. Full suite 802 passing. Co-Authored-By: Claude Opus 5 --- tests/test_tracker_capture.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_tracker_capture.py b/tests/test_tracker_capture.py index 24355df..682e4a5 100644 --- a/tests/test_tracker_capture.py +++ b/tests/test_tracker_capture.py @@ -288,6 +288,31 @@ def test_prune_runs_independent_of_viewers(monkeypatch): assert _wait_until(lambda: len(service.history.raw_points) == 0, timeout=1.0) +class EndlessBlah2Client: + """Never runs out of frames, each with a fresh timestamp. + + The draining FakeBlah2Client cannot be used for a test that waits on a + *second* render. `_run()` only re-renders when `new_frames_since_render > + 0`, and the first render resets that counter. The capture thread starts on + service.start() and consumes frames at POLL_INTERVAL_S while the main + thread is still on its way to attach(), so with a fixed two-frame list + there is a race: if both frames are gone before a viewer is registered, + the first render fires on _refresh_requested, the counter resets, no frame + ever arrives again and the second render can never happen. The wait then + expires no matter how long it is. + + That raced roughly 1 run in 20, on any machine, and a longer timeout would + not have helped. + """ + + def __init__(self): + self._ts = 1000 + + def get_detection(self): + self._ts += 1000 + return make_frame(self._ts) + + def test_refresh_failure_does_not_kill_capture_loop(monkeypatch): calls = {"n": 0} @@ -297,8 +322,7 @@ def flaky(): raise ValueError("boom") stub_refresh(monkeypatch, None, side_effect=flaky) - client = FakeBlah2Client([make_frame(1000), make_frame(2000)]) - service = make_service(client) + service = make_service(EndlessBlah2Client()) service.start() q = service.attach()