diff --git a/scripts/mutation_check.py b/scripts/mutation_check.py index f64b485..e1a22ae 100755 --- a/scripts/mutation_check.py +++ b/scripts/mutation_check.py @@ -84,6 +84,7 @@ def check(mutation: dict, repo: Path = REPO) -> tuple[bool, str]: return False, f"anchor not found in {mutation['file']} — the table has drifted" INFLIGHT.write_text(f"{path}\n") + stat = path.stat() # mtime included; see the restore below try: path.write_text(src.replace(mutation["old"], mutation["new"], 1)) red = run(test, repo) != 0 @@ -92,6 +93,13 @@ def check(mutation: dict, repo: Path = REPO) -> tuple[bool, str]: # Verified, not assumed: a restore that silently failed would leave a # mutation in a tracked file and the next run would measure it. assert path.read_text() == src, f"RESTORE FAILED for {path} — fix by hand" + # ⚠ AND THE MTIME, which matters more here than it would elsewhere. + # This repo IS its own deployment root and nothing takes effect until + # the service restarts, so "is :8090 stale?" is answered by comparing + # the service's start time against source mtimes. A tool that churns + # those mtimes without changing a byte makes that check lie — it + # reported the live service 16 minutes stale when it was current. + os.utime(path, ns=(stat.st_atime_ns, stat.st_mtime_ns)) INFLIGHT.unlink(missing_ok=True) return red, "" if red else "VACUOUS — stayed green under the change it forbids" diff --git a/tests/test_mutation_check.py b/tests/test_mutation_check.py index 09c262e..1fd4230 100644 --- a/tests/test_mutation_check.py +++ b/tests/test_mutation_check.py @@ -108,3 +108,23 @@ def test_the_source_is_restored_even_when_the_mutation_proves(tmp_path): check({"label": "flip", "file": "mod.py", "test": "test_probe.py::test_f", "old": "return 2", "new": "return 3"}, repo=repo) assert (repo / "mod.py").read_text() == before + + +def test_a_reverted_file_keeps_its_mtime(tmp_path): + """The repo IS its own deployment root: nothing takes effect until the + service restarts, so "is :8090 stale?" is answered by comparing the + service's start time against source mtimes. A tool that rewrites a file + with identical bytes still bumps its mtime and makes that check lie — it + reported the live service 16 minutes stale when it was current. + + Defeating change: dropping the os.utime in the restore.""" + import os + + repo = _tree(tmp_path, "def f():\n return 2\n", + "def test_f():\n assert f() == 2\n") + mod = repo / "mod.py" + os.utime(mod, (1_000_000_000, 1_000_000_000)) + before = mod.stat().st_mtime_ns + check({"label": "flip", "file": "mod.py", "test": "test_probe.py::test_f", + "old": "return 2", "new": "return 3"}, repo=repo) + assert mod.stat().st_mtime_ns == before