mirror of
https://github.com/unclebob/mutator.git
synced 2026-11-06 18:23:50 +00:00
A self-run left covered gaps and live mutants. These cases exercise the real boundaries and leave equivalent mutants and lines coverage cannot see.
845 lines
30 KiB
Python
845 lines
30 KiB
Python
"""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(
|
|
"""<?xml version="1.0"?>
|
|
<report name="demo">
|
|
<package name="demo">
|
|
<sourcefile name="Board.java">
|
|
<line nr="6" mi="0" ci="1" mb="0" cb="0"/>
|
|
</sourcefile>
|
|
</package>
|
|
</report>
|
|
""",
|
|
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(
|
|
'<span title="1 out of 1 forms covered">4 </span>\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("<project/>\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")
|