. "$BST_LIB" # BST-TIMEOUT: 120 # ~14 bmon execs at ~1 s of wasm instantiation each, plus a # # seeding burst. MEASURED on 2026-09-01: the suite ran # # 13 s of its 120 s bound. Nothing here # # waits on bmon's own read interval -- every run carries # # quitafter=1, which exits after the FIRST draw -- so the # # cost is instantiation, not elapsed monitoring. Replace # # this comment with an ==BST-SUITE elapsed= reading from the # # first run that produces one. # bmon -- the bandwidth monitor, tested through its scriptable arm. # # WHAT THIS SUITE ASSERTS is the ticket's own pass criterion: that # `-o format:fmt=...` emits one parseable line per interval whose fields have # the declared names and types, and that the byte counters ADVANCE by the # expected order of magnitude once a known payload has crossed loopback. The # curses arm is deliberately absent -- it is a follow-on under BRINTOS-780 and # needs a terminal this runner does not give a suite. # # THE MACHINE HAS EXACTLY ONE INTERFACE, lo, and no net hardware -- the same # finding 60-net-tools.test and 85-nicstat.test each record. So `-p lo` selects # the whole inventory, and the rx and tx columns move together, because a # loopback reply is counted in both directions. # # TRAFFIC IS SEEDED HERE rather than assumed: a freshly booted machine's lo # counters are zero, so something must move them before any counter case means # anything. NOT WITH ping: /bin/ping is inetutils' (`ping -> ping.inetutils`), # and it dies in its own start-up with `ping: setuid: Function not implemented` # -- the raw ICMP socket IS created, and ping then fails the setuid(getuid()) # that would drop the privilege it no longer needs. That is brintos/linux#19, # credential setters ENOSYS, named at the case in 88-inetutils.test. It exits # rc=1 having sent nothing, so a ping-seeded counter case measures a counter # that never moved. Measured on this image, 2026-09-01: lo rx bytes 0 before # the ping and 0 after it. # # The seed is a loopback UDP datagram instead, which needs no raw socket and no # credential call. Two details of it are load-bearing, both measured rather than # reasoned: (1) ONE write(2) PER DATAGRAM -- `head -c 60000` writes in 8 KB # blocks, and only the first lands, because the ICMP port-unreachable from the # closed port errors the CONNECTED socket and every later write gets # ECONNREFUSED (measured: 8796 bytes moved, not 60000). `dd bs=60000 count=1` # issues the single write the datagram needs. (2) A FRESH SOCKET PER DATAGRAM, # for the same reason -- a reused fd is already in error. So: 60604 bytes for # one datagram, 363624 for six, and /proc/net/dev and netlink agree exactly on # the total (424228 both ways). # # THE COUNTER-DELTA ARM RUNS bmon TWICE rather than asking one run for two # samples, and that is a deliberate choice about what is being measured. bmon's # main loop paces itself with usleep(3) (bmon.c:380), and this kernel has form # here: sysstat's alarm(2)+pause(2) second sample returns AT ONCE # (linux-in-the-browser#120), which divides every sysstat rate by a ~0 s window, # while nicstat's poll(NULL, 0, ms) really does sleep. Whether usleep(3) belongs # to the working group or the broken one is NOT KNOWN HERE and is not something # this suite needs to know: two invocations with a seeding burst between them # measure the # COUNTER, which is what the ticket's criterion is about, and they do it whatever # the interval does. bmon/interval-elapses below measures the interval on its # own, as a separate question with its own case, so that the answer is recorded # rather than assumed either way. # # NO busybox or toybox applet is named bmon and the recipe registers no # update-alternatives link, so a bare `bmon` can only be this port. require bmon bmon/present W=${TMPDIR:-/tmp}/bst-bmon.$$ mkdir -p "$W" 2>/dev/null # The scriptable format. No comma and no semicolon anywhere in it: bmon splits # its -o argument on ',' between modules and on ';' between a module's options # (conf.c:209, parse_module), so either character inside the format string would # be eaten by the parser before out_format ever saw it. FMT='$(element:name) $(attr:rx:bytes) $(attr:tx:bytes)\n' # lo's byte counters as bmon reports them, one shot. quitafter=1 makes # out_format's draw callback exit(0) after the first draw (out_format.c, # format_draw), so this returns rather than monitoring. bmon_bytes() { bmon -i proc -p lo -o "format:fmt=$FMT;quitafter=1" 2>/dev/null | awk '$1 == "lo" { print $2; exit }' } # lo's rx bytes as the KERNEL reports them -- the independent view the bmon # numbers are scored against. The " lo:" label is stripped so $1 is rx bytes # whatever the leading whitespace does to field splitting, exactly as # 85-nicstat.test reads the same row. kbytes() { awk '/^ *lo:/ { sub(/^[^:]*:/, ""); print $1; exit }' /proc/net/dev } # Move lo's counters by ~360 KB. Six 60000-byte UDP datagrams to the discard # port, one write(2) each and a fresh socket each, for the reasons in the header. # bash is the only shell here with /dev/udp -- the runner's own shell is busybox # ash, which has no such redirection -- so the burst is one `bash -c`. seed_lo() { bash -c 'i=0 while [ $i -lt 6 ]; do exec 3>/dev/udp/127.0.0.1/9 dd if=/dev/zero bs=60000 count=1 >&3 2>/dev/null exec 3>&- i=$((i + 1)) done' 2>/dev/null } # ----------------------------------------------------------- A packaging ---- check_eq bmon/path /usr/bin/bmon "$(command -v bmon)" check_out bmon/version "bmon 4.0" bmon -V # -h prints the version banner and the usage text and exits 1, not 0 (bmon.c:148) # -- so check_out, which requires rc 0, cannot express it. out=$(bmon -h 2>&1); rc=$? case "$out" in *"Usage: bmon"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 1 ] && [ "$hit" -eq 1 ]; then bst_ok bmon/help else bst_notok bmon/help "rc=$rc want=1 want:'Usage: bmon' got:'$(_bst_1line "$out")'"; fi # --------------------------------------------------------- B module inventory - # `-o list` and `-i list` print the compiled-in module set and exit 1 # (module_set returns 1 for "list", which parse_args_post propagates). This is # the only case that says WHICH modules this build actually carries, which is # what separates a port built without libnl or without curses from a whole one. ilist=$(bmon -i list 2>&1) olist=$(bmon -o list 2>&1) for m in proc netlink dummy null; do case "$ilist" in *"$m"*) bst_ok "bmon/in-$m" ;; *) bst_notok "bmon/in-$m" "input module '$m' absent from -i list: $(_bst_1line "$ilist")" ;; esac done for m in format ascii curses null; do case "$olist" in *"$m"*) bst_ok "bmon/out-$m" ;; *) bst_notok "bmon/out-$m" "output module '$m' absent from -o list: $(_bst_1line "$olist")" ;; esac done # ------------------------------------------------------------- C seed lo ----- seed_lo k0=$(kbytes) if [ "${k0:-0}" -gt 0 ] 2>/dev/null; then bst_ok bmon/traffic-seeded else bst_notok bmon/traffic-seeded "lo still shows 0 rx bytes in /proc/net/dev after the seeding burst ('$k0'); every case below is measuring nothing"; fi # ------------------------------------------------- D the pass criterion ------ # ONE PARSEABLE LINE PER ELEMENT, with the declared fields. bst_probe bounds the # run and keeps whatever it printed: a bmon that does not return would otherwise # take the suite down with it and leave no transcript, which is the failure mode # bst_probe exists for. bst_probe bmon/format-returns "$W/fmt" 20 \ bmon -i proc -p lo -o "format:fmt=$FMT;quitafter=1" line=$(awk '$1 == "lo" { print; exit }' "$W/fmt" 2>/dev/null) if [ -n "$line" ]; then bst_ok bmon/format-lo-line else bst_notok bmon/format-lo-line "no line for lo in the format output: $(_bst_1line "$(cat "$W/fmt" 2>/dev/null)")"; fi # THE FIELDS HAVE THE DECLARED TYPES. $(attr:rx:bytes) and $(attr:tx:bytes) are # rate_get_total() through "%" PRIu64 (out_format.c, get_token), so each must be # a bare unsigned decimal -- not "unknown", which is what get_token returns for # an attribute name it cannot resolve, and not a float. set -- $line if [ $# -eq 3 ]; then bst_ok bmon/format-field-count else bst_notok bmon/format-field-count "want 3 fields from '$FMT', got $#: '$(_bst_1line "$line")'"; fi rxb=$2; txb=$3 for f in rx:$rxb tx:$txb; do v=${f#*:} case "$v" in ''|*[!0-9]*) bst_notok "bmon/format-${f%%:*}-integer" "want an unsigned decimal, got '$v' (get_token answers 'unknown' for an unresolved attribute)" ;; *) bst_ok "bmon/format-${f%%:*}-integer" ;; esac done # AND THEY ARE THE KERNEL'S NUMBERS, not zero placeholders. Scored against # /proc/net/dev rather than against a constant: bmon read the same file a moment # later, so it must be at or above what we read, and within a factor of two of # it -- the band that separates "read the right column" from "read whatever # field happened to parse". if [ -n "$rxb" ] && [ "${rxb:-0}" -gt 0 ] 2>/dev/null && awk -v k="$k0" -v b="$rxb" 'BEGIN { exit !(k > 0 && b >= k && b < k * 2) }'; then bst_ok bmon/format-matches-proc else bst_notok bmon/format-matches-proc "bmon rx bytes=$rxb, /proc/net/dev rx bytes=$k0 (want bmon >= proc and within 2x)" fi # THE COUNTERS ADVANCE. The ticket's criterion in its own words: after a known # payload crosses loopback the byte counter must move by the expected ORDER OF # MAGNITUDE. Six 60000-byte datagrams, echoed, is ~360 KB across both directions # (363624 measured); # 100 KB is the floor asserted, which is an order of magnitude below the payload # and an order of magnitude above the few hundred bytes of background chatter a # quiet lo carries between two runs. b1=$(bmon_bytes) seed_lo b2=$(bmon_bytes) if [ -n "$b1" ] && [ -n "$b2" ] && awk -v a="$b1" -v b="$b2" 'BEGIN { exit !(a > 0 && b > a && (b - a) > 100000) }'; then bst_ok bmon/counters-advance else bst_notok bmon/counters-advance "rx bytes went $b1 -> $b2 across a ~360 KB loopback burst; want a rise over 100000" fi # The kernel agrees that it moved, which is what says the rise above is traffic # rather than bmon's own arithmetic. k1=$(kbytes) if awk -v a="$k0" -v b="$k1" 'BEGIN { exit !(b > a) }'; then bst_ok bmon/proc-agrees-it-moved else bst_notok bmon/proc-agrees-it-moved "/proc/net/dev rx bytes did not move across the second burst ($k0 -> $k1)"; fi # ------------------------------------------------------ E the ascii arm ------ # The ticket's second clause: `-o ascii` produces a one-shot table for the same # interface. diagram=details is the form that names the attributes; quitafter=1 # is what makes it one-shot rather than a monitor. bst_probe bmon/ascii-returns "$W/ascii" 20 \ bmon -i proc -p lo -o 'ascii:diagram=details;quitafter=1' asc=$(cat "$W/ascii" 2>/dev/null) case "$asc" in *lo*) bst_ok bmon/ascii-names-lo ;; *) bst_notok bmon/ascii-names-lo "no 'lo' in the ascii table: $(_bst_1line "$asc")" ;; esac case "$asc" in *[0-9]*) bst_ok bmon/ascii-has-numbers ;; *) bst_notok bmon/ascii-has-numbers "the ascii table carries no digits at all: $(_bst_1line "$asc")" ;; esac # ------------------------------------------- F the default input module ------ # WHAT THIS CASE IS FOR. Everything above pins `-i proc`, so it measures the # procfs reader and says nothing about what a user who types plain `bmon` gets. # The default is not proc: activate_default (input.c) tries "netlink" FIRST and # only then "proc" -- and the fall-through is UNREACHABLE, because a failing # probe takes module_set into `quit("No working input module found")` (module.c:154) # rather than back to its caller. So if NETLINK_ROUTE does not answer here, plain # `bmon` does not silently degrade to /proc/net/dev; it exits 1 with that message. # # EXPECTED, NOT YET MEASURED: this is expected to PASS. 63-macchanger.test uses # `ip -o link show lo` as the independent kernel view it scores its ioctl path # against, and 79-libsocketcan.test reads links over netlink too, so rtnetlink # answers on this image -- which is what netlink_probe() (in_netlink.c:911) needs: # nl_connect(NETLINK_ROUTE) plus one rtnl_link_alloc_cache dump. The gap this # image is known to have is netlink protocol 12, NETLINK_NETFILTER # (brintos/linux#86), and bmon asks for protocol 0. A red here therefore means # rtnetlink is narrower than those two suites imply, and its detail carries what # bmon said instead -- which is the whole value of asking. out=$(bmon -p lo -o "format:fmt=$FMT;quitafter=1" 2>&1); rc=$? case "$out" in *"No working input module found"*) bst_notok bmon/default-input "the default (netlink) input module probed false, and activate_default cannot fall back to proc: $(_bst_1line "$out")" ;; *) if [ "$rc" -eq 0 ]; then bst_ok bmon/default-input else bst_notok bmon/default-input "rc=$rc with the default input module: $(_bst_1line "$out")"; fi ;; esac # ------------------------------------------------- G the read interval ------- # THE ONE CASE THAT IS ABOUT usleep(3), asked separately so its answer is # recorded rather than folded into a counter assertion. `-r 1` with quitafter=2 # draws once immediately and once a read interval later, so the run must take at # LEAST a second of wall clock. /proc/uptime is the clock, for the reason # bst_probe gives: it is the one that advances here. # # EXPECTED, NOT YET MEASURED, AND GENUINELY OPEN. sysstat's alarm(2)+pause(2) # returns at once on this kernel (linux-in-the-browser#120) while nicstat's # poll(NULL, 0, ms) sleeps properly; usleep(3) is a third path and belongs to # neither group by measurement. A red here is a finding about the kernel's timer # surface, not about bmon, and nothing else in this suite depends on it. read -r u0 _ < /proc/uptime bst_probe bmon/interval-returns "$W/iv" 30 \ bmon -i proc -p lo -r 1 -o "format:fmt=$FMT;quitafter=2" read -r u1 _ < /proc/uptime if awk -v a="${u0%.*}" -v b="${u1%.*}" 'BEGIN { exit !(b - a >= 1) }'; then bst_ok bmon/interval-elapses else bst_notok bmon/interval-elapses "a -r 1 run with quitafter=2 took ${u0}s -> ${u1}s; two draws one read interval apart cannot take under a second unless usleep(3) returned early" fi # Two draws were asked for and two lines for lo must have arrived. n=$(awk '$1 == "lo" { n++ } END { print n+0 }' "$W/iv" 2>/dev/null) check_eq bmon/interval-two-draws 2 "$n" # ------------------------------------------------------- H error paths ------- # Both are refused before any input module is touched, so neither depends on # anything above. An unknown module name is quit()'s exit 1; an unterminated # placeholder is format_probe returning 0, which fails the module's probe. out=$(bmon -i no_such_input_module 2>&1); rc=$? case "$out" in *"Unknown input module"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -ne 0 ] && [ "$hit" -eq 1 ]; then bst_ok bmon/bad-input-module else bst_notok bmon/bad-input-module "rc=$rc want non-zero, want:'Unknown input module' got:'$(_bst_1line "$out")'"; fi out=$(bmon -i proc -p lo -o 'format:fmt=$(element:name' 2>&1); rc=$? case "$out" in *"Missing ')'"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -ne 0 ] && [ "$hit" -eq 1 ]; then bst_ok bmon/bad-format-string else bst_notok bmon/bad-format-string "rc=$rc want non-zero, want:\"Missing ')'\" got:'$(_bst_1line "$out")'"; fi rm -rf "$W"