open-standards/wai/tools/quantum-docs/check_docs.py
Transaction Science c4c1294e00 wai-quantum: documentation that runs in CI, its drift fixed, an index of every page, and a changelog entry
wai/tools/quantum-docs/check_docs.py runs every block marked runnable on the
pages docs.json names (the engine, the command line, the service, Python,
the Studio, the desktop app, the editors, the providers, the compiler,
estimates, verdicts, the feature matrix, the course and the agent skill) in
a scratch directory of fixtures, holds each shown output to the real one,
verifies every receipt a block writes, builds and runs the Rust blocks, and
checks every block that cannot run for the commands and flags it names,
every wai-quantum command in the prose, every relative link and every
dependency shown; a translation must hold its English page's code. ci.sh
builds the command line, the service and the wheel and starts a service, as
wai-quantum-docs.yml runs it, beside the course's and the locales' tests.

Drift fixed: the crate pinned "0.4", named an unrelated crate for the command
line, ran its research tools outside lab, claimed the whole suite under
wasip2, and split its feature table from its header; the command line's
README said the reader refuses while, inv and pow, used placeholders and
optional flags that do not run, and merged versions that do not exist; the
estimate page's output lacked its receipts line; build steps now match CI.
An observable may lead with its coefficient ("0.5 X2"), as documented.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 08:43:06 -04:00

841 lines
37 KiB
Python

