. "$BST_LIB" # BST-TIMEOUT: 300 # MEASURED: the first image run reported elapsed=37s with a # # 20-second daemon window (==BST-SUITE vnstat elapsed=37s # # cases=30, 2026-09-01). That window is now 80 s -- see # # section G, and the re-run of the corrected suite on that # # same image reported elapsed=101s bound=300s cases=30, # # pass=30. 300 leaves headroom rather than trading a red for # # a timeout. # vnStat -- the network traffic database, its query tool and its daemon. # # WHAT THIS SUITE ASSERTS is the ticket's own pass criterion: that the sqlite # database this image already supports is initialised, that an interface is # added to it, that a SEEDED sample comes back BYTE-EXACT through --json and # --oneline, that an unknown interface exits non-zero, and that a single vnstatd # update pass runs under the /proc/uptime sentinel rather than a timed sleep # (linux-in-the-browser#120, where a relative sleep placed after the shell reaps # a foreground child returns at once). # # THE MACHINE HAS EXACTLY ONE INTERFACE, lo, and no net hardware -- the same # finding 60-net-tools.test, 85-nicstat.test and 85-bmon.test each record. That # is not incidental here, it is the shape of the whole suite, because # # VNSTAT'S DISCOVERY REFUSES LOOPBACK, AND ON A LOOPBACK-ONLY MACHINE THAT # LEAVES THE DAEMON WITH NOTHING TO RECONCILE AGAINST -- WHICH IS NOT THE SAME # AS LEAVING IT NOTHING TO MONITOR. isifvalid() (src/ifinfo.c:458) returns 0 # for exactly "lo", "lo0", "sit0" and any name containing ':', and the daemon's # visible-interface list is built through it (getifliststring -> # getiflist(..., validate=1)), so that list comes back EMPTY here. # # An empty list does NOT make the daemon disable lo. getiflist_linux() returns # 1 on any successful fopen of /proc/net/dev even when it added no interface, # so interfacechangecheck() proceeds to simplehash(""), which is 0 by that # function's own `len <= 0` guard, and compares it against s->iflisthash -- # which initdstate() also set to 0 (src/daemon.c:256). They match, so # interfacechangecheck() RETURNS AT src/daemon.c:865, ahead of both the disable # and the enable branch, on this pass and on every later one. # # So the daemon KEEPS lo, monitors it, and runs its update pass normally. # 'Interface "lo" disabled.' and "Nothing to monitor" never appear, the removal # at src/daemon.c:636 is never reached, and the log stays at two lines for the # life of the run -- the DATABASE is what records that the pass happened. # MEASURED, not derived: vnStat 2.6 run natively on the build host inside # `unshare -rn`, whose /proc/net/dev carries lo alone exactly as this image's # does, reproduces the target's transcript line for line. 2026-09-01. # # THIS SUITE ASSERTED THE OPPOSITE BEFORE THE FIRST IMAGE RAN IT, and the # reason is worth keeping rather than quietly correcting: the original native # dry-run used the build host's ROOT namespace, which has real interfaces, so # the validated list was non-empty, its hash differed from 0, and lo was duly # disabled and removed. That transcript is real -- it just belongs to a machine # that has other interfaces, which this one does not. # # THAT ALSO DECIDES HOW THE DATABASE IS BUILT. `vnstat --add` cannot create # one -- it opens read-only and says so ("The vnStat daemon should have created # the database when started", src/vnstat.c:78) -- while `vnstatd --noadd` # creates the schema, finds nothing to add and exits 1. So the bootstrap here # is vnstatd --noadd first, `vnstat --add -i lo` second; --add does NOT go # through isifvalid and accepts lo. # # THE SAMPLE IS SEEDED THROUGH sqlite3(1) rather than accumulated, and that is # the point rather than a shortcut: the round trip under test is database -> # formatter, and seeding is what makes both sides of a BYTE-EXACT comparison # knowable. The reference strings below were MEASURED, not derived -- vnStat 2.6 # built and run natively on the build host against this exact seed, TZ=UTC, # 2026-09-01. A difference on target is therefore a real difference between the # two word sizes and not a guess about formatting. # # THE CONFIG IS THE SUITE'S OWN, in $TMPDIR, naming only DatabaseDir, PidFile and # the daemon intervals. Everything the formatters read -- DayFormat, # MonthFormat, UnitMode, RateUnit, OutputStyle -- is left at its COMPILED-IN # default, which is the condition the reference was measured under. Reading # /etc/vnstat.conf instead would make every byte-exact case a test of that file. # The shipped config gets its own case, separately. # # NO busybox or toybox applet is named vnstat, vnstatd or vnstati and the recipe # registers no update-alternatives link, so a bare `vnstat` can only be this port. require vnstat vnstat/present W=${TMPDIR:-/tmp}/bst-vnstat.$$ mkdir -p "$W" 2>/dev/null DB=$W/vnstat.db C=$W/vnstat.conf # TZ pins the one thing the round trip is sensitive to. vnStat stores its dates # as LOCAL-time text and reads them back through localtime(3), so the identity # only holds if both halves agree -- and UTC is what the reference was measured # under. A bare "UTC" needs no zoneinfo file: POSIX parses it as a zero-offset # zone name. TZ=UTC export TZ printf '%s\n' \ "DatabaseDir \"$W\"" \ "PidFile \"$W/vnstatd.pid\"" \ "UseLogging 0" \ "BandwidthDetection 0" \ "UpdateInterval 5" \ "SaveInterval 1" > "$C" # ----------------------------------------------------------- A packaging ---- check_eq vnstat/path /usr/bin/vnstat "$(command -v vnstat)" check_eq vnstati/path /usr/bin/vnstati "$(command -v vnstati)" check_eq vnstatd/path /usr/sbin/vnstatd "$(command -v vnstatd)" check_out vnstat/version "vnStat 2.6" vnstat -v check_out vnstatd/version "vnStat daemon 2.6" vnstatd -v check_out vnstati/version "vnStat image output" vnstati -v # The shipped /etc/vnstat.conf, read by the tool rather than by grep: --showconfig # re-emits the parsed configuration, so a file this build cannot parse cannot # produce the key. if [ -f /etc/vnstat.conf ]; then bst_ok vnstat/etc-conf-present else bst_notok vnstat/etc-conf-present "/etc/vnstat.conf is not on the image; the recipe packages it"; fi check_out vnstat/showconfig "DatabaseDir" vnstat --config /etc/vnstat.conf --showconfig # ------------------------------------------------- B database bootstrap ----- # vnstatd --noadd creates the schema and exits 1 having found nothing to add. # check_rc, not check_out: the non-zero status is the documented outcome here, # and it is what says the daemon reached preparedatabase() rather than dying # earlier. check_rc vnstat/db-bootstrap-rc 1 vnstatd --config "$C" --nodaemon --noadd if [ -s "$DB" ]; then bst_ok vnstat/db-created else bst_notok vnstat/db-created "vnstatd --noadd left no non-empty $DB; every case below is measuring nothing"; fi # THE SCHEMA IS THE ONE vnStat 2.6 DECLARES, asked of sqlite3 rather than of # vnstat, so this case says the database is a real one this image's sqlite can # open -- which is the ticket's "the sqlite database already supported by this # image" in its own words. tables=$(sqlite3 "$DB" "select name from sqlite_master where type='table' order by name;" 2>&1 | tr '\n' ' ') check_eq vnstat/db-schema "day fiveminute hour info interface month top year " "$tables" # ------------------------------------------------------ C add interface ----- # --add does not go through isifvalid, so it accepts lo where the daemon's own # discovery will not. check vnstat/add-lo vnstat --config "$C" --add -i lo check_eq vnstat/add-lo-row "lo|1" "$(sqlite3 "$DB" "select name,active from interface;" 2>&1)" # ------------------------------------------------ D the seeded round trip ---- # Fixed timestamps and byte counts, all powers of two, chosen so the rate fields # are exact rather than nearly so: `updated` at 12:00:00 makes the daily divisor # 43200 s, and 2026-01-01 00:00 -> 2026-01-02 12:00 makes the monthly divisor # 129600 s. The interface row is REPLACED rather than added to, so the id, the # alias and both totals are ours and not whatever --add left. sqlite3 "$DB" "delete from interface; insert into interface (id,name,alias,active,created,updated,rxcounter,txcounter,rxtotal,txtotal) values (1,'lo','',1,'2026-01-01 00:00:00','2026-01-02 12:00:00',0,0,4194304,2097152); insert into hour (interface,date,rx,tx) values (1,'2026-01-02 11:00:00',1048576,524288); insert into day (interface,date,rx,tx) values (1,'2026-01-02 00:00:00',2097152,1048576); insert into month (interface,date,rx,tx) values (1,'2026-01-01 00:00:00',4194304,2097152); insert into year (interface,date,rx,tx) values (1,'2026-01-01 00:00:00',4194304,2097152); insert into top (interface,date,rx,tx) values (1,'2026-01-02 00:00:00',2097152,1048576);" >"$W/seed.out" 2>&1 if [ ! -s "$W/seed.out" ]; then bst_ok vnstat/seed else bst_notok vnstat/seed "sqlite3 refused the seed: $(_bst_1line "$(cat "$W/seed.out")")"; fi # --oneline b, the RAW mode (ostyle 4): every byte field is a bare uint64 # straight out of the database and every rate is an integer division. This is # the case that would move first if 64-bit counters did not survive the wasm32 # data model, which is why it is asserted before the human-readable form. check_eq vnstat/oneline-raw \ '1;lo;2026-01-02;2097152;1048576;3145728;72;2026-01;4194304;2097152;6291456;48;4194304;2097152;6291456' \ "$(vnstat --config "$C" -i lo --oneline b 2>&1)" # --oneline, the DEFAULT mode: the same numbers through getvalue() and # gettrafficrate(), so this one is about the formatter -- binary prefixes at two # decimals, and a bit/s rate floored to an integer (3145728*8/43200 = 582.5 -> # 582; 6291456*8/129600 = 388.4 -> 388). check_eq vnstat/oneline \ '1;lo;2026-01-02;2.00 MiB;1.00 MiB;3.00 MiB;582 bit/s;2026-01;4.00 MiB;2.00 MiB;6.00 MiB;388 bit/s;4.00 MiB;2.00 MiB;6.00 MiB' \ "$(vnstat --config "$C" -i lo --oneline 2>&1)" # --json, whole and byte-exact. vnStat emits it as ONE line with no insignificant # whitespace, so the comparison needs no parser and no jq -- which is the reason # this is the format the criterion names. check_eq vnstat/json \ '{"vnstatversion":"2.6","jsonversion":"2","interfaces":[{"name":"lo","alias":"","created":{"date":{"year":2026,"month":1,"day":1}},"updated":{"date":{"year":2026,"month":1,"day":2},"time":{"hour":12,"minute":0}},"traffic":{"total":{"rx":4194304,"tx":2097152},"fiveminute":[],"hour":[{"id":1,"date":{"year":2026,"month":1,"day":2},"time":{"hour":11,"minute":0},"rx":1048576,"tx":524288}],"day":[{"id":1,"date":{"year":2026,"month":1,"day":2},"rx":2097152,"tx":1048576}],"month":[{"id":1,"date":{"year":2026,"month":1},"rx":4194304,"tx":2097152}],"year":[{"id":1,"date":{"year":2026},"rx":4194304,"tx":2097152}],"top":[{"id":1,"date":{"year":2026,"month":1,"day":2},"rx":2097152,"tx":1048576}]}}]}' \ "$(vnstat --config "$C" -i lo --json 2>&1)" # The XML arm of the same seed, asserted structurally rather than byte-exact: # it is pretty-printed across ~40 lines, so a whole-document comparison would be # testing indentation. The two totals are what carry the data. xml=$(vnstat --config "$C" -i lo --xml 2>&1) case "$xml" in *'41943042097152'*) bst_ok vnstat/xml-total ;; *) bst_notok vnstat/xml-total "no seeded total in the xml output: $(_bst_1line "$xml")" ;; esac # ---------------------------------------------------- E the error paths ----- # THE TICKET'S THIRD CLAUSE. An interface that is not in the database is refused # with a non-zero status. out=$(vnstat --config "$C" -i nosuchif0 2>&1); rc=$? case "$out" in *'not found in database'*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -ne 0 ] && [ "$hit" -eq 1 ]; then bst_ok vnstat/unknown-iface else bst_notok vnstat/unknown-iface "rc=$rc want non-zero, want:'not found in database' got:'$(_bst_1line "$out")'"; fi # AND THE UPSTREAM QUIRK BESIDE IT, recorded rather than asserted away: the same # unknown interface through --json exits ZERO while printing the same error and # no JSON document. Measured natively against 2.6 on the build host, so this case # asserts what vnStat does rather than what it arguably should -- a change here # is a change in vnStat, and that is worth a red. out=$(vnstat --config "$C" -i nosuchif0 --json 2>&1); rc=$? case "$out" in '{'*) bst_notok vnstat/unknown-iface-json "--json emitted a document for an interface that is not in the database: $(_bst_1line "$out")" ;; *'not found in database'*) bst_ok vnstat/unknown-iface-json ;; *) bst_notok vnstat/unknown-iface-json "rc=$rc, neither the error nor a document: '$(_bst_1line "$out")'" ;; esac # A directory with no database at all: read-only open fails and says which file. rm -rf "$W/empty"; mkdir -p "$W/empty" out=$(vnstat --config "$C" --dbdir "$W/empty" -i lo 2>&1); rc=$? case "$out" in *'in read-only mode'*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -ne 0 ] && [ "$hit" -eq 1 ]; then bst_ok vnstat/no-database else bst_notok vnstat/no-database "rc=$rc want non-zero, want:'in read-only mode' got:'$(_bst_1line "$out")'"; fi # ------------------------------------------------------- F vnstati (gd) ----- # THE PNG ARM, which is the one case that exercises what this port's bbappend # fixed: libgd's PNG writer, and through it libpng and zlib, reaching symbols # that on a static-only arm are only on the link line because the bbappend put # them there. A wasmld build missing them cannot link at all, so reaching a file # here at runtime is the second half of that statement. bst_probe vnstat/vnstati-returns "$W/vi" 30 \ vnstati --config "$C" -i lo -s -o "$W/out.png" if [ -s "$W/out.png" ]; then bst_ok vnstat/png-nonempty else bst_notok vnstat/png-nonempty "vnstati produced no non-empty $W/out.png: $(_bst_1line "$(cat "$W/vi.err" 2>/dev/null)")"; fi # The 8-byte PNG signature, read with od(1) rather than by matching binary in the # shell: \x89PNG\r\n\x1a\n. sig=$(od -An -N8 -tx1 "$W/out.png" 2>/dev/null | tr -d ' \n') check_eq vnstat/png-signature "89504e470d0a1a0a" "$sig" # ------------------------------------------- G one vnstatd update pass ------ # THE TICKET'S FOURTH CLAUSE, and the case the /proc/uptime discipline is for. # # WHAT IS BEING MEASURED. vnstatd is started, left to run its update passes and # one periodic database save, and stopped. On this image the pass does NOT end at # "Nothing to monitor" -- see the head of this file: the validated interface list # is empty, its hash collides with the "no hash yet" sentinel, and # interfacechangecheck() returns before it can disable anything. lo is therefore # KEPT, the log stays at its two startup lines for the whole run, and what says # the pass ran is the DATABASE. # # WHY THE WINDOW IS 80 SECONDS AND NOT 20. The evidence is the periodic save, and # SaveInterval is in MINUTES -- daemon.c:405 multiplies it by 60, so the "1" # below is 60 seconds. A 20-second window ends before any save and leaves the # database exactly as --add left it, which is indistinguishable from a daemon # that never ran at all. MEASURED natively in the loopback-only namespace: at 80 s # under SIGKILL alone, interface.updated has advanced ~64 s past what --add wrote # and one hour row and one fiveminute row exist; at 20 s none of that is true. # # HOW IT IS BOUNDED. /proc/uptime is the clock and the loop body is BUILTINS ONLY # -- `read`, `[`, arithmetic expansion and `sleep 1`. Neither half is ceremony: an # elapsed counter does not advance here, and a relative sleep placed directly # after the shell reaps a FOREGROUND child returns at once # (linux-in-the-browser#120), which is why the daemon is a background child that # is never waited on inside the loop. Eighty guest-seconds is a CEILING, not a # sleep: nothing here waits for it to elapse except this one case. # # SIGKILL, AND NOTHING BELOW RELIES ON A CLEAN SHUTDOWN. vnstatd does flush its # cache on SIGTERM, and that flush is the obvious way to get a database write -- # but 87-cronie.test records signal delivery as broken platform-wide on this # runtime (kill(2) succeeds and nothing is delivered, SIGKILL included), so a # suite that needed a handler to run would be asserting the runtime rather than # vnStat. Everything below is written by the PERIODIC save inside the main loop, # which no signal takes part in, and was measured under SIGKILL alone. # # ON A DATABASE OF ITS OWN, and that is not tidiness: it keeps this case # independent of the sample section D seeds, so a change there cannot move the # counts asserted here. Wd=$W/daemon Cd=$W/daemon.conf mkdir -p "$Wd" 2>/dev/null printf '%s\n' \ "DatabaseDir \"$Wd\"" \ "PidFile \"$Wd/vnstatd.pid\"" \ "UseLogging 0" \ "BandwidthDetection 0" \ "UpdateInterval 5" \ "SaveInterval 1" > "$Cd" vnstatd --config "$Cd" --nodaemon --noadd >/dev/null 2>&1 vnstat --config "$Cd" --add -i lo >/dev/null 2>&1 # What --add left, so the advance asserted below is measured against the row's # own previous value rather than against a clock this suite reads separately. addedat=$(sqlite3 "$Wd/vnstat.db" "select updated from interface where name='lo';" 2>&1) # stdin comes from /dev/null and stdout goes to a FILE so the daemon holds no # descriptor of the suite's own -- a background child left holding the runner's # stdout is how a suite that has finished fails to end. vnstatd --config "$Cd" --nodaemon > "$W/d.out" 2>&1 /dev/null read -r u0 _ < /proc/uptime t0=${u0%.*} while [ -e /proc/"$dpid" ]; do read -r u _ < /proc/uptime [ $(( ${u%.*} - t0 )) -ge 10 ] && break sleep 1 done if [ ! -e /proc/"$dpid" ]; then bst_ok vnstat/daemon-reaped else bst_notok vnstat/daemon-reaped "vnstatd (pid $dpid) is still in /proc 10s after SIGKILL"; fi d=$(cat "$W/d.out" 2>/dev/null) case "$d" in *'vnStat daemon 2.6 started'*) bst_ok vnstat/daemon-started ;; *) bst_notok vnstat/daemon-started "no start banner from vnstatd: $(_bst_1line "$d")" ;; esac case "$d" in *'Monitoring (1): lo'*) bst_ok vnstat/daemon-picks-up-lo ;; *) bst_notok vnstat/daemon-picks-up-lo "vnstatd did not take lo from the database into its update list: $(_bst_1line "$d")" ;; esac # The loopback KEEP, asserted as the upstream behaviour it is rather than left to # surface as a puzzle in some later suite. Both strings are checked, because they # are the two ways the other branch would announce itself. case "$d" in *'Interface "lo" disabled'*) bst_notok vnstat/daemon-keeps-lo "vnstatd disabled lo; with only lo present the validated iface list is empty and interfacechangecheck() returns at the src/daemon.c:865 hash sentinel before the disable branch, so this transcript is not this machine's: $(_bst_1line "$d")" ;; *'Nothing to monitor'*) bst_notok vnstat/daemon-keeps-lo "vnstatd reached 'Nothing to monitor'; lo should have been kept: $(_bst_1line "$d")" ;; *) bst_ok vnstat/daemon-keeps-lo ;; esac # THE PASS REACHED THE DATABASE, not just the log. An hour row and a fiveminute # row are created by a completed processdatacache() and written by the periodic # save; neither exists after --add alone, so their presence is the pass. hrows=$(sqlite3 "$Wd/vnstat.db" "select count(*) from hour;" 2>&1) frows=$(sqlite3 "$Wd/vnstat.db" "select count(*) from fiveminute;" 2>&1) if [ "$hrows" -ge 1 ] 2>/dev/null && [ "$frows" -ge 1 ] 2>/dev/null; then bst_ok vnstat/daemon-pass-completes else bst_notok vnstat/daemon-pass-completes "no periodic save landed inside the 80 s window: hour rows='$hrows', fiveminute rows='$frows', want at least one of each"; fi # And lo is still in the database, with its timestamp moved on by that save -- # the removal at src/daemon.c:636 is the thing this machine never reaches. nowat=$(sqlite3 "$Wd/vnstat.db" "select updated from interface where name='lo';" 2>&1) cnt=$(sqlite3 "$Wd/vnstat.db" "select count(*) from interface where name='lo';" 2>&1) if [ "$cnt" = "1" ] && [ -n "$nowat" ] && [ "$nowat" != "$addedat" ]; then bst_ok vnstat/daemon-wrote-db else bst_notok vnstat/daemon-wrote-db "lo rows='$cnt' (want 1), updated '$addedat' -> '$nowat' (want it to advance)"; fi rm -rf "$W"