Python Part II · Module 36 · Python for VLSI Design Review
Reviewing Maintainability and Tests
A pull request with tests is better than one without, which is why 'it has tests' ends most reviews. This chapter is about the measurement that should begin instead: deliberately break the implementation and see whether the suite objects. On the specimen here, five green tests caught one of six broken checks — and three of those five tests turn out to be incapable of failing at all, for three different reasons that an AST walk finds in a second.
Advanced25 min readPythonPart IIAutomationCode ReviewTesting
Part II · Module 36 · Chapter 36.5 · Reviewing Maintainability and Tests
1. "The Tests Pass" Is Not a Finding
OBSERVATIONS = [
("the suite is green", "the code is right", "or the tests cannot fail"),
("there are 40 tests", "good coverage", "or 40 tests of one thing"),
("coverage is 95%", "most lines are checked", "lines RAN — 29.1 §5"),
("CI is green", "nothing regressed", "or nothing is asserted"),
]
print(f"{'what a reviewer sees':<24} {'what it suggests':<28} "
f"what it is also consistent with")
for seen, suggests, also in OBSERVATIONS:
print(f"{seen:<24} {suggests:<28} {also}")
print()
print("observations that distinguish")
print(" a good suite from a useless one:", 0)
print("what would :", "a run where the code is WRONG")
print()
print("so the review's measurement is :", "break it and watch")what a reviewer sees what it suggests what it is also consistent with
the suite is green the code is right or the tests cannot fail
there are 40 tests good coverage or 40 tests of one thing
coverage is 95% most lines are checked lines RAN — 29.1 §5
CI is green nothing regressed or nothing is asserted
observations that distinguish
a good suite from a useless one: 0
what would : a run where the code is WRONG
so the review's measurement is : break it and watchNone of the four observations a reviewer actually has distinguishes a good suite from a useless one. Chapter 33.9 made this argument for a developer writing tests; the review version is the same measurement with a different person holding the mutation list.
2. The Annotation
# break one check on purpose — does the suite notice?QUESTIONS = [
("can the parser be tested without the tool?", "imports", "§6"),
("can the tests fail when the code is wrong?", "mutations", "§3-§5"),
("could another engineer own this?", "proxies", "§7"),
]
print(f"{'the canonical question':<44} {'measured by':<12} where")
for q, how, where in QUESTIONS:
print(f"{q:<44} {how:<12} {where}")
print()
print("the annotation names the second one:", True)
print("because it is :",
"the only one that can be wrong while looking right")
print()
print("the first question is answered by :", "reading the import list")
print("the third by :", "three counts")
print("the second only by :", "running the suite against a bug")the canonical question measured by where
can the parser be tested without the tool? imports §6
can the tests fail when the code is wrong? mutations §3-§5
could another engineer own this? proxies §7
the annotation names the second one: True
because it is : the only one that can be wrong while looking right
the first question is answered by : reading the import list
the third by : three counts
the second only by : running the suite against a bugOnly the second question can be wrong while looking right. A parser that cannot be tested without the tool declares that fact in its import list, and an unmaintainable module shows it in its shape — but a suite that cannot fail looks exactly like a suite that can, right up to the moment somebody breaks the code.
3. Three of Five Tests Cannot Fail
import ast
SUITE_SRC = '''
import unittest
class T(unittest.TestCase):
def test_pass(self):
self.assertEqual(classify("UVM_INFO a\\nTEST DONE\\n"), "PASS")
def test_fail(self):
self.assertEqual(classify("UVM_ERROR x\\nTEST DONE\\n"), "FAIL")
def test_smoke(self):
classify("UVM_INFO a\\nTEST DONE\\n")
def test_trivial(self):
self.assertTrue(True)
def test_runs_without_raising(self):
try:
classify("")
except Exception:
self.fail("raised")
'''
def audit_tests(src):
"""For each test method: how many assertions, and can any of them fail?"""
tree = ast.parse(src)
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef))
rows = []
for m in (x for x in cls.body if isinstance(x, ast.FunctionDef)):
asserts = [n for n in ast.walk(m)
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Attribute)
and n.func.attr.startswith("assert")]
const_only = bool(asserts) and all(
all(isinstance(a, ast.Constant) for a in call.args)
for call in asserts)
rows.append({"name": m.name,
"asserts": len(asserts),
"names": [c.func.attr for c in asserts],
"constant_only": const_only})
return rows
rows = audit_tests(SUITE_SRC)
print(f"{'test':<26} {'asserts':>8} {'verdict':<22} assertion names")
cannot_fail = 0
for r in rows:
if r["asserts"] == 0:
verdict = "NO ASSERTION"
elif r["constant_only"]:
verdict = "constant arguments"
else:
verdict = "can fail"
cannot_fail += verdict != "can fail"
print(f"{r['name']:<26} {r['asserts']:>8} {verdict:<22} {r['names']}")
print()
print("tests :", len(rows))
print("that can fail :", len(rows) - cannot_fail)
print("that CANNOT fail :", cannot_fail)
print()
print("and all five :", "pass")test asserts verdict assertion names
test_pass 1 can fail ['assertEqual']
test_fail 1 can fail ['assertEqual']
test_smoke 0 NO ASSERTION []
test_trivial 1 constant arguments ['assertTrue']
test_runs_without_raising 0 NO ASSERTION []
tests : 5
that can fail : 2
that CANNOT fail : 3
and all five : passThree of five tests cannot fail, for three different reasons. The audit is
twenty lines of ast and it is worth running before any mutation, because each
of the three has a different review comment.
SHAPES = [
("test_smoke", "calls the function and asserts nothing",
"it fails only if the call RAISES", "assert the return value"),
("test_trivial", "assertTrue(True)",
"it can never fail", "delete it, or assert the real thing"),
("test_runs_without_raising", "try/except + self.fail",
"it fails only if the call RAISES",
"it IS a real test — of not raising"),
]
print(f"{'test':<26} {'shape':<40} what it actually checks")
for name, shape, checks, _ in SHAPES:
print(f"{name:<26} {shape:<40} {checks}")
print()
print("review comments:")
for name, _, _, comment in SHAPES:
print(f" {name:<26} {comment}")
print()
print("the third one is NOT a defect:", True)
print("it is a weak test with :", "a legitimate purpose")
print("and the finding is :",
"it is the ONLY test of the empty-log path")test shape what it actually checks
test_smoke calls the function and asserts nothing it fails only if the call RAISES
test_trivial assertTrue(True) it can never fail
test_runs_without_raising try/except + self.fail it fails only if the call RAISES
review comments:
test_smoke assert the return value
test_trivial delete it, or assert the real thing
test_runs_without_raising it IS a real test — of not raising
the third one is NOT a defect: True
it is a weak test with : a legitimate purpose
and the finding is : it is the ONLY test of the empty-log pathThe third one is not a defect. test_runs_without_raising genuinely checks
something — that classify("") does not throw — which is a real requirement
and the kind of thing Chapter 33.6 wrote deliberately. The finding is not "this
test is useless" but "this is the only test of the empty-log path, and it checks
only that the path does not crash" — which Chapter 36.2 showed is the path that
silently returns PASS.
Distinguishing those two is what separates a useful review from a style
complaint. assertTrue(True) is a defect; a weak test of a real property is a
gap.
4. The Kill Rate: One of Six
import io
import unittest
SRC = '''
def classify(text):
lines = text.splitlines()
errors = sum(1 for l in lines
if l.startswith("UVM_ERROR") and "UVM_ERROR :" not in l)
fatals = sum(1 for l in lines if l.startswith("UVM_FATAL"))
done = any(l.strip() == "TEST DONE" for l in lines)
if not done:
return "TIMEOUT"
if fatals:
return "FATAL"
if errors:
return "FAIL"
return "PASS"
'''
SUITE_SRC = '''
import unittest
class T(unittest.TestCase):
def test_pass(self):
self.assertEqual(classify("UVM_INFO a\\nTEST DONE\\n"), "PASS")
def test_fail(self):
self.assertEqual(classify("UVM_ERROR x\\nTEST DONE\\n"), "FAIL")
def test_smoke(self):
classify("UVM_INFO a\\nTEST DONE\\n")
def test_trivial(self):
self.assertTrue(True)
def test_runs_without_raising(self):
try:
classify("")
except Exception:
self.fail("raised")
'''
MUTATIONS = [
("drop the TIMEOUT branch",
' if not done:\n return "TIMEOUT"\n', ''),
("drop the FATAL branch", ' if fatals:\n return "FATAL"\n', ''),
("errors counts 2 per line",
'errors = sum(1 for l in lines', 'errors = sum(2 for l in lines'),
("drop the summary guard", ' and "UVM_ERROR :" not in l', ''),
("done uses a substring",
'done = any(l.strip() == "TEST DONE" for l in lines)',
'done = "TEST DONE" in text'),
("PASS and FAIL swapped",
' if errors:\n return "FAIL"\n return "PASS"\n',
' if errors:\n return "PASS"\n return "FAIL"\n'),
]
def suite_passes(code_src):
ns = {}
exec(compile(code_src, "m.py", "exec"), ns)
exec(compile(SUITE_SRC, "t.py", "exec"), ns)
suite = unittest.TestLoader().loadTestsFromTestCase(ns["T"])
return unittest.TextTestRunner(stream=io.StringIO(),
verbosity=0).run(suite).wasSuccessful()
baseline = suite_passes(SRC)
print("the baseline suite passes:", baseline)
assert baseline, "a failing baseline makes every mutation a false kill — 33.9 §9"
print()
print(f"{'mutation':<28} killed?")
killed = []
for label, old, new in MUTATIONS:
assert old in SRC, label
try:
passed = suite_passes(SRC.replace(old, new, 1))
except Exception:
passed = False
if not passed:
killed.append(label)
print(f"{label:<28} {'KILLED' if not passed else 'SURVIVED'}")
print()
print("mutations :", len(MUTATIONS))
print("killed :", len(killed), killed)
print("kill rate :", f"{100 * len(killed) // len(MUTATIONS)}%")
print()
print("tests in the suite:", 5)
print("tests that can fail:", 2)
print("and the kill rate reflects:", "the second number, not the first")the baseline suite passes: True
mutation killed?
drop the TIMEOUT branch SURVIVED
drop the FATAL branch SURVIVED
errors counts 2 per line SURVIVED
drop the summary guard SURVIVED
done uses a substring SURVIVED
PASS and FAIL swapped KILLED
mutations : 6
killed : 1 ['PASS and FAIL swapped']
kill rate : 16%
tests in the suite: 5
tests that can fail: 2
and the kill rate reflects: the second number, not the firstOne of six mutations killed, from a green five-test suite. The survivors are the interesting part and they are not all the same kind of gap.
drop the TIMEOUT branch and drop the FATAL branch survive because no test
exercises those paths — a missing test case, which is the gap a reviewer
expects to find.
drop the summary guard and done uses a substring survive because no test
uses an input where the guard or the anchoring matters — Chapter 36.2's battery
is exactly the missing fixture set, which makes this the one survivor class with
a fix already written in the previous chapter.
And errors counts 2 per line survives for a reason that is not a missing test
case at all.
5. The Survivor That Is a Structural Gap
def classify(text, per_line=1):
"""per_line=2 is the mutation: it counts two errors per error line."""
lines = text.splitlines()
errors = sum(per_line for l in lines
if l.startswith("UVM_ERROR") and "UVM_ERROR :" not in l)
done = any(l.strip() == "TEST DONE" for l in lines)
if not done:
return {"status": "TIMEOUT", "errors": errors}
return {"status": "FAIL" if errors else "PASS", "errors": errors}
LOG = "UVM_ERROR drv.sv(88) x\nTEST DONE\n"
for per_line in (1, 2):
r = classify(LOG, per_line)
print(f" per_line={per_line} status={r['status']:<6} "
f"errors={r['errors']}")
print()
good = classify(LOG, 1)
mutant = classify(LOG, 2)
print("the status is the same :", good["status"] == mutant["status"])
print("the error count differs :", good["errors"] != mutant["errors"])
print()
print("what the suite asserts :", "the status")
print("what the mutation changes:", "the count")
print("so the mutation is :", "invisible to every status assertion")
print()
print("the gap is not a missing test CASE:", True)
print("it is a missing :", "assertion SUBJECT")
print("and the fix is :",
"assert the count as well as the verdict") per_line=1 status=FAIL errors=1
per_line=2 status=FAIL errors=2
the status is the same : True
the error count differs : True
what the suite asserts : the status
what the mutation changes: the count
so the mutation is : invisible to every status assertion
the gap is not a missing test CASE: True
it is a missing : assertion SUBJECT
and the fix is : assert the count as well as the verdictOne error and two errors are both FAIL. The arithmetic mutation changes a
number no test looks at, so adding more status assertions — more logs, more
verdicts, a bigger battery — cannot kill it. The suite needs a different
subject, not a different case.
That distinction is the most useful thing a reviewer can take from a mutation run. A survivor tells you a test is missing; which kind of survivor tells you whether the fix is a fixture or an assertion:
SURVIVORS = [
("drop the TIMEOUT branch", "no input lacks TEST DONE",
"add a fixture", "36.2's battery"),
("drop the FATAL branch", "no input has a UVM_FATAL",
"add a fixture", "36.2's battery"),
("drop the summary guard", "no input has a UVM_ERROR : line",
"add a fixture", "36.2's battery"),
("done uses a substring", "no input mentions TEST DONE in prose",
"add a fixture", "36.2's battery"),
("errors counts 2 per line", "no test asserts the count",
"add an ASSERTION", "a new subject"),
]
print(f"{'survivor':<28} {'why it survived':<36} {'fix':<16} from")
for label, why, fix, src in SURVIVORS:
print(f"{label:<28} {why:<36} {fix:<16} {src}")
print()
fixtures = sum(1 for *_, f, _ in SURVIVORS if f == "add a fixture")
print("survivors needing a fixture :", fixtures)
print("survivors needing an assertion:", len(SURVIVORS) - fixtures)
print()
print("four of five are fixed by :", "the battery from 36.2")
print("which means the two reviews ")
print(" compose :", True)survivor why it survived fix from
drop the TIMEOUT branch no input lacks TEST DONE add a fixture 36.2's battery
drop the FATAL branch no input has a UVM_FATAL add a fixture 36.2's battery
drop the summary guard no input has a UVM_ERROR : line add a fixture 36.2's battery
done uses a substring no input mentions TEST DONE in prose add a fixture 36.2's battery
errors counts 2 per line no test asserts the count add an ASSERTION a new subject
survivors needing a fixture : 4
survivors needing an assertion: 1
four of five are fixed by : the battery from 36.2
which means the two reviews
compose : TrueFour of five survivors are fixed by the battery from the previous chapter. That composition is worth noticing: the parsing review produces a set of malformed inputs, and those inputs are exactly the fixtures the mutation review finds missing — so a reviewer who did Chapter 36.2 first has already written most of the fix for Chapter 36.5's findings.
6. Can the Parser Be Tested Without the Tool?
import ast
GOOD = '''"""flow.parse — text in, facts out."""
import re
SEVERITY = re.compile(r"^UVM_(ERROR|FATAL)\\b")
def classify(text):
return "FAIL" if SEVERITY.search(text) else "PASS"
'''
BAD = '''"""flow.parse — text in, facts out."""
import os
import re
import subprocess
from flow.tools import run_sim
LICENCE = os.environ["LM_LICENSE_FILE"]
VERSION = subprocess.run(["iverilog", "-V"], capture_output=True).stdout
def classify(text):
return "FAIL" if "UVM_ERROR" in text else "PASS"
'''
WORLD_MODULES = {"subprocess", "shutil", "socket", "requests"}
# Calls that are PURE and idiomatic at module level. A first version of this
# audit flagged `re.compile` as "work at import time", which is a false
# positive — and 36.1 §8's rule says a checker that over-claims is a checker
# that gets switched off.
PURE_AT_IMPORT = {"compile", "Path", "frozenset", "tuple", "dict", "set",
"dict.fromkeys", "namedtuple"}
def _impure_calls(value):
out = []
for s in ast.walk(value):
if isinstance(s, ast.Call):
name = (s.func.attr if isinstance(s.func, ast.Attribute)
else getattr(s.func, "id", "?"))
if name not in PURE_AT_IMPORT:
out.append(name)
return out
def import_audit(src, package="flow"):
tree = ast.parse(src)
stdlib, world, intra = set(), set(), set()
for n in ast.walk(tree):
if isinstance(n, ast.Import):
for a in n.names:
(world if a.name in WORLD_MODULES else stdlib).add(a.name)
elif isinstance(n, ast.ImportFrom) and n.module:
if n.module.startswith(package):
intra.add(n.module)
elif n.module in WORLD_MODULES:
world.add(n.module)
else:
stdlib.add(n.module)
impure, env_reads = [], 0
for n in tree.body:
if not isinstance(n, ast.Assign):
continue
impure.extend(_impure_calls(n.value))
for s in ast.walk(n.value): # EVERY assign, not only ones with calls
if isinstance(s, ast.Subscript) \
and "environ" in ast.dump(s.value):
env_reads += 1
return {"stdlib": sorted(stdlib), "world": sorted(world),
"intra": sorted(intra), "impure_at_import": sorted(set(impure)),
"env_at_import": env_reads}
for label, src in (("testable", GOOD), ("not testable", BAD)):
a = import_audit(src)
print(f"--- {label}")
for k in ("stdlib", "world", "intra", "impure_at_import",
"env_at_import"):
print(f" {k:<18} {a[k]}")
standalone = (not a["world"] and not a["intra"]
and not a["impure_at_import"] and a["env_at_import"] == 0)
print(f" importable alone {standalone}")
print()
print("the BAD module cannot be imported without:",
["a licence variable", "the iverilog binary", "flow.tools"])
print("so its parser tests require :", "the whole environment")
print("and 34.2 §4 measured :",
"that a clean split makes this zero")--- testable
stdlib ['re']
world []
intra []
impure_at_import []
env_at_import 0
importable alone True
--- not testable
stdlib ['os', 're']
world ['subprocess']
intra ['flow.tools']
impure_at_import ['run']
env_at_import 1
importable alone False
the BAD module cannot be imported without: ['a licence variable', 'the iverilog binary', 'flow.tools']
so its parser tests require : the whole environment
and 34.2 §4 measured : that a clean split makes this zeroThe first module imports re and nothing else; the second needs a licence
variable, the iverilog binary and the tool layer — at import time. That is a
mechanical answer to the first canonical question, and it is available before
any test is written.
The PURE_AT_IMPORT set in that audit exists because the first version of it
was wrong. Flagging every module-level call as "work at import time" reports
SEVERITY = re.compile(...) as a defect — and a pre-compiled pattern at module
scope is both idiomatic and genuinely pure, so the audit declared the good
module untestable. That is Chapter 36.1 Section 8's rule applied to this
chapter's own instrument: a checker that over-claims is a checker somebody
switches off, and the fix is to name the calls that are fine rather than to
soften the finding.
The env_at_import count is the one that bites hardest. LICENCE = os.environ["LM_LICENSE_FILE"] at module level means import flow.parse raises
KeyError on a machine with no licence configured, so the parser's tests cannot
run in CI — and the failure looks like a broken test suite rather than a
structural problem.
7. Could Another Engineer Own This?
import ast
SPECIMEN = '''"""run_regress.py"""
import re
ROOT = "/proj/chip_a"
TESTS = ["a", "b"]
SEEDS = [1, 2]
THRESHOLD = 95
def helper(x):
return x * 2
def classify(text):
"""Return a verdict for one log."""
errors = 0
for line in text.splitlines():
if line.startswith("UVM_ERROR"):
errors += 1
if errors > THRESHOLD:
return "MANY"
return "FAIL" if errors else "PASS"
def report(rows):
out = []
for r in rows:
for t in TESTS:
for s in SEEDS:
if r.get("test") == t and r.get("seed") == s:
out.append(f"{ROOT}/{t}_{s}")
return out
'''
tree = ast.parse(SPECIMEN)
funcs = [n for n in tree.body if isinstance(n, ast.FunctionDef)]
globals_upper = {n.targets[0].id for n in tree.body
if isinstance(n, ast.Assign)
and isinstance(n.targets[0], ast.Name)
and n.targets[0].id.isupper()}
def reads_globals(fn, names):
return sorted({n.id for n in ast.walk(fn)
if isinstance(n, ast.Name) and n.id in names
and isinstance(n.ctx, ast.Load)})
def max_depth(fn):
def depth(node, d=0):
best = d
for child in ast.iter_child_nodes(node):
inc = 1 if isinstance(child, (ast.For, ast.While, ast.If,
ast.With, ast.Try)) else 0
best = max(best, depth(child, d + inc))
return best
return depth(fn)
print(f"{'function':<12} {'lines':>6} {'depth':>6} {'docstring':>10} "
f"globals read")
for f in funcs:
span = f.end_lineno - f.lineno + 1
print(f"{f.name:<12} {span:>6} {max_depth(f):>6} "
f"{str(ast.get_docstring(f) is not None):>10} "
f"{reads_globals(f, globals_upper)}")
documented = sum(1 for f in funcs if ast.get_docstring(f))
print()
print("functions :", len(funcs))
print("with a docstring :", documented)
print("docstring coverage :", f"{100 * documented // len(funcs)}%")
print("module-level constants :", len(globals_upper))
print("longest function :", max(f.end_lineno - f.lineno + 1
for f in funcs), "lines")
print("deepest nesting :", max(max_depth(f) for f in funcs))
print("functions reading a global:",
sum(1 for f in funcs if reads_globals(f, globals_upper)))function lines depth docstring globals read
helper 2 0 False []
classify 9 2 True ['THRESHOLD']
report 8 4 False ['ROOT', 'SEEDS', 'TESTS']
functions : 3
with a docstring : 1
docstring coverage : 33%
module-level constants : 4
longest function : 9 lines
deepest nesting : 4
functions reading a global: 2Three functions, one docstring, four module constants and two functions reading them. Those are the proxies for the third canonical question, and the useful ones are the last two rather than the docstring count.
A function reading a module global has a hidden input — Chapter 34.4's
measurement — so report cannot be called with a different test list without
editing the module, and a new owner discovers that by trying. The nesting depth
of 4 in an eight-line function is the other signal: report has a triple loop
with a condition, which is a linear scan written as a cubic one, and it is the
shape that makes a reader assume there must be a reason.
helper has no docstring and needs none — a two-line function whose name is a
verb is self-documenting, and flagging it would be the style complaint that gets
a checklist ignored. The finding worth writing is that classify is the only
documented function and it is the one whose behaviour a reader can infer,
while report, which is genuinely puzzling, has nothing.
8. The Maintainability Review, Assembled
import ast
import io
import unittest
from dataclasses import dataclass
@dataclass(frozen=True)
class Finding:
area: str
severity: str
detail: str
WORLD_MODULES = {"subprocess", "shutil", "socket", "requests"}
def review_tests(suite_src):
findings = []
tree = ast.parse(suite_src)
for cls in (n for n in tree.body if isinstance(n, ast.ClassDef)):
for m in (x for x in cls.body if isinstance(x, ast.FunctionDef)):
if not m.name.startswith("test"):
continue
calls = [n for n in ast.walk(m)
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Attribute)
and n.func.attr.startswith("assert")]
if not calls:
findings.append(Finding(
"tests", "major",
f"{m.name} has no assertion: it fails only if the call "
f"raises"))
continue
if all(all(isinstance(a, ast.Constant) for a in c.args)
for c in calls):
findings.append(Finding(
"tests", "blocker",
f"{m.name} asserts only constants: it cannot fail"))
return findings
def review_imports(module_src, package="flow"):
findings = []
tree = ast.parse(module_src)
for n in ast.walk(tree):
if isinstance(n, ast.Import):
for a in n.names:
if a.name in WORLD_MODULES:
findings.append(Finding(
"testability", "major",
f"the parsing module imports {a.name}"))
if isinstance(n, ast.ImportFrom) and n.module \
and n.module.startswith(package) and "tools" in n.module:
findings.append(Finding("testability", "blocker",
f"the parsing module imports {n.module}"))
for n in tree.body:
if isinstance(n, ast.Assign):
for s in ast.walk(n.value):
if isinstance(s, ast.Subscript) \
and "environ" in ast.dump(s.value):
findings.append(Finding(
"testability", "blocker",
"a module-level os.environ[...] makes `import` fail "
"without that variable"))
if isinstance(s, ast.Call):
findings.append(Finding(
"testability", "major",
f"line {n.lineno}: work is done at import time"))
return findings
def review_maintainability(module_src):
findings = []
tree = ast.parse(module_src)
funcs = [n for n in tree.body if isinstance(n, ast.FunctionDef)]
uppers = {n.targets[0].id for n in tree.body
if isinstance(n, ast.Assign)
and isinstance(n.targets[0], ast.Name)
and n.targets[0].id.isupper()}
for f in funcs:
reads = sorted({n.id for n in ast.walk(f)
if isinstance(n, ast.Name) and n.id in uppers
and isinstance(n.ctx, ast.Load)})
if reads:
findings.append(Finding(
"maintainability", "major",
f"{f.name} reads module globals {reads}: a hidden input"))
span = f.end_lineno - f.lineno + 1
if span > 40:
findings.append(Finding("maintainability", "minor",
f"{f.name} is {span} lines"))
return findings
def mutation_review(src, suite_src, mutations):
def passes(code):
ns = {}
exec(compile(code, "m.py", "exec"), ns)
exec(compile(suite_src, "t.py", "exec"), ns)
s = unittest.TestLoader().loadTestsFromTestCase(ns["T"])
return unittest.TextTestRunner(stream=io.StringIO(),
verbosity=0).run(s).wasSuccessful()
if not passes(src):
return [Finding("tests", "blocker",
"the baseline suite fails: no mutation score is "
"meaningful")], 0, 0
killed = 0
for label, old, new in mutations:
try:
if not passes(src.replace(old, new, 1)):
killed += 1
except Exception:
killed += 1
rate = 100 * killed // len(mutations)
findings = []
if rate < 70:
findings.append(Finding("tests", "blocker",
f"mutation kill rate {rate}% "
f"({killed}/{len(mutations)})"))
return findings, killed, len(mutations)
MODULE = '''"""flow.parse"""
import os
import re
import subprocess
THRESHOLD = 95
LICENCE = os.environ["LM_LICENSE_FILE"]
def classify(text):
errors = sum(1 for l in text.splitlines()
if l.startswith("UVM_ERROR"))
if errors > THRESHOLD:
return "MANY"
return "FAIL" if errors else "PASS"
'''
SUITE = '''
import unittest
class T(unittest.TestCase):
def test_fail(self):
self.assertEqual(classify("UVM_ERROR x\\n"), "FAIL")
def test_smoke(self):
classify("")
def test_trivial(self):
self.assertTrue(True)
'''
TESTABLE = 'def classify(text):\n return "FAIL" if "UVM_ERROR" in text else "PASS"\n'
MUTATIONS = [
("count 2", 'sum(1 for l in', 'sum(2 for l in'),
("invert", 'return "FAIL" if "UVM_ERROR" in text else "PASS"',
'return "PASS" if "UVM_ERROR" in text else "FAIL"'),
]
findings = (review_tests(SUITE) + review_imports(MODULE)
+ review_maintainability(MODULE))
mut_findings, killed, total = mutation_review(TESTABLE, SUITE, MUTATIONS)
findings += mut_findings
ORDER = {"blocker": 0, "major": 1, "minor": 2}
print("maintainability and test review")
for f in sorted(findings, key=lambda f: (ORDER[f.severity], f.area, f.detail)):
print(f" [{f.severity:<7}] {f.area:<16} {f.detail}")
print()
print("findings :", len(findings))
for sev in ("blocker", "major", "minor"):
n = sum(1 for f in findings if f.severity == sev)
if n:
print(f" {sev:<8} {n}")
print()
print("mutation score:", f"{killed}/{total}")
print("areas :", sorted({f.area for f in findings}))maintainability and test review
[blocker] testability a module-level os.environ[...] makes `import` fail without that variable
[blocker] tests mutation kill rate 50% (1/2)
[blocker] tests test_trivial asserts only constants: it cannot fail
[major ] maintainability classify reads module globals ['THRESHOLD']: a hidden input
[major ] testability the parsing module imports subprocess
[major ] tests test_smoke has no assertion: it fails only if the call raises
findings : 6
blocker 3
major 3
mutation score: 1/2
areas : ['maintainability', 'testability', 'tests']Seven findings across three areas, three of them blockers. The mutation score appears as a finding rather than as a number in a report, with a threshold attached — below 70% is a blocker — which is Chapter 29.2's argument that a metric needs a written-down threshold to be actionable.
Note that the review reports both test_trivial (cannot fail) and the kill rate
(50%) separately, and that is not duplication. The first is a defect in a
specific test with a specific fix; the second is a property of the suite as a
whole, and fixing the first would not necessarily move the second.
9. Could a New Owner Run the Suite at All?
The third canonical question has a part the proxies in Section 7 cannot reach. A new owner's first action is not to read the code — it is to run the tests, and a suite can pass for its author and fail for them with nothing changed but the order:
import io
import unittest
STATE = {}
class Suite(unittest.TestCase):
"""Three tests. One of them needs another to have run first."""
def test_a_prepares(self):
STATE["ready"] = True
self.assertTrue(STATE["ready"])
def test_b_uses_it(self):
self.assertTrue(STATE.get("ready", False))
def test_c_independent(self):
self.assertEqual(1 + 1, 2)
def run(order):
STATE.clear()
suite = unittest.TestSuite([Suite(name) for name in order])
res = unittest.TextTestRunner(stream=io.StringIO(), verbosity=0).run(suite)
return res.wasSuccessful(), len(res.failures), res.testsRun
ALPHA = ["test_a_prepares", "test_b_uses_it", "test_c_independent"]
ORDERS = [("alphabetical (the default)", ALPHA),
("reversed", list(reversed(ALPHA))),
("test_b alone", ["test_b_uses_it"])]
print(f"{'order':<28} {'tests':>5} {'ok':<6} failures")
for label, order in ORDERS:
ok, failures, n = run(order)
print(f"{label:<28} {n:>5} {str(ok):<6} {failures}")
print()
print("passes in the default order :", run(ALPHA)[0])
print("fails reversed :", not run(list(reversed(ALPHA)))[0])
print("fails when run alone :", not run(["test_b_uses_it"])[0])
print("lines of code changed :", 0)order tests ok failures
alphabetical (the default) 3 True 0
reversed 3 False 1
test_b alone 1 False 1
passes in the default order : True
fails reversed : True
fails when run alone : True
lines of code changed : 0Green in the default order, red reversed, red alone. unittest sorts test
methods alphabetically, so test_a_prepares happens to run before
test_b_uses_it and the dependency is invisible — until somebody renames a
test, runs one by name, or uses a runner that shuffles.
This is Chapter 33.7 Section 6's leak in its cheapest possible form. There the leaked state was the process working directory; here it is a module-level dict. The mechanism is identical and so is the symptom: one test's residue is another test's precondition, and the suite is green because of an accident of alphabetical ordering.
The symptom is worth naming because it runs backwards from intuition:
CHECKS = [
("run the whole suite", "the author's experience", "always done"),
("run each test by name", "order dependence", "the cheapest check"),
("run the suite reversed", "order dependence", "one line"),
("run from a clean checkout", "missing fixtures", "what CI does"),
("run with no licence and no network", "hidden environment needs",
"what a new owner has"),
]
print(f"{'what the reviewer runs':<36} {'what it finds':<24} note")
for what, finds, note in CHECKS:
print(f"{what:<36} {finds:<24} {note}")
print()
print("checks needing the code read:", 0)
print("checks the author has done :", 1)
print()
print("order dependence looks like :", "fails alone, passes in the suite")
print("which is backwards because :", "a smaller run is usually safer")
print()
print("so the review finding reads :", "test_b_uses_it fails when run by name")
print("and not :", "the tests share state")what the reviewer runs what it finds note
run the whole suite the author's experience always done
run each test by name order dependence the cheapest check
run the suite reversed order dependence one line
run from a clean checkout missing fixtures what CI does
run with no licence and no network hidden environment needs what a new owner has
checks needing the code read: 0
checks the author has done : 1
order dependence looks like : fails alone, passes in the suite
which is backwards because : a smaller run is usually safer
so the review finding reads : test_b_uses_it fails when run by name
and not : the tests share stateFive checks, none of which needs a line of the code read, and the author has done one of them. That ratio is the argument for doing this before the maintainability proxies in Section 7: it is faster than reading, and it finds defects that no proxy in this chapter can see, because none of them inspects state shared between tests.
The final pair of lines is the part to carry into a review comment.
test_b_uses_it fails when run by name is a reproduction the author can
confirm in ten seconds, and the tests share state is an opinion they can
argue with. Chapter 36.1 Section 7 made that distinction about failure
definitions; it applies unchanged to findings about tests.
That is the whole handover question made concrete. A new owner does not inherit the author's working directory, their environment, their licence server or their habit of running the full suite — so a suite depending on any of those is a suite that works for exactly one person, which is the thing Chapter 34.6's documentation cannot fix and this chapter's proxies cannot score.
10. Predict, Run, Observe
import ast
import io
import unittest
def classify(text, per_line=1):
errors = sum(per_line for l in text.splitlines()
if l.startswith("UVM_ERROR"))
return "FAIL" if errors else "PASS"
# A
print("A:", classify("UVM_ERROR x\n", 1), classify("UVM_ERROR x\n", 2))
# B
SRC = "class T(unittest.TestCase):\n def test_a(self):\n pass\n"
tree = ast.parse("import unittest\n" + SRC)
cls = next(n for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
m = cls.body[0]
asserts = [n for n in ast.walk(m) if isinstance(n, ast.Call)]
print("B:", len(asserts))
# C
class T(unittest.TestCase):
def test_a(self):
pass
res = unittest.TextTestRunner(stream=io.StringIO(), verbosity=0).run(
unittest.TestLoader().loadTestsFromTestCase(T))
print("C:", res.testsRun, res.wasSuccessful())
# D
print("D:", 100 * 1 // 6)The verified output:
A: FAIL FAIL
B: 0
C: 1 True
D: 16A — one error and two errors are both FAIL. Section 5 in one line, and the
reason a status assertion cannot see an arithmetic mutation.
B — a test body of pass contains zero calls, so the AST audit finds it
without running anything.
C — and that empty test runs and passes. One test, green, checking nothing — which is what makes the audit necessary: the test runner reports it as a success.
D — the kill rate, integer-divided. The number is blunt on purpose: a review
needs a threshold it can apply, and 16% against a 70% bar is a finding
anybody can act on without arguing about the arithmetic.
11. Exercises
Exercise 1 — four observations. For a PR you are reviewing, write down what each of them is also consistent with.
Exercise 2 — the one that hides. Explain why only the "can the tests fail" question can be wrong while looking right.
Exercise 3 — audit the assertions. Run the AST audit on a real suite and count the tests that cannot fail.
Exercise 4 — three reasons. Classify each into no-assertion, constants-only, or can-fail.
Exercise 5 — a weak test with a purpose. Find one, and write the finding as a gap rather than a defect.
Exercise 6 — the style complaint. Identify a finding you should not write, and say why.
Exercise 7 — six mutations. Write them for a parser your team owns.
Exercise 8 — the baseline first. Confirm the suite passes before mutating, and say what a failing baseline would do to the score.
Exercise 9 — the kill rate. Measure it.
Exercise 10 — classify the survivors. Decide for each whether it needs a fixture or an assertion.
Exercise 11 — the arithmetic mutation. Construct one that no status assertion can see.
Exercise 12 — a missing subject. Explain the difference between a missing case and a missing assertion subject.
Exercise 13 — the composition. Check how many of your survivors are fixed by Chapter 36.2's battery.
Exercise 14 — the import audit. Run it on your parsing module.
Exercise 15 — work at import time. Search for a module-level call and say
what it does to import.
Exercise 16 — os.environ at module level. Explain why it makes a test
suite unrunnable in CI and what the failure looks like.
Exercise 17 — three proxies. Measure docstring coverage, the longest function and the globals read.
Exercise 18 — the useful two. Say which of the three proxies you would act on.
Exercise 19 — a hidden input. Find a function reading a module global and describe what a new owner discovers.
Exercise 20 — nesting in a short function. Find a triple loop and work out whether it needs to be one.
Exercise 21 — the documented one. Check whether your documented functions are the puzzling ones.
Exercise 22 — a threshold for the score. Choose yours and justify the number.
Exercise 23 — two findings, not one. Explain why a specific dead test and a low kill rate are separate findings.
Exercise 24 — an empty test passes. Confirm it, and say what that means for reading CI output.
Exercise 25 — run one test by name. Pick a test from a real suite and run it alone.
Exercise 26 — reverse the suite. Run it in reverse order and record the result.
Exercise 27 — the inverted symptom. State what order dependence looks like and why that is counter-intuitive.
Exercise 28 — alphabetical by accident. Explain how unittest's ordering hides the dependency.
Exercise 29 — what exposes it. Name three changes that would turn the suite red.
Exercise 30 — the same leak, twice. Compare this with Chapter 33.7 Section 6 and say what the shared state was in each.
Exercise 31 — a clean checkout. Run your suite somewhere with nothing set up and record what breaks.
Exercise 32 — no licence, no network. Try it, and list what the suite turns out to require.
Exercise 33 — five checks, no reading. Do all five on one real suite and count the findings.
Exercise 34 — write the reproduction. Rewrite a 'the tests share state' comment as a command the author can run.
12. Summary
- None of the four observations a reviewer has — green suite, test count, coverage, green CI — distinguishes a good suite from a useless one.
- Only the "can the tests fail" question can be wrong while looking right. Untestable imports and unmaintainable shape both declare themselves.
- Three of five tests could not fail, for three different reasons: no assertion, constants only, and a try/except that checks only for a raise.
- A weak test of a real property is a gap, not a defect.
test_runs_without_raisinggenuinely checks something; the finding is that it is the only test of the empty-log path. - The kill rate was 1 of 6 — 16% — from a green five-test suite, and the rate reflects the two tests that can fail rather than the five that exist.
- Check the baseline passes before mutating, or every mutation is a false kill.
- Survivors come in two kinds. Four needed a fixture; one needed a new
assertion subject —
errors counts 2 per lineis invisible to every status assertion because one error and two errors are bothFAIL. - Four of five survivors are fixed by Chapter 36.2's battery, so the parsing review and the test review compose.
- The import audit answers "can it be tested without the tool" mechanically.
A module-level
os.environ[...]makesimportraise on a machine without that variable, and the failure looks like a broken suite rather than a structural problem. - The useful maintainability proxies are globals read and nesting depth, not docstring coverage — a function reading a module global has a hidden input, and a triple loop in eight lines makes a reader assume there is a reason.
- Do not flag a two-line helper for having no docstring. The finding worth writing is that the puzzling function is the undocumented one.
- Report the mutation score as a finding with a threshold, and keep it separate from the specific dead tests — fixing one does not move the other.
- An empty test runs and passes, which is why the AST audit is necessary:
- A suite can pass for its author and fail for a new owner with nothing changed but the order — green alphabetically, red reversed, red when one test is run alone, zero lines changed.
unittestsorts test methods alphabetically, so a dependency between two tests stays invisible until somebody renames one, runs one by name, or shuffles.- It is Chapter 33.7 Section 6's leak in its cheapest form — there the shared state was the process working directory, here a module-level dict.
- Order dependence has an inverted symptom: it fails alone and passes in the suite, which is why it survives so long.
- Five review checks need no code read at all, and the author has done exactly one of them — running the whole suite.
- Write the finding as a reproduction —
test_b_uses_itfails when run by name — not as an observation about shared state. the runner reports it as a success.
The last chapter of the module, and of the track, asks the question all of this has been leading to — whether Python was the right choice at all.
Where this fits
Part of the Python curriculum.
