From 3bf38e33d598b39386393d439bbb2854610ed5ae Mon Sep 17 00:00:00 2001 From: Roland Date: Sun, 4 Oct 2026 18:33:13 -0600 Subject: [PATCH] tests: fix the midnight and event-loop timing flakes test_import_config_preserves_completed_once_downloads created a download "5 minutes ago" and expected it to count toward today's limit, which starts at local midnight; in the first five minutes after local midnight it was yesterday's. It failed in CI started at 00:00:35 UTC, and reproduces on master with the clock at 00:02. The test now pins the clock to local noon, as the download manager reckons local time. test_import_all_db_configs_does_not_block_event_loop required 90% of the expected 50 ms ticks, so timer overshoot on a busy runner added up across the run (CI got 28 of the 28.8 required). It now measures the longest gap between ticks, including after the last tick: an import on the loop freezes it for the whole 0.2 s sleep, while jitter delays a tick by milliseconds. Moving the first or the last import onto the loop still fails it. Co-Authored-By: Claude Opus 5.5 --- .../test_cross_config_disaster_recovery.py | 18 +++++++++++------- wrolpi/test/test_downloads_config.py | 7 ++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/wrolpi/test/test_cross_config_disaster_recovery.py b/wrolpi/test/test_cross_config_disaster_recovery.py index cd4b4834..880866cf 100644 --- a/wrolpi/test/test_cross_config_disaster_recovery.py +++ b/wrolpi/test/test_cross_config_disaster_recovery.py @@ -608,27 +608,31 @@ async def test_import_all_db_configs_does_not_block_event_loop(test_session, tes 'modules.flasher.config.FlasherConfig.import_config', ] - ticks = 0 tick_seconds = 0.05 + tick_times = list() async def ticker(): - nonlocal ticks while True: await asyncio.sleep(tick_seconds) - ticks += 1 + tick_times.append(time.monotonic()) + start = time.monotonic() ticker_task = asyncio.create_task(ticker()) try: with contextlib.ExitStack() as stack: for target in patched: stack.enter_context(mock.patch(target, slow_import)) await import_all_db_configs() + finished = time.monotonic() finally: ticker_task.cancel() with contextlib.suppress(asyncio.CancelledError): await ticker_task - # A responsive loop ticks through the whole sleep budget; require most of it so that any one - # import moving back onto the loop (losing its share of ticks) fails. - expected = len(patched) * sleep_seconds / tick_seconds - assert ticks >= expected * 0.9, f'event loop was blocked during import ({ticks=}, {expected=})' + # An import on the loop freezes it for that import's whole sleep, so no tick fires for at least that long. + # Timer jitter on a busy machine delays a tick by milliseconds; it does not add up to a sleep, as a count of + # ticks over the whole run would. The final gap catches a blocking import after the last tick. + assert tick_times, 'the event loop never ran the ticker' + times = [start, *tick_times, finished] + longest_gap = max(later - earlier for earlier, later in zip(times, times[1:])) + assert longest_gap < sleep_seconds, f'event loop was blocked for {longest_gap:.3f} s during the import' diff --git a/wrolpi/test/test_downloads_config.py b/wrolpi/test/test_downloads_config.py index 0e817069..fba264d5 100644 --- a/wrolpi/test/test_downloads_config.py +++ b/wrolpi/test/test_downloads_config.py @@ -380,13 +380,18 @@ class TestDownloadManagerConfigEdgeCases: @pytest.mark.asyncio async def test_import_config_preserves_completed_once_downloads(test_session, test_directory, async_client, - test_download_manager, test_download_manager_config): + test_download_manager, test_download_manager_config, + fake_now): """A completed once-download is never written to the config, so importing the config (which happens on every startup) must not delete it: its `last_download_attempt` is what the daily download limits count.""" from datetime import timedelta from wrolpi.dates import now + # The daily limits count from local midnight, so "5 minutes ago" must fall on the same local day. Pin the + # clock to local noon (as the download manager reckons local time), or this fails just after midnight. + fake_now(test_download_manager._get_local_now().replace(hour=12, minute=0, second=0, microsecond=0)) + config = get_download_manager_config() config_path = config.get_file()