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:
| M | test/cg/run.sh | | | 435 | +++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------- |
| M | test/elf/run.sh | | | 385 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- |
| A | test/lib/parallel.sh | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | test/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 -----------------------------------