kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 79ae72f38b6d035e974765489d3e851a89196050
parent 9351e593c35aafabb76580dee163dc5e95ba5c1f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 17:43:32 -0700

test: batch path-E podman runs and cache start.o

Per-`podman run` cost on macOS is ~150 ms regardless of payload, dominated
by the client→server round-trip. test-link issued ~40 such runs and
test-cg ~190, so path E was burning ~46 s of wall-clock per full pass.

Add test/lib/exec_aarch64.sh with queue/flush/run API. Path E now links
each case inline (~25 ms) and queues the linked.exe; after the case loop,
a single `podman run -i` reads a tab-separated manifest from stdin and
loops over every queued exe inside the container, writing per-case
.rc/.out/.err into the bind-mounted build/test/ tree. Qemu hosts (and
no-runner hosts) fall back to a serial host-side loop with the same API.

Also cache start.o per harness invocation rather than recompiling it from
the same source for every case.

Net per-pass:
  test-link  E: 7884 ms → 1574 ms  (5.0×)
  test-cg    E: 37718 ms → 5929 ms (6.4×)

Output ordering: R and J still print inline per case; E PASS/FAIL lines
print together after the batch flush, prefixed by a "Running path E (N
cases batched)..." line. Pass/fail set unchanged.

Diffstat:
Mtest/cg/run.sh | 109++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Atest/lib/exec_aarch64.sh | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/link/run.sh | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mtest/parse/run.sh | 98+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
4 files changed, 469 insertions(+), 160 deletions(-)

