Skip to content

Commit 77bd652

Browse files
committed
tests: parallelize test-cross and replay (JOBS, default nproc)
Run independent targets/fixtures JOBS at a time instead of serially. Each job writes its line + a status token to per-target files; results aggregate in launch order so the summary stays deterministic (JOBS=1 restores serial).
1 parent be9b244 commit 77bd652

2 files changed

Lines changed: 171 additions & 105 deletions

File tree

tests/replay

Lines changed: 132 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -99,108 +99,145 @@ native_bindir() {
9999
# Pull "key:value" out of a fixture meta.txt (collect's format).
100100
meta() { grep -E "^$2:" "$1/meta.txt" 2>/dev/null | head -1 | sed -E 's/^[^:]*:[[:space:]]*//'; }
101101

102-
pass=0; fail=0; skip=0; scratch_keep=""
102+
pass=0; fail=0; skip=0
103+
104+
# Fixtures are independent (each gets its own mktemp scratch sysroot), so they
105+
# can replay JOBS at a time. The work is qemu-startup/I-O bound, not CPU bound,
106+
# so concurrency well above the core count still pays off. KEEP=1 (inspect the
107+
# scratch dir) forces serial so the kept path is unambiguous.
108+
JOBS=${JOBS:-$(nproc 2>/dev/null || echo 1)}
109+
[ "${KEEP:-0}" = 1 ] && JOBS=1
110+
111+
# run_fixture writes its human-readable line to stdout (the caller redirects it
112+
# to a per-fixture .res) and a pass/fail/skip token to $st, so invocations can
113+
# run concurrently without sharing shell state. Uses `return` (not `continue`)
114+
# since it runs as a backgrounded function.
115+
run_fixture() {
116+
d=$1 st=$2
117+
name=$(echo "$d" | sed "s#$FIXROOT/##")
118+
119+
if [ ! -f "$d/meta.txt" ] || [ ! -f "$d/sizes.txt" ]; then
120+
printf 'SKIP %-44s (no meta.txt/sizes.txt)\n' "$name"; echo skip >"$st"; return
121+
fi
122+
123+
machine=$(meta "$d" uname_m_raw)
124+
release=$(meta "$d" kernel_release)
125+
126+
if [ "${KASLD_NATIVE:-0}" = 1 ]; then
127+
# No qemu: only fixtures the host can run directly; binary from build/.
128+
if ! native_ok "$machine"; then
129+
printf 'SKIP %-44s (native mode: non-host arch %s)\n' "$name" "$machine"; echo skip >"$st"; return
130+
fi
131+
bindir=$(native_bindir "$machine")
132+
if [ -z "$bindir" ]; then
133+
printf 'SKIP %-44s (native mode: no host binary for %s — run make)\n' "$name" "$machine"; echo skip >"$st"; return
134+
fi
135+
kasld="$bindir/kasld"; comps="$bindir/components"; runner=""
136+
else
137+
map=$(target_for "$machine")
138+
if [ -z "$map" ]; then
139+
printf 'SKIP %-44s (no target mapping for uname_m=%s)\n' "$name" "$machine"; echo skip >"$st"; return
140+
fi
141+
subdir=${map% *}; qbin=${map#* }
142+
kasld="$BUILD_DIR/$subdir/kasld"
143+
comps="$BUILD_DIR/$subdir/components"
144+
if [ ! -x "$kasld" ]; then
145+
printf 'SKIP %-44s (no binary: %s — run make cross)\n' "$name" "$subdir/kasld"; echo skip >"$st"; return
146+
fi
147+
if [ ! -x "$QEMU_DIR/$qbin" ]; then
148+
printf 'SKIP %-44s (no qemu: %s)\n' "$name" "$qbin"; echo skip >"$st"; return
149+
fi
150+
runner="$QEMU_DIR/$qbin"
151+
fi
152+
153+
# Reconstruct a scratch sysroot: copy captured facts, then materialise the
154+
# /boot images as sparse files of their true size (header prefix preserved).
155+
sc=$(mktemp -d)
156+
cp -a "$d/sysroot" "$sc/r"
157+
while read -r sz path; do
158+
if [ -z "$sz" ] || [ -z "$path" ]; then continue; fi
159+
mkdir -p "$sc/r$(dirname "$path")" 2>/dev/null
160+
truncate -s "$sz" "$sc/r$path" 2>/dev/null
161+
done < "$d/sizes.txt"
162+
163+
# Release handling: QEMU_UNAME sets the top-level process's uname, but is
164+
# NOT honored after qemu's self-re-exec for foreign-arch component children
165+
# — so KASLD_UNAME_RELEASE (read by kasld_uname(), propagated via the
166+
# environment) is what actually gives every process the fixture's release
167+
# for /boot path construction. Both are set; the latter is the load-bearing
168+
# one.
169+
#
170+
# KASLD_EXEC_WRAPPER lets the orchestrator's component children execve
171+
# through the host-arch qemu wrapper instead of trying to execve a guest-
172+
# arch ELF directly (which the host kernel refuses with ENOEXEC unless
173+
# binfmt_misc is registered). When $runner is non-empty (foreign-arch
174+
# fixture) we point KASLD_EXEC_WRAPPER at it; for native-arch fixtures
175+
# $runner is empty and KASLD_EXEC_WRAPPER stays unset, so the orchestrator
176+
# takes the production direct-execve path.
177+
QEMU_UNAME="$release" KASLD_UNAME_RELEASE="$release" KASLD_SYSROOT="$sc/r" \
178+
KASLD_COMPONENT_DIR="$comps" KASLD_EXEC_WRAPPER="$runner" \
179+
$runner "$kasld" -v > "$sc/replay.txt" 2>/dev/null
180+
rc=$?
181+
182+
# Smoke verdict: the run must complete without an emulation crash. kasld
183+
# exits 0 with results, 1 with no tagged results (a valid empty run);
184+
# anything killed by a signal exits >= 128. A crash on any arch's calling
185+
# convention is the failure this harness exists to catch.
186+
#
187+
# With KASLD_EXEC_WRAPPER pointed at the host qemu binary, foreign-arch
188+
# component children do exec successfully under nested qemu-user — so
189+
# both x86 (native) and foreign-arch fixtures should produce results and
190+
# render a summary.
191+
has_summary=$(grep -cE '^(========| KASLR analysis:|Virtual memory layout)' \
192+
"$sc/replay.txt" 2>/dev/null || true)
193+
n_results=$(grep -cE '^[VPD] ' "$sc/replay.txt" 2>/dev/null || true)
194+
195+
if [ "$rc" -ge 128 ]; then
196+
printf 'FAIL %-44s (crashed under emulation: signal %d)\n' \
197+
"$name" "$((rc - 128))"
198+
echo fail >"$st"
199+
elif [ "$has_summary" -gt 0 ]; then
200+
printf 'PASS %-44s rc=%d, %d results, summary rendered\n' \
201+
"$name" "$rc" "$n_results"
202+
echo pass >"$st"
203+
else
204+
printf 'PASS %-44s rc=%d, no results\n' "$name" "$rc"
205+
echo pass >"$st"
206+
fi
207+
208+
if [ "${KEEP:-0}" = 1 ]; then echo " kept scratch sysroot: $sc"; else rm -rf "$sc"; fi
209+
}
103210

211+
# Launch up to JOBS fixtures concurrently, keyed by a launch index so the
212+
# summary prints in a stable order regardless of finish order. Barrier every
213+
# JOBS launches (POSIX sh has no `wait -n`).
214+
RESDIR=$(mktemp -d)
215+
n=0; running=0
104216
for fx in "${@:-$FIXROOT/*/*}"; do
105217
# Expand the default glob; skip non-dirs.
106218
for d in $fx; do
107219
[ -d "$d" ] || continue
108-
name=$(echo "$d" | sed "s#$FIXROOT/##")
109-
110-
if [ ! -f "$d/meta.txt" ] || [ ! -f "$d/sizes.txt" ]; then
111-
printf 'SKIP %-44s (no meta.txt/sizes.txt)\n' "$name"; skip=$((skip+1)); continue
112-
fi
113-
114-
machine=$(meta "$d" uname_m_raw)
115-
release=$(meta "$d" kernel_release)
116-
117-
if [ "${KASLD_NATIVE:-0}" = 1 ]; then
118-
# No qemu: only fixtures the host can run directly; binary from build/.
119-
if ! native_ok "$machine"; then
120-
printf 'SKIP %-44s (native mode: non-host arch %s)\n' "$name" "$machine"; skip=$((skip+1)); continue
121-
fi
122-
bindir=$(native_bindir "$machine")
123-
if [ -z "$bindir" ]; then
124-
printf 'SKIP %-44s (native mode: no host binary for %s — run make)\n' "$name" "$machine"; skip=$((skip+1)); continue
125-
fi
126-
kasld="$bindir/kasld"; comps="$bindir/components"; runner=""
127-
else
128-
map=$(target_for "$machine")
129-
if [ -z "$map" ]; then
130-
printf 'SKIP %-44s (no target mapping for uname_m=%s)\n' "$name" "$machine"; skip=$((skip+1)); continue
131-
fi
132-
subdir=${map% *}; qbin=${map#* }
133-
kasld="$BUILD_DIR/$subdir/kasld"
134-
comps="$BUILD_DIR/$subdir/components"
135-
if [ ! -x "$kasld" ]; then
136-
printf 'SKIP %-44s (no binary: %s — run make cross)\n' "$name" "$subdir/kasld"; skip=$((skip+1)); continue
137-
fi
138-
if [ ! -x "$QEMU_DIR/$qbin" ]; then
139-
printf 'SKIP %-44s (no qemu: %s)\n' "$name" "$qbin"; skip=$((skip+1)); continue
140-
fi
141-
runner="$QEMU_DIR/$qbin"
142-
fi
143-
144-
# Reconstruct a scratch sysroot: copy captured facts, then materialise the
145-
# /boot images as sparse files of their true size (header prefix preserved).
146-
sc=$(mktemp -d)
147-
cp -a "$d/sysroot" "$sc/r"
148-
while read -r sz path; do
149-
if [ -z "$sz" ] || [ -z "$path" ]; then continue; fi
150-
mkdir -p "$sc/r$(dirname "$path")" 2>/dev/null
151-
truncate -s "$sz" "$sc/r$path" 2>/dev/null
152-
done < "$d/sizes.txt"
153-
154-
# Release handling: QEMU_UNAME sets the top-level process's uname, but is
155-
# NOT honored after qemu's self-re-exec for foreign-arch component children
156-
# — so KASLD_UNAME_RELEASE (read by kasld_uname(), propagated via the
157-
# environment) is what actually gives every process the fixture's release
158-
# for /boot path construction. Both are set; the latter is the load-bearing
159-
# one.
160-
#
161-
# KASLD_EXEC_WRAPPER lets the orchestrator's component children execve
162-
# through the host-arch qemu wrapper instead of trying to execve a guest-
163-
# arch ELF directly (which the host kernel refuses with ENOEXEC unless
164-
# binfmt_misc is registered). When $runner is non-empty (foreign-arch
165-
# fixture) we point KASLD_EXEC_WRAPPER at it; for native-arch fixtures
166-
# $runner is empty and KASLD_EXEC_WRAPPER stays unset, so the orchestrator
167-
# takes the production direct-execve path.
168-
QEMU_UNAME="$release" KASLD_UNAME_RELEASE="$release" KASLD_SYSROOT="$sc/r" \
169-
KASLD_COMPONENT_DIR="$comps" KASLD_EXEC_WRAPPER="$runner" \
170-
$runner "$kasld" -v > "$sc/replay.txt" 2>/dev/null
171-
rc=$?
172-
173-
# Smoke verdict: the run must complete without an emulation crash. kasld
174-
# exits 0 with results, 1 with no tagged results (a valid empty run);
175-
# anything killed by a signal exits >= 128. A crash on any arch's calling
176-
# convention is the failure this harness exists to catch.
177-
#
178-
# With KASLD_EXEC_WRAPPER pointed at the host qemu binary, foreign-arch
179-
# component children do exec successfully under nested qemu-user — so
180-
# both x86 (native) and foreign-arch fixtures should produce results and
181-
# render a summary.
182-
has_summary=$(grep -cE '^(========| KASLR analysis:|Virtual memory layout)' \
183-
"$sc/replay.txt" 2>/dev/null || true)
184-
n_results=$(grep -cE '^[VPD] ' "$sc/replay.txt" 2>/dev/null || true)
185-
186-
if [ "$rc" -ge 128 ]; then
187-
printf 'FAIL %-44s (crashed under emulation: signal %d)\n' \
188-
"$name" "$((rc - 128))"
189-
fail=$((fail+1))
190-
elif [ "$has_summary" -gt 0 ]; then
191-
printf 'PASS %-44s rc=%d, %d results, summary rendered\n' \
192-
"$name" "$rc" "$n_results"
193-
pass=$((pass+1))
194-
else
195-
printf 'PASS %-44s rc=%d, no results\n' "$name" "$rc"
196-
pass=$((pass+1))
197-
fi
198-
199-
if [ "${KEEP:-0}" = 1 ]; then scratch_keep="$sc"; else rm -rf "$sc"; fi
220+
n=$((n + 1))
221+
run_fixture "$d" "$RESDIR/$n.st" >"$RESDIR/$n.res" 2>&1 &
222+
running=$((running + 1))
223+
if [ "$running" -ge "$JOBS" ]; then wait; running=0; fi
200224
done
201225
done
226+
wait
227+
228+
# Aggregate in launch order; a missing token (job died) counts as a failure.
229+
i=1
230+
while [ "$i" -le "$n" ]; do
231+
[ -f "$RESDIR/$i.res" ] && cat "$RESDIR/$i.res"
232+
case $(cat "$RESDIR/$i.st" 2>/dev/null) in
233+
pass) pass=$((pass + 1)) ;;
234+
skip) skip=$((skip + 1)) ;;
235+
*) fail=$((fail + 1)) ;;
236+
esac
237+
i=$((i + 1))
238+
done
239+
rm -rf "$RESDIR"
202240

