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
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.
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
@ -26,6 +30,7 @@ import shutil
import subprocess
import sys
import tempfile
from collections.abc import Sequence
from dataclasses import dataclass
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)
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:
del argv
survived: list[str] = []
@ -311,7 +330,7 @@ def main(argv: list[str] | None = None) -> int:
print(f" survived: {label}")
for problem in errors:
print(f" ERROR: {problem}")
return 2 if errors else 0
return verdict(survived, errors)
if __name__ == "__main__":