mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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:
parent
cfbed9e4d7
commit
3bf38e33d5
2 changed files with 17 additions and 8 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue