Python Part II · Module 25 · Simulation Automation
Compiling the Design and Keeping the Compile Log
Step three takes the filelist and turns it into something runnable. It is the first step that invokes a real tool, so everything Modules 23 and 24 built arrives at once — the run directory, the log file, the timeout, the recorded command. What is new is the decision at the end: a design that does not compile has not failed a test, and the single most consequential line in this chapter is the one that stops before running one.
Intermediate18 min readPythonPart IIAutomationSimulationVerification Flow
Part II · Module 25 · Chapter 25.3 · Compiling the Design and Keeping the Compile Log
1. Stop Before Running
res = compile_design(run_dir, files_f, sim=sim)
if res.status != "OK":
return resTwo lines, and the second is the one worth arguing for.
COMPILE_FAILED = True
print("without the early return:")
print(" compile : Error-[SE] Syntax error, core/regfile.sv line 18")
print(" run : ./simv: No such file or directory")
print(" log ends : the LAST error is the one about simv")
print()
print("with the early return:")
print(" compile : Error-[SE] Syntax error, core/regfile.sv line 18")
print(" run : not attempted")
print(" status : COMPILE_ERROR")
print()
print("errors a person sees, without:", 2)
print("errors a person sees, with :", 1)
print("which one is read first :", "the last one printed")without the early return:
compile : Error-[SE] Syntax error, core/regfile.sv line 18
run : ./simv: No such file or directory
log ends : the LAST error is the one about simv
with the early return:
compile : Error-[SE] Syntax error, core/regfile.sv line 18
run : not attempted
status : COMPILE_ERROR
errors a person sees, without: 2
errors a person sees, with : 1
which one is read first : the last one printedRunning the simulator after a failed compile manufactures a second, louder, useless error. It arrives last, so it is what a reader sees first, and it points at a missing binary rather than at line 18 of regfile.sv.
Chapter 23.4 made this argument for a single tool: a non-zero return code is a decision, not a log line. Here it decides the shape of a two-step function — and Chapter 23.11 Section 6's runner stopped at the first failing step for exactly this reason.
2. The Compile Step
import os
import sys
# A stand-in compiler. Writes a log, and a binary unless told to fail.
mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
log.write("FakeVCS Version 1.0\n")
log.write("Parsing design file 'core/alu.sv'\n")
if mode == "syntax":
log.write("Error-[SE] Syntax error\n")
log.write(' "core/regfile.sv", 18: token is \';\'\n')
sys.exit(1)
if mode == "nobinary":
log.write("Compilation completed\n")
sys.exit(0)
log.write("Compilation completed\n")
with open("simv", "w", encoding="utf-8") as binary:
binary.write("#!/bin/sh\necho simulating\n")
os.chmod("simv", 0o755)import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass(frozen=True)
class CompileResult:
status: str
returncode: Optional[int] = None
log_path: Optional[str] = None
binary: Optional[str] = None
errors: tuple = ()
COMPILER = Path("fake_vcs.py").resolve()
def compile_design(run_dir, files_f, *, sim, mode="ok", timeout_s=600):
run_dir = Path(run_dir)
run_dir.mkdir(parents=True, exist_ok=True)
cmd = [sys.executable, str(COMPILER), "-f", str(Path(files_f).resolve())]
env = {**os.environ, "FAKE_MODE": mode}
cp = subprocess.run(cmd, cwd=run_dir, env=env, timeout=timeout_s,
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
log = run_dir / "compile.log"
return CompileResult(status="OK" if cp.returncode == 0 else "COMPILE_ERROR",
returncode=cp.returncode, log_path=str(log),
binary=str(run_dir / "simv"))
Path("files.f").write_text("core/alu.sv\n", encoding="utf-8")
for mode in ("ok", "syntax"):
res = compile_design(f"runs/{mode}", "files.f", sim="vcs", mode=mode)
print(f"{mode:<8} status={res.status:<14} rc={res.returncode} "
f"log exists={Path(res.log_path).exists()}")ok status=OK rc=0 log exists=True
syntax status=COMPILE_ERROR rc=1 log exists=TrueEverything from Modules 23 and 24 is in that call. The command is a list (23.2), shell=False is the default (23.3), the return code decides the status (23.4), output goes to a log file in the run directory rather than being captured (23.5), cwd=run_dir keeps the artefacts contained (23.7), and a timeout_s bounds it (23.8).
The log exists in both cases, which is the point of writing it to a file: a failing compile's log is the entire content of the failure, and capturing it into a variable that is then discarded on the error path is the classic way to lose it.
3. COMPILE_ERROR Is Not FAIL
RESULTS = [("axi_smoke", "COMPILE_ERROR"), ("axi_reset", "COMPILE_ERROR"),
("axi_burst", "COMPILE_ERROR"), ("axi_soak", "COMPILE_ERROR")]
print("a regression where the design does not compile:")
for test, status in RESULTS:
print(f" {test:<11} {status}")
print()
print("if these were reported as FAIL:")
print(" it reads as :", "four tests found four bugs")
print(" it actually is :", "one syntax error, zero tests run")
print(" who gets paged :", "the verification team, wrongly")
print()
print("tests that exercised the design:", 0)
print("distinct root causes :", 1)a regression where the design does not compile:
axi_smoke COMPILE_ERROR
axi_reset COMPILE_ERROR
axi_burst COMPILE_ERROR
axi_soak COMPILE_ERROR
if these were reported as FAIL:
it reads as : four tests found four bugs
it actually is : one syntax error, zero tests run
who gets paged : the verification team, wrongly
tests that exercised the design: 0
distinct root causes : 1Chapter 25.1 Section 6 put COMPILE_ERROR in the status vocabulary and the third column of that table was who looks at it. This is that argument with numbers on it: a compile error sends somebody to the RTL, and a FAIL sends somebody to the testbench, and reporting one as the other sends the wrong team to the wrong file.
It also shares ERROR's property from Chapter 23.9 Section 7: none of those runs tested anything, so four COMPILE_ERROR rows in a hundred-test regression make the pass rate 96 out of 96, not 96 out of 100.
The two are still distinct. ERROR means the infrastructure broke; COMPILE_ERROR means the infrastructure worked perfectly and told you the design is wrong. That is a real result about the design — just not a test result.
4. Reading the Compile Log
The log below is a constructed specimen — no simulator is installed here, per Chapter 23.1 Section 1 — written in the shape this family of tools uses. What is demonstrated is the parsing design, not any vendor's exact wording:
FakeVCS Version 1.0 -- Build Date Jan 01 2024
Parsing design file 'core/alu.sv'
Parsing design file 'core/fifo.sv'
Warning-[PCWM-W] Port connection width mismatch
core/fifo.sv, 42
Error-[SE] Syntax error
Following verilog source has syntax error :
"core/regfile.sv", 18: token is ';'
Error-[IND] Identifier not declared
core/regfile.sv, 25: 'wdata_q' is not declared
Warning-[LCA_FEATURES_ENABLED] Usage warningimport re
from pathlib import Path
TAG = re.compile(r"^(Error|Warning)-\[([\w-]+)\]\s*(.*)")
LOC = re.compile(r'"?([\w./]+\.sv)"?,\s*(\d+)')
lines = Path("compile.log").read_text(encoding="utf-8").splitlines()
findings = []
for i, line in enumerate(lines):
m = TAG.match(line)
if not m:
continue
kind, tag, text = m.groups()
where = None
for ahead in lines[i + 1:i + 4]:
hit = LOC.search(ahead)
if hit:
where = f"{hit.group(1)}:{hit.group(2)}"
break
findings.append((kind, tag, text, where))
for kind, tag, text, where in findings:
print(f"{kind:<8} {tag:<22} {str(where):<22} {text}")
print()
print("errors :", sum(1 for k, *_ in findings if k == "Error"))
print("warnings:", sum(1 for k, *_ in findings if k == "Warning"))Warning PCWM-W core/fifo.sv:42 Port connection width mismatch
Error SE core/regfile.sv:18 Syntax error
Error IND core/regfile.sv:25 Identifier not declared
Warning LCA_FEATURES_ENABLED None Usage warning
errors : 2
warnings: 2Three design decisions in that parser, and the second is the one most people get wrong first.
The tag is the classifier, not the message text. SE and IND are stable identifiers; the human-readable text after them changes between releases. Chapter 19's advice applies — anchor on the stable part.
The location is on a following line. Error-[SE] says what happened and the file and line arrive one or two lines later, so a scanner that looks at one line at a time finds every error and knows where none of them are. The three-line lookahead is the whole fix, and the last row shows what happens when there is no location at all: None, recorded rather than crashed.
Errors cascade. Error-[IND] says wdata_q is not declared — which is almost certainly true because of the syntax error two lines above. Two errors, one root cause, and the count is the less useful number:
FINDINGS = [("Error", "SE", "core/regfile.sv:18"),
("Error", "IND", "core/regfile.sv:25"),
("Error", "IND", "core/regfile.sv:31"),
("Error", "IND", "core/regfile.sv:44")]
print("error count :", len(FINDINGS))
print("first error :", FINDINGS[0][1], "at", FINDINGS[0][2])
print("distinct tags :", sorted({t for _, t, _ in FINDINGS}))
print("distinct files :", sorted({w.split(':')[0] for *_, w in FINDINGS}))
print()
print("what to put in the result:", "the first error, and the count")
print("what to send somebody to :", FINDINGS[0][2])error count : 4
first error : SE at core/regfile.sv:18
distinct tags : ['IND', 'SE']
distinct files : ['core/regfile.sv']
what to put in the result: the first error, and the count
what to send somebody to : core/regfile.sv:18Four errors and one thing to fix. This is Chapter 20.10's first-failure argument, which was about a simulation log, arriving unchanged in the compile log: the count tells you how bad it looks and the first one tells you where to go. Record both; lead with the first.
5. A Zero Return Code Is Not a Binary
import os
import sys
mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
log.write("FakeVCS Version 1.0\n")
if mode == "syntax":
log.write("Error-[SE] Syntax error\n")
log.write(' "core/regfile.sv", 18: token is \';\'\n')
sys.exit(1)
log.write("Compilation completed\n")
if mode != "nobinary":
with open("simv", "w", encoding="utf-8") as binary:
binary.write("#!/bin/sh\necho simulating\n")
os.chmod("simv", 0o755)import os
import subprocess
import sys
from pathlib import Path
COMPILER = Path("fake_vcs.py").resolve()
def compile_once(run_dir, mode):
run_dir = Path(run_dir)
run_dir.mkdir(parents=True, exist_ok=True)
env = {**os.environ, "FAKE_MODE": mode}
cp = subprocess.run([sys.executable, str(COMPILER)], cwd=run_dir, env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
binary = run_dir / "simv"
return cp.returncode, binary.exists(), os.access(binary, os.X_OK) if binary.exists() else False
for mode in ("ok", "nobinary", "syntax"):
rc, exists, executable = compile_once(f"runs/b_{mode}", mode)
verdict = "OK" if rc == 0 and exists and executable else "COMPILE_ERROR"
print(f"{mode:<10} rc={rc} binary={exists!s:<6} executable={executable!s:<6} -> {verdict}")ok rc=0 binary=True executable=True -> OK
nobinary rc=0 binary=False executable=False -> COMPILE_ERROR
syntax rc=1 binary=False executable=False -> COMPILE_ERRORThe middle row exited zero and produced nothing.
A return code is the tool's claim about its own work. Real compilers manage this surprisingly often: an analysis-only invocation that was never asked to elaborate, a flag combination that produces a library rather than a binary, a tool that reports success and then fails to link. Chapter 23.4 Section 3 said the return code is necessary and not sufficient; here is the sufficient part.
Checking the artefact as well as the status costs one exists() and converts a bewildering downstream failure — Section 1's "cannot execute ./simv" — into a compile error reported at the compile step, which is where somebody can act on it.
The executable bit is checked too, for Chapter 23.9 Section 4's reason: a file that exists and cannot be run produces PermissionError at launch, three steps later.
6. The Step, Assembled
import os
import sys
mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
log.write("FakeVCS Version 1.0\n")
if mode == "syntax":
log.write("Error-[SE] Syntax error\n")
log.write(' "core/regfile.sv", 18: token is \';\'\n')
sys.exit(1)
log.write("Compilation completed\n")
if mode != "nobinary":
with open("simv", "w", encoding="utf-8") as binary:
binary.write("#!/bin/sh\necho simulating\n")
os.chmod("simv", 0o755)import os
import re
import shlex
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
TAG = re.compile(r"^(Error|Warning)-\[([\w-]+)\]\s*(.*)")
LOC = re.compile(r'"?([\w./]+\.sv)"?,\s*(\d+)')
COMPILER = Path("fake_vcs.py").resolve()
@dataclass(frozen=True)
class CompileResult:
status: str
returncode: Optional[int] = None
log_path: Optional[str] = None
binary: Optional[str] = None
first_error: Optional[str] = None
error_count: int = 0
warning_count: int = 0
runtime_s: float = field(default=0.0, compare=False)
def parse_compile_log(path):
lines = Path(path).read_text(encoding="utf-8", errors="replace").splitlines()
out = []
for i, line in enumerate(lines):
m = TAG.match(line)
if not m:
continue
where = next((f"{h.group(1)}:{h.group(2)}"
for h in (LOC.search(a) for a in lines[i + 1:i + 4]) if h), None)
out.append((m.group(1), m.group(2), where))
return out
def compile_design(run_dir, files_f, *, sim, mode="ok", timeout_s=600):
"""Compile once. Always returns a CompileResult; never raises for a bad design."""
run_dir = Path(run_dir)
run_dir.mkdir(parents=True, exist_ok=True)
cmd = [sys.executable, str(COMPILER), "-f", str(Path(files_f).resolve())]
(run_dir / "compile_cmd.sh").write_text(
"#!/bin/sh\nexec " + shlex.join(cmd) + "\n", encoding="utf-8")
env = {**os.environ, "FAKE_MODE": mode}
try:
cp = subprocess.run(cmd, cwd=run_dir, env=env, timeout=timeout_s,
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
except subprocess.TimeoutExpired:
return CompileResult(status="NO_FINISH", log_path=str(run_dir / "compile.log"))
log = run_dir / "compile.log"
findings = parse_compile_log(log) if log.exists() else []
errors = [f for f in findings if f[0] == "Error"]
binary = run_dir / "simv"
ok = cp.returncode == 0 and binary.exists() and os.access(binary, os.X_OK)
return CompileResult(
status="OK" if ok else "COMPILE_ERROR",
returncode=cp.returncode,
log_path=str(log),
binary=str(binary) if binary.exists() else None,
first_error=(f"{errors[0][1]} at {errors[0][2]}" if errors else None),
error_count=len(errors),
warning_count=len(findings) - len(errors))
Path("files.f").write_text("core/alu.sv\n", encoding="utf-8")
for mode in ("ok", "syntax", "nobinary"):
res = compile_design(f"runs/{mode}", "files.f", sim="vcs", mode=mode)
print(f"{mode:<9} {res.status:<14} errors={res.error_count} "
f"first={str(res.first_error):<28} binary={res.binary is not None}")
print()
print("compile_cmd.sh written:", Path("runs/ok/compile_cmd.sh").exists())ok OK errors=0 first=None binary=True
syntax COMPILE_ERROR errors=1 first=SE at core/regfile.sv:18 binary=False
nobinary COMPILE_ERROR errors=0 first=None binary=False
compile_cmd.sh written: TrueThe nobinary row is the one to read twice: zero errors, zero warnings, and COMPILE_ERROR. The log says nothing is wrong and the artefact is missing, and the status reflects the artefact. A step that trusted the log alone would have called that a success.
Three other decisions carry forward.
A compile timeout is NO_FINISH, not COMPILE_ERROR. Chapter 23.8 Section 5's distinction: the tool did not finish, so it never reached a verdict about the design. A six-hour gate-level compile hitting its limit is a resource problem, not an RTL problem.
compile_cmd.sh is written before the compile runs, not after. Chapter 23.10's artefact is most useful on the run that failed, and writing it afterwards means the timeout path never writes it at all.
runtime_s carries compare=False — Chapter 25.1 Section 7, applied to this result type too, so two compiles of the same design compare equal.
7. Compiling Once for Many Tests
import hashlib
SOURCES = ["core/alu.sv", "core/fifo.sv", "core/regfile.sv"]
TESTS = ["axi_smoke", "axi_reset", "axi_burst", "axi_soak", "axi_hang"]
def compile_key(sources, defines, sim):
"""Everything that changes the binary. Anything omitted is a stale-cache bug."""
canonical = "\n".join([sim, *sorted(f"{k}={v}" for k, v in sorted(defines.items())),
*sorted(sources)])
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:12]
cache = {}
compiles = 0
for test in TESTS:
key = compile_key(SOURCES, {"DUMP_WAVES": 1}, "vcs")
if key not in cache:
cache[key] = f"runs/build_{key}/simv"
compiles += 1
print(f"{test:<11} key={key} binary={cache[key]}")
print()
print("tests :", len(TESTS))
print("compiles:", compiles)
print()
other = compile_key(SOURCES + ["core/newmod.sv"], {"DUMP_WAVES": 1}, "vcs")
print("a changed source set gives a different key:", other != key)
print("a changed define likewise :",
compile_key(SOURCES, {"DUMP_WAVES": 0}, "vcs") != key)
print("a changed simulator likewise :",
compile_key(SOURCES, {"DUMP_WAVES": 1}, "xrun") != key)axi_smoke key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_reset key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_burst key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_soak key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_hang key=8b542636c900 binary=runs/build_8b542636c900/simv
tests : 5
compiles: 1
a changed source set gives a different key: True
a changed define likewise : True
a changed simulator likewise : TrueFive tests, one compile, and in a four-hundred-test regression that is the difference between a nightly that finishes and one that does not. A compile is typically the longest single step in a flow and it produces the same binary for every test that shares its inputs.
The whole risk lives in the key, and it is worth stating starkly:
Anything that changes the binary and is not in the key is a stale-binary bug — and every test in the regression inherits it.
That is the worst failure mode in this module, because it is silent and it is global. A key that omits the defines serves a DUMP_WAVES=0 binary to a run that asked for waves, and the waveform is simply absent. A key that omits the simulator serves a VCS binary to an Xcelium run. A key that omits a source file serves yesterday's RTL to today's regression, and the results look completely normal.
The three checks at the end are not decoration — they are the test that the key covers its inputs, and a cache without them is a cache nobody should trust. Chapter 25.2 Section 8's source fingerprint is the right first component, because it already answers "did the source set change?" in a form that is stable across checkouts.
8. Predict, Run, Observe
import os
import sys
mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
log.write("Error-[SE] Syntax error\n" if mode == "syntax" else "done\n")
if mode == "syntax":
log.write(' "a.sv", 7: token is \';\'\n')
sys.exit(1 if mode == "syntax" else 0)import os
import re
import subprocess
import sys
from pathlib import Path
C = Path("fake_vcs.py").resolve()
TAG = re.compile(r"^(Error|Warning)-\[([\w-]+)\]")
LOC = re.compile(r'"?([\w./]+\.sv)"?,\s*(\d+)')
def run(mode):
d = Path("r_" + mode)
d.mkdir(exist_ok=True)
cp = subprocess.run([sys.executable, str(C)], cwd=d,
env={**os.environ, "FAKE_MODE": mode},
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
return cp.returncode, (d / "compile.log").read_text(encoding="utf-8")
# A
rc, log = run("ok")
print("A:", rc, repr(log.strip()))
# B
rc, log = run("syntax")
print("B:", rc)
# C
print("C:", [TAG.match(l).group(2) for l in log.splitlines() if TAG.match(l)])
# D
print("D:", TAG.match(log.splitlines()[0]) is not None,
LOC.search(log.splitlines()[0]) is not None)
# E
print("E:", LOC.search(log.splitlines()[1]).groups())
# F
print("F:", Path("r_ok/simv").exists())
# G
print("G:", "COMPILE_ERROR" if rc != 0 or not Path("r_syntax/simv").exists() else "OK")The verified output:
A: 0 'done'
B: 1
C: ['SE']
D: True False
E: ('a.sv', '7')
F: False
G: COMPILE_ERRORA — a clean compile exits zero and the log is kept either way.
B — a syntax error exits non-zero.
C — the tag is what identifies the error class.
D — the error line matches the tag pattern and carries no location.
E — the location is on the next line. Section 4's whole point.
F — this stand-in writes no binary, so even the clean run has none.
G — and the artefact check is what turns that into COMPILE_ERROR regardless of the code.
9. Exercises
Exercise 1 — the second error. Run a simulator after a failed compile and record both errors. Say which a reader sees first.
Exercise 2 — the early return. Add it and count the errors a person now sees.
Exercise 3 — every module so far. For your own compile step, name the chapter each of its six decisions came from.
Exercise 4 — the log survives. Show the compile log present after a failing compile, and say what capturing instead would have cost.
Exercise 5 — four of one. Take a regression where the design does not compile and compute the pass rate with and without those rows in the denominator.
Exercise 6 — who gets paged. Say where COMPILE_ERROR sends somebody and where FAIL does.
Exercise 7 — ERROR versus COMPILE_ERROR. Distinguish them in one sentence each.
Exercise 8 — the tag. Extract error tags from a real compile log. Say why the tag beats the message text.
Exercise 9 — the missing location. Show that the error line has no file or line, and implement the lookahead.
Exercise 10 — no location at all. Produce a finding with no location and show your parser recording None rather than crashing.
Exercise 11 — the cascade. Find a real log where one root cause produced several errors. Report the count and the first.
Exercise 12 — count or first. Say what each number is for, and which belongs at the top of a report.
Exercise 13 — zero and nothing. Construct a compile that exits zero and produces no binary. Say how your step catches it.
Exercise 14 — the executable bit. Say what a non-executable binary produces three steps later, citing Chapter 23.9.
Exercise 15 — the timeout status. Argue in two sentences why a compile timeout is NO_FINISH.
Exercise 16 — write the command first. Say what is lost if compile_cmd.sh is written after the compile.
Exercise 17 — compile once. Implement the cache and report compiles against tests for your regression.
Exercise 18 — cover the key. Write the three checks that prove your key changes when its inputs do. Add a fourth for something your flow has that this example does not.
Exercise 19 — the stale binary. Describe the symptom of a key that omits the defines, and say why it is worse than a crash.
Exercise 20 — which world. Decide whether your cache must handle in-place edits, and implement the consequence.
10. Summary
- Return early on a failed compile. Running the simulator afterwards manufactures a louder, useless error that arrives last and is read first.
- The compile step is every lesson from Modules 23 and 24 at once: list form, return code, log to a file,
cwd=, timeout, recorded command. - Write the log to a file, not a variable, because a failing compile's log is the failure.
COMPILE_ERRORsends somebody to the RTL;FAILsends somebody to the testbench. Reporting one as the other sends the wrong team.- Compile errors tested nothing, so they must not count in a pass-rate denominator — the same rule as
ERROR. ERRORmeans the infrastructure broke;COMPILE_ERRORmeans it worked and the design is wrong.- Classify on the error tag, which is stable, not the message text, which is not.
- The file and line are on a following line, so a one-line-at-a-time scan knows every error and no locations. Use a short lookahead.
- Record
Nonefor a finding with no location rather than crashing on it. - Errors cascade: the count says how bad it looks, the first error says where to go. Record both, lead with the first.
- A zero return code is not a binary. Check the artefact exists and is executable.
- A compile timeout is
NO_FINISH, because the tool never reached a verdict about the design. - Write
compile_cmd.shbefore the compile, or the timeout path never writes it. - Compile once per distinct input set — five tests, one compile, and in a regression that is the difference between finishing and not.
- Anything that changes the binary and is not in the cache key is a stale-binary bug, inherited silently by every test.
- Test that the key changes when sources, defines or the simulator change.
- The key identifies the input set, not the contents — decide whether your flow needs content hashing or a clean-checkout guarantee.
The next chapter runs the binary this one produced, with a test name and a seed.
Where this fits
Part of the Python curriculum.
