. "$BST_LIB" # BST-TIMEOUT: 1500 # # nopoll 0.4.6.b400 (meta-networking recipes-protocols/nopoll) -- the noPoll # WebSocket library, RFC 6455 in ansi C, ws:// and wss:// over openssl. The # image's FIRST WebSocket implementation: the networking tier had HTTP servers # (lighttpd, civetweb), an MQTT broker and a messaging pair (nanomsg, nng), but # nothing that speaks the WebSocket upgrade or its framing. # # EVERY EXPECTED VALUE BELOW IS PINNED FROM AN x86-64 CONTROL, not guessed: the # same tarball, configured as the recipe configures it, built natively and driven # through this same sequence on 2026-08-19. The control was run in BOTH network # shapes -- an ordinary host, and a network namespace holding only `lo' with # IPv6 administratively disabled, which is this guest's shape -- because the two # disagree sharply and only the second predicts the target. # # THREE FACTS SHAPE THIS FILE. # # 1. NOTHING RUNS IN PLACE. Both upstream programs open their fixtures BY # RELATIVE PATH from the working directory (test-certificate.crt, # test-private.key, server.pem, root.pem, client.pem) and both also WRITE # there -- the client creates `tmp', the listener appends through its # open-file command and reads nopoll-regression-client.c for its file # transfer case. The rootfs is read-only at browser runtime and do_install # ships the payload 0644, so section A stages the whole directory into /tmp # and everything below runs from there. /tmp is a mode=1777 tmpfs with no # noexec, which is what makes the chmod +x work. # # 2. THE UPSTREAM REGRESSION CLIENT IS MONOLITHIC AND HAS AN UPSTREAM CEILING. # It runs ~50 cases in a fixed order and `return -1's at the first failure, # with no selector, so everything after the first red is simply not run. # That ceiling is TEST 17 and it is NOT this platform's: # # stock tarball, x86-64, full network 16 cases, then Test 06 (TLS) # fresh fixtures, x86-64, full network 28 cases, then Test 17 # fresh fixtures, x86-64, lo-only, no IPv6 28 cases, then Test 17 # # Test 06 falls to the tarball's own expired 1024-bit/SHA-1 certificates, # which nopoll_%.bbappend regenerates at build time. Test 17 hand-writes a # malformed frame header (opcode 0x7E with every RSV bit set, a 127 length # byte and a mask sent with the MASK bit clear) and expects a parse this # nopoll does not perform; it fails identically on a stock workstation, in # both network shapes, deterministically. So `nopoll/regression-ceiling' # below records WHERE the run stopped rather than asserting it got further, # and the ~19 cases past 17 are not claimed by this suite at all. # # 3. THE PAIR CANNOT MAKE TWO OF THE ASSERTIONS THE PORT IS FOR, WHICH IS WHY # THERE IS A PROBE. NOPOLL_BINARY_FRAME appears NOWHERE in upstream's test/ # directory -- the regression suite sends text only -- and the pair never # compares received bytes with sent ones, only sizes. A payload arriving # with the right length and the wrong contents passes every case it has. # Section B is the byte-exact half and it is the load-bearing one. # # NO SIGNAL IS EVER SENT TO THE LISTENER. brintos/linux#21 delivers none, # SIGKILL included, so the backgrounded listener is started, used, and left; the # guest is torn down after the suite. Nothing below wait(1)s on it. require /usr/libexec/nopoll-tests/nopoll-probe nopoll/present D=${TMPDIR:-/tmp}/bst-nopoll.$$ SRC=/usr/libexec/nopoll-tests rm -rf "$D"; mkdir -p "$D" || { bst_notok nopoll/workdir "cannot create $D"; exit 0; } # =========================================================================== # A. STAGING -- THE FIXTURES ONLY. THE PROGRAMS ARE EXEC'D WHERE THEY LIE. # # The three programs are ~41 MB EACH: every one is a whole-program static link of # libnopoll + libssl + libcrypto, which is simply what this arm costs (the gensio # probe is 45 MB for the same reason, and -Os -g0 moves ours by 27 kB -- the size # is OpenSSL and the coroutinize expansion, not DWARF). # # COPYING ONE INTO /tmp IS WHAT MAKES IT UNEXECUTABLE, AND THAT IS THE WHOLE # REASON THIS SECTION LOOKS THE WAY IT DOES. /tmp is a RAM-backed tmpfs, and an # exec image is staged out of that same heap as a segment list capped at # WASM32_IMG_MAX_SEGS=1024. Fill the heap with a 41 MB copy first and it can only # hand out 8 kB blocks, which puts the ceiling at ~8 MB and the kernel refuses in # as many words: # # [k] binfmt_wasm: staging a 41482909-byte image needs more than 1024 segments # (heap too fragmented to hand out 8192-byte blocks; cap # WASM32_IMG_MAX_SEGS=1024) — refusing -ENOMEM rather than degrading # further (never-silent) # # MEASURED ON TARGET, one variable at a time, same image: # # probe copied to /tmp, then exec'd -> ENOMEM; 11 cases UNRUN # probe exec'd in place from /usr -> ALL 11 PASS # pair copied to /tmp, then exec'd -> ENOMEM; the listener never started # pair exec'd in place from /usr -> runs, reaches the upstream ceiling # # So nothing is copied. nopoll_%.bbappend installs all three 0755, so they are # executable where they lie, and what they actually need from a writable place is # a writable WORKING directory -- they open their fixtures by relative path and # write `tmp' and their file-transfer output there. $D is that directory and holds # the fixtures alone, ~120 kB. # # This is a DEPARTURE from the copy-to-/tmp idiom the other probes in this layer # use, and deliberately so: those are staged from brintos-selftests' own `data/', # which ships 0644 and cannot be exec'd in place at all. A program installed 0755 # by its own recipe has no such constraint, and at this size the copy is actively # harmful. # # ONE CAVEAT, SEEN ONCE AND NOT REPRODUCED: a boot exec'ing in place wedged with # "HWJS-B-2 exec: kernel-routed wasm32_user_exec_load trapped" -- a coroutinized # frame parked under a native caller (brintos/hwjs-cc#69) whose lane is never # reclaimed (brintos/hardwarejs#28). Three later boots of the same design did not. # It is recorded rather than designed around because the alternative is the copy, # which fails every time instead of occasionally. for f in "$SRC"/*.pem "$SRC"/*.crt "$SRC"/*.key "$SRC"/nopoll-regression-client.c; do cp "$f" "$D"/ 2>/dev/null || { bst_notok nopoll/stage "cannot stage $f into $D"; exit 0; } done bst_ok nopoll/stage cd "$D" || { bst_notok nopoll/workdir-cd "cannot cd $D"; exit 0; } # The library ships as a static archive and a header; assert the deliverable # exists before anything is run against it. check nopoll/libnopoll-a test -f /usr/lib/libnopoll.a check nopoll/header test -f /usr/include/nopoll/nopoll.h check nopoll/pkgconfig test -f /usr/lib/pkgconfig/nopoll.pc # =========================================================================== # B. THE PROBE -- the port's own assertions, byte-exact, both ends in one # process. This is what the ticket asks for and it does not depend on the # upstream pair reaching any particular case. # # Its ids in the order the probe emits them. Declared here rather than derived # from the output so that a case the probe never reached becomes a red instead # of vanishing from the count -- an absent case and a passing one are otherwise # indistinguishable in a summary. PROBE_CASES="loopback-control ctx listener connect accept handshake text-frame binary-frame fragment-reassemble close-client close-peer" POUT=$D/probe.out; PERR=$D/probe.err : > "$POUT"; : > "$PERR" "$SRC/nopoll-probe" 21049 > "$POUT" 2> "$PERR"; PRC=$? PERRTXT=$(cat "$PERR" 2>/dev/null) # The probe's exec image is a static link of libnopoll + libssl + libcrypto, and # this guest intermittently refuses to stage a large one (brintos/linux#44, # binfmt_wasm segment cap). A case whose command was never staged must not claim # a verdict, so those become bst_unrun rather than reds that read like a package # regression. if [ ! -s "$POUT" ] && bst_execfail "$PERRTXT"; then for c in $PROBE_CASES; do bst_unrun "nopoll/$c" "$PERRTXT"; done else awk -v cases="$PROBE_CASES" -v whynot="probe rc=$PRC $(_bst_1line "$PERRTXT")" ' function oneline(s) { gsub(/[\n\t]/, " ", s) if (length(s) > 100) s = substr(s, 1, 100) return s } $1 == "R" && $2 ~ /^nopoll\// { st[$2] = $3 d = "" for (i = 4; i <= NF; i++) d = d (i > 4 ? " " : "") $i det[$2] = d next } END { n = split(cases, c, /[ \t\n]+/) for (i = 1; i <= n; i++) { if (c[i] == "") continue id = "nopoll/" c[i] if (st[id] == "OK") printf "BST ok %s\n", id else if (st[id] == "FAIL") printf "BST not ok %s : %s\n", id, oneline(det[id]) else printf "BST not ok %s : nopoll-probe reported no result for this case -- %s\n", id, whynot } }' "$POUT" fi # =========================================================================== # C. UPSTREAM'S OWN REGRESSION PAIR, TWO PROCESSES OVER THE LOOPBACK # # Genuinely a different assertion from B rather than a repeat of it: these are # the shipped programs, in separate processes, exercising the library through # its event loop (nopoll_loop_wait) and its TLS listeners, which one in-process # probe does not reach. # # The client is run under timeout(1) so a guest slower than the control cannot # cost this file its remaining cases -- test 12 opens and closes 4000 # connections sequentially (3.2 s natively, unknown here) and test 16 spends # 10 s in deliberate sleeps. Whatever the client emitted before it was cut is # still scored below; being cut short shows up as a lower ceiling, not as # silence. LOG=$D/listener.log CLOG=$D/client.log : > "$LOG"; : > "$CLOG" "$SRC/nopoll-regression-listener" > "$LOG" 2>&1 & sleep 3 timeout 900 "$SRC/nopoll-regression-client" > "$CLOG" 2>&1 CRC=$? # EVERY ASSERTION ON THE LISTENER LOG IS MADE AFTER THE CLIENT RUN, NOT BEFORE. # The listener's stdout is a pipe, so libc gives it 4 kB full buffering and its # start-up banner is still sitting in that buffer three seconds in -- asserting # on it then reads an EMPTY log and reports a listener that did not start, while # the same assertions pass at the end of the run. Measured in the x86-64 # dry-run, where the early form produced three reds beside four greens off the # same file. Nothing is lost by waiting: these are statements about the whole # exchange, not about start-up timing. # Upstream prints "[ OK ]" / "[ OK ]" (the spacing varies by case) and # "[ FAILED ]". One awk pass turns the whole transcript into cases, so a # regression names itself instead of collapsing into one aggregate red. # # THE SLUG IS TAKEN FROM THE WHOLE DESCRIPTION, NOT THE "Test NN" LABEL, because # upstream REUSES labels: "Test 04-a", "04-b" and "04-c" each name two different # cases (the streaming-api series, then the non-blocking / overflow / file # series). Slugging the label alone silently merged three pairs and cost this # suite three cases -- 25 where the control reaches 28 -- which is exactly the # shape of a miscount that reads like a partial run. # # A case the patched client SKIPPED is emitted as `skip', not `ok'. test_02a and # test_06a return true after printing their skip, so upstream prints an OK line # for them too; scoring that as a pass would claim IPv6 coverage this guest # cannot have. awk -v crc="$CRC" ' function slug(s) { sub(/\[[^]]*\][ \t]*$/, "", s) gsub(/^Test[ \t]+/, "", s) gsub(/[^A-Za-z0-9-]/, "-", s) gsub(/-+/, "-", s); gsub(/^-|-$/, "", s) return substr(tolower(s), 1, 52) } function label(s) { sub(/^Test[ \t]+/, "", s); sub(/:.*/, "", s); return s } function oneline(t) { gsub(/[\n\t]/, " ", t) if (length(t) > 120) t = substr(t, 1, 120) return t } # THE CLIENT SAYS WHY IT STOPPED, ONE LINE ABOVE THE [ FAILED ], AND THE FIRST # TARGET RUN THREW IT AWAY. The suite deletes its working directory, so the # transcript is gone by the time anyone reads the report -- leaving "upstream # regression case failed" and nothing to act on. Keep the most recent ERROR: # line and hand it to the ceiling case, which is the one a reader consults. /^ERROR/ { why = $0; next } # echoes= AND pings= ARE WHAT LOCATED THE ON-TARGET CEILING, so they ship. # test_sending_and_check_echo() is called exactly twice before test 07, both # inside test_02b -- once BEFORE nopoll_conn_send_ping() and once after. So # echoes=1 with pings=1 says, on its own, that the first echo round-tripped # and the one immediately following the PING did not -- a different and far # more actionable statement than "upstream regression case failed". # NOTE: this awk program is inside a single-quoted shell string, so it must # contain no apostrophe anywhere, comments included. /received reply and echo matches/ { echoes++ ; next } /connection ready, sending PING/ { pinged++ ; next } /SKIPPED, host has no usable IPv6/ { skipped[label($0)] = 1; next } /^Test .*\[ *OK *\]/ { id = slug($0); if (id == "" || seen[id]++) next if (label($0) in skipped) { printf "BST skip nopoll/reg-%s : host has no IPv6 loopback (CONFIG_IPV6 unset)\n", id; s++ } else { printf "BST ok nopoll/reg-%s\n", id; n++ } next } /^Test .*\[ *FAILED *\]/ { id = slug($0); if (id == "" || seen[id]++) next printf "BST not ok nopoll/reg-%s : upstream regression case failed\n", id failed = id next } END { total = n + s if (total >= 28) printf "BST ok nopoll/regression-ceiling\n" else printf "BST not ok nopoll/regression-ceiling : reached %d upstream cases (%d pass, %d skip), control reaches 28 (stopped at %s, client rc=%s) -- last diagnostic: %s\n", total, n, s, (failed ? failed : "no failure recorded"), crc, (why ? oneline(why) : "none printed") " [echoes=" echoes+0 " pings=" pinged+0 "]" }' "$CLOG" # EVERY IPv6 CASE THE CLIENT REACHED MUST HAVE SKIPPED -- bounded from below, # not pinned at two. The client is monolithic and stops at its first failure, so # whether it reaches test_06a at all is a property of the ceiling above rather # than of the IPv6 patch this case is about. Pinning 2 made a ceiling at test_02b # report as an IPv6 regression, which is the opposite of what it is. IPV6_SKIPS=$(grep -c 'SKIPPED, host has no usable IPv6' "$CLOG" 2>/dev/null) if [ "${IPV6_SKIPS:-0}" -ge 1 ] && ! grep -q 'IPv6.*\[ *FAILED *\]' "$CLOG" 2>/dev/null; then bst_ok nopoll/ipv6-cases-skipped else bst_notok nopoll/ipv6-cases-skipped "expected every IPv6 case reached to skip; skips=$IPV6_SKIPS, and an IPv6 case reported FAILED" fi # --------------------------------------------------------------------------- # THE LISTENER'S OWN VIEW -- ASSERTED ONLY IF THE CLIENT DROVE IT THAT FAR. # # These say what the client cannot: that the listener received the payload, # echoed the same size back, saw fragmented frames, and closed. They are real # assertions, and they are also STRICTLY DOWNSTREAM of the client getting past # the corresponding case -- so when the client stops early they cannot be # evaluated at all, and reporting them as failures turns ONE finding into EIGHT. # That is exactly what the first full target run did: the client stopped at # test_02b and seven listener cases went red saying `got:`, none of which # was a fact about the listener. # # The emptiness has a second cause worth stating, because it makes the reds # doubly meaningless: the listener's stdout is a pipe, libc gives it 4 kB of full # buffering, and the listener NEVER EXITS (brintos/linux#21 delivers no signal, # SIGKILL included, so nothing can make it flush). Under ~4 kB of output the log # is empty no matter what the listener did. # # So they are skipped, with the ceiling named as the reason, whenever the log has # not been flushed. Nothing is hidden: `regression-ceiling' above is red in # exactly that case and carries the client's own diagnostic. if [ -s "$LOG" ]; then check_out nopoll/listener-ipv4 "noPoll listener started at: 0.0.0.0:1234" cat "$LOG" check_out nopoll/listener-ipv6-skipped "no usable IPv6 loopback" cat "$LOG" check_out nopoll/listener-tls "starting listener with TLS" cat "$LOG" check_out nopoll/listener-received "Listener received (size: 14" cat "$LOG" check_out nopoll/listener-echoed "Sending reply... (same message size: 14)" cat "$LOG" check_out nopoll/listener-fragments "fragment: 1)" cat "$LOG" check_out nopoll/listener-closes "Reg test: called connection close" cat "$LOG" else for c in listener-ipv4 listener-ipv6-skipped listener-tls listener-received \ listener-echoed listener-fragments listener-closes; do bst_skip "nopoll/$c" "listener log still unflushed (<4 kB, and it never exits to flush) -- the client stopped early; see nopoll/regression-ceiling" done fi rm -rf "$D"