open-standards/scripts/wai-conformance-lists.py
Transaction Science 946f3beda9
All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 6s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 7s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 6s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 6s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 6s
WAI operation catalogue / does this change touch it (pull_request) Successful in 6s
WAI conformance / does this change touch it (pull_request) Successful in 6s
WAI energy receipts / does this change touch it (pull_request) Successful in 5s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 6s
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Successful in 7s
wai-ledger service / does this change touch it (pull_request) Successful in 6s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 1m25s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 6s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 6s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 6s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 6s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m57s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m7s
WAI Studio / does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 6s
wai-web wasm32 / does this change touch it (pull_request) Successful in 6s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 29s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 2m1s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 2m59s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m1s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 26s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m19s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m4s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m15s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 5m15s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 6m19s
wai-fleet service / service · macos (pull_request) Successful in 1m2s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m46s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 1m43s
wai-ledger service / service · macos (pull_request) Successful in 1m24s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 1m2s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m18s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m19s
wai-pack / wai-pack · macos (pull_request) Successful in 3m8s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 30s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 7m49s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 3m23s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m39s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m9s
wai-fleet service / service · linux (pull_request) Successful in 2m5s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 3m28s
wai-ledger service / service · linux (pull_request) Successful in 1m43s
wai-pack / wai-pack · linux (pull_request) Successful in 1m55s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m0s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 1m10s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m12s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 11m32s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 4m51s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 13m12s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 15m2s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 4m3s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 16m34s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m40s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 17m22s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m44s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 5m46s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 34s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 17s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 9m28s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m7s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 14m49s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m34s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m16s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 11m4s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 13m6s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m58s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 10m55s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 9m51s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 10m15s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 9m21s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 30m14s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h4m7s
Changelog entries and the conformance workflow's lists move into files a change adds
Every change to WAI added an entry at the head of wai/CHANGELOG.md, and every new module
appended to the hand-kept lists of .github/workflows/wai-conformance.yml. So any two
branches conflicted on those lines and had to merge one after another. This moves both into
files a change adds.

- Changelog entries: a change adds wai/changes/<yyyy-mm-dd>-<slug>.md holding the entry
  text. scripts/changelog-assemble.py renders the entries into CHANGELOG.md's Unreleased
  section for a release, in a stable order. scripts/check-changelog-fragment.py, run by
  repo-checks on every pull request, fails a pull request that changes wai/ and adds or
  edits no entry file directly in wai/changes/ (a release passes, and so does a commit
  with the trailer line "Changelog: none"). A pull request opened before the check reached
  main passes as it is; one opened after it that adds its entry to the head of Unreleased
  fails and is told where the entry goes. wai breaking reads breaking changes from entry
  files too.
- The conformance workflow: each per-module verifier step is a file in
  .github/wai-conformance/verifiers/, each corpus a file in corpora/, and the
  integer-kernel tests carry a tag line. scripts/wai-conformance-lists.py derives every
  list the workflow kept by hand (WAI_FEATURES, the union --bin and --test lists, the
  features-alone lists, the kernel and corpus steps) from those files when the workflow
  runs. Its `selftest --exact` holds the derived lists to the hand lists of the commit
  before this one, and its dry-run prints every command each job runs: the dry-runs before
  and after this change differ only in the gate's path list, which gains the two paths this
  change adds.
- What stays shared, and how to insert there, is written in wai/CONTRIBUTING.md and
  wai/changes/README.md: wai-rs Cargo.toml features and [[test]] entries, lib.rs, registry
  rows, wai/.gitattributes and the live corpus table take an insert beside the related
  entry, never at the end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 01:41:29 -05:00

1021 lines
46 KiB
Python
Executable file