203241
echo "----------------------------------------------------------------------"
204-
printf 'replay: %d pass, %d fail, %d skip\n' "$pass" "$fail" "$skip"
205-
[ -n "$scratch_keep" ] && echo "kept last scratch sysroot: $scratch_keep"
242+
printf 'replay: %d pass, %d fail, %d skip (JOBS=%s)\n' "$pass" "$fail" "$skip" "$JOBS"
206243
[ "$fail" -eq 0 ]

tests/test-cross

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,28 @@ ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
2828
QEMU_DIR=${QEMU_DIR:-/home/user/qemu/build}
2929
OUT=$ROOT/build/tests
3030
CFLAGS=${CFLAGS:--std=c99 -O2 -static -I$ROOT/src}
31+
# Targets are independent (separate build dir + binaries each), so build/run
32+
# them JOBS at a time. Default to one per core; JOBS=1 restores serial order.
33+
JOBS=${JOBS:-$(nproc 2>/dev/null || echo 1)}
34+
mkdir -p "$OUT"
3135

3236
CORE="$ROOT/src/estimate.c $ROOT/src/quantities.c $ROOT/src/evidence.c $ROOT/src/engine.c"
3337
RULES=$(echo "$ROOT"/src/rules/*.c)
3438

3539
pass=0; fail=0; skip=0
3640

41+
# run_target writes its human-readable line(s) to stdout (the caller redirects
42+
# this to a per-target .res file) and a single pass/fail/skip token to its .st
43+
# file, so invocations can run concurrently without sharing shell state.
3744
run_target() {
3845
triple=$1 qbin=$2
3946
cc="${triple}-gcc"; qemu="$QEMU_DIR/$qbin"
47+
st="$OUT/$triple.st"
4048
if ! command -v "$cc" >/dev/null 2>&1; then
41-
printf 'SKIP %-30s (no toolchain: %s)\n' "$triple" "$cc"; skip=$((skip+1)); return
49+
printf 'SKIP %-30s (no toolchain: %s)\n' "$triple" "$cc"; echo skip >"$st"; return
4250
fi
4351
if [ ! -x "$qemu" ]; then
44-
printf 'SKIP %-30s (no qemu: %s)\n' "$triple" "$qbin"; skip=$((skip+1)); return
52+
printf 'SKIP %-30s (no qemu: %s)\n' "$triple" "$qbin"; echo skip >"$st"; return
4553
fi
4654
d="$OUT/$triple"; mkdir -p "$d"
4755
# Per-rule engine test (every rule via the wildcard) AND the full-registry
@@ -51,19 +59,19 @@ run_target() {
5159
# (This harness has caught real arch bugs this way: a 64-bit shift overflow
5260
# and PHYS_OFFSET-relative assertion errors.)
5361
if ! $cc $CFLAGS "$ROOT/tests/test_engine.c" $CORE $RULES -o "$d/test_engine" 2>"$d/cc.log"; then
54-
printf 'FAIL %-30s (compile test_engine)\n' "$triple"; sed 's/^/ /' "$d/cc.log" | head -5; fail=$((fail+1)); return
62+
printf 'FAIL %-30s (compile test_engine)\n' "$triple"; sed 's/^/ /' "$d/cc.log" | head -5; echo fail >"$st"; return
5563
fi
5664
if ! $cc $CFLAGS "$ROOT/tests/test_engine_integration.c" $CORE "$ROOT/src/engine_rules.c" $RULES \
5765
-o "$d/test_integration" 2>"$d/cc.log"; then
58-
printf 'FAIL %-30s (compile test_integration)\n' "$triple"; sed 's/^/ /' "$d/cc.log" | head -5; fail=$((fail+1)); return
66+
printf 'FAIL %-30s (compile test_integration)\n' "$triple"; sed 's/^/ /' "$d/cc.log" | head -5; echo fail >"$st"; return
5967
fi
6068
# The parser / result-model / merge / engine-sync / renderer unit suite. It
6169
# includes the orchestrator + render translation units directly (hence
6270
# -DKASLD_TESTING and -lpthread) and derives every fixture address from the
6371
# arch's own layout constants, so it runs on every width and endianness —
6472
# which is how the renderer's hardening paths get exercised on each arch.
6573
if ! $cc $CFLAGS -DKASLD_TESTING "$ROOT/tests/test_kasld.c" -o "$d/test_kasld" -lpthread 2>"$d/cc.log"; then
66-
printf 'FAIL %-30s (compile test_kasld)\n' "$triple"; sed 's/^/ /' "$d/cc.log" | head -5; fail=$((fail+1)); return
74+
printf 'FAIL %-30s (compile test_kasld)\n' "$triple"; sed 's/^/ /' "$d/cc.log" | head -5; echo fail >"$st"; return
6775
fi
6876
ok=1
6977
for t in test_engine test_integration test_kasld; do
@@ -72,17 +80,16 @@ run_target() {
7280
fi
7381
done
7482
if [ "$ok" = 1 ]; then
75-
printf 'PASS %-30s engine + integration + kasld\n' "$triple"; pass=$((pass+1))
83+
printf 'PASS %-30s engine + integration + kasld\n' "$triple"; echo pass >"$st"
7684
else
77-
fail=$((fail+1))
85+
echo fail >"$st"
7886
fi
7987
[ "${KEEP:-0}" = 1 ] || rm -rf "$d"
8088
}
8189

8290
# "<toolchain-triple> <qemu-binary>" per target. <triple>-gcc must be on
8391
# PATH (the same musl-cross set `make cross` uses); qemu-<arch> must be
8492
# in QEMU_DIR.
85-
# Here-doc (not a pipe) so the counters survive into the summary/exit status.
8693
#
8794
# Both 64-bit and 32-bit targets. Tests that model 64-bit-only kernel layouts
8895
# (x86_64 RANDOMIZE_MEMORY vmalloc/vmemmap, LA57/arm64 VA-bits, TiB-scale direct
@@ -96,9 +103,17 @@ run_target() {
96103
# riscv32 -> page_offset_invariant_pin + text_base_coupling_synth active
97104
# The ppc32 little-endian toolchain (powerpcle-unknown-linux-musl) has no
98105
# 32-bit-LE qemu-user binary, so that variant stays live-test-only.
106+
# Launch up to JOBS targets concurrently, each writing to its own .res/.st;
107+
# barrier every JOBS launches (POSIX sh has no `wait -n`). The target order is
108+
# recorded so the summary prints deterministically regardless of finish order.
109+
order=""
110+
running=0
99111
while read -r triple qbin; do
100112
[ -z "$triple" ] && continue
101-
run_target "$triple" "$qbin"
113+
order="$order $triple"
114+
run_target "$triple" "$qbin" >"$OUT/$triple.res" 2>&1 &
115+
running=$((running + 1))
116+
if [ "$running" -ge "$JOBS" ]; then wait; running=0; fi
102117
done <<'EOF'
103118
aarch64-linux-musl qemu-aarch64
104119
arm-unknown-linux-musleabi qemu-arm
@@ -118,7 +133,21 @@ riscv64-linux-musl qemu-riscv64
118133
s390x-ibm-linux-musl qemu-s390x
119134
x86_64-linux-musl qemu-x86_64
120135
EOF
136+
wait
137+
138+
# Aggregate in launch order: print each target's captured output, tally its
139+
# status token. A missing token (a job that died before writing) counts as a
140+
# failure rather than vanishing.
141+
for triple in $order; do
142+
[ -f "$OUT/$triple.res" ] && cat "$OUT/$triple.res"
143+
case $(cat "$OUT/$triple.st" 2>/dev/null) in
144+
pass) pass=$((pass + 1)) ;;
145+
skip) skip=$((skip + 1)) ;;
146+
*) fail=$((fail + 1)) ;;
147+
esac
148+
rm -f "$OUT/$triple.res" "$OUT/$triple.st"
149+
done
121150

122151
echo "----------------------------------------------------------------------"
123-
printf 'test-cross: %d pass, %d fail, %d skip\n' "$pass" "$fail" "$skip"
152+
printf 'test-cross: %d pass, %d fail, %d skip (JOBS=%s)\n' "$pass" "$fail" "$skip" "$JOBS"
124153
[ "$fail" -eq 0 ]

0 commit comments

Comments
 (0)