1818# this is a local/CI-with-qemu aid, not part of the host `make test`.
1919# Exit status is non-zero if any present target fails (CI-friendly).
2020#
21+ # Compilation, not the qemu runs, dominates the cost: the eight binaries per
22+ # target come to ~105 translation units against a couple of seconds of emulated
23+ # execution. Two properties hold that down. The engine core and rule objects are
24+ # built once per target and linked into both engine binaries, so the rule set
25+ # costs one compile rather than one per binary. And every compile is a
26+ # single-source `-c`, the only form ccache can serve; handed several sources at
27+ # once it declines and forwards to the compiler, caching nothing.
28+ #
2129# Usage: tests/test-cross [triple...] (default: the local cross set; CI passes
2230# one triple per matrix job)
23- # Env: QEMU_DIR (default: resolve qemu-<arch> from PATH), CFLAGS, KEEP=1
31+ # Env: QEMU_DIR (default: resolve qemu-<arch> from PATH), CFLAGS, KEEP=1,
32+ # JOBS (default: one per core), USE_CCACHE=0 to compile without ccache
2433# ---
2534# <bcoles@gmail.com>
2635
@@ -40,7 +49,8 @@ resolve_qemu() {
4049# -Werror on the overflow/shift classes: these only manifest on a given word
4150# size, so the cross (esp. 32-bit) compile is where a `1ul << 40` shift or a
4251# constant overflow gets caught. Kept targeted (not full -Werror) so the test
43- # TUs' pre-existing unused-static warnings don't turn fatal.
52+ # TUs' pre-existing unused-static warnings don't turn fatal. Both are front-end
53+ # diagnostics and fire at any optimisation level.
4454CFLAGS=${CFLAGS:- -std=c99 -O2 -static -I$ROOT / src -Werror=shift-count-overflow -Werror=overflow}
4555# Every binary this script builds is a test, so the hermeticity probe applies to
4656# all of them: a test that reads a kernel fact from the machine running it is
@@ -51,6 +61,16 @@ CFLAGS="$CFLAGS -DKASLD_HERMETIC_PROBE"
5161# them JOBS at a time. Default to one per core; JOBS=1 restores serial order.
5262JOBS=${JOBS:- $(nproc 2>/ dev/ null || echo 1)}
5363
64+ # ccache when it is installed, nothing when it is not — a compiler cache is an
65+ # optimisation, never a requirement, and a host without one must still run the
66+ # suite. USE_CCACHE=0 forces the direct compiler. The cache lives wherever
67+ # ccache itself is configured to put it, so its size limit and eviction stay
68+ # under the operator's control rather than this script's.
69+ CCPREFIX=" "
70+ if [ " ${USE_CCACHE:- 1} " != 0 ]; then
71+ CCPREFIX=$( command -v ccache 2> /dev/null) || CCPREFIX=" "
72+ fi
73+
5474# A UNIQUE build dir per invocation. Two overlapping runs -- or an orphaned
5575# run_target subshell whose parent `make` was killed but which lives on and
5676# still runs its `rm -rf "$d"` cleanup -- must not share `build/tests/<triple>`,
@@ -79,6 +99,29 @@ RULES=$(echo "$ROOT"/src/rules/*.c)
7999
80100pass=0; fail=0; skip=0
81101
102+ # compile_obj <src> <out.o> [extra cflags...] — one source, one object, so the
103+ # result is cacheable. Reads $cc / $triple / $d from the calling run_target.
104+ compile_obj () {
105+ co_src=$1 ; co_out=$2 ; shift 2
106+ # shellcheck disable=SC2086 # $CCPREFIX/$cc/$CFLAGS are command + flag lists, split intentionally
107+ if ! $CCPREFIX $cc $CFLAGS " $@ " -c " $co_src " -o " $co_out " 2> " $d /cc.log" ; then
108+ printf ' FAIL %-30s (compile %s)\n' " $triple " " ${co_src##*/ } "
109+ show_cc_log " $d /cc.log"
110+ return 1
111+ fi
112+ }
113+
114+ # link_bin <out> <object...> [-l...] — link step; libraries follow the objects.
115+ link_bin () {
116+ lb_out=$1 ; shift
117+ # shellcheck disable=SC2086 # $cc/$CFLAGS are command + flag list, split intentionally
118+ if ! $cc $CFLAGS " $@ " -o " $lb_out " 2> " $d /cc.log" ; then
119+ printf ' FAIL %-30s (link %s)\n' " $triple " " ${lb_out##*/ } "
120+ show_cc_log " $d /cc.log"
121+ return 1
122+ fi
123+ }
124+
82125# run_target writes its human-readable line(s) to stdout (the caller redirects
83126# this to a per-target .res file) and a single pass/fail/skip token to its .st
84127# file, so invocations can run concurrently without sharing shell state.
@@ -92,61 +135,55 @@ run_target() {
92135 if [ -z " $qemu " ]; then
93136 printf ' SKIP %-30s (no qemu: %s)\n' " $triple " " $qbin " ; echo skip > " $st " ; return
94137 fi
95- d=" $OUT /$triple " ; mkdir -p " $d "
138+ d=" $OUT /$triple " ; obj=" $d /obj" ; sh_obj=" $obj /shared" ; mkdir -p " $sh_obj "
139+ # The engine core and every rule, one object each, shared by the two engine
140+ # binaries below so the rule set is built once per target. engine_rules.c is
141+ # the registry and belongs only to the integration binary, so it stays out of
142+ # the shared set.
143+ for src in $CORE $RULES ; do
144+ compile_obj " $src " " $sh_obj /$( basename " $src " .c) .o" || { echo fail > " $st " ; return ; }
145+ done
146+ compile_obj " $ROOT /src/engine_rules.c" " $obj /engine_rules.o" || { echo fail > " $st " ; return ; }
96147 # Per-rule engine test (every rule via the wildcard) AND the full-registry
97148 # integration test, both under qemu so each arch-gated rule body executes its
98149 # active path on its own arch. The tests anchor addresses to the arch's
99150 # quantity tops / PHYS_OFFSET, so they are portable across 64-bit arches.
100151 # (This harness has caught real arch bugs this way: a 64-bit shift overflow
101152 # and PHYS_OFFSET-relative assertion errors.)
102- # shellcheck disable=SC2086 # $cc/$CFLAGS/$CORE/$RULES are command + flag/file lists, split intentionally
103- if ! $cc $CFLAGS " $ROOT /tests/test_engine.c" $CORE $RULES -o " $d /test_engine" 2> " $d /cc.log" ; then
104- printf ' FAIL %-30s (compile test_engine)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
105- fi
106- # shellcheck disable=SC2086 # $cc/$CFLAGS/$CORE/$RULES are command + flag/file lists, split intentionally
107- if ! $cc $CFLAGS " $ROOT /tests/test_engine_integration.c" $CORE " $ROOT /src/engine_rules.c" $RULES \
108- -o " $d /test_integration" 2> " $d /cc.log" ; then
109- printf ' FAIL %-30s (compile test_integration)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
110- fi
153+ compile_obj " $ROOT /tests/test_engine.c" " $obj /test_engine.o" || { echo fail > " $st " ; return ; }
154+ compile_obj " $ROOT /tests/test_engine_integration.c" " $obj /test_integration.o" || { echo fail > " $st " ; return ; }
111155 # The parser / result-model / merge / engine-sync unit suite (its renderer
112156 # half is now test_render, below). It includes the orchestrator + render
113157 # translation units directly (hence -DKASLD_TESTING and -lpthread) and derives
114158 # every fixture address from the arch's own layout constants, so it runs on
115159 # every width and endianness.
116- # shellcheck disable=SC2086 # $cc/$CFLAGS are command + flag list, split intentionally
117- if ! $cc $CFLAGS -DKASLD_TESTING " $ROOT /tests/test_kasld.c" -o " $d /test_kasld" -lpthread 2> " $d /cc.log" ; then
118- printf ' FAIL %-30s (compile test_kasld)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
119- fi
160+ compile_obj " $ROOT /tests/test_kasld.c" " $obj /test_kasld.o" -DKASLD_TESTING || { echo fail > " $st " ; return ; }
120161 # The renderer unit suite (split from test_kasld); same single-TU model.
121- # shellcheck disable=SC2086 # $cc/$CFLAGS are command + flag list, split intentionally
122- if ! $cc $CFLAGS -DKASLD_TESTING " $ROOT /tests/test_render.c" -o " $d /test_render" -lpthread 2> " $d /cc.log" ; then
123- printf ' FAIL %-30s (compile test_render)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
124- fi
162+ compile_obj " $ROOT /tests/test_render.c" " $obj /test_render.o" -DKASLD_TESTING || { echo fail > " $st " ; return ; }
125163 # The estimate-algebra + honest-top soundness suite (links only estimate.c +
126164 # quantities.c). Its arch-gated interval_admits() asserts validate each arch's
127165 # honest-top floor/ceiling on its own arch — dormant on the x86_64 host, live
128166 # here, so a phys/virt floor that excludes a valid base fails on the arch it
129167 # affects.
130- # shellcheck disable=SC2086 # $cc/$CFLAGS are command + flag list, split intentionally
131- if ! $cc $CFLAGS " $ROOT /tests/test_estimate.c" " $ROOT /src/estimate.c" " $ROOT /src/quantities.c" -o " $d /test_estimate" 2> " $d /cc.log" ; then
132- printf ' FAIL %-30s (compile test_estimate)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
133- fi
168+ compile_obj " $ROOT /tests/test_estimate.c" " $obj /test_estimate.o" || { echo fail > " $st " ; return ; }
134169 # The width-sensitive suites. These are the reason the 32-bit targets matter:
135170 # kasld_addr_t is the build's word, so on a 64-bit host the too-wide branch of
136171 # kasld_addr_parse is unreachable and the refusal that the parser exists for
137172 # never executes. Here it does.
138- # shellcheck disable=SC2086 # $cc/$CFLAGS are command + flag list, split intentionally
139- if ! $cc $CFLAGS " $ROOT /tests/test_addr_parse.c" -o " $d /test_addr_parse" 2> " $d /cc.log" ; then
140- printf ' FAIL %-30s (compile test_addr_parse)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
141- fi
142- # shellcheck disable=SC2086 # $cc/$CFLAGS are command + flag list, split intentionally
143- if ! $cc $CFLAGS " $ROOT /tests/test_target_width.c" -o " $d /test_target_width" 2> " $d /cc.log" ; then
144- printf ' FAIL %-30s (compile test_target_width)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
145- fi
146- # shellcheck disable=SC2086 # $cc/$CFLAGS are command + flag list, split intentionally
147- if ! $cc $CFLAGS " $ROOT /tests/test_proc_kallsyms.c" -o " $d /test_proc_kallsyms" 2> " $d /cc.log" ; then
148- printf ' FAIL %-30s (compile test_proc_kallsyms)\n' " $triple " ; show_cc_log " $d /cc.log" ; echo fail > " $st " ; return
149- fi
173+ for t in test_addr_parse test_target_width test_proc_kallsyms; do
174+ compile_obj " $ROOT /tests/$t .c" " $obj /$t .o" || { echo fail > " $st " ; return ; }
175+ done
176+
177+ # shellcheck disable=SC2086 # the shared-object glob is a file list, split intentionally
178+ link_bin " $d /test_engine" " $obj /test_engine.o" $sh_obj /* .o || { echo fail > " $st " ; return ; }
179+ # shellcheck disable=SC2086 # the shared-object glob is a file list, split intentionally
180+ link_bin " $d /test_integration" " $obj /test_integration.o" " $obj /engine_rules.o" $sh_obj /* .o || { echo fail > " $st " ; return ; }
181+ link_bin " $d /test_kasld" " $obj /test_kasld.o" -lpthread || { echo fail > " $st " ; return ; }
182+ link_bin " $d /test_render" " $obj /test_render.o" -lpthread || { echo fail > " $st " ; return ; }
183+ link_bin " $d /test_estimate" " $obj /test_estimate.o" " $sh_obj /estimate.o" " $sh_obj /quantities.o" || { echo fail > " $st " ; return ; }
184+ for t in test_addr_parse test_target_width test_proc_kallsyms; do
185+ link_bin " $d /$t " " $obj /$t .o" || { echo fail > " $st " ; return ; }
186+ done
150187 ok=1
151188 for t in test_engine test_integration test_estimate test_kasld test_render \
152189 test_addr_parse test_target_width test_proc_kallsyms; do
@@ -213,11 +250,18 @@ if [ "$#" -eq 0 ]; then
213250 set -- $default_triples
214251fi
215252
216- # Launch up to JOBS targets concurrently, each writing to its own .res/.st;
217- # barrier every JOBS launches (POSIX sh has no `wait -n`). Order is recorded so
218- # the summary prints deterministically regardless of finish order.
253+ # Launch up to JOBS targets concurrently, each writing to its own .res/.st.
254+ # At capacity, wait for the OLDEST target still running and launch the next as
255+ # soon as it ends, rather than draining all JOBS of them first: one slow arch
256+ # then delays at most the targets ahead of it in the queue, not a whole batch.
257+ # `wait <pid>` returns however that child ended — including killed by a signal —
258+ # so a target that dies without cleaning up after itself cannot stall the run.
259+ # Order is recorded so the summary prints deterministically regardless of finish
260+ # order.
219261order=" "
220- running=0
262+ running=" " # PIDs of live targets, oldest first
263+ inflight=0
264+
221265for triple in " $@ " ; do
222266 order=" $order $triple "
223267 qbin=$( qemu_for " $triple " )
@@ -227,8 +271,14 @@ for triple in "$@"; do
227271 continue
228272 fi
229273 run_target " $triple " " $qbin " > " $OUT /$triple .res" 2>&1 &
230- running=$(( running + 1 ))
231- if [ " $running " -ge " $JOBS " ]; then wait ; running=0; fi
274+ running=" ${running: +$running } $! "
275+ inflight=$(( inflight + 1 ))
276+ if [ " $inflight " -ge " $JOBS " ]; then
277+ oldest=${running%% * }
278+ if [ " $oldest " = " $running " ]; then running=" " ; else running=${running#* } ; fi
279+ wait " $oldest " 2> /dev/null
280+ inflight=$(( inflight - 1 ))
281+ fi
232282done
233283wait
234284
0 commit comments