Runs an existing test suite with the system clock moved to dates that human intuition does not cover, and reports which of those dates change the outcome.
It answers one question: on which dates does this codebase stop behaving?
- It does not find bugs. It finds dates. A failing date is evidence that something is date-sensitive; deciding what to do about it is a human call.
- It cannot tell whether a failure lives in the test or in the code under test. Both are real problems and both are worth knowing about, but they are fixed in different places, so the report prints the raw output next to every failure.
- A clean sweep is not proof of correctness. It means no tested behaviour depends on the wall clock in a way these particular dates disturb.
Human intuition spans roughly one human life. It does not span these:
| Date | Hazard |
|---|---|
| 1970-01-01 | the epoch itself; zero, negative and "unset" collide |
| 2000-02-29 | a real leap day; day- and year-counting arithmetic breaks here |
| 2038-01-19 | signed 32-bit seconds overflow |
| 2100-02-28 | 2100 is not a leap year, so 2100-02-29 must not exist |
| 2262-04-11 | int64 nanoseconds overflow |
| 9999-12-31 | far edge of four-digit-year formatting |
| 2027-03-14 | DST spring-forward: 02:30 local does not exist |
| 2027-11-07 | DST fall-back: 01:30 local happens twice |
| 2026-12-31 | year rollover; off-by-one in %Y and ISO week numbers |
| 2016-12-31 | nearest representable instant to the 2016 leap second |
None of these are exotic, and all of them are invisible to code review, because the reviewer is a human with the same blind spots as the author.
One example found by hand while building this, which is what convinced me the class is real. Converting the last second of the year 9999 to local time:
| Timezone | datetime.fromtimestamp(253402300799) |
|---|---|
| UTC | works |
| Asia/Shanghai (UTC+8) | ValueError: year 10000 is out of range |
| America/New_York | works |
The same instant is fine in New York and raises in Shanghai. Code that only ever runs in one timezone will never reveal this.
pip install chronofuzz # clock-shift engines are detected, not bundled
pip install "chronofuzz[python-engine]" # pre-install freezegun for the python engineOr just run the file, which has no dependencies of its own:
python3 chronofuzz.py --versionchronofuzz list # show the clock targets
chronofuzz run -- pytest -q # sweep the whole list
chronofuzz run --targets-filter quick -- pytest -q # the three cheap ones
chronofuzz run --markdown report.md -- npm test # other languages too
chronofuzz selftest # verify the harness itselfrun exits non-zero when at least one shifted clock changes the outcome, and
also when the baseline run fails, so it drops straight into a pipeline.
| Engine | Mechanism | Affects | Verified |
|---|---|---|---|
python |
freezegun, injected through sitecustomize on PYTHONPATH, so no project file is modified |
clock reads made by Python code and by the test runner | yes, on macOS with Python 3.13 |
faketime |
the faketime wrapper prefixes your command |
any process, any language | implemented; exercised on Linux by the workflow in this repo, not verified on macOS |
libfaketime |
LD_PRELOAD / DYLD_INSERT_LIBRARIES |
any process, any language | compiles on macOS; not verified, the authoring machine forbids the shared memory it needs |
none |
no shift | used for the baseline run | yes |
--engine auto picks python when the command looks like Python, otherwise the
best libc shim available on the machine.
The libc engines deliberately keep time.monotonic() real
(FAKETIME_DONT_FAKE_MONOTONIC=1; freezegun never patches it). Without that,
every sleep, retry and timeout in the suite would fire spuriously and the report
would be noise.
### y2038 - 2038-01-19T03:14:08+00:00
`E AssertionError: assert True is False | FAILED test_retention.py::test_yesterday_record_is_kept`
> assert should_purge(now - datetime.timedelta(days=1), now) is False
E AssertionError: assert True is False
E + where True = should_purge(FakeDatetime(2038, 1, 19, 3, 14, 8, ...))
That is a defect in the code: a retention policy that deletes everything once
the clock passes 2038. The same shape of output can also mean the test itself
compares against now(), which is a defect in the suite. The tail is printed so
you can tell which one you are looking at.
chronofuzz selftest plants exactly the defect above and asserts that the sweep
catches it, so the harness is checked rather than assumed.
- uses: laodaixp/chronofuzz@v0.1.0
with:
command: pytest -q
targets: quickThe action installs the clock shim, runs the sweep, writes the report into the job summary, and fails the job if a shifted clock changed the outcome.
- The
pythonengine shifts Python-level clock reads only. C extensions that calltime()themselves, and subprocesses that are not Python, are unaffected. Use a libc engine if that matters. - The
pythonengine prepends freezegun toPYTHONPATH. A project that already ships its ownfreezegunorpython-dateutilmay see this copy first. - Each clock shift re-runs the whole suite, so cost is the suite's runtime times
the number of targets.
--targets-filter quickexists for that reason. - Timeouts are per run and default to 300 seconds; a slow suite needs a larger
--timeoutor fewer targets.
python3 chronofuzz.py selftest --engine python
python3 chronofuzz.py selftest --engine faketime # needs `faketime` installedBoth run in CI on every push and once a month on a schedule.
MIT.