. "$BST_LIB" # BST-TIMEOUT: 300 # # nng 1.7.3 (meta-networking recipes-connectivity/nanomsg) -- nanomsg-next- # generation, the brokerless messaging library: pair, req/rep, pub/sub and # pipeline as socket types over inproc, ipc and tcp, plus the nngcat command-line # tool. Successor to nanomsg (88-nanomsg.test); the two coexist, nng keeping its # compat headers under /usr/include/nng/compat/nanomsg/. # # EVERY EXPECTED VALUE BELOW IS PINNED FROM AN x86-64 CONTROL, not guessed: the # same SRCREV (85fbe7f9) configured with exactly the recipe's cmake switches, # built natively, and driven through this same sequence on 2026-08-18. Where a # case says "control", that is what it means. The control was run TWICE -- once # normally, and once under an LD_PRELOAD that fails every pthread_create with # EAGAIN, which is what this platform does to threads -- so both the green shape # and the red shape below were observed before this file shipped. # # FOUR PLATFORM FACTS SHAPE THIS FILE. # # 1. NNG CANNOT START HERE, AND THE SUITE MEASURES *WHICH* GATE STOPS IT RATHER # THAN ASSUMING ONE. That distinction is the whole of this section, and it is # here because assuming was wrong on the first target run. # # nng reports a single error code out of nng_*_open() for "the library could # not start", and there are two independent platform gates behind it, reached # in this order inside nni_plat_init(): # # (a) THE POLLER. nni_posix_pollq_sysinit() runs FIRST, before any thread # exists. The epoll backend calls epoll_create1() and eventfd(), and # both are absent from the wasm32 syscall table and answer ENOSYS, # which nng maps to NNG_ENOTSUP (rv=9). That is brintos/linux#53 # (epoll_create/epoll_create1 return ENOSYS although CONFIG_EPOLL=y) # and brintos/linux#76 (eventfd absent though fs/eventfd.o is linked). # nng_%.bbappend denies both cmake probes, which selects upstream's # poll(2) multiplexer -- the same resolution nanomsg and mosquitto # reached on this image -- so this gate should NOT be the one a current # image hits. It is described because a rebuild that lost those denials # would land here, and the case detail would then say so by itself. # # (b) THE THREADS. Past the poller, nng starts a task queue, a reaper and # an aio expiry thread before any socket exists, and this platform # refuses a thread-mode clone outright: the kernel prints "wasm32: # REFUSING thread-mode clone (CLONE_VM, flags=0x3d0f00) with EAGAIN -- # the coro substrate's runtime protocol state is single-lane per # process", pthread_create() gets EAGAIN, and nng maps that to # NNG_ENOMEM (rv=2). That is brintos/linux-in-the-browser#85 (open), # with brintos/hardwarejs#12 behind it for the join side. # # THE FIRST READING OF A TARGET RUN GOT THIS WRONG, which is why this is # measured now. Every red was attributed to the thread gap because that is # what the sibling ports hit -- and the run had in fact stopped at (a), with # rv=9 and not one thread ever attempted. The tell was in the log and was # nearly missed: the kernel printed NO refusal line at all. # # SO $GATE IS KEYED ON NNG'S OWN ERROR CODE (gate_of, below), with the # probe's direct epoll_create1()/eventfd()/pipe()/pthread_create() calls as # corroboration -- and that order matters. The obvious arrangement, naming # whichever facility the probe finds missing, was tried and is wrong: epoll # and eventfd remain ENOSYS on this kernel after the bbappend selects the # poll(2) backend, so it blamed a poller nng was no longer using while the # run had actually stopped at the threads. Only nng knows which gate it # reached; the probe can only say which facilities exist. # # EVERY CASE BLOCKED BY A GATE IS LEFT RED AND ATTRIBUTED, never skipped, so # it flips green by itself when the gate lifts -- the same choice 88-civetweb # and 79-libdaq make. Each such red carries $GATE in its detail, so a sweep # can separate "the platform cannot do this" from "nng regressed" by grep. # # IT IS NOT A THREAD-COUNT PROBLEM either, which is the next thing anyone # will assume. nng settles that in its own source: src/core/taskq.c reads # `if (num_thr < 2) { num_thr = 2; }` AFTER applying # NNG_INIT_NUM_TASK_THREADS, so nng's floor is two threads and no tuning # reaches a working library on a platform that refuses the first one. Nor is # HWJS_EXPERIMENTAL_THREAD_CLONE=1 a way out: it is read from the HOST node # process's environment, so nothing in the guest can set it. # # WHAT IS NOT RED IS NOT NOTHING, and on this package it is a larger share # than the gap suggests. nng_msg_*, nng_url_parse, nng_version, nng_strerror # and nng_tls_config_alloc all skip nni_init() -- the message data path and # the address parser every transport is built on -- so the probe exercises # them for real and they pass, as does `nngcat --version`. Two cases assert # what is genuinely good here: nng REPORTS the refusal through its own API # and nngcat DIAGNOSES it on stderr, rather than either trapping and taking # the guest down. Both are written to hold on BOTH sides of the transition. # 2. `sleep N` DOES NOT SLEEP (brintos/linux#48, linux-in-the-browser#120), # and timeout(1) DID NOT FIRE before the brintos/linux#77 fix, alarm(2) # being ENOSYS. Every wait below therefore spins against /proc/uptime and # every bound is enforced by this file, not by a timer. # 3. EVERY EXEC COSTS A PARK SLOT. hwjs-cc#69 orphans one per exec and # brintos/hardwarejs#28 leaves the lane unreclaimed, so an exec-happy suite # does not merely run slowly -- it can WEDGE THE GUEST and take every result # with it (measured on 88-civetweb: 62 substitutions wedged 2 runs in 3). # This file spends 12. File tests are builtins, the probe's twenty-odd # measurements are parsed in ONE builtin pass, `check_out`/`check` are avoided # because they fork per case while `check_eq` does not, and files are read # with `read` rather than `$(cat f)`. # 4. LOOPBACK BY ADDRESS. 127.0.0.1 numerically, never a name, so nothing # traverses getaddrinfo(3)'s dns leg -- independently broken on this image # (linux-in-the-browser#96) and it would otherwise turn a messaging result # into a resolver result. lo is the only interface this guest has. # # PORT 8091 and up, not the 8088-8090 that 88-civetweb uses and not lighttpd's # 8080: a listener this file did not start must never be mistaken for one it did. # The probe takes a FIRST port and uses four consecutive ones (8091-8094). require /usr/bin/nngcat nng/present CAT=/usr/bin/nngcat PROBE=/usr/libexec/nng-tests/nng-probe PORT0=8091 # the probe's first tcp port; it uses PORT0..PORT0+3 CPORT=8095 # the nngcat round trip's own port CHEX=1F9F # printf '%04X' 8095 PAYLOAD=nng-cat-fixed-payload D=${TMPDIR:-/tmp}/bst-nng.$$ rm -rf "$D" mkdir -p "$D" || { bst_notok nng/workdir "cannot create $D"; exit 0; } # $GATE is set by gate_of() from what nng itself reported, once per transport. # Until then it names no issue: a red must never claim an attribution this run # did not establish. GATE="platform gate not identified -- nng-probe did not report one" now() { read NOW _rest < /proc/uptime; NOW=${NOW%.*}; } spin() { _i=0; while [ $_i -lt 20000 ]; do _i=$((_i + 1)); done; } # have -- one case per shipped artifact, with no process spent. have() { if [ -f "$2" ]; then bst_ok "$1"; else bst_notok "$1" "missing: $2"; fi } # run_bounded -- run something that may never return under # a wall-clock bound this file imposes (fact 2). Sets RB_RC, or 124 on the bound. run_bounded() { _lim=$1; _tag=$2; shift 2 rm -f "$D/$_tag.done" { "$@" > "$D/$_tag.log" 2>&1; echo $? > "$D/$_tag.done"; } & _p=$! now; _s=$NOW while [ ! -f "$D/$_tag.done" ]; do now if [ $((NOW - _s)) -ge "$_lim" ]; then kill -9 "$_p" 2>/dev/null RB_RC=124 return 0 fi spin done read RB_RC < "$D/$_tag.done" return 0 } # slurp -> SLURP, newlines folded to spaces. # # The [ -f ] guard is not defensive noise: a redirection is performed BEFORE the # stderr redirect beside it, so `done < missing 2>/dev/null` still prints the # shell's "cannot open" to the real stderr -- straight into the runner's # transcript, where it reads as a bug in this suite. Measured on the x86-64 # control, on the path where the publisher never runs because no socket opened. slurp() { SLURP= [ -f "$1" ] || return 0 # BST-REDIRECT-OK: its callers pass $D/*, under $TMPDIR. while IFS= read -r _l || [ -n "$_l" ]; do SLURP="$SLURP$_l "; done < "$1" } ## ## 1. What the port put on the image ## # The deliverable is a static archive plus headers plus nngcat: this distro is # --disable-shared and the bbappend turns the stock recipe's # -DBUILD_SHARED_LIBS=ON off, so a libnng.so here would mean that switch stopped # taking effect. have nng/lib-static /usr/lib/libnng.a have nng/header /usr/include/nng/nng.h have nng/cmake-package /usr/lib/cmake/nng/nng-config.cmake have nng/nngcat-present /usr/bin/nngcat have nng/probe-present "$PROBE" # 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 nng/no-shared "/usr/lib/libnng.so*" # One case for the four protocol headers the ticket names, since a port that # shipped three of them is one finding, not three. if [ -f /usr/include/nng/protocol/pair1/pair.h ] && [ -f /usr/include/nng/protocol/reqrep0/req.h ] && [ -f /usr/include/nng/protocol/pubsub0/sub.h ] && [ -f /usr/include/nng/protocol/pipeline0/push.h ]; then bst_ok nng/protocol-headers else bst_notok nng/protocol-headers "one of pair1/reqrep0/pubsub0/pipeline0 is missing" fi # nng and nanomsg are separate ports of two separate libraries and both are on # this image. nng's nanomsg-compatibility headers live under its own nng/compat # prefix, so the case worth asserting is that neither has displaced the other. if [ -f /usr/include/nng/compat/nanomsg/nn.h ] && [ -f /usr/include/nanomsg/nn.h ]; then bst_ok nng/coexists-with-nanomsg else bst_notok nng/coexists-with-nanomsg \ "nng compat header and/or nanomsg's own nn.h missing -- the two ports collided" fi ## ## 2. nngcat, on the surface that needs no socket ## V=$($CAT --version 2>&1) check_eq nng/nngcat-version 1.7.3 "$V" # --help exits 1 upstream (control), so the case is about the text, not the rc -- # check_out would fail it for the wrong reason. H=$($CAT --help 2>&1) case "$H" in *--req0*--pub0*--push0*) bst_ok nng/nngcat-usage ;; *) bst_notok nng/nngcat-usage "usage text does not list the protocols: $(_bst_1line "$H")" ;; esac ## ## 3. The platform gates, and the library surface that needs no nng thread ## # nng_msg_*, nng_url_parse, nng_version, nng_strerror and nng_tls_config_alloc # all skip nni_init(), so this whole section runs for real on this platform. It # is the part of nng the image can genuinely use today, and it is asserted at the # same depth it would be on a host with threads. The PLAT-* lines in the same # run are what $GATE is built from (fact 1). if [ ! -f "$PROBE" ]; then bst_notok nng/surface-completes "$PROBE is not installed -- nng-tests missing from the image" else run_bounded 60 surface "$PROBE" surface # ONE builtin pass (fact 3). Unset keys stay empty, which fails their case # rather than passing it vacuously. P_EPOLL=; P_EVENTFD=; P_PIPE=; P_THREAD= S_VER=; S_VERM=; S_ENOMEM=; S_TLS=; S_ALLOC=; S_APPEND=; S_INSERT= S_TRIM=; S_HLEN=; S_HU32=; S_DUP=; S_UINP=; S_UIPC=; S_UTCP=; S_LAST= while read -r _k _v _rest || [ -n "$_k" ]; do [ -n "$_k" ] && S_LAST="$_k $_v" case "$_k" in PLAT-EPOLL) P_EPOLL="$_v $_rest" ;; PLAT-EVENTFD) P_EVENTFD="$_v $_rest" ;; PLAT-PIPE) P_PIPE="$_v $_rest" ;; PLAT-THREAD) P_THREAD="$_v $_rest" ;; VERSION) S_VER=$_v ;; VERSION-MATCH) S_VERM=$_v ;; STRERROR-ENOMEM) S_ENOMEM="$_v $_rest" ;; TLS-OFF) S_TLS=$_v ;; MSG-ALLOC) S_ALLOC=$_v ;; MSG-APPEND) S_APPEND=$_v ;; MSG-INSERT) S_INSERT=$_v ;; MSG-TRIM-CHOP) S_TRIM=$_v ;; MSG-HEADER-LEN) S_HLEN=$_v ;; MSG-HEADER-U32) S_HU32=$_v ;; MSG-DUP) S_DUP=$_v ;; URL-INPROC) S_UINP=$_v ;; URL-IPC) S_UIPC=$_v ;; URL-TCP) S_UTCP=$_v ;; esac done < "$D/surface.log" 2>/dev/null if [ "$RB_RC" = "0" ]; then bst_ok nng/surface-completes elif [ "$RB_RC" = "124" ]; then bst_notok nng/surface-completes "probe did not finish in 60 s; last line: $(_bst_1line "$S_LAST")" else bst_notok nng/surface-completes "rc=$RB_RC; last line: $(_bst_1line "$S_LAST")" fi check_eq nng/version 1.7.3 "$S_VER" check_eq nng/version-matches ok "$S_VERM" # The exact text NNG_ENOMEM carries, because that is the string a # thread-starved nng_*_open() reports and a red below may quote it. check_eq nng/strerror-enomem "Out of memory" "$S_ENOMEM" # The ticket asks for TLS backed off. Asked of the shipped library, not of the # recipe: nng compiles nng_tls_config_alloc() either way and answers # NNG_ENOTSUP when no engine was built in (control, both arms). check_eq nng/tls-off ok "$S_TLS" check_eq nng/msg-alloc ok "$S_ALLOC" check_eq nng/msg-append ok "$S_APPEND" check_eq nng/msg-insert ok "$S_INSERT" check_eq nng/msg-trim-chop ok "$S_TRIM" check_eq nng/msg-header-len ok "$S_HLEN" check_eq nng/msg-header-u32 ok "$S_HU32" check_eq nng/msg-dup ok "$S_DUP" check_eq nng/url-inproc ok "$S_UINP" check_eq nng/url-ipc ok "$S_UIPC" check_eq nng/url-tcp ok "$S_UTCP" # The poll(2) fallback the bbappend selects needs pipe(2). Asserted rather than # assumed: if this ever goes red, denying epoll/eventfd stops being a fix. check_eq nng/plat-pipe-available ok "${P_PIPE% *}" fi ## ## 4. The four protocols, end to end, over each transport ## # One probe run per transport, each running BOTH ends of every exchange inside # one process -- which is what the ticket asks for and what nngcat cannot do. # The probe exits 3 when nng could not initialise, which is a platform gate and # is attributed as such; it exits 1 only when an exchange ran and answered # wrongly, which would be a real regression. # gate_of -- set $GATE from what nng ITSELF reported, corroborated by # the probe's syscall measurements. # # The error code is the primary evidence and the syscalls are the corroboration, # not the other way round. An earlier version had it backwards -- it named the # poller whenever epoll/eventfd were absent -- and that went wrong the moment the # bbappend selected the poll(2) backend: epoll and eventfd are still ENOSYS on # this kernel and nng no longer calls either, so the run stopped at the threads # while the detail blamed a poller nng was not using. Only nng knows which gate # it reached; the probe can only say which facilities exist. # # rv=2 NNG_ENOMEM -- nni_plat_thr_init() mapped pthread_create's EAGAIN # rv=9 NNG_ENOTSUP -- a syscall answered ENOSYS (posix_debug.c's errno map), # which for nng's init path means the poller gate_of() { case "$1" in "rv=2"*) GATE="threads refused (brintos/linux-in-the-browser#85, brintos/hardwarejs#12) -- pthread_create $P_THREAD" ;; "rv=9"*) GATE="poller refused (brintos/linux#53 epoll, brintos/linux#76 eventfd) -- epoll=$P_EPOLL eventfd=$P_EVENTFD; the bbappend's -DNNG_HAVE_EPOLL=0 -DNNG_HAVE_EVENTFD=0 should have selected poll(2), whose waker is pipe(2)" ;; "rv=0"*) GATE="no gate -- nng started" ;; *) GATE="platform gate not identified -- nng reported '$1'; epoll=$P_EPOLL eventfd=$P_EVENTFD pipe=$P_PIPE pthread_create=$P_THREAD" ;; esac } rt_transport() { _tr=$1; _arg=$2 if [ ! -f "$PROBE" ]; then bst_notok "nng/init-$_tr" "$PROBE is not installed" return 0 fi run_bounded 90 "rt-$_tr" "$PROBE" rt "$_tr" "$_arg" R_INIT=; R_PAIR_A=; R_PAIR_B=; R_RQ=; R_RP=; R_FILTER=; R_QUIET=; R_ORDER= R_LAST= while read -r _k _v _rest || [ -n "$_k" ]; do [ -n "$_k" ] && R_LAST="$_k $_v $_rest" case "$_k" in INIT) R_INIT="$_v $_rest" ;; PAIR-A2B) R_PAIR_A=$_v ;; PAIR-B2A) R_PAIR_B=$_v ;; REQREP-REQUEST) R_RQ=$_v ;; REQREP-REPLY) R_RP=$_v ;; PUBSUB-FILTER) R_FILTER=$_v ;; PUBSUB-QUIET) R_QUIET=$_v ;; PIPELINE-ORDER) R_ORDER=$_v ;; esac done < "$D/rt-$_tr.log" 2>/dev/null gate_of "$R_INIT" # The library either started or it did not, and that single fact decides how # every case under it is reported. case "$R_INIT" in "rv=0"*) bst_ok "nng/init-$_tr" ;; "rv="*) bst_notok "nng/init-$_tr" "$GATE -- nng_pair1_open: $R_INIT" ;; *) bst_notok "nng/init-$_tr" "no INIT line (rc=$RB_RC); last: $(_bst_1line "$R_LAST")" ;; esac # A hang is its own finding. rc=3 is a platform gate; anything else with cases # missing is reported with what the probe last managed to say. for _c in pair-a2b:"$R_PAIR_A" pair-b2a:"$R_PAIR_B" reqrep-request:"$R_RQ" \ reqrep-reply:"$R_RP" pubsub-filter:"$R_FILTER" pubsub-quiet:"$R_QUIET" \ pipeline-order:"$R_ORDER"; do _name=${_c%%:*}; _verdict=${_c#*:} if [ "$_verdict" = "ok" ]; then bst_ok "nng/$_name-$_tr" elif [ "$RB_RC" = "124" ]; then bst_notok "nng/$_name-$_tr" "probe did not finish in 90 s; last: $(_bst_1line "$R_LAST")" elif [ "$RB_RC" = "3" ]; then bst_notok "nng/$_name-$_tr" "$GATE -- no socket could be opened, so this exchange never ran" elif [ -z "$_verdict" ]; then bst_notok "nng/$_name-$_tr" "no result line (rc=$RB_RC); last: $(_bst_1line "$R_LAST")" else bst_notok "nng/$_name-$_tr" "$_verdict (rc=$RB_RC); last: $(_bst_1line "$R_LAST")" fi done } rt_transport inproc - rt_transport ipc "$D" rt_transport tcp "$PORT0" ## ## 5. nngcat on the command line -- the ticket's two-process case ## # One nngcat publishes a fixed payload, another subscribes and writes it to a # file this case compares byte-exactly. The SUBSCRIBER listens and the publisher # dials, which is the arrangement that works: nngcat dials synchronously and the # control showed the other way round losing the race with "Unable to dial on # tcp://127.0.0.1:PORT: Connection refused". rm -f "$D/cat.out" ( $CAT --sub0 --listen "tcp://127.0.0.1:$CPORT" --subscribe '' \ --recv-timeout 20 --count 1 --raw > "$D/cat.out" 2> "$D/cat.sub.err" ) & SUBPID=$! # Wait for the subscriber's LISTEN, read from /proc rather than through `ss` so # the case asserts the KERNEL's view. Give up early if the child is already gone # -- with no socket obtainable it dies immediately, and spinning the full bound # for a process that has exited would cost 20 s per run for nothing. now; _s=$NOW; LISTENING=0 while [ -d "/proc/$SUBPID" ]; do while read -r _sl _la _rest; do case "$_la" in *:$CHEX) case "$_rest" in *" 0A "*) LISTENING=1 ;; esac ;; esac done < /proc/net/tcp 2>/dev/null [ "$LISTENING" = "1" ] && break now; [ $((NOW - _s)) -ge 20 ] && break spin done if [ "$LISTENING" = "1" ]; then run_bounded 40 cat-pub $CAT --pub0 --dial "tcp://127.0.0.1:$CPORT" \ --data "$PAYLOAD" --count 1 --delay 1 --interval 1 PUBRC=$RB_RC else # No listener, so nothing to publish to. Create the log anyway: every reader # below is uniform, and an absent file would be indistinguishable from an # empty one. : > "$D/cat-pub.log" PUBRC=skipped fi # Wait for the subscriber to finish writing before reading its file. now; _s=$NOW while [ -d "/proc/$SUBPID" ]; do now; [ $((NOW - _s)) -ge 30 ] && kill -9 "$SUBPID" 2>/dev/null && break spin done slurp "$D/cat.out"; GOT=${SLURP% } slurp "$D/cat.sub.err"; SUBERR=${SLURP% } if [ "$GOT" = "$PAYLOAD" ]; then bst_ok nng/nngcat-pubsub-file elif [ "$LISTENING" != "1" ]; then bst_notok nng/nngcat-pubsub-file \ "$GATE -- the subscriber never listened: $(_bst_1line "$SUBERR")" else bst_notok nng/nngcat-pubsub-file \ "pub rc=$PUBRC want:'$PAYLOAD' got:'$(_bst_1line "$GOT")' err:'$(_bst_1line "$SUBERR")'" fi ## ## 6. The cases that hold on BOTH sides of the platform gates ## # These are the ones worth reading. They pass today, with no socket obtainable, # AND after the gates lift, when every socket opens -- so they assert the # property that actually matters (nng reports what it cannot do, through its own # API and on stderr, rather than trapping) instead of asserting today's symptom. # A case written the obvious way round -- "opening a socket fails" -- would go # red the day the platform is fixed, which is the wrong direction for a # regression to point. case "$R_INIT" in "rv=0"*|"rv="*[!\ ]*) # Either it initialised, or it returned an nng error code carrying nng's own # text for it. What must never happen is the probe dying without saying why. if [ "$RB_RC" = "0" ] || [ "$RB_RC" = "3" ]; then bst_ok nng/open-failure-is-catchable else bst_notok nng/open-failure-is-catchable "rc=$RB_RC with INIT '$R_INIT'" fi ;; *) bst_notok nng/open-failure-is-catchable "no INIT line at all (rc=$RB_RC)" ;; esac # No red in this file may rest on an attribution the run did not establish. This # case is the guard on that, and it is the one that would have caught the first # target run's misreading: it goes red if nng failed to start while the probe # named no gate, which is exactly the state in which a hand-written attribution # would be a guess. case "$R_INIT" in "rv=0"*) bst_ok nng/init-failure-is-explained ;; *) case "$GATE" in "platform gate not identified"*|"no gate"*) bst_notok nng/init-failure-is-explained \ "nng did not start ($R_INIT) but no platform gate was measured: $GATE" ;; *) bst_ok nng/init-failure-is-explained ;; esac ;; esac slurp "$D/cat-pub.log"; PUBOUT=${SLURP% } if [ "$PUBRC" = "0" ]; then bst_ok nng/nngcat-diagnoses-refusal else case "$PUBOUT$SUBERR" in *"Unable to open socket"*|*"Out of memory"*|*"Not supported"*) bst_ok nng/nngcat-diagnoses-refusal ;; *) bst_notok nng/nngcat-diagnoses-refusal \ "nngcat neither worked nor said why (pub rc=$PUBRC): $(_bst_1line "$PUBOUT$SUBERR")" ;; esac fi rm -rf "$D"