From 4f7887720123600d0c76b2b8c6d96f8910000465 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 13 Nov 2026 03:02:00 +0100 Subject: [PATCH] test: the app's clock never goes back in the test suite The docker VM the suite runs in steps its wall clock back now and then (measured: 2 steps in 20 s, up to 0.5 s), so rows a test made one after another could read out of order (a files list sorted newest first, a scheduler's last runs). Every app module that took datetime from the datetime module reads a clock that never repeats or goes back; the scheduler test's own fixture for this goes. --- backend/tests/conftest.py | 36 +++++++++++++++++++++++++++++++++ backend/tests/test_scheduler.py | 25 ----------------------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 66cb9a8b..ba9f4f6d 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -13,6 +13,7 @@ that the migration chain applies cleanly. Each test then gets a clean database v tests that COMMIT (the whole point of this lane — transaction-ownership behaviour) don't leak. """ +import datetime as _dt import os import shutil from urllib.parse import quote @@ -317,6 +318,41 @@ def _reset_rate_limiters(): obj.reset() +class _NeverBack(_dt.datetime): + """The app's clock in tests: the wall clock, but never repeating and never going back — each + reading at least a microsecond after the last one in this process.""" + + last: _dt.datetime | None = None + clock: type[_dt.datetime] = ( + _dt.datetime + ) # what it reads; a probe can put a stepping one under it + + @classmethod + def now(cls, tz=None): + t = cls.clock.now(tz) + last = cls.last + if last is not None and (last.tzinfo is None) == (t.tzinfo is None) and t <= last: + t = last + _dt.timedelta(microseconds=1) + cls.last = t + return t + + +@pytest.fixture(autouse=True) +def _clock_never_goes_back(monkeypatch): + """Rows a test makes one after another are stamped in that order. The wall clock of the docker + VM the suite runs in steps BACK now and then (measured: 2 steps in 20 s, up to 0.5 s), and a + test that makes three files and reads them newest first then got them out of order. Every loaded + `app.*` module that took `datetime` from the datetime module reads this clock instead (a sweep, + as the limiter reset below does, so a new module is covered the day it lands). A database + default (`server_default=func.now()`) is Postgres's clock and is not covered: a test that orders + such rows made within a second of each other gives them distinct times itself.""" + import sys + + for name, mod in list(sys.modules.items()): + if (name == "app" or name.startswith("app.")) and vars(mod).get("datetime") is _dt.datetime: + monkeypatch.setattr(mod, "datetime", _NeverBack) + + @pytest.fixture(autouse=True) def _reset_log_erasures(): """`logs._erased` is module-level and remembers an erased account for an hour of monotonic time diff --git a/backend/tests/test_scheduler.py b/backend/tests/test_scheduler.py index 2465ed30..cab4b7bf 100644 --- a/backend/tests/test_scheduler.py +++ b/backend/tests/test_scheduler.py @@ -22,31 +22,6 @@ from app.runstatus import RunStatus, RunTrigger _JOB = "unit_test_claim" # not a real job id — exercises the primitive without touching live jobs -class _StrictlyLater(datetime): - """A clock that never repeats or goes back: each reading at least a microsecond after the last.""" - - last: datetime | None = None - clock: type[datetime] = datetime - - @classmethod - def now(cls, tz=None): - t = cls.clock.now(tz) - if cls.last is not None and t <= cls.last: - t = cls.last + timedelta(microseconds=1) - cls.last = t - return t - - -@pytest.fixture(autouse=True) -def _runs_start_in_order(monkeypatch): - """Runs started in a row start in that order. These tests open several runs within microseconds - and read them back newest first, by start time; the wall clock of a container on a VM can step - back between two readings (seen: the third of three runs sorted before the second).""" - _StrictlyLater.last = None - _StrictlyLater.clock = jobruns.datetime - monkeypatch.setattr(jobruns, "datetime", _StrictlyLater) - - def _release(): scheduler._record(_JOB, running=False)