boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit fa542273abf56197707d1ce2db72563bdd9daa12
parent 445f751aceb58da4012431734742011229962bea
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 27 Apr 2026 10:23:27 -0700

scripts: collapse test runner to one container per arch

The host runner used to re-enter the container per fixture (and
per build/run within a fixture). Move all suite logic into
scripts/boot-run-tests.sh so a whole arch's suite is one podman
invocation; the host now just dispatches and aggregates totals.
m1pp's lint preflight stays on the host because lint.sh runs python.

Diffstat:
MMakefile | 7++++---
Ascripts/boot-run-tests.sh | 431+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/run-tests.sh | 581+++++++++++--------------------------------------------------------------------
3 files changed, 512 insertions(+), 507 deletions(-)

diff --git a/Makefile b/Makefile @@ -225,9 +225,10 @@ build/native-tools/m1pp: scripts/build-native-tools.sh M1pp/M1pp.c # # `make test` runs every suite. SUITE selects one; ARCH restricts to one # arch. Make ensures every dependency (images, tools, tables, m1pp -# expanders) exists before scripts/run-tests.sh is invoked. The runner -# itself only loops fixtures, builds per-fixture binaries via the boot-* -# scripts, runs them in the container, and diffs. +# expanders) exists before scripts/run-tests.sh is invoked. The host +# runner starts one container per arch and hands off to +# scripts/boot-run-tests.sh, which loops the fixtures, builds +# per-fixture binaries via the boot-* scripts, runs them, and diffs. SUITE ?= diff --git a/scripts/boot-run-tests.sh b/scripts/boot-run-tests.sh @@ -0,0 +1,431 @@ +#!/bin/sh +## boot-run-tests.sh — in-container suite runner. +## +## One invocation handles every requested fixture in a suite for $ARCH, +## instead of the host re-entering the container per fixture. The host +## runner (scripts/run-tests.sh) starts one podman process per arch and +## lets this script do all the build / execute / diff work. +## +## PASS/FAIL lines stream to stdout in the same format the host expects; +## the host greps them to update its totals, then prints the final +## summary itself. Fixture-name discovery happens here when no names +## are passed (so the host can stay agnostic about each suite's layout), +## except for m1pp: scripts/lint.sh runs python on the host, so the +## host preflights lint and passes the explicit kept list down. +## +## Env: ARCH=aarch64|amd64|riscv64 +## Usage: boot-run-tests.sh --suite=<m1pp|p1|scheme1|cc-util|cc-lex|cc-pp|cc-cg|cc-parse|cc-e2e> [name ...] + +set -eu + +: "${ARCH:?ARCH must be set}" + +SUITE= +NAMES= + +while [ "$#" -gt 0 ]; do + case "$1" in + --suite) shift; SUITE=$1 ;; + --suite=*) SUITE=${1#--suite=} ;; + --) shift; while [ "$#" -gt 0 ]; do NAMES="$NAMES $1"; shift; done; break ;; + -*) echo "$0: unknown flag '$1'" >&2; exit 2 ;; + *) NAMES="$NAMES $1" ;; + esac + shift +done + +case "$SUITE" in + m1pp|p1|scheme1|cc-util|cc-lex|cc-pp|cc-cg|cc-parse|cc-e2e) ;; + "") echo "$0: --suite required" >&2; exit 2 ;; + *) echo "$0: unknown suite '$SUITE'" >&2; exit 2 ;; +esac + +discover() { + dir=$1; ext=$2 + ls "$dir" 2>/dev/null \ + | sed -n "s/^\\([^_][^.]*\\)\\.$ext\$/\\1/p" \ + | sort -u +} + +report() { + label=$1; status=$2 + echo " $status $label" +} + +show_diff() { + expected=$1; actual=$2 + echo " --- expected ---" + printf '%s\n' "$expected" | sed 's/^/ /' + echo " --- actual ---" + printf '%s\n' "$actual" | sed 's/^/ /' +} + +## --- m1pp suite --------------------------------------------------------- + +run_m1pp_suite() { + if [ -z "$NAMES" ]; then + m1=$(discover tests/M1pp M1) + m1pp=$(discover tests/M1pp M1pp) + NAMES=$(printf '%s\n%s\n' "$m1" "$m1pp" | sort -u | tr '\n' ' ') + fi + for name in $NAMES; do + expected=tests/M1pp/$name.expected + m1_src=tests/M1pp/$name.M1 + m1pp_src=tests/M1pp/$name.M1pp + + if [ ! -e "$expected" ]; then + echo " SKIP $name (no .expected)" + continue + fi + expected_content=$(cat "$expected") + + label="[$ARCH] $name" + if [ -e "$m1pp_src" ]; then + outfile=build/$ARCH/m1pp-out/$name + mkdir -p "$(dirname "$outfile")" + rm -f "$outfile" + "./build/$ARCH/m1pp" "$m1pp_src" "$outfile" >/dev/null 2>&1 || true + if [ -e "$outfile" ]; then + actual=$(cat "$outfile") + else + actual= + fi + elif [ -e "$m1_src" ]; then + bin=build/$ARCH/m1pp-tests/$name + if ! sh scripts/boot-build-p1.sh "$m1_src" "$bin" >/dev/null 2>&1; then + report "$label" FAIL + sh scripts/boot-build-p1.sh "$m1_src" "$bin" 2>&1 \ + | sed 's/^/ /' >&2 || true + continue + fi + actual=$("./$bin" 2>&1 || true) + else + echo " SKIP $name (no .M1 or .M1pp)" + continue + fi + + if [ "$actual" = "$expected_content" ]; then + report "$label" PASS + else + report "$label" FAIL + show_diff "$expected_content" "$actual" + fi + done +} + +## --- p1 suite ----------------------------------------------------------- + +run_p1_suite() { + if [ -z "$NAMES" ]; then + NAMES=$(discover tests/P1 P1pp) + fi + for name in $NAMES; do + fixture=tests/P1/$name.P1pp + expected=tests/P1/$name.expected + if [ ! -e "$fixture" ]; then echo " SKIP $name (no .P1pp)"; continue; fi + if [ ! -e "$expected" ]; then echo " SKIP $name (no .expected)"; continue; fi + expected_content=$(cat "$expected") + + label="[$ARCH] $name" + bin=build/$ARCH/p1-tests/$name + if ! sh scripts/boot-build-p1pp.sh "$fixture" "$bin" >/dev/null 2>&1; then + report "$label" FAIL + sh scripts/boot-build-p1pp.sh "$fixture" "$bin" 2>&1 \ + | sed 's/^/ /' >&2 || true + continue + fi + actual=$("./$bin" 2>&1 || true) + if [ "$actual" = "$expected_content" ]; then + report "$label" PASS + else + report "$label" FAIL + show_diff "$expected_content" "$actual" + fi + done +} + +## --- scheme1 suite ------------------------------------------------------ + +run_scheme1_suite() { + if [ -z "$NAMES" ]; then + NAMES=$(discover tests/scheme1 scm) + fi + for name in $NAMES; do + fixture=tests/scheme1/$name.scm + expected_stdout_file=tests/scheme1/$name.expected + expected_exit_file=tests/scheme1/$name.expected-exit + + if [ ! -e "$fixture" ]; then echo " SKIP $name (no .scm)"; continue; fi + if [ -e "$expected_stdout_file" ]; then + expected_stdout=$(cat "$expected_stdout_file") + else + expected_stdout= + fi + if [ -e "$expected_exit_file" ]; then + expected_exit=$(cat "$expected_exit_file") + else + expected_exit=0 + fi + + label="[$ARCH] $name" + bin=build/$ARCH/scheme1 + if [ ! -x "$bin" ]; then + report "$label" FAIL + echo " (missing $bin -- run 'make scheme1 ARCH=$ARCH')" >&2 + continue + fi + + tmp_stdout=$(mktemp) + if sh scripts/boot-run-scheme1.sh "$fixture" >"$tmp_stdout" 2>&1; then + actual_exit=0 + else + actual_exit=$? + fi + actual_stdout=$(cat "$tmp_stdout") + rm -f "$tmp_stdout" + + if [ "$actual_stdout" = "$expected_stdout" ] \ + && [ "$actual_exit" = "$expected_exit" ]; then + report "$label" PASS + else + report "$label" FAIL + if [ "$actual_stdout" != "$expected_stdout" ]; then + show_diff "$expected_stdout" "$actual_stdout" + fi + if [ "$actual_exit" != "$expected_exit" ]; then + echo " exit: expected $expected_exit, got $actual_exit" + fi + fi + done +} + +## --- cc-* suites -------------------------------------------------------- + +_cc_check() { + lbl=$1; exp_out=$2; exp_exit=$3; act_out=$4; act_exit=$5 + if [ "$act_out" = "$exp_out" ] && [ "$act_exit" = "$exp_exit" ]; then + report "$lbl" PASS + else + report "$lbl" FAIL + if [ "$act_out" != "$exp_out" ]; then show_diff "$exp_out" "$act_out"; fi + if [ "$act_exit" != "$exp_exit" ]; then + echo " exit: expected $exp_exit, got $act_exit" + fi + fi +} + +# _cc_unit_suite <suite-name> <expected-ext> <layer-list> +_cc_unit_suite() { + suite=$1; ext=$2; layers=$3 + [ -n "$NAMES" ] || NAMES=$(discover tests/$suite scm) + for name in $NAMES; do + fixture=tests/$suite/$name.scm + [ -e "$fixture" ] || { echo " SKIP $name (no .scm)"; continue; } + if [ -e "tests/$suite/$name.$ext" ]; then + expout=$(cat "tests/$suite/$name.$ext") + else + expout= + fi + if [ -e "tests/$suite/$name.expected-exit" ]; then + expexit=$(cat "tests/$suite/$name.expected-exit") + else + expexit=0 + fi + tmp=$(mktemp) + # Wrap in `sh -c` so catm failure doesn't abort under set -e and + # scheme1 still runs (matches host runner's prior behavior). + if sh -c " + build/$ARCH/tools/catm /tmp/cc-test.scm $layers $fixture + exec build/$ARCH/scheme1 /tmp/cc-test.scm + " >"$tmp" 2>&1; then + act_exit=0 + else + act_exit=$? + fi + act_out=$(cat "$tmp"); rm -f "$tmp" + _cc_check "[$ARCH] $suite/$name" "$expout" "$expexit" "$act_out" "$act_exit" + done +} + +run_cc_util_suite() { + _cc_unit_suite cc-util expected "scheme1/prelude.scm cc/util.scm" +} + +# _cc_pipeline_suite <suite-name> <expected-ext> <layers> +_cc_pipeline_suite() { + suite=$1; ext=$2; layers=$3 + [ -n "$NAMES" ] || NAMES=$(discover tests/$suite c) + for name in $NAMES; do + fixture=tests/$suite/$name.c + [ -e "$fixture" ] || { echo " SKIP $name (no .c)"; continue; } + if [ -e "tests/$suite/$name.$ext" ]; then + expout=$(grep -v '^;;' "tests/$suite/$name.$ext" || true) + else + expout= + fi + if [ -e "tests/$suite/$name.expected-exit" ]; then + expexit=$(cat "tests/$suite/$name.expected-exit") + else + expexit=0 + fi + tmp=$(mktemp) + if sh -c " + build/$ARCH/tools/catm /tmp/cc-test.scm $layers + exec build/$ARCH/scheme1 /tmp/cc-test.scm $fixture + " >"$tmp" 2>&1; then + act_exit=0 + else + act_exit=$? + fi + if [ "$expexit" != "0" ]; then + act_out= + else + act_out=$(cat "$tmp") + fi + rm -f "$tmp" + _cc_check "[$ARCH] $suite/$name" "$expout" "$expexit" "$act_out" "$act_exit" + done +} + +run_cc_lex_suite() { + _cc_pipeline_suite cc-lex expected-toks \ + "scheme1/prelude.scm cc/util.scm cc/data.scm cc/lex.scm tests/cc-lex/_run-lex.scm" +} + +# Two passes: .c fixtures via the lex+pp pipeline; the lone .scm fixture +# (22-initial-defines) covers the -D mechanism the driver doesn't expose, +# so it stays on the unit-suite path. NAMES is restored between passes +# because _cc_pipeline_suite may have populated it via discovery. +run_cc_pp_suite() { + saved=$NAMES + _cc_pipeline_suite cc-pp expected-toks \ + "scheme1/prelude.scm cc/util.scm cc/data.scm cc/lex.scm cc/pp.scm tests/cc-pp/_run-pp.scm" + NAMES=$saved + _cc_unit_suite cc-pp expected \ + "scheme1/prelude.scm cc/util.scm cc/data.scm cc/pp.scm" +} + +# _cc_runtime_suite <suite-name> <fixture-ext> <layers> [<fixture-as-arg?>] +_cc_runtime_suite() { + suite=$1; fext=$2; layers=$3; arg_pass=${4:-0} + [ -n "$NAMES" ] || NAMES=$(discover tests/$suite "$fext") + for name in $NAMES; do + fixture=tests/$suite/$name.$fext + [ -e "$fixture" ] || { echo " SKIP $name (no .$fext)"; continue; } + + if [ -e tests/$suite/$name.expected ]; then + expout=$(cat tests/$suite/$name.expected) + else + expout= + fi + if [ -e tests/$suite/$name.expected-exit ]; then + expexit=$(cat tests/$suite/$name.expected-exit) + else + expexit=0 + fi + + outdir=build/$ARCH/$suite/$name + p1pp=$outdir/$name.P1pp + elf=$outdir/$name + mkdir -p "$outdir" + + if [ "$arg_pass" = "1" ]; then + cmd=" + build/$ARCH/tools/catm /tmp/cc-test.scm $layers + exec build/$ARCH/scheme1 /tmp/cc-test.scm $fixture + " + else + cmd=" + build/$ARCH/tools/catm /tmp/cc-test.scm $layers $fixture + exec build/$ARCH/scheme1 /tmp/cc-test.scm + " + fi + if ! sh -c "$cmd" >"$p1pp" 2>/dev/null; then + report "[$ARCH] $suite/$name" FAIL + echo " cg emission failed:" + sh -c "$cmd" 2>&1 >/dev/null | sed 's/^/ /' >&2 || true + continue + fi + + if ! sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" >/dev/null 2>&1; then + report "[$ARCH] $suite/$name" FAIL + echo " P1pp assemble failed:" + sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" 2>&1 \ + | sed 's/^/ /' >&2 || true + continue + fi + + tmp=$(mktemp) + if "./$elf" >"$tmp" 2>&1; then + act_exit=0 + else + act_exit=$? + fi + act_out=$(cat "$tmp"); rm -f "$tmp" + _cc_check "[$ARCH] $suite/$name" "$expout" "$expexit" "$act_out" "$act_exit" + done +} + +run_cc_cg_suite() { + _cc_runtime_suite cc-cg scm \ + "scheme1/prelude.scm cc/util.scm cc/data.scm cc/cg.scm" 0 +} + +run_cc_parse_suite() { + _cc_runtime_suite cc-parse c \ + "scheme1/prelude.scm cc/util.scm cc/data.scm cc/lex.scm cc/pp.scm cc/cg.scm cc/parse.scm tests/cc-parse/_run-parse.scm" 1 +} + +run_cc_e2e_suite() { + [ -n "$NAMES" ] || NAMES=$(discover tests/cc-e2e c) + for name in $NAMES; do + src=tests/cc-e2e/$name.c + [ -e "$src" ] || { echo " SKIP $name (no .c)"; continue; } + if [ -e tests/cc-e2e/$name.expected ]; then + expout=$(cat tests/cc-e2e/$name.expected) + else + expout= + fi + if [ -e tests/cc-e2e/$name.expected-exit ]; then + expexit=$(cat tests/cc-e2e/$name.expected-exit) + else + expexit=0 + fi + outdir=build/$ARCH/cc-e2e/$name + p1pp=$outdir/$name.P1pp + elf=$outdir/$name + mkdir -p "$outdir" + if ! build/$ARCH/scheme1 build/$ARCH/cc/cc.scm "$src" "$p1pp" \ + >/dev/null 2>&1; then + report "[$ARCH] cc-e2e/$name" FAIL + echo " cc compile failed" + continue + fi + if ! sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" >/dev/null 2>&1; then + report "[$ARCH] cc-e2e/$name" FAIL + sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" 2>&1 \ + | sed 's/^/ /' >&2 || true + continue + fi + tmp=$(mktemp) + if "./$elf" >"$tmp" 2>&1; then + act_exit=0 + else + act_exit=$? + fi + act_out=$(cat "$tmp"); rm -f "$tmp" + _cc_check "[$ARCH] cc-e2e/$name" "$expout" "$expexit" "$act_out" "$act_exit" + done +} + +case "$SUITE" in + m1pp) run_m1pp_suite ;; + p1) run_p1_suite ;; + scheme1) run_scheme1_suite ;; + cc-util) run_cc_util_suite ;; + cc-lex) run_cc_lex_suite ;; + cc-pp) run_cc_pp_suite ;; + cc-cg) run_cc_cg_suite ;; + cc-parse) run_cc_parse_suite ;; + cc-e2e) run_cc_e2e_suite ;; +esac diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh @@ -1,31 +1,35 @@ #!/bin/sh -## run-tests.sh — unified test runner for the m1pp, p1, and scheme1 suites. +## run-tests.sh — host-side dispatcher for the unified test runner. ## -## Each suite is a directory of `<name>.<ext>` fixtures with sibling -## `<name>.expected` files. The runner builds each fixture, runs it inside -## the matching busybox container (`boot2-busybox:<arch>`), and diffs -## actual against expected output. Filenames starting with `_` are skipped -## (parked, ad-hoc debugging). +## All build/run/diff work happens inside the container via +## scripts/boot-run-tests.sh: this script just starts one podman +## process per requested arch and aggregates the per-arch +## PASS/FAIL totals. Prior versions of this runner re-entered the +## container per fixture (and per build/run within a fixture); now +## a whole arch's suite is one podman invocation. +## +## The one bit of work that stays on the host is the m1pp lint +## preflight: scripts/lint.sh runs python, which the busybox +## container doesn't carry. Names that fail lint are reported here +## (FAIL + diagnostic) and excluded from the in-container batch. ## ## Suites: -## m1pp tests/M1pp/<name>.M1 — P1 program built via build-p1.sh -## for each requested arch, run in -## container, stdout diffed. -## tests/M1pp/<name>.M1pp — m1pp expander parity test: per-arch -## m1pp binary consumes the fixture -## and writes <out>; diffed. -## p1 tests/P1/<name>.P1pp — P1pp program built via build-p1pp.sh -## for each requested arch, run in -## container, stdout diffed. -## scheme1 tests/scheme1/<name>.scm — Scheme source run by the per-arch -## scheme1 binary. stdout diffed -## against <name>.expected (default -## empty); exit code diffed against -## <name>.expected-exit (default 0). +## m1pp tests/M1pp/<name>.M1 — built via boot-build-p1.sh +## and run; stdout diffed. +## tests/M1pp/<name>.M1pp — m1pp expander parity test. +## p1 tests/P1/<name>.P1pp — built via boot-build-p1pp.sh +## and run; stdout diffed. +## scheme1 tests/scheme1/<name>.scm — run by per-arch scheme1. +## cc-util tests/cc-util/<name>.scm — scheme1 prelude+util byte-diff. +## cc-lex tests/cc-lex/<name>.c — lex pipeline byte-diff. +## cc-pp tests/cc-pp/<name>.c — pp pipeline byte-diff (+ .scm). +## cc-cg tests/cc-cg/<name>.scm — cg emit -> P1pp -> ELF -> run. +## cc-parse tests/cc-parse/<name>.c — full pipeline -> ELF -> run. +## cc-e2e tests/cc-e2e/<name>.c — cc -> P1pp -> ELF -> run. ## ## All three arches by default; --arch restricts to one. ## -## Usage: scripts/run-tests.sh --suite <m1pp|p1|scheme1> [--arch ARCH] [name ...] +## Usage: scripts/run-tests.sh --suite <suite> [--arch ARCH] [name ...] set -eu @@ -73,504 +77,73 @@ run_in_container() { "boot2-busybox:$arch" "$@" } -discover() { - dir=$1; ext=$2 - ls "$dir" 2>/dev/null \ - | sed -n "s/^\\([^_][^.]*\\)\\.$ext\$/\\1/p" \ - | sort -u -} +if [ -z "$ARCH" ]; then + ARCHES="aarch64 amd64 riscv64" +else + ARCHES=$ARCH +fi PASS=0 FAIL=0 -report() { - label=$1; status=$2 - case "$status" in - PASS) PASS=$((PASS + 1));; - FAIL) FAIL=$((FAIL + 1));; - esac - echo " $status $label" -} - -show_diff() { - expected=$1; actual=$2 - echo " --- expected ---" - printf '%s\n' "$expected" | sed 's/^/ /' - echo " --- actual ---" - printf '%s\n' "$actual" | sed 's/^/ /' -} - -## --- m1pp suite --------------------------------------------------------- -## -## Caller (Make) ensures build/<arch>/m1pp expanders, build/<arch>/tools/M0, -## the per-arch image, and P1/P1-<arch>.M1 tables exist before this runs. -## .M1 fixtures are still built per-fixture inline via boot-build-p1.sh -## (per-fixture build is test work, not infrastructure). - -run_m1pp_suite() { - if [ -z "$ARCH" ]; then - ARCHES="aarch64 amd64 riscv64" - else - ARCHES=$ARCH - fi - if [ -z "$NAMES" ]; then - m1=$(discover tests/M1pp M1) - m1pp=$(discover tests/M1pp M1pp) - NAMES=$(printf '%s\n%s\n' "$m1" "$m1pp" | sort -u | tr '\n' ' ') - fi - for name in $NAMES; do - expected=tests/M1pp/$name.expected - m1_src=tests/M1pp/$name.M1 - m1pp_src=tests/M1pp/$name.M1pp - - if [ ! -e "$expected" ]; then - echo " SKIP $name (no .expected)" - continue - fi - expected_content=$(cat "$expected") - - for arch in $ARCHES; do - label="[$arch] $name" - if [ -e "$m1pp_src" ]; then - outfile=build/$arch/m1pp-out/$name - mkdir -p "$(dirname "$outfile")" - rm -f "$outfile" - run_in_container "$arch" "./build/$arch/m1pp" "$m1pp_src" "$outfile" \ - >/dev/null 2>&1 || true - actual=$([ -e "$outfile" ] && cat "$outfile" || echo "") - elif [ -e "$m1_src" ]; then - bin=build/$arch/m1pp-tests/$name - if ! ARCH=$arch sh scripts/lint.sh "$m1_src" >/dev/null 2>&1 \ - || ! run_in_container "$arch" sh scripts/boot-build-p1.sh "$m1_src" "$bin" \ - >/dev/null 2>&1; then - report "$label" FAIL - ARCH=$arch sh scripts/lint.sh "$m1_src" 2>&1 | sed 's/^/ /' >&2 || true - run_in_container "$arch" sh scripts/boot-build-p1.sh "$m1_src" "$bin" \ - 2>&1 | sed 's/^/ /' >&2 || true - continue - fi - actual=$(run_in_container "$arch" "./$bin" 2>&1 || true) - else - echo " SKIP $name (no .M1 or .M1pp)" - break - fi - - if [ "$actual" = "$expected_content" ]; then - report "$label" PASS - else - report "$label" FAIL - show_diff "$expected_content" "$actual" - fi - done - done -} - -## --- p1 suite ----------------------------------------------------------- - -run_p1_suite() { - if [ -z "$ARCH" ]; then - ARCHES="aarch64 amd64 riscv64" - else - ARCHES=$ARCH - fi +# m1pp lint preflight: lint.sh uses python (host-only). Discover the +# fixture set if --names was empty, lint each .M1 fixture, emit a +# host-side FAIL + diagnostic for any miss, write the kept name list +# to $keep_file. FAIL line goes to stdout to interleave with the +# container's PASS/FAIL output; the FAIL counter updates in-scope. +m1pp_preflight() { + arch=$1; keep_file=$2 + : > "$keep_file" if [ -z "$NAMES" ]; then - NAMES=$(discover tests/P1 P1pp) - fi - for name in $NAMES; do - fixture=tests/P1/$name.P1pp - expected=tests/P1/$name.expected - if [ ! -e "$fixture" ]; then - echo " SKIP $name (no .P1pp)"; continue - fi - if [ ! -e "$expected" ]; then - echo " SKIP $name (no .expected)"; continue - fi - expected_content=$(cat "$expected") - - for arch in $ARCHES; do - label="[$arch] $name" - bin=build/$arch/p1-tests/$name - if ! run_in_container "$arch" sh scripts/boot-build-p1pp.sh "$fixture" "$bin" \ - >/dev/null 2>&1; then - report "$label" FAIL - run_in_container "$arch" sh scripts/boot-build-p1pp.sh "$fixture" "$bin" \ - 2>&1 | sed 's/^/ /' >&2 || true - continue - fi - actual=$(run_in_container "$arch" "./$bin" 2>&1 || true) - if [ "$actual" = "$expected_content" ]; then - report "$label" PASS - else - report "$label" FAIL - show_diff "$expected_content" "$actual" - fi - done - done -} - -## --- scheme1 suite ------------------------------------------------------ -## -## Caller (Make) ensures build/<arch>/scheme1 already exists. The runner -## invokes scripts/boot-run-scheme1.sh inside the container, which catm's -## scheme1/prelude.scm in front of the .scm fixture before handing the -## combined file to the binary. stdout is diffed against <name>.expected -## (defaults to empty if absent); the exit status is diffed against -## <name>.expected-exit (defaults to 0 if absent). - -run_scheme1_suite() { - if [ -z "$ARCH" ]; then - ARCHES="aarch64 amd64 riscv64" + m1=$(ls tests/M1pp 2>/dev/null \ + | sed -n 's/^\([^_][^.]*\)\.M1$/\1/p') + m1pp_n=$(ls tests/M1pp 2>/dev/null \ + | sed -n 's/^\([^_][^.]*\)\.M1pp$/\1/p') + all=$(printf '%s\n%s\n' "$m1" "$m1pp_n" | sort -u | tr '\n' ' ') else - ARCHES=$ARCH + all=$NAMES fi - if [ -z "$NAMES" ]; then - NAMES=$(discover tests/scheme1 scm) - fi - for name in $NAMES; do - fixture=tests/scheme1/$name.scm - expected_stdout_file=tests/scheme1/$name.expected - expected_exit_file=tests/scheme1/$name.expected-exit - - if [ ! -e "$fixture" ]; then - echo " SKIP $name (no .scm)"; continue - fi - if [ -e "$expected_stdout_file" ]; then - expected_stdout=$(cat "$expected_stdout_file") - else - expected_stdout= - fi - if [ -e "$expected_exit_file" ]; then - expected_exit=$(cat "$expected_exit_file") + for name in $all; do + m1_src=tests/M1pp/$name.M1 + if [ -e "$m1_src" ] \ + && ! ARCH=$arch sh scripts/lint.sh "$m1_src" >/dev/null 2>&1; then + echo " FAIL [$arch] $name" + ARCH=$arch sh scripts/lint.sh "$m1_src" 2>&1 \ + | sed 's/^/ /' >&2 || true + FAIL=$((FAIL + 1)) else - expected_exit=0 + printf '%s ' "$name" >> "$keep_file" fi - - for arch in $ARCHES; do - label="[$arch] $name" - bin=build/$arch/scheme1 - if [ ! -x "$bin" ]; then - report "$label" FAIL - echo " (missing $bin -- run 'make scheme1 ARCH=$arch')" >&2 - continue - fi - - tmp_stdout=$(mktemp) - if run_in_container "$arch" sh scripts/boot-run-scheme1.sh "$fixture" \ - >"$tmp_stdout" 2>&1; then - actual_exit=0 - else - actual_exit=$? - fi - actual_stdout=$(cat "$tmp_stdout") - rm -f "$tmp_stdout" - - if [ "$actual_stdout" = "$expected_stdout" ] \ - && [ "$actual_exit" = "$expected_exit" ]; then - report "$label" PASS - else - report "$label" FAIL - if [ "$actual_stdout" != "$expected_stdout" ]; then - show_diff "$expected_stdout" "$actual_stdout" - fi - if [ "$actual_exit" != "$expected_exit" ]; then - echo " exit: expected $expected_exit, got $actual_exit" - fi - fi - done done } -## --- cc-* suites -------------------------------------------------------- -## -## Six suites, all sharing the same shape: catm a list of layer files -## into a per-fixture combined .scm, run scheme1 on the result, diff -## merged stdout+stderr against an expected-output file, diff exit -## status against .expected-exit (default 0). -## -## Caller (Make) ensures build/<arch>/scheme1 and build/<arch>/cc/cc.scm -## already exist. The cc-e2e suite additionally requires the P1pp -## toolchain (m1pp + tools), which the Make deps already cover. -## -## Filenames starting with `_` are skipped by `discover()` — that's how -## suite-internal driver files (_run-lex.scm, _run-pp.scm, -## _run-parse.scm) avoid being picked up as fixtures. - -# _cc_check <label> <expected-stdout> <expected-exit> <actual-stdout> <actual-exit> -_cc_check() { - lbl=$1; exp_out=$2; exp_exit=$3; act_out=$4; act_exit=$5 - if [ "$act_out" = "$exp_out" ] && [ "$act_exit" = "$exp_exit" ]; then - report "$lbl" PASS - else - report "$lbl" FAIL - if [ "$act_out" != "$exp_out" ]; then show_diff "$exp_out" "$act_out"; fi - if [ "$act_exit" != "$exp_exit" ]; then - echo " exit: expected $exp_exit, got $act_exit" +for arch in $ARCHES; do + if [ "$SUITE" = "m1pp" ]; then + keep_file=$(mktemp) + m1pp_preflight "$arch" "$keep_file" + names=$(cat "$keep_file") + rm -f "$keep_file" + # Skip the container call only when the user gave names AND + # all of them failed lint. With no names, an empty kept set + # would mean nothing to run anyway. + names_trimmed=$(echo "$names" | tr -d ' \t\n') + if [ -z "$names_trimmed" ]; then + continue fi + else + names=$NAMES fi -} - -# _cc_arches: aarch64 amd64 riscv64 by default; honors --arch=... -_cc_arches() { - if [ -z "$ARCH" ]; then echo "aarch64 amd64 riscv64" - else echo "$ARCH"; fi -} - -# _cc_unit_suite <suite-name> <expected-ext> <layer-list> -# Generic byte-diff unit-suite runner for cc-util and cc-pp's .scm -# form. Each fixture is `tests/<suite>/<name>.scm`; expected stdout -# in `tests/<suite>/<name>.<expected-ext>` (default empty); exit in -# `tests/<suite>/<name>.expected-exit` (default 0). -# cc-cg and cc-parse use _cc_runtime_suite instead — they -# compile-and-run, asserting runtime behavior. -_cc_unit_suite() { - suite=$1; ext=$2; layers=$3 - [ -n "$NAMES" ] || NAMES=$(discover tests/$suite scm) - for arch in $(_cc_arches); do - for name in $NAMES; do - fixture=tests/$suite/$name.scm - [ -e "$fixture" ] || { echo " SKIP $name (no .scm)"; continue; } - if [ -e "tests/$suite/$name.$ext" ]; then - expout=$(cat "tests/$suite/$name.$ext") - else - expout= - fi - if [ -e "tests/$suite/$name.expected-exit" ]; then - expexit=$(cat "tests/$suite/$name.expected-exit") - else - expexit=0 - fi - tmp=$(mktemp) - if run_in_container "$arch" sh -c " - build/$arch/tools/catm /tmp/cc-test.scm $layers $fixture - exec build/$arch/scheme1 /tmp/cc-test.scm - " >"$tmp" 2>&1; then - act_exit=0 - else - act_exit=$? - fi - act_out=$(cat "$tmp"); rm -f "$tmp" - _cc_check "[$arch] $suite/$name" "$expout" "$expexit" "$act_out" "$act_exit" - done - done -} - -# cc-util: scheme1 prelude + util only. -run_cc_util_suite() { - _cc_unit_suite cc-util expected "scheme1/prelude.scm cc/util.scm" -} - -# _cc_pipeline_suite <suite-name> <expected-ext> <layers> -# Byte-diff pipeline runner for cc-lex and cc-pp's .c form. -# Each fixture is tests/<suite>/<name>.c. `<layers>` ends with the -# suite-specific _run-<phase>.scm driver, which the test runner passes -# the fixture path to as argv[2]. Expected stdout in -# tests/<suite>/<name>.<expected-ext> ("|| true" filters the negative- -# fixture `;;` notes); exit in <name>.expected-exit (default 0). -# Negative fixtures (expected-exit != 0) only check the exit code. -_cc_pipeline_suite() { - suite=$1; ext=$2; layers=$3 - [ -n "$NAMES" ] || NAMES=$(discover tests/$suite c) - for arch in $(_cc_arches); do - for name in $NAMES; do - fixture=tests/$suite/$name.c - [ -e "$fixture" ] || { echo " SKIP $name (no .c)"; continue; } - if [ -e "tests/$suite/$name.$ext" ]; then - expout=$(grep -v '^;;' "tests/$suite/$name.$ext" || true) - else - expout= - fi - if [ -e "tests/$suite/$name.expected-exit" ]; then - expexit=$(cat "tests/$suite/$name.expected-exit") - else - expexit=0 - fi - tmp=$(mktemp) - if run_in_container "$arch" sh -c " - build/$arch/tools/catm /tmp/cc-test.scm $layers - exec build/$arch/scheme1 /tmp/cc-test.scm $fixture - " >"$tmp" 2>&1; then - act_exit=0 - else - act_exit=$? - fi - if [ "$expexit" != "0" ]; then - act_out= - else - act_out=$(cat "$tmp") - fi - rm -f "$tmp" - _cc_check "[$arch] $suite/$name" "$expout" "$expexit" "$act_out" "$act_exit" - done - done -} - -# cc-lex: prelude + util + data + lex + run-lex driver. -run_cc_lex_suite() { - _cc_pipeline_suite cc-lex expected-toks \ - "scheme1/prelude.scm cc/util.scm cc/data.scm cc/lex.scm tests/cc-lex/_run-lex.scm" -} - -# cc-pp: prelude + util + data + lex + pp + run-pp driver. -# Two passes: .c fixtures via the pipeline (real lex+pp); the lone .scm -# fixture (22-initial-defines) covers the -D mechanism the driver doesn't -# expose, so it stays on the unit-suite path. -run_cc_pp_suite() { - saved=$NAMES - NAMES=$saved - _cc_pipeline_suite cc-pp expected-toks \ - "scheme1/prelude.scm cc/util.scm cc/data.scm cc/lex.scm cc/pp.scm tests/cc-pp/_run-pp.scm" - NAMES=$saved - _cc_unit_suite cc-pp expected \ - "scheme1/prelude.scm cc/util.scm cc/data.scm cc/pp.scm" -} - -# _cc_runtime_suite <suite-name> <fixture-ext> <layers> [<fixture-as-arg?>] -# Runtime-validating runner for cc-cg and cc-parse. -# -# Each fixture is `tests/<suite>/<name>.<fixture-ext>`. The runner: -# 1. catms <layers> + (cg-driver) into a combined .scm in the -# container's tmpfs. -# 2. Runs scheme1 on it; captures stdout to a per-fixture .P1pp file. -# For cc-parse the .c fixture is passed as argv[2] to the driver -# (fixture-as-arg=1); for cc-cg the .scm fixture *is* the last -# layer (fixture-as-arg=0). -# 3. Assembles that .P1pp through boot-build-p1pp.sh into a native -# ELF. -# 4. Runs the ELF in the container; diffs exit code against -# <name>.expected-exit (default 0) and stdout against -# <name>.expected (default empty). -# -# Any pipeline failure (compile, assemble, run) is a FAIL with the -# first failing stage's stderr surfaced. -_cc_runtime_suite() { - suite=$1; fext=$2; layers=$3; arg_pass=${4:-0} - [ -n "$NAMES" ] || NAMES=$(discover tests/$suite "$fext") - for arch in $(_cc_arches); do - for name in $NAMES; do - fixture=tests/$suite/$name.$fext - [ -e "$fixture" ] || { echo " SKIP $name (no .$fext)"; continue; } - - expout=$([ -e tests/$suite/$name.expected ] \ - && cat tests/$suite/$name.expected || echo "") - expexit=$([ -e tests/$suite/$name.expected-exit ] \ - && cat tests/$suite/$name.expected-exit || echo 0) - - outdir=build/$arch/$suite/$name - p1pp=$outdir/$name.P1pp - elf=$outdir/$name - mkdir -p "$outdir" - - # Stage 1+2: catm + scheme1 → P1pp file. - if [ "$arg_pass" = "1" ]; then - # cc-parse: layers end with the driver; .c fixture is argv[2]. - cmd=" - build/$arch/tools/catm /tmp/cc-test.scm $layers - exec build/$arch/scheme1 /tmp/cc-test.scm $fixture - " - else - # cc-cg: the .scm fixture itself is the last layer. - cmd=" - build/$arch/tools/catm /tmp/cc-test.scm $layers $fixture - exec build/$arch/scheme1 /tmp/cc-test.scm - " - fi - if ! run_in_container "$arch" sh -c "$cmd" >"$p1pp" 2>/dev/null; then - report "[$arch] $suite/$name" FAIL - echo " cg emission failed:" - run_in_container "$arch" sh -c "$cmd" 2>&1 >/dev/null \ - | sed 's/^/ /' >&2 || true - continue - fi - - # Stage 3: P1pp → ELF. - if ! run_in_container "$arch" sh scripts/boot-build-p1pp.sh \ - "$p1pp" "$elf" >/dev/null 2>&1; then - report "[$arch] $suite/$name" FAIL - echo " P1pp assemble failed:" - run_in_container "$arch" sh scripts/boot-build-p1pp.sh \ - "$p1pp" "$elf" 2>&1 | sed 's/^/ /' >&2 || true - continue - fi - - # Stage 4: run + diff. - tmp=$(mktemp) - if run_in_container "$arch" "./$elf" >"$tmp" 2>&1; then - act_exit=0 - else - act_exit=$? - fi - act_out=$(cat "$tmp"); rm -f "$tmp" - _cc_check "[$arch] $suite/$name" "$expout" "$expexit" "$act_out" "$act_exit" - done - done -} -# cc-cg: prelude + util + data + cg + fixture; emit P1pp; build; run. -run_cc_cg_suite() { - _cc_runtime_suite cc-cg scm \ - "scheme1/prelude.scm cc/util.scm cc/data.scm cc/cg.scm" 0 -} - -# cc-parse: full pipeline through real cg + driver; emit P1pp; build; run. -run_cc_parse_suite() { - _cc_runtime_suite cc-parse c \ - "scheme1/prelude.scm cc/util.scm cc/data.scm cc/lex.scm cc/pp.scm cc/cg.scm cc/parse.scm tests/cc-parse/_run-parse.scm" 1 -} - -# cc-e2e: compile a .c through cc, assemble to ELF, run. -# Fixture: <name>.c; expected stdout in <name>.expected (default empty); -# exit in <name>.expected-exit (default 0). -run_cc_e2e_suite() { - [ -n "$NAMES" ] || NAMES=$(discover tests/cc-e2e c) - for arch in $(_cc_arches); do - for name in $NAMES; do - src=tests/cc-e2e/$name.c - [ -e "$src" ] || { echo " SKIP $name (no .c)"; continue; } - expout=$([ -e tests/cc-e2e/$name.expected ] \ - && cat tests/cc-e2e/$name.expected || echo "") - expexit=$([ -e tests/cc-e2e/$name.expected-exit ] \ - && cat tests/cc-e2e/$name.expected-exit || echo 0) - outdir=build/$arch/cc-e2e/$name - p1pp=$outdir/$name.P1pp - elf=$outdir/$name - mkdir -p "$outdir" - if ! run_in_container "$arch" sh -c \ - "build/$arch/scheme1 build/$arch/cc/cc.scm $src $p1pp" \ - >/dev/null 2>&1; then - report "[$arch] cc-e2e/$name" FAIL - echo " cc compile failed" - continue - fi - if ! run_in_container "$arch" sh scripts/boot-build-p1pp.sh \ - "$p1pp" "$elf" >/dev/null 2>&1; then - report "[$arch] cc-e2e/$name" FAIL - run_in_container "$arch" sh scripts/boot-build-p1pp.sh \ - "$p1pp" "$elf" 2>&1 | sed 's/^/ /' >&2 || true - continue - fi - tmp=$(mktemp) - if run_in_container "$arch" "./$elf" >"$tmp" 2>&1; then - act_exit=0 - else - act_exit=$? - fi - act_out=$(cat "$tmp"); rm -f "$tmp" - _cc_check "[$arch] cc-e2e/$name" "$expout" "$expexit" "$act_out" "$act_exit" - done - done -} - -case "$SUITE" in - m1pp) run_m1pp_suite ;; - p1) run_p1_suite ;; - scheme1) run_scheme1_suite ;; - cc-util) run_cc_util_suite ;; - cc-lex) run_cc_lex_suite ;; - cc-pp) run_cc_pp_suite ;; - cc-cg) run_cc_cg_suite ;; - cc-parse) run_cc_parse_suite ;; - cc-e2e) run_cc_e2e_suite ;; -esac + out=$(mktemp) + # shellcheck disable=SC2086 # $names is intentionally word-split. + run_in_container "$arch" sh scripts/boot-run-tests.sh \ + --suite="$SUITE" $names | tee "$out" + p=$(grep -c '^ PASS ' "$out" 2>/dev/null || true) + f=$(grep -c '^ FAIL ' "$out" 2>/dev/null || true) + PASS=$((PASS + ${p:-0})) + FAIL=$((FAIL + ${f:-0})) + rm -f "$out" +done echo "$PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ]