# BST-TIMEOUT: 300 # # aml -- "Andri's Main Loop", an event loop shipped as an archive plus three # packaged example drivers. # # THE LOOP IS UNREACHABLE ON THIS KERNEL, AND THAT IS PROBED RATHER THAN # ASSUMED. aml's only Linux backend is epoll, and epoll_create/epoll_create1 # answer ENOSYS on wasm32 (brintos/linux#53), so aml_new() returns NULL and # every example exits 1 having printed nothing. The two other file sources the # backend uses are absent for the same reason: timerfd_create (brintos/linux#110) # and signalfd (brintos/linux#114, filed from this port's own measurement). # # aml itself is present, linked and correct -- what is missing is the syscall -- # so the loop cases are reported as ATTRIBUTED SKIPS rather than reds, the same # treatment 81-libubox, 89-nng and 79-libqb already give this gap. The skip is # gated on the EXACT expected errno: any other errno, or a probe that does not # report at all, leaves aml/platform-loop-probe RED, because that would be a new # finding rather than this known one. When #53 lands, the probe reports success, # the other arm takes over on its own and the real assertions below start # running -- delete the ENOSYS arm then. # # Two bounding rules the live cases follow, both learned the expensive way: # # * A run bounded with the DEFAULT signal loses everything it printed. These # binaries write to a pipe, so libc block-buffers them and a SIGTERM'd child # never flushes -- the case would fail for want of output rather than for # want of the behaviour. `timeout -s INT` instead: aml BLOCKS SIGINT and # reads it back off a signalfd, so each example's own handler unwinds the # loop and libc flushes on the way out. # * A spinning writer is bounded by a READER, not only by a clock. The guest # does not return memory on process exit, so `head` closing the pipe (and # SIGPIPE-ing the writer) is what keeps an unbounded printf from costing the # whole boot. # # The probe arms alarm(2) as a second, in-process bound. On THIS kernel that # bound is inert -- alarm is ENOSYS too (brintos/linux#77) -- which is exactly # why the timeout(1) bound is not optional: a bound resting on one mechanism can # go missing without saying so. # # Select this suite exactly -- `brintos-selftest aml` also matches 73-cyaml and # 73-libyaml, whose names contain "aml". . "$BST_LIB" S=/usr/lib B=/usr/bin W=${TMPDIR:-/tmp}/bst-aml.$$ mkdir -p "$W" 2>/dev/null ENOSYS=38 # ---------------------------------------------------------------- what shipped check aml/archive test -f "$S/libaml.a" check_eq aml/archive-magic "!" "$(head -c 7 "$S/libaml.a" 2>/dev/null)" check aml/header test -f /usr/include/aml.h check_out aml/header-guard "AML_UNSTABLE_API" cat /usr/include/aml.h check aml/pkgconfig test -f "$S/pkgconfig/aml.pc" check_out aml/pc-name "Name: aml" cat "$S/pkgconfig/aml.pc" check_out aml/pc-version "Version: 0.3.0" cat "$S/pkgconfig/aml.pc" check_out aml/pc-libs "Libs: -L\${libdir} -laml -lrt -pthread" cat "$S/pkgconfig/aml.pc" check_out aml/pc-cflags "Cflags: -I\${includedir} -pthread" cat "$S/pkgconfig/aml.pc" # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape aml/no-shared "$S/libaml.so*" for x in ticker nested-ticker reader; do check "aml/example-$x" test -f "$B/$x" check_eq "aml/example-$x-wasm" "0061736d" \ "$(od -A n -t x1 -N 4 "$B/$x" 2>/dev/null | tr -d ' \n')" # `[ -x ]` is not an oracle on the virtiofs root -- the files are owned # 1000:1001, the guest is root, and busybox test falls through to a # getgroups(2) that answers ENOSYS. Read the mode off ls(1) instead. check_out "aml/example-$x-mode" "-rwxr-xr-x" ls -l "$B/$x" done # ------------------------------------------------- the probe, and the arm it picks # data/aml-probe is cross-linked against the very archive this package installs # (src/aml_probe.c beside it). One phase per invocation, each bounded, so a # phase that hangs costs its own cases and nothing else. probe=$W/aml-probe probe_ok=0 loop_state=broken if [ -f "$BST_DATA/aml-probe" ] && cp "$BST_DATA/aml-probe" "$probe" 2>/dev/null \ && chmod +x "$probe" 2>/dev/null; then bst_ok aml/probe-stage probe_ok=1 else bst_notok aml/probe-stage "cannot stage $BST_DATA/aml-probe" fi # value and errno of one `KEY= errno=` line; p_get takes a plain KEY=VALUE pv() { printf '%s\n' "$2" | sed -n "s/^$1=\\([-0-9][0-9]*\\).*/\\1/p" | head -1; } pe() { printf '%s\n' "$2" | sed -n "s/^$1=[-0-9][0-9]* errno=\\([0-9][0-9]*\\).*/\\1/p" | head -1; } p_get() { printf '%s\n' "$2" | sed -n "s/^$1=//p" | head -1; } if [ "$probe_ok" = 1 ]; then sys=$(timeout 30 "$probe" syscalls 2>&1) # The negative controls. These two must work whatever the loop does: they are # what separates "this kernel lacks some syscalls" from "the probe is broken". check_eq aml/probe-clock-gettime "0" "$(pv clock_gettime "$sys")" check_eq aml/probe-pipe "0" "$(pv pipe "$sys")" if [ "$(pv epoll_create "$sys")" -ge 0 ] 2>/dev/null; then loop_state=up elif [ "$(pv epoll_create "$sys")" = "-1" ] && [ "$(pe epoll_create "$sys")" = "$ENOSYS" ] && [ "$(pv epoll_create1 "$sys")" = "-1" ] && [ "$(pe epoll_create1 "$sys")" = "$ENOSYS" ]; then loop_state=enosys fi if [ "$loop_state" = broken ]; then bst_notok aml/platform-loop-probe "unexpected epoll probe result: $(_bst_1line "$sys")" else bst_ok aml/platform-loop-probe fi # Identity: both symbols are DEFINED BY THE ARCHIVE, so reading them back is # what establishes the probe linked against it rather than merely compiling. # aml_version is GIT_VERSION, so it also pins the recipe's SRCREV. This phase # creates no loop, so it is asserted on every arm. o=$(timeout 20 "$probe" ident 2>&1); rc=$? check_eq aml/probe-ident-rc 0 "$rc" check_eq aml/probe-ident-version "v0.3.0-0-gb83f357 (master)" "$(p_get version "$o")" check_eq aml/probe-ident-abi "1" "$(p_get abi "$o")" fi WHY="aml_new() cannot succeed on this kernel: epoll_create/epoll_create1 are ENOSYS (brintos/linux#53); aml is built, linked and correct, the syscall is absent" WHY_T="$WHY; timerfd_create is ENOSYS too (brintos/linux#110)" WHY_S="$WHY; signalfd is ENOSYS too (brintos/linux#114)" # ------------------------------------------------------------------ the ticker # A 1 s ticker, ten ticks, then the callback calls aml_exit and aml_run returns # on its own. ~10 s, and the output is fully determined. if [ "$loop_state" = enosys ]; then bst_skip aml/ticker-rc "$WHY_T" bst_skip aml/ticker-sequence "$WHY_T" bst_skip aml/ticker-stops-at-10 "$WHY_T" elif [ -f "$B/ticker" ]; then t_out=$(timeout 40 "$B/ticker" 2>&1); t_rc=$? t_exp=$(i=1; while [ "$i" -le 10 ]; do echo "tick $i!"; i=$((i+1)); done; echo "Exiting...") check_eq aml/ticker-rc 0 "$t_rc" check_eq aml/ticker-sequence "$t_exp" "$t_out" # aml_exit must STOP the loop, not merely mark it: an eleventh tick would mean # aml_run drained one more dispatch round on its way out. case "$t_out" in *"tick 11!"*) bst_notok aml/ticker-stops-at-10 "an 11th tick was dispatched" ;; *) bst_ok aml/ticker-stops-at-10 ;; esac else for c in ticker-rc ticker-sequence ticker-stops-at-10; do bst_skip "aml/$c" "no $B/ticker" done fi # ----------------------------------------------------------- the nested ticker # nested-ticker drives the loop BY HAND -- aml_poll(0) + aml_dispatch inside its # own poll(2) on aml_get_fd() -- so the aml_exit its on_tick calls at ten ticks # has nothing to unblock: only aml_run reads that flag, and upstream's loop # condition is `while (!do_exit)`, set solely by SIGINT. This binary is # therefore EXPECTED to tick past ten and to keep going until it is signalled. # Both halves are asserted, because the ticket's "completes and unwinds at ten" # reading is not what the shipped example does. if [ "$loop_state" = enosys ]; then for c in nested-sigint-rc nested-ticks nested-runs-past-10 nested-sigint-unwinds \ nested-tick-rate; do bst_skip "aml/$c" "$WHY_S" done elif [ -f "$B/nested-ticker" ]; then n_out=$(timeout -s INT 15 "$B/nested-ticker" 2>&1); n_rc=$? n_ticks=$(printf '%s\n' "$n_out" | grep -c '^tick [0-9]*!$') check_eq aml/nested-sigint-rc 0 "$n_rc" case "$n_out" in *"tick 1!"*) bst_ok aml/nested-ticks ;; *) bst_notok aml/nested-ticks "no tick in 15 s: $(_bst_1line "$n_out")" ;; esac case "$n_out" in *"tick 11!"*) bst_ok aml/nested-runs-past-10 ;; *) bst_notok aml/nested-runs-past-10 "stopped at ten: $(_bst_1line "$n_out")" ;; esac case "$n_out" in *"Exiting..."*) bst_ok aml/nested-sigint-unwinds ;; *) bst_notok aml/nested-sigint-unwinds "no epilogue: $(_bst_1line "$n_out")" ;; esac # A floor, not a count: 15 s at a 1 s period, plus up to one more before the # loop re-tests its flag. Fewer than twelve would mean the period is not being # honoured, which is the failure a green "it ticked" would hide. if [ "$n_ticks" -ge 12 ]; then bst_ok aml/nested-tick-rate else bst_notok aml/nested-tick-rate "$n_ticks ticks in 15 s, want >= 12" fi else for c in nested-sigint-rc nested-ticks nested-runs-past-10 nested-sigint-unwinds \ nested-tick-rate; do bst_skip "aml/$c" "no $B/nested-ticker" done fi # ------------------------------------------------------------------ the reader # An aml_handler on stdin. on_line fscanf()s one word and prints it; the word # "exit" calls aml_exit, which unblocks aml_run. if [ "$loop_state" = enosys ]; then for c in reader-rc reader-exit-out reader-lines-rc reader-lines reader-eof-respins; do bst_skip "aml/$c" "$WHY" done elif [ -f "$B/reader" ]; then r_out=$(printf 'exit\n' | timeout 20 "$B/reader" 2>&1); r_rc=$? check_eq aml/reader-rc 0 "$r_rc" check_eq aml/reader-exit-out "Got line: exit Exiting..." "$r_out" r2_out=$(printf 'hello\nworld\nexit\n' | timeout 20 "$B/reader" 2>&1); r2_rc=$? check_eq aml/reader-lines-rc 0 "$r2_rc" check_eq aml/reader-lines "Got line: hello Got line: world Got line: exit Exiting..." "$r2_out" # EOF is NOT a clean stop for this example, and the suite states the measured # behaviour rather than the ticket's reading of it. on_line ignores fscanf's # return value, so at EOF the handler stays readable, fscanf leaves `line` # holding the PREVIOUS word, and the example reprints it without end. `head` # closes the pipe after twenty lines and SIGPIPEs the writer, so the spin # costs a few kilobytes rather than the guest's memory. r3_out=$(printf 'hello\n' | timeout -s INT 10 "$B/reader" 2>&1 | head -n 20) r3_n=$(printf '%s\n' "$r3_out" | grep -c '^Got line: hello$') if [ "$r3_n" -ge 2 ]; then bst_ok aml/reader-eof-respins else bst_notok aml/reader-eof-respins "expected a repeat at EOF, saw $r3_n" fi else for c in reader-rc reader-exit-out reader-lines-rc reader-lines reader-eof-respins; do bst_skip "aml/$c" "no $B/reader" done fi # ------------------------------------------------- the probe's loop-bearing phases if [ "$probe_ok" != 1 ] || [ "$loop_state" = enosys ]; then if [ "$loop_state" = enosys ]; then _w=$WHY; else _w="probe could not be staged"; fi for c in probe-loop-rc probe-loop-new probe-loop-fd probe-loop-default \ probe-order-rc probe-order-run probe-order-timer probe-order-shot-last \ probe-order-one-shot probe-order-repeats \ probe-exit-rc probe-exit-returned probe-exit-blocked probe-exit-run \ probe-idle-rc probe-idle-hits \ probe-work-rc probe-work-hits probe-work-done; do bst_skip "aml/$c" "$_w" done for c in probe-signal-rc probe-signal-new probe-signal-hits probe-signal-run; do if [ "$loop_state" = enosys ]; then bst_skip "aml/$c" "$WHY_S" else bst_skip "aml/$c" "probe could not be staged"; fi done else o=$(timeout 20 "$probe" loop 2>&1); rc=$? check_eq aml/probe-loop-rc 0 "$rc" check_eq aml/probe-loop-new "ok" "$(p_get loop_new "$o")" check_eq aml/probe-loop-fd "1" "$(p_get fd_ok "$o")" check_eq aml/probe-loop-default "1" "$(p_get default_ok "$o")" # A 20 ms ticker against a 200 ms one-shot. The claim is the ORDER: every # repeat lands before the single shot, and the shot fires once. o=$(timeout 30 "$probe" order 2>&1); rc=$? ord=$(p_get order "$o") check_eq aml/probe-order-rc 0 "$rc" check_eq aml/probe-order-run "0" "$(p_get run_rc "$o")" check_eq aml/probe-order-timer "1" "$(p_get timer_hits "$o")" check_eq aml/probe-order-shot-last "" "$(printf '%s' "${ord%t}" | tr -d r)" check_eq aml/probe-order-one-shot "1" "$(printf '%s' "$ord" | tr -cd t | wc -c | tr -d ' ')" if [ "$(p_get ticker_hits "$o")" -ge 3 ] 2>/dev/null; then bst_ok aml/probe-order-repeats else bst_notok aml/probe-order-repeats "ticker_hits=$(p_get ticker_hits "$o") order=$ord" fi o=$(timeout 30 "$probe" signal 2>&1); rc=$? check_eq aml/probe-signal-rc 0 "$rc" check_eq aml/probe-signal-new "ok" "$(p_get signal_new "$o")" check_eq aml/probe-signal-hits "1" "$(p_get sig_hits "$o")" check_eq aml/probe-signal-run "0" "$(p_get run_rc "$o")" # aml_exit unblocking aml_run. Bounded from BELOW as well: a run that returned # at once would satisfy "it returned" while proving nothing was ever blocked. o=$(timeout 30 "$probe" exit 2>&1); rc=$? check_eq aml/probe-exit-rc 0 "$rc" check_eq aml/probe-exit-returned "1" "$(p_get run_returned "$o")" check_eq aml/probe-exit-blocked "1" "$(p_get run_blocked "$o")" check_eq aml/probe-exit-run "0" "$(p_get run_rc "$o")" o=$(timeout 30 "$probe" idle 2>&1); rc=$? check_eq aml/probe-idle-rc 0 "$rc" check_eq aml/probe-idle-hits "1" "$(p_get idle_hits "$o")" # The worker pool is the only part of aml that needs a second thread, so this # phase reports on the platform as much as on the library. o=$(timeout 30 "$probe" work 2>&1); rc=$? check_eq aml/probe-work-rc 0 "$rc" check_eq aml/probe-work-hits "1" "$(p_get work_hits "$o")" check_eq aml/probe-work-done "1" "$(p_get done_hits "$o")" fi rm -f "$probe" rm -rf "$W"