File size: 3,598 Bytes
57ad771
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"""Mutation matrix over the lint rules.

For each rule, neuter it so it yields nothing, run the suite, and record which
tests fail. A row with zero failures means the rule is either dead code or has
no test guarding it, and you cannot tell which without looking.

Run with: python mutation_check.py
"""

from __future__ import annotations

import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

HERE = Path(__file__).resolve().parent
RULE_DEF = re.compile(r"^def (rule_[a-z_]+)\(", re.MULTILINE)


def rule_names(source: str) -> list[str]:
    return RULE_DEF.findall(source)


def mutate(source: str, rule: str) -> str:
    """Insert an immediate return into one rule, leaving it a generator."""
    pattern = re.compile(rf"^(def {re.escape(rule)}\(.*:\n)", re.MULTILINE)
    mutated, count = pattern.subn(r"\1    return  # MUTATED\n", source)
    if count != 1:
        raise SystemExit(f"anchor for {rule} matched {count} times, expected 1")
    if mutated == source:
        raise SystemExit(f"mutation for {rule} was a no-op")
    return mutated


def run_suite(workdir: Path) -> tuple[int, str]:
    # Every mutated lint.py is the same length, and CPython validates a cached
    # .pyc on (mtime, size). Inside one second that pair does not change, so the
    # interpreter would silently re-import the previous mutation's bytecode and
    # the whole matrix would report another rule's result.
    env = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
    proc = subprocess.run(
        [sys.executable, "-B", "-m", "pytest", "-q", "--no-header", "-p", "no:cacheprovider"],
        cwd=workdir, capture_output=True, text=True, env=env,
    )
    return proc.returncode, proc.stdout + proc.stderr


def failed_tests(output: str) -> list[str]:
    return sorted({m.group(1) for m in re.finditer(r"^FAILED (\S+)", output, re.MULTILINE)})


def main() -> int:
    source = (HERE / "lint.py").read_text()
    rules = rule_names(source)
    if not rules:
        raise SystemExit("no rules found - the anchor is wrong, not the code")

    with tempfile.TemporaryDirectory() as tmp:
        base = Path(tmp)
        shutil.copy(HERE / "test_lint.py", base / "test_lint.py")

        # Baseline must be green, or every verdict below is meaningless.
        (base / "lint.py").write_text(source)
        rc, out = run_suite(base)
        if rc != 0:
            print("BASELINE IS NOT GREEN - fix that before trusting any row\n")
            print(out[-2000:])
            return 1
        baseline_count = re.search(r"(\d+) passed", out)
        print(f"baseline: {baseline_count.group(1) if baseline_count else '?'} passed\n")

        zero_rows: list[str] = []
        print(f"{'rule':<38} {'rc':>3}  result")
        print("-" * 78)
        for rule in rules:
            (base / "lint.py").write_text(mutate(source, rule))
            rc, out = run_suite(base)
            fails = failed_tests(out)
            if not fails and rc != 0:
                verdict = "CRASHED (covered)"
            elif not fails:
                verdict = "*** ZERO - dead code or unguarded ***"
                zero_rows.append(rule)
            else:
                verdict = f"{len(fails)} failed: " + ", ".join(f.split("::")[-1] for f in fails)
            print(f"{rule:<38} {rc:>3}  {verdict}")

    print()
    if zero_rows:
        print(f"{len(zero_rows)} rule(s) with no failing test: {', '.join(zero_rows)}")
        return 1
    print("every rule is guarded by at least one test")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())