diff --git a/tests/test_boundaries.py b/tests/test_boundaries.py
new file mode 100644
index 0000000..bebde36
--- /dev/null
+++ b/tests/test_boundaries.py
@@ -0,0 +1,844 @@
+"""Boundaries the first mutation run left uncovered or alive.
+
+A site on a line coverage.py never emits stays uncovered; these tests do not
+reformat production code to expose those lines. Equivalent mutants are left
+alone when no input can tell the operators apart.
+"""
+
+import hashlib
+import sys
+from pathlib import Path
+
+import pytest
+
+from mutator.cli import (
+ _changed_files,
+ _is_source,
+ _prepare_coverage,
+ _take,
+ parse_args,
+ run,
+ select_files,
+)
+from mutator.coverage import _candidates, _jacoco_index, _suffix_hit, covered_lines
+from mutator.crapper_link import ensure_crapper
+from mutator.edn import loads
+from mutator.engine import (
+ _backup,
+ _carry_forward,
+ _covered,
+ _drop_bytecode,
+ _forms,
+ _restore_if_dirty,
+ mutate_file,
+ restore_backups,
+ scan_file,
+)
+from mutator.functions import (
+ _mutation_field,
+ _owner,
+ digest,
+ form_spans,
+ is_private,
+ mutation_file,
+)
+from mutator.metrics import (
+ _drop_stale,
+ _form_file,
+ _form_file_only,
+ _mentions_file,
+ _outcomes_of,
+ _remove_empty_dirs,
+ _write_namespace,
+ load_history,
+ snapshot_path,
+ write_results,
+)
+from mutator.model import FormResult, History, PriorForm, Site
+from mutator.report import format_results, format_scan
+from mutator.runner import CommandResult, CommandRunner, _clojure_command
+from mutator.runner import test_plan as command_for
+from mutator.sites import _grammar
+
+
+def _form(namespace, name, file_name, digest_text, killed=1, private=False, line=2):
+ form_id = f"defn-/{name}" if private else f"defn/{name}"
+ return FormResult(
+ id=form_id,
+ namespace=namespace,
+ name=name,
+ private=private,
+ file=file_name,
+ line=line,
+ end_line=line + 2,
+ digest=digest_text,
+ killed=killed,
+ survived=0,
+ uncovered=0,
+ sites=1,
+ )
+
+
+def _site(**overrides) -> Site:
+ values = dict(
+ file="src/demo.py",
+ namespace="demo",
+ form_id="defn/place",
+ name="place",
+ line=2,
+ start=0,
+ end=1,
+ original=">",
+ mutant=">=",
+ category="comparison",
+ mutation_id="m",
+ )
+ values.update(overrides)
+ return Site(**values)
+
+
+def _pairs(tmp_path: Path, relative: str, source: str):
+ from mutator.functions import sites_in_file
+
+ path = tmp_path / relative
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(source, encoding="utf-8")
+ sites = sites_in_file(source, path, tmp_path, relative)
+ return sites, {(site.original, site.mutant) for site in sites}
+
+
+class _Fn:
+ def __init__(self, name, start, end):
+ self.name = name
+ self.start_line = start
+ self.end_line = end
+
+
+class _Ok:
+ def __init__(self, seconds=0.01):
+ self.verbose = False
+ self.seconds = seconds
+ self.timeouts = []
+
+ def run(self, command, cwd, timeout):
+ self.timeouts.append(timeout)
+ code = 0 if timeout is None else 1
+ return CommandResult(code=code, timed_out=False, seconds=self.seconds, output="")
+
+
+def test_score_and_description_follow_the_counts():
+ even = _form("demo", "place", "a.py", "d", killed=1)
+ even.survived = 1
+ assert even.score == 0.5
+ blank = _form("demo", "place", "a.py", "d", killed=0)
+ assert blank.score is None
+ perfect = _form("demo", "place", "a.py", "d", killed=1)
+ assert perfect.score == 1.0
+ assert _site(original=">", mutant=">=").description == "> -> >="
+ assert _site(original="not", mutant="").description == "delete not"
+
+
+def test_report_text_states_the_score_the_total_and_coverage():
+ assert format_results([]) == "No functions to mutate.\n"
+ mixed = _form("demo", "place", "a.py", "d", killed=1)
+ mixed.survived = 1
+ mixed.sites = 2
+ text = format_results([mixed])
+ assert "50.0%" in next(line for line in text.splitlines() if "place" in line)
+ total = next(line for line in text.splitlines() if line.startswith("Total"))
+ assert total.split() == ["Total", "1", "1", "0", "2", "50.0%"]
+
+ perfect = _form("demo", "place", "a.py", "d", killed=1)
+ perfect_total = next(line for line in format_results([perfect]).splitlines() if line.startswith("Total"))
+ assert perfect_total.split()[-1] == "100.0%"
+ idle = _form("demo", "place", "a.py", "d", killed=0)
+ idle.sites = 2
+ idle.uncovered = 2
+ idle_total = next(line for line in format_results([idle]).splitlines() if line.startswith("Total"))
+ assert idle_total.split()[-1] == "n/a"
+
+ covered = format_scan("src/demo.py", [_site()], set(), {"m": True})
+ missing = format_scan("src/demo.py", [_site()], set(), {})
+ bare = format_scan("src/demo.py", [_site()], set(), {"m": False})
+ assert "uncovered" not in covered
+ assert "uncovered" not in missing
+ assert "uncovered" in bare
+
+
+def test_edn_errors_comments_sets_and_nil_keys():
+ with pytest.raises(ValueError, match="empty EDN"):
+ loads("")
+ with pytest.raises(ValueError, match="empty EDN"):
+ loads("; hi")
+ with pytest.raises(ValueError, match="unexpected end of EDN"):
+ loads("{")
+ with pytest.raises(ValueError, match="unterminated string"):
+ loads('"hi')
+ with pytest.raises(ValueError, match="unterminated string escape"):
+ loads('"\\')
+ with pytest.raises(ValueError, match="empty keyword"):
+ loads(":")
+ assert loads("; kept\n12") == 12
+ assert loads("#{1 2}") == {1, 2}
+ assert loads("{nil 1}") == {None: 1}
+
+
+def test_privacy_headers_digests_owners_and_mutation_fields(tmp_path):
+ assert is_private("python", "__init__", "def __init__(self):\n pass\n", 1, 2) is False
+ assert is_private("python", "_hide", "def _hide():\n return 1\n", 1, 2) is True
+ assert is_private("python", "__x", "def __x():\n return 1\n", 1, 2) is True
+ assert is_private("rust", "place", "fn place() {}\n", 1, 1) is True
+ assert is_private("rust", "place", "#[inline]\npub fn place() {}\n", 1, 2) is False
+ assert is_private("typescript", "#place", "#place() {}\n", 1, 1) is True
+ assert is_private("typescript", "place", "private place() {}\n", 1, 1) is True
+ assert is_private("typescript", "place", "function place() {}\n", 1, 1) is False
+ assert is_private("cobol", "place", "PLACE.\n", 1, 1) is False
+
+ source = "def place(x):\n return x > 0\n"
+ chunk = "def place(x):\n return x > 0"
+ assert digest(source, 1, 2) == hashlib.sha256(chunk.encode("utf-8")).hexdigest()
+
+ assert _owner([_Fn("outer", 1, 10), _Fn("inner", 8, 9)], 8).name == "inner"
+
+ path = tmp_path / "src" / "demo.py"
+ path.parent.mkdir(parents=True)
+ hidden = "def _hide():\n return 1\n"
+ path.write_text(hidden, encoding="utf-8")
+ assert list(form_spans(hidden, path, tmp_path).values())[0][3] is True
+
+ assert _mutation_field("not-json", 0) is None
+ assert _mutation_field('"hello"', 0) is None
+ assert _mutation_field("[1]", 0) is None
+ assert _mutation_field('["only"]', 1) is None
+ assert mutation_file('["src/a.py","demo","defn/place",1,2,">",">="]') == "src/a.py"
+
+
+def test_grammar_guards_and_non_head_clojure_symbols(tmp_path):
+ assert _grammar("typescript", "src/app.tsx") == "tsx"
+ assert _grammar("typescript", "src/app.ts") == "typescript"
+ assert _grammar("python", "src/app.py") == "python"
+
+ sites, pairs = _pairs(tmp_path, "src/demo/core.clj", "(ns demo.core)\n(defn place []\n [inc])\n")
+ assert "inc" not in {site.original for site in sites}
+ sites, _pairs_ignored = _pairs(tmp_path, "src/demo/add.clj", "(ns demo.core)\n(defn place []\n (+ + 1))\n")
+ assert sum(site.original == "+" and site.mutant == "-" for site in sites) == 1
+ sites, _pairs_ignored = _pairs(tmp_path, "src/demo/two.clj", "(ns demo.core)\n(defn place []\n 2)\n")
+ assert sites == []
+
+ _sites, pairs = _pairs(tmp_path, "src/demo/app.py", "def place(a, b):\n return a - b\n")
+ assert ("-", "+") in pairs
+ assert ("-", "") not in pairs
+ _sites, pairs = _pairs(tmp_path, "src/demo/name.py", "def place(true):\n return true\n")
+ assert ("true", "false") not in pairs
+ _sites, pairs = _pairs(tmp_path, "src/demo/two.py", "def place():\n return 2\n")
+ assert pairs == set()
+ sites, _pairs_ignored = _pairs(tmp_path, "src/lib.rs", "fn place() -> bool { true }\n")
+ assert sum(site.original == "true" and site.mutant == "false" for site in sites) == 1
+
+
+def test_coverage_indexes_use_package_names_form_counts_and_exact_paths(tmp_path):
+ assert _candidates(lambda value: value, "") == []
+ assert _candidates(lambda _value: "same", "src/app.py") == ["same"]
+ assert _suffix_hit("demo/Board.java", ["Other.java"], lambda left, right: left.endswith(right)) is False
+
+ report = tmp_path / "target" / "site" / "jacoco" / "jacoco.xml"
+ report.parent.mkdir(parents=True)
+ report.write_text(
+ """
+
+
+
+
+
+
+
+""",
+ encoding="utf-8",
+ )
+ source = tmp_path / "src" / "demo" / "Board.java"
+ source.parent.mkdir(parents=True)
+ source.write_text("package demo;\nclass Board {}\n", encoding="utf-8")
+ assert "demo/Board.java" in _jacoco_index(tmp_path)
+ assert covered_lines(tmp_path, source, "java") == {6}
+
+ html = tmp_path / "target" / "coverage" / "src" / "demo" / "core.clj.html"
+ html.parent.mkdir(parents=True)
+ html.write_text(
+ '4 \n',
+ encoding="utf-8",
+ )
+ lcov = tmp_path / "target" / "coverage" / "clojure" / "lcov.info"
+ lcov.parent.mkdir(parents=True)
+ lcov.write_text("SF:src/demo/core.clj\nDA:9,1\nend_of_record\n", encoding="utf-8")
+ clojure = tmp_path / "src" / "demo" / "core.clj"
+ clojure.write_text("(ns demo.core)\n(defn place [] 1)\n", encoding="utf-8")
+ assert covered_lines(tmp_path, clojure, "clojure") == {4}
+
+
+def test_commands_follow_the_nearest_project_and_a_killed_process(tmp_path):
+ assert CommandRunner().verbose is False
+ both = tmp_path / "both"
+ both.mkdir()
+ (both / "bb.edn").write_text("{:tasks {test (clojure)}}\n", encoding="utf-8")
+ (both / "deps.edn").write_text("{:deps {}}\n", encoding="utf-8")
+ assert _clojure_command(both) == "clj -M:test"
+
+ root = tmp_path / "proj"
+ sub = root / "svc"
+ sub.mkdir(parents=True)
+ (sub / "bb.edn").write_text("{:tasks {test (clojure)}}\n", encoding="utf-8")
+ clojure = sub / "src" / "a.clj"
+ clojure.parent.mkdir(parents=True)
+ clojure.write_text("(defn place [] 1)\n", encoding="utf-8")
+ command, directory = command_for(root, clojure, "clojure", None)
+ assert directory == sub
+ assert command == "bb test"
+
+ (sub / "pom.xml").write_text("\n", encoding="utf-8")
+ java = sub / "src" / "A.java"
+ java.write_text("class A { int place(){ return 1; } }\n", encoding="utf-8")
+ command, directory = command_for(root, java, "java", None)
+ assert directory == sub
+ assert command == "mvn -q test -DexcludeTags=no-mutate"
+
+ (sub / "package.json").write_text("{}\n", encoding="utf-8")
+ typescript = sub / "src" / "a.ts"
+ typescript.write_text("export function place(){ return 1; }\n", encoding="utf-8")
+ command, directory = command_for(root, typescript, "typescript", None)
+ assert directory == sub
+ assert command == "npm test"
+
+ (sub / "Cargo.toml").write_text("[package]\nname='demo'\n", encoding="utf-8")
+ rust = sub / "src" / "lib.rs"
+ rust.write_text("fn place() -> i32 { 1 }\n", encoding="utf-8")
+ command, directory = command_for(root, rust, "rust", None)
+ assert directory == sub
+ assert command == "cargo test"
+
+ (sub / "go.mod").write_text("module example.com/demo\n", encoding="utf-8")
+ go = sub / "pkg" / "widget.go"
+ go.parent.mkdir()
+ go.write_text("package pkg\nfunc Run() int { return 1 }\n", encoding="utf-8")
+ command, directory = command_for(root, go, "go", None)
+ assert directory == sub
+ assert command == "go test -count=1 ./pkg"
+
+ (sub / "pyproject.toml").write_text("[project]\ndependencies=['pytest']\n", encoding="utf-8")
+ python = sub / "app.py"
+ python.write_text("def place():\n return 1\n", encoding="utf-8")
+ command, directory = command_for(root, python, "python", None)
+ assert directory == sub
+ assert command.endswith("-m pytest")
+
+ result = CommandRunner().run(f"{sys.executable} -c 'import time; time.sleep(30)'", tmp_path, 0.4)
+ assert result.timed_out is True
+ assert result.code == 124
+
+
+def test_a_process_without_a_return_code_counts_as_failure(monkeypatch, tmp_path):
+ class Proc:
+ returncode = None
+ pid = 1
+
+ def communicate(self, timeout=None):
+ return (None, None)
+
+ monkeypatch.setattr("mutator.runner.subprocess.Popen", lambda *args, **kwargs: Proc())
+ result = CommandRunner().run("true", tmp_path, 5)
+ assert result.code == 1
+ assert result.output == ""
+ assert result.timed_out is False
+
+
+def test_options_reject_bad_values_and_keep_flag_defaults(monkeypatch):
+ assert _take(["--root", "/tmp", "extra"], 0, "--root") == "/tmp"
+ assert _take(["--root", "value", ""], 0, "--root") == "value"
+ with pytest.raises(ValueError):
+ _take(["--root"], 0, "--root")
+ with pytest.raises(ValueError):
+ _take(["--root", ""], 0, "--root")
+ with pytest.raises(ValueError):
+ _take(["--lines", "-1"], 0, "--lines")
+
+ assert parse_args(["--lines", "1"]).lines == {1}
+ assert parse_args(["--lines", "0"]).exit_code == 1
+ assert parse_args(["--timeout-factor", "1"]).timeout_factor == 1
+ assert parse_args(["--timeout-factor", "0"]).exit_code == 1
+ assert parse_args(["--timeout-factor", "-1"]).exit_code == 1
+ assert parse_args(["--use-existing-coverage"]).use_existing_coverage is True
+ assert parse_args(["--reuse-coverage"]).use_existing_coverage is True
+ assert parse_args(["--verbose"]).verbose is True
+ assert parse_args(["--changed"]).changed is True
+ assert parse_args(["--not-a-flag"]).exit_code == 1
+ monkeypatch.setattr(sys, "argv", ["prog", "--verbose"])
+ options = parse_args(None)
+ assert options.verbose is True
+ assert options.positionals == []
+
+
+def test_help_and_conflicts_use_different_streams(capsys):
+ assert run(["--help"]) == 0
+ captured = capsys.readouterr()
+ assert "uml-viewer" in captured.out
+ assert captured.err == ""
+ assert run(["--mutate-all", "--since-last-run"]) == 1
+ captured = capsys.readouterr()
+ assert "cannot be combined" in captured.err
+ assert captured.out == ""
+
+
+def test_git_status_keeps_short_names_renames_and_the_failure_text(monkeypatch, tmp_path, capsys):
+ class Result:
+ def __init__(self, code, out="", err=""):
+ self.returncode = code
+ self.stdout = out
+ self.stderr = err
+
+ def fail(*args, **kwargs):
+ return Result(1, err="boom\n")
+
+ monkeypatch.setattr("mutator.cli.subprocess.run", fail)
+ assert _changed_files(tmp_path) == []
+ assert capsys.readouterr().err.strip() == "boom"
+
+ def status(*args, **kwargs):
+ return Result(0, out="?? a\nR src/old.py -> src/new.py\n")
+
+ monkeypatch.setattr("mutator.cli.subprocess.run", status)
+ found = _changed_files(tmp_path)
+ assert (tmp_path / "a").resolve() in found
+ assert (tmp_path / "src" / "new.py").resolve() in found
+ assert (tmp_path / "src" / "old.py").resolve() not in found
+
+
+def test_selection_joins_the_root_and_skips_unknown_files(tmp_path, capsys):
+ path = tmp_path / "src" / "demo.py"
+ path.parent.mkdir()
+ path.write_text("def place(x):\n return x > 0\n", encoding="utf-8")
+ note = tmp_path / "note.txt"
+ note.write_text("hello\n", encoding="utf-8")
+ crapper = ensure_crapper()
+ assert _is_source(note, crapper) is False
+ options = parse_args(["--root", str(tmp_path), "src/demo.py"])
+ assert select_files(options) == [path.resolve()]
+
+ cwd = tmp_path / "cwd"
+ project = tmp_path / "proj"
+ decoy = cwd / "src" / "demo.py"
+ real = project / "src" / "demo.py"
+ decoy.parent.mkdir(parents=True)
+ real.parent.mkdir(parents=True)
+ decoy.write_text("def other(x):\n return x > 1\n", encoding="utf-8")
+ real.write_text("def place(x):\n return x > 0\n", encoding="utf-8")
+ monkeypatch = pytest.MonkeyPatch()
+ monkeypatch.chdir(cwd)
+ try:
+ chosen = select_files(parse_args(["--root", str(project), "src/demo.py"]))
+ finally:
+ monkeypatch.undo()
+ assert chosen == [real.resolve()]
+
+ code = run(["--scan", "--no-coverage", "--root", str(tmp_path), "--source-root", "src"])
+ captured = capsys.readouterr()
+ assert code == 0
+ assert "demo.py" in captured.out
+
+
+def test_existing_coverage_is_not_regenerated(monkeypatch, tmp_path):
+ crapper = ensure_crapper()
+ calls = []
+ monkeypatch.setattr(crapper.runners, "run_coverage", lambda *args, **kwargs: calls.append(args))
+ root = tmp_path
+ _prepare_coverage(parse_args(["--no-coverage", "--root", str(root)]), root, [])
+ _prepare_coverage(parse_args(["--use-existing-coverage", "--root", str(root)]), root, [])
+ assert calls == []
+ _prepare_coverage(parse_args(["--root", str(root)]), root, [])
+ assert len(calls) == 1
+
+
+def test_cli_exit_codes_follow_kills_baseline_failure_and_an_empty_tree(tmp_path, capsys):
+ empty = run(["--no-coverage", "--root", str(tmp_path)])
+ assert empty == 0
+ assert "No source files" in capsys.readouterr().out
+
+ path = tmp_path / "src" / "demo.py"
+ path.parent.mkdir()
+ path.write_text("def add(a, b):\n return a + b\n", encoding="utf-8")
+ command = (
+ f"{sys.executable} -c \"import sys; sys.path.insert(0, 'src'); "
+ "from demo import add; raise SystemExit(0 if add(2, 3) == 5 else 1)\""
+ )
+ code = run(
+ [
+ "--no-coverage",
+ "--mutate-all",
+ "--root",
+ str(tmp_path),
+ "--test-command",
+ command,
+ "src/demo.py",
+ ]
+ )
+ assert code == 0
+ assert "KILLED" in capsys.readouterr().out
+ snapshot = snapshot_path(tmp_path, "demo")
+ recorded = snapshot.read_text(encoding="utf-8")
+
+ path.write_text("def place(x):\n return x > 0\n", encoding="utf-8")
+ failing = f"{sys.executable} -c 'raise SystemExit(1)'"
+ code = run(
+ [
+ "--no-coverage",
+ "--mutate-all",
+ "--root",
+ str(tmp_path),
+ "--test-command",
+ failing,
+ "src/demo.py",
+ ]
+ )
+ assert code == 2
+ assert snapshot.read_text(encoding="utf-8") == recorded
+
+
+def test_a_scan_does_not_restore_a_backup_and_a_run_does(tmp_path, capsys):
+ source = tmp_path / "src" / "demo.py"
+ source.parent.mkdir()
+ dirty = "def place(x):\n return x > 0\n"
+ clean = "def place(x):\n return x > 1\n"
+ source.write_text(dirty, encoding="utf-8")
+ backup = tmp_path / "target" / "mutator-backup" / "src" / "demo.py"
+ backup.parent.mkdir(parents=True)
+ backup.write_text(clean, encoding="utf-8")
+ code = run(["--scan", "--no-coverage", "--root", str(tmp_path), "src/demo.py"])
+ assert code == 0
+ assert source.read_text(encoding="utf-8") == dirty
+ scanned = capsys.readouterr().out
+ assert "0 -> 1" in scanned
+ assert "1 -> 0" not in scanned
+
+ failing = f"{sys.executable} -c 'raise SystemExit(1)'"
+ code = run(
+ [
+ "--no-coverage",
+ "--mutate-all",
+ "--root",
+ str(tmp_path),
+ "--test-command",
+ failing,
+ "src/demo.py",
+ ]
+ )
+ assert code == 2
+ assert source.read_text(encoding="utf-8") == clean
+
+
+def test_backups_bytecode_and_a_dirty_file_are_put_back(tmp_path):
+ backup = tmp_path / "target" / "mutator-backup" / "a" / "b" / "demo.py"
+ backup.parent.mkdir(parents=True)
+ backup.write_bytes(b"original\n")
+ restored = restore_backups(tmp_path)
+ assert (tmp_path / "a" / "b" / "demo.py").read_bytes() == b"original\n"
+ assert restored == [tmp_path / "a" / "b" / "demo.py"]
+
+ path = tmp_path / "src" / "demo.py"
+ path.parent.mkdir()
+ path.write_bytes(b"one")
+ saved = _backup(tmp_path, path, b"one")
+ assert saved.read_bytes() == b"one"
+ assert _backup(tmp_path, path, b"two").read_bytes() == b"one"
+
+ cache = path.parent / "__pycache__"
+ cache.mkdir()
+ compiled = cache / f"{path.stem}.cpython-314.pyc"
+ compiled.write_bytes(b"pyc")
+ _drop_bytecode(path)
+ assert not compiled.exists()
+
+ path.write_bytes(b"dirty")
+ _restore_if_dirty(path, b"clean")
+ assert path.read_bytes() == b"clean"
+
+
+def test_selection_coverage_and_timeouts_keep_their_boundaries(tmp_path, capsys, monkeypatch):
+ path = tmp_path / "src" / "demo.py"
+ path.parent.mkdir()
+ path.write_text("def place(x):\n return x > 0\n", encoding="utf-8")
+ site = _site()
+ assert _covered(site, None, False) is False
+ assert _covered(site, {2}, False) is True
+
+ spans = {
+ ("demo.Zebra", "defn/long"): (1, 10, "long", False),
+ ("demo", "defn/short"): (3, 4, "short", False),
+ }
+ monkeypatch.setattr("mutator.engine.form_spans", lambda *args: spans)
+ monkeypatch.setattr("mutator.engine.form_digests", lambda *args: {key: "d" for key in spans})
+ ordered = _forms("x", path, tmp_path, "src/demo.py", [], {}, {}, None)
+ assert [form.name for form in ordered] == ["long", "short"]
+
+ owned = _site(namespace="demo", form_id="defn/place", mutation_id="m", line=2)
+ place_span = {("demo", "defn/place"): (1, 3, "place", False)}
+ monkeypatch.setattr("mutator.engine.form_spans", lambda *args: place_span)
+ monkeypatch.setattr("mutator.engine.form_digests", lambda *args: {("demo", "defn/place"): "d"})
+ counted = _forms("x", path, tmp_path, "src/demo.py", [owned], {"m": True}, {"m": "survived"}, {2})
+ assert counted[0].survived == 1
+ assert counted[0].killed == 0
+ missing = _forms("x", path, tmp_path, "src/demo.py", [owned], {"m": True}, {}, None)
+ assert missing[0].survived == 1
+
+ history = History(
+ forms={("demo", "defn/place"): PriorForm("demo", "defn/place", "old", "src/demo.py")},
+ outcomes={"missing": "killed", "m": "killed"},
+ )
+ carried = _carry_forward([owned], {"m": True}, history, {("demo", "defn/place"): "new"})
+ assert carried == {}
+ uncovered = _carry_forward([owned], {"m": False}, history, {("demo", "defn/place"): "old"})
+ assert uncovered == {}
+
+ warned = mutate_file(
+ path,
+ tmp_path,
+ runner=_Ok(),
+ covered_lines=None,
+ ignore_coverage=False,
+ mutate_all=True,
+ lines=None,
+ test_command="fake",
+ timeout_factor=10,
+ mutation_warning=50,
+ baselines={},
+ )
+ assert warned.forms[0].uncovered == 2
+ assert "No coverage data" in capsys.readouterr().err
+
+ quiet = mutate_file(
+ path,
+ tmp_path,
+ runner=_Ok(),
+ covered_lines=None,
+ ignore_coverage=True,
+ mutate_all=True,
+ lines=None,
+ test_command="fake",
+ timeout_factor=10,
+ mutation_warning=2,
+ baselines={},
+ )
+ assert quiet.forms[0].killed == 2
+ quiet_err = capsys.readouterr().err
+ assert "WARNING" not in quiet_err
+ assert "No coverage data" not in quiet_err
+
+ clock = _Ok(seconds=30)
+ mutate_file(
+ path,
+ tmp_path,
+ runner=clock,
+ covered_lines=None,
+ ignore_coverage=True,
+ mutate_all=True,
+ lines=None,
+ test_command="fake",
+ timeout_factor=10,
+ mutation_warning=50,
+ baselines={},
+ )
+ assert clock.timeouts[0] is None
+ assert clock.timeouts[1] == 300
+
+ class Red:
+ verbose = False
+
+ def run(self, command, cwd, timeout):
+ lines = ["START"] + [f"M{i}" for i in range(1, 24)] + ["END"]
+ return CommandResult(code=1, timed_out=False, seconds=0.01, output="\n".join(lines))
+
+ failed = mutate_file(
+ path,
+ tmp_path,
+ runner=Red(),
+ covered_lines=None,
+ ignore_coverage=True,
+ mutate_all=True,
+ lines=None,
+ test_command="fake",
+ timeout_factor=10,
+ mutation_warning=50,
+ baselines={},
+ )
+ assert "M5" in failed.baseline_message
+ assert "START" not in failed.baseline_message
+
+ scanned = scan_file(path, tmp_path, covered_lines={1}, ignore_coverage=False, lines=None)
+ assert "uncovered" in scanned
+ ignored = scan_file(path, tmp_path, covered_lines={1}, ignore_coverage=True, lines=None)
+ assert "uncovered" not in ignored
+
+
+def test_mutate_all_reruns_a_killed_mutant_and_clears_its_backup(tmp_path):
+ path = tmp_path / "src" / "demo.py"
+ path.parent.mkdir()
+ path.write_text("def add(a, b):\n if a > 0:\n return a + b\n return a + b\n", encoding="utf-8")
+
+ class Exec:
+ def __init__(self):
+ self.verbose = False
+ self.calls = 0
+
+ def run(self, command, cwd, timeout):
+ self.calls += 1
+ text = (cwd / "src" / "demo.py").read_text(encoding="utf-8")
+ namespace: dict = {}
+ exec(text, namespace)
+ ok = namespace["add"](1, 2) == 3
+ return CommandResult(code=0 if ok else 1, timed_out=False, seconds=0.01, output="")
+
+ runner = Exec()
+ first = mutate_file(
+ path,
+ tmp_path,
+ runner=runner,
+ covered_lines=None,
+ ignore_coverage=True,
+ mutate_all=True,
+ lines=None,
+ test_command="fake",
+ timeout_factor=10,
+ mutation_warning=50,
+ baselines={},
+ )
+ killed_once = first.forms[0].killed
+ first_calls = runner.calls
+ second = mutate_file(
+ path,
+ tmp_path,
+ runner=runner,
+ covered_lines=None,
+ ignore_coverage=True,
+ mutate_all=True,
+ lines=None,
+ test_command="fake",
+ timeout_factor=10,
+ mutation_warning=50,
+ baselines={},
+ )
+ assert second.forms[0].killed == killed_once
+ assert runner.calls - first_calls == first_calls
+ assert [item for item in (tmp_path / "target").rglob("*") if item.is_file()] == []
+
+
+def test_a_missing_backup_is_not_unlinked_again(tmp_path):
+ path = tmp_path / "src" / "demo.py"
+ path.parent.mkdir()
+ path.write_text("def place(x):\n return x > 0\n", encoding="utf-8")
+
+ class Drop:
+ verbose = False
+
+ def run(self, command, cwd, timeout):
+ if timeout is not None:
+ for backup in (cwd / "target" / "mutator-backup").rglob("*"):
+ if backup.is_file():
+ backup.unlink()
+ return CommandResult(code=0 if timeout is None else 1, timed_out=False, seconds=0.01, output="")
+
+ mutate_file(
+ path,
+ tmp_path,
+ runner=Drop(),
+ covered_lines=None,
+ ignore_coverage=True,
+ mutate_all=True,
+ lines=None,
+ test_command="fake",
+ timeout_factor=10,
+ mutation_warning=50,
+ baselines={},
+ )
+ assert path.read_text(encoding="utf-8").startswith("def place")
+
+
+def test_an_unrelated_snapshot_is_left_in_place(tmp_path):
+ from mutator.edn import dumps
+
+ legacy = tmp_path / ".metrics" / "mutate" / "legacy.edn"
+ legacy.parent.mkdir(parents=True)
+ legacy.write_text(
+ dumps({"namespace": "untouched", "source": "c.go", "forms": [], "outcomes": {}}) + "\n",
+ encoding="utf-8",
+ )
+ write_results(tmp_path, "a.go", [_form("demo", "Run", "a.go", "h")], {})
+ assert legacy.is_file()
+
+
+def test_snapshots_keep_other_files_empty_directories_and_odd_records(tmp_path):
+ assert _outcomes_of({"outcomes": {"id": ":killed"}}) == {"id": "killed"}
+ assert _form_file({"file": ""}, {"source": "a.go"}) == "a.go"
+ assert _mentions_file({"forms": [{"file": "a.go"}]}, "a.go") is True
+ assert _mentions_file({"forms": [{"file": "b.go"}]}, "a.go") is False
+ assert _form_file_only({"source": "a.go", "forms": []}, "a.go") is True
+ assert _form_file_only({"source": "b.go", "forms": []}, "a.go") is False
+ assert _form_file_only({"forms": [{"file": "a.go"}]}, "a.go") is True
+ assert _form_file_only({"forms": [{"file": "b.go"}]}, "a.go") is False
+
+ assert _write_namespace(tmp_path, "ghost", [], {}, [], {}, "a.go") is None
+ missing = tmp_path / "missing.edn"
+ _drop_stale([(missing, {"namespace": "demo"})], {"demo"}, set(), "a.go")
+ assert not missing.exists()
+
+ write_results(tmp_path, "b.go", [_form("demo", "Stop", "b.go", "hash-b")], {})
+ write_results(tmp_path, "a.go", [], {})
+ kept = loads(snapshot_path(tmp_path, "demo").read_text(encoding="utf-8"))
+ assert kept["source"] == "b.go"
+ assert kept["forms"][0]["id"] == "defn/Stop"
+
+ from mutator.edn import dumps, keyword
+ from mutator.functions import mutation_id
+
+ other = tmp_path / ".metrics" / "mutate" / "other.edn"
+ other.write_text(
+ dumps({"namespace": "other", "source": "c.go", "forms": [{"id": "defn/Stay", "file": "c.go", "line": 1, "hash": "h"}], "outcomes": {}})
+ + "\n",
+ encoding="utf-8",
+ )
+ foreign = mutation_id("b.go", "demo", "defn/Stop", 1, 2, "+", "-")
+ snapshot_path(tmp_path, "demo").write_text(
+ dumps(
+ {
+ "namespace": "demo",
+ "source": "a.go",
+ "forms": [
+ {"id": "defn/m", "file": "b.go", "hash": "m"},
+ {"id": "defn/a", "file": "b.go", "line": 1, "hash": "a"},
+ ],
+ "outcomes": {foreign: keyword("killed")},
+ }
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+ write_results(tmp_path, "a.go", [], {})
+ rewritten = loads(snapshot_path(tmp_path, "demo").read_text(encoding="utf-8"))
+ assert [form["id"] for form in rewritten["forms"]] == ["defn/m", "defn/a"]
+ assert foreign in rewritten["outcomes"]
+ assert other.exists()
+
+ odd = tmp_path / ".metrics" / "mutate" / "odd.edn"
+ odd.write_text(
+ dumps({"namespace": "odd", "forms": [{"id": 1, "hash": "digest", "file": "a.go"}], "outcomes": {}}) + "\n",
+ encoding="utf-8",
+ )
+ assert load_history(tmp_path, "a.go").forms == {}
+
+ base = tmp_path / "empty-metrics"
+ nested = base / "empty" / "nested"
+ nested.mkdir(parents=True)
+ full = base / "full"
+ full.mkdir()
+ (full / "keep.edn").write_text("x", encoding="utf-8")
+ _remove_empty_dirs(base)
+ assert not nested.exists()
+ assert not (base / "empty").exists()
+ assert (full / "keep.edn").is_file()
+ _remove_empty_dirs(tmp_path / "missing-metrics")