kit

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

commit 4bd590c72d314bb417d5eca0553be1ee2fa6f094
parent 4820b382e16649610c6a4e1e43e0380bacf3d89b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 11 May 2026 17:02:37 -0700

Parallelize parse cg and elf test harnesses

Diffstat:
Mtest/cg/run.sh | 435+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mtest/elf/run.sh | 385++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Atest/lib/parallel.sh | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/parse/run.sh | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
4 files changed, 760 insertions(+), 305 deletions(-)

diff --git a/test/cg/run.sh b/test/cg/run.sh @@ -36,6 +36,11 @@ # name_filter substring match against case name (e.g. "a01", "add") # paths subset of "DREJW" (default "DREJW") # Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS. +# +# Parallelism: +# default run in parallel with a capped CPU-count default. +# CFREE_TEST_JOBS=N run up to N cases per opt level concurrently. +# CFREE_TEST_JOBS=auto same as the default. set -u @@ -52,6 +57,9 @@ JIT_RUNNER="$BUILD_DIR/jit-runner" DWARF_CHECK="$BUILD_DIR/cg-check-dwarf" NORMALIZE="$ROOT/test/elf/normalize.py" +# shellcheck source=../lib/parallel.sh +source "$ROOT/test/lib/parallel.sh" + # CFREE_TEST_ARCH and CFREE_TEST_OBJ together select the cross-target # the harness drives the compiler at. Defaults aa64+elf preserve # historical behavior. The runners (cg-runner / link-exe-runner / @@ -117,6 +125,10 @@ now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; } mkdir -p "$BUILD_DIR" "$BUILD_DIR/cg" +TEST_JOBS="$(cfree_parallel_jobs)" || exit 2 +PARALLEL_DIR="$BUILD_DIR/cg.parallel/$$" +mkdir -p "$PARALLEL_DIR" + PASS=0; FAIL=0; SKIP=0 FAIL_NAMES=(); SKIP_NAMES=() @@ -128,6 +140,98 @@ note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } +event_path() { printf '%s/%s.%04d.events' "$PARALLEL_DIR" "$1" "$2"; } +worker_stdout_path() { printf '%s/%s.%04d.stdout' "$PARALLEL_DIR" "$1" "$2"; } +worker_stderr_path() { printf '%s/%s.%04d.stderr' "$PARALLEL_DIR" "$1" "$2"; } + +emit_event() { + local file="$1" kind="$2" + shift 2 + printf '%s' "$kind" >> "$file" + while [ $# -gt 0 ]; do + printf '\t%s' "$1" >> "$file" + shift + done + printf '\n' >> "$file" +} + +replay_events() { + local event="$1" stdout_log="$2" stderr_log="$3" + local kind a b c d e f + + if [ ! -s "$event" ]; then + note_fail "internal: missing worker result $event" + if [ -s "$stdout_log" ]; then sed 's/^/ | /' "$stdout_log"; fi + if [ -s "$stderr_log" ]; then sed 's/^/ | /' "$stderr_log"; fi + return + fi + + while IFS=$'\t' read -r kind a b c d e f; do + case "$kind" in + NOOP) : ;; + PASS) note_pass "$a" ;; + FAIL) note_fail "$a" ;; + SKIP) note_skip "$a" "$b" ;; + TIME) + case "$a" in + D) T_D=$(( T_D + b )) ;; + R) T_R=$(( T_R + b )) ;; + E) T_E=$(( T_E + b )) ;; + J) T_J=$(( T_J + b )) ;; + W) T_W=$(( T_W + b )) ;; + S) T_S=$(( T_S + b )) ;; + esac + ;; + QUEUE_E) + E_NAMES+=("$a") + E_WORK+=("$b") + E_LINK_MS+=("$c") + E_EXPECTED+=("$d") + T_E=$(( T_E + c )) + exec_target_queue "$f" "$e" "$b/linked.exe" \ + "$b/exec.out" "$b/exec.err" "$b/exec.rc" + ;; + *) + note_fail "internal: malformed worker event in $event" + ;; + esac + done < "$event" +} + +run_parallel_items() { + local layer="$1" worker="$2" + shift 2 + + local events=() + local stdout_logs=() + local stderr_logs=() + local idx=0 + local item event stdout_log stderr_log + + for item in "$@"; do + event="$(event_path "$layer" "$idx")" + stdout_log="$(worker_stdout_path "$layer" "$idx")" + stderr_log="$(worker_stderr_path "$layer" "$idx")" + : > "$event" + : > "$stdout_log" + : > "$stderr_log" + events+=("$event") + stdout_logs+=("$stdout_log") + stderr_logs+=("$stderr_log") + cfree_parallel_run "$TEST_JOBS" "$worker" "$idx" "$item" "$event" \ + > "$stdout_log" 2> "$stderr_log" + idx=$((idx+1)) + done + + cfree_parallel_wait_all || true + + idx=0 + while [ $idx -lt ${#events[@]} ]; do + replay_events "${events[$idx]}" "${stdout_logs[$idx]}" "${stderr_logs[$idx]}" + idx=$((idx+1)) + done +} + # ---- tool detection -------------------------------------------------------- have_clang_cross=0 @@ -291,189 +395,208 @@ fi CASES="$($CG_RUNNER --list)" -# Each level wraps cg-runner with --opt-level N. Level 0 drives the AArch64 -# backend directly; level >0 inserts opt_cgtarget. Cases tagged with /L<N> -# in the output when level>0 so failures localize to the level. -for OPT_LEVEL in $OPT_LEVELS; do - if [ "$OPT_LEVEL" = "0" ]; then - CG_RUN=("$CG_RUNNER") - TAG="" - WORK_SUB="cg" - else - CG_RUN=("$CG_RUNNER" "--opt-level" "$OPT_LEVEL") - TAG="/L${OPT_LEVEL}" - WORK_SUB="cg-L${OPT_LEVEL}" +run_cg_case() { + local _idx="$1" name="$2" event="$3" + local work case_arches expected expected_byte case_tag obj t0 dt + local d_rc rt r_ok r_msg exe link_dt j_rc w_rc + : "$_idx" + + work="$BUILD_DIR/$WORK_SUB/$name" + mkdir -p "$work" + + # Filter cases whose declared arch mask excludes the test arch. + # cg-runner --arches NAME prints one token per arch the case + # supports; skip if our $EXEC_ARCH isn't listed. + case_arches="$("${CG_RUN[@]}" --arches "$name" 2>/dev/null)" + if [ -n "$case_arches" ] && \ + ! printf '%s\n' "$case_arches" | grep -qx "$EXEC_ARCH"; then + emit_event "$event" NOOP + return 0 fi - printf 'Running cases (opt-level %s)...\n' "$OPT_LEVEL" - - # Path E result bookkeeping (per level — flushed at end of this iteration). - E_NAMES=() - E_WORK=() - E_LINK_MS=() - E_EXPECTED=() - - for name in $CASES; do - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - work="$BUILD_DIR/$WORK_SUB/$name" - mkdir -p "$work" - - # Filter cases whose declared arch mask excludes the test arch. - # cg-runner --arches NAME prints one token per arch the case - # supports; skip if our $EXEC_ARCH isn't listed. - case_arches="$("${CG_RUN[@]}" --arches "$name" 2>/dev/null)" - if [ -n "$case_arches" ] && \ - ! printf '%s\n' "$case_arches" | grep -qx "$EXEC_ARCH"; then - continue - fi - - expected="$("${CG_RUN[@]}" --expected "$name" 2>/dev/null)" - expected="${expected:-0}" - # Exit codes are mod 256 on POSIX; mask the expected the same way so - # negative-return cases compare correctly. - expected_byte=$(( expected & 0xff )) - - # Path E target tag. The shell drives every case at the - # (CFREE_TEST_ARCH, CFREE_TEST_OBJ)-selected target — emit panics - # on stub backends surface as case failures rather than harness - # skips, which is the multi-arch/multi-obj contract through - # Phase 2. cg-runner's --arches output is informational at this - # stage. - case_tag="$EXEC_TAG" - - # ---- Path D: in-process JIT (only when host arch == cross-target) ---- - if [ $RUN_D -eq 1 ]; then - if [ $is_native_target -eq 1 ]; then - t0=$(now_ms) - "${CG_RUN[@]}" --jit "$name" >"$work/d.out" 2>"$work/d.err" - d_rc=$? - dt=$(( $(now_ms) - t0 )); T_D=$(( T_D + dt )) - if [ "$d_rc" -eq "$expected_byte" ]; then - note_pass "$name/D${TAG} (${dt}ms)" - else - note_fail "$name/D${TAG} (expected $expected_byte got $d_rc, ${dt}ms)" - fi + expected="$("${CG_RUN[@]}" --expected "$name" 2>/dev/null)" + expected="${expected:-0}" + # Exit codes are mod 256 on POSIX; mask the expected the same way so + # negative-return cases compare correctly. + expected_byte=$(( expected & 0xff )) + + # Path E target tag. The shell drives every case at the + # (CFREE_TEST_ARCH, CFREE_TEST_OBJ)-selected target — emit panics + # on stub backends surface as case failures rather than harness + # skips, which is the multi-arch/multi-obj contract through + # Phase 2. cg-runner's --arches output is informational at this + # stage. + case_tag="$EXEC_TAG" + + # ---- Path D: in-process JIT (only when host arch == cross-target) ---- + if [ $RUN_D -eq 1 ]; then + if [ $is_native_target -eq 1 ]; then + t0=$(now_ms) + "${CG_RUN[@]}" --jit "$name" >"$work/d.out" 2>"$work/d.err" + d_rc=$? + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME D "$dt" + if [ "$d_rc" -eq "$expected_byte" ]; then + emit_event "$event" PASS "$name/D${TAG} (${dt}ms)" else - note_skip "$name/D${TAG}" "host arch != $TEST_ARCH (no native JIT)" + emit_event "$event" FAIL "$name/D${TAG} (expected $expected_byte got $d_rc, ${dt}ms)" fi + else + emit_event "$event" SKIP "$name/D${TAG}" "host arch != $TEST_ARCH (no native JIT)" fi + fi - # ---- emit (needed by R/E/J/W) ----------------------------------------- - obj="$work/$name.o" - if [ $RUN_R -eq 1 ] || [ $RUN_E -eq 1 ] || [ $RUN_J -eq 1 ] \ - || [ $RUN_W -eq 1 ]; then - if ! "${CG_RUN[@]}" --emit "$name" "$obj" 2>"$work/emit.err"; then - note_fail "$name/emit${TAG} (cg-runner --emit failed; see $work/emit.err)" - continue - fi + # ---- emit (needed by R/E/J/W) ----------------------------------------- + obj="$work/$name.o" + if [ $RUN_R -eq 1 ] || [ $RUN_E -eq 1 ] || [ $RUN_J -eq 1 ] \ + || [ $RUN_W -eq 1 ]; then + if ! "${CG_RUN[@]}" --emit "$name" "$obj" 2>"$work/emit.err"; then + emit_event "$event" FAIL "$name/emit${TAG} (cg-runner --emit failed; see $work/emit.err)" + return 0 fi + fi - # ---- Path R: ELF roundtrip -------------------------------------------- - if [ $RUN_R -eq 1 ]; then - if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then - t0=$(now_ms) - rt="$work/$name.rt.o" - r_ok=1; r_msg="" - if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt.err"; then - r_ok=0; r_msg=" (roundtrip failed)" - else - "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null - "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null - diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1 || r_ok=0 - fi - dt=$(( $(now_ms) - t0 )); T_R=$(( T_R + dt )) - if [ $r_ok -eq 1 ]; then note_pass "$name/R${TAG} (${dt}ms)" - else note_fail "$name/R${TAG}${r_msg} (${dt}ms)"; fi + # ---- Path R: ELF roundtrip -------------------------------------------- + if [ $RUN_R -eq 1 ]; then + if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then + t0=$(now_ms) + rt="$work/$name.rt.o" + r_ok=1; r_msg="" + if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt.err"; then + r_ok=0; r_msg=" (roundtrip failed)" else - note_skip "$name/R${TAG}" "missing roundtrip/readelf/python3" + "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null + "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null + diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1 || r_ok=0 fi + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME R "$dt" + if [ $r_ok -eq 1 ]; then emit_event "$event" PASS "$name/R${TAG} (${dt}ms)" + else emit_event "$event" FAIL "$name/R${TAG}${r_msg} (${dt}ms)"; fi + else + emit_event "$event" SKIP "$name/R${TAG}" "missing roundtrip/readelf/python3" fi + fi - # ---- 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 ] \ - && [ $have_start_obj -eq 1 ]; then - t0=$(now_ms) - exe="$work/linked.exe" - 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${TAG} (link failed, ${dt}ms)" - elif exec_target_supported "$case_tag"; then - 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") - # Queue with a level-tagged key so cases at different - # opt levels don't collide in the batched runner. - exec_target_queue "$case_tag" "L${OPT_LEVEL}_${name}" \ - "$exe" "$work/exec.out" "$work/exec.err" \ - "$work/exec.rc" - else - note_skip "$name/E${TAG}" "no runner for $case_tag" - fi + # ---- 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 ] \ + && [ $have_start_obj -eq 1 ]; then + t0=$(now_ms) + exe="$work/linked.exe" + if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" \ + >"$work/exec_link.out" 2>"$work/exec_link.err"; then + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME E "$dt" + emit_event "$event" FAIL "$name/E${TAG} (link failed, ${dt}ms)" + elif exec_target_supported "$case_tag"; then + link_dt=$(( $(now_ms) - t0 )) + # Queue with a level-tagged key so cases at different + # opt levels don't collide in the batched runner. + emit_event "$event" QUEUE_E "$name" "$work" "$link_dt" \ + "$expected_byte" "L${OPT_LEVEL}_${name}" "$case_tag" else - note_skip "$name/E${TAG}" "no link-exe-runner, aarch64 clang, or start.o" + emit_event "$event" SKIP "$name/E${TAG}" "no runner for $case_tag" fi + else + emit_event "$event" SKIP "$name/E${TAG}" "no link-exe-runner, aarch64 clang, or start.o" fi + fi - # ---- Path J: jit-via-file --------------------------------------------- - if [ $RUN_J -eq 1 ]; then - if [ $have_jit_runner -eq 1 ]; then - t0=$(now_ms) - "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err" - j_rc=$? - dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt )) - if [ "$j_rc" -eq "$expected_byte" ]; then - note_pass "$name/J${TAG} (${dt}ms)" - else - note_fail "$name/J${TAG} (expected $expected_byte got $j_rc, ${dt}ms)" - fi + # ---- Path J: jit-via-file --------------------------------------------- + if [ $RUN_J -eq 1 ]; then + if [ $have_jit_runner -eq 1 ]; then + t0=$(now_ms) + "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err" + j_rc=$? + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME J "$dt" + if [ "$j_rc" -eq "$expected_byte" ]; then + emit_event "$event" PASS "$name/J${TAG} (${dt}ms)" else - note_skip "$name/J${TAG}" "no jit-runner (host arch != $TEST_ARCH)" + emit_event "$event" FAIL "$name/J${TAG} (expected $expected_byte got $j_rc, ${dt}ms)" fi + else + emit_event "$event" SKIP "$name/J${TAG}" "no jit-runner (host arch != $TEST_ARCH)" fi + fi - # ---- Path W: DWARF check ---------------------------------------------- - # Cases that don't register directives produce empty stdout from - # --dwarf-checks; we silently skip those (no SKIP entry, since W is - # opt-in per case rather than per host). DWARF / opt-level - # equivalence is a Phase 5+ concern, so skip W when level > 0. - if [ $RUN_W -eq 1 ] && [ "$OPT_LEVEL" = "0" ]; then - "${CG_RUN[@]}" --dwarf-checks "$name" >"$work/w.directives" \ - 2>"$work/w.dc.err" - if [ -s "$work/w.directives" ]; then - if [ $have_dwarf_check -eq 1 ]; then - t0=$(now_ms) - "$DWARF_CHECK" "$obj" <"$work/w.directives" \ - >"$work/w.out" 2>"$work/w.err" - w_rc=$? - dt=$(( $(now_ms) - t0 )); T_W=$(( T_W + dt )) - if [ "$w_rc" -eq 0 ]; then - note_pass "$name/W (${dt}ms)" - else - note_fail "$name/W (see $work/w.out, $work/w.err; ${dt}ms)" - fi + # ---- Path W: DWARF check ---------------------------------------------- + # Cases that don't register directives produce empty stdout from + # --dwarf-checks; we silently skip those (no SKIP entry, since W is + # opt-in per case rather than per host). DWARF / opt-level + # equivalence is a Phase 5+ concern, so skip W when level > 0. + if [ $RUN_W -eq 1 ] && [ "$OPT_LEVEL" = "0" ]; then + "${CG_RUN[@]}" --dwarf-checks "$name" >"$work/w.directives" \ + 2>"$work/w.dc.err" + if [ -s "$work/w.directives" ]; then + if [ $have_dwarf_check -eq 1 ]; then + t0=$(now_ms) + "$DWARF_CHECK" "$obj" <"$work/w.directives" \ + >"$work/w.out" 2>"$work/w.err" + w_rc=$? + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME W "$dt" + if [ "$w_rc" -eq 0 ]; then + emit_event "$event" PASS "$name/W (${dt}ms)" else - note_skip "$name/W" "no cg-check-dwarf" + emit_event "$event" FAIL "$name/W (see $work/w.out, $work/w.err; ${dt}ms)" fi + else + emit_event "$event" SKIP "$name/W" "no cg-check-dwarf" fi fi + fi - # ---- Path S: asm roundtrip (phase-1 stub) ----------------------------- - # Walks .text through cfree_disasm_iter_*, reassembles via - # asm-runner --encode, byte-compares against the emitted bytes. - # Phase 1 per doc/ASM.md §5: the iterator and parse_asm are still - # stubs, so we report SKIP unconditionally when S is requested. - # When phase 3+4 land, replace this block with the real - # disasm/reassemble pipeline. - if [ $RUN_S -eq 1 ]; then - note_skip "$name/S${TAG}" "phase 1: cfree_disasm_iter_* / parse_asm are stubs" - fi + # ---- Path S: asm roundtrip (phase-1 stub) ----------------------------- + # Walks .text through cfree_disasm_iter_*, reassembles via + # asm-runner --encode, byte-compares against the emitted bytes. + # Phase 1 per doc/ASM.md §5: the iterator and parse_asm are still + # stubs, so we report SKIP unconditionally when S is requested. + # When phase 3+4 land, replace this block with the real + # disasm/reassemble pipeline. + if [ $RUN_S -eq 1 ]; then + emit_event "$event" SKIP "$name/S${TAG}" "phase 1: cfree_disasm_iter_* / parse_asm are stubs" + fi + + # W-only runs intentionally produce no output for cases without DWARF + # directives. Mark those as handled so replay can still distinguish them + # from workers that failed before writing a result. + emit_event "$event" NOOP + return 0 +} + +# Each level wraps cg-runner with --opt-level N. Level 0 drives the AArch64 +# backend directly; level >0 inserts opt_cgtarget. Cases tagged with /L<N> +# in the output when level>0 so failures localize to the level. +for OPT_LEVEL in $OPT_LEVELS; do + if [ "$OPT_LEVEL" = "0" ]; then + CG_RUN=("$CG_RUNNER") + TAG="" + WORK_SUB="cg" + else + CG_RUN=("$CG_RUNNER" "--opt-level" "$OPT_LEVEL") + TAG="/L${OPT_LEVEL}" + WORK_SUB="cg-L${OPT_LEVEL}" + fi + + printf 'Running cases (opt-level %s, %s jobs)...\n' "$OPT_LEVEL" "$TEST_JOBS" + + # Path E result bookkeeping (per level — flushed at end of this iteration). + E_NAMES=() + E_WORK=() + E_LINK_MS=() + E_EXPECTED=() + + FILTERED_CASES=() + for name in $CASES; do + [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue + FILTERED_CASES+=("$name") done + run_parallel_items "cg-L${OPT_LEVEL}" run_cg_case "${FILTERED_CASES[@]}" + # ---- batched path-E flush + verification (per level) ------------------- # Run every queued case in a single podman invocation per arch, then # iterate the queue to read each exit code and emit PASS/FAIL. diff --git a/test/elf/run.sh b/test/elf/run.sh @@ -20,6 +20,13 @@ # dependent layer to be skipped, not failed. Set CFREE_TEST_ALLOW_SKIP=1 # to allow the harness to exit 0 with skips; otherwise any skip makes # the run fail (so CI catches a silently degraded run). +# +# Parallelism: +# default run in parallel with a capped CPU-count default. +# CFREE_TEST_JOBS=N run up to N cases per layer concurrently. +# CFREE_TEST_JOBS=auto same as the default. +# The console summary is replayed by the parent in deterministic order; +# per-case command output remains in build/test/ for failure inspection. set -u @@ -29,8 +36,15 @@ BUILD_DIR="$ROOT/build/test" LIB_AR="$ROOT/build/libcfree.a" NORMALIZE="$TEST_DIR/normalize.py" +# shellcheck source=../lib/parallel.sh +source "$ROOT/test/lib/parallel.sh" + mkdir -p "$BUILD_DIR" +TEST_JOBS="$(cfree_parallel_jobs)" || exit 2 +PARALLEL_DIR="$BUILD_DIR/elf.parallel/$$" +mkdir -p "$PARALLEL_DIR" + PASS=0 FAIL=0 SKIP=0 @@ -45,6 +59,100 @@ note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } +event_path() { + printf '%s/%s.%04d.events' "$PARALLEL_DIR" "$1" "$2" +} + +worker_stdout_path() { + printf '%s/%s.%04d.stdout' "$PARALLEL_DIR" "$1" "$2" +} + +worker_stderr_path() { + printf '%s/%s.%04d.stderr' "$PARALLEL_DIR" "$1" "$2" +} + +emit_event() { + local file="$1" kind="$2" arg1="${3:-}" arg2="${4:-}" + printf '%s\t%s\t%s\n' "$kind" "$arg1" "$arg2" >> "$file" +} + +replay_events() { + local event="$1" stdout_log="$2" stderr_log="$3" + local kind arg1 arg2 + + if [ ! -s "$event" ]; then + note_fail "internal: missing worker result $event" + if [ -s "$stdout_log" ]; then sed 's/^/ | /' "$stdout_log"; fi + if [ -s "$stderr_log" ]; then sed 's/^/ | /' "$stderr_log"; fi + return + fi + + while IFS=$'\t' read -r kind arg1 arg2; do + case "$kind" in + PASS) + note_pass "$arg1" + ;; + FAIL) + note_fail "$arg1" + ;; + SKIP) + note_skip "$arg1" "$arg2" + ;; + SKIP_NA) + printf ' %s %s — N/A on %s\n' "$(color_yel SKIP-NA)" "$arg1" "$arg2" + ;; + DUMP) + if [ -f "$arg1" ]; then + if [ -n "$arg2" ] && [ "$arg2" -gt 0 ] 2>/dev/null; then + head -n "$arg2" "$arg1" | sed 's/^/ | /' + else + sed 's/^/ | /' "$arg1" + fi + else + printf ' | (missing %s)\n' "$arg1" + fi + ;; + *) + note_fail "internal: malformed worker event in $event" + ;; + esac + done < "$event" +} + +run_parallel_items() { + local layer="$1" worker="$2" + shift 2 + + local events=() + local stdout_logs=() + local stderr_logs=() + local idx=0 + local item event stdout_log stderr_log + + for item in "$@"; do + event="$(event_path "$layer" "$idx")" + stdout_log="$(worker_stdout_path "$layer" "$idx")" + stderr_log="$(worker_stderr_path "$layer" "$idx")" + : > "$event" + : > "$stdout_log" + : > "$stderr_log" + events+=("$event") + stdout_logs+=("$stdout_log") + stderr_logs+=("$stderr_log") + cfree_parallel_run "$TEST_JOBS" "$worker" "$idx" "$item" "$event" \ + > "$stdout_log" 2> "$stderr_log" + idx=$((idx+1)) + done + + cfree_parallel_wait_all || true + + idx=0 + while [ $idx -lt ${#events[@]} ]; do + replay_events "${events[$idx]}" "${stdout_logs[$idx]}" "${stderr_logs[$idx]}" + idx=$((idx+1)) + done +} + # ----- tool detection ---------------------------------------------------- have_clang=0 @@ -72,41 +180,56 @@ printf ' clang: %s\n' "$([ $have_clang -eq 1 ] && echo yes || echo no printf ' llvm-readelf: %s\n' "$([ $have_llvm_readelf -eq 1 ] && echo yes || echo no)" printf ' python3: %s\n' "$([ $have_python3 -eq 1 ] && echo yes || echo no)" printf ' cfree-roundtrip: %s\n' "$([ $roundtrip_ok -eq 1 ] && echo found || echo "MISSING — run \"make $ROUNDTRIP_BIN\"")" +printf ' parallel jobs: %s\n' "$TEST_JOBS" printf '\n' # ----- Layer A: unit/*.c ------------------------------------------------- +UNIT_CC="${CC:-clang}" +UNIT_SYSROOT_FLAGS=() +unit_sysroot="$(xcrun --show-sdk-path 2>/dev/null || true)" +[ -n "$unit_sysroot" ] && UNIT_SYSROOT_FLAGS=(-isysroot "$unit_sysroot") + +run_unit_case() { + local _idx="$1" src="$2" event="$3" + local name stem bin build_log out_log + : "$_idx" + + name="unit/$(basename "$src" .c)" + stem="$(basename "$src" .c)" + bin="$BUILD_DIR/$stem" + build_log="$BUILD_DIR/$stem.build.log" + out_log="$BUILD_DIR/$stem.out" + + if [ ! -f "$LIB_AR" ]; then + emit_event "$event" SKIP "$name" "build/libcfree.a missing" + return 0 + fi + + if ! "$UNIT_CC" -std=c11 -Wall -Wextra -Werror \ + "${UNIT_SYSROOT_FLAGS[@]}" \ + -I"$ROOT/include" -I"$ROOT/src" -I"$ROOT/test" \ + "$src" "$LIB_AR" -o "$bin" 2> "$build_log"; then + emit_event "$event" FAIL "$name (build failed; see $build_log)" + return 0 + fi + + if "$bin" > "$out_log" 2>&1; then + emit_event "$event" PASS "$name" + else + emit_event "$event" FAIL "$name" + emit_event "$event" DUMP "$out_log" + fi + return 0 +} + printf 'Layer A — unit tests\n' shopt -s nullglob unit_srcs=( "$TEST_DIR"/unit/*.c ) if [ ${#unit_srcs[@]} -eq 0 ]; then printf ' (no unit tests yet)\n' else - for src in "${unit_srcs[@]}"; do - name="unit/$(basename "$src" .c)" - if [ ! -f "$LIB_AR" ]; then - note_skip "$name" "build/libcfree.a missing" - continue - fi - bin="$BUILD_DIR/$(basename "$src" .c)" - cc="${CC:-clang}" - sysroot="$(xcrun --show-sdk-path 2>/dev/null || true)" - sysroot_flag="" - [ -n "$sysroot" ] && sysroot_flag="-isysroot $sysroot" - # shellcheck disable=SC2086 - if ! "$cc" -std=c11 -Wall -Wextra -Werror $sysroot_flag \ - -I"$ROOT/include" -I"$ROOT/src" -I"$ROOT/test" \ - "$src" "$LIB_AR" -o "$bin" 2> "$BUILD_DIR/$(basename "$src" .c).build.log"; then - note_fail "$name" - continue - fi - if "$bin" > "$BUILD_DIR/$(basename "$src" .c).out" 2>&1; then - note_pass "$name" - else - note_fail "$name" - sed 's/^/ | /' "$BUILD_DIR/$(basename "$src" .c).out" - fi - done + run_parallel_items "unit" run_unit_case "${unit_srcs[@]}" fi printf '\n' @@ -136,115 +259,139 @@ case "${CFREE_TEST_OBJ:-elf}" in *) printf 'unknown CFREE_TEST_OBJ=%s\n' "${CFREE_TEST_OBJ}" >&2; exit 2 ;; esac +CUR_TUPLE="${CFREE_TEST_ARCH:-aa64}-${CFREE_TEST_OBJ:-elf}" + +run_oracle_case() { + local _idx="$1" src="$2" event="$3" + local name targets_file applicable tuple stem wd extra_cflags tok + : "$_idx" + + name="cases/$(basename "$src" .c)" + + # Per-case applicability: a NN_name.targets file lists the + # <arch>-<obj> tuples the case applies to (one per line, or + # whitespace-separated). When the current tuple isn't listed + # the case is silently filtered out — not a skip — because it + # exercises target-specific features with no equivalent + # elsewhere (e.g. AArch64 BTI/PAC notes have no RISC-V analogue; + # ELF features have no Mach-O peer). + targets_file="${src%.c}.targets" + if [ -f "$targets_file" ]; then + applicable=0 + for tuple in $(cat "$targets_file"); do + [ "$tuple" = "$CUR_TUPLE" ] && applicable=1 + done + if [ $applicable -eq 0 ]; then + emit_event "$event" SKIP_NA "$name" "$CUR_TUPLE" + return 0 + fi + fi + + # Per-case skip reasons: + if [ $roundtrip_ok -ne 1 ]; then emit_event "$event" SKIP "$name" "cfree-roundtrip not built"; return 0; fi + if [ $have_clang -ne 1 ]; then emit_event "$event" SKIP "$name" "clang missing"; return 0; fi + if [ $have_llvm_readelf -ne 1 ]; then emit_event "$event" SKIP "$name" "llvm-readelf missing"; return 0; fi + if [ $have_python3 -ne 1 ]; then emit_event "$event" SKIP "$name" "python3 missing"; return 0; fi + + stem="$(basename "$src" .c)" + wd="$BUILD_DIR/$stem" + mkdir -p "$wd" + + # Per-case extra compiler flags: drop a NN_name.cflags file alongside + # the .c to pass additional flags (e.g. -ffunction-sections, -x c++). + extra_cflags=() + if [ -f "${src%.c}.cflags" ]; then + # Existing harness behavior tokenized this file on shell + # whitespace; keep that convention. + for tok in $(cat "${src%.c}.cflags"); do + [ -n "$tok" ] && extra_cflags+=("$tok") + done + fi + + if ! clang --target="$CLANG_TARGET" -c -O0 "${extra_cflags[@]}" \ + "$src" -o "$wd/golden.o" 2> "$wd/clang.log"; then + emit_event "$event" SKIP "$name" "clang -c failed (cross-compile not configured?)" + return 0 + fi + + if ! "$ROUNDTRIP_BIN" "$wd/golden.o" "$wd/rt.o" 2> "$wd/roundtrip.log"; then + emit_event "$event" FAIL "$name (roundtrip failed)" + emit_event "$event" DUMP "$wd/roundtrip.log" + return 0 + fi + + # Structural diff: readelf normalized. + python3 "$NORMALIZE" readelf "$wd/golden.o" > "$wd/golden.readelf" 2> /dev/null || true + python3 "$NORMALIZE" readelf "$wd/rt.o" > "$wd/rt.readelf" 2> /dev/null || true + if ! diff -u "$wd/golden.readelf" "$wd/rt.readelf" > "$wd/readelf.diff"; then + emit_event "$event" FAIL "$name (readelf diff)" + emit_event "$event" DUMP "$wd/readelf.diff" 40 + return 0 + fi + + emit_event "$event" PASS "$name" + return 0 +} + printf 'Layer B — clang-oracle cases\n' case_srcs=( "$TEST_DIR"/cases/*.c ) if [ ${#case_srcs[@]} -eq 0 ]; then printf ' (no cases yet)\n' else - for src in "${case_srcs[@]}"; do - name="cases/$(basename "$src" .c)" - # Per-case applicability: a NN_name.targets file lists the - # <arch>-<obj> tuples the case applies to (one per line, or - # whitespace-separated). When the current tuple isn't listed - # the case is silently filtered out — not a skip — because it - # exercises target-specific features with no equivalent - # elsewhere (e.g. AArch64 BTI/PAC notes have no RISC-V analogue; - # ELF features have no Mach-O peer). - targets_file="${src%.c}.targets" - cur_tuple="${CFREE_TEST_ARCH:-aa64}-${CFREE_TEST_OBJ:-elf}" - if [ -f "$targets_file" ]; then - applicable=0 - for tuple in $(cat "$targets_file"); do - [ "$tuple" = "$cur_tuple" ] && applicable=1 - done - if [ $applicable -eq 0 ]; then - printf ' %s %s — N/A on %s\n' "$(color_yel SKIP-NA)" "$name" "$cur_tuple" - continue - fi - fi - # Per-case skip reasons: - if [ $roundtrip_ok -ne 1 ]; then note_skip "$name" "cfree-roundtrip not built"; continue; fi - if [ $have_clang -ne 1 ]; then note_skip "$name" "clang missing"; continue; fi - if [ $have_llvm_readelf -ne 1 ]; then note_skip "$name" "llvm-readelf missing"; continue; fi - if [ $have_python3 -ne 1 ]; then note_skip "$name" "python3 missing"; continue; fi - - stem="$(basename "$src" .c)" - wd="$BUILD_DIR/$stem" - mkdir -p "$wd" - - # Per-case extra compiler flags: drop a NN_name.cflags file alongside - # the .c to pass additional flags (e.g. -ffunction-sections, -x c++). - extra_cflags="" - # shellcheck disable=SC2034 - [ -f "${src%.c}.cflags" ] && extra_cflags="$(cat "${src%.c}.cflags")" - - # shellcheck disable=SC2086 - if ! clang --target="$CLANG_TARGET" -c -O0 $extra_cflags "$src" -o "$wd/golden.o" \ - 2> "$wd/clang.log"; then - note_skip "$name" "clang -c failed (cross-compile not configured?)" - continue - fi - - if ! "$ROUNDTRIP_BIN" "$wd/golden.o" "$wd/rt.o" 2> "$wd/roundtrip.log"; then - note_fail "$name (roundtrip failed)" - sed 's/^/ | /' "$wd/roundtrip.log" - continue - fi - - # Structural diff: readelf normalized. - python3 "$NORMALIZE" readelf "$wd/golden.o" > "$wd/golden.readelf" 2> /dev/null || true - python3 "$NORMALIZE" readelf "$wd/rt.o" > "$wd/rt.readelf" 2> /dev/null || true - if ! diff -u "$wd/golden.readelf" "$wd/rt.readelf" > "$wd/readelf.diff"; then - note_fail "$name (readelf diff)" - head -40 "$wd/readelf.diff" | sed 's/^/ | /' - continue - fi - - note_pass "$name" - done + run_parallel_items "cases" run_oracle_case "${case_srcs[@]}" fi printf '\n' # ----- Layer C: bad/*.elf ------------------------------------------------ +run_bad_case() { + local _idx="$1" blob="$2" event="$3" + local name stem wd rc expect_file expect + : "$_idx" + + name="bad/$(basename "$blob" .elf)" + if [ $roundtrip_ok -ne 1 ]; then + emit_event "$event" SKIP "$name" "cfree-roundtrip not built" + return 0 + fi + + stem="$(basename "$blob" .elf)" + wd="$BUILD_DIR/bad_$stem" + mkdir -p "$wd" + + rc=0 + "$ROUNDTRIP_BIN" "$blob" "$wd/out.o" > "$wd/stdout.log" 2> "$wd/stderr.log" || rc=$? + + if [ $rc -eq 0 ]; then + emit_event "$event" FAIL "$name (expected nonzero exit, got 0)" + return 0 + fi + if [ $rc -ge 128 ]; then + emit_event "$event" FAIL "$name (terminated by signal $((rc - 128)) — segfault?)" + return 0 + fi + + expect_file="${blob%.elf}.expect" + if [ ! -f "$expect_file" ]; then + emit_event "$event" FAIL "$name (missing $expect_file)" + return 0 + fi + expect="$(cat "$expect_file")" + if grep -qF -- "$expect" "$wd/stderr.log"; then + emit_event "$event" PASS "$name" + else + emit_event "$event" FAIL "$name (stderr did not contain: $expect)" + emit_event "$event" DUMP "$wd/stderr.log" + fi + return 0 +} + printf 'Layer C — negative read_elf inputs\n' bad_files=( "$TEST_DIR"/bad/*.elf ) if [ ${#bad_files[@]} -eq 0 ]; then printf ' (no bad inputs yet)\n' else - for blob in "${bad_files[@]}"; do - name="bad/$(basename "$blob" .elf)" - if [ $roundtrip_ok -ne 1 ]; then note_skip "$name" "cfree-roundtrip not built"; continue; fi - - stem="$(basename "$blob" .elf)" - wd="$BUILD_DIR/bad_$stem" - mkdir -p "$wd" - - rc=0 - "$ROUNDTRIP_BIN" "$blob" "$wd/out.o" > "$wd/stdout.log" 2> "$wd/stderr.log" || rc=$? - - if [ $rc -eq 0 ]; then - note_fail "$name (expected nonzero exit, got 0)" - continue - fi - if [ $rc -ge 128 ]; then - note_fail "$name (terminated by signal $((rc - 128)) — segfault?)" - continue - fi - - expect_file="${blob%.elf}.expect" - if [ ! -f "$expect_file" ]; then - note_fail "$name (missing $expect_file)" - continue - fi - expect="$(cat "$expect_file")" - if grep -qF -- "$expect" "$wd/stderr.log"; then - note_pass "$name" - else - note_fail "$name (stderr did not contain: $expect)" - sed 's/^/ | /' "$wd/stderr.log" - fi - done + run_parallel_items "bad" run_bad_case "${bad_files[@]}" fi printf '\n' diff --git a/test/lib/parallel.sh b/test/lib/parallel.sh @@ -0,0 +1,71 @@ +# test/lib/parallel.sh - small shared helpers for parallel shell harnesses. +# +# Harnesses keep ownership of their result format and counters. This file +# only parses CFREE_TEST_JOBS and throttles background jobs so callers can +# write per-case logs and replay them serially. + +cfree_parallel_jobs() { + local requested="${CFREE_TEST_JOBS:-auto}" + local n + + case "$requested" in + ""|1) + echo 1 + return + ;; + auto) + n="$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)" + if [ -z "$n" ]; then + n="$(sysctl -n hw.ncpu 2>/dev/null || true)" + fi + case "$n" in + ''|*[!0-9]*) n=1 ;; + esac + # Keep default auto parallelism modest; these harnesses often + # launch compilers and object dumpers rather than tiny commands. + [ "$n" -gt 8 ] && n=8 + [ "$n" -lt 1 ] && n=1 + echo "$n" + return + ;; + *[!0-9]*) + printf 'invalid CFREE_TEST_JOBS=%s\n' "$requested" >&2 + return 2 + ;; + *) + [ "$requested" -lt 1 ] && requested=1 + echo "$requested" + return + ;; + esac +} + +cfree_parallel_running_count() { + jobs -pr | wc -l | tr -d '[:space:]' +} + +cfree_parallel_wait_for_slot() { + local max_jobs="$1" + + [ "$max_jobs" -le 1 ] && return 0 + while [ "$(cfree_parallel_running_count)" -ge "$max_jobs" ]; do + sleep 0.05 + done +} + +cfree_parallel_run() { + local max_jobs="$1" + shift + + if [ "$max_jobs" -le 1 ]; then + "$@" + return $? + fi + + cfree_parallel_wait_for_slot "$max_jobs" + "$@" & +} + +cfree_parallel_wait_all() { + wait +} diff --git a/test/parse/run.sh b/test/parse/run.sh @@ -26,6 +26,11 @@ # name_filter substring match against case basename # paths subset of "DREJ" (default "DREJ") # Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS. +# +# Parallelism: +# default run in parallel with a capped CPU-count default. +# CFREE_TEST_JOBS=N run up to N cases concurrently. +# CFREE_TEST_JOBS=auto same as the default. set -u @@ -41,6 +46,9 @@ LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner" JIT_RUNNER="$BUILD_DIR/jit-runner" NORMALIZE="$ROOT/test/elf/normalize.py" +# shellcheck source=../lib/parallel.sh +source "$ROOT/test/lib/parallel.sh" + # CFREE_TEST_ARCH selects the cross-target. Default aa64 preserves the # pre-multiarch behavior. The C runners read the same env via # test/lib/cfree_test_target.h. @@ -69,6 +77,10 @@ now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; } mkdir -p "$BUILD_DIR" "$BUILD_DIR/parse" +TEST_JOBS="$(cfree_parallel_jobs)" || exit 2 +PARALLEL_DIR="$BUILD_DIR/parse.parallel/$$" +mkdir -p "$PARALLEL_DIR" + PASS=0; FAIL=0; SKIP=0 FAIL_NAMES=(); SKIP_NAMES=() @@ -80,6 +92,95 @@ note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } +event_path() { printf '%s/%s.%04d.events' "$PARALLEL_DIR" "$1" "$2"; } +worker_stdout_path() { printf '%s/%s.%04d.stdout' "$PARALLEL_DIR" "$1" "$2"; } +worker_stderr_path() { printf '%s/%s.%04d.stderr' "$PARALLEL_DIR" "$1" "$2"; } + +emit_event() { + local file="$1" kind="$2" + shift 2 + printf '%s' "$kind" >> "$file" + while [ $# -gt 0 ]; do + printf '\t%s' "$1" >> "$file" + shift + done + printf '\n' >> "$file" +} + +replay_events() { + local event="$1" stdout_log="$2" stderr_log="$3" + local kind a b c d e f + + if [ ! -s "$event" ]; then + note_fail "internal: missing worker result $event" + if [ -s "$stdout_log" ]; then sed 's/^/ | /' "$stdout_log"; fi + if [ -s "$stderr_log" ]; then sed 's/^/ | /' "$stderr_log"; fi + return + fi + + while IFS=$'\t' read -r kind a b c d e f; do + case "$kind" in + PASS) note_pass "$a" ;; + FAIL) note_fail "$a" ;; + SKIP) note_skip "$a" "$b" ;; + TIME) + case "$a" in + D) T_D=$(( T_D + b )) ;; + R) T_R=$(( T_R + b )) ;; + E) T_E=$(( T_E + b )) ;; + J) T_J=$(( T_J + b )) ;; + esac + ;; + QUEUE_E) + E_NAMES+=("$a") + E_WORK+=("$b") + E_LINK_MS+=("$c") + E_EXPECTED+=("$d") + T_E=$(( T_E + c )) + exec_target_queue "$f" "$e" "$b/linked.exe" \ + "$b/exec.out" "$b/exec.err" "$b/exec.rc" + ;; + *) + note_fail "internal: malformed worker event in $event" + ;; + esac + done < "$event" +} + +run_parallel_items() { + local layer="$1" worker="$2" + shift 2 + + local events=() + local stdout_logs=() + local stderr_logs=() + local idx=0 + local item event stdout_log stderr_log + + for item in "$@"; do + event="$(event_path "$layer" "$idx")" + stdout_log="$(worker_stdout_path "$layer" "$idx")" + stderr_log="$(worker_stderr_path "$layer" "$idx")" + : > "$event" + : > "$stdout_log" + : > "$stderr_log" + events+=("$event") + stdout_logs+=("$stdout_log") + stderr_logs+=("$stderr_log") + cfree_parallel_run "$TEST_JOBS" "$worker" "$idx" "$item" "$event" \ + > "$stdout_log" 2> "$stderr_log" + idx=$((idx+1)) + done + + cfree_parallel_wait_all || true + + idx=0 + while [ $idx -lt ${#events[@]} ]; do + replay_events "${events[$idx]}" "${stdout_logs[$idx]}" "${stderr_logs[$idx]}" + idx=$((idx+1)) + done +} + # ---- tool detection (mirrors test/cg/run.sh) ------------------------------- have_clang_cross=0 @@ -199,7 +300,7 @@ if [ $have_clang_cross -eq 1 ]; then fi fi -printf 'Running cases...\n' +printf 'Running cases (%s jobs)...\n' "$TEST_JOBS" # ---- per-case loop --------------------------------------------------------- @@ -215,17 +316,28 @@ E_WORK=() E_LINK_MS=() E_EXPECTED=() +FILTERED_CASES=() for src in "${CASES[@]}"; do name="$(basename "$src" .c)" [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue + FILTERED_CASES+=("$src") +done + +run_parse_case() { + local _idx="$1" src="$2" event="$3" + local name work reason expected expected_byte obj t0 dt d_rc r_ok r_msg rt + local exe link_dt j_rc + : "$_idx" + + name="$(basename "$src" .c)" work="$BUILD_DIR/parse/$name" mkdir -p "$work" # Skip sidecar if [ -e "$TEST_DIR/cases/$name.skip" ]; then reason=$(head -n1 "$TEST_DIR/cases/$name.skip") - note_skip "$name" "$reason" - continue + emit_event "$event" SKIP "$name" "$reason" + return 0 fi # Expected exit code (default 0) @@ -241,14 +353,15 @@ for src in "${CASES[@]}"; do t0=$(now_ms) "$PARSE_RUNNER" --jit "$src" >"$work/d.out" 2>"$work/d.err" d_rc=$? - dt=$(( $(now_ms) - t0 )); T_D=$(( T_D + dt )) + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME D "$dt" if [ "$d_rc" -eq "$expected_byte" ]; then - note_pass "$name/D (${dt}ms)" + emit_event "$event" PASS "$name/D (${dt}ms)" else - note_fail "$name/D (expected $expected_byte got $d_rc, ${dt}ms)" + emit_event "$event" FAIL "$name/D (expected $expected_byte got $d_rc, ${dt}ms)" fi else - note_skip "$name/D" "host arch != $TEST_ARCH (no native JIT)" + emit_event "$event" SKIP "$name/D" "host arch != $TEST_ARCH (no native JIT)" fi fi @@ -256,8 +369,8 @@ for src in "${CASES[@]}"; do obj="$work/$name.o" if [ $RUN_R -eq 1 ] || [ $RUN_E -eq 1 ] || [ $RUN_J -eq 1 ]; then if ! "$PARSE_RUNNER" --emit "$src" "$obj" 2>"$work/emit.err"; then - note_fail "$name/emit (parse-runner --emit failed; see $work/emit.err)" - continue + emit_event "$event" FAIL "$name/emit (parse-runner --emit failed; see $work/emit.err)" + return 0 fi fi @@ -272,13 +385,14 @@ for src in "${CASES[@]}"; do else "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null - diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1 || r_ok=0 + diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1 || r_ok=0 fi - dt=$(( $(now_ms) - t0 )); T_R=$(( T_R + dt )) - if [ $r_ok -eq 1 ]; then note_pass "$name/R (${dt}ms)" - else note_fail "$name/R${r_msg} (${dt}ms)"; fi + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME R "$dt" + if [ $r_ok -eq 1 ]; then emit_event "$event" PASS "$name/R (${dt}ms)" + else emit_event "$event" FAIL "$name/R${r_msg} (${dt}ms)"; fi else - note_skip "$name/R" "missing roundtrip/readelf/python3" + emit_event "$event" SKIP "$name/R" "missing roundtrip/readelf/python3" fi fi @@ -290,21 +404,17 @@ for src in "${CASES[@]}"; do exe="$work/linked.exe" 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)" + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME E "$dt" + emit_event "$event" FAIL "$name/E (link failed, ${dt}ms)" elif exec_target_supported "$EXEC_ARCH"; then - 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_target_queue "$EXEC_ARCH" "$name" "$exe" \ - "$work/exec.out" "$work/exec.err" "$work/exec.rc" + link_dt=$(( $(now_ms) - t0 )) + emit_event "$event" QUEUE_E "$name" "$work" "$link_dt" "$expected_byte" "$name" "$EXEC_ARCH" else - note_skip "$name/E" "no runner for $EXEC_ARCH" + emit_event "$event" SKIP "$name/E" "no runner for $EXEC_ARCH" fi else - note_skip "$name/E" "no link-exe-runner, $TEST_ARCH clang, or start.o" + emit_event "$event" SKIP "$name/E" "no link-exe-runner, $TEST_ARCH clang, or start.o" fi fi @@ -314,17 +424,21 @@ for src in "${CASES[@]}"; do t0=$(now_ms) "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err" j_rc=$? - dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt )) + dt=$(( $(now_ms) - t0 )) + emit_event "$event" TIME J "$dt" if [ "$j_rc" -eq "$expected_byte" ]; then - note_pass "$name/J (${dt}ms)" + emit_event "$event" PASS "$name/J (${dt}ms)" else - note_fail "$name/J (expected $expected_byte got $j_rc, ${dt}ms)" + emit_event "$event" FAIL "$name/J (expected $expected_byte got $j_rc, ${dt}ms)" fi else - note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH)" + emit_event "$event" SKIP "$name/J" "no jit-runner (host arch != $TEST_ARCH)" fi fi -done + return 0 +} + +run_parallel_items "cases" run_parse_case "${FILTERED_CASES[@]}" # ---- batched path-E flush + verification -----------------------------------