§5 said the browser crate's SIMD validation was done by hand, "which CI does not check". Since "wai-web-wasm: the packages validate with relaxed SIMD and threads off", wai-web-wasm.yml validates both bundles and reach/record/wai_web_bg.wasm with relaxed SIMD and threads disabled. §5 now says so, and says what CI still does not check: that they hold no other SIMD instruction. The changelog gains the CI entry for this branch. The commands that reproduce CI's wasmtime legs ran a stock wasmtime: the WAI Quantum READMEs (English and Chinese), the bit_identity test's header, the WAI README's edge example, and the conformance READMEs (quantum, qasm3, formats, answers, intents). Each now passes -W nan-canonicalization=y -W relaxed-simd-deterministic=y, as CI does. The determinism-cost run scripts under wai/research keep their commands: they record runs already made. The quantum docs check could not see that drift. For a `norun=ci:` block it stripped every leading VAR=value before matching the command against the workflow, and its pattern, \S*, did not read a quoted value. It now reads a quoted or bare value, and when the workflow sets VAR (as an env: key or in a script) the documented value must be one the workflow gives it, so CARGO_TARGET_WASM32_WASIP2_RUNNER=wasmtime is refused against a workflow that sets it to the flagged runner. Co-Authored-By: Claude Opus 5.5 <[email protected]>
862 lines
39 KiB
Python
862 lines
39 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; a leading `VAR=value` must be a value the
|
|
workflow gives VAR, where it sets VAR), `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 workflow_values(text, var):
|
|
"""Every value a workflow gives VAR: as an `env:` key (`VAR: value`) or in a script
|
|
(`VAR=value`, `VAR="value"`, `VAR='value'`), quotes off and blanks collapsed."""
|
|
vals = set()
|
|
for m in re.finditer(rf"^\s*{var}:[ \t]*(\S.*?)\s*$", text, re.M):
|
|
vals.add(" ".join(m.group(1).strip("'\"").split()))
|
|
for m in re.finditer(rf"\b{var}=(\"[^\"]*\"|'[^']*'|\S+)", text):
|
|
vals.add(" ".join(m.group(1).strip("'\"").split()))
|
|
return vals
|
|
|
|
|
|
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
|
|
raw = wf.read_text(encoding="utf-8")
|
|
flow = " ".join(raw.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()
|
|
# A leading VAR=value (quoted or not) is not part of the command. When the
|
|
# workflow sets VAR, the value must be one it gives VAR, so a documented
|
|
# setting, such as the wasm runner and its flags, cannot drift from CI's.
|
|
while m := re.match(r"([A-Z_][A-Z0-9_]*)=('[^']*'|\"[^\"]*\"|\S*)\s+", seg):
|
|
var, val = m.group(1), " ".join(m.group(2).strip("'\"").split())
|
|
sets = workflow_values(raw, var)
|
|
if sets and val not in sets:
|
|
report.fail(block.where, f"{var}={val}: the workflow {detail} sets it to " + " or ".join(f"`{s}`" for s in sorted(sets)))
|
|
seg = seg[m.end():]
|
|
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())
|