fix(mutants): a surviving mutant exits 1 (H4)

`verdict(survived, errors)` is the one place the run's exit code is
decided: 2 when a mutant could not be applied (it was never measured,
and that outranks everything), 1 when one survived, 0 otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-19 05:49:59 +02:00
commit f5b263f5ef
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q

View file

@ -18,6 +18,10 @@ edited, so an interrupted run cannot leave a mutant behind. A pattern that does
not match the expected number of times is reported as an ERROR and the run not match the expected number of times is reported as an ERROR and the run
exits 2 -- a mutant that could not be applied was never measured, and counting exits 2 -- a mutant that could not be applied was never measured, and counting
it as killed is the same mistake as reading an empty search as an absence. it as killed is the same mistake as reading an empty search as an absence.
A SURVIVOR EXITS 1. Until 2026-09-19 the run ended `2 if errors else 0`, so
`killed 0 of 1` with the survivor printed beside it was an exit 0 and nothing
could fail on the finding this harness exists to produce (H4).
""" """
from __future__ import annotations from __future__ import annotations
@ -26,6 +30,7 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@ -267,6 +272,20 @@ def _apply(text: str, mutant: Mutant) -> str:
return text.replace(mutant.old, mutant.new, 1 if mutant.first_only else -1) return text.replace(mutant.old, mutant.new, 1 if mutant.first_only else -1)
def verdict(survived: Sequence[str], errors: Sequence[str]) -> int:
"""The run's exit code: 0 clean, 1 a mutant survived, 2 one was not measured.
Until 2026-09-19 this was `2 if errors else 0`, so a run that printed
`killed 0 of 1` and named its survivor exited 0 and no caller could fail
on it (H4). A mutant that survived names a check nothing holds, which is
the finding this harness exists to produce; a mutant that could not be
applied was never measured at all, and that outranks it.
"""
if errors:
return 2
return 1 if survived else 0
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
del argv del argv
survived: list[str] = [] survived: list[str] = []
@ -311,7 +330,7 @@ def main(argv: list[str] | None = None) -> int:
print(f" survived: {label}") print(f" survived: {label}")
for problem in errors: for problem in errors:
print(f" ERROR: {problem}") print(f" ERROR: {problem}")
return 2 if errors else 0 return verdict(survived, errors)
if __name__ == "__main__": if __name__ == "__main__":