Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions toolgate/scripts/mutation_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
MCP = "toolgate/tests/test_mcp_adapter.py::"
ARCHIVE = "toolgate/tests/test_execution_boundary.py::"
INTEGRITY = "toolgate/tests/test_approval_integrity.py::"
# Failures from a mutant that cannot run, not from the behaviour it changes.
BROKEN_MUTANT = ("NameError", "ImportError", "SyntaxError", "IndentationError")

# A mutant counts as caught only when its named behavioral test fails, never
# when collection fails or the subprocess cannot start.
Expand Down Expand Up @@ -93,9 +95,12 @@
if guard is not None:
guard(conn, record)
if record.get("status") != "pending":""",
""" record = get("request", request_id)
if not record:
# A stale read: the row is read outside the write transaction, then decided on.
""" with _conn() as read:
row = read.execute("SELECT * FROM v2_objects WHERE kind='request' AND id=?", (request_id,)).fetchone()
if not row:
return None
record = _row(row)
with _conn() as conn:
if guard is not None:
guard(conn, record)
Expand Down Expand Up @@ -472,6 +477,12 @@ def main() -> int:
return 1
finally:
path.write_text(original, encoding="utf-8")
output = result.stdout + result.stderr
if any(crash in output for crash in BROKEN_MUTANT):
# A test failing on the mutant's own crash proves nothing about the behaviour.
print(output)
print(f"BROKEN MUTANT (it crashes instead of changing behaviour): {label}")
return 1
if result.returncode != 1 or f"FAILED {test}" not in result.stdout:
print(result.stdout + result.stderr)
print(f"NOT CAUGHT: {label}")
Expand Down
32 changes: 32 additions & 0 deletions toolgate/tests/test_mutation_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,35 @@ def fake_run_tests(root, tests, name):
assert drill.main() == 1
assert calls == ["baseline", "mutant-0"]
assert "TIMED OUT: drill anchor mutated" in capsys.readouterr().out


def test_a_mutant_that_crashes_is_reported_broken_not_caught(drill, monkeypatch, capsys):
crash = "E NameError: name 'get' is not defined\nFAILED some/test - NameError"

def fake_run_tests(root, tests, name):
if name == "baseline":
return subprocess.CompletedProcess([], 0, stdout="", stderr="")
return subprocess.CompletedProcess([], 1, stdout=crash, stderr="")

monkeypatch.setattr(drill, "run_tests", fake_run_tests)
monkeypatch.setattr(
drill,
"MUTANTS",
[("crashing mutant", ANCHOR[1], ANCHOR[0], ANCHOR[0] + " # mutated", "some/test")],
)
assert drill.main() == 1
out = capsys.readouterr().out
assert "BROKEN MUTANT (it crashes instead of changing behaviour): crashing mutant" in out
assert "Caught: crashing mutant" not in out


def test_the_stale_decision_mutant_reads_the_row_itself():
# The stale-decision mutant once called control_plane.get from approvals.py, which does not
# import it: the test failed on NameError and the drill printed "Caught".
spec = importlib.util.spec_from_file_location("mutation_check_names", SCRIPT)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
label, _file, _before, after, _test = next(
m for m in module.MUTANTS if m[0] == "stale decision overwrites consumption"
)
assert 'get("request"' not in after, label
Loading