Skip to content
VLSI Mentor

Python Part II · Module 22 · Building Command-Line VLSI Tools

A Command-Line Contract a Whole Team Shares

Everything in this module so far makes one script well-behaved. A flow is twenty of them, written by six people over four years, and being individually well-behaved is not enough — one tool uses --test and another uses --testname, one exits 1 for a failing test and another exits 1 for a missing file, one prints a result and another prints a result with a banner above it. Each is defensible alone and together they do not compose, so every pair of tools acquires an adapter and the adapters are where the flow's real bugs live. The fix is three agreements and about thirty lines.

Intermediate18 min readPythonPart IIAutomationVerification FlowTooling

Part II · Module 22 · Chapter 22.6 · A Command-Line Contract a Whole Team Shares

1. Three Tools, Three Dialects

Here is a flow that grew one tool at a time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
run_sim.py      --test axi_smoke --seed 4 --sim vcs
check_log.py    --logfile sim.log --simulator vcs
summarise.py    -f results.csv --tool=vcs

Three names for the simulator, three conventions for a flag, and that is the visible half. The invisible half is worse:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TOOLS = {
    "run_sim.py":   {"sim flag": "--sim",       "exit 1 means": "test failed",
                     "stdout": "JSON record"},
    "check_log.py": {"sim flag": "--simulator", "exit 1 means": "could not read the log",
                     "stdout": "a banner, then PASS or FAIL"},
    "summarise.py": {"sim flag": "--tool",      "exit 1 means": "any problem at all",
                     "stdout": "a human table"},
}

