All checks were successful
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m10s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m58s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m30s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m14s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m16s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m18s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m18s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m7s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m49s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 7m51s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 14s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 15m38s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 24s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 49s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m54s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 4s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m5s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m12s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 4s
WAI section references / cited sections exist (pull_request) Successful in 11s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 4m33s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 2m53s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 35m41s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 36m22s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 4m31s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 4m56s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 4m42s
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Successful in 4s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m17s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m6s
wai-pack / wai-pack · linux (pull_request) Successful in 1m5s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 1m47s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 16s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m20s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 36m54s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 52m0s
standards workspaces build + test / aac-rs (pull_request) Successful in 8m54s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 6m49s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 5m1s
standards workspaces build + test / map-rs (pull_request) Successful in 6m36s
standards workspaces build + test / mesh-rs (pull_request) Successful in 1m6s
standards workspaces build + test / openpay (pull_request) Successful in 1m31s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 9m7s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 1m7s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 3m18s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 2m51s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m54s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m5s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 11m1s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 29s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 3m10s
wai-pack / wai-pack · macos (pull_request) Successful in 2m13s
A mutation used to pass when http_verify.py merely exited non-zero, so a crash counted as a catch: receipt-surrogates printed "ok … 0 failures" because the mutated verifier raised UnicodeEncodeError in lp() on the lone surrogate it had just admitted. - falsify/mutations.txt lists each of the 77 mutations with the cases it must fail, as each patch does; run.sh reads the mutations from it. A mutation passes only when http_verify.py exits non-zero and prints a FAIL line for every listed case; a traceback, or a non-zero exit with no named FAIL, is reported as a harness failure. The mutation count, and so the shard totals, are unchanged. - receipt-surrogates models a reader that admits an unpaired surrogate (RFC 8259 §8.2) and carries it on. Receipt text is now encoded with text_bytes(), which never raises (the unpaired surrogate goes as the bytes a lenient reader would hash), so the mutated verifier reaches a verdict: playlist_receipt_lone_surrogate and playlist_receipt_lone_low_surrogate give playlist_receipt_invalid where the corpus says playlist_receipt_malformed. The unmutated reader still refuses such text first, by name (playlist_receipt_malformed), as wai-rs does; no corpus case changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
257 lines
11 KiB
Shell
Executable file
257 lines
11 KiB
Shell
Executable file
#!/usr/bin/env bash
|
||
# Falsify the HTTP corpus tool and its independent verifier.
|
||
#
|
||
# Patches: each patch here breaks one function of wai-rs. In one worktree of HEAD,
|
||
# built once, each patch is applied, the tree rebuilt incrementally, and
|
||
# `wai_http_vectors verify` must exit non-zero naming every case the patch's first line
|
||
# lists; then the patch is reversed and the tree must be clean again. The builds use
|
||
# the `falsify` profile (wai-rs/Cargo.toml): overflow checks and debug assertions on, as
|
||
# in the tests, so a patch is judged by the code's own checks, and incremental, so each
|
||
# patch rebuilds only what it touched. The unpatched tree must pass first.
|
||
#
|
||
# Mutations: each `http_verify.py --mutate NAME` must exit non-zero naming, as FAIL lines,
|
||
# every case mutations.txt lists for it; a crash, or a non-zero exit that names none, is
|
||
# a failure of the harness.
|
||
#
|
||
# The differential: wai-rs writes random envelopes' covered prefixes and head proofs,
|
||
# http_verify.py verifies every one, and refuses exactly the ten it changes one
|
||
# chaining-value byte in.
|
||
#
|
||
# bash wai/http-conformance/falsify/run.sh [work-dir] [--patches I/N | --mutations I/N |
|
||
# --differential | --check-shards 'patches I/N …' 'mutations I/N …' | patch-name…]
|
||
#
|
||
# With no selector, all three run. `--patches I/N` runs the patches whose index in the
|
||
# sorted list, mod N, is I; `--mutations I/N` the same over the mutations. Each shard
|
||
# prints every patch or mutation it ran, and fails unless what it ran is exactly what was
|
||
# assigned to it. `--check-shards` takes the shards a workflow declares and fails unless
|
||
# their totals agree, their indices are 0..N-1, and their assignments cover every patch
|
||
# and every mutation exactly once. With patch names, only those patches run.
|
||
#
|
||
# Run from the repository root. CARGO_TARGET_DIR, if set, holds the builds, which need
|
||
# the features http, video_manifest (the playlist receipts) and key_discovery (the
|
||
# receipts, keys, grants and sets of §8.1–§8.5). The receipt and key cases of
|
||
# http_verify.py need the `cryptography` package.
|
||
set -uo pipefail
|
||
ROOT="$(git rev-parse --show-toplevel)"
|
||
FALSIFY="$ROOT/wai/http-conformance/falsify"
|
||
WORK="${RUNNER_TEMP:-/tmp}/wai-http-falsify"
|
||
if [ $# -gt 0 ] && [ "${1#--}" = "$1" ] && [ ! -f "$FALSIFY/$1.patch" ]; then
|
||
WORK="$1"
|
||
shift
|
||
fi
|
||
mkdir -p "$WORK"
|
||
FEATURES=http,video_manifest,key_discovery
|
||
fail=0
|
||
|
||
# The mutations and the cases each must fail, from mutations.txt (one a line: the name,
|
||
# then the cases).
|
||
MUTATIONS=()
|
||
while read -r m _; do
|
||
case "$m" in ""|"#"*) continue ;; esac
|
||
MUTATIONS+=("$m")
|
||
done < "$FALSIFY/mutations.txt"
|
||
|
||
# The cases the mutation $1 must fail, as mutations.txt lists them.
|
||
mutation_cases() {
|
||
awk -v m="$1" '$1 == m { $1 = ""; print; exit }' "$FALSIFY/mutations.txt"
|
||
}
|
||
|
||
# Every patch, by name, in sorted order: the list shards index into.
|
||
all_patches() {
|
||
local p
|
||
for p in "$FALSIFY"/*.patch; do basename "$p" .patch; done | LC_ALL=C sort
|
||
}
|
||
|
||
# The members of a list (one per line on stdin) whose index mod N is I.
|
||
shard_of() {
|
||
awk -v i="$1" -v n="$2" '(NR - 1) % n == i'
|
||
}
|
||
|
||
# Parse I/N into the globals SI and SN, refusing anything else.
|
||
parse_shard() {
|
||
if ! [[ "$1" =~ ^([0-9]+)/([0-9]+)$ ]] || [ "${BASH_REMATCH[2]}" -eq 0 ] || [ "${BASH_REMATCH[1]}" -ge "${BASH_REMATCH[2]}" ]; then
|
||
echo "::error::a shard is I/N with 0 <= I < N, not '$1'"
|
||
exit 2
|
||
fi
|
||
SI="${BASH_REMATCH[1]}"
|
||
SN="${BASH_REMATCH[2]}"
|
||
}
|
||
|
||
# The lines of stdin into the array NAMES (bash 3.2 has no mapfile).
|
||
read_names() {
|
||
NAMES=()
|
||
local line
|
||
while IFS= read -r line; do [ -n "$line" ] && NAMES+=("$line"); done
|
||
}
|
||
|
||
# The patches stage over the names on stdin, in one worktree of HEAD.
|
||
run_patches() {
|
||
local names wt bin name patch must rc missing ran=() t0 t1
|
||
read_names
|
||
names=("${NAMES[@]}")
|
||
wt="$WORK/tree"
|
||
git -C "$ROOT" worktree remove --force "$wt" > /dev/null 2>&1
|
||
rm -rf "$wt"
|
||
git -C "$ROOT" worktree add --detach --quiet "$wt" HEAD
|
||
bin="${CARGO_TARGET_DIR:-$wt/wai/wai-rs/target}/falsify/wai_http_vectors"
|
||
build() { (cd "$wt/wai/wai-rs" && cargo build --profile falsify --no-default-features --features "$FEATURES" --bin wai_http_vectors); }
|
||
# The unpatched tree builds and passes, or no patch's failure means anything.
|
||
t0=$(date +%s)
|
||
if ! build > "$WORK/unpatched.build.log" 2>&1; then
|
||
echo "::error::the unpatched tree does not build"; tail -20 "$WORK/unpatched.build.log"
|
||
git -C "$ROOT" worktree remove --force "$wt"; return 1
|
||
fi
|
||
if ! "$bin" verify "$wt/wai/http-conformance" > "$WORK/unpatched.verify.log" 2>&1; then
|
||
echo "::error::the unpatched tree does not pass"; tail -20 "$WORK/unpatched.verify.log"
|
||
git -C "$ROOT" worktree remove --force "$wt"; return 1
|
||
fi
|
||
t1=$(date +%s)
|
||
echo "the unpatched tree builds and passes ($((t1 - t0)) s)"
|
||
for name in "${names[@]}"; do
|
||
patch="$FALSIFY/$name.patch"
|
||
t0=$(date +%s)
|
||
must="$(head -1 "$patch" | sed 's/^# must fail: //')"
|
||
if ! git -C "$wt" apply "$patch"; then
|
||
echo "::error::$name: the patch does not apply"; fail=1; continue
|
||
fi
|
||
if ! build > "$WORK/$name.build.log" 2>&1; then
|
||
echo "::error::$name: the patched tree does not build"; tail -20 "$WORK/$name.build.log"; fail=1
|
||
else
|
||
"$bin" verify "$wt/wai/http-conformance" > "$WORK/$name.verify.log" 2>&1
|
||
rc=$?
|
||
missing=""
|
||
for c in $must; do grep -q "^FAIL $c:" "$WORK/$name.verify.log" || missing="$missing $c"; done
|
||
t1=$(date +%s)
|
||
if [ $rc -eq 0 ] || [ -n "$missing" ]; then
|
||
echo "::error::$name: verify exited $rc; cases not failed:$missing"; fail=1
|
||
else
|
||
echo "ok $name: verify exited $rc and failed$(printf ' %s' $must) ($(grep -c '^FAIL' "$WORK/$name.verify.log") failures, $((t1 - t0)) s)"
|
||
fi
|
||
fi
|
||
ran+=("$name")
|
||
# Back to HEAD exactly, or every later patch is judged on a changed tree.
|
||
if ! git -C "$wt" apply -R "$patch" || [ -n "$(git -C "$wt" status --porcelain --untracked-files=no)" ]; then
|
||
echo "::error::$name: the tree is not clean after reversing the patch"; git -C "$ROOT" worktree remove --force "$wt"; return 1
|
||
fi
|
||
done
|
||
git -C "$ROOT" worktree remove --force "$wt"
|
||
check_ran patches "$(printf '%s\n' "${names[@]}")" "$(printf '%s\n' "${ran[@]}")"
|
||
return $fail
|
||
}
|
||
|
||
# The mutations stage over the names on stdin.
|
||
run_mutations() {
|
||
local names m ran=()
|
||
read_names
|
||
names=("${NAMES[@]}")
|
||
for m in "${names[@]}"; do
|
||
local must log rc missing c
|
||
must="$(mutation_cases "$m")"
|
||
log="$WORK/py-$m.log"
|
||
(cd "$ROOT/wai/http-conformance" && python3 http_verify.py . --mutate "$m") > "$log" 2>&1
|
||
rc=$?
|
||
missing=""
|
||
for c in $must; do grep -q "^FAIL $c:" "$log" || missing="$missing $c"; done
|
||
if [ -z "$must" ]; then
|
||
echo "::error::$m: mutations.txt declares no case it must fail"; fail=1
|
||
elif [ $rc -eq 0 ]; then
|
||
echo "::error::http_verify.py --mutate $m passed"; fail=1
|
||
elif grep -q '^Traceback' "$log" || ! grep -q '^FAIL ' "$log"; then
|
||
# A crash, or a refusal to run, is not a catch: the harness failed.
|
||
echo "::error::$m: a harness failure: http_verify.py exited $rc without naming the cases it failed"; tail -5 "$log"; fail=1
|
||
elif [ -n "$missing" ]; then
|
||
echo "::error::$m: http_verify.py exited $rc; cases not failed:$missing"; fail=1
|
||
else
|
||
echo "ok http_verify.py --mutate $m: failed$(printf ' %s' $must) ($(grep -c '^FAIL' "$log") failures)"
|
||
fi
|
||
ran+=("$m")
|
||
done
|
||
check_ran mutations "$(printf '%s\n' "${names[@]}")" "$(printf '%s\n' "${ran[@]}")"
|
||
return $fail
|
||
}
|
||
|
||
# A shard ran exactly what it was assigned (one name a line in each): printed, and
|
||
# compared.
|
||
check_ran() {
|
||
local kind="$1" want got
|
||
want="$(printf '%s\n' "$2" | grep -v '^$' | LC_ALL=C sort)"
|
||
got="$(printf '%s\n' "$3" | grep -v '^$' | LC_ALL=C sort)"
|
||
echo "$kind run in this shard ($(printf '%s\n' "$got" | grep -c .)): $(echo $got)"
|
||
if [ "$want" != "$got" ]; then
|
||
echo "::error::this shard did not run exactly the $kind assigned to it"
|
||
diff <(printf '%s\n' "$want") <(printf '%s\n' "$got")
|
||
fail=1
|
||
fi
|
||
}
|
||
|
||
# The shards a workflow declares cover every patch and every mutation exactly once.
|
||
check_shards() {
|
||
local kind list declared n total covered expected
|
||
for kind in patches mutations; do
|
||
if [ "$kind" = patches ]; then list="$(all_patches)"; else list="$(printf '%s\n' "${MUTATIONS[@]}")"; fi
|
||
declared="$(printf '%s\n' "$@" | awk -v k="$kind" '$1 == k { print $2 }')"
|
||
if [ -z "$declared" ]; then echo "::error::no $kind shard is declared"; fail=1; continue; fi
|
||
total="$(printf '%s\n' "$declared" | cut -d/ -f2 | sort -u)"
|
||
if [ "$(printf '%s\n' "$total" | wc -l)" -ne 1 ]; then echo "::error::$kind shards disagree on their total: $(echo $total)"; fail=1; continue; fi
|
||
n="$total"
|
||
if [ "$(printf '%s\n' "$declared" | cut -d/ -f1 | LC_ALL=C sort -n | tr '\n' ' ')" != "$(seq 0 $((n - 1)) | tr '\n' ' ')" ]; then
|
||
echo "::error::$kind shards are not 0..$((n - 1)) of $n, each once: $(echo $declared)"; fail=1; continue
|
||
fi
|
||
covered="$(for s in $declared; do parse_shard "$s"; printf '%s\n' "$list" | shard_of "$SI" "$SN"; done | LC_ALL=C sort)"
|
||
expected="$(printf '%s\n' "$list" | LC_ALL=C sort)"
|
||
if [ "$covered" != "$expected" ]; then
|
||
echo "::error::the $kind shards do not cover every one exactly once"
|
||
diff <(printf '%s\n' "$expected") <(printf '%s\n' "$covered")
|
||
fail=1
|
||
else
|
||
echo "the $n $kind shards cover all $(printf '%s\n' "$expected" | wc -l | tr -d ' ') $kind, each once"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# The differential, with the unpatched tool.
|
||
run_differential() {
|
||
local bin
|
||
if ! (cd "$ROOT/wai/wai-rs" && cargo build --profile falsify --no-default-features --features "$FEATURES" --bin wai_http_vectors) > "$WORK/differential.build.log" 2>&1; then
|
||
echo "::error::the unpatched tool does not build"; tail -20 "$WORK/differential.build.log"; fail=1; return
|
||
fi
|
||
bin="${CARGO_TARGET_DIR:-$ROOT/wai/wai-rs/target}/falsify/wai_http_vectors"
|
||
cd "$ROOT/wai/http-conformance" || return 1
|
||
"$bin" diff . --seed 7 --count 500 --out "$WORK/diff.jsonl" > /dev/null || fail=1
|
||
python3 http_verify.py diff "$WORK/diff.jsonl" || fail=1
|
||
python3 http_verify.py diff "$WORK/diff.jsonl" --flip 10 --seed 3 || fail=1
|
||
cd "$ROOT" || return 1
|
||
}
|
||
|
||
case "${1:-}" in
|
||
--patches)
|
||
parse_shard "${2:?--patches I/N}"
|
||
all_patches | shard_of "$SI" "$SN" | run_patches || fail=1
|
||
;;
|
||
--mutations)
|
||
parse_shard "${2:?--mutations I/N}"
|
||
printf '%s\n' "${MUTATIONS[@]}" | shard_of "$SI" "$SN" | run_mutations || fail=1
|
||
;;
|
||
--differential)
|
||
run_differential
|
||
;;
|
||
--check-shards)
|
||
shift
|
||
check_shards "$@"
|
||
;;
|
||
"")
|
||
all_patches | run_patches || fail=1
|
||
printf '%s\n' "${MUTATIONS[@]}" | run_mutations || fail=1
|
||
run_differential
|
||
;;
|
||
--*)
|
||
echo "::error::unknown option $1"; exit 2
|
||
;;
|
||
*)
|
||
for name in "$@"; do
|
||
[ -f "$FALSIFY/$name.patch" ] || { echo "::error::no patch $name"; exit 2; }
|
||
done
|
||
printf '%s\n' "$@" | run_patches || fail=1
|
||
;;
|
||
esac
|
||
exit $fail
|