diff --git a/test/cg/run.sh b/test/cg/run.sh @@ -103,26 +103,14 @@ arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" -RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}" -run_aarch64() { - local exe="$1" out="$2" err="$3" - if [ $have_qemu -eq 1 ]; then - "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return - fi - if [ $have_podman -eq 1 ]; then - local dir base platform_flag=() - dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" - # `--platform linux/arm64` triggers a registry manifest lookup (~30s) - # even when the local image is already arm64. Only pass it on hosts - # where the podman machine isn't natively arm64. - [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64) - podman run --rm "${platform_flag[@]}" --net=none \ - -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err" - RUN_RC=$?; return - fi - RUN_RC=127 -} +# Shared aarch64 exec helper — see test/lib/exec_aarch64.sh. Path E queues +# each linked.exe and we drain the queue in a single batched podman run +# after the case loop, amortizing the per-launch podman overhead across +# all ~200 cg cases. +EXEC_AARCH64_MOUNT_ROOT="$BUILD_DIR" +# shellcheck source=../lib/exec_aarch64.sh +source "$ROOT/test/lib/exec_aarch64.sh" # ---- build harness binaries ------------------------------------------------ @@ -221,12 +209,31 @@ else "$(color_yel warn)" "$BUILD_DIR/cg-check-dwarf.err" >&2 fi +# Cached start.o — every case used to recompile this from the same source +# (~40 ms × N cases). Build it once for the whole harness run. +START_OBJ="$BUILD_DIR/cg_start.o" +have_start_obj=0 +if [ $have_clang_cross -eq 1 ]; then + if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie \ + -c "$LINK_TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then + have_start_obj=1 + fi +fi + printf 'Running cases...\n' # ---- per-case loop --------------------------------------------------------- CASES="$($CG_RUNNER --list)" +# Path E result bookkeeping. We queue exes during the main loop and verify +# after a single batched podman flush. +E_NAMES=() +E_WORK=() +E_LINK_MS=() +E_EXPECTED=() + for name in $CASES; do [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue work="$BUILD_DIR/cg/$name" @@ -286,33 +293,30 @@ for name in $CASES; do fi fi - # ---- Path E: link + qemu ---------------------------------------------- + # ---- Path E: link + (batched) qemu/podman ------------------------------ + # Link now (per case); the run is queued for the post-loop flush. if [ $RUN_E -eq 1 ]; then - if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ]; then + if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ] \ + && [ $have_start_obj -eq 1 ]; then t0=$(now_ms) - start_obj="$work/start.o" - clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ - -fno-PIC -fno-pie \ - -c "$LINK_TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null - exe="$work/linked.exe" - if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \ + if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" \ >"$work/exec_link.out" 2>"$work/exec_link.err"; then dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) note_fail "$name/E (link failed, ${dt}ms)" elif [ $have_runner -eq 1 ]; then - run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" - dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - if [ "$RUN_RC" -eq "$expected_byte" ]; then - note_pass "$name/E (${dt}ms)" - else - note_fail "$name/E (expected $expected_byte got $RUN_RC, ${dt}ms)" - fi + link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt )) + E_NAMES+=("$name") + E_WORK+=("$work") + E_LINK_MS+=("$link_dt") + E_EXPECTED+=("$expected_byte") + exec_aarch64_queue "$name" "$exe" \ + "$work/exec.out" "$work/exec.err" "$work/exec.rc" else note_skip "$name/E" "no qemu/podman" fi else - note_skip "$name/E" "no link-exe-runner or aarch64 clang" + note_skip "$name/E" "no link-exe-runner, aarch64 clang, or start.o" fi fi @@ -359,11 +363,42 @@ for name in $CASES; do fi done +# ---- batched path-E flush + verification ----------------------------------- +# Run every queued case in a single podman invocation, then iterate the +# queue to read each exit code and emit PASS/FAIL. + +T_E_BATCH=0 +if [ "$(exec_aarch64_queue_size)" -gt 0 ]; then + printf 'Running path E (%d cases batched)...\n' "$(exec_aarch64_queue_size)" + t0=$(now_ms) + exec_aarch64_flush + T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH )) + + i=0 + while [ $i -lt ${#E_NAMES[@]} ]; do + name="${E_NAMES[$i]}" + work="${E_WORK[$i]}" + link_dt="${E_LINK_MS[$i]}" + expected_byte="${E_EXPECTED[$i]}" + if [ ! -f "$work/exec.rc" ]; then + note_fail "$name/E (no rc; podman batch did not produce results)" + else + RUN_RC="$(cat "$work/exec.rc")" + if [ "$RUN_RC" -eq "$expected_byte" ]; then + note_pass "$name/E (link ${link_dt}ms)" + else + note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)" + fi + fi + i=$((i+1)) + done +fi + # ---- summary --------------------------------------------------------------- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -printf 'Time: D=%dms R=%dms E=%dms J=%dms W=%dms\n' \ - "$T_D" "$T_R" "$T_E" "$T_J" "$T_W" +printf 'Time: D=%dms R=%dms E=%dms (batch %dms) J=%dms W=%dms\n' \ + "$T_D" "$T_R" "$T_E" "$T_E_BATCH" "$T_J" "$T_W" if [ ${#FAIL_NAMES[@]} -gt 0 ]; then printf 'Failed:\n' diff --git a/test/lib/exec_aarch64.sh b/test/lib/exec_aarch64.sh @@ -0,0 +1,132 @@ +# test/lib/exec_aarch64.sh — shared aarch64 exec helper for test harnesses. +# +# Sourced by test/{link,cg,parse}/run.sh. Provides two execution modes: +# +# exec_aarch64_run — synchronous one-shot. Used for kernel images and +# negative-test bad/ cases that need an immediate rc. +# exec_aarch64_queue — append to internal queue. +# exec_aarch64_flush — drain the queue. On podman hosts, single +# `podman run` loops over all queued exes inside +# the container, amortizing the ~150 ms per-launch +# podman client round-trip across the whole suite. +# On qemu hosts (or when no runner is available), +# falls back to a serial loop on the host. +# +# Caller contract: +# - Sets the following before sourcing or calling: have_qemu, have_podman, +# is_aarch64, QEMU_BIN. (Already detected by every harness.) +# - For batched podman, sets EXEC_AARCH64_MOUNT_ROOT to a host directory +# that contains *every* exe / out / err / rc path that will be queued. +# The same path is bind-mounted at the same path inside the container, +# so no path translation is needed in the manifest. +# - Optional: RUN_AARCH64_IMAGE overrides the container image +# (default alpine:latest, matching the prior inline implementation). + +# Internal queue arrays. Bash arrays are local to the sourcing shell. +EXEC_AARCH64_NAMES=() +EXEC_AARCH64_EXES=() +EXEC_AARCH64_OUTS=() +EXEC_AARCH64_ERRS=() +EXEC_AARCH64_RCS=() + +exec_aarch64_supported() { + [ "${have_qemu:-0}" -eq 1 ] || [ "${have_podman:-0}" -eq 1 ] +} + +# Synchronous run; sets RUN_RC. Mirrors the prior run_aarch64() in each harness. +exec_aarch64_run() { + local exe="$1" out="$2" err="$3" + if [ "${have_qemu:-0}" -eq 1 ]; then + "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return + fi + if [ "${have_podman:-0}" -eq 1 ]; then + local dir base platform_flag=() + dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" + # `--platform linux/arm64` triggers a registry manifest lookup + # (~30 s) even when the local image is already arm64. Only pass it + # on hosts where the podman machine isn't natively arm64. + [ "${is_aarch64:-0}" -eq 0 ] && platform_flag=(--platform linux/arm64) + podman run --rm "${platform_flag[@]}" --net=none \ + -v "$dir":/work:Z -w /work \ + "${RUN_AARCH64_IMAGE:-alpine:latest}" "./$base" \ + >"$out" 2>"$err" + RUN_RC=$?; return + fi + RUN_RC=127 +} + +# Queue an exe to run later. Stored verbatim; flush writes <rc_file> with +# the integer exit code, and routes stdout/stderr to <out_file>/<err_file>. +exec_aarch64_queue() { + EXEC_AARCH64_NAMES+=("$1") + EXEC_AARCH64_EXES+=("$2") + EXEC_AARCH64_OUTS+=("$3") + EXEC_AARCH64_ERRS+=("$4") + EXEC_AARCH64_RCS+=("$5") +} + +exec_aarch64_queue_size() { echo "${#EXEC_AARCH64_EXES[@]}"; } + +# Drain the queue. Reads back via the .rc files written into the bind-mounted +# tree; callers iterate their own bookkeeping arrays after this returns. +exec_aarch64_flush() { + local n="${#EXEC_AARCH64_EXES[@]}" + [ "$n" -eq 0 ] && return 0 + + if [ "${have_qemu:-0}" -eq 1 ]; then + local i=0 + while [ $i -lt "$n" ]; do + "$QEMU_BIN" "${EXEC_AARCH64_EXES[$i]}" \ + >"${EXEC_AARCH64_OUTS[$i]}" 2>"${EXEC_AARCH64_ERRS[$i]}" + echo $? >"${EXEC_AARCH64_RCS[$i]}" + i=$((i+1)) + done + elif [ "${have_podman:-0}" -eq 1 ]; then + if [ -z "${EXEC_AARCH64_MOUNT_ROOT:-}" ]; then + echo "exec_aarch64_flush: EXEC_AARCH64_MOUNT_ROOT must be set" >&2 + return 2 + fi + local platform_flag=() + [ "${is_aarch64:-0}" -eq 0 ] && platform_flag=(--platform linux/arm64) + local i=0 + # Manifest is fed via stdin; one tab-separated line per case. + # The in-container shell loop runs each exe and writes its rc to + # the bind-mounted .rc file, so the host can poll results after + # `podman run` returns. stdout/stderr from individual exes go to + # their .out/.err files inside the same mount. + { + while [ $i -lt "$n" ]; do + printf '%s\t%s\t%s\t%s\n' \ + "${EXEC_AARCH64_EXES[$i]}" \ + "${EXEC_AARCH64_OUTS[$i]}" \ + "${EXEC_AARCH64_ERRS[$i]}" \ + "${EXEC_AARCH64_RCS[$i]}" + i=$((i+1)) + done + } | podman run -i --rm "${platform_flag[@]}" --net=none \ + -v "$EXEC_AARCH64_MOUNT_ROOT":"$EXEC_AARCH64_MOUNT_ROOT":Z \ + "${RUN_AARCH64_IMAGE:-alpine:latest}" \ + /bin/sh -c ' +set -u +while IFS=" " read -r exe out err rc; do + "$exe" >"$out" 2>"$err" + echo $? >"$rc" +done +' + else + # No runner: mark each as 127, matching the prior fallback. + local i=0 + while [ $i -lt "$n" ]; do + : >"${EXEC_AARCH64_OUTS[$i]}" + : >"${EXEC_AARCH64_ERRS[$i]}" + echo 127 >"${EXEC_AARCH64_RCS[$i]}" + i=$((i+1)) + done + fi + + EXEC_AARCH64_NAMES=() + EXEC_AARCH64_EXES=() + EXEC_AARCH64_OUTS=() + EXEC_AARCH64_ERRS=() + EXEC_AARCH64_RCS=() +} diff --git a/test/link/run.sh b/test/link/run.sh @@ -29,6 +29,11 @@ # gc_present — one symbol per line; verified present post-link # (jit_runner --check-present for J; readelf -s for E) # archive_b — package b.o into b.a; content "demand" or "whole" +# linker_script — basename of an .lds file in the case dir; passed via +# --linker-script to both runners +# kernel_image — empty marker; case is a freestanding kernel image. +# Skips paths R and J; on E, runs the linked exe via +# qemu-system-aarch64 -kernel … with semihosting. # # Filtering: # ./run.sh [name_filter] [paths] @@ -112,26 +117,13 @@ arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" -RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}" -run_aarch64() { - local exe="$1" out="$2" err="$3" - if [ $have_qemu -eq 1 ]; then - "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return - fi - if [ $have_podman -eq 1 ]; then - local dir base platform_flag=() - dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" - # `--platform linux/arm64` triggers a registry manifest lookup (~30s) - # even when the local image is already arm64. Only pass it on hosts - # where the podman machine isn't natively arm64. - [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64) - podman run --rm "${platform_flag[@]}" --net=none \ - -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err" - RUN_RC=$?; return - fi - RUN_RC=127 -} +# Shared aarch64 exec helper. Path E queues each linked.exe and we drain +# all cases in a single `podman run` after the main loop — amortizes the +# ~150 ms per-launch podman client overhead across the whole suite. +EXEC_AARCH64_MOUNT_ROOT="$BUILD_DIR" +# shellcheck source=../lib/exec_aarch64.sh +source "$ROOT/test/lib/exec_aarch64.sh" # ---- locate harness binaries ------------------------------------------------ # The Makefile's `test-link` target builds these as proper Make targets so @@ -166,8 +158,37 @@ if [ $is_aarch64 -eq 1 ]; then fi fi +# Cached start.o — every case used to recompile this from the same source +# (~40 ms × N cases). Build it once for the whole harness run. +START_OBJ="$BUILD_DIR/link_start.o" +have_start_obj=0 +if [ $have_clang_cross -eq 1 ]; then + if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie \ + -c "$TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then + have_start_obj=1 + fi +fi + printf 'Running cases...\n' +# Path E result bookkeeping. We queue each linked.exe during the main loop +# and drain them all in one podman invocation after the loop, then verify +# the recorded exit codes / symbol checks here. +# +# bad/ cases are NOT queued: their linker is expected to fail, so the +# podman runner never gets invoked. +# +# The kernel_image case is also not queued: it uses qemu-system-aarch64 +# rather than the user-mode runner and runs synchronously. +E_NAMES=() +E_WORK=() +E_EXE=() +E_LINK_MS=() +E_EXPECTED=() +E_GC_ABSENT_LIST=() # newline-joined per case (empty if none) +E_GC_PRESENT_LIST=() + # ---- per-case loop --------------------------------------------------------- for case_dir in "$TEST_DIR/cases"/*/; do @@ -219,12 +240,21 @@ for case_dir in "$TEST_DIR/cases"/*/; do done < "$case_dir/cflags" fi - # Collect source files + # Collect source files (.c and .S; clang -c accepts both) tu_srcs=() - for f in "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do + for f in "$case_dir/entry.S" "$case_dir/a.S" "$case_dir/b.S" \ + "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do [ -f "$f" ] && tu_srcs+=("$f") done + # Linker script + kernel-image markers + linker_script_file="" + if [ -f "$case_dir/linker_script" ]; then + linker_script_file="$case_dir/$(cat "$case_dir/linker_script" | tr -d '[:space:]')" + fi + kernel_image=0 + [ -f "$case_dir/kernel_image" ] && kernel_image=1 + # ---- compile with clang cross ------------------------------------------ if [ $have_clang_cross -eq 0 ]; then note_skip "$name/R" "no aarch64 clang" @@ -235,7 +265,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do obj_files=(); compile_ok=1 for src in "${tu_srcs[@]}"; do - base="$(basename "$src" .c)" + base="$(basename "$src")"; base="${base%.c}"; base="${base%.S}" obj="$work/${base}.o" if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \ -fno-PIC -fno-pie -fcommon \ @@ -278,7 +308,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do done # ---- Path R: roundtrip -------------------------------------------------- - if [ $jit_only -eq 0 ] && [ $RUN_R -eq 1 ]; then + if [ $jit_only -eq 0 ] && [ $RUN_R -eq 1 ] && [ $kernel_image -eq 0 ]; then if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then t0=$(now_ms) r_ok=1 @@ -307,63 +337,73 @@ for case_dir in "$TEST_DIR/cases"/*/; do fi # ---- Path E: exec ------------------------------------------------------- + # Two stages: link now (per case), then run later in a single batched + # podman invocation. Kernel-image cases are an exception — they use + # qemu-system-aarch64 and run inline. if [ $jit_only -eq 0 ] && [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ]; then t0=$(now_ms) - # Compile start.o - start_obj="$work/start.o" - clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ - -fno-PIC -fno-pie \ - -c "$TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null + script_flags=() + if [ -n "$linker_script_file" ]; then + script_flags=(--linker-script "$linker_script_file") + fi exe="$work/linked.exe" - link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" -o "$exe" \ - "${link_obj_files[@]}" "$start_obj" "${link_arc_flags[@]}") + if [ $kernel_image -eq 1 ]; then + # Freestanding kernel image: no harness start.o, no startup + # crt; the case's own entry.S is the program entry. + link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \ + "${script_flags[@]}" -o "$exe" \ + "${link_obj_files[@]}" "${link_arc_flags[@]}") + elif [ $have_start_obj -eq 1 ]; then + link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \ + "${script_flags[@]}" -o "$exe" \ + "${link_obj_files[@]}" "$START_OBJ" "${link_arc_flags[@]}") + else + note_skip "$name/E" "no cached start.o" + continue + fi if ! "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) note_fail "$name/E (link failed, ${dt}ms)" - elif [ $have_runner -eq 1 ]; then - run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" - dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - - # Symbol presence/absence checks via readelf -s. The cfree - # exe writer emits PHDRs only — no .symtab — so on the E path - # this check is skipped today. (The J path validates presence - # via cfree_jit_lookup, which is the authoritative check.) We - # keep the wiring here so the day cfree-link-exe gains a symtab - # the verification activates without further harness changes. - e_ok=1 - if [ "$RUN_RC" -ne "$expected" ]; then e_ok=0; fi - if [ $e_ok -eq 1 ] && [ $have_readelf -eq 1 ] && \ - { [ ${#gc_absent_syms[@]} -gt 0 ] || \ - [ ${#gc_present_syms[@]} -gt 0 ]; }; then - "$READELF_BIN" -sW "$exe" >"$work/exec_syms.txt" 2>/dev/null - if [ -s "$work/exec_syms.txt" ]; then - for sym in "${gc_absent_syms[@]}"; do - if awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \ - "$work/exec_syms.txt"; then - e_ok=0 - note_fail "$name/E gc_absent: '$sym' present" - break - fi - done - if [ $e_ok -eq 1 ]; then - for sym in "${gc_present_syms[@]}"; do - if ! awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \ - "$work/exec_syms.txt"; then - e_ok=0 - note_fail "$name/E gc_present: '$sym' missing" - break - fi - done - fi + elif [ $kernel_image -eq 1 ]; then + QEMU_KERNEL_BIN="$(command -v qemu-system-aarch64 2>/dev/null || true)" + if [ -z "$QEMU_KERNEL_BIN" ]; then + dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) + note_skip "$name/E" "no qemu-system-aarch64" + else + "$QEMU_KERNEL_BIN" -machine virt -cpu cortex-a72 \ + -kernel "$exe" -nographic \ + -semihosting-config enable=on,target=native \ + -no-reboot >"$work/exec.out" 2>"$work/exec.err" + RUN_RC=$? + dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) + if [ "$RUN_RC" -eq "$expected" ]; then + note_pass "$name/E (${dt}ms)" + else + note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)" fi fi - - if [ $e_ok -eq 1 ]; then - if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E (${dt}ms)" - else note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)"; fi - fi + elif [ $have_runner -eq 1 ]; then + # Queue for the post-loop batched flush. Per-case wall-clock + # for the run portion is amortized; we record link-time only. + link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt )) + E_NAMES+=("$name") + E_WORK+=("$work") + E_EXE+=("$exe") + E_LINK_MS+=("$link_dt") + E_EXPECTED+=("$expected") + # Newline-join the gc symbol lists so we can stash multi-element + # data inside scalar array slots (bash arrays-of-arrays don't + # exist). Empty string = no checks. + gca="" + for s in "${gc_absent_syms[@]:-}"; do [ -n "$s" ] && gca="${gca}${s}"$'\n'; done + gcp="" + for s in "${gc_present_syms[@]:-}"; do [ -n "$s" ] && gcp="${gcp}${s}"$'\n'; done + E_GC_ABSENT_LIST+=("$gca") + E_GC_PRESENT_LIST+=("$gcp") + exec_aarch64_queue "$name" "$exe" \ + "$work/exec.out" "$work/exec.err" "$work/exec.rc" else note_skip "$name/E" "no runner (qemu/podman)" fi @@ -374,10 +414,11 @@ for case_dir in "$TEST_DIR/cases"/*/; do fi # ---- Path J: JIT -------------------------------------------------------- - if [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ]; then + if [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ] && [ $kernel_image -eq 0 ]; then t0=$(now_ms) jit_cmd=("$JIT_RUNNER" "${extra_flags[@]}") [ $use_resolver -eq 1 ] && jit_cmd+=(--use-resolver) + [ -n "$linker_script_file" ] && jit_cmd+=(--linker-script "$linker_script_file") jit_cmd+=("${link_obj_files[@]}" "${link_arc_flags[@]}") "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err" @@ -410,7 +451,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt )) if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J (${dt}ms)" else note_fail "$name/J (expected $expected, got $j_rc, ${dt}ms)"; fi - elif [ $RUN_J -eq 1 ]; then + elif [ $RUN_J -eq 1 ] && [ $kernel_image -eq 0 ]; then note_skip "$name/J" "no jit-runner (not aarch64 host or build failed)" fi @@ -458,25 +499,26 @@ for case_dir in "$TEST_DIR/bad"/*/; do continue fi - # Path E + # Path E (negative). Linker is expected to fail; no exe is run, so + # this case never enters the podman queue. if [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ]; then - start_obj="$work/start.o" - clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ - -fno-PIC -fno-pie \ - -c "$TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null - exe="$work/linked.exe" - if "$LINK_EXE_RUNNER" -o "$exe" "${obj_files[@]}" "$start_obj" \ - >"$work/link.out" 2>"$work/link.err"; then - note_fail "$name/E (linker succeeded; expected non-zero exit)" + if [ $have_start_obj -eq 0 ]; then + note_skip "$name/E" "no cached start.o" else - rc=$? - if [ $rc -ge 128 ]; then - note_fail "$name/E (linker died via signal $((rc-128)))" - elif ! grep -qF -- "$expect" "$work/link.err"; then - note_fail "$name/E (stderr did not contain: $expect)" - sed 's/^/ | /' "$work/link.err" + exe="$work/linked.exe" + if "$LINK_EXE_RUNNER" -o "$exe" "${obj_files[@]}" "$START_OBJ" \ + >"$work/link.out" 2>"$work/link.err"; then + note_fail "$name/E (linker succeeded; expected non-zero exit)" else - note_pass "$name/E" + rc=$? + if [ $rc -ge 128 ]; then + note_fail "$name/E (linker died via signal $((rc-128)))" + elif ! grep -qF -- "$expect" "$work/link.err"; then + note_fail "$name/E (stderr did not contain: $expect)" + sed 's/^/ | /' "$work/link.err" + else + note_pass "$name/E" + fi fi fi elif [ $RUN_E -eq 1 ]; then @@ -503,11 +545,81 @@ for case_dir in "$TEST_DIR/bad"/*/; do fi done +# ---- batched path-E flush + verification ----------------------------------- +# Run every queued user case in a single podman invocation, then iterate +# the queue to read each exit code and emit PASS/FAIL. R and J results +# already printed inline above; E results print together here. + +T_E_BATCH=0 +if [ "$(exec_aarch64_queue_size)" -gt 0 ]; then + printf 'Running path E (%d cases batched)...\n' "$(exec_aarch64_queue_size)" + t0=$(now_ms) + exec_aarch64_flush + T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH )) + + i=0 + while [ $i -lt ${#E_NAMES[@]} ]; do + name="${E_NAMES[$i]}" + work="${E_WORK[$i]}" + exe="${E_EXE[$i]}" + link_dt="${E_LINK_MS[$i]}" + expected="${E_EXPECTED[$i]}" + + if [ ! -f "$work/exec.rc" ]; then + note_fail "$name/E (no rc; podman batch did not produce results)" + i=$((i+1)); continue + fi + RUN_RC="$(cat "$work/exec.rc")" + + e_ok=1 + if [ "$RUN_RC" -ne "$expected" ]; then e_ok=0; fi + + # Symbol presence/absence checks via readelf -s. The cfree exe + # writer emits PHDRs only — no .symtab — so today this check is a + # no-op for path E (J is the authoritative validator). Wired here + # so it activates as soon as cfree-link-exe gains a symtab. + if [ $e_ok -eq 1 ] && [ $have_readelf -eq 1 ] && \ + { [ -n "${E_GC_ABSENT_LIST[$i]}" ] || \ + [ -n "${E_GC_PRESENT_LIST[$i]}" ]; }; then + "$READELF_BIN" -sW "$exe" >"$work/exec_syms.txt" 2>/dev/null + if [ -s "$work/exec_syms.txt" ]; then + while IFS= read -r sym; do + [ -z "$sym" ] && continue + if awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \ + "$work/exec_syms.txt"; then + e_ok=0 + note_fail "$name/E gc_absent: '$sym' present" + break + fi + done <<< "${E_GC_ABSENT_LIST[$i]}" + if [ $e_ok -eq 1 ]; then + while IFS= read -r sym; do + [ -z "$sym" ] && continue + if ! awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \ + "$work/exec_syms.txt"; then + e_ok=0 + note_fail "$name/E gc_present: '$sym' missing" + break + fi + done <<< "${E_GC_PRESENT_LIST[$i]}" + fi + fi + fi + + if [ $e_ok -eq 1 ]; then + if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E (link ${link_dt}ms)" + else note_fail "$name/E (expected $expected, got $RUN_RC, link ${link_dt}ms)"; fi + fi + i=$((i+1)) + done +fi + # ---- summary --------------------------------------------------------------- printf '\n' printf 'Results: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -printf 'Time: R=%dms E=%dms J=%dms\n' "$T_R" "$T_E" "$T_J" +printf 'Time: R=%dms E=%dms (batch %dms) J=%dms\n' \ + "$T_R" "$T_E" "$T_E_BATCH" "$T_J" if [ ${#FAIL_NAMES[@]} -gt 0 ]; then printf 'Failed:\n' diff --git a/test/parse/run.sh b/test/parse/run.sh @@ -97,23 +97,11 @@ arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" -RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}" -run_aarch64() { - local exe="$1" out="$2" err="$3" - if [ $have_qemu -eq 1 ]; then - "$QEMU_BIN" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return - fi - if [ $have_podman -eq 1 ]; then - local dir base platform_flag=() - dir="$(cd "$(dirname "$exe")" && pwd)"; base="$(basename "$exe")" - [ $is_aarch64 -eq 0 ] && platform_flag=(--platform linux/arm64) - podman run --rm "${platform_flag[@]}" --net=none \ - -v "$dir":/work:Z -w /work "$RUN_AARCH64_IMAGE" "./$base" >"$out" 2>"$err" - RUN_RC=$?; return - fi - RUN_RC=127 -} +# Shared aarch64 exec helper — see test/lib/exec_aarch64.sh. +EXEC_AARCH64_MOUNT_ROOT="$BUILD_DIR" +# shellcheck source=../lib/exec_aarch64.sh +source "$ROOT/test/lib/exec_aarch64.sh" # ---- build harness binaries ------------------------------------------------ @@ -179,6 +167,17 @@ if [ $is_aarch64 -eq 1 ]; then fi fi +# Cached start.o — build once for the harness run rather than per case. +START_OBJ="$BUILD_DIR/parse_start.o" +have_start_obj=0 +if [ $have_clang_cross -eq 1 ]; then + if clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie \ + -c "$LINK_TEST_DIR/harness/start.c" -o "$START_OBJ" 2>/dev/null; then + have_start_obj=1 + fi +fi + printf 'Running cases...\n' # ---- per-case loop --------------------------------------------------------- @@ -189,6 +188,12 @@ for src in "$TEST_DIR"/cases/*.c; do CASES+=("$src") done +# Path E result bookkeeping — same shape as test/cg. +E_NAMES=() +E_WORK=() +E_LINK_MS=() +E_EXPECTED=() + for src in "${CASES[@]}"; do name="$(basename "$src" .c)" [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue @@ -256,33 +261,29 @@ for src in "${CASES[@]}"; do fi fi - # ---- Path E: link + qemu/podman -------------------------------------- + # ---- Path E: link + (batched) qemu/podman ---------------------------- if [ $RUN_E -eq 1 ]; then - if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ]; then + if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ] \ + && [ $have_start_obj -eq 1 ]; then t0=$(now_ms) - start_obj="$work/start.o" - clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ - -fno-PIC -fno-pie \ - -c "$LINK_TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null - exe="$work/linked.exe" - if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \ + if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" \ >"$work/exec_link.out" 2>"$work/exec_link.err"; then dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) note_fail "$name/E (link failed, ${dt}ms)" elif [ $have_runner -eq 1 ]; then - run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" - dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - if [ "$RUN_RC" -eq "$expected_byte" ]; then - note_pass "$name/E (${dt}ms)" - else - note_fail "$name/E (expected $expected_byte got $RUN_RC, ${dt}ms)" - fi + link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt )) + E_NAMES+=("$name") + E_WORK+=("$work") + E_LINK_MS+=("$link_dt") + E_EXPECTED+=("$expected_byte") + exec_aarch64_queue "$name" "$exe" \ + "$work/exec.out" "$work/exec.err" "$work/exec.rc" else note_skip "$name/E" "no qemu/podman" fi else - note_skip "$name/E" "no link-exe-runner or aarch64 clang" + note_skip "$name/E" "no link-exe-runner, aarch64 clang, or start.o" fi fi @@ -304,11 +305,40 @@ for src in "${CASES[@]}"; do fi done +# ---- batched path-E flush + verification ----------------------------------- + +T_E_BATCH=0 +if [ "$(exec_aarch64_queue_size)" -gt 0 ]; then + printf 'Running path E (%d cases batched)...\n' "$(exec_aarch64_queue_size)" + t0=$(now_ms) + exec_aarch64_flush + T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH )) + + i=0 + while [ $i -lt ${#E_NAMES[@]} ]; do + name="${E_NAMES[$i]}" + work="${E_WORK[$i]}" + link_dt="${E_LINK_MS[$i]}" + expected_byte="${E_EXPECTED[$i]}" + if [ ! -f "$work/exec.rc" ]; then + note_fail "$name/E (no rc; podman batch did not produce results)" + else + RUN_RC="$(cat "$work/exec.rc")" + if [ "$RUN_RC" -eq "$expected_byte" ]; then + note_pass "$name/E (link ${link_dt}ms)" + else + note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)" + fi + fi + i=$((i+1)) + done +fi + # ---- summary --------------------------------------------------------------- printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -printf 'Time: D=%dms R=%dms E=%dms J=%dms\n' \ - "$T_D" "$T_R" "$T_E" "$T_J" +printf 'Time: D=%dms R=%dms E=%dms (batch %dms) J=%dms\n' \ + "$T_D" "$T_R" "$T_E" "$T_E_BATCH" "$T_J" if [ ${#FAIL_NAMES[@]} -gt 0 ]; then printf 'Failed:\n'