#!/usr/bin/env python3
"""wai-conformance-lists.py: the lists wai-conformance.yml runs, derived from the files that
own them, so a change adds a file instead of editing the workflow's shared lists.
Each list below used to be kept by hand in the workflow, and every new module of WAI appended
to it, so any two branches that added one conflicted. Each is now read at run time from its
source:
.github/wai-conformance/verifiers/*.sh one file a step of the independent verifiers: its
shell body, and a header of `# key: value` lines that
gives its features, binaries, paths and order. From
them: WAI_FEATURES, WAI_PQ_FEATURES and
WAI_RELEASE_FEATURES, the binaries and tests the job
builds first, the steps it runs, the feature lists the
features-alone job checks, and the paths the workflow's
gate runs on.
.github/wai-conformance/corpora/*.txt one file a cargo-class corpus: its tool and directory.
The cargo-classes corpus loop runs them, with the
features their tools require (Cargo.toml) and the
step's own.
wai/wai-rs/tests/*.rs a test file whose line `// wai-conformance: <tag>...`
names a tag is run by the steps that take that tag:
int-kernels (both profiles), int-kernels-release (the
release profile only), int-kernels-amd (the AVX-512
host). Their features are the tests' required-features
and the step's own.
A verifier file's header (the leading `# key: value` lines; the first other line ends it):
step its name in the log (required)
order an integer: steps run in ascending order, a tie by file name (required)
features feature lists, space-separated, each comma-joined: each compiles alone in
the features-alone job, and every feature in them joins the build's list
alone the lists features-alone checks instead of `features`; `-` for none
build union (the default: WAI_FEATURES) or pq (WAI_PQ_FEATURES)
release-features features of the optimised build (WAI_RELEASE_FEATURES)
bins, pq-bins binaries the job builds first and copies to $RUNNER_TEMP/wai-bins/union or
/pq, beside every `wai-bins/union/NAME` and `wai-bins/pq/NAME` the body names
paths globs whose change runs the workflow (each under one of its `paths:` globs)
working-directory where the body runs, from the repository root (default wai)
The tests the job builds first are the `--test NAME` of the body's lines that build with
"$WAI_FEATURES", and with "$WAI_RELEASE_FEATURES" for the optimised build.
Commands (the workflow runs the first group):
env WAI_FEATURES=..., WAI_PQ_FEATURES=..., WAI_RELEASE_FEATURES=...
prebuild the shell script that builds the binaries and tests first
run [--only SLUG]... every verifier step in order, each in a log group; the first that
fails stops the run with its exit status
alone the feature lists features-alone checks, one a line
paths the paths the verifier files bring to the gate, one a line
tests TAG... `--test NAME` for every test file that carries one of the tags
features TAG... [--extra-features A,B]
the tests' required-features with A,B, comma-joined
test-tagged TAG... [--extra-features A,B] -- ARGS...
cargo test ARGS --features <features> <tests>, printed, then run
corpora [--verify] [--extra-features A,B]
the corpora, `name bin dir` a line; with --verify, run each tool's
verify over its corpus (cargo run), each in a log group
dry-run [--ref REF] every list and every step the workflow would run, in a stable form;
of the tree at REF instead with --ref, read from its files or, before
they existed, from the lists its workflow kept by hand. It ends with
the cargo command lines of the steps outside the verifiers' job that
take derived lists (the integer kernels' two steps, the corpus step,
int-kernels-amd, the features-alone loop), read from the workflow's own
step text and evaluated, each in one form (flags and features
sorted, each target once), and per shell family when the unix legs
and the Windows leg differ. A reviewer diffs two dry-runs: the same
lines are the same builds and tests.
baseline --ref REF the lists REF's workflow kept by hand, as JSON
check every file above is well formed and names what exists
selftest [--exact] check, then every list derived here holds every entry of
.github/wai-conformance/baseline.json (the hand lists of the commit
that moved them out), and every step it names still runs; --exact
requires the lists equal, and each step the same body and directory
as the hand list's step of its name, in the same order (a step or a
corpus the tree adds is named as an addition)
Standard library only; runs on the Python 3.9 the runner images carry. --root DIR reads
another tree.
"""
from __future__ import annotations
import importlib.util
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
VERIFIERS = ".github/wai-conformance/verifiers"
CORPORA = ".github/wai-conformance/corpora"
BASELINE = ".github/wai-conformance/baseline.json"
WORKFLOW = ".github/workflows/wai-conformance.yml"
CRATE = "wai/wai-rs"
TAGS = ("int-kernels", "int-kernels-release", "int-kernels-amd")
VERIFIER_KEYS = {
"step": "one", "order": "one", "features": "many", "alone": "many", "build": "one",
"release-features": "many", "bins": "many", "pq-bins": "many", "paths": "many",
"working-directory": "one",
}
CORPUS_KEYS = {"corpus": "one", "order": "one", "bin": "one", "dir": "one"}
HEADER = re.compile(r"^# ([a-z][a-z-]*): ?(.*)$")
MARKER = re.compile(r"^//\s*wai-conformance:\s*(.*?)\s*$", re.M)
class Bad(Exception):
pass
def uniq(xs):
out, seen = [], set()
for x in xs:
if x not in seen:
seen.add(x)
out.append(x)
return out
def gate_module(root):
"""scripts/pr-touches-paths.py, imported: the gate's own reading of paths and headers."""
path = os.path.join(root, "scripts", "pr-touches-paths.py")
if not os.path.isfile(path):
path = os.path.join(HERE, "pr-touches-paths.py")
spec = importlib.util.spec_from_file_location("pr_touches_paths", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# ---------------------------------------------------------------------------- the files
def read_header(path, keys, label):
"""(header, body, errors) of a file whose leading `# key: value` lines are its header;
`label` names it in the errors. The body is what follows the leading comment block
(header and prose)."""
with open(path, encoding="utf-8") as fh:
lines = fh.read().split("\n")
head, errs, i = {}, [], 0
while i < len(lines):
m = HEADER.match(lines[i])
if not m:
break
k, v = m.group(1), m.group(2).strip()
if k not in keys:
errs.append(f"{label}: line {i + 1}: `{k}` is not a key of this file ({', '.join(sorted(keys))})")
elif keys[k] == "one":
if k in head:
errs.append(f"{label}: line {i + 1}: `{k}` is given twice")
head[k] = v
else:
head.setdefault(k, []).extend(v.split())
i += 1
while i < len(lines) and lines[i].startswith("#"):
i += 1
body = "\n".join(lines[i:]).rstrip("\n")
return head, body, errs
def joined(body):
"""The body's lines with `\\` continuations joined."""
return re.sub(r"\\\n\s*", " ", body).split("\n")
def verifiers(root):
d = os.path.join(root, VERIFIERS)
if not os.path.isdir(d):
raise Bad(f"{VERIFIERS}: no such directory")
mods, errs = [], []
for f in sorted(os.listdir(d)):
if not f.endswith(".sh"):
continue
rel = f"{VERIFIERS}/{f}"
h, body, e = read_header(os.path.join(d, f), VERIFIER_KEYS, rel)
errs += e
for k in ("step", "order"):
if k not in h:
errs.append(f"{rel}: no `{k}:` line in its header")
try:
order = int(h.get("order", "0"))
except ValueError:
errs.append(f"{rel}: order `{h.get('order')}` is not an integer")
order = 0
build = h.get("build", "union")
if build not in ("union", "pq"):
errs.append(f"{rel}: build `{build}` is neither union nor pq")
alone = h.get("alone")
if alone == ["-"]:
alone = []
elif alone is not None and "-" in alone:
errs.append(f"{rel}: alone `-` stands alone")
if not body.strip():
errs.append(f"{rel}: no body: the step would run nothing")
mods.append({
"slug": f[:-3], "file": rel, "step": h.get("step", ""), "order": order,
"features": h.get("features", []), "alone": alone, "build": build,
"release": h.get("release-features", []), "bins": h.get("bins", []),
"pq_bins": h.get("pq-bins", []), "paths": h.get("paths", []),
"wd": h.get("working-directory", "wai"), "body": body,
})
mods.sort(key=lambda m: (m["order"], m["slug"]))
seen = {}
for m in mods:
if m["step"] and m["step"] in seen:
errs.append(f"{m['file']}: step `{m['step']}` is also {seen[m['step']]}'s")
seen.setdefault(m["step"], m["file"])
return mods, errs
def corpora(root):
d = os.path.join(root, CORPORA)
if not os.path.isdir(d):
raise Bad(f"{CORPORA}: no such directory")
out, errs = [], []
for f in sorted(os.listdir(d)):
if not f.endswith(".txt"):
continue
rel = f"{CORPORA}/{f}"
h, body, e = read_header(os.path.join(d, f), CORPUS_KEYS, rel)
errs += e
for k in CORPUS_KEYS:
if k not in h:
errs.append(f"{rel}: no `{k}:` line")
if body.strip():
errs.append(f"{rel}: holds lines after its header; a corpus file is its header alone")
try:
order = int(h.get("order", "0"))
except ValueError:
errs.append(f"{rel}: order `{h.get('order')}` is not an integer")
order = 0
out.append({"slug": f[:-4], "file": rel, "corpus": h.get("corpus", ""), "order": order,
"bin": h.get("bin", ""), "dir": h.get("dir", "")})
out.sort(key=lambda c: (c["order"], c["slug"]))
return out, errs
def cargo_targets(root):
"""{(kind, name): required-features or []} of the crate's [[bin]] and [[test]] entries,
and the feature names of its [features] table."""
with open(os.path.join(root, CRATE, "Cargo.toml"), encoding="utf-8") as fh:
text = fh.read()
targets = {}
for block in re.split(r"^\[\[", text, flags=re.M)[1:]:
kind = block.split("]]", 1)[0]
body = re.split(r"^\[", block[len(kind) + 2:], flags=re.M)[0]
name = re.search(r'^name\s*=\s*"([^"]+)"', body, re.M)
if not name:
continue
rf = re.search(r"^required-features\s*=\s*\[(.*?)\]", body, re.M | re.S)
targets[(kind, name.group(1))] = sorted(re.findall(r'"([^"]+)"', rf.group(1))) if rf else []
sec = re.search(r"^\[features\]\n(.*?)(?=^\[)", text, re.M | re.S)
feats = set(re.findall(r"^([A-Za-z0-9_-]+)\s*=", sec.group(1), re.M)) if sec else set()
return targets, feats
def markers(root):
"""{test name: [tags]} of the crate's test files that carry a `wai-conformance:` line."""
tdir = os.path.join(root, CRATE, "tests")
out = {}
for f in sorted(os.listdir(tdir)):
if not f.endswith(".rs"):
continue
with open(os.path.join(tdir, f), encoding="utf-8") as fh:
found = MARKER.findall(fh.read())
if found:
out[f[:-3]] = uniq(t for line in found for t in line.split())
return out
# ------------------------------------------------------------------------- derivations
def derive(mods):
union = [m for m in mods if m["build"] == "union"]
pq = [m for m in mods if m["build"] == "pq"]
feats = lambda ms: uniq(f for m in ms for lst in m["features"] for f in lst.split(","))
tests_with = lambda var: uniq(
t for m in mods for line in joined(m["body"]) if f'"${var}"' in line for t in re.findall(r"--test[ =]([A-Za-z0-9_-]+)", line))
bins_of = lambda key, build: uniq(
b for m in mods for b in m[key] + re.findall(rf"wai-bins/{build}/([A-Za-z0-9_-]+)", m["body"]))
return {
"WAI_FEATURES": feats(union),
"WAI_PQ_FEATURES": feats(pq),
"WAI_RELEASE_FEATURES": uniq(f for m in mods for f in m["release"]),
"union_bins": bins_of("bins", "union"),
"pq_bins": bins_of("pq_bins", "pq"),
"union_tests": tests_with("WAI_FEATURES"),
"release_tests": tests_with("WAI_RELEASE_FEATURES"),
"alone": uniq(lst for m in mods for lst in (m["features"] if m["alone"] is None else m["alone"])),
"paths": uniq(p for m in mods for p in m["paths"]),
}
def tagged(root, tags):
return sorted(t for t, ts in markers(root).items() if set(ts) & set(tags))
def tagged_features(root, tags, extra):
targets, _ = cargo_targets(root)
out = set(extra)
for t in tagged(root, tags):
out |= set(targets.get(("test", t)) or [])
return sorted(out)
def corpora_features(root, cs, extra):
targets, _ = cargo_targets(root)
out = set(extra)
for c in cs:
out |= set(targets.get(("bin", c["bin"])) or [])
return sorted(out)
def prebuild(d):
"""The build step's script: the binaries the steps run, copied per build, then the test
builds every step finds fresh. The order matters (see the step's comment)."""
s = ["set -euo pipefail", 'mkdir -p "$RUNNER_TEMP/wai-bins/union" "$RUNNER_TEMP/wai-bins/pq"']
def build(var, bins, dest):
if not bins:
return
s.append(f'cargo build --no-default-features --features "${var}" \\')
s.append(" " + " ".join(f"--bin {b}" for b in bins))
s.append(f"for b in {' '.join(bins)}; do")
s.append(f' cp "${{CARGO_TARGET_DIR:-target}}/debug/$b" "$RUNNER_TEMP/wai-bins/{dest}/"')
s.append("done")
build("WAI_FEATURES", d["union_bins"], "union")
if d["pq_bins"]:
s.append("# Copied before this build replaces target/debug's copies of the ones it shares.")
build("WAI_PQ_FEATURES", d["pq_bins"], "pq")
s.append('cargo test --no-run --no-default-features --features "$WAI_FEATURES" --lib \\')
s.append(" " + " ".join(f"--test {t}" for t in d["union_tests"]))
if d["release_tests"]:
s.append('cargo test --release --no-run --no-default-features --features "$WAI_RELEASE_FEATURES" \\')
s.append(" " + " ".join(f"--test {t}" for t in d["release_tests"]))
return "\n".join(s) + "\n"
def workflow_extras(text):
"""The features the workflow's steps add to their derived lists: {command: [features]},
the command being what precedes `--extra-features` on the step's line."""
out = {}
for m in re.finditer(r"wai-conformance-lists\.py ((?:[a-z-]+ )+)--extra-features ([A-Za-z0-9_,-]+)", text):
out.setdefault(m.group(1).strip(), m.group(2).split(","))
return out
def wide_globs(text):
"""The globs the workflow's gate replaces with the verifier files' paths (--wide)."""
return re.findall(r"--wide '([^']+)'", text)
def gate_paths(root):
"""What the gate runs on, as the gate reads it (scripts/pr-touches-paths.py with the
workflow's --paths-from and --wide): the workflow's paths but the wide ones, and the
verifier files' paths."""
gate = gate_module(root)
from pathlib import Path
wf = os.path.join(root, WORKFLOW)
with open(wf, encoding="utf-8") as fh:
text = fh.read()
dirs = [Path(os.path.join(root, d)) for d in re.findall(r"--paths-from (\S+)", text)]
globs = gate.gate_globs(Path(wf), dirs, wide_globs(text))
if globs is None:
raise Bad(f"the gate cannot read {WORKFLOW}'s paths with its --paths-from and --wide: it would run on every change")
return uniq(globs)
def from_tree(root):
"""Every list, as this tree's files give them."""
mods, errs = verifiers(root)
cs, e2 = corpora(root)
if errs + e2:
raise Bad("\n".join(errs + e2))
with open(os.path.join(root, WORKFLOW), encoding="utf-8") as fh:
text = fh.read()
x = workflow_extras(text)
d = derive(mods)
dbg = tagged(root, ["int-kernels"])
rel = tagged(root, ["int-kernels", "int-kernels-release"])
return {
"paths": gate_paths(root),
"WAI_FEATURES": d["WAI_FEATURES"], "WAI_PQ_FEATURES": d["WAI_PQ_FEATURES"],
"WAI_RELEASE_FEATURES": d["WAI_RELEASE_FEATURES"],
"union_bins": d["union_bins"], "pq_bins": d["pq_bins"],
"union_tests": d["union_tests"], "release_tests": d["release_tests"],
"alone": d["alone"],
"steps": [{"step": m["step"], "wd": m["wd"], "body": m["body"]} for m in mods],
"int_kernels_debug_features": tagged_features(root, ["int-kernels"], x.get("test-tagged int-kernels", [])),
"int_kernels_debug_tests": dbg,
"int_kernels_release_features": tagged_features(root, ["int-kernels", "int-kernels-release"], x.get("test-tagged int-kernels int-kernels-release", [])),
"int_kernels_release_tests": rel,
"int_kernels_amd_features": tagged_features(root, ["int-kernels"], x.get("features int-kernels", [])),
"int_kernels_amd_tests": tagged(root, ["int-kernels-amd"]),
"corpora_features": corpora_features(root, cs, x.get("corpora --verify", [])),
"corpora": [f"{c['corpus']} {c['bin']} {c['dir']}" for c in cs],
}
# ------------------------------------------------- the lists a workflow kept by hand
def job_steps(text, job):
"""[{name, wd, body, if, shell}] of a job's steps, read from the workflow text."""
lines = text.split("\n")
start = next((i for i, l in enumerate(lines) if l == f" {job}:"), None)
if start is None:
raise Bad(f"{WORKFLOW} has no job `{job}`")
end = next((i for i in range(start + 1, len(lines)) if re.match(r"^ [a-z0-9_-]+:\s*$", lines[i])), len(lines))
out, i = [], start
while i < end:
m = re.match(r"^ - name: (.*)$", lines[i])
i += 1
if not m:
continue
s = {"name": m.group(1).strip(), "wd": None, "body": None, "if": None, "shell": None}
while i < end and lines[i].startswith(" "):
k = re.match(r"^ ([a-z-]+): ?(.*)$", lines[i])
if k and k.group(1) == "run" and k.group(2) == "|":
b = []
i += 1
while i < end and (lines[i].startswith(" ") or not lines[i].strip()):
b.append(lines[i][10:])
i += 1
s["body"] = "\n".join(b).rstrip("\n")
continue
if k and k.group(1) == "run":
s["body"] = k.group(2)
elif k and k.group(1) == "working-directory":
s["wd"] = k.group(2)
elif k and k.group(1) in ("if", "shell"):
s[k.group(1)] = k.group(2).strip()
i += 1
out.append(s)
return out
def steps_of_job(text, job):
"""[(name, working-directory, run body)] of a job's steps, read from the workflow text."""
return [(s["name"], s["wd"], s["body"]) for s in job_steps(text, job)]
def from_hand_lists(root):
"""Every list, as a workflow that kept them by hand states them (before this change)."""
gate = gate_module(root)
from pathlib import Path
wf = os.path.join(root, WORKFLOW)
with open(wf, encoding="utf-8") as fh:
text = fh.read()
env = lambda k: re.search(rf"^ {k}: (.*)$", text, re.M).group(1).split(",")
iv = steps_of_job(text, "independent-verifiers")
b = next(n for n, s in enumerate(iv) if s[0].startswith("Build wai-rs once"))
build = joined(iv[b][2])
pick = lambda pat: next(l for l in build if re.search(pat, l))
union_build = pick(r'cargo build .*"\$WAI_FEATURES"')
pq_build = pick(r'cargo build .*"\$WAI_PQ_FEATURES"')
union_test = pick(r'cargo test --no-run .*"\$WAI_FEATURES"')
rel_test = pick(r'cargo test --release --no-run')
fa = steps_of_job(text, "features-alone")
loop = re.search(r"for f in (.*?); do", " ".join(joined(fa[[s[0] for s in fa].index("wai-rs, each feature list alone")][2])))
cc = steps_of_job(text, "cargo-classes")
ik = lambda name: next(s[2] for s in cc if s[0] == name)
dbg = ik("Fast integer kernels ≡ reference kernels (overflow checks on)")
rel = ik("Fast integer kernels ≡ reference kernels (release, overflow checks off)")
corp = ik("Every cargo-class corpus reproduces its frozen hashes (unix)")
amd = steps_of_job(text, "int-kernels-amd")[-1][2]
tests = lambda l: re.findall(r"--test ([A-Za-z0-9_-]+)", l)
feats = lambda l: re.search(r"--features (\S+)", l).group(1).split(",")
return {
"paths": uniq(gate.paths_of(Path(wf))),
"WAI_FEATURES": env("WAI_FEATURES"), "WAI_PQ_FEATURES": env("WAI_PQ_FEATURES"),
"WAI_RELEASE_FEATURES": env("WAI_RELEASE_FEATURES"),
"union_bins": re.findall(r"--bin ([A-Za-z0-9_-]+)", union_build),
"pq_bins": re.findall(r"--bin ([A-Za-z0-9_-]+)", pq_build),
"union_tests": tests(union_test), "release_tests": tests(rel_test),
"alone": loop.group(1).split(),
"steps": [{"step": n, "wd": wd or ".", "body": body} for n, wd, body in iv[b + 1:]],
"int_kernels_debug_features": sorted(feats(dbg)), "int_kernels_debug_tests": sorted(tests(dbg)),
"int_kernels_release_features": sorted(feats(rel)), "int_kernels_release_tests": sorted(tests(rel)),
"int_kernels_amd_features": sorted(re.search(r"^F=(\S+)$", amd, re.M).group(1).split(",")),
"int_kernels_amd_tests": sorted(tests(re.search(r'^T="(.*)"$', amd, re.M).group(1))),
"corpora_features": sorted(re.search(r"^F=(\S+)$", corp, re.M).group(1).split(",")),
"corpora": [" ".join(m.groups()) for m in re.finditer(r"^v (\S+)\s+(\S+)\s+(\S+)$", corp, re.M)],
}
def at_ref(ref, fn):
"""fn(root) over the tree at a git ref, extracted to a scratch directory."""
tmp = tempfile.mkdtemp(prefix="wai-lists-")
try:
arch = subprocess.run(["git", "archive", ref, ".github", "scripts", f"{CRATE}/Cargo.toml", f"{CRATE}/tests"],
capture_output=True, check=True).stdout
subprocess.run(["tar", "-x", "-C", tmp], input=arch, check=True)
return fn(tmp)
finally:
shutil.rmtree(tmp, ignore_errors=True)
def lists_of(root):
return from_tree(root) if os.path.isdir(os.path.join(root, VERIFIERS)) else from_hand_lists(root)
# ------------------------------------------------- the command lines of the other steps
# The steps outside the independent verifiers whose lists are derived: (label, job, the
# start of the step's name). Their names lost the leg's suffix when the corpus step's two
# copies, one a shell family, became one step, so they are found by the start of the name.
COMMAND_STEPS = (
("cargo-classes: fast integer kernels, overflow checks on", "cargo-classes",
"Fast integer kernels ≡ reference kernels (overflow checks on)"),
("cargo-classes: fast integer kernels, release", "cargo-classes",
"Fast integer kernels ≡ reference kernels (release, overflow checks off)"),
("cargo-classes: every cargo-class corpus", "cargo-classes",
"Every cargo-class corpus reproduces its frozen hashes"),
("int-kernels-amd", "int-kernels-amd", "Fast integer kernels ≡ reference kernels, every kernel set"),
("features-alone", "features-alone", "wai-rs, each feature list alone"),
)
# cargo's options that take a value, as `--opt value` or `--opt=value`.
VALUE_OPTS = {"--features", "-F", "--test", "--bin", "--example", "--bench", "--target", "-p", "--package",
"--manifest-path", "--profile", "-j", "--jobs", "--target-dir"}
SCRIPT_CALL = re.compile(r"^python3\s+\S*wai-conformance-lists\.py\s+(.*)$")
def tagged_args(rest):
"""(tags, extra features, cargo args) of `tests|features|test-tagged TAG... [--extra-features
A,B] [-- ARGS...]`, refusing what would run no test."""
rest = list(rest)
cargo_args = []
if "--" in rest:
cargo_args = rest[rest.index("--") + 1:]
rest = rest[:rest.index("--")]
extra = [x for x in (opt(rest, "--extra-features") or "").split(",") if x]
unknown = [t for t in rest if t not in TAGS]
if unknown or not rest:
raise Bad(f"tags: {' '.join(unknown) or '(none given)'}; a tag is one of {', '.join(TAGS)}")
return rest, extra, cargo_args
def test_tagged_line(root, tags, extra, cargo_args):
names = tagged(root, tags)
if not names:
raise Bad(f"no test file carries {' or '.join(tags)}: the step would run no test")
feats = tagged_features(root, tags, extra)
return ["cargo", "test"] + cargo_args + ["--features", ",".join(feats)] + [x for t in names for x in ("--test", t)]
def corpus_lines(root, cs, extra):
f = ",".join(corpora_features(root, cs, extra))
return f, [["cargo", "run", "-q", "--no-default-features", "--features", f, "--bin", c["bin"], "--", "verify", c["dir"]]
for c in cs]
def script_value(root, argv):
"""What `$(python3 .../wai-conformance-lists.py ARGV)` prints, for the commands a step's
shell takes from it."""
cmd, rest = argv[0], argv[1:]
if cmd in ("tests", "features"):
tags, extra, _ = tagged_args(rest)
if not tagged(root, tags):
raise Bad(f"no test file carries {' or '.join(tags)}: the step would run no test")
if cmd == "tests":
return " ".join(f"--test {t}" for t in tagged(root, tags))
return ",".join(tagged_features(root, tags, extra))
if cmd == "alone":
mods, errs = verifiers(root)
if errs:
raise Bad("\n".join(errs))
return "\n".join(derive(mods)["alone"])
raise Bad(f"`$(wai-conformance-lists.py {cmd} ...)`: the dry-run reads no value of `{cmd}`")
def script_commands(root, argv):
"""The cargo command lines `python3 .../wai-conformance-lists.py ARGV` runs."""
cmd, rest = argv[0], argv[1:]
if cmd == "test-tagged":
return [test_tagged_line(root, *tagged_args(rest))]
if cmd == "corpora" and "--verify" in rest:
rest = list(rest)
extra = [x for x in (opt(rest, "--extra-features") or "").split(",") if x]
cs, errs = corpora(root)
if errs:
raise Bad("\n".join(errs))
return corpus_lines(root, cs, extra)[1]
raise Bad(f"`wai-conformance-lists.py {cmd}` in a step the dry-run reads: it reads the commands of test-tagged and corpora --verify only")
def subst(s, env):
"""s with `$NAME` and `${NAME}` replaced from env; Bad for a name with no known value."""
def rep(m):
name = m.group(1) or m.group(2)
if env.get(name) is None:
raise Bad(f"`${name}` has no value the dry-run knows")
return env[name]
return re.sub(r"\$\{(\w+)\}|\$(\w+)", rep, s)
def shell_value(rhs, env, root):
"""The value an assignment's right-hand side gives, or None when it reads the runner's
environment."""
m = re.match(r"^\$\(\s*(python3\s+\S*wai-conformance-lists\.py\s+.*?)\s*\)$", rhs)
if m:
return script_value(root, shlex.split(SCRIPT_CALL.match(m.group(1)).group(1)))
if rhs.startswith("$(") or "`" in rhs:
return None
try:
if rhs.startswith("'") and rhs.endswith("'") and len(rhs) > 1:
return rhs[1:-1]
if rhs.startswith('"') and rhs.endswith('"') and len(rhs) > 1:
return subst(rhs[1:-1], env)
return subst(rhs, env)
except Bad:
return None
def run_lines(lines, env, funcs, root, out):
"""Append to out the cargo command lines (as argv) that a step's shell lines run: bash or
PowerShell, as far as the steps COMMAND_STEPS names use them (assignments, `for` loops, a
function a line calls, the derivations of this script, and cargo itself). A line that names
cargo but none of these is refused rather than skipped."""
i = 0
while i < len(lines):
line = lines[i].strip()
i += 1
if not line or line.startswith("#"):
continue
m = re.match(r"^for (\w+) in (.*); do$", line)
if m:
depth, j = 1, i
while j < len(lines):
t = lines[j].strip()
if re.match(r"^for \w+ in .*; do$", t):
depth += 1
elif t == "done":
depth -= 1
if not depth:
break
j += 1
else:
raise Bad(f"`{line}` has no `done`")
for v in shlex.split(subst(m.group(2), env)):
run_lines(lines[i:j], {**env, m.group(1): v}, funcs, root, out)
i = j + 1
continue
m = re.match(r"^(\w+)\(\)\s*\{\s*(.*?)\s*\}$", line)
if m:
funcs[m.group(1)] = (None, m.group(2).split(";"))
continue
m = re.match(r"^function (\w+)\((.*)\)\s*\{$", line)
if m:
j = next((j for j in range(i, len(lines)) if lines[j].strip() == "}"), None)
if j is None:
raise Bad(f"`{line}` has no closing `}}`")
funcs[m.group(1)] = ([p.strip().lstrip("$") for p in m.group(2).split(",") if p.strip()], lines[i:j])
i = j + 1
continue
m = re.match(r"^(?:export\s+)?\$?([A-Za-z_]\w*)\s*=\s*(.*)$", line)
if m:
env[m.group(1)] = shell_value(m.group(2), env, root)
continue
word = line.split()[0]
if word in funcs:
params, body = funcs[word]
args = shlex.split(subst(line[len(word):], env))
e = dict(env)
for n, a in enumerate(args, 1):
e[str(n)] = a
for p, a in zip(params or [], args):
e[p] = a
run_lines(body, e, funcs, root, out)
continue
m = SCRIPT_CALL.match(line)
if m:
out += script_commands(root, shlex.split(subst(m.group(1), env)))
continue
if re.search(r"(^|\s)cargo\s", line):
toks = shlex.split(subst(line, env))
if "cargo" not in toks:
raise Bad(f"`{line}`: the dry-run cannot read this command")
out.append(toks)
continue
if "wai-conformance-lists.py" in line:
raise Bad(f"`{line}`: the dry-run cannot read this call of the script")
def canon(toks):
"""A cargo command line in one form for every spelling of it: its flags sorted, its
features sorted and comma-joined, each target once and sorted, anything after `--` as it
was. Two commands that build and run the same thing print the same."""
at = toks.index("cargo")
pre, sub, rest = toks[:at], toks[at + 1], toks[at + 2:]
tail = []
if "--" in rest:
tail, rest = rest[rest.index("--"):], rest[:rest.index("--")]
flags, feats, vals = set(), set(), set()
i = 0
while i < len(rest):
t = rest[i]
if t.startswith("--") and "=" in t and t.split("=", 1)[0] in VALUE_OPTS:
k, v = t.split("=", 1)
i += 1
elif t in VALUE_OPTS:
if i + 1 >= len(rest):
raise Bad(f"`{' '.join(toks)}`: {t} with no value")
k, v = t, rest[i + 1]
i += 2
else:
flags.add(t)
i += 1
continue
if k in ("--features", "-F"):
feats |= {f for f in re.split(r"[ ,]+", v) if f}
else:
vals.add((k, v))
out = pre + ["cargo", sub] + sorted(flags) + (["--features", ",".join(sorted(feats))] if feats else [])
out += [x for k, v in sorted(vals) for x in (k, v)] + tail
return " ".join(shlex.quote(t) for t in out)
def admits(cond, leg):
"""Whether a step's `if:` lets it run on a leg of the family `leg` (unix or windows)."""
if cond is None:
return True
m = re.fullmatch(r"(?:\$\{\{\s*)?matrix\.os (==|!=) '([a-z0-9-]+)'(?:\s*\}\})?", cond)
if not m or m.group(2) != "windows":
raise Bad(f"a step's condition `{cond}` is none the dry-run reads (matrix.os == or != 'windows')")
return (m.group(1) == "==") == (leg == "windows")
def step_commands(root):
"""[(label, legs, working directories, [command lines])] of the steps COMMAND_STEPS names,
each command line in canon() form; `legs` is "every leg" when the unix legs and the
Windows leg run the same, and one entry a family otherwise."""
with open(os.path.join(root, WORKFLOW), encoding="utf-8") as fh:
text = fh.read()
out = []
for label, job, prefix in COMMAND_STEPS:
steps = [s for s in job_steps(text, job) if s["name"].startswith(prefix)]
if not steps:
raise Bad(f"{WORKFLOW}: job `{job}` has no step `{prefix}...`")
per = {}
for leg in ("unix", "windows"):
cmds, wds = [], []
for s in steps:
if not admits(s["if"], leg):
continue
argv = []
try:
run_lines(joined(s["body"] or ""), {}, {}, root, argv)
except Bad as e:
raise Bad(f"{WORKFLOW}: {job} / {s['name']}: {e}")
if not argv:
raise Bad(f"{WORKFLOW}: {job} / {s['name']}: the dry-run finds no cargo command in it")
cmds += [canon(a) for a in argv]
wds.append(s["wd"] or ".")
if label == "features-alone":
cmds = sorted(cmds)
per[leg] = (uniq(wds), cmds)
if per["unix"] == per["windows"]:
out.append((label, "every leg", *per["unix"]))
else:
out += [(label, "unix legs", *per["unix"]), (label, "the Windows leg", *per["windows"])]
return out
# ----------------------------------------------------------------------------- output
SETS = ("paths", "WAI_FEATURES", "WAI_PQ_FEATURES", "WAI_RELEASE_FEATURES", "union_bins", "pq_bins",
"union_tests", "release_tests", "alone", "int_kernels_debug_features", "int_kernels_debug_tests",
"int_kernels_release_features", "int_kernels_release_tests", "int_kernels_amd_features",
"int_kernels_amd_tests", "corpora_features")
def norm_list(k, xs):
if k == "alone":
return sorted(uniq(",".join(sorted(x.split(","))) for x in xs))
return sorted(uniq(xs))
def dry_run(L, C):
out = ["# wai-conformance.yml: what its derived lists make it run, in a stable form",
"# (lists sorted; steps and corpora in the order they run)"]
for k in SETS:
out.append(f"{k}: {' '.join(norm_list(k, L[k]))}")
out.append("corpora (in order):")
out += [f" {c}" for c in L["corpora"]]
out.append("independent-verifiers steps (in order):")
for s in L["steps"]:
out.append(f" - step: {s['step']} (working-directory: {s['wd']})")
out += [f" | {l}" for l in s["body"].split("\n")]
out.append("the other derived steps' commands (as cargo is given them: flags sorted, features sorted, each")
out.append("target once; in the order they run, but features-alone's sorted):")
for label, legs, wds, cmds in C:
out.append(f" - {label} ({legs}; working-directory: {', '.join(wds) or '-'})")
out += [f" $ {c}" for c in cmds]
return "\n".join(out) + "\n"
def check(root):
"""Every finding about the files this script reads."""
errs = []
try:
mods, e = verifiers(root)
cs, e2 = corpora(root)
except Bad as x:
return [str(x)]
errs += e + e2
targets, feats = cargo_targets(root)
tests = {n for k, n in targets if k == "test"} | {f[:-3] for f in os.listdir(os.path.join(root, CRATE, "tests")) if f.endswith(".rs")}
bins = {n for k, n in targets if k == "bin"}
# A checkout, rather than the files `dry-run --ref` extracts: directories can be checked.
full = os.path.isdir(os.path.join(root, CRATE, "src"))
gate = gate_module(root)
from pathlib import Path
wf_globs = gate.paths_of(Path(os.path.join(root, WORKFLOW)))
if sorted(gate.declared_paths(Path(os.path.join(root, VERIFIERS))) or []) != sorted(p for m in mods for p in m["paths"]):
errs.append(f"scripts/pr-touches-paths.py reads other paths from {VERIFIERS} than this script does")
try:
gate_paths(root)
except Bad as x:
errs.append(str(x))
for m in mods:
for lst in m["features"] + (m["alone"] or []) + m["release"]:
for f in lst.split(","):
if f not in feats:
errs.append(f"{m['file']}: feature `{f}` is not in {CRATE}/Cargo.toml [features]")
for line in joined(m["body"]):
for t in re.findall(r"--test[ =]([A-Za-z0-9_-]+)", line):
if "cd wai-rs" in line and t not in tests:
errs.append(f"{m['file']}: --test {t}: {CRATE} has no such test")
for b in m["bins"] + m["pq_bins"] + re.findall(r"wai-bins/(?:union|pq)/([A-Za-z0-9_-]+)", m["body"]):
if b not in bins:
errs.append(f"{m['file']}: binary `{b}` is not a [[bin]] of {CRATE}/Cargo.toml")
for p in m["paths"]:
# A path the forge's own filter does not cover never starts the workflow, whatever
# the gate would say: a file under the glob stands for every file it matches.
sample = p.replace("**", "s/s").replace("*", "s").replace("?", "s")
if not any(gate.matches(sample, g) for g in wf_globs):
errs.append(f"{m['file']}: path `{p}` is under none of {WORKFLOW}'s paths, so the forge would never start the workflow for it")
if full and not os.path.isdir(os.path.join(root, m["wd"])):
errs.append(f"{m['file']}: working-directory `{m['wd']}` does not exist")
for c in cs:
if c["bin"] not in bins:
errs.append(f"{c['file']}: binary `{c['bin']}` is not a [[bin]] of {CRATE}/Cargo.toml")
if full and not os.path.isdir(os.path.normpath(os.path.join(root, CRATE, c["dir"]))):
errs.append(f"{c['file']}: dir `{c['dir']}` is no directory (it is read from {CRATE})")
for t, ts in markers(root).items():
for tag in ts:
if tag not in TAGS:
errs.append(f"{CRATE}/tests/{t}.rs: `wai-conformance: {tag}` is no tag a step takes ({', '.join(TAGS)})")
return errs
def selftest(root, exact):
errs = check(root)
for e in errs:
print(f"check: {e}")
if errs:
return 1
with open(os.path.join(root, BASELINE), encoding="utf-8") as fh:
base = json.load(fh)
L = from_tree(root)
added = base.get("added", {})
bad = 0
for k in SETS + ("corpora",):
want = norm_list(k, base[k] + added.get(k, [])) if k in SETS else base[k]
got = norm_list(k, L[k]) if k in SETS else L[k]
lost = [x for x in want if x not in got]
more = [x for x in got if x not in want]
if lost:
print(f"FAIL {k}: lost {' '.join(lost)}")
bad += 1
elif exact and more:
print(f"FAIL {k}: more than the hand list: {' '.join(more)}")
bad += 1
else:
print(f"ok {k}: the hand list's {len(want)}{' and ' + str(len(more)) + ' more' if more else ''}")
names = [s["step"] for s in L["steps"]]
for s in base["steps"]:
if s["step"] not in names:
print(f"FAIL step `{s['step']}` no longer runs")
bad += 1
if exact:
# Each by name, not by place: a step or a corpus the tree adds is one addition, and
# moves no other along. The order is that of the entries both hold.
want = [s["step"] for s in base["steps"]]
for n in names:
if n not in want:
print(f"FAIL step `{n}`: more than the hand list")
bad += 1
if [n for n in names if n in want] != [n for n in want if n in names]:
print("FAIL the steps both lists hold run in another order than the hand list's")
bad += 1
derived = {s["step"]: s for s in L["steps"]}
for s in base["steps"]:
t = derived.get(s["step"])
if t is not None and (s["wd"], s["body"]) != (t["wd"], t["body"]):
print(f"FAIL step `{s['step']}`: its body or directory is not the hand list's")
bad += 1
if [c for c in L["corpora"] if c in base["corpora"]] != [c for c in base["corpora"] if c in L["corpora"]]:
print("FAIL the corpora both lists hold run in another order than the hand list's")
bad += 1
print(f"ok {len(base['steps'])} steps of the hand list run" if not bad else f"wai-conformance-lists selftest: {bad} failure(s)")
return 1 if bad else 0
# ------------------------------------------------------------------------------ main
def opt(args, name):
if name in args:
i = args.index(name)
v = args[i + 1]
del args[i:i + 2]
return v
return None
def main(argv):
args = list(argv)
root = opt(args, "--root") or os.path.dirname(HERE)
if not args:
print(__doc__)
return 2
cmd, rest = args[0], args[1:]
try:
if cmd in ("env", "prebuild", "run", "alone", "paths"):
mods, errs = verifiers(root)
if errs:
raise Bad("\n".join(errs))
d = derive(mods)
if cmd == "env":
for k in ("WAI_FEATURES", "WAI_PQ_FEATURES", "WAI_RELEASE_FEATURES"):
print(f"{k}={','.join(d[k])}")
elif cmd == "prebuild":
sys.stdout.write(prebuild(d))
elif cmd == "alone":
print("\n".join(d["alone"]))
elif cmd == "paths":
print("\n".join(d["paths"]))
else:
only = [rest[i + 1] for i, a in enumerate(rest) if a == "--only"]
env = dict(os.environ)
for k in ("WAI_FEATURES", "WAI_PQ_FEATURES", "WAI_RELEASE_FEATURES"):
env[k] = ",".join(d[k])
env.setdefault("RUNNER_TEMP", tempfile.gettempdir())
todo = [m for m in mods if not only or m["slug"] in only]
if only and len(todo) != len(set(only)):
raise Bad(f"--only names a step no file holds: {' '.join(sorted(set(only) - {m['slug'] for m in mods}))}")
for m in todo:
print(f"::group::{m['step']} ({m['file']})", flush=True)
rc = subprocess.call(["bash", "--noprofile", "--norc", "-eo", "pipefail", os.path.join(root, m["file"])],
cwd=os.path.join(root, m["wd"]), env=env)
print("::endgroup::", flush=True)
if rc:
print(f"::error::{m['step']} failed with exit status {rc} ({m['file']})", flush=True)
return rc
print(f"every step ran and passed: {len(todo)} of {len(mods)}")
elif cmd in ("tests", "features"):
print(script_value(root, [cmd] + rest))
elif cmd == "test-tagged":
line = test_tagged_line(root, *tagged_args(rest))
print("+ " + " ".join(line), flush=True)
return subprocess.call(line)
elif cmd == "corpora":
extra = [x for x in (opt(rest, "--extra-features") or "").split(",") if x]
cs, errs = corpora(root)
if errs:
raise Bad("\n".join(errs))
if "--verify" not in rest:
for c in cs:
print(f"{c['corpus']} {c['bin']} {c['dir']}")
return 0
f, lines = corpus_lines(root, cs, extra)
print(f"the corpora's features: {f}", flush=True)
for c, line in zip(cs, lines):
print(f"::group::{c['corpus']}", flush=True)
print("+ " + " ".join(line), flush=True)
rc = subprocess.call(line)
print("::endgroup::", flush=True)
if rc:
print(f"::error::the {c['corpus']} corpus failed with exit status {rc} ({c['file']})", flush=True)
return rc
elif cmd == "dry-run":
ref = opt(rest, "--ref")
both = lambda r: (lists_of(r), step_commands(r))
L, C = at_ref(ref, both) if ref else both(root)
sys.stdout.write(dry_run(L, C))
elif cmd == "baseline":
ref = opt(rest, "--ref")
if not ref:
raise Bad("baseline --ref REF: the commit whose workflow kept the lists by hand")
L = at_ref(ref, from_hand_lists)
commit = subprocess.run(["git", "rev-parse", ref], capture_output=True, text=True, check=True).stdout.strip()
print(json.dumps({"commit": commit, **L}, indent=1, ensure_ascii=False))
elif cmd == "check":
errs = check(root)
for e in errs:
print(e)
if errs:
print(f"wai-conformance-lists check: {len(errs)} finding(s)")
return 1
print("wai-conformance-lists check: every file is well formed and names what exists.")
elif cmd == "selftest":
return selftest(root, "--exact" in rest)
else:
print(__doc__)
return 2
except Bad as e:
print(f"wai-conformance-lists {cmd}: {e}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))