Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 18s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 9m2s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m30s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m10s
Every `MUTATION` row of `docs/guard-inventory.md` now carries a DECLARED clause mutation that is applied to an isolated copy of the repository on every suite run, with the row's own named test required to go red carrying a declared diagnostic. Manifest and MUTATION rows are compared for set equality both directions; the other 22 guards each carry a stated reason, compared the same way. Measured rather than assumed: 12 of 13 guards admit a single-clause mutation; `instrumentation_faults` does not, and that entry carries the surviving finer mutation, re-run every suite. Nine cold cross-family review rounds. Rounds 1, 2, 7 and 8 each found real mechanism defects — two mutations that measured nothing, an incomplete git-environment sanitisation, a reset that restored its own mutant, and a proof of that fix which was not itself isolated. All fixed and witnessed red. fixes #790 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
465 lines
25 KiB
Python
465 lines
25 KiB
Python
"""Every `MUTATION` row of `docs/guard-inventory.md` is EXECUTED, not asserted (ersatztv#790).
|
|
|
|
The `MUTATION` grade means "a clause-level mutation was executed and this named test was witnessed
|
|
red". Witnessed once, by hand, that is evidence about the day the row was written and nothing else:
|
|
it decays as soon as the guard is edited, and a wrong grade has no way to announce itself.
|
|
|
|
So this file re-runs every one of them: for each declared mutation in `mutation_manifest.py`, apply
|
|
it to an isolated copy of this repository and require the row's OWN named test to go red.
|
|
|
|
WHAT A GREEN RUN HERE DOES AND DOES NOT PROVE — stated because a mutation harness that overclaims is
|
|
the same defect one level up:
|
|
|
|
* It proves the recorded proof ref names a test that EXISTS, still collects, and still reacts to
|
|
the declared clause. That is precisely the decay #790 was filed about.
|
|
* It proves the declared clause still occurs, exactly once, in the entry's declared `target` —
|
|
which is not always the guard's own file. A clause that has been reworded fails here rather than
|
|
silently mutating nothing.
|
|
* Combined with the positive control below, it proves every named proof test is GREEN on the
|
|
unmutated sandbox — so "the mutation was noticed" cannot be confused with "the test was already
|
|
red". For the proof tests that perform a disarm of their own, that control also runs the disarm;
|
|
the two named tests that are plain production set-equality checks have no disarm of their own,
|
|
and this harness supplies theirs.
|
|
* It does NOT prove the declared clause is the ONLY thing the guard hangs on. Some entries redden
|
|
through the proof test's own "the clause has moved, RETARGET this" assertion rather than through
|
|
changed behaviour. That is the intended reading, not a hole: those tests perform their
|
|
behavioural disarm themselves on every green run, and what they could not do is notice their own
|
|
clause reference going stale. Which entries those are is a dated measurement and lives in
|
|
`docs/decisions/records/testing/mutation-claims-are-executed.md`, not here.
|
|
|
|
The `granularity` column is where this file refuses to flatter itself. See `Mutation` in
|
|
`mutation_harness_lib.py`: all but one guard admits a single-clause mutation, and the one that does
|
|
not CARRIES the finer mutation that survived, which is re-run and required to keep surviving.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts.tests.mutation_harness_lib import (
|
|
_BASELINES,
|
|
Mutation,
|
|
_git,
|
|
build_sandbox,
|
|
reset_sandbox,
|
|
run_pytest,
|
|
verify_mutation,
|
|
)
|
|
from scripts.tests.mutation_manifest import MUTATIONS, UNDECLARED
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
INVENTORY = REPO_ROOT / "docs" / "guard-inventory.md"
|
|
|
|
# `| `guard` | blocks | kind | proof | proof ref |` — the five-column inventory row. The column
|
|
# count is what excludes the two-column tables elsewhere in the file; `test_the_inventory_rows_parse`
|
|
# below is the floor that catches the shape changing under it.
|
|
_ROW = re.compile(r"^\|\s*`([^`]+)`\s*\|([^|]*)\|\s*(\w[\w-]*)\s*\|\s*([A-Z-]+)\s*\|\s*(.*?)\s*\|\s*$", re.M)
|
|
|
|
|
|
def _inventory_rows() -> list[tuple[str, str, str, str]]:
|
|
"""(guard, kind, proof grade, proof ref) for every inventory row.
|
|
|
|
Derived from the document the grades live in, never hand-listed: the whole point is that a row
|
|
cannot change its grade without this file noticing.
|
|
"""
|
|
rows = [(m.group(1), m.group(3), m.group(4), m.group(5).strip("`")) for m in _ROW.finditer(INVENTORY.read_text())]
|
|
guards = [g for g, _k, _p, _r in rows]
|
|
duplicates = sorted({g for g in guards if guards.count(g) > 1})
|
|
assert not duplicates, (
|
|
f"the inventory lists {duplicates} more than once. Every comparison below reduces rows "
|
|
"through a set or a dict, so a duplicate row is INVISIBLE to this file — it would report all "
|
|
"entries verified over a table that contradicts itself. `test_guard_inventory.py` catches "
|
|
"this too, but only when the whole suite runs, and this file is routinely run alone."
|
|
)
|
|
assert len(rows) >= 50, (
|
|
f"only parsed {len(rows)} inventory rows — the table's shape has changed and this regex now "
|
|
"reads a fraction of it. Every set comparison below would then pass over a population that "
|
|
"is mostly missing, which is the vacuous-completeness failure the inventory itself exists to "
|
|
"prevent."
|
|
)
|
|
return rows
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# THE MANIFEST IS PINNED TO THE INVENTORY — a grade cannot change without this file changing
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_manifest_covers_exactly_the_MUTATION_rows():
|
|
"""Set equality, both directions, because they are different defects.
|
|
|
|
A `MUTATION` row with no manifest entry is a claim nothing checks — the state #790 was filed
|
|
about. A manifest entry for a row that is no longer graded `MUTATION` is a check whose subject
|
|
has moved out from under it, and it would keep passing.
|
|
"""
|
|
graded = {guard for guard, _kind, grade, _ref in _inventory_rows() if grade == "MUTATION"}
|
|
declared = {m.guard for m in MUTATIONS}
|
|
assert graded, "no row in the inventory is graded MUTATION, so this whole file would prove nothing"
|
|
assert declared - graded == set(), (
|
|
f"declared mutations for rows that are not graded MUTATION: {sorted(declared - graded)}"
|
|
)
|
|
assert graded - declared == set(), (
|
|
f"these rows claim a MUTATION proof with nothing executing it: {sorted(graded - declared)}. "
|
|
"Declare the clause in mutation_manifest.py, or regrade the row."
|
|
)
|
|
assert len(MUTATIONS) == len(declared), "two manifest entries name the same guard"
|
|
|
|
|
|
def test_every_declared_mutation_names_the_row_s_OWN_proof_ref():
|
|
"""The manifest may not point at a different test than the row does.
|
|
|
|
Without this, the inventory could keep citing a stale proof while the harness quietly exercised
|
|
a healthier one, and the row would read as verified.
|
|
"""
|
|
refs = {guard: ref for guard, _kind, grade, ref in _inventory_rows() if grade == "MUTATION"}
|
|
wrong = [(m.guard, m.proof, refs.get(m.guard)) for m in MUTATIONS if refs.get(m.guard) != m.proof]
|
|
assert not wrong, f"manifest proof ref disagrees with the inventory row: {wrong}"
|
|
|
|
|
|
def test_every_GUARD_row_is_either_DECLARED_or_STATED_here():
|
|
"""`Done-when`: guards whose mutation cannot be declared are STATED, not silently skipped.
|
|
|
|
Set equality against the inventory's GUARD rows, both directions, keyed on the guard. Keying on
|
|
the row's GRADE instead would be cheaper and tautological — a new guard graded NONE would inherit
|
|
a reason automatically and nobody would look at it — and a COUNT moves only on net change, so one
|
|
guard arriving as another is promoted leaves it unchanged. This is the same hand-maintained,
|
|
machine-checked shape as `docs/guard-inventory.md` itself, which is what makes it safe.
|
|
|
|
The partition covers `Kind == GUARD` rows only. `TOOLING` asserts nothing and `PROOF` files exist
|
|
to prove other guards, so neither carries a mutation claim to verify — a rule about the Kind
|
|
column rather than a list anyone maintains.
|
|
"""
|
|
guards = {guard for guard, kind, _grade, _ref in _inventory_rows() if kind == "GUARD"}
|
|
declared = {m.guard for m in MUTATIONS}
|
|
stated = set(UNDECLARED)
|
|
|
|
assert not (declared & stated), (
|
|
f"these guards are both declared and stated as undeclared: {sorted(declared & stated)}"
|
|
)
|
|
unaccounted = sorted(guards - declared - stated)
|
|
assert not unaccounted, (
|
|
f"these guards are neither declared nor stated: {unaccounted}. Declare the clause in "
|
|
"mutation_manifest.py, or write a line in UNDECLARED saying what a proof would need — "
|
|
"silence is the one option that is not available."
|
|
)
|
|
orphaned = sorted(stated - guards)
|
|
assert not orphaned, (
|
|
f"UNDECLARED names rows that are not GUARD-kind rows any more: {orphaned}. A reason for a "
|
|
"guard that no longer exists reads as coverage."
|
|
)
|
|
thin = [g for g, reason in UNDECLARED.items() if len(reason.strip()) < 60]
|
|
assert not thin, f"these UNDECLARED entries say nothing a reader could act on: {thin}"
|
|
|
|
|
|
def test_every_entry_declares_a_known_granularity_and_DETECTOR_entries_CARRY_their_survivor():
|
|
"""`DETECTOR` is an admission, and an unevidenced one would be a grading curve.
|
|
|
|
#790's complaint about `pin_population_faults` — that neutering a whole helper is "coarse enough
|
|
that a single surviving clause would not be noticed" — applies to every entry graded here. So an
|
|
entry may only claim `DETECTOR` while carrying the finer mutation that was tried, as data rather
|
|
than as a sentence: `test_every_SURVIVING_clause_mutation_still_does` then runs it.
|
|
"""
|
|
bad = [m.guard for m in MUTATIONS if m.granularity not in (Mutation.CLAUSE, Mutation.DETECTOR)]
|
|
assert not bad, f"unknown granularity on {bad}; allowed: CLAUSE, DETECTOR"
|
|
assert all(m.why.strip() for m in MUTATIONS), "every declared mutation must say what its clause does"
|
|
|
|
unevidenced = [m.guard for m in MUTATIONS if m.granularity == Mutation.DETECTOR and not m.survived_clause]
|
|
assert not unevidenced, (
|
|
f"these entries claim DETECTOR granularity while naming no finer mutation that was tried: "
|
|
f"{unevidenced}. Carry the survivor, or declare the finer clause instead."
|
|
)
|
|
misplaced = [m.guard for m in MUTATIONS if m.granularity == Mutation.CLAUSE and m.survived_clause]
|
|
assert not misplaced, (
|
|
f"these entries are graded CLAUSE but carry a surviving finer mutation: {misplaced}. If a "
|
|
"finer clause exists and survives, the grade is DETECTOR."
|
|
)
|
|
half = [m.guard for m in MUTATIONS if bool(m.survived_clause) != bool(m.survived_replacement)]
|
|
assert not half, f"a survivor needs both a clause and a replacement: {half}"
|
|
|
|
|
|
def test_every_entry_declares_a_SPECIFIC_diagnostic_it_must_redden_with():
|
|
"""`expect` is what stops exit code 1 from being the whole verdict, so it cannot be a token.
|
|
|
|
An empty or near-empty expectation matches any output and hands the verdict straight back to the
|
|
exit status — the state this field exists to leave. It must also be a substring of no other
|
|
entry's, or two rows could be satisfied by one another's diagnostic.
|
|
"""
|
|
vague = [(m.guard, m.expect) for m in MUTATIONS if len(m.expect.strip()) < 20]
|
|
assert not vague, f"these expectations are too weak to distinguish one red from another: {vague}"
|
|
for m in MUTATIONS:
|
|
clashes = [o.guard for o in MUTATIONS if o is not m and m.expect in o.expect]
|
|
assert not clashes, f"{m.guard}'s expectation is contained in {clashes}'s — neither is specific"
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# THE EXECUTION — one isolated repository, reused, reset between mutations
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def sandbox(tmp_path_factory):
|
|
"""An isolated copy of this repository, with the POSITIVE CONTROL already run in it.
|
|
|
|
The control is inside the fixture rather than in a test of its own so ordering is a dependency
|
|
rather than a convention: no mutation can be judged before every proof test has been shown green
|
|
on the unmutated tree. Without it, "the named test went red" is satisfied just as well by a proof
|
|
test that was already broken, and reporting that as a verified mutation is the failure mode this
|
|
whole file exists to remove.
|
|
"""
|
|
sb = build_sandbox(tmp_path_factory.mktemp("mutation-sandbox"))
|
|
# The sandbox holds what GIT TRACKS, so a proof test in a file that has never been `git add`ed is
|
|
# simply absent there and the run below reports "file or directory not found" — accurate, and
|
|
# unreadable as a diagnosis. Named here instead, because this is what a developer adding a guard
|
|
# hits first.
|
|
missing = sorted({m.node_id.split("::")[0] for m in MUTATIONS if not (sb / m.node_id.split("::")[0]).is_file()})
|
|
assert not missing, (
|
|
f"these proof files are not in the sandbox: {missing}. The sandbox is derived from "
|
|
"`git ls-files`, so an unstaged new file is not in it — `git add` it and re-run."
|
|
)
|
|
result = run_pytest(sb, [m.node_id for m in MUTATIONS])
|
|
assert result.returncode == 0, (
|
|
"the proof tests named by the inventory are NOT green on an unmutated copy of this "
|
|
"repository, so nothing below can distinguish 'the mutation was noticed' from 'the test was "
|
|
f"already red'. Fix them first.\n{result.stdout[-4000:]}{result.stderr[-2000:]}"
|
|
)
|
|
assert "passed" in result.stdout, f"the control run collected nothing: {result.stdout!r}"
|
|
try:
|
|
yield sb
|
|
finally:
|
|
# ~115 MiB of tracked files plus its own git objects. pytest keeps the last three sessions'
|
|
# tmp dirs by default, so leaving it costs a third of a gigabyte on a developer machine that
|
|
# runs this a few times.
|
|
shutil.rmtree(sb, ignore_errors=True)
|
|
|
|
|
|
def test_the_reset_restores_the_BASELINE_even_after_a_proof_COMMITS(sandbox):
|
|
"""One sandbox serves every mutation, so the reset has to be a reset to a fixed point.
|
|
|
|
`git reset --hard` with no argument resets to whatever HEAD currently is. A proof test that
|
|
commits inside the sandbox — several drive `git commit` for real — moves HEAD onto a commit
|
|
carrying whatever was in the tree at the time, and every later reset would faithfully restore
|
|
THAT. The contamination surfaces as an unrelated red several mutations further on, which is the
|
|
hardest kind of harness defect to attribute.
|
|
"""
|
|
subject = sandbox / "docs" / "guard-inventory.md"
|
|
baseline = subject.read_text()
|
|
|
|
subject.write_text(baseline + "\n<!-- planted, then COMMITTED -->\n")
|
|
_git(sandbox, "add", "-A")
|
|
_git(sandbox, "commit", "-qm", "a proof test committing inside the sandbox")
|
|
assert subject.read_text() != baseline, "the planted change did not land, so this proves nothing"
|
|
|
|
reset_sandbox(sandbox)
|
|
|
|
assert subject.read_text() == baseline, (
|
|
"the reset restored the sandbox to a commit made DURING a mutation rather than to the "
|
|
"pristine baseline, so every later verdict is computed against a contaminated tree"
|
|
)
|
|
# Through `_git`, not a raw `subprocess.run`: an ambient `GIT_DIR` — which a git hook exports,
|
|
# and this suite runs from one — would resolve this against the REAL repository and compare two
|
|
# commits that have nothing to do with the sandbox. Asserting isolation with an unisolated call
|
|
# is the defect this harness exists to catch, one level up.
|
|
head = _git(sandbox, "rev-parse", "HEAD").stdout.decode().strip()
|
|
assert head == _BASELINES[str(sandbox.resolve())], f"HEAD was left off the recorded baseline commit: {head}"
|
|
|
|
|
|
@pytest.mark.parametrize("mutation", MUTATIONS, ids=lambda m: m.guard)
|
|
def test_MUTATION_the_declared_clause_reddens_the_named_proof(sandbox, mutation):
|
|
reset_sandbox(sandbox)
|
|
verdict = verify_mutation(sandbox, mutation)
|
|
assert verdict.ok, f"{mutation.guard}: {verdict.reason}"
|
|
|
|
|
|
_SURVIVORS = tuple(m for m in MUTATIONS if m.granularity == Mutation.DETECTOR)
|
|
|
|
|
|
@pytest.mark.parametrize("mutation", _SURVIVORS, ids=lambda m: m.guard)
|
|
def test_every_SURVIVING_clause_mutation_still_does(sandbox, mutation):
|
|
"""The DETECTOR grade, executed rather than recited.
|
|
|
|
A finer mutation that has since STARTED reddening the proof test means the guard now admits
|
|
clause-level proof and the entry should be regraded — the coarse grade would otherwise persist as
|
|
an excuse long after the reason for it went away. Failing here is therefore good news; it just
|
|
has to be acted on.
|
|
"""
|
|
reset_sandbox(sandbox)
|
|
finer = replace(
|
|
mutation,
|
|
clause=mutation.survived_clause,
|
|
replacement=mutation.survived_replacement,
|
|
survived_clause="",
|
|
survived_replacement="",
|
|
)
|
|
verdict = verify_mutation(sandbox, finer)
|
|
assert not verdict.ok, (
|
|
f"{mutation.guard} is graded DETECTOR because {mutation.survived_clause!r} was tried and left "
|
|
"the proof test green — but it reddens it now. Regrade the entry to CLAUSE with that mutation."
|
|
)
|
|
assert "still PASSED" in verdict.reason, (
|
|
f"the survivor did not survive for the recorded reason — it failed with: {verdict.reason}"
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------
|
|
# THIS GUARD'S OWN MUTATION PROOF — the redness clause, disarmed, on a sandbox of two files
|
|
# ------------------------------------------------------------------------------------------------
|
|
|
|
EXIT_STATUS_CLAUSE = " if result.returncode != 1:"
|
|
DIAGNOSTIC_CLAUSE = " if mutation.expect not in diagnostic:"
|
|
LIB = REPO_ROOT / "scripts" / "tests" / "mutation_harness_lib.py"
|
|
|
|
|
|
def _inert_sandbox(tmp_path: Path, body: str = " assert True\n") -> tuple[Path, Mutation]:
|
|
"""A minimal synthetic sandbox holding one test file, whose outcome is fixed by `body`.
|
|
|
|
Deliberately not a copy of the repo: the subject here is `verify_mutation`'s verdict, and a real
|
|
sandbox would cost four seconds to prove something about two lines of control flow.
|
|
"""
|
|
tests = tmp_path / "scripts" / "tests"
|
|
tests.mkdir(parents=True)
|
|
(tests / "test_inert.py").write_text(f"# INERT MARKER\n\n\ndef test_ok():\n{body}")
|
|
return tmp_path, Mutation(
|
|
guard="inert",
|
|
target="scripts/tests/test_inert.py",
|
|
clause="# INERT MARKER",
|
|
replacement="# INERT MARKER, CHANGED",
|
|
proof="test_inert.py::test_ok",
|
|
granularity=Mutation.CLAUSE,
|
|
# A legal expectation, not the empty string: an empty one is a shape the manifest forbids,
|
|
# so a fixture relying on it would be proving something about a configuration that cannot
|
|
# ship. Each caller that needs a different one passes it through `replace`.
|
|
expect="a diagnostic no run of this fixture produces",
|
|
why="an inert mutation: the named test cannot notice it",
|
|
)
|
|
|
|
|
|
def _lib_with(tmp_path: Path, clause: str, replacement: str, label: str):
|
|
"""Import a copy of the library with one clause replaced.
|
|
|
|
The copy is registered in `sys.modules` before execution: `@dataclass` resolves a string
|
|
annotation through `sys.modules[cls.__module__]`, so an unregistered module raises AttributeError
|
|
on the first dataclass it defines rather than on anything to do with the clause.
|
|
"""
|
|
source = LIB.read_text()
|
|
assert source.count(clause) == 1, (
|
|
f"the {label} has moved or been reworded; RETARGET this mutation rather than loosening the "
|
|
"match — and update the mutation_manifest entry for this file, which names the same string"
|
|
)
|
|
path = tmp_path / f"mutant_{label.replace(' ', '_')}.py"
|
|
path.write_text(source.replace(clause, replacement, 1))
|
|
|
|
spec = importlib.util.spec_from_file_location(path.stem, path)
|
|
mutant = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = mutant
|
|
try:
|
|
spec.loader.exec_module(mutant)
|
|
finally:
|
|
sys.modules.pop(spec.name, None)
|
|
return mutant
|
|
|
|
|
|
def test_an_INERT_mutation_is_REPORTED_rather_than_passed(tmp_path):
|
|
"""The behavioural half. A harness that cannot tell a real disarm from a comment edit would
|
|
report every row verified on any tree at all."""
|
|
sb, inert = _inert_sandbox(tmp_path)
|
|
verdict = verify_mutation(sb, inert)
|
|
assert not verdict.ok, "a mutation the named test cannot possibly notice was reported as verified"
|
|
assert "still PASSED" in verdict.reason, verdict.reason
|
|
|
|
|
|
def test_a_red_for_the_WRONG_REASON_is_not_accepted(tmp_path):
|
|
"""The gate `expect` exists for. Pytest reports an ordinary exception exactly as it reports a
|
|
failed assertion, so a mutation that CRASHES the proof test looks identical to one it detected.
|
|
A verdict that cannot tell those apart certifies rows on evidence about nothing."""
|
|
sb, inert = _inert_sandbox(tmp_path, " raise RuntimeError('an unrelated crash')\n")
|
|
crashing = replace(inert, expect="the diagnostic this row is supposed to produce")
|
|
verdict = verify_mutation(sb, crashing)
|
|
assert not verdict.ok, "a red with nothing to do with the declared diagnostic was accepted"
|
|
assert "NOT with the declared diagnostic" in verdict.reason, verdict.reason
|
|
|
|
|
|
def test_MUTATION_disarming_the_DIAGNOSTIC_gate_accepts_a_red_for_the_wrong_reason(tmp_path):
|
|
"""The clause-level proof, on the newer of the two gates every verdict passes through.
|
|
|
|
Disarm the check that the failure carries the declared diagnostic, and the crashing case above
|
|
must start reporting as verified. If it does not, that rejection is coming from somewhere other
|
|
than the clause, and the test above proves nothing about it.
|
|
"""
|
|
sb, inert = _inert_sandbox(tmp_path, " raise RuntimeError('an unrelated crash')\n")
|
|
crashing = replace(inert, expect="the diagnostic this row is supposed to produce")
|
|
assert not verify_mutation(sb, crashing).ok, (
|
|
"the UNMUTATED verdict already accepted it, so the mutant proves nothing"
|
|
)
|
|
|
|
mutant = _lib_with(tmp_path, DIAGNOSTIC_CLAUSE, " if False:", "diagnostic gate")
|
|
assert mutant.verify_mutation(sb, crashing).ok, (
|
|
"the diagnostic gate was replaced with a constant and a red for an unrelated reason was "
|
|
"STILL rejected, so the verdict does not hang on the clause that reads it"
|
|
)
|
|
|
|
|
|
def test_MUTATION_disarming_the_EXIT_STATUS_gate_accepts_a_run_that_NEVER_RAN_A_TEST(tmp_path):
|
|
"""The same proof for the older gate, and it has to be built carefully to isolate it.
|
|
|
|
A GREEN run cannot serve: it produces no exception output, so only an EMPTY expectation would
|
|
reach the status gate — and an empty expectation is a shape the manifest forbids, which would
|
|
make this a proof about a configuration that cannot ship. Instead the sandbox's test file fails
|
|
at IMPORT: pytest exits non-1 (nothing was collected, so nothing ran) while still printing the
|
|
exception, so a legal non-empty expectation matches and the exit status is the ONLY thing
|
|
rejecting it. That is the case this gate exists for — a proof ref that no longer names a
|
|
collectable test must not read as a guard going red.
|
|
"""
|
|
boom = "a deliberate import-time failure, which is not a test result"
|
|
sb, inert = _inert_sandbox(tmp_path)
|
|
(sb / "scripts" / "tests" / "test_inert.py").write_text(f"# INERT MARKER\nraise RuntimeError({boom!r})\n")
|
|
uncollectable = replace(inert, expect=boom)
|
|
|
|
verdict = verify_mutation(sb, uncollectable)
|
|
assert not verdict.ok, "a run in which no test executed was accepted as a guard going red"
|
|
assert len(uncollectable.expect) >= 20, "the expectation must be one the manifest would accept"
|
|
|
|
mutant = _lib_with(tmp_path, EXIT_STATUS_CLAUSE, " if False:", "exit status gate")
|
|
assert mutant.verify_mutation(sb, uncollectable).ok, (
|
|
"the exit-status gate was replaced with a constant and a run that never executed a test was "
|
|
"still rejected, so the verdict does not hang on the status it reads"
|
|
)
|
|
|
|
|
|
def test_a_clause_that_no_longer_OCCURS_ONCE_is_reported_rather_than_applied(tmp_path):
|
|
"""The failure paths, driven directly, because this is where a harness quietly stops harnessing.
|
|
|
|
A clause that has been reworded away, or that now matches a second site, must produce a verdict
|
|
naming the problem. Silently replacing nothing — or replacing the wrong site — would leave every
|
|
row in this file reporting verified while mutating something nobody declared.
|
|
"""
|
|
sb, inert = _inert_sandbox(tmp_path)
|
|
|
|
gone = replace(inert, clause="# A CLAUSE THAT IS NOT THERE")
|
|
assert not verify_mutation(sb, gone).ok
|
|
assert "occurs 0 times" in verify_mutation(sb, gone).reason
|
|
|
|
(sb / "scripts" / "tests" / "test_inert.py").write_text(
|
|
"# INERT MARKER\n# INERT MARKER\n\n\ndef test_ok():\n assert True\n"
|
|
)
|
|
assert "occurs 2 times" in verify_mutation(sb, inert).reason
|
|
|
|
absent = replace(inert, target="scripts/tests/no_such_file.py")
|
|
assert "does not exist in the sandbox" in verify_mutation(sb, absent).reason
|
|
|
|
|
|
def test_the_sandbox_is_left_UNCHANGED_by_a_verdict(tmp_path):
|
|
"""One sandbox serves every mutation, so a verdict that leaves its edit behind would make each
|
|
result a function of the ones before it."""
|
|
sb, inert = _inert_sandbox(tmp_path)
|
|
subject = sb / "scripts" / "tests" / "test_inert.py"
|
|
before = subject.read_text()
|
|
verify_mutation(sb, inert)
|
|
assert subject.read_text() == before, "verify_mutation left its mutation in the sandbox"
|