`docker/Dockerfile`'s web-build stage is gitless twice over — the build context is `web/` + `design-system/` so there is no `.git`, and `node:22-bookworm-slim` ships no git binary. Members of the SPA suite need one or the other, so running the suite there required naming the ones that cannot run. That list was a population nothing derived: #883 added a third member without updating the hand-written pair of `--exclude`s, and because `Build & push image (amd64)` is `if: github.event_name != 'pull_request'` the resulting red was unreachable on a PR. It landed on `main` and on the `v*` tag path instead — every image build failed, `:latest` stopped being republished, and a release cut would have failed at the image build. Adding a third `--exclude` re-arms the trap, so the list is removed rather than extended: the stage now lints, typechecks and BUILDS the SPA, and the suite runs once, unfiltered, in `docker-build.yml`'s `test` job on a real checkout. `build` carries `needs: [test, migrations, scan]`, so no image is published past a red suite. `scripts/tests/test_image_build_delegates_the_spa_suite.py` holds both halves — the negative one alone would be satisfied by deleting the `needs:` edge. Three populations, all derived: tracked Dockerfiles and workflows from the git index, and which npm scripts ARE the suite from `web/package.json` (so `test` is in and the Playwright `test:ui-e2e` is out, with no exemption list). Publishing jobs come from the `docker/build-push-action` step and the Dockerfile each builds from that step's own `file:` input, which is why `ci-image.yml` is out of scope by derivation rather than by an entry that would outlive its reason. Four mutants witnessed red, each by the intended test: a filtered suite run put back into the Dockerfile, the `needs:` edge deleted, and the gating run narrowed in both the block and the single-line `run:` step forms. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
617 lines
34 KiB
Python
617 lines
34 KiB
Python
"""No file population in this repo admits a file git does not track (ersatztv#806).
|
|
|
|
The regression for #778's third defect, hoisted to cover every guard that shares the mechanism
|
|
rather than being copied into each of them. `Path.rglob` enumerated `.husky/_/` — 17 husky shims
|
|
generated by `npm ci`, gitignored and untracked — so `test_remote_state_inventory.py` was RED on
|
|
every developer checkout and GREEN in CI, whose `script-tests` job checks out and pip-installs but
|
|
never runs `npm ci`. A guard that fails everywhere except where it runs trains its readers to ignore
|
|
it, and it did so on the artifact whose entire thesis is population correctness.
|
|
|
|
TWO PROOFS, because they fail differently and either alone leaves a hole.
|
|
|
|
* `test_the_primitive_REALLY_excludes_an_untracked_file` builds a throwaway git repo, commits one
|
|
file, leaves an identical sibling untracked, and runs the real derivation against it. It proves
|
|
the mechanism by EXECUTING it rather than by recognising its shape — no monkeypatching, no
|
|
stand-in for git. Nothing here is a claim about `git ls-files`; it is `git ls-files`.
|
|
* `test_no_derivation_admits_an_untracked_file` narrows the tracked set under each real derivation
|
|
and requires the dropped member to vanish from the population even though the file is still on
|
|
disk and still matches the scope. That is the property stated over the ACTUAL guards, so a
|
|
future refactor that quietly reintroduces a filesystem walk in any one of them fails here rather
|
|
than on somebody's laptop.
|
|
|
|
`DERIVATIONS` is the reason this file is not one near-copy per derivation: a guard that starts deriving a file
|
|
population registers here, and both proofs cover it for free. The register is hand-written and that
|
|
is a SCOPE decision, not a population one — per `testing.guard-derives-population-from-source`, a
|
|
scope mirroring an authoritative source needs its own equality check, and
|
|
`test_every_index_derived_module_is_registered` is it: it reads which modules import the shared
|
|
helper and demands each one appear below, so adding another derivation and forgetting this file is
|
|
red rather than silently uncovered.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import glob
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts.tests import test_ci_image_pin_population as image_pin
|
|
from scripts.tests import test_ci_status_context_uniqueness as ctx_unique
|
|
from scripts.tests import test_guard_inventory as guard_inventory
|
|
from scripts.tests import test_hook_fire_log as hook_fire
|
|
from scripts.tests import test_image_build_delegates_the_spa_suite as image_build
|
|
from scripts.tests import test_pr_changed_files as pr_changed
|
|
from scripts.tests import test_remote_state_inventory as remote_state
|
|
from scripts.tests import test_workflow_job_guards as job_guards
|
|
from scripts.tests import test_workflow_persist_credentials as persist_creds
|
|
from scripts.tests import tracked_files
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
TESTS_DIR = REPO_ROOT / "scripts" / "tests"
|
|
|
|
|
|
class _EmptyScan:
|
|
"""An exhausted ITERATOR that is also a context manager, standing in for `os.scandir`.
|
|
|
|
Both halves are load-bearing and each was missing in turn. `os.scandir` is used as
|
|
`with os.scandir(...) as it`, so a bare iterator broke the context-manager protocol; and
|
|
`os.walk` does `entry = next(scandir_it)` on the result, so an ITERABLE defining only
|
|
`__iter__` broke that. Either way the enumeration assertion still fired with the right message,
|
|
but the report also carried a TypeError about the harness — and a finding that arrives beside a
|
|
harness error invites doubting the finding rather than the code.
|
|
"""
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __next__(self):
|
|
raise StopIteration
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_exc):
|
|
return False
|
|
|
|
def close(self):
|
|
return None
|
|
|
|
|
|
def _as_relative_strings(members) -> set[str]:
|
|
"""Derivations return either repo-relative strings or absolute `Path`s; compare on one form.
|
|
|
|
Iterating into a set is also what DRAINS a derivation that returns a generator, which the
|
|
enumeration proof depends on — see its call site.
|
|
|
|
An unexpected member type is REPORTED rather than stringified. `str(m)` on anything at all meant
|
|
a derivation yielding, say, nested generators produced plausible-looking members and compared
|
|
equal to nothing, which is a population check passing over data it did not understand.
|
|
"""
|
|
out = set()
|
|
for m in members:
|
|
assert isinstance(m, str | Path), (
|
|
f"a derivation yielded {type(m).__name__} ({m!r}); populations here are repo-relative "
|
|
"strings or absolute Paths, and stringifying anything else would compare a plausible "
|
|
"value against a set that can never contain it."
|
|
)
|
|
out.add(str(Path(m).relative_to(REPO_ROOT)) if isinstance(m, Path) else str(m))
|
|
return out
|
|
|
|
|
|
# (label, callable, a floor below which the derivation has plainly broken). `test_hook_fire_log`
|
|
# floors the same population at the same number for its own coverage assertions; that is not a
|
|
# duplicate guard masking another, because the two protect different consumers from going
|
|
# vacuous — delete this one and THIS file's proofs iterate over nothing while reporting success.
|
|
DERIVATIONS = (
|
|
("test_guard_inventory.derived_guard_files", guard_inventory.derived_guard_files, 25),
|
|
("test_hook_fire_log.hook_scripts", hook_fire.hook_scripts, 10),
|
|
("test_image_build_delegates_the_spa_suite.dockerfiles", image_build.dockerfiles, 4),
|
|
("test_image_build_delegates_the_spa_suite.workflow_files", image_build.workflow_files, 5),
|
|
("test_ci_image_pin_population.workflow_files", image_pin.workflow_files, 5),
|
|
("test_ci_status_context_uniqueness.workflow_files", ctx_unique.workflow_files, 5),
|
|
("test_remote_state_inventory.derived_population", remote_state.derived_population, 40),
|
|
("test_pr_changed_files._workflow_files", pr_changed._workflow_files, 5),
|
|
("test_workflow_persist_credentials.workflow_files", persist_creds.workflow_files, 5),
|
|
("test_workflow_job_guards.workflow_files", job_guards.workflow_files, 5),
|
|
)
|
|
|
|
# The floor matters only to the anti-vacuity test; the two property tests take the pair, so an
|
|
# unused parameter cannot drift into looking like an assertion they make.
|
|
_IDS = [d[0] for d in DERIVATIONS]
|
|
_PAIRS = [(label, derive) for label, derive, _ in DERIVATIONS]
|
|
|
|
# Modules that import the shared derivation WITHOUT deriving a guard population. Kept here, beside
|
|
# DERIVATIONS, so adding one is an edit to this file that a reviewer sees.
|
|
POPULATION_EXEMPT = {
|
|
# Uses the index to assemble a HERMETIC tmp fixture copy; nothing in it asserts membership.
|
|
"test_ci_release_path_scan_job.py": "index-derived fixture copy, not a population",
|
|
}
|
|
|
|
_HELPER = "scripts.tests.tracked_files"
|
|
_PACKAGE = ["scripts", "tests"]
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# ANTI-VACUITY FIRST — every assertion below compares sets, and a derivation that collapsed to
|
|
# nothing would satisfy all of them while proving nothing.
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(("label", "derive", "floor"), DERIVATIONS, ids=_IDS)
|
|
def test_each_derivation_found_something(label, derive, floor):
|
|
members = _as_relative_strings(derive())
|
|
assert len(members) >= floor, (
|
|
f"{label} derived only {len(members)} members, below its floor of {floor} — the derivation "
|
|
"is broken, not the repo, and every set comparison built on it is vacuous."
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# PROOF 1 — the primitive, executed against a real git repo rather than described
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_primitive_REALLY_excludes_an_untracked_file(tmp_path, monkeypatch):
|
|
"""A tracked and an untracked file, identical in name shape and both on disk. Only one is in.
|
|
|
|
Run rather than reasoned about. This class of defect is produced by arguments about what a
|
|
traversal WOULD enumerate, and such arguments are locally convincing whether or not they are
|
|
right; only executing the traversal distinguishes the two.
|
|
"""
|
|
repo = tmp_path / "repo"
|
|
(repo / ".claude" / "hooks").mkdir(parents=True)
|
|
(repo / ".claude" / "hooks" / "committed.sh").write_text("#!/bin/sh\n")
|
|
(repo / ".claude" / "hooks" / "untracked.sh").write_text("#!/bin/sh\n")
|
|
|
|
def git(*args):
|
|
subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True)
|
|
|
|
git("init", "-q")
|
|
git("config", "user.email", "guard@example.invalid")
|
|
git("config", "user.name", "guard")
|
|
git("add", ".claude/hooks/committed.sh")
|
|
git("commit", "-qm", "one tracked hook")
|
|
|
|
monkeypatch.setattr(tracked_files, "REPO_ROOT", repo)
|
|
found = tracked_files.tracked_children(".claude/hooks", ("*.sh",))
|
|
|
|
assert (repo / ".claude" / "hooks" / "untracked.sh").is_file(), (
|
|
"the untracked file must still be on disk, or this proves nothing about the index winning over the filesystem"
|
|
)
|
|
assert found == {".claude/hooks/committed.sh"}, (
|
|
f"the derivation returned {sorted(found)}. A filesystem walk returns both files here; only "
|
|
"the index distinguishes them, and that difference is the entire point of ersatztv#806."
|
|
)
|
|
|
|
|
|
def test_the_primitive_does_not_recurse_into_an_untracked_subdirectory(tmp_path, monkeypatch):
|
|
"""`.husky/_/` in miniature — the shape that made #778 red on every checkout.
|
|
|
|
Even a TRACKED nested file must stay out: `tracked_children` is direct-children-only by design,
|
|
and recursion is what dragged the shims in. Proving it with a tracked file makes the assertion
|
|
about the traversal rather than about the index, so the two properties cannot mask each other.
|
|
"""
|
|
repo = tmp_path / "repo"
|
|
(repo / ".husky" / "_").mkdir(parents=True)
|
|
(repo / ".husky" / "pre-commit").write_text("#!/bin/sh\n")
|
|
(repo / ".husky" / "_" / "husky.sh").write_text("#!/bin/sh\n")
|
|
|
|
def git(*args):
|
|
subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True)
|
|
|
|
git("init", "-q")
|
|
git("config", "user.email", "guard@example.invalid")
|
|
git("config", "user.name", "guard")
|
|
git("add", "-A")
|
|
git("commit", "-qm", "husky plus a nested shim, both tracked")
|
|
|
|
monkeypatch.setattr(tracked_files, "REPO_ROOT", repo)
|
|
assert tracked_files.tracked_children(".husky", ("*",)) == {".husky/pre-commit"}, (
|
|
"a nested file entered a flat population — this is the `.husky/_/` shape, and it was red on "
|
|
"every developer checkout the last time it shipped"
|
|
)
|
|
|
|
|
|
def test_an_empty_index_FAILS_LOUDLY_rather_than_reporting_an_empty_population(tmp_path, monkeypatch):
|
|
"""The floor under every floor. A silent empty population is how a completeness guard reports
|
|
total coverage having examined nothing, which is the failure mode this repo has shipped twice
|
|
(#631, #751)."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "-C", str(repo), "init", "-q"], check=True, capture_output=True)
|
|
|
|
monkeypatch.setattr(tracked_files, "REPO_ROOT", repo)
|
|
with pytest.raises(AssertionError, match="reported nothing"):
|
|
tracked_files.tracked_children(".claude/hooks", ("*.sh",))
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# PROOF 2 — the property, over the real guards
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(("label", "derive"), _PAIRS, ids=_IDS)
|
|
def test_no_derivation_admits_an_untracked_file(label, derive):
|
|
"""Narrow the index, leave the disk alone, and require the member to disappear — for EVERY
|
|
member, one at a time.
|
|
|
|
EXHAUSTIVE RATHER THAN ONE VICTIM, and the difference is not thoroughness for its own sake.
|
|
`derived_guard_files` unions four sources; one victim is always drawn from whichever sorts
|
|
first, so a mutant putting only the third source back on a filesystem walk passes while the
|
|
proof reports on all four. A sample cannot see the source it did not draw from — this file
|
|
applying `testing.guard-derives-population-from-source` to itself.
|
|
|
|
The floors cannot substitute, and the numbers say why. Reproduce with:
|
|
|
|
PYTHONPATH=. python3 -c "from scripts.tests import test_guard_inventory as g; \
|
|
print(len(g.derived_guard_files()))"
|
|
|
|
61 members on 2026-08-22; suppressing a single contributor leaves 39 (`scripts/tests`), 48
|
|
(hooks) or 58 (husky), all far above the anti-vacuity floor of 25. The figures move whenever a
|
|
guard is added — they were 60/39/47/57 one commit earlier — so read them as an illustration of
|
|
the GAP, not as values to assert against. A floor tight enough to catch a lost source
|
|
would go red every time a guard is legitimately deleted, which is the wrong instrument.
|
|
|
|
Its own `monkeypatch` context, never the shared fixture instance: the function-scoped fixture is
|
|
the same object `conftest.py`'s autouse `isolate_hook_fire_log` patched, so calling `undo()` on
|
|
it here reverts `ETV_HOOK_FIRE_LOG_DIR` too. Since ersatztv#809 that reverts to the SESSION
|
|
directory `pytest_configure` set rather than to unset, so it no longer re-points anything at the
|
|
PRODUCTION log — but it still collapses THIS test's remaining per-test isolation into the shared
|
|
session dir, and reaching into another fixture's patch object to undo it is the habit worth not
|
|
having either way.
|
|
|
|
WHAT REMOVAL CANNOT SEE, so it is not read as more than it is: a source contributing ONLY
|
|
untracked members has nothing here to remove, and #778's defect was exactly that shape (an
|
|
`rglob` over `.husky/_/` adds 17 untracked members and removes none). That direction is
|
|
`test_no_derivation_ENUMERATES_the_filesystem` below; the two are complements, not duplicates.
|
|
"""
|
|
before = _as_relative_strings(derive())
|
|
assert before, f"{label} derived nothing; there is no victim to remove"
|
|
|
|
real = tracked_files._git_ls_files()
|
|
survivors = []
|
|
absent = []
|
|
with pytest.MonkeyPatch.context() as m:
|
|
for victim in sorted(before):
|
|
if not (REPO_ROOT / victim).is_file():
|
|
absent.append(victim)
|
|
continue
|
|
m.setattr(tracked_files, "_git_ls_files", lambda v=victim: [p for p in real if p != v])
|
|
if victim in _as_relative_strings(derive()):
|
|
survivors.append(victim)
|
|
|
|
assert not absent, (
|
|
f"{label} contains {absent}, which git tracks but are not on disk. The proof below asserts "
|
|
"that the INDEX decides while the file is still present; it cannot mean that for a member "
|
|
"that is missing, so this is reported rather than skipped."
|
|
)
|
|
assert not survivors, (
|
|
f"{label} still contains {survivors} after git stopped tracking them. Every one of those is "
|
|
"still on disk, so the derivation is reading the filesystem for that member and untracked "
|
|
"build output can redden it on a developer checkout while CI stays green (ersatztv#778)."
|
|
)
|
|
|
|
|
|
# The directory-listing APIs a Python file population is realistically written with. NOT every way a
|
|
# process can list a directory — `subprocess.run(["ls"])`, a module-level alias captured before the
|
|
# patch, and any C-level call all walk straight past this, all three verified by cold review. That
|
|
# bounds what the check below can claim, and the docstring says so rather than implying a sandbox.
|
|
# Reading a file stays allowed: `derived_guard_files` must read workflow bodies.
|
|
_ENUMERATORS = (
|
|
(Path, "glob"),
|
|
(Path, "rglob"),
|
|
(Path, "iterdir"),
|
|
(Path, "walk"),
|
|
(os, "listdir"),
|
|
(os, "walk"),
|
|
(os, "scandir"),
|
|
(glob, "glob"),
|
|
(glob, "iglob"),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(("label", "derive"), _PAIRS, ids=_IDS)
|
|
def test_no_derivation_ENUMERATES_the_filesystem(label, derive):
|
|
"""The ADD direction, without arranging any state: a derivation may READ files, but it may not
|
|
LIST a directory while it runs.
|
|
|
|
WHY THE ADD DIRECTION NEEDS ITS OWN TEST. Removing members from the index cannot see a source
|
|
that contributes ONLY untracked members: it adds and never takes away, so nothing of its is
|
|
available to remove. `.husky/_/` is that shape exactly — 17 shims `npm ci` writes, none ever in
|
|
the index — and it is the shape #778 shipped. Scope the claim precisely: a source appending
|
|
`(REPO_ROOT / ".husky" / "_").rglob("*")` leaves the removal proof green ON A MACHINE WHERE THAT
|
|
DIRECTORY IS ABSENT, which is the `script-tests` checkout. Where the shims exist, the appended
|
|
members are present and removal reddens too. The blind spot is an append-only source that yields
|
|
nothing HERE — which is exactly the CI shape, and exactly where a guard going quiet matters.
|
|
|
|
WHY THE PROPERTY IS "DOES NOT ENUMERATE" RATHER THAN "DOES NOT RETURN AN UNTRACKED FILE". Both
|
|
obvious formulations of the latter are machine-dependent, which is the very fault #806 exists to
|
|
remove:
|
|
|
|
* creating real probe files in the checkout and requiring they not enter. That needs a
|
|
`test_*.py` probe to reach the `scripts/tests` scope — a file pytest COLLECTS mid-session —
|
|
and its parametrised names collide across `-n auto` workers and concurrent sessions,
|
|
`finally` does not survive SIGKILL, and a concurrent `git add -A` can stage one. Defects in
|
|
the test, not in the thing tested.
|
|
* neutralising the shared primitive and requiring the derivation to go empty. That misses the
|
|
`.husky/_/` source on any machine where `.husky/_/` does not exist — which is every CI
|
|
checkout of `script-tests`, which never runs `npm ci`. Green where it runs, red only on a
|
|
laptop: the inverted asymmetry again, inside the proof written to abolish it.
|
|
|
|
Watching for the CALL needs no arranged state: an `rglob` issued while deriving is caught even
|
|
where the directory it walks is empty, because the evidence is the call rather than what it
|
|
returned. Reading is untouched, so a derivation may still parse the workflow bodies it scrapes
|
|
for referenced scripts.
|
|
|
|
WHAT IT DOES NOT COVER. The boundary is not "synchronous", which is what two earlier drafts
|
|
said and what measurement disproved — a thread that outlives the `derive()` call but finishes
|
|
while its result is being drained IS caught, as is a `__del__` firing during that drain. The
|
|
boundary is mechanical rather than temporal: a call to one of the SPIES is observed, wherever and
|
|
whenever it happens in this process before the assertion below. "While the patch is active"
|
|
under-claims it — the spy appends to a list that outlives the patch, so a reference captured
|
|
during the window and invoked after it still records. What decides observation is whether the
|
|
call goes through a spy, not when. The result is drained while the patch is installed, so a lazy
|
|
generator is reached.
|
|
|
|
NOT REACHED, because no spy was ever installed on that path. Enumeration HOISTED TO MODULE SCOPE
|
|
runs at import, before this test exists — the likeliest instance rather than a contrivance, since
|
|
`test_ci_image_pin_population.py` already precomputes `_DOC` that way — as does an `atexit` hook,
|
|
and a cached property warmed by the baseline call below. `from os import listdir` binds the real
|
|
function before any patch; `from os import walk` IS caught (it routes through the patched
|
|
`os.scandir`) and so is `from glob import glob` (through the patched `glob.iglob`). And anything
|
|
listing in ANOTHER PROCESS — a deliberate `subprocess.run(["ls"])`, or a forked child. This is a
|
|
regression guard against the shapes that arrive by accident, not a sandbox.
|
|
|
|
It cuts the other way too: any spy call at all reddens this test, so unrelated background thread
|
|
activity touching a patched name during the window would too. Nothing in this suite does that
|
|
today, and the report names the call, so a false red would be diagnosable rather than
|
|
mysterious.
|
|
|
|
It also cannot see a derivation that admits a HARDCODED path without listing anything
|
|
(`if (REPO_ROOT / "x.sh").exists(): add`) — listing is the commonest way to discover an untracked
|
|
member, not the only one. The removal proof above catches that shape, and catches memoisation,
|
|
which this one cannot: the baseline call below warms any cache outside the patch. The two are
|
|
complements.
|
|
"""
|
|
assert derive(), f"{label} derived nothing; this proof needs a baseline"
|
|
|
|
calls: list[str] = []
|
|
|
|
def _spy(what):
|
|
# RECORDS and returns empty rather than raising. Raising made the assertion "did an
|
|
# exception reach us", which a derivation defeats by catching it: a `try: ... except
|
|
# Exception: return set()` around an `rglob` enumerated the filesystem and this test passed,
|
|
# measured. The evidence is the CALL, so the call is what is asserted on.
|
|
def spy(*_args, **_kwargs):
|
|
calls.append(what)
|
|
# Iterator AND context manager: `os.scandir` is used as `with os.scandir(...) as it`,
|
|
# and a bare iterator made the failure report carry a test-induced TypeError about the
|
|
# context manager protocol alongside the real finding. The assertion fired correctly
|
|
# either way, but a report that blames the harness invites doubting the finding.
|
|
return _EmptyScan()
|
|
|
|
return spy
|
|
|
|
failure = None
|
|
with pytest.MonkeyPatch.context() as m:
|
|
for owner, name in _ENUMERATORS:
|
|
m.setattr(owner, name, _spy(f"{getattr(owner, '__name__', owner)}.{name}"), raising=False)
|
|
try:
|
|
# DRAINED inside the context, never `derive()` discarded. A derivation returning a lazy
|
|
# generator does its work when the caller drains it, so discarding the result moved the
|
|
# whole walk outside the patch: a generator yielding the index population and then
|
|
# appending `.husky/_` passed here and admitted 17 untracked shims on a checkout where
|
|
# that directory exists. `_as_relative_strings` is what drains it — it iterates into a
|
|
# set — so the call must stay here rather than being hoisted out or wrapped in something
|
|
# lazier.
|
|
_as_relative_strings(derive())
|
|
except BaseException as exc: # re-raised below, after the evidence has been judged
|
|
failure = exc
|
|
|
|
assert not calls, (
|
|
f"{label} enumerated the filesystem via {sorted(set(calls))} while deriving its population. "
|
|
"Directory listings report build output, generated shims and editor droppings, and differ "
|
|
"between the CI checkout and a developer's, so the member set stops being a property of the "
|
|
f"repo (ersatztv#778, #806)." + (f" It also raised: {failure!r}" if failure is not None else "")
|
|
)
|
|
if failure is not None:
|
|
raise failure
|
|
|
|
|
|
@pytest.mark.parametrize(("label", "derive"), _PAIRS, ids=_IDS)
|
|
def test_every_derived_member_is_tracked(label, derive):
|
|
"""The same property as an invariant over the real tree, which is the form that catches a
|
|
refactor going back to a filesystem walk without also touching this file."""
|
|
tracked = set(tracked_files._git_ls_files())
|
|
stray = sorted(m for m in _as_relative_strings(derive()) if m not in tracked)
|
|
assert not stray, f"{label} contains untracked path(s): {stray}"
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# THE SCOPE MIRROR ABOVE IS ITSELF CHECKED
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
def _modules_importing_the_helper() -> set[str]:
|
|
"""Which `scripts/tests/test_*.py` import the shared derivation, by PARSING them.
|
|
|
|
`ast` rather than a substring scan, and the distinction is the point rather than tidiness. A
|
|
substring scan over source both EVADES and FALSELY FIRES here: `import scripts.tests.
|
|
tracked_files as tf` and `from scripts.tests import tracked_files as tf` escape a scan for
|
|
`"tracked_files import"`, while a comment merely citing `scripts/tests/tracked_files.py` matches
|
|
a scan for `"tracked_files."` and would redden a correct tree over prose.
|
|
`docs/decisions/records/testing/guard-derives-population-from-source.md` records why patching
|
|
such a predicate does not converge: it is not a parser. Python ships the parser, and a comment
|
|
is not a node at all.
|
|
|
|
WHAT THE PARSE DOES NOT REACH, stated rather than implied by the word "parsing": STATIC import
|
|
statements naming the helper. `importlib.import_module("scripts.tests.tracked_files")`, a
|
|
re-export through `scripts/tests/__init__.py`, and `from scripts.tests import *` are invisible,
|
|
verified by executing each. (`from scripts.tests.tracked_files import *` IS seen — it names the
|
|
module.) Those sit inside the same residual as a module that derives a population without the
|
|
helper at all — the residual named below — and no mechanical check closes it.
|
|
|
|
The FILE LIST is a filesystem walk on purpose, and it is not the defect this file forbids: it is
|
|
a superset check over what pytest itself collects, so an untracked stray `test_x.py` here makes
|
|
the guard MORE demanding, never blind. Using the index would let an unstaged new guard escape
|
|
registration, which is the wrong direction for a check about coverage.
|
|
"""
|
|
found = set()
|
|
for path in sorted(TESTS_DIR.glob("test_*.py")):
|
|
for node in ast.walk(ast.parse(path.read_text())):
|
|
# RESOLVE the name to an absolute module and compare exactly. Testing `base ==
|
|
# "tracked_files"` handled `from .tracked_files import x` but silently missed
|
|
# `from ..tests.tracked_files import x`, which resolves to the same helper — a false
|
|
# NEGATIVE, the direction that lets a module adopt the helper and escape registration.
|
|
# A suffix match instead over-accepts `from unrelated.package import tracked_files`,
|
|
# reddening a correct tree over a module this repo does not own. Resolution is the only
|
|
# form with neither failure: these files live in `scripts.tests`, so level 1 resolves to
|
|
# `scripts.tests` and level 2 to `scripts` — see the guard below for why there is no
|
|
# level 3.
|
|
if isinstance(node, ast.Import):
|
|
hit = any(a.name == _HELPER for a in node.names)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
if node.level > len(_PACKAGE):
|
|
# Beyond the top-level package: Python raises ImportError for this, so it cannot
|
|
# be an import of the helper. Guarded explicitly because `_PACKAGE[:negative]`
|
|
# silently WRAPS — level 4 produced the same prefix as level 2 — which pinned an
|
|
# unimportable form as a valid detection. Valid levels here are exactly 1 and 2:
|
|
# 1 resolves to `scripts.tests`, 2 to `scripts`, and 3 or more is beyond the
|
|
# top-level package, which Python refuses.
|
|
continue
|
|
prefix = _PACKAGE[: len(_PACKAGE) - (node.level - 1)] if node.level else []
|
|
base_parts = prefix + ([node.module] if node.module else [])
|
|
base = ".".join(base_parts)
|
|
hit = base == _HELPER or any(f"{base}.{a.name}" == _HELPER for a in node.names)
|
|
else:
|
|
continue
|
|
if hit:
|
|
found.add(path.name)
|
|
break
|
|
return found
|
|
|
|
|
|
# (source, should the matcher see it). Every row is a form that has actually been mis-classified; the
|
|
# table is here so the next edit to `_modules_importing_the_helper` cannot re-open one silently.
|
|
# A false NEGATIVE lets a module adopt the helper and escape registration; a false POSITIVE reddens
|
|
# a correct tree over a module this repo does not own. Both directions are pinned.
|
|
_IMPORT_FORMS = (
|
|
("import scripts.tests.tracked_files as tf", True),
|
|
("from scripts.tests import tracked_files as tf", True),
|
|
("from scripts.tests.tracked_files import tracked_paths", True),
|
|
("from scripts.tests.tracked_files import *", True),
|
|
("from . import tracked_files", True),
|
|
("from .tracked_files import tracked_paths", True),
|
|
("from ..tests.tracked_files import tracked_paths", True),
|
|
# Beyond the top-level package from `scripts.tests`: Python raises ImportError, so there is
|
|
# nothing to detect. Pinned False so the negative-slicing wrap that once made it look
|
|
# detectable cannot come back.
|
|
("from ...scripts.tests.tracked_files import tracked_paths", False),
|
|
# Witnesses the negative-slice wrap specifically: without the level guard this one
|
|
# resolves through `scripts` and matches.
|
|
("from ....tests.tracked_files import tracked_paths", False),
|
|
("def f():\n from scripts.tests import tracked_files\n return tracked_files", True),
|
|
("from unrelated.package import tracked_files", False),
|
|
("from ..something import tracked_files", False),
|
|
("from .something import tracked_files", False),
|
|
("from ..other.tracked_files import x", False),
|
|
("# see scripts/tests/tracked_files.py for the rationale\nimport re", False),
|
|
# Pins PARSER VISIBILITY, not importability: executing this line really does import the helper.
|
|
# The row records that a static parse cannot see it — a known gap, pinned so it is not a
|
|
# surprise — and closing it would mean updating this row, which is the intended friction.
|
|
("import importlib\nimportlib.import_module('scripts.tests.tracked_files')", False),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(("source", "expected"), _IMPORT_FORMS, ids=[s.splitlines()[0][:48] for s, _ in _IMPORT_FORMS])
|
|
def test_the_import_matcher_classifies_every_reviewed_form(source, expected, monkeypatch):
|
|
class _Fake:
|
|
name = "test_probe.py"
|
|
|
|
def read_text(self):
|
|
return source
|
|
|
|
def __lt__(self, other):
|
|
return True
|
|
|
|
class _Dir:
|
|
def glob(self, _pattern):
|
|
return [_Fake()]
|
|
|
|
monkeypatch.setattr(sys.modules[__name__], "TESTS_DIR", _Dir())
|
|
seen = "test_probe.py" in _modules_importing_the_helper()
|
|
assert seen is expected, f"the import matcher {'missed' if expected else 'falsely matched'} this form:\n{source}"
|
|
|
|
|
|
def test_every_index_derived_module_is_registered():
|
|
"""`DERIVATIONS` is a hand-written mirror, so it gets an equality check rather than a promise.
|
|
|
|
Without this, adding another index-derived guard and forgetting to register it leaves that guard
|
|
unproven while this file reads as covering them all — a completeness claim standing behind a
|
|
hand-maintained list, which is the defect one altitude up (#773 Family C, and the shape that put
|
|
`MARKED_JOBS` in the record as a residual gap until #787 closed it).
|
|
|
|
THE SCOPE THIS CANNOT SEE, stated because a check described as complete stops being re-examined:
|
|
it detects modules that IMPORT the shared helper. A module deriving a file population some other
|
|
way — shelling out to `git ls-files` itself, or going back to `Path.rglob` — is invisible to it,
|
|
and no mechanical check can close that. `test_guard_inventory.py`'s own header argues the same
|
|
point about flagging filter-shaped guards by token, and #774 concluded there that the honest
|
|
answer is no. What is mechanised here is the case that actually recurs: someone adopts the
|
|
helper and forgets this file.
|
|
|
|
Registration is MODULE-level, not derivation-level, so a second population added inside an
|
|
already-registered module is covered only if it is registered too.
|
|
|
|
`POPULATION_EXEMPT` is the opt-out, and it lives HERE rather than as a marker comment in the
|
|
exempt file because the two directions are not symmetric: a false import-match only reddens,
|
|
while a false EXEMPTION is silent. A marker a file grants itself by containing a token is
|
|
trippable from that file's prose — this file's own error message names the token — so it would
|
|
be a one-line silent kill switch, the shape `test_ci_release_path_scan_job.py` argues against
|
|
for its recursion fence. Listing exemptions beside the registrations makes adding one a visible
|
|
edit here.
|
|
|
|
WHAT NO ASSERTION CAN DECIDE, dated so it is re-examined rather than assumed: whether an
|
|
exemption is still WARRANTED. A stale key and an unexplained one are both caught below, but an
|
|
exempt module that later grows a real derived population stays uncovered and silent. Reviewed
|
|
2026-08-22 — the single entry uses the helper only to assemble a tmp fixture copy and asserts
|
|
nothing about membership.
|
|
"""
|
|
exempt = {Path(__file__).name} | set(POPULATION_EXEMPT)
|
|
importers = _modules_importing_the_helper()
|
|
registered = {label.split(".", 1)[0] + ".py" for label, _, _ in DERIVATIONS}
|
|
|
|
# ANTI-VACUITY, and only that. A broken parse is caught loudly by `phantom` below — every
|
|
# registered module would go missing at once — so this is the cheaper, more specific signal, not
|
|
# the thing standing between a broken parse and a green run.
|
|
assert len(importers) >= len(DERIVATIONS), (
|
|
f"the import parse found only {sorted(importers)}, fewer modules than DERIVATIONS registers "
|
|
f"({sorted(registered)}) — the parse has broken."
|
|
)
|
|
|
|
stale = sorted(name for name in POPULATION_EXEMPT if name not in importers)
|
|
assert not stale, (
|
|
f"POPULATION_EXEMPT lists {stale}, which the parser no longer sees importing the shared "
|
|
"helper (renamed, "
|
|
"deleted, or the import removed). A stale exemption is worse than none: if the filename is "
|
|
"ever reused, the new module is exempt from birth without anyone deciding that."
|
|
)
|
|
thin = sorted(name for name, why in POPULATION_EXEMPT.items() if not str(why).strip())
|
|
assert not thin, f"POPULATION_EXEMPT entries with no stated reason: {thin}"
|
|
|
|
unregistered = sorted(importers - registered - exempt)
|
|
assert not unregistered, (
|
|
f"{unregistered} import the shared index derivation but are not in DERIVATIONS, so neither "
|
|
"proof in this file covers them. Add a named derivation function and register it, or add "
|
|
"the module to POPULATION_EXEMPT in this file if it imports the helper without deriving a "
|
|
"population."
|
|
)
|
|
phantom = sorted(registered - importers)
|
|
assert not phantom, (
|
|
f"DERIVATIONS registers {phantom}, which no longer import the shared helper. A row for "
|
|
"a derivation that is not there reads as coverage and is not."
|
|
)
|