. "$BST_LIB" # nicstat is the third rate tool on this image and the only one whose interval # form means anything here. procps (85-procps.test) answers "what is the system # doing now"; sysstat (85-sysstat.test) reads /proc twice and divides, but its # second sample comes from alarm(2)+pause(2), which returns AT ONCE on this # kernel (linux-in-the-browser#120), so every sysstat rate is divided by a ~0 s # window. nicstat sleeps in poll(NULL, 0, ms) instead, and THAT works -- so # `nicstat 1 3` really does take two seconds and really does report per-second # rates. nicstat/interval-elapses below is the case that says so, and it is the # reason this suite sits beside sysstat's rather than with the network tools. # # Two things about this machine shape every case here: # # - IT HAS EXACTLY ONE INTERFACE, lo, and no net hardware (the same finding # 60-net-tools.test records). So `nicstat -n` (non-local only) has nothing to # report, and %Util/IErr/OErr/Coll can never be anything but zero. # - nicstat SKIPS ANY INTERFACE THAT HAS NEVER SEEN A PACKET, and exits 2 # "no matching interface" when that leaves nothing. On a freshly booted # machine lo's counters are all zero, so a bare `nicstat` fails until # something has used the loopback. That is stock nicstat behaviour, not a # wasm32 effect -- and it is why this suite generates its own traffic first # rather than assuming an earlier suite left some. # # No busybox applet is named nicstat and the recipe registers no # update-alternatives link, so /usr/bin/nicstat can only be this port; the # version string is asserted anyway because it is free. # # A note on shape: a nicstat start-up costs about a second of wasm instantiation # and the runner bounds a suite at 60 s, so where several cases read the same # report this file CAPTURES ONCE and asserts several times, rather than paying # for one invocation per assertion. require nicstat nicstat/present # check_fail -- an error case is only right when # the exit status and the diagnostic both are; check_out cannot express it # (it requires rc 0). nicstat's die() exits 2, its usage() exits 1. check_fail() { _id=$1; _want=$2; _needle=$3; shift 3 _out=$("$@" 2>&1); _rc=$? case "$_out" in *"$_needle"*) _hit=1 ;; *) _hit=0 ;; esac if [ "$_rc" -eq "$_want" ] && [ "$_hit" -eq 1 ]; then bst_ok "$_id" else bst_notok "$_id" "rc=$_rc want=$_want needle:'$_needle' got:'$(_bst_1line "$_out")'"; fi } # has -- assert a captured report contains a literal. has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "no '$2' in: $(_bst_1line "$3")" ;; esac } # --- give lo something to count ---------------------------------------------- # Three big echoes rather than many small ones: the byte counter is what the rate # cases below divide by uptime, and ping paces itself at one packet a second, so # size is free where count is not. The reply is counted on lo in BOTH directions, # which is why the rx and tx columns match throughout this suite. ping -c 3 -s 60000 -W 1 127.0.0.1 >/dev/null 2>&1 # /proc/net/dev's lo row with the " lo:" label stripped, so $1 is rx bytes and # $2 is rx packets whatever the leading whitespace does to field splitting. _lo=$(awk '/^ *lo:/ {sub(/^[^:]*:/, ""); print $1, $2}' /proc/net/dev) _bytes=${_lo% *}; _pkts=${_lo#* } if [ "${_pkts:-0}" -gt 0 ] 2>/dev/null; then bst_ok nicstat/traffic-seeded else bst_notok nicstat/traffic-seeded "lo still shows 0 rx packets in /proc/net/dev after the ping ('$_lo'); every case below is measuring nothing"; fi # --- identity ---------------------------------------------------------------- check_eq nicstat/path /usr/bin/nicstat "$(command -v nicstat)" check_out nicstat/version "nicstat version 1.95" nicstat -v # --- the default report ------------------------------------------------------ _full=$(nicstat 2>&1) has nicstat/header "%Util" "$_full" # Exactly one data row, and it is lo. The count is exact because the interface # inventory is: lo. check_eq nicstat/lo-row 1 "$(printf '%s\n' "$_full" | awk '$2 == "lo" {n++} END {print n+0}')" # The numbers come off /proc/net/dev rather than being zero placeholders: after # the burst above, both the byte rate and the packet rate are > 0. _rates=$(printf '%s\n' "$_full" | awk '$2 == "lo" {print $3, $5}') if [ -n "$_rates" ] && printf '%s\n' "$_rates" | awk '{exit !($1 > 0 && $2 > 0)}'; then bst_ok nicstat/counters-nonzero else bst_notok nicstat/counters-nonzero "rKB/s and rPk/s were '$_rates', want both > 0"; fi # The first sample is SINCE BOOT -- rate = counter / uptime, with no second # sample involved -- so it can be recomputed from the two files nicstat read. # Agreement inside a factor of two is the assertion (the two reads are a second # or so apart and lo keeps ticking); that band is what distinguishes "divided by # the right clock" from "divided by whatever tv_sec happened to hold". _want=$(awk -v b="$_bytes" -v u="$(awk '{print $1}' /proc/uptime)" 'BEGIN {print b / 1024 / u}') _got=$(printf '%s\n' "$_full" | awk '$2 == "lo" {print $3}') if [ -n "$_got" ] && awk -v w="$_want" -v g="$_got" 'BEGIN {exit !(w > 0 && g > 0 && g < w*2 && g > w/2)}'; then bst_ok nicstat/since-boot-rate else bst_notok nicstat/since-boot-rate "nicstat rKB/s=$_got, /proc/net/dev over /proc/uptime=$_want (want within 2x)"; fi # --- the other report styles ------------------------------------------------- check_out nicstat/extended "IErr OErr Coll" nicstat -x check_out nicstat/mbit "rMbps" nicstat -M check_out nicstat/util-both "%rUtil %wUtil" nicstat -U # -s is the same two rates at %14.3f, so the fixed three decimals are what # identify it -- "rKB/s" alone is in the default header too. _sum=$(nicstat -s 2>/dev/null | awk '$2 == "lo" {print $3}') if awk -v v="$_sum" 'BEGIN {exit !(v ~ /^[0-9]+\.[0-9][0-9][0-9]$/)}'; then bst_ok nicstat/summary else bst_notok nicstat/summary "summary rKB/s is not a 3-decimal number: '$_sum'"; fi # -l is the inventory rather than a report. It knows lo is a loopback # (NIC_LOOPBACK, set from the interface name in /proc/net/dev), which is what # makes it print "-" for the speed instead of a bogus 0 Mbit/s, and with no -S # given it has no duplex to report either. _list=$(nicstat -l 2>&1) has nicstat/list "Int Loopback Mbit/s Duplex State" "$_list" check_eq nicstat/list-loopback "Yes - unkn up" \ "$(printf '%s\n' "$_list" | awk '$1 == "lo" {print $2, $3, $4, $5}')" # --- parseable output: the one form a script consumes ------------------------ # This is where the port's single source fix lives. Every parseable line leads # with tv_sec, which upstream prints as %ld -- correct only where long is as wide # as time_t. Here long is 4 bytes and time_t is 8, so printf consumed half the # timestamp and every later conversion read the wrong slot: the name came out # "(null)" off the timestamp's high half and each %.*f drew a garbage precision, # printing one rate with several hundred fraction digits. The three cases below # assert the three symptoms separately, so a regression names itself. _p=$(nicstat -p 2>/dev/null | grep ':lo:' | head -1) check_eq nicstat/parseable-fields 8 "$(printf '%s\n' "$_p" | awk -F: '{print NF}')" # Field 1 is a ten-digit epoch, not a truncated or garbled one. if printf '%s\n' "$_p" | awk -F: '{exit !(length($1) == 10 && $1 ~ /^[0-9]+$/)}'; then bst_ok nicstat/parseable-epoch else bst_notok nicstat/parseable-epoch "field 1 is not a 10-digit epoch: '$(_bst_1line "$_p")'"; fi # ...and every rate field carries at most three decimals (precision_p's maximum). if printf '%s\n' "$_p" | awk -F: '{for (i = 3; i <= NF; i++) if ($i !~ /^[0-9]+\.[0-9][0-9]?[0-9]?$/) exit 1; exit 0}'; then bst_ok nicstat/parseable-precision else bst_notok nicstat/parseable-precision "a rate field is not a <=3-decimal number: '$(_bst_1line "$_p")'"; fi # --- the /proc/net/snmp and /proc/net/netstat readers ------------------------ # -t and -u are a different data source from everything above: nicstat parses the # Tcp:/Udp: lines of /proc/net/snmp by their exact column-header text, and -t also # reads TcpExt: out of /proc/net/netstat. A kernel that laid those files out # differently would print a row of zeros here rather than failing, so the # parseable forms -- which carry the field count and the timestamp -- are asserted # alongside the human ones. check_out nicstat/tcp "InSeg OutSeg" nicstat -t check_out nicstat/udp "InDG OutDG" nicstat -u # One capture covers four things at once, which is also the cheapest way to prove # the -a alias really is "-x -u -t": with -p it emits the TCP line, the UDP line # AND the extended-parseable interface line, so asserting all three field counts # says the alias set every flag it documents. _a=$(nicstat -a -p 2>&1) check_eq nicstat/all-tcp 12 "$(printf '%s\n' "$_a" | grep ':TCP:' | head -1 | awk -F: '{print NF}')" check_eq nicstat/all-udp 6 "$(printf '%s\n' "$_a" | grep ':UDP:' | head -1 | awk -F: '{print NF}')" check_eq nicstat/all-extended 13 "$(printf '%s\n' "$_a" | grep ':lo:' | head -1 | awk -F: '{print NF}')" # The InSeg field is a well-formed rate rather than the garbage the %ld bug # produced. It is asserted for SHAPE, not for a non-zero value: nothing on this # machine is obliged to have opened a TCP connection by the time this suite runs # (the ping above is ICMP), so a legitimate 0.000 must stay green. _seg=$(printf '%s\n' "$_a" | grep ':TCP:' | head -1 | awk -F: '{print $5}') if awk -v s="$_seg" 'BEGIN {exit !(s ~ /^[0-9]+\.[0-9][0-9]?[0-9]?$/)}'; then bst_ok nicstat/tcp-counters else bst_notok nicstat/tcp-counters "InSeg field is not a <=3-decimal number: '$_seg'"; fi # --- interface selection ----------------------------------------------------- # -i takes a comma list; an unknown name in it is simply not matched, while an # -i that matches nothing at all is the die() path. check_eq nicstat/select-if 1 "$(nicstat -i lo,nosuchif 2>/dev/null | awk '$2 == "lo" {n++} END {print n+0}')" check_fail nicstat/select-none 2 "no matching interface" nicstat -i nosuchif # -n excludes loopback, and loopback is all this machine has. This is the same # g_nicdata_count <= 0 exit a freshly booted machine gives for a bare nicstat, # reached deterministically -- so it covers the boot-state contract described at # the top of this file without depending on how much traffic has already run. check_fail nicstat/nonlocal-none 2 "no matching interface" nicstat -n # --- -S, the Linux-only speed override --------------------------------------- # lo answers no SIOCETHTOOL, so get_speed_duplex() sets NIC_NO_GSET and falls back # to the -S list. The duplex column flipping from the "unkn" asserted above to # "full" (and to "half" for a :NNNhd record) is the proof that both halves of that # path ran: the ioctl failure was handled, and the list was parsed, matched by # interface name, and its duplex suffix decoded. check_eq nicstat/speed-flag-full "full" \ "$(nicstat -S lo:1000fd -l 2>/dev/null | awk '$1 == "lo" {print $4}')" check_eq nicstat/speed-flag-half "half" \ "$(nicstat -S lo:100hd -l 2>/dev/null | awk '$1 == "lo" {print $4}')" # --- error contracts --------------------------------------------------------- # usage() exits 1 and prints to stderr; die() exits 2. Both statuses are part of # the CLI contract for a script that drives this tool. check_fail nicstat/usage 1 "USAGE: nicstat" nicstat -h check_fail nicstat/bad-option 1 "invalid option" nicstat -Q check_fail nicstat/bad-interval 1 "USAGE: nicstat" nicstat 0 # --- the interval form, which is the whole point of a rate tool -------------- # One run, two assertions: three samples (the since-boot one plus two computed # over a real elapsed second), and >= 2 s of wall clock to produce them. The # second is what separates nicstat from sysstat on this machine: sysstat's # alarm(2)+pause(2) returns at once and its `sar -u 1 3` finishes in about a # second with every rate divided by ~0, while nicstat's poll(NULL, 0, ms) blocks # for the interval it was given. If poll ever regresses to the same immediate # return, nicstat/interval-elapses goes red and nicstat/interval-samples stays # green -- exactly the split that says the rates became meaningless while the # output shape did not. _s=$(date +%s); _iv=$(nicstat 1 3 2>&1); _e=$(date +%s) check_eq nicstat/interval-samples 3 "$(printf '%s\n' "$_iv" | awk '$2 == "lo" {n++} END {print n+0}')" _el=$((_e - _s)) if [ "$_el" -ge 2 ]; then bst_ok nicstat/interval-elapses else bst_notok nicstat/interval-elapses "\`nicstat 1 3\` took ${_el}s, want >= 2s: poll(NULL,0,ms) is not sleeping, so every interval rate is divided by a ~0s window"; fi # --- present but not reachable on this machine (attributed, NOT executed as # assertions: each would flip on its own if the platform gained the source) bst_skip nicstat/util-percent \ "%Util is structurally 0.00 here: it is bytes*800/speed, lo reports no link speed, and even -S lo:1000fd over loopback ping traffic rounds to zero -- the column is asserted present, never non-zero" bst_skip nicstat/ethtool-speed \ "the SIOCETHTOOL/ETHTOOL_GSET discovery path cannot be covered: lo answers no ethtool ioctl on any Linux, and this machine has no other interface. Its FALLBACK is covered by nicstat/speed-flag-full" bst_skip nicstat/nonlocal-interfaces \ "-n's reporting mode needs a non-loopback interface and this machine has none (no virtio-net; 60-net-tools.test records the same). Its error contract is asserted by nicstat/nonlocal-none" bst_skip nicstat/error-columns \ "IErr/OErr/Coll/NoCP/Defer are always 0 on lo -- there is no hardware here to drop, collide or defer a frame, so the -x columns can be asserted present but never exercised" bst_skip nicstat/man-page \ "nicstat.1 is packaged into nicstat-doc and this image installs no -doc packages"