Each firing stored last_run_at = the moment the ~60 s loop noticed it, and the next was due a whole interval after that, so every firing's lateness carried into the next: Nightly Docker Prune moved from 05:44:45 to 05:47:05 over six days. A firing now stores its due slot, the latest last_run_at + k * interval not after now (due_slot), for both a launch and a skipped firing. Missed slots after an outage are skipped, not run in a burst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
"""Unit tests for the schedule due-check (pure, no DB)."""
|
|
from datetime import datetime, timedelta, timezone
|
|
from types import SimpleNamespace
|
|
|
|
from steward.ansible.scheduler import due_slot, is_due
|
|
|
|
NOW = datetime(2026, 6, 16, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
def _sched(enabled=True, last=None, interval=3600):
|
|
return SimpleNamespace(enabled=enabled, last_run_at=last, interval_seconds=interval)
|
|
|
|
|
|
def test_never_run_is_due():
|
|
assert is_due(_sched(last=None), NOW) is True
|
|
|
|
|
|
def test_disabled_is_never_due():
|
|
assert is_due(_sched(enabled=False, last=None), NOW) is False
|
|
|
|
|
|
def test_due_when_interval_elapsed():
|
|
assert is_due(_sched(last=NOW - timedelta(seconds=3600), interval=3600), NOW) is True
|
|
assert is_due(_sched(last=NOW - timedelta(seconds=3700), interval=3600), NOW) is True
|
|
|
|
|
|
def test_not_due_before_interval():
|
|
assert is_due(_sched(last=NOW - timedelta(seconds=1800), interval=3600), NOW) is False
|
|
|
|
|
|
def test_a_daily_schedule_holds_its_time_of_day():
|
|
"""#5432: noticed 40 s late every day, a daily run still belongs to its
|
|
05:44:45 slot, so the lateness never adds up."""
|
|
day = 86400
|
|
last = datetime(2026, 10, 2, 5, 44, 45, tzinfo=timezone.utc)
|
|
for _ in range(6):
|
|
noticed = last + timedelta(seconds=day + 40)
|
|
assert is_due(_sched(last=last, interval=day), noticed)
|
|
last = due_slot(_sched(last=last, interval=day), noticed)
|
|
assert last == datetime(2026, 10, 8, 5, 44, 45, tzinfo=timezone.utc)
|
|
|
|
|
|
def test_missed_slots_are_skipped_not_run_in_a_burst():
|
|
last = NOW - timedelta(hours=5, minutes=10)
|
|
slot = due_slot(_sched(last=last, interval=3600), NOW)
|
|
assert slot == NOW - timedelta(minutes=10)
|
|
assert not is_due(_sched(last=slot, interval=3600), NOW)
|
|
|
|
|
|
def test_a_first_run_is_its_own_slot():
|
|
assert due_slot(_sched(last=None), NOW) == NOW
|
|
|