#!/usr/bin/env python3
"""The WAI Quantum documentation, run: every command and every snippet in it,
checked against what the code does.
python3 wai/tools/quantum-docs/check_docs.py --cli PATH/TO/wai-quantum \\
[--service URL] [--python PATH/TO/python] [--rust] [--only SUBSTRING] [--keep]
The documents are the ones `docs.json` beside this file names. In each, every
fenced block is one of these, by its info string (the words after the fence):
- ```` ```sh runnable ```` (or bash, shell): run in order, one document's
blocks in one scratch directory holding `fixtures/`, as a reader would run
them, with `wai-quantum` the command line given (`in=DIR` runs it in that
directory of the scratch). Each command must exit 0,
except a line whose comment begins `# exits N` (or `# exit N`), which must
exit N; `exit=N` holds the whole block to N. `copy=DIR` copies a directory
beside the document into the scratch first and `timeout=S` gives it longer.
`needs=service` needs `--service`; the documented service address is run
against that one. `cargo=DIR` runs its `cargo run` against the crate in that
directory beside the document.
- ```` ```console runnable ````: each `$ command` run, and its output held to
the lines after it.
- ```` ```text output ```` right after a runnable block: that block's output,
held to it line by line (a run's `time` line, which is wall-clock, aside).
- ```` ```python runnable ````: run in order in one interpreter (`--python`,
with the package installed), one namespace for the document.
- ```` ```rust runnable ```` or ```` ```rust compile ````: built (and run) as
an example against the crate (`crate=NAME`, default wai-quantum,
`features=a,b`), with `--rust`.
- ```` ```qasm ````: read by the engine (`wai-quantum lint`); `refused` holds
it to a refusal, and `file=NAME` writes it into the scratch for the blocks
after it.
- ```` ```toml ````: each dependency on a crate of this repository names a
version its version satisfies.
- ```` ```json ````: parses (`fragment` excepts one).
- ```` ```<lang> norun=REASON ````: a block that cannot run here, for a stated
reason: `ci:WORKFLOW` (the steps a CI workflow runs: each script it names
exists, and the workflow names it), `install` (each package it installs is
this repository's, at its version), `data` (it needs files the repository
does not hold), `account` (it needs what only the person has: an account,
a device, a program of their own), `server`
(it starts a server that does not return), `usage` (a synopsis). Every
`wai-quantum` command in it must still name a command, and flags, that
exist.
A shell, Python or Rust block with none of these, or a command in a text
block, is an error: documentation either runs here or says why it cannot.
A translation (`NAME.zh-CN.md`) is marked machine-assisted on its first line
and holds its English page's code (comments aside); one whose code is the
English page's exactly is not run again.
Every JSON file a runnable block writes, and every output that is one, that
carries a receipt is checked with `wai-quantum verify`. In prose, every
`wai-quantum …` in backquotes must name a command that exists, and every
relative link must reach a file.
Exit status: 0 when every check passes, 1 otherwise, 2 on bad arguments.
"""
import argparse
import fnmatch
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[2]
SERVICE_URL = "https://wai.transaction.science/quantum-jobs"
SHELLS = {"sh", "bash", "shell", "zsh"}
REASONS = {"ci", "install", "data", "account", "server", "usage"}
TEXTS = {"", "text", "txt", "plain"}
FENCE = re.compile(r"^(\s*)(```+|~~~+)(.*)$")
class Block:
def __init__(self, doc, line, info, body):
self.doc, self.line, self.info, self.body = doc, line, info.strip(), body
words = self.info.split()
self.lang = words[0].lower() if words else ""
self.flags = set()
self.attrs = {}
for w in words[1:]:
if "=" in w:
k, v = w.split("=", 1)
self.attrs[k] = v
else:
self.flags.add(w)
@property
def where(self):
return f"{self.doc.rel}:{self.line}"
def text(self):
return "\n".join(self.body) + ("\n" if self.body else "")
class Doc:
def __init__(self, path):
self.path = path
self.rel = path.relative_to(ROOT).as_posix()
self.text = path.read_text(encoding="utf-8")
self.blocks = []
self.prose = [] # (line, text) outside fences
lines = self.text.split("\n")
i = 0
while i < len(lines):
m = FENCE.match(lines[i])
if m:
indent, fence, info = m.group(1), m.group(2), m.group(3)
body = []
j = i + 1
while j < len(lines) and not re.match(r"^\s*" + re.escape(fence[0]) * len(fence) + r"\s*$", lines[j]):
body.append(lines[j][len(indent):] if lines[j].startswith(indent) else lines[j].lstrip())
j += 1
self.blocks.append(Block(self, i + 1, info, body))
i = j + 1
continue
self.prose.append((i + 1, lines[i]))
i += 1
class Report:
def __init__(self):
self.fails = []
self.counts = {}
def ok(self, kind):
self.counts[kind] = self.counts.get(kind, 0) + 1
def fail(self, where, msg):
self.fails.append((where, msg))
print(f"FAIL {where}: {msg}", flush=True)
# ---- the repository's packages ---------------------------------------------
def packages():
"""Every Cargo package under wai/, by name: its version and whether it has a binary."""
out = {}
for toml in (ROOT / "wai").rglob("Cargo.toml"):
if any(p in ("target", "node_modules") for p in toml.parts):
continue
t = toml.read_text(encoding="utf-8", errors="replace")
pkg = re.search(r"^\[package\](.*?)(?=^\[|\Z)", t, re.S | re.M)
if not pkg:
continue
name = re.search(r'^name\s*=\s*"([^"]+)"', pkg.group(1), re.M)
ver = re.search(r'^version\s*=\s*"([^"]+)"', pkg.group(1), re.M)
if not name:
continue
bins = re.findall(r'^\[\[bin\]\][^\[]*?name\s*=\s*"([^"]+)"', t, re.S | re.M)
if (toml.parent / "src" / "main.rs").exists():
bins.append(name.group(1))
bins += [b.stem for b in (toml.parent / "src" / "bin").glob("*.rs")]
out[name.group(1)] = {"version": ver.group(1) if ver else None, "bins": bins, "dir": toml.parent}
return out
def python_packages():
out = {}
for py in (ROOT / "wai").rglob("pyproject.toml"):
if "node_modules" in py.parts:
continue
t = py.read_text(encoding="utf-8")
name = re.search(r'^name\s*=\s*"([^"]+)"', t, re.M)
if name:
cargo = py.parent / "Cargo.toml"
ver = re.search(r'^version\s*=\s*"([^"]+)"', cargo.read_text(encoding="utf-8"), re.M) if cargo.exists() else None
out[name.group(1).replace("_", "-")] = ver.group(1) if ver else None
return out
def caret_ok(req, have):
"""Whether the version `have` satisfies the Cargo requirement `req` (caret by default)."""
req = req.strip()
if not have:
return False
h = [int(x) for x in re.findall(r"\d+", have.split("-")[0])[:3]]
if req.startswith("="):
return h == [int(x) for x in re.findall(r"\d+", req)][:3]
parts = [int(x) for x in re.findall(r"\d+", req.lstrip("^"))[:3]]
if not parts:
return False
if h < parts + [0] * (3 - len(parts)):
return False
for i, x in enumerate(parts):
if x != 0 or i == len(parts) - 1:
return h[: i + 1] == parts[: i + 1]
return True
# ---- the command line's shape ----------------------------------------------
class Cli:
def __init__(self, path):
self.path = path
self.help = {}
self.lab = None
def run(self, args, env=None, timeout=60):
return subprocess.run([self.path, *args], capture_output=True, text=True, timeout=timeout, env=env, stdin=subprocess.DEVNULL)
def help_of(self, path):
if list(path) == ["lab"]:
return "lab"
key = tuple(path)
if key not in self.help:
p = self.run([*path, "--help"], env={**os.environ, "LANG": "C.UTF-8", "WAI_QUANTUM_LANG": "en"})
self.help[key] = p.stdout if p.returncode == 0 else None
return self.help[key]
def lab_tools(self):
if self.lab is None:
text = self.run(["lab", "help"]).stdout + self.run(["lab", "help"]).stderr
self.lab = {}
for line in text.split("\n"):
m = re.search(r"wai-quantum lab ([a-z][a-z0-9-]*)(.*)$", line)
if m:
self.lab.setdefault(m.group(1), set()).update(re.findall(r"--[a-z][a-z0-9-]*", m.group(2)))
# A tool's usage continues on the indented lines after it.
cur = None
for line in text.split("\n"):
m = re.search(r"wai-quantum lab ([a-z][a-z0-9-]*)", line)
if m:
cur = m.group(1)
elif cur and line.startswith(" "):
self.lab[cur].update(re.findall(r"--[a-z][a-z0-9-]*", line))
elif not line.strip():
cur = None
return self.lab
def check_command(self, tokens):
"""Why `wai-quantum TOKENS…` names no command or flag the command line has, or None."""
words = []
rest = list(tokens)
while rest and re.match(r"^[a-z][a-z0-9-]*$", rest[0]) and self.help_of(words + [rest[0]]) is not None:
words.append(rest.pop(0))
if words == ["lab"]:
break
if not words:
return f"`wai-quantum {' '.join(tokens[:2])}`: no such command" if tokens and not tokens[0].startswith("-") else None
if words == ["lab"]:
tools = self.lab_tools()
if not rest or rest[0] in ("help", "--help", "-h"):
return None
tool = rest[0]
if tool not in tools:
return f"`wai-quantum lab {tool}`: no such research tool (`wai-quantum lab help` lists them)"
flags = tools[tool]
for f in rest[1:]:
f = f.split("=", 1)[0]
if f.startswith("--") and f not in flags and f != "--help":
return f"`wai-quantum lab {tool}`: no flag {f} in its usage"
return None
h = self.help_of(words) or ""
root = self.help_of([]) or ""
known = set(re.findall(r"--[a-z][a-z0-9-]*", h)) | set(re.findall(r"--[a-z][a-z0-9-]*", root))
for f in rest:
if f == "--":
break
f = f.split("=", 1)[0]
if re.match(r"^--[a-z][a-z0-9-]*$", f) and f not in known:
return f"`wai-quantum {' '.join(words)}`: no flag {f}"
# A word after the path where the command takes subcommands must be one of them.
subs = subcommands(h)
if subs and rest and re.match(r"^[a-z][a-z0-9-]*$", rest[0]) and rest[0] not in subs:
return f"`wai-quantum {' '.join(words)} {rest[0]}`: no such command ({', '.join(sorted(subs))})"
return None
def subcommands(help_text):
"""The subcommands a command's help lists under Commands:, if any."""
if "\nCommands:" not in "\n" + help_text:
return set()
sec = ("\n" + help_text).split("\nCommands:", 1)[1]
out = set()
for line in sec.split("\n")[1:]:
if not line.startswith(" "):
break
m = re.match(r"^\s+([a-z][a-z0-9-]*)\b", line)
if m and m.group(1) != "help":
out.add(m.group(1))
return out
def command_tokens(line):
"""The tokens of a shell line's commands, each a list (pipes and && split)."""
line = line.strip()
if line.startswith("$ "):
line = line[2:]
line = re.sub(r"\s#.*$", "", line) if not line.startswith("#") else ""
if not line:
return []
try:
toks = shlex.split(line, comments=False)
except ValueError:
toks = line.split()
cmds, cur = [], []
for t in toks:
if t in ("|", "&&", "||", ";", "&"):
cmds.append(cur)
cur = []
else:
cur.append(t)
cmds.append(cur)
out = []
for c in cmds:
while c and re.match(r"^[A-Z_][A-Z0-9_]*=", c[0]):
c = c[1:]
out.append(c)
return [c for c in out if c]
# ---- the checks --------------------------------------------------------------
def static_line_checks(cli, block, line, report, pkgs, pys):
for cmd in command_tokens(line):
head = cmd[0].lstrip("(")
if head == "wai-quantum":
why = cli.check_command([t for t in cmd[1:] if not t.startswith(">") and not t.startswith("<")])
if why:
report.fail(block.where, why)
elif head == "cargo" and len(cmd) > 2 and cmd[1] == "install":
name = next((t for t in cmd[2:] if not t.startswith("-")), None)
if name and (name not in pkgs or not pkgs[name]["bins"]):
report.fail(block.where, f"`cargo install {name}`: no package of this repository with a binary is named {name}")
elif head in ("pip", "pip3", "uv") and "install" in cmd:
for t in cmd[cmd.index("install") + 1:]:
if t.startswith("-") or t.startswith("dist/"):
continue
m = re.match(r"^([A-Za-z0-9_.-]+?)-(\d[\w.]*)-.*\.whl", t.split("[")[0])
if m:
name, ver = m.group(1).replace("_", "-"), m.group(2)
if name not in pys:
report.fail(block.where, f"pip install {t}: no Python package of this repository is named {name}")
elif pys[name] != ver:
report.fail(block.where, f"pip install {t}: the package is at {pys[name]}, not {ver}")
else:
name = t.split("[")[0].replace("_", "-")
if name and name not in pys and not name.endswith(".whl"):
report.fail(block.where, f"pip install {t}: no Python package of this repository is named {name}")
def check_norun(cli, block, report, pkgs, pys):
reason = block.attrs.get("norun", "")
kind, _, detail = reason.partition(":")
if kind not in REASONS:
report.fail(block.where, f"norun={reason}: the reasons are {', '.join(sorted(REASONS))}")
return
for line in block.body:
static_line_checks(cli, block, line, report, pkgs, pys)
if kind == "usage":
bins = {b for p in pkgs.values() for b in p["bins"]}
for line in block.body:
words = line.split()
if words and not words[0].startswith("#") and words[0] not in bins:
report.fail(block.where, f"{words[0]}: no package of this repository builds a binary of that name")
if kind == "ci":
wf = ROOT / ".github" / "workflows" / detail
if not detail or not wf.exists():
report.fail(block.where, f"norun=ci:{detail}: no such workflow")
return
flow = " ".join(wf.read_text(encoding="utf-8").split())
for line in block.body:
line = re.sub(r"\s+#.*$", "", line).strip()
if not line or line.startswith("#"):
continue
for seg in re.split(r"&&|;", line):
seg = seg.strip().strip("()").strip().rstrip("&").strip()
seg = re.sub(r"^([A-Z_][A-Z0-9_]*=\S*\s+)+", "", seg)
if not seg or seg.startswith("cd "):
continue
# A placeholder (PATH/TO/PKG, FILE, $VAR, <x>) stands for any one word.
pat = r"\s+".join(r"\S+" if re.fullmatch(r"[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]*)*|\$\S+|<[^>]+>", t) else re.escape(t) for t in seg.split())
if not re.search(pat, flow):
report.fail(block.where, f"`{seg}`: the workflow {detail} does not run this")
report.ok("norun")
def check_toml(block, report, pkgs):
for line in block.body:
m = re.match(r'^\s*([A-Za-z0-9_-]+)\s*=\s*(?:"([^"]+)"|\{[^}]*version\s*=\s*"([^"]+)")', line)
if not m:
continue
name, req = m.group(1), m.group(2) or m.group(3)
if name in pkgs:
have = pkgs[name]["version"]
if not caret_ok(req, have):
report.fail(block.where, f'{name} = "{req}": the crate is at {have}, which that requirement does not take')
else:
report.ok("toml")
def check_prose(cli, doc, report):
for n, line in doc.prose:
for span in re.findall(r"`(wai-quantum [^`]+)`", line):
toks = command_tokens(span)
if not toks:
continue
words = [t for t in toks[0][1:] if not re.search(r"[A-Z]{2,}|…|\.\.\.|\[|<", t)]
why = cli.check_command(words)
if why:
report.fail(f"{doc.rel}:{n}", why)
else:
report.ok("prose command")
for target in re.findall(r"\]\(([^)\s]+)\)", re.sub(r"`[^`]*`", "", line)):
if re.match(r"^[a-z]+:", target) or target.startswith("#"):
if re.match(r"^https://(crates\.io/crates|docs\.rs)/([A-Za-z0-9_-]+)", target):
name = re.match(r"^https://(?:crates\.io/crates|docs\.rs)/([A-Za-z0-9_-]+)", target).group(1)
if name not in PKGS:
report.fail(f"{doc.rel}:{n}", f"{target}: no crate of this repository is named {name}")
continue
path = target.split("#")[0]
if path and not (doc.path.parent / path).exists():
report.fail(f"{doc.rel}:{n}", f"the link {target} reaches no file")
else:
report.ok("link")
def normalize(text, scratch):
out = []
for line in text.replace(str(scratch), ".").split("\n"):
line = line.rstrip()
# A key's did:key is the fixture's here and the reader's there.
line = re.sub(r"did:key:z6Mk[1-9A-HJ-NP-Za-km-z]+", "did:key:z6Mk…", line)
if re.match(r"^time\s+\S", line):
line = re.sub(r"^time\s+.*$", "time <wall clock>", line)
out.append(line)
while out and not out[-1]:
out.pop()
while out and not out[0]:
out.pop(0)
return "\n".join(out)
def transform(lines):
"""A block's lines as a script: each `# exits N` line held to its status."""
out = ["set -eo pipefail"]
heredoc = None
cont = False
for line in lines:
if heredoc:
out.append(line)
if line.strip() == heredoc:
heredoc = None
continue
m = re.search(r"<<-?\s*'?\"?([A-Za-z_]+)'?\"?", line)
if m and not line.lstrip().startswith("#"):
heredoc = m.group(1)
e = re.match(r"^(?P<cmd>[^#]*?\S)\s+#\s*exits?\s+(?P<n>\d+)\b", line)
if e and not cont and not heredoc:
cmd, n = e.group("cmd"), e.group("n")
q = shlex.quote(cmd)
out.append(f'__s=0; {cmd} || __s=$?; [ "$__s" -eq {n} ] || {{ echo "docs: expected exit {n}, got $__s: "{q} >&2; exit 97; }}')
else:
out.append(line)
cont = line.rstrip().endswith("\\")
return "\n".join(out) + "\n"
class Runner:
def __init__(self, args, report):
self.args = args
self.report = report
self.cli = Cli(args.cli)
self.tmp = Path(tempfile.mkdtemp(prefix="wai-quantum-docs-"))
self.bin = self.tmp / "bin"
self.bin.mkdir()
(self.bin / "wai-quantum").symlink_to(Path(args.cli).resolve())
def env(self, scratch):
e = dict(os.environ)
e.update({
"PATH": f"{self.bin}{os.pathsep}{e.get('PATH', '')}",
"HOME": str(scratch / ".home"),
"XDG_CONFIG_HOME": str(scratch / ".config"),
"WAI_QUANTUM_KEY": str(scratch / "quantum-key"),
# Credentials in a file under the scratch's configuration, never the keychain.
"WAI_QUANTUM_CREDENTIALS": "file",
"LANG": "C.UTF-8",
"LC_ALL": "C.UTF-8",
"WAI_QUANTUM_LANG": "",
})
e.pop("WAI_QUANTUM_LEARN", None)
# cargo outside the checkout uses the toolchain the checkout names.
e.setdefault("RUSTUP_TOOLCHAIN", TOOLCHAIN)
if self.args.service:
e["WAI_QUANTUM_SERVICE"] = self.args.service
else:
e.pop("WAI_QUANTUM_SERVICE", None)
return e
def scratch_for(self, doc):
d = self.tmp / re.sub(r"[^A-Za-z0-9]+", "-", doc.rel)
if d.exists():
shutil.rmtree(d)
shutil.copytree(HERE / "fixtures", d)
(d / ".home").mkdir(exist_ok=True)
(d / ".config").mkdir(exist_ok=True)
return d
def verify_receipts(self, block, scratch, since, stdout, env):
"""Every JSON the block wrote, or printed, that carries a receipt, checked."""
files = [p for p in scratch.rglob("*.json") if p.stat().st_mtime >= since and ".config" not in p.parts and ".home" not in p.parts]
n = 0
cands = [(str(p.relative_to(scratch)), p.read_text(encoding="utf-8", errors="replace")) for p in files]
if stdout.strip().startswith("{"):
cands.append(("its output", stdout))
for name, text in cands:
try:
v = json.loads(text)
except ValueError:
continue
if not isinstance(v, dict) or not isinstance(v.get("result"), dict) or "request" not in v:
continue
if "receipt" not in v["result"]:
continue
p = subprocess.run([self.args.cli, "verify", "-"], input=text, capture_output=True, text=True, env=env, cwd=scratch, timeout=600)
if p.returncode != 0:
self.report.fail(block.where, f"the receipt in {name} does not verify: {(p.stdout + p.stderr).strip()[-300:]}")
else:
n += 1
if n:
self.report.ok("receipt")
return n
def run_shell(self, doc, block, scratch, expected):
if "service" in block.attrs.get("needs", "") and not self.args.service:
self.report.fail(block.where, "needs the service: give --service URL")
return
for line in block.body:
static_line_checks(self.cli, block, line, self.report, PKGS, PYS)
if "copy" in block.attrs:
src = (doc.path.parent / block.attrs["copy"]).resolve()
dst = scratch / src.name
if src.is_dir():
shutil.copytree(src, dst, dirs_exist_ok=True)
else:
shutil.copy(src, dst)
lines = block.body
if "cargo" in block.attrs:
# `cargo run` as the document runs it beside the crate, here in the scratch.
manifest = (doc.path.parent / block.attrs["cargo"] / "Cargo.toml").resolve()
lines = [re.sub(r"\bcargo (run|build|test)\b", lambda m: f"cargo {m.group(1)} --manifest-path {shlex.quote(str(manifest))}", l) for l in lines]
if self.args.service:
lines = [l.replace(SERVICE_URL, self.args.service.rstrip("/")) for l in lines]
script = scratch / f".block-{block.line}.sh"
script.write_text(transform(lines), encoding="utf-8")
cwd = scratch / block.attrs["in"] if "in" in block.attrs else scratch
env = self.env(scratch)
since = time.time() - 1
try:
p = subprocess.run(["bash", str(script)], cwd=cwd, env=env, capture_output=True, text=True, timeout=int(block.attrs.get("timeout", "300")), stdin=subprocess.DEVNULL)
except subprocess.TimeoutExpired:
self.report.fail(block.where, f"did not finish within {block.attrs.get('timeout', '300')} s")
return
want = int(block.attrs.get("exit", "0"))
if p.returncode != want:
self.report.fail(block.where, f"exit {p.returncode}, not {want}:\n{(p.stderr or p.stdout).strip()[-1500:]}")
return
self.verify_receipts(block, scratch, since, p.stdout, env)
if expected is not None:
got, want_text = normalize(p.stdout, scratch), normalize(expected.text(), scratch)
if got != want_text:
self.report.fail(expected.where, "the output shown is not the output:\n" + diff(want_text, got))
return
self.report.ok("output")
self.report.ok("shell")
def run_console(self, doc, block, scratch):
items, cur = [], None
for line in block.body:
if line.startswith("$ "):
cur = [line[2:], []]
items.append(cur)
elif cur is not None:
cur[1].append(line)
env = self.env(scratch)
for cmd, out in items:
static_line_checks(self.cli, block, cmd, self.report, PKGS, PYS)
p = subprocess.run(["bash", "-o", "pipefail", "-c", cmd], cwd=scratch, env=env, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)
if p.returncode != 0:
self.report.fail(block.where, f"$ {cmd}: exit {p.returncode}:\n{p.stderr.strip()[-1000:]}")
continue
got, want = normalize(p.stdout, scratch), normalize("\n".join(out), scratch)
if got != want:
self.report.fail(block.where, f"$ {cmd}: the output shown is not the output:\n" + diff(want, got))
else:
self.report.ok("console")
def run_qasm(self, block, scratch):
name = block.attrs.get("file")
if name:
(scratch / name).write_text(block.text(), encoding="utf-8")
p = subprocess.run([self.args.cli, "lint", "-"], input=block.text(), capture_output=True, text=True, env=self.env(scratch), timeout=60)
refused = "refused" in block.flags
if (p.returncode == 0) == refused:
self.report.fail(block.where, f"the program {'reads' if not refused else 'is refused'}, but the block says otherwise: {(p.stdout + p.stderr).strip()}")
else:
self.report.ok("qasm")
def run_python(self, doc, blocks, scratch):
if not self.args.python:
for b in blocks:
self.report.fail(b.where, "a Python block: give --python, an interpreter with the package installed")
return
payload = scratch / ".python-blocks.json"
payload.write_text(json.dumps([[b.where, b.text()] for b in blocks]), encoding="utf-8")
driver = scratch / ".python-driver.py"
driver.write_text(PY_DRIVER, encoding="utf-8")
env = self.env(scratch)
since = time.time() - 1
p = subprocess.run([self.args.python, "-I", str(driver), str(payload)], cwd=scratch, env=env, capture_output=True, text=True, timeout=900)
done = set(re.findall(r"^__BLOCK_OK__ (\d+)$", p.stdout, re.M))
for i, b in enumerate(blocks):
if str(i) in done:
self.report.ok("python")
else:
self.report.fail(b.where, f"the Python block did not run:\n{p.stderr.strip()[-1500:]}")
break
self.verify_receipts(blocks[0], scratch, since, "", env)
def run_rust(self, doc, blocks):
if not self.args.rust:
for b in blocks:
self.report.fail(b.where, "a Rust block: give --rust (cargo builds it)")
return
for b in blocks:
crate = b.attrs.get("crate", "wai-quantum")
if crate not in PKGS:
self.report.fail(b.where, f"crate={crate}: no such crate here")
continue
proj = self.tmp / f"rust-{re.sub(r'[^A-Za-z0-9]+', '-', doc.rel)}-{b.line}"
(proj / "examples").mkdir(parents=True, exist_ok=True)
feats = [f for f in b.attrs.get("features", "").split(",") if f]
dep = f'{crate} = {{ path = "{PKGS[crate]["dir"].as_posix()}", features = {json.dumps(feats)} }}'
(proj / "Cargo.toml").write_text(f'[package]\nname = "docs-snippet"\nversion = "0.0.0"\nedition = "2024"\npublish = false\n\n[dependencies]\n{dep}\n\n[workspace]\n', encoding="utf-8")
lock = PKGS[crate]["dir"] / "Cargo.lock"
if lock.exists():
shutil.copy(lock, proj / "Cargo.lock")
code = b.text()
if "fn main" not in code:
uses = "\n".join(l for l in code.split("\n") if l.startswith("use "))
body = "\n".join(l for l in code.split("\n") if not l.startswith("use "))
code = f"{uses}\n\n#[allow(unused_variables)]\nfn main() -> Result<(), Box<dyn std::error::Error>> {{\n{body}\nOk(())\n}}\n"
(proj / "examples" / "snippet.rs").write_text(code, encoding="utf-8")
(proj / "src").mkdir(exist_ok=True)
(proj / "src" / "lib.rs").write_text("", encoding="utf-8")
cmd = ["cargo", "run" if "runnable" in b.flags else "build", "--release", "--quiet", "--example", "snippet"]
env = dict(os.environ)
env.setdefault("RUSTUP_TOOLCHAIN", TOOLCHAIN)
p = subprocess.run(cmd, cwd=proj, env=env, capture_output=True, text=True, timeout=3600)
if p.returncode != 0:
self.report.fail(b.where, f"the Rust block does not {'run' if 'runnable' in b.flags else 'build'}:\n{p.stderr.strip()[-2000:]}")
else:
self.report.ok("rust")
def check_translation(self, doc):
"""A translation is marked machine-assisted and holds its English page's code."""
if not doc.path.name.endswith(".zh-CN.md"):
return False
stem = doc.path.name[: -len(".zh-CN.md")]
english = next((p for p in (doc.path.with_name(stem + ".md"), doc.path.with_name(stem + ".en.md")) if p.exists()), None)
first = next((l for l in doc.text.split("\n") if l.strip()), "")
if "机器辅助翻译" not in first or "machine-assisted" not in first:
self.report.fail(doc.rel, "a translation's first line marks it machine-assisted, to be reviewed by a fluent reader")
if english is None:
self.report.fail(doc.rel, "no English page beside the translation")
return False
def code(d, lang=True):
out = []
for b in d.blocks:
if b.lang in TEXTS and "output" not in b.flags:
continue
body = [re.sub(r"\s+#.*$|^\s*#.*$", "", l) for l in b.body]
if lang:
body = [l.replace(" --lang zh-CN", "") for l in body]
out.append((b.info, [l.rstrip() for l in body if l.strip()]))
return out
mine, theirs = code(doc), code(Doc(english))
if mine != theirs:
at = next((i for i, (a, b) in enumerate(zip(mine, theirs)) if a != b), min(len(mine), len(theirs)))
self.report.fail(doc.rel, f"its code differs from {english.name}'s, from code block {at + 1} on: a translation holds the same commands and snippets")
return False
self.report.ok("translation")
# The same commands as its English page, which runs them: nothing more to run.
return code(doc, lang=False) == code(Doc(english), lang=False)
def run_doc(self, doc):
scratch = self.scratch_for(doc)
same = self.check_translation(doc)
check_prose(self.cli, doc, self.report)
if same:
if not self.args.keep:
shutil.rmtree(scratch, ignore_errors=True)
return
pending_python = []
rust = []
blocks = doc.blocks
for i, b in enumerate(blocks):
nxt = blocks[i + 1] if i + 1 < len(blocks) else None
if b.lang in TEXTS and "output" in b.flags:
continue # held to the block before it
if "norun" in b.attrs:
check_norun(self.cli, b, self.report, PKGS, PYS)
continue
if b.lang in SHELLS:
if "runnable" not in b.flags:
self.report.fail(b.where, f"a {b.lang} block that neither runs (runnable) nor says why it cannot (norun=REASON)")
continue
expected = nxt if nxt and nxt.lang in TEXTS and "output" in nxt.flags else None
self.run_shell(doc, b, scratch, expected)
elif b.lang == "console":
if "runnable" not in b.flags:
self.report.fail(b.where, "a console block that neither runs (runnable) nor says why it cannot (norun=REASON)")
continue
self.run_console(doc, b, scratch)
elif b.lang in ("python", "py"):
if "runnable" not in b.flags:
self.report.fail(b.where, "a Python block that neither runs (runnable) nor says why it cannot (norun=REASON)")
continue
pending_python.append(b)
elif b.lang == "rust":
if not ({"runnable", "compile"} & b.flags):
self.report.fail(b.where, "a Rust block that is neither runnable nor compile, nor says why it cannot run (norun=REASON)")
continue
rust.append(b)
elif b.lang == "qasm":
self.run_qasm(b, scratch)
elif b.lang == "toml":
check_toml(b, self.report, PKGS)
elif b.lang == "json":
if "fragment" in b.flags:
continue
try:
json.loads(b.text())
self.report.ok("json")
except ValueError as e:
self.report.fail(b.where, f"the JSON does not parse: {e}")
elif b.lang in TEXTS:
for line in b.body:
l = line.strip()
if re.match(r"^(\$ )?(wai-quantum|cargo|curl|node|npm|pip|python3?) ", l):
self.report.fail(b.where, f"a command in a text block: `{l[:60]}`; make it a shell block that runs, or say why it cannot (norun=REASON)")
break
if pending_python:
self.run_python(doc, pending_python, scratch)
if rust:
self.run_rust(doc, rust)
if not self.args.keep:
shutil.rmtree(scratch, ignore_errors=True)
PY_DRIVER = r'''
import json, sys, traceback
blocks = json.load(open(sys.argv[1], encoding="utf-8"))
g = {"__name__": "__main__"}
for i, (where, code) in enumerate(blocks):
try:
exec(compile(code, where, "exec"), g)
except BaseException:
traceback.print_exc()
print(f"__BLOCK_FAIL__ {i}", flush=True)
sys.exit(1)
print(f"__BLOCK_OK__ {i}", flush=True)
'''
def diff(want, got):
import difflib
return "\n".join(list(difflib.unified_diff(want.split("\n"), got.split("\n"), "shown", "run", lineterm=""))[:60])
PKGS = {}
PYS = {}
_tc = re.search(r'channel\s*=\s*"([^"]+)"', (ROOT / "rust-toolchain.toml").read_text(encoding="utf-8")) if (ROOT / "rust-toolchain.toml").exists() else None
TOOLCHAIN = _tc.group(1) if _tc else "stable"
def main():
global PKGS, PYS
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--cli", required=True, help="the wai-quantum binary")
ap.add_argument("--service", help="a job service to run the service's documented commands against")
ap.add_argument("--python", help="a Python with the wai_quantum package installed")
ap.add_argument("--rust", action="store_true", help="build and run the Rust blocks with cargo")
ap.add_argument("--only", action="append", default=[], help="only the documents whose path holds this")
ap.add_argument("--skip", action="append", default=[], help="leave out the documents whose path holds this")
ap.add_argument("--keep", action="store_true", help="keep the scratch directories")
args = ap.parse_args()
if not os.access(args.cli, os.X_OK):
print(f"--cli {args.cli}: not an executable", file=sys.stderr)
return 2
args.cli = str(Path(args.cli).resolve())
PKGS, PYS = packages(), python_packages()
manifest = json.loads((HERE / "docs.json").read_text(encoding="utf-8"))
report = Report()
paths = []
for pat in manifest["docs"]:
if args.only and not any(o in pat or fnmatch.fnmatch(o, pat) for o in args.only):
continue
hits = sorted(p for p in ROOT.glob(pat) if p.is_file())
if not hits:
report.fail("docs.json", f"{pat} names no file")
paths.extend(h for h in hits if h not in paths)
if args.only:
paths = [p for p in paths if any(o in p.relative_to(ROOT).as_posix() for o in args.only)]
paths = [p for p in paths if not any(k in p.relative_to(ROOT).as_posix() for k in args.skip)]
runner = Runner(args, report)
try:
for p in paths:
doc = Doc(p)
before = len(report.fails)
t0 = time.time()
runner.run_doc(doc)
n = len(report.fails) - before
print(f"{'ok ' if n == 0 else 'FAIL'} {doc.rel} ({len(doc.blocks)} blocks, {time.time() - t0:.1f} s){'' if n == 0 else f': {n} failed'}", flush=True)
finally:
if not args.keep:
shutil.rmtree(runner.tmp, ignore_errors=True)
else:
print(f"scratch kept in {runner.tmp}")
counts = ", ".join(f"{v} {k}" for k, v in sorted(report.counts.items()))
print(f"\n{len(paths)} documents; checked: {counts}")
if report.fails:
print(f"{len(report.fails)} check(s) failed")
return 1
print("every check passed")
return 0
if __name__ == "__main__":
sys.exit(main())