print(f"{'tool':<15} {'sim flag':<13} {'exit 1 means':<26} stdout")
for name, facts in TOOLS.items():
    print(f"{name:<15} {facts['sim flag']:<13} {facts['exit 1 means']:<26} {facts['stdout']}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tool            sim flag      exit 1 means               stdout
run_sim.py      --sim         test failed                JSON record
check_log.py    --simulator   could not read the log     a banner, then PASS or FAIL
summarise.py    --tool        any problem at all         a human table

The --sim row is annoying. The exit 1 means row is dangerous, because a wrapper that treats non-zero as "a test failed" will report a missing log file as a design bug — and nothing anywhere will contradict it.

Each tool is individually reasonable. Chapter 22.2 would approve of all three parsers, Chapter 22.3 of all three error messages. What is missing is not quality; it is agreement.

2. Three Things to Agree

Three blocks in a row joined by two arrows. On the left, flag names: the same spelling for the same idea in every tool, so a command is predictable without reading the help. In the middle, exit-code meanings: zero passed, one failed, two could not run, identically in every tool. On the right, output format: a machine-readable result on standard output, so one tool's answer is another tool's input. An arrow labelled predictable runs from flag names to exit codes, and an arrow labelled composable runs from exit codes to output format.Flag namesone spelling per ideaExit codes0 pass, 1 fail, 2 infraOutput formata result on stdoutusablecomposes12
Figure 1 — the three agreements. Flag names make a command predictable, exit codes make a result machine-readable, and output format makes one tool's answer another tool's input. A flow needs all three; any two leave a gap an adapter has to fill.

The canonical annotation states the middle one as a comment and the left one by example:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# every tool in the flow: 0 pass, 1 fail, 2 infrastructure error
run_sim.py --test T --seed N
run_regress.py --suite nightly

Note what those two commands share and what they do not. Both tools take --sim, --json and --quiet because every tool in the flow does; --test belongs to one and --suite to the other, because they are different jobs. A contract is about the flags every tool has, not about making every tool take the same arguments.

3. The Contract as a Module

An agreement written in a wiki is one people have to remember. An agreement written as thirty lines of Python is one a new tool inherits:

Azvya Education Pvt. Ltd.VLSI Mentor
flowcli.py
"""The command-line contract every tool in this flow keeps."""
import argparse
import json
import sys

EXIT_OK = 0        # ran, and everything passed
EXIT_FAILED = 1    # ran, and something failed
EXIT_INFRA = 2     # could not run at all

SUPPORTED_SIMS = ["vcs", "xrun", "vsim"]


def make_parser(prog, description):
    """A parser carrying the flags every tool in the flow accepts."""
    parser = argparse.ArgumentParser(prog=prog, description=description)
    parser.add_argument("--sim", default="vcs", choices=SUPPORTED_SIMS,
                        help="simulator (default: %(default)s)")
    parser.add_argument("--json", action="store_true",
                        help="emit the result as JSON on stdout")
    parser.add_argument("--quiet", action="store_true",
                        help="suppress progress on stderr")
    return parser


def emit(record, as_json):
    """The result, on stdout, in one of the two agreed shapes."""
    if as_json:
        print(json.dumps(record, sort_keys=True))
    else:
        print(f"{record['status']:<6} {record.get('name', '')}")


def note(text, quiet):
    """Commentary, on stderr, unless the caller asked for quiet."""
    if not quiet:
        print(text, file=sys.stderr)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import flowcli

print("exit codes :", flowcli.EXIT_OK, flowcli.EXIT_FAILED, flowcli.EXIT_INFRA)
print("sims       :", flowcli.SUPPORTED_SIMS)

parser = flowcli.make_parser("demo.py", "A tool in this flow.")
args = parser.parse_args(["--json", "--sim", "xrun"])
print("parsed     :", vars(args))

flowcli.emit({"name": "axi_smoke", "status": "FAIL"}, as_json=False)
flowcli.emit({"name": "axi_smoke", "status": "FAIL", "errors": 2}, as_json=True)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exit codes : 0 1 2
sims       : ['vcs', 'xrun', 'vsim']
parsed     : {'sim': 'xrun', 'json': True, 'quiet': False}
FAIL   axi_smoke
{"errors": 2, "name": "axi_smoke", "status": "FAIL"}

Four pieces, each enforcing one part of the agreement.

EXIT_OK, EXIT_FAILED, EXIT_INFRA are Chapter 22.4's three codes with names. sys.exit(EXIT_INFRA) reads as a decision; sys.exit(2) reads as a number somebody chose, and the next person wonders whether 2 was significant.

make_parser gives every tool the common flags. A tool that forgets --json cannot be composed with one that expects it, and this is what makes forgetting impossible.

emit puts the result on stdout in one of two agreed shapes, and sort_keys=True makes the JSON diffable — Chapter 21.4 Section 5.

note puts commentary on stderr and honours --quiet, which is Chapter 22.5 Section 7's promise that --quiet silences the commentary and never the result.

parse_args([...]) with an explicit list is worth noticing too: it takes arguments from a list instead of sys.argv, which is what makes a parser testable without a subprocess. Chapter 22.1's device becomes unnecessary for testing the declaration itself.

4. Two Tools Built on It

Azvya Education Pvt. Ltd.VLSI Mentor
flowcli.py
"""The command-line contract every tool in this flow keeps."""
import argparse
import json
import sys

EXIT_OK = 0
EXIT_FAILED = 1
EXIT_INFRA = 2

SUPPORTED_SIMS = ["vcs", "xrun", "vsim"]


def make_parser(prog, description):
    parser = argparse.ArgumentParser(prog=prog, description=description)
    parser.add_argument("--sim", default="vcs", choices=SUPPORTED_SIMS,
                        help="simulator (default: %(default)s)")
    parser.add_argument("--json", action="store_true", help="emit JSON on stdout")
    parser.add_argument("--quiet", action="store_true", help="suppress progress")
    return parser


def emit(record, as_json):
    if as_json:
        print(json.dumps(record, sort_keys=True))
    else:
        print(f"{record['status']:<6} {record.get('name', '')}")


def note(text, quiet):
    if not quiet:
        print(text, file=sys.stderr)
Azvya Education Pvt. Ltd.VLSI Mentor
run_sim.py
import sys

from flowcli import make_parser, emit, note, EXIT_OK, EXIT_FAILED, EXIT_INFRA

parser = make_parser("run_sim.py", "Run one simulation.")
parser.add_argument("--test", required=True, metavar="NAME")
parser.add_argument("--seed", type=int, default=1, metavar="N")
args = parser.parse_args()

note(f"running {args.test} seed {args.seed} on {args.sim}", args.quiet)

if args.test == "missing_test":
    print(f"run_sim.py: error: no such test {args.test!r}", file=sys.stderr)
    sys.exit(EXIT_INFRA)

errors = 2 if args.test == "axi_smoke" else 0
record = {"name": args.test, "seed": args.seed, "errors": errors,
          "status": "FAIL" if errors else "PASS"}

emit(record, args.json)
sys.exit(EXIT_FAILED if errors else EXIT_OK)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import subprocess
import sys

for extra in (["--test", "axi_reset"], ["--test", "axi_smoke", "--json"],
              ["--test", "missing_test"], ["--test", "axi_reset", "--quiet"]):
    done = subprocess.run([sys.executable, "run_sim.py"] + extra,
                          capture_output=True, text=True)
    print(f"$ run_sim.py {' '.join(extra)}")
    print(f"   rc={done.returncode}  out={done.stdout.strip()!r}")
    print(f"             err={done.stderr.strip()!r}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ run_sim.py --test axi_reset
   rc=0  out='PASS   axi_reset'
             err='running axi_reset seed 1 on vcs'
$ run_sim.py --test axi_smoke --json
   rc=1  out='{"errors": 2, "name": "axi_smoke", "seed": 1, "status": "FAIL"}'
             err='running axi_smoke seed 1 on vcs'
$ run_sim.py --test missing_test
   rc=2  out=''
             err="running missing_test seed 1 on vcs\nrun_sim.py: error: no such test 'missing_test'"
$ run_sim.py --test axi_reset --quiet
   rc=0  out='PASS   axi_reset'
             err=''

The tool's own code is eleven lines and none of them is about the contract — make_parser supplied the common flags, emit chose the stream and shape, note honoured --quiet, and the three constants named the statuses.

Row four is the one to check: --quiet removed the commentary and left the result exactly as it was. A --quiet that silenced stdout would have broken the next section.

5. Composition

Azvya Education Pvt. Ltd.VLSI Mentor
flowcli.py
"""The command-line contract every tool in this flow keeps."""
import argparse
import json
import sys

EXIT_OK = 0
EXIT_FAILED = 1
EXIT_INFRA = 2

SUPPORTED_SIMS = ["vcs", "xrun", "vsim"]


def make_parser(prog, description):
    parser = argparse.ArgumentParser(prog=prog, description=description)
    parser.add_argument("--sim", default="vcs", choices=SUPPORTED_SIMS,
                        help="simulator (default: %(default)s)")
    parser.add_argument("--json", action="store_true", help="emit JSON on stdout")
    parser.add_argument("--quiet", action="store_true", help="suppress progress")
    return parser


def emit(record, as_json):
    if as_json:
        print(json.dumps(record, sort_keys=True))
    else:
        print(f"{record['status']:<6} {record.get('name', '')}")


def note(text, quiet):
    if not quiet:
        print(text, file=sys.stderr)
Azvya Education Pvt. Ltd.VLSI Mentor
run_sim.py
import sys

from flowcli import make_parser, emit, note, EXIT_OK, EXIT_FAILED, EXIT_INFRA

parser = make_parser("run_sim.py", "Run one simulation.")
parser.add_argument("--test", required=True, metavar="NAME")
parser.add_argument("--seed", type=int, default=1, metavar="N")
args = parser.parse_args()

note(f"running {args.test} seed {args.seed} on {args.sim}", args.quiet)

if args.test == "missing_test":
    print(f"run_sim.py: error: no such test {args.test!r}", file=sys.stderr)
    sys.exit(EXIT_INFRA)

errors = 2 if args.test == "axi_smoke" else 0
record = {"name": args.test, "seed": args.seed, "errors": errors,
          "status": "FAIL" if errors else "PASS"}

emit(record, args.json)
sys.exit(EXIT_FAILED if errors else EXIT_OK)
Azvya Education Pvt. Ltd.VLSI Mentor
run_regress.py
import json
import subprocess
import sys

from flowcli import make_parser, emit, note, EXIT_OK, EXIT_FAILED, EXIT_INFRA

SUITES = {"nightly": ["axi_smoke", "axi_reset"], "empty": []}

parser = make_parser("run_regress.py", "Run a suite of simulations.")
parser.add_argument("--suite", required=True, choices=sorted(SUITES))
args = parser.parse_args()

tests = SUITES[args.suite]
if not tests:
    print(f"run_regress.py: error: suite {args.suite!r} has no tests", file=sys.stderr)
    sys.exit(EXIT_INFRA)

results = []
worst = EXIT_OK

for test in tests:
    note(f"-> {test}", args.quiet)
    done = subprocess.run(
        [sys.executable, "run_sim.py", "--test", test, "--sim", args.sim, "--json", "--quiet"],
        capture_output=True, text=True)

    if done.returncode == EXIT_INFRA:
        print(done.stderr.strip(), file=sys.stderr)
        sys.exit(EXIT_INFRA)

    results.append(json.loads(done.stdout))
    worst = max(worst, done.returncode)

emit({"name": args.suite, "status": "FAIL" if worst else "PASS",
      "tests": len(results),
      "failed": sum(1 for r in results if r["status"] == "FAIL")}, args.json)
sys.exit(worst)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import subprocess
import sys

for extra in (["--suite", "nightly", "--json"], ["--suite", "nightly"], ["--suite", "empty"]):
    done = subprocess.run([sys.executable, "run_regress.py"] + extra,
                          capture_output=True, text=True)
    print(f"$ run_regress.py {' '.join(extra)}")
    print(f"   rc={done.returncode}  out={done.stdout.strip()!r}")
    print(f"             err={done.stderr.strip().replace(chr(10), ' | ')!r}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ run_regress.py --suite nightly --json
   rc=1  out='{"failed": 1, "name": "nightly", "status": "FAIL", "tests": 2}'
             err='-> axi_smoke | -> axi_reset'
$ run_regress.py --suite nightly
   rc=1  out='FAIL   nightly'
             err='-> axi_smoke | -> axi_reset'
$ run_regress.py --suite empty
   rc=2  out=''
             err="run_regress.py: error: suite 'empty' has no tests"

run_regress.py calls run_sim.py and relies on all three agreements at once:

Flag names. It passes --sim straight through by name. If the two tools spelled it differently, the suite runner would need a translation table — and the table would have to be updated every time a tool was added.

Output format. It passes --json --quiet and parses done.stdout with no filtering, because the contract guarantees stdout is exactly the record. Chapter 22.5 Section 3's four-character difference is what makes json.loads(done.stdout) safe to write without a try.

Exit codes. This is the one doing the most work. EXIT_INFRA from a child aborts the whole suite immediately, because a missing test means the suite is misconfigured and the remaining runs would be meaningless. EXIT_OK and EXIT_FAILED are reduced with max, so the suite's status is the worst of its children's. Three distinct behaviours from one number, and none of them requires reading a message.

The max reduction works precisely because the codes are ordered by severity — Chapter 22.4's ladder. That ordering is not an accident of the numbers; it is a property worth preserving if a flow ever adds a fourth.

6. What the Caller Can Rely On

Every tool in the flow promises the same five things:

PromiseEnforced by
--sim, --json, --quiet exist and mean the same thingmake_parser
rc 0 passed, 1 failed, 2 could not runthe three constants
stdout is the result; empty when rc is 2emit, and never printing to stdout on the error path
stderr is ignorable commentarynote
--quiet silences commentary onlynote taking the flag

A caller that knows those five can use a tool it has never seen. That is the actual deliverable of this module — not any one well-behaved script, but the property that a new tool is usable without reading it.

It also makes a whole class of wrapper disappear. A nightly job becomes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RESULTS = [
    {"tool": "run_regress.py --suite nightly", "rc": 1},
    {"tool": "run_regress.py --suite smoke", "rc": 0},
    {"tool": "check_coverage.py --db cov.ucdb", "rc": 2},
]

worst = max(r["rc"] for r in RESULTS)
MEANING = {0: "all passed", 1: "something failed", 2: "the flow is broken"}

for result in RESULTS:
    print(f"  rc={result['rc']}  {result['tool']}")
print()
print("nightly exit:", worst, "-", MEANING[worst])
print("who to call :", "nobody" if worst == 0 else ("design/verification" if worst == 1 else "flow owner"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  rc=1  run_regress.py --suite nightly
  rc=0  run_regress.py --suite smoke
  rc=2  check_coverage.py --db cov.ucdb

nightly exit: 2 - the flow is broken
who to call : flow owner

The nightly does not know what any of those tools do. It knows the contract, and the contract is enough to route the morning's email to the right person — which is the whole point of distinguishing 1 from 2 rather than "it worked" and "it did not".

7. Without the Contract

What the same nightly looks like against Section 1's three dialects:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RAW = [
    {"tool": "run_sim.py", "rc": 1, "stdout": '{"status": "FAIL"}'},
    {"tool": "check_log.py", "rc": 1, "stdout": "=== check_log 2.1 ===\nFAIL"},
    {"tool": "summarise.py", "rc": 1, "stdout": "2 of 40 failed"},
]


def adapt(entry):
    """Every tool needs its own translation. This is the layer the contract removes."""
    if entry["tool"] == "run_sim.py":
        return "failed" if entry["rc"] == 1 else "ok"
    if entry["tool"] == "check_log.py":
        return "could not run" if entry["rc"] == 1 else "ok"
    if entry["tool"] == "summarise.py":
        return "something, unclear what"
    return "unknown tool"


for entry in RAW:
    print(f"  {entry['tool']:<15} rc={entry['rc']}  ->  {adapt(entry)}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  run_sim.py      rc=1  ->  failed
  check_log.py    rc=1  ->  could not run
  summarise.py    rc=1  ->  something, unclear what

Three identical exit statuses, three different meanings. The adapter is the only thing that knows, and it knows by having a branch per tool.

That layer has three properties that make it the worst code in a flow:

It is invisible to every validator. Nothing type-checks a mapping from a tool name to a meaning. An adapter that is wrong about check_log.py reports an infrastructure failure as a design bug, forever, and the only symptom is engineers occasionally being sent to the wrong place.

It must be updated when any tool changes, and the tool's author has no reason to know the adapter exists. Chapter 22.3's drifting usage string, at the scale of a flow.

It grows quadratically. Each new tool needs a branch, and each tool that wants to call another needs its own copy of the knowledge.

The contract removes the layer entirely. Not by making it shorter — by making it unnecessary, because there is nothing to translate.

8. Changing a Contract

A contract that cannot change is one people work around. Three rules make changing it survivable:

Add before you remove. A new flag is backwards compatible; a renamed flag breaks every caller. To rename --sim to --simulator, add the new spelling as an alias, let both work for a release, then remove the old one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import argparse

parser = argparse.ArgumentParser(prog="run_sim.py")
parser.add_argument("--simulator", "--sim", dest="sim", default="vcs")

for argv in (["--sim", "xrun"], ["--simulator", "xrun"], []):
    args = parser.parse_args(argv)
    print(f"{str(argv):<22} -> sim={args.sim}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['--sim', 'xrun']      -> sim=xrun
['--simulator', 'xrun'] -> sim=xrun
[]                     -> sim=vcs

Two spellings, one dest, no caller broken — the same dest mechanism Chapter 22.2 Section 6 used for an on/off flag pair.

Never redefine an exit code. Adding a meaning for 3 is safe because no caller expects 3 today. Changing what 1 means is not, and the breakage is silent: every existing caller keeps doing what it did and is now wrong. If a code's meaning must change, it is a new code.

The output format is the hardest to change, which is why --json exists as a flag rather than as the default. A tool can change its human-readable output freely — nothing parses it — and its JSON record is a published interface governed by Chapter 21.1's rules. Adding a key to the record is safe; renaming or removing one is not.

9. Testing the Contract

Because the contract is a module, it can be tested without running any tool:

Azvya Education Pvt. Ltd.VLSI Mentor
flowcli.py
"""The command-line contract every tool in this flow keeps."""
import argparse
import json
import sys

EXIT_OK = 0
EXIT_FAILED = 1
EXIT_INFRA = 2

SUPPORTED_SIMS = ["vcs", "xrun", "vsim"]


def make_parser(prog, description):
    parser = argparse.ArgumentParser(prog=prog, description=description)
    parser.add_argument("--sim", default="vcs", choices=SUPPORTED_SIMS,
                        help="simulator (default: %(default)s)")
    parser.add_argument("--json", action="store_true", help="emit JSON on stdout")
    parser.add_argument("--quiet", action="store_true", help="suppress progress")
    return parser


def emit(record, as_json):
    if as_json:
        print(json.dumps(record, sort_keys=True))
    else:
        print(f"{record['status']:<6} {record.get('name', '')}")


def note(text, quiet):
    if not quiet:
        print(text, file=sys.stderr)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import flowcli

CASES = []


def check(label, got, want):
    CASES.append((label, got == want, got))


parser = flowcli.make_parser("t.py", "t")

check("codes are 0 1 2",
      (flowcli.EXIT_OK, flowcli.EXIT_FAILED, flowcli.EXIT_INFRA), (0, 1, 2))
check("codes are ordered by severity",
      flowcli.EXIT_OK < flowcli.EXIT_FAILED < flowcli.EXIT_INFRA, True)
check("--sim defaults to vcs", parser.parse_args([]).sim, "vcs")
check("--json defaults off", parser.parse_args([]).json, False)
check("--quiet defaults off", parser.parse_args([]).quiet, False)
check("every common flag present",
      sorted(vars(parser.parse_args([]))), ["json", "quiet", "sim"])
rejected = False
try:
    parser.parse_args(["--sim", "icarus"])
except SystemExit as exc:
    rejected = exc.code == 2
check("--sim rejects unknown with rc 2", rejected, True)

for label, ok, got in CASES:
    print(f"{'ok  ' if ok else 'FAIL'} {label:<34} {got!r}")
print("failures:", sum(1 for _, ok, _ in CASES if not ok))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
usage: t.py [-h] [--sim {vcs,xrun,vsim}] [--json] [--quiet]
t.py: error: argument --sim: invalid choice: 'icarus' (choose from vcs, xrun, vsim)
ok   codes are 0 1 2                    (0, 1, 2)
ok   codes are ordered by severity      True
ok   --sim defaults to vcs              'vcs'
ok   --json defaults off                False
ok   --quiet defaults off               False
ok   every common flag present          ['json', 'quiet', 'sim']
ok   --sim rejects unknown with rc 2    True
failures: 0

Seven assertions, no subprocess, no files. parse_args([...]) takes a list, so the parser's declarations are testable directly — which is the thing Chapter 22.1's hand-written parser could also have offered and a sys.argv-reading script cannot.

The second case is the one worth keeping. EXIT_OK < EXIT_FAILED < EXIT_INFRA is what makes Section 5's max() reduction correct, and it is exactly the kind of property somebody breaks while "tidying up" the constants. Chapter 19.18 Section 6's rule applies: the test that matters is the one that fails when somebody changes something they thought was cosmetic.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import argparse

EXIT_OK, EXIT_FAILED, EXIT_INFRA = 0, 1, 2

parser = argparse.ArgumentParser(prog="t.py")
parser.add_argument("--simulator", "--sim", dest="sim", default="vcs")
parser.add_argument("--json", action="store_true")

# A
print("A:", parser.parse_args([]).sim)

# B
print("B:", parser.parse_args(["--sim", "xrun"]).sim)

# C
print("C:", parser.parse_args(["--simulator", "xrun"]).sim)

# D
print("D:", sorted(vars(parser.parse_args([]))))

# E
print("E:", max([EXIT_OK, EXIT_FAILED]), max([EXIT_FAILED, EXIT_INFRA]), max([EXIT_OK]))

# F
print("F:", EXIT_OK < EXIT_FAILED < EXIT_INFRA)

# G
print("G:", max([]) if False else "max of nothing raises")

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: vcs
B: xrun
C: xrun
D: ['json', 'sim']
E: 1 2 0
F: True
G: max of nothing raises

A — the default, with neither spelling given.

B and C — both spellings reach the same dest, which is how a flag is renamed without breaking callers.

D — the attribute is sim, from dest, not from either flag name.

E — the max reduction: any failure beats a pass, and an infrastructure error beats a failure.

F — the ordering that makes E correct, and the assertion worth keeping in the contract test.

G — max([]) raises, which is why Chapter 22.4 Section 7's sweep returns 2 for an empty run rather than reducing nothing.

11. Exercises

Exercise 1 — find the dialects. List three tools in your own flow and, for each, its spelling of a shared concept, what exit 1 means, and what it prints on stdout.

Exercise 2 — the dangerous row. Identify two tools in your flow whose exit 1 means different things. Say what a wrapper treating non-zero as "test failed" would report.

Exercise 3 — three agreements. Write the one-sentence contract your flow would adopt for flag names, exit codes and output format.

Exercise 4 — common and specific. For two of your tools, separate the flags every tool should have from the ones only that tool needs.

Exercise 5 — the module. Write flowcli.py for your flow with named exit codes, a parser factory, an emitter and a note function.

Exercise 6 — named codes. Replace every bare sys.exit(2) in a tool with a named constant. Say what a reader learns from the name.

Exercise 7 — build on it. Rewrite one tool to use your factory. Count the lines that are about the contract rather than about the job.

Exercise 8 — quiet keeps the result. Confirm --quiet silences stderr and leaves stdout unchanged. Say what would break if it silenced both.

Exercise 9 — compose. Write a suite runner that calls your tool, parses its JSON, propagates an infrastructure error and reduces the rest with max.

Exercise 10 — three behaviours from one number. In your runner, point at the three distinct things the child's exit status caused.

Exercise 11 — the adapter. Write the translation layer your flow needs without a contract. Count its branches and say what happens when a tool is added.

Exercise 12 — rename safely. Add an alias for an existing flag with a shared dest. Confirm both spellings work and no caller breaks.

Exercise 13 — never redefine. Explain in two sentences why adding exit code 3 is safe and changing what 1 means is not.

Exercise 14 — test the agreement. Write seven assertions against your contract module with no subprocess. Include the severity-ordering one, then reorder the constants and confirm it fails.

12. Summary

  • Individually well-behaved tools do not compose. Three reasonable scripts can use three spellings for the simulator and three meanings for exit 1.
  • The flag names are annoying; the exit codes are dangerous, because a wrapper reading non-zero as "a test failed" misreports an infrastructure error with nothing to contradict it.
  • Three agreements: flag names, exit-code meanings, and output format.
  • A contract covers the flags every tool has, not the arguments specific to each job.
  • Write the agreement as a module, not a wiki page — a parser factory, named exit codes, an emitter and a note function, in about thirty lines.
  • Named constants read as decisions; sys.exit(2) reads as a number somebody chose.
  • --quiet silences the commentary and never the result, or composition breaks.
  • Composition uses all three agreements at once: pass --sim through by name, parse stdout with no filtering, and let the exit status drive three distinct behaviours.
  • An infrastructure error aborts a suite; a failure is reduced with max — which works because the codes are ordered by severity.
  • Five promises make a tool usable without reading it, which is the module's real deliverable.
  • Without a contract every pair of tools needs an adapter, and the adapter is invisible to every validator, must change when any tool does, and grows with every tool added.
  • The adapter is written by whoever wrote neither tool, which is why it is wrong so often.
  • Add before you remove. A flag alias with a shared dest renames without breaking callers.
  • Never redefine an exit code — the breakage is silent, because every existing caller keeps doing what it did.
  • Adding a key to the JSON record is safe; renaming or removing one is not, which is Chapter 21.1's contract applied to a tool's output.
  • Test the agreement without a subprocess. parse_args([...]) takes a list, and the severity-ordering assertion is the one that catches a tidy-up.

That completes Module 22, and with it the interface half of a flow tool. A script now takes arguments somebody else can predict, refuses bad ones with a message they can act on, reports its result to a machine with a number and to a person with a line, and keeps the same promises as every other tool beside it. Module 23 turns the direction around: instead of being called, the script starts calling the simulator itself.

Where this fits

Part of the Python curriculum.