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 <noreply@anthropic.com>
This commit is contained in:
Roland 2026-10-04 18:33:13 -06:00
parent cfbed9e4d7
commit 3bf38e33d5
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 17 additions and 8 deletions

View file

@ -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'

View file

@ -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()