import json
import os
from pathlib import Path
import sys
EXIT_MEANINGS = {
1: "usage error",
4: "the tests fail without any mutation",
5: "the diff does not match this tree",
6: "the diff could not be parsed",
70: "internal error",
}
def fail(message):
print(f"::error title=Mutation testing::{message}")
raise SystemExit(1)
def lines(path):
return path.read_text().splitlines() if path.exists() else []
def main():
if len(sys.argv) not in (3, 4):
raise SystemExit(__doc__)
report = Path(sys.argv[1]) / "mutants.out"
status = int(sys.argv[2])
expected = int(sys.argv[3]) if len(sys.argv) == 4 else None
if status not in (0, 2, 3):
meaning = EXIT_MEANINGS.get(status, "unexpected exit code")
fail(f"cargo mutants exited {status}: {meaning}")
outcomes = json.loads((report / "outcomes.json").read_text())
tested = outcomes["total_mutants"]
if tested == 0:
fail("cargo mutants tested no mutants")
if expected is not None and tested != expected:
fail(f"cargo mutants tested {tested} mutants; the plan has {expected}")
missed = lines(report / "missed.txt")
timeouts = lines(report / "timeout.txt")
summary = (
f"{tested} mutants: {outcomes['caught']} caught, {len(missed)} missed, "
f"{len(timeouts)} timed out, {outcomes['unviable']} unviable"
)
print(summary)
if path := os.environ.get("GITHUB_STEP_SUMMARY"):
with open(path, "a") as out:
out.write(f"{summary}\n\n")
for title, names in (("Missed", missed), ("Timed out", timeouts)):
if names:
out.write(f"{title}:\n\n```\n" + "\n".join(names) + "\n```\n\n")
for name in timeouts:
print(f"::warning title=Mutant timed out::{name}")
if missed:
print("\n".join(missed))
fail(f"missed mutants: {len(missed)}; no test failed when they were applied")
main()