#!/bin/sh # test/run — driver for `make test`. Plan 9 rc-flavoured but plain sh. # # Walks test/wcc/_.c → out/bin/test_ binary, fans out # across $JOBS (default $(nproc)) workers via xargs -P. Each worker # writes its status to a per-job file; the collector enumerates them in # lexicographic order so output stays deterministic across runs even # though execution interleaves. Exit non-zero if any worker failed. BIN=${BIN:-out/bin} WW=${WW:-$BIN/ww} # Worker mode: invoked once per test by xargs. Argv: --one . # Writes .status; touches .fail on a non-zero binary exit. if [ "$1" = "--one" ]; then prefix=$2 t=$3 name=${t##*/} name=${name%.c} short=${name#[0-9][0-9][0-9]_} bin=$BIN/test_$short if [ ! -x "$bin" ]; then # A missing binary means an unwired test/wcc file: `make test` # builds every $(TESTS) target before this runs, so the only way # to get here is a .c with no Makefile rule. That used to SKIP # and still count toward "all N tests passed" — 953_arrlit_slice # sat dark for weeks under a green gate. Fail loud instead. { printf 'FAIL %s (no binary %s)\n' "$name" "$bin" printf 'unwired test: add $(BIN)/test_%s to TESTS + a build rule in Makefile\n' "$short" } > "$prefix.status" : > "$prefix.fail" exit 0 fi # Per-test wall-clock (task #10 baseline). Stamp around the binary run # only; build cost lives in `make all`, not here. Duration lands in # .dur as "\t" for the collector's timing section — # kept out of .status so the ok/FAIL log format is untouched. t0=$(date +%s.%N) if "$bin" > "$prefix.out" 2> "$prefix.err"; then t1=$(date +%s.%N) printf 'ok %s\n' "$name" > "$prefix.status" else rc=$? t1=$(date +%s.%N) { printf 'FAIL %s (rc=%d)\n' "$name" "$rc" echo '--- stdout ---' cat "$prefix.out" echo '--- stderr ---' cat "$prefix.err" } > "$prefix.status" : > "$prefix.fail" fi awk -v a="$t0" -v b="$t1" -v n="$name" 'BEGIN{printf "%.3f\t%s\n", b-a, n}' \ > "$prefix.dur" exit 0 fi set -e RESULTS=$(mktemp -d "${TMPDIR:-/tmp}/wwtest.XXXXXX") trap 'rm -rf "$RESULTS"' EXIT JOBS=${JOBS:-$(nproc 2>/dev/null || echo 4)} # ---- content-keyed gate cache (task #10 T1; .ai/rob-testperf-go.md L1) ------- # `make test-commit` (COMMIT=1) reports the expensive wwstage byte-id / # self-compile gates as `cached` when their full input set is byte-identical to # the last green run. Those gate outcomes are a PURE function of (stage binaries # + selfhost/lib/cmd/rt sources + test fixtures + this harness + the Makefile); # if that content hash equals the recorded last-green key the result is provably # unchanged, so `cached` is sound — not a guess. `make test` NEVER reads the # cache (it stays the no-skip pre-push bar) but a green run of the gates DOES # record the key, as the natural primer. A red run never records: the write is # guarded on fail=0 (and the fail>0 path exits before it). Cache lives under # out/.testcache (out/ is gitignored); `make clean` wipes out/ and thus the # cache, which is correct — the key is keyed on the just-built binaries, so a # from-scratch rebuild must start from a clean cache. OUT_DIR=$(dirname "$BIN") CACHE_DIR=$OUT_DIR/.testcache CACHE_FILE=$CACHE_DIR/last-green # The exact cached set, confirmed from the Makefile/test/run: the UNIT-guarded # phase-2 gates (950 + 990-997) plus the single phase-1 lib byte-id gate # (989_lib_byteid). The other 989_*/998/999 files are cheap functional _run # tests, NOT byte-id gates, and stay always-on. Every member is a deterministic # pure function of the hashed inputs (no /tmp/env/clock/network reads); a future # gate that reads outside that set must be kept OUT of this list (Go marks such # tests uncacheable for the same reason). is_cached_gate() { case $1 in 950_selfcheck.c|989_lib_byteid.c|\ 990_*|991_*|992_*|993_*|994_*|995_*|996_*|997_*) return 0 ;; esac return 1 } emit_cached() { printf 'cached %s\n' "$1" > "$RESULTS/$1.status" : > "$RESULTS/$1.cached" } # Content key over EVERYTHING that determines the cached gates' outcomes: the # stage binary md5s first (fixed order), then tracked source/fixture/harness # content (git ls-files is sorted → deterministic; md5sum hashes working-tree # bytes, so uncommitted edits flip the key too). Any changed byte flips the key # → the gates RUN. A missed input would be a false green (rob's veto), so the # set is a deliberate conservative superset (wwstage binary md5s overlap the # selfhost/lib source hashes, rt/ is hashed though it only reaches the gates via # libwwrt); an extra input only ever costs a needless honest re-run. examples/ # is hashed because 996_dyn_ww links examples/mandelbrot/mandelbrot.{ww,o} — a # real gate input outside the source tree. testcache_key() { for b in ww w6c w6a w6l wwdump ww_ww w6c_ww w6a_ww w6l_ww wwdump_ww; do [ -f "$BIN/$b" ] && md5sum "$BIN/$b" done git ls-files selfhost lib cmd rt examples test/wcc test/run Makefile | xargs md5sum } KEY="" if [ "${UNIT:-0}" != "1" ]; then KEY=$(testcache_key 2>/dev/null | md5sum 2>/dev/null | cut -d' ' -f1) || KEY="" fi CACHE_HIT=0 if [ "${COMMIT:-0}" = "1" ] && [ -n "$KEY" ] && [ -f "$CACHE_FILE" ] \ && [ "$(cat "$CACHE_FILE" 2>/dev/null)" = "$KEY" ]; then CACHE_HIT=1 fi # Phase wall-clock stamps (task #10 baseline). Each phase records start/end # so the collector can report wall shares onto ken's three poles. run_start=$(date +%s.%N) # Phase 2 runs the wwstage byte-id / self-compile gates (990-997) + # 950_selfcheck. They're kept out of phase 1 only so the heavy 43k # self-compiles don't contend with the phase-1 fixture swarm; within # phase 2 they all run in parallel (post-T3 each build's intermediates # follow its output via `-o`/temp, so no member writes a path another # reads — see the phase-2 block below). for t in test/wcc/*.c; do [ -f "$t" ] || continue case ${t##*/} in 950_*|990_*|991_*|992_*|993_*|994_*|995_*|996_*|997_*) continue ;; esac name=${t##*/}; name=${name%.c} # The only cached gate that lives in phase 1 is 989_lib_byteid; on a # content-key hit report it cached instead of re-running it. if [ "$CACHE_HIT" = 1 ] && is_cached_gate "${t##*/}"; then emit_cached "$name" continue fi printf '%s/%s\0%s\0' "$RESULTS" "$name" "$t" done | xargs -0 -n2 -P "$JOBS" "$0" --one || true p1_end=$(date +%s.%N) # UNIT=1 (make test-unit) is the inner-loop short path: skip the wwstage # byte-id gates (990-997) and 950_selfcheck entirely. Pre-push uses # `make test` for the full suite. if [ "${UNIT:-0}" != "1" ]; then if [ "$CACHE_HIT" = 1 ]; then # Content key matched last-green → the wwstage byte-id / self-compile # gates are provably unchanged. Report them cached; do NOT re-run. for t in test/wcc/990_*.c test/wcc/991_*.c test/wcc/992_*.c \ test/wcc/993_*.c test/wcc/994_*.c test/wcc/995_*.c \ test/wcc/996_*.c test/wcc/997_*.c test/wcc/950_*.c; do [ -f "$t" ] || continue name=${t##*/}; name=${name%.c} emit_cached "$name" done else p2par_start=$(date +%s.%N) # Phase 2 — the wwstage byte-id / self-compile gates, all parallel. # Pre-T3 the source-tree writers (993/995/950) ran in a serial tail # because ww_ww wrote intermediates next to every traversed source, so # they raced each other AND the 990/991/992/994 readers. T3 made every # build's .combined.ww/.s/.o follow its OUTPUT (`-o`/per-pid temp): 993 # → its dc/dw workdirs, 995 → /tmp/wwsr__, 950 → /tmp only. # No member now writes a stem another reads (the readers see only the # stable make-all regen), so the whole group fans out via xargs -P. for t in test/wcc/990_*.c test/wcc/991_*.c test/wcc/992_*.c \ test/wcc/993_*.c test/wcc/994_*.c test/wcc/995_*.c \ test/wcc/996_*.c test/wcc/997_*.c test/wcc/950_*.c; do [ -f "$t" ] || continue name=${t##*/}; name=${name%.c} printf '%s/%s\0%s\0' "$RESULTS" "$name" "$t" done | xargs -0 -n2 -P "$JOBS" "$0" --one || true p2par_end=$(date +%s.%N) fi fi fail=0 ran=0 cached=0 for s in "$RESULTS"/*.status; do [ -f "$s" ] || continue cat "$s" prefix=${s%.status} if [ -f "$prefix.cached" ]; then cached=$((cached + 1)) else ran=$((ran + 1)) fi [ -f "$prefix.fail" ] && fail=$((fail + 1)) done # ---- ww source-level tests ---------------------------------------------- if [ -d test/lang ]; then for f in test/lang/*/*.ww; do [ -f "$f" ] || continue echo "skip $f (ww run not online yet)" done fi # ---- timing (task #10 baseline) ----------------------------------------- # Per-test durations + phase walls. Emitted after the per-test ok/FAIL dump # but before the summary line's exit checks, so timing prints even on the # failure path; the "all N tests passed" summary stays the last line. No log # consumer is disturbed: the "---" delimiters and "\t" rows match # neither the "all N tests passed" nor the "FAIL" grep keys. run_end=$(date +%s.%N) echo '--- timing: per-test (sec, desc) ---' cat "$RESULTS"/*.dur 2>/dev/null | sort -rn echo '--- timing: phase walls (sec) ---' awk -v rs="$run_start" -v p1e="$p1_end" \ -v ppar_s="$p2par_start" -v ppar_e="$p2par_end" \ -v pser_s="$p2ser_start" -v pser_e="$p2ser_end" \ -v re="$run_end" 'BEGIN{ printf "phase1_parallel\t%.3f\n", p1e - rs if (ppar_e != "") printf "phase2_parallel\t%.3f\n", ppar_e - ppar_s if (pser_e != "") printf "phase2_serial_tail\t%.3f\n", pser_e - pser_s printf "total\t%.3f\n", re - rs }' if [ $ran -eq 0 ] && [ $cached -eq 0 ]; then echo "no tests were run" exit 1 fi if [ $fail -gt 0 ]; then echo "$fail test(s) failed" exit 1 fi # Green and the gates actually RAN (not a cache hit, not test-unit): record the # content key as the last-green primer. A red run exits above, never here. if [ "${UNIT:-0}" != "1" ] && [ "$CACHE_HIT" != 1 ] && [ -n "$KEY" ]; then mkdir -p "$CACHE_DIR" && printf '%s\n' "$KEY" > "$CACHE_FILE" fi [ $cached -gt 0 ] && echo "$cached gate(s) cached (content key matched last green)" echo "all $ran tests passed"