. "$BST_LIB" # BST-TIMEOUT: 300 # measured 25s (28 cases) on a local guest, 2026-08-19; the # # stock 60s bound is too tight for two socket transfers plus # # their poll loops on a loaded host, and 300 is what the # # ticket gates and the nightly already pass. # # iperf 2.0.13 (meta-oe recipes-benchmark) -- the ORIGINAL iperf, and the first # tool on this image that COMPLETES a throughput transfer rather than # demonstrating why one cannot. It is not interface-compatible with the iperf3 # already here (BRINTOS-24): different binary name (/usr/bin/iperf vs # /usr/bin/iperf3), different wire protocol, different options. They coexist. # # WHY IT WORKS WHERE iperf3 DOES NOT, which is the whole point of the port. # 84-iperf3.test's loopback-transfer case is red and stays red: iperf 3.16 # replaced the select() loop over streams with one thread per stream and 3.18 # has no threadless build mode, so pthread_create() is unavoidable and # brintos/linux-in-the-browser#85 takes it every time. iperf2 predates that # design and keeps a maintained no-threads path, so iperf2_%.bbappend builds it # with --disable-threads and nothing here creates a thread at all. # # ORDERING IS LOAD-BEARING, same discipline as 84-iperf3.test. Packaging first, # then identity (which is where the two build decisions are witnessed), then the # argument and error surface, then the socket surface, and the TRANSFERS LAST. A # red in an early block is a packaging or build change; a red in a late one is # the platform or the network stack. # # NOTHING HERE ASSERTS A THROUGHPUT FIGURE. The number is meaningless on this # substrate -- it measures the host's memcpy through a virtualised loopback, not # a network -- so every case asserts that the run COMPLETED and that its report # is WELL-FORMED. Rates are printed for the record instead, tagged IPERF2-*. require iperf iperf2/present PORT=5301 # TCP; 5271 is iperf3's, 12865 netperf's, 7003 tbench's UPORT=5302 # UDP DEADPORT=5303 # deliberately nothing listening D=/tmp/.bst-iperf2 rm -rf "$D"; mkdir -p "$D" # --- packaging --------------------------------------------------------------- # iperf2 ships exactly ONE file to this image: /usr/bin/iperf. src/Makefile.am has # `bin_PROGRAMS = iperf`, and checkdelay/checkpdfs/checkisoch/igmp_querier are # noinst_PROGRAMS and are deliberately NOT installed. That last one matters here: # igmp_querier is the only file in the tree that calls alarm(2), which is ENOSYS # on this arm (brintos/linux#77), and it never reaches the rootfs. # # The man page is NOT asserted, and its absence is not a packaging fault. OE puts # ${mandir} in FILES:${PN}-doc, and this image installs no -doc package at all # (measured: 0 of them in the rootfs manifest). The 2670 man pages that ARE here # arrive by other routes. Asserting iperf.1 would therefore pin an image policy # this ticket does not own, and it would go red for a reason that has nothing to # do with iperf2 -- which is exactly what the first draft of this suite did. inv="" for f in /usr/bin/iperf; do [ -e "$f" ] || inv="$inv $f" done check_eq iperf2/inventory "" "$inv" # The noinst programs must NOT be here. A recipe change that started installing # them would drag in the alarm(2) user above. extra="" for f in /usr/bin/checkdelay /usr/bin/checkpdfs /usr/bin/checkisoch /usr/bin/igmp_querier; do [ -e "$f" ] && extra="$extra $f" done check_eq iperf2/no-noinst-programs "" "$extra" # busybox has no iperf applet, so a bare `iperf` can only be this port. check_eq iperf2/resolves-to-port "/usr/bin/iperf" "$(command -v iperf)" # iperf2 and iperf3 are DIFFERENT PROGRAMS, and the ticket's premise is that # neither substitutes for the other. Pin that they are two files, not a symlink # somebody added to save space -- which would silently turn every case below # into a second, worse iperf3 suite. if [ -L /usr/bin/iperf ] || [ -L /usr/bin/iperf3 ]; then bst_notok iperf2/distinct-from-iperf3 "one of /usr/bin/iperf, /usr/bin/iperf3 is a symlink" elif [ ! -e /usr/bin/iperf3 ]; then bst_notok iperf2/distinct-from-iperf3 "/usr/bin/iperf3 is missing; this image should carry both" else bst_ok iperf2/distinct-from-iperf3 fi # --- identity, and the two build decisions this port rests on ---------------- # `iperf --version` prints ONE line and exits 1 (not 0) -- upstream routes # --version through the same path as a usage error. That is iperf2 behaviour, # not a fault, so the rc is pinned at 1 deliberately: a future release that # "fixed" it would change the contract every script wrapping this tool depends # on, and this case is where we would find out. ver=$(iperf --version 2>&1) rc=$? printf 'IPERF2-VERSION %s\n' "$(_bst_1line "$ver")" check_eq iperf2/version "iperf version 2.0.13 (21 Jan 2019) single threaded" "$ver" check_eq iperf2/version-rc 1 "$rc" # THE THREADING WITNESS, and it is the single most load-bearing case in this # file. That trailing word is emitted by main.cpp from the build's own macros: # "pthreads" when HAVE_POSIX_THREAD is defined, "single threaded" when it is # not. It is therefore an ON-TARGET register of whether iperf2_%.bbappend's # --disable-threads actually took. If this flips to "pthreads" the transfer # cases below will start failing with brintos/linux-in-the-browser#85 park-slot # traps, and this case is what says WHY in one line rather than leaving the # reader to rediscover it from a SIGILL. case "$ver" in *"single threaded"*) bst_ok iperf2/threadless-build ;; *"pthreads"*) bst_notok iperf2/threadless-build "built WITH pthreads: --disable-threads did not take, so linux-in-the-browser#85 governs the transfers below" ;; *) bst_notok iperf2/threadless-build "neither shape: $(_bst_1line "$ver")" ;; esac # The other half of the same witness, from the opposite direction. -P on the # SERVER means "serve N connections then exit", and a threadless build refuses # it in as many words (Settings.cpp). So this message appearing is proof the # binary is the no-threads variant, independently of the version string, and its # ABSENCE would mean -P was accepted -- i.e. a threaded build. pout=$(iperf -s -P 1 --version 2>&1) case "$pout" in *"not valid in single threaded"*) bst_ok iperf2/threadless-rejects-P ;; *) bst_notok iperf2/threadless-rejects-P "no single-threaded warning for -P: $(_bst_1line "$pout")" ;; esac check_out iperf2/help-usage "[-s|-c host] [options]" sh -c 'iperf --help 2>&1; exit 0' # --- argument and error surface (no sockets involved) ------------------------ # A bare `iperf` prints usage and exits 0 -- NOT 1 as iperf3 does. Pinned # because it is exactly the sort of difference the ticket's "not # interface-compatible with iperf3" is about, and because a script that treats # the two alike will be wrong here. check_rc iperf2/no-args-rc 0 iperf check_out iperf2/no-args-usage "Try \`iperf --help' for more information." sh -c 'iperf 2>&1; exit 0' check_out iperf2/bad-option-msg "unrecognized option" sh -c 'iperf --nosuchopt 2>&1; exit 0' # --- socket surface, before any transfer ------------------------------------- # A connect() to a port nothing is listening on. This is a real assertion about # the substrate's TCP: the text is strerror(ECONNREFUSED), so a stack that # swallowed the RST or reported a different errno is visible right here. check_out iperf2/connect-refused "connect failed: Connection refused" \ sh -c "iperf -c 127.0.0.1 -p $DEADPORT -t 1 2>&1; exit 0" check_rc iperf2/connect-refused-rc 1 iperf -c 127.0.0.1 -p $DEADPORT -t 1 # listening_tcp() -- true when the KERNEL shows a LISTEN row # (state 0A) for that local port. Read from /proc rather than through `ss` so # the case asserts the kernel's view and cannot fail for iproute2's reasons. # # Both files are scanned. Measured: this image has no IPv6 at all, so today the # answer always comes from /proc/net/tcp -- but which family a server lands in # is a property of the stack rather than of iperf2, and pinning v4 would go red # the day IPv6 arrives, which would be good news. 84-iperf3.test learned this # the same way. # # Note the `hit` flag rather than a bare `exit 0` inside the rule: awk's exit # jumps to END, whose own exit status wins, so the obvious spelling reports "not # found" every time. listening_tcp() { _hex=$(printf '%04X' "$1") _one() { awk -v p=":$_hex" '$4 == "0A" && index($2, p) { hit = 1 } END { exit !hit }' "$1" 2>/dev/null; } _one /proc/net/tcp || _one /proc/net/tcp6 } bound_udp() { _hex=$(printf '%04X' "$1") _one() { awk -v p=":$_hex" 'NR > 1 && index($2, p) { hit = 1 } END { exit !hit }' "$1" 2>/dev/null; } _one /proc/net/udp || _one /proc/net/udp6 } # THE SERVER RUNS UNDER setsid, and NOT for 84-iperf3.test's reason. That suite # needs a session boundary because its threads trap in the runtime; this build # creates no threads. What it buys here is that a server which outlives its # suite -- and this one must be killed, see below -- is not left in the runner's # own process group, where the supervisor's cleanup would reach it at an # arbitrary later point in an unrelated suite. Every fork/exec also leaks a park # slot (brintos/hwjs-cc#69), so the fewer processes sharing this one's session # the better. # # THE SERVER CANNOT SELF-TERMINATE, unlike iperf3's `-s -1`. iperf2's equivalent # is `-s -P 1`, and a threadless build rejects it (see iperf2/threadless-rejects-P # above) -- Listener.cpp's accept loop runs `while (!sInterupted && ...)` and # nothing else ends it. So the suite kills it, and every case below is written # so that a server killed early reports as a failed assertion rather than a hang. setsid iperf -s -p "$PORT" > "$D/tcp-server.log" 2>&1 & SRV=$! # `sleep 1` does not actually sleep on this arm (linux-in-the-browser#120), so # this is a bounded spin rather than a 25-second wait; the iteration count is # what bounds it, exactly as in 84-iperf3.test. i=0 while [ $i -lt 25 ]; do listening_tcp "$PORT" && break; i=$((i + 1)); sleep 1; done if listening_tcp "$PORT"; then bst_ok iperf2/server-listens else bst_notok iperf2/server-listens "no LISTEN row for port $PORT in /proc/net/tcp{,6} after $i polls: $(_bst_1line "$(cat "$D/tcp-server.log" 2>&1)")"; fi # --- the TCP transfer: what the tool exists for ------------------------------ # -i 1 is NOT decoration. With interval reporting off, Client.cpp:403 skips the # loop's own clock read whenever HAVE_SETITIMER is defined, and the run then has # no exit but a SIGALRM this arm never delivers. iperf2_%.bbappend answers # setitimer "absent" precisely so that a plain `-t` also terminates -- but the # ticket asks for an interval count anyway, and driving it this way exercises # both the interval path and the summary path in one run. out=$(iperf -c 127.0.0.1 -p "$PORT" -t 2 -i 1 2>&1) rc=$? printf '%s\n' "$out" | sed 's/^/IPERF2-TCP-CLIENT /' printf 'IPERF2-TCP-CLIENT-RC %s\n' "$rc" check_eq iperf2/tcp-client-rc 0 "$rc" # A "completed transfer" for iperf2 is the FINAL SUMMARY line: an interval that # starts at 0.0 and spans the whole run. Interval lines alone do not prove the # run ended -- a client killed mid-run still has printed some. summary=$(printf '%s\n' "$out" | grep 'bits/sec' | tail -1) case "$summary" in *" 0.0-"*" sec "*"Bytes"*"bits/sec"*) bst_ok iperf2/tcp-summary-line ;; *) bst_notok iperf2/tcp-summary-line "no whole-run summary: $(_bst_1line "$summary")" ;; esac # Non-zero interval count, per the ticket. -t 2 -i 1 yields two interval lines # plus the summary; the assertion is `at least two reporting lines` rather than # exactly three, because the boundary case (a slow guest emitting a third short # interval) is not a defect and pinning the count would make it one. nlines=$(printf '%s\n' "$out" | grep -c 'bits/sec') printf 'IPERF2-TCP-REPORT-LINES %s\n' "$nlines" if [ "$nlines" -ge 2 ]; then bst_ok iperf2/tcp-interval-count else bst_notok iperf2/tcp-interval-count "want >=2 reporting lines (intervals + summary), got $nlines"; fi # The bandwidth field is PARSED, not compared. Units and a positive number are # the assertion; the magnitude is deliberately not, per the header above. bw=$(printf '%s\n' "$summary" | sed -n 's/.*[Bb]ytes *\([0-9.]*\) \([KMG]*bits\/sec\).*/\1 \2/p') printf 'IPERF2-TCP-BANDWIDTH %s\n' "${bw:-unparseable}" set -- $bw if [ -n "${1:-}" ] && [ -n "${2:-}" ] && [ "$(printf '%s\n' "$1" | awk '{exit !($1 > 0)}'; echo $?)" = 0 ]; then bst_ok iperf2/tcp-bandwidth-parses else bst_notok iperf2/tcp-bandwidth-parses "no positive in: $(_bst_1line "$summary")" fi # THE SERVER REPORTS THE MATCHING SESSION, and the session is identified by the # CLIENT'S EPHEMERAL PORT, which both sides print. That is an exact join between # the two logs -- far stronger than "the server also printed a summary", which # would pass against a leftover server serving somebody else's connection. cport=$(printf '%s\n' "$out" | sed -n 's/.*local 127\.0\.0\.1 port \([0-9][0-9]*\) connected with.*/\1/p' | head -1) printf 'IPERF2-TCP-CLIENT-PORT %s\n' "${cport:-none}" srv=$(cat "$D/tcp-server.log" 2>/dev/null) printf '%s\n' "$srv" | sed 's/^/IPERF2-TCP-SERVER /' if [ -n "$cport" ] && printf '%s\n' "$srv" | grep -q "connected with 127\.0\.0\.1 port $cport"; then bst_ok iperf2/tcp-server-matching-session else bst_notok iperf2/tcp-server-matching-session "server log has no 'connected with 127.0.0.1 port $cport': $(_bst_1line "$srv")" fi # The server's own summary for that session. Its stdout is a file and therefore # block-buffered, and the server is killed rather than exiting cleanly, so this # can legitimately be short of the client's view -- which is why the byte counts # are printed rather than compared. What is asserted is that the server rendered # a well-formed transfer line at all. srvsum=$(printf '%s\n' "$srv" | grep 'bits/sec' | tail -1) case "$srvsum" in *" sec "*"Bytes"*"bits/sec"*) bst_ok iperf2/tcp-server-summary-line ;; *) bst_notok iperf2/tcp-server-summary-line "no transfer line from the server: $(_bst_1line "$srv")" ;; esac kill "$SRV" 2>/dev/null # --- the UDP transfer: jitter and loss, which no other tool here reports ----- # UDP is where iperf2 says something netperf and iperf3 do not on this image: a # datagram-level jitter figure and a loss ratio, from the RECEIVER, echoed back # to the sender as a "Server Report". nuttcp states loss but not jitter. setsid iperf -s -u -p "$UPORT" > "$D/udp-server.log" 2>&1 & USRV=$! i=0 while [ $i -lt 25 ]; do bound_udp "$UPORT" && break; i=$((i + 1)); sleep 1; done if bound_udp "$UPORT"; then bst_ok iperf2/udp-server-binds else bst_notok iperf2/udp-server-binds "no row for port $UPORT in /proc/net/udp{,6} after $i polls: $(_bst_1line "$(cat "$D/udp-server.log" 2>&1)")"; fi uout=$(iperf -c 127.0.0.1 -u -p "$UPORT" -t 2 -i 1 2>&1) urc=$? printf '%s\n' "$uout" | sed 's/^/IPERF2-UDP-CLIENT /' printf 'IPERF2-UDP-CLIENT-RC %s\n' "$urc" check_eq iperf2/udp-client-rc 0 "$urc" # `Sent N datagrams` is the client's closing trailer, and its PRESENCE is what is # asserted: it is printed only on the `stats->free == 1` path (ReportDefault.c:280), # i.e. only once the run has been torn down in an orderly way, so a client that # died mid-transfer or never completed the UDP FIN exchange does not emit it. # # ITS VALUE IS RECORDED, NOT ASSERTED, and that is a deliberate choice with a # measurement behind it. On this arm the trailer reads `Sent 0 datagrams` while # the very same run's Server Report reads `0/ 180 (0%)` and the server's own log # agrees at 180 -- so the datagrams demonstrably flowed and only the CLIENT'S own # counter is short. The same source, built with the same flags, prints `Sent 180 # datagrams` on an x86_64/glibc control. # # It is left unasserted rather than pinned red because the mechanism is NOT # root-caused, and the two candidates visible in the source are both iperf2's own # report bookkeeping rather than anything the platform provides: Reporter.c:1263 # renders a whole-run summary from the absolute `cntDatagrams`, while :1337-1339 # renders an INTERVAL summary from `cntDatagrams - lastDatagrams`, which is # exactly 0 once a final interval has already consumed every datagram. Asserting # a number on that basis would pin a guess. The count that carries real # information -- the receiver's -- is asserted, twice, by # iperf2/udp-jitter-loss-report and iperf2/udp-server-jitter-loss below. # # The value is printed so a CHANGE in it is visible: if this ever starts agreeing # with the server, the diagnostic line says so without anyone re-deriving it. sent=$(printf '%s\n' "$uout" | sed -n 's/.*Sent \([0-9][0-9]*\) datagrams.*/\1/p' | head -1) printf 'IPERF2-UDP-SENT %s\n' "${sent:-none}" if [ -n "$sent" ]; then bst_ok iperf2/udp-sent-trailer else bst_notok iperf2/udp-sent-trailer "no 'Sent datagrams' trailer, so the client did not reach an orderly teardown: $(_bst_1line "$uout")"; fi # The Server Report is the round trip: the receiver's verdict, carried back over # the control exchange at the end of the run. Its absence means the UDP FIN # handshake did not complete even though datagrams may have flowed. case "$uout" in *"Server Report:"*) bst_ok iperf2/udp-server-report ;; *) bst_notok iperf2/udp-server-report "no 'Server Report:' in the client's output: $(_bst_1line "$uout")" ;; esac # JITTER AND LOSS, parsed out of the server-report line on the CLIENT side. # Shape: `... 1.05 Mbits/sec 0.001 ms 0/ 180 (0%)`. Both fields are # asserted to be PRESENT AND WELL-FORMED; neither value is asserted, because a # loss ratio on a virtualised loopback measures the guest's scheduling, not a # network, and a suite that demanded 0% would go red for reasons that are not # defects. jl=$(printf '%s\n' "$uout" | sed -n 's/.*bits\/sec *\([0-9][0-9.]*\) ms *\([0-9][0-9]*\)\/ *\([0-9][0-9]*\) (\([0-9.]*\)%).*/jitter=\1ms lost=\2 total=\3 loss=\4%/p' | head -1) printf 'IPERF2-UDP-JITTER-LOSS %s\n' "${jl:-unparseable}" if [ -n "$jl" ]; then bst_ok iperf2/udp-jitter-loss-report else bst_notok iperf2/udp-jitter-loss-report "no ' ms / (%)' field group: $(_bst_1line "$uout")"; fi # The receiver's own log must carry the same report, under the header that names # the two columns. This is what proves the figures above came from the server # rather than being synthesised by the client. usrv=$(cat "$D/udp-server.log" 2>/dev/null) printf '%s\n' "$usrv" | sed 's/^/IPERF2-UDP-SERVER /' case "$usrv" in *"Jitter"*"Lost/Total Datagrams"*) bst_ok iperf2/udp-server-jitter-header ;; *) bst_notok iperf2/udp-server-jitter-header "server log has no jitter/loss header: $(_bst_1line "$usrv")" ;; esac if printf '%s\n' "$usrv" | grep -q 'ms .*[0-9]/ *[0-9].*%'; then bst_ok iperf2/udp-server-jitter-loss else bst_notok iperf2/udp-server-jitter-loss "no jitter/loss data line from the server: $(_bst_1line "$usrv")" fi kill "$USRV" 2>/dev/null rm -rf "$D"