. "$BST_LIB" # BST-TIMEOUT: 600 # 64 cases, 44 of them an lspci exec over a two-dump corpus. # MEASURED at elapsed=120s on an idle hz1 (2026-08-23, # ==BST-SUITE pciutils elapsed=120s cases=64), declared at 5x # that. The default 60s would trip on the corpus alone; the # 1800 this first carried was 15x and gave the sentinel back # the protection the bound exists to provide. # pciutils -- lspci's OFFLINE arm, compared BYTE FOR BYTE against a native x86-64 # oracle. # # WHAT THIS MACHINE CAN AND CANNOT TEST. lspci's usual job is to enumerate a bus, # and there is no bus here: no /sys/bus/pci, no /proc/bus/pci, no /dev/mem. Plain # `lspci` and every `setpci` therefore have nowhere to look, and they are recorded # at the end as expected reds attributed to the absence of PCI hardware rather than # fixed. What IS testable is the arm the port was scoped around: `lspci -F ` # decodes a configuration-space dump saved earlier by `lspci -x` having opened no # bus at all, and the pci.ids lookup that turns numeric ids into vendor and device # names is a pure table read. That is the majority of lspci's code and all of # libpci's decode path. # # WHY A COMPARISON RATHER THAN ASSERTIONS. Decoding a dump is parse, table-index # and format -- arithmetic on bytes with no I/O in it -- so a correct wasm32 build # must print exactly what a native build of the same version prints on the same # input. Every alternative is weaker in a way that matters: an assertion that the # output "contains Intel" passes on a build that got a bitfield boundary wrong and # mislabelled a capability; a checked-in expectation is a snapshot of one upstream # version that rots into a false red the day poky bumps pciutils, and then gets # re-blessed from whatever the target printed, which is the comparison inverted. # The expectation is generated at build time by pciutils-native -- the SAME recipe # version -- and installed as /usr/libexec/pciutils-tests. # # WHAT IS DELIBERATELY NOT COMPARED. The access-method list. x86-64 compiles in # intel-conf1 and intel-conf2 (port I/O) and wasm32 structurally cannot, so the # lists differ for a reason that is not a defect. It is asserted by MEMBERSHIP # instead -- `dump` is present, which is the one method this port turns on. # # AND WHY THE CONTROLS AT THE END ARE NOT DECORATION. A comparator with a bug in it # passes everything, and 44 green comparisons look identical either way. The # controls take a known-good oracle answer, change ONE character of it, and require # the comparator to say so; each reports whether the mutation actually applied, so # a control that poses no question is a named failure rather than a quiet pass. bst_ok pciutils/suite-canary # first line: absorbs brintos/linux#96, which drops it require lspci pciutils/present T=/usr/libexec/pciutils-tests if [ ! -f "$T/oracle-info" ] || [ ! -d "$T/cases" ]; then bst_notok pciutils/corpus "no oracle corpus at $T -- pciutils-tests is not installed, so every comparison in this suite is unrunnable" exit 0 fi W=${TMPDIR:-/tmp}/bst-pciutils.$$ if ! mkdir -p "$W" 2>/dev/null; then bst_notok pciutils/stage "cannot create working directory $W" exit 0 fi # The corpus is COPIED TO /tmp and read from there, never redirected from the # rootfs. A top-level `while read ... done < /usr/...` on the virtiofs root hits # brintos/linux#73: dup2(2) returns 513, ash prints "Unknown error 513" and exits, # and the runner then reports every case emitted so far as a pass and nothing at # all about the ones never reached -- a clean-looking run that is simply shorter. if ! cp -r "$T/cases" "$W/cases" 2>/dev/null; then bst_notok pciutils/stage "cannot copy $T/cases to $W" rm -rf "$W"; exit 0 fi info_pv=$(sed -n 's/^pv //p' "$T/oracle-info") info_cases=$(sed -n 's/^cases //p' "$T/oracle-info") info_flagsets=$(sed -n 's/^flagsets //p' "$T/oracle-info") info_dumps=$(sed -n 's/^dumps //p' "$T/oracle-info") info_ids_sha=$(sed -n 's/^ids-sha256 //p' "$T/oracle-info") # --- The comparator. --------------------------------------------------------- # cmp is byte-exact; the fallback exists so a stripped image reports a real # comparison rather than skipping the suite. `$(cat)` drops trailing newlines on # both sides, so the byte count is what closes that hole -- and # control-trailing-newline below proves the pair actually does. if command -v cmp >/dev/null 2>&1; then PU_CMP=1; else PU_CMP=; fi pu_same() { # pu_same if [ -n "$PU_CMP" ]; then cmp -s "$1" "$2" else [ "$(cat "$1")" = "$(cat "$2")" ] && [ "$(wc -c < "$1")" = "$(wc -c < "$2")" ] fi } # --- The two things the ticket said to check rather than assume. ------------- # 1. WHICH BACK-ENDS THE BUILD COMPILED IN, asked of the binary rather than of # lib/config.mk. A define that survived configure and a method that registered # at run time are different claims, and only the second one can serve `-F`. methods=$(lspci -A help 2>&1) case "$methods" in *dump*) bst_ok pciutils/dump-backend ;; *) bst_notok pciutils/dump-backend "the dump back-end is not registered, so lspci -F -- this port's entire pass criterion -- cannot work. Methods: $(_bst_1line "$methods")" ;; esac # 2. THE DATABASE, NOT A PROMISE OF ONE. update-pciids(8) downloads pci.ids at # first run and this image has no network at boot, so a port that shipped only # the downloader would answer every name query with a bare number -- while # still exiting 0. if [ -f /usr/share/pci.ids.gz ]; then bst_ok pciutils/ids-present # Identity, not mere presence: the whole corpus is names read out of this file, # so a target carrying a DIFFERENT pci.ids would fail every comparison below # for a reason that has nothing to do with the port. An image without # sha256sum reports a skip rather than an empty-string mismatch, which would # read as a real divergence. if command -v sha256sum >/dev/null 2>&1; then tgt_sha=$(sha256sum /usr/share/pci.ids.gz 2>/dev/null | cut -d' ' -f1) check_eq pciutils/ids-identity "$info_ids_sha" "$tgt_sha" else bst_skip pciutils/ids-identity "no sha256sum on this image, so the database bytes cannot be pinned to the oracle's" fi else bst_notok pciutils/ids-present "no /usr/share/pci.ids.gz -- every name in the corpus below is read out of that file" bst_notok pciutils/ids-identity "no database to hash" fi # 2b. THE DEFAULT PATH RESOLVES TO THE INSTALLED DATABASE. Every comparison in # this suite names the database with -i, because the oracle must (its own # compiled-in path does not resolve at build time). That deliberately leaves # the question the ticket actually asked -- is pci.ids really installed where # this binary looks for it? -- unasked by the corpus, so it is asked here, on # the target, where it is a property worth having. Same command twice, once # letting the binary find its own database and once naming it: identical # output is the only outcome consistent with the compiled-in path resolving. D=$T/dumps/pciutils-synth.dump if [ -f "$D" ] && [ -f /usr/share/pci.ids.gz ]; then lspci -F "$D" > "$W/ids.default" 2>&1 lspci -F "$D" -i /usr/share/pci.ids.gz > "$W/ids.explicit" 2>&1 if pu_same "$W/ids.default" "$W/ids.explicit"; then bst_ok pciutils/ids-default-path else bst_notok pciutils/ids-default-path "lspci finds a different database on its own than at /usr/share/pci.ids.gz, so the compiled-in PCI_PATH_IDS_DIR does not resolve to the installed file -- default: $(_bst_1line "$(head -2 "$W/ids.default")") explicit: $(_bst_1line "$(head -2 "$W/ids.explicit")")" fi # 2c. ...AND THE DATABASE IS NOT EMPTY. An lspci that cannot read pci.ids does # not warn and does not fail -- it prints a complete, stable decode with # every name replaced by its numeric id. So "default matches explicit" # above is also satisfied by two equally name-free outputs. The control is # the same command against an empty database: same units, no threshold, # nothing hardcoded about which vendors this corpus holds. lspci -F "$D" -i /dev/null > "$W/ids.empty" 2>&1 if pu_same "$W/ids.explicit" "$W/ids.empty"; then bst_notok pciutils/ids-resolves "lspci prints the same bytes with /usr/share/pci.ids.gz as with an empty database, so no name is being looked up and every comparison above is measuring the numeric fallback" else bst_ok pciutils/ids-resolves fi else bst_notok pciutils/ids-default-path "no corpus dump or no database to compare" bst_notok pciutils/ids-resolves "no corpus dump or no database to compare" fi # --- The rest of the deliverable: the library and its headers. --------------- check pciutils/libpci-static test -f /usr/lib/libpci.a check pciutils/libpci-header test -f /usr/include/pci/pci.h check pciutils/libpci-pc test -f /usr/lib/pkgconfig/libpci.pc # lspci is an update-alternatives link; the binary ships as lspci.pciutils and the # name the corpus is driven through only exists if the link was made at rootfs time. check pciutils/lspci-link test -x /usr/bin/lspci ver=$(lspci --version 2>&1) case "$ver" in *"$info_pv"*) bst_ok pciutils/version ;; *) bst_notok pciutils/version "target lspci is not the oracle's version $info_pv: $(_bst_1line "$ver")" ;; esac # The argv is READ FROM THE CORPUS, not re-typed here. The oracle wrote it with # @DUMP@/@IDS@ left in place precisely so each side resolves its own paths -- which # also means a flagset added to the oracle reaches the target without editing this # file, and the two cannot drift. pu_run() { # pu_run ; sets $pu_rc, writes $W/o # Both arguments are read into names BEFORE `set --` clears the positional # parameters, and the dump path is resolved once rather than per argument. pu_dir=$1; pu_id=$2 pu_dump=$T/dumps/$(cat "$pu_dir/$pu_id.dump").dump set -- while IFS= read -r a; do case "$a" in *@DUMP@*) a=$(printf '%s' "$a" | sed "s|@DUMP@|$pu_dump|g") ;; *@IDS@*) a=$(printf '%s' "$a" | sed "s|@IDS@|/usr/share/pci.ids.gz|g") ;; esac set -- "$@" "$a" # $pu_dir is always "$W/cases", the $TMPDIR copy staged at the top of this # file for exactly this reason -- the checker cannot see that through a # function parameter. The loop must stay in the top-level shell: it builds # the positional parameters the caller then runs. # BST-REDIRECT-OK: reads the $TMPDIR corpus copy, never the rootfs done < "$pu_dir/$pu_id.argv" lspci "$@" > "$W/o" 2>&1 pu_rc=$? } : > "$W/list" ( cd "$W/cases" && find . -name '*.argv' | sed 's|^\./||; s|\.argv$||' | sort ) > "$W/list" n_all=0 n_bad=0 while IFS= read -r id; do [ -n "$id" ] || continue n_all=$((n_all + 1)) pu_run "$W/cases" "$id" arc=$pu_rc erc=$(cat "$W/cases/$id.rc") diff= pu_same "$W/cases/$id.out" "$W/o" || diff=output [ "$arc" = "$erc" ] || diff="${diff:+$diff,}rc(oracle $erc, target $arc)" # The corpus names cases __; the result id drops the shared # "pciutils-" prefix so a red reads as pciutils/synth-verbose2 rather than # pciutils/pciutils-synth__verbose2. label=$(printf '%s' "$id" | sed 's|^pciutils-||; s|__|-|') if [ -z "$diff" ]; then bst_ok "pciutils/$label" else n_bad=$((n_bad + 1)) bst_notok "pciutils/$label" "differs from the x86-64 oracle in $diff -- oracle: $(_bst_1line "$(cat "$W/cases/$id.out")") target: $(_bst_1line "$(cat "$W/o")")" fi done < "$W/list" # The corpus must be the whole corpus. A partially-installed one would make every # comparison above pass over fewer cases, which is the one regression a green run # cannot show. check_eq pciutils/case-count "$info_cases" "$n_all" # --- Controls on the comparator itself. -------------------------------------- # Each takes a known-good oracle answer, changes exactly one thing, and requires # the comparison to notice. Each also reports whether the mutation APPLIED, so a # control that posed no question is a named failure rather than a quiet pass. ctl=$W/cases/pciutils-synth__plain.out if [ -f "$ctl" ]; then cp "$ctl" "$W/ctl.orig" # Whether a mutation APPLIED is asked with grep, never with the comparator the # control exists to test. Asking pu_same both questions makes them one question: # a mutation that changed nothing leaves the files identical, pu_same says # "same", and the control reports a blind comparator when the truth is that it # posed no question at all. Measured on the native control run 2026-08-23, # where the corpus legitimately contained no "Intel" to change and the red # named the wrong cause. # 1. One character, in the middle of a device name -- the pci.ids half. if ! grep -q 'Intel' "$W/ctl.orig"; then bst_notok pciutils/control-one-char "the corpus holds no 'Intel' to change, so this control posed no question -- which itself means the oracle resolved no names" else sed 's/Intel/Intex/' "$W/ctl.orig" > "$W/ctl.1" if pu_same "$W/ctl.orig" "$W/ctl.1"; then bst_notok pciutils/control-one-char "the comparator cannot see a changed device name" else bst_ok pciutils/control-one-char fi fi # 2. One HEX DIGIT of a device address. The addresses are what the dump parser # produced, so this is the control for the half of the pipeline the name # mutation above cannot reach. if ! grep -q '^0000:00:03\.0' "$W/ctl.orig"; then bst_notok pciutils/control-one-address "the corpus holds no 0000:00:03.0 line to change, so this control posed no question" else sed 's/^0000:00:03\.0/0000:00:04.0/' "$W/ctl.orig" > "$W/ctl.2" if pu_same "$W/ctl.orig" "$W/ctl.2"; then bst_notok pciutils/control-one-address "the comparator cannot see a changed device address" else bst_ok pciutils/control-one-address fi fi # 3. The TRAILING NEWLINE alone. This is the one the `$(cat)` fallback # comparator is structurally blind to, which is why pu_same carries a byte # count beside the string compare -- and why that pairing is tested rather # than assumed. printf '%s' "$(cat "$W/ctl.orig")" > "$W/ctl.3" if pu_same "$W/ctl.orig" "$W/ctl.3"; then bst_notok pciutils/control-trailing-newline "the comparator cannot see a missing trailing newline" else bst_ok pciutils/control-trailing-newline fi # 4. A DROPPED LINE. A corpus truncated in transport would otherwise compare # equal over the part that arrived. sed '2d' "$W/ctl.orig" > "$W/ctl.4" if pu_same "$W/ctl.orig" "$W/ctl.4"; then bst_notok pciutils/control-dropped-line "the comparator cannot see a dropped line" else bst_ok pciutils/control-dropped-line fi else bst_notok pciutils/control-one-char "no $ctl to mutate" bst_notok pciutils/control-one-address "no $ctl to mutate" bst_notok pciutils/control-trailing-newline "no $ctl to mutate" bst_notok pciutils/control-dropped-line "no $ctl to mutate" fi # 5. The control that establishes what a non-empty comparison MEANS. lspci exits 0 # and prints nothing on input it cannot parse, so "the output matched" would be # satisfiable by two empty strings. This is the same control the oracle ran on # the build host, re-run here so the claim holds on both sides. printf 'this is not a configuration space dump\n' > "$W/bad.dump" badout=$(lspci -F "$W/bad.dump" 2>&1) if [ -z "$badout" ]; then bst_ok pciutils/control-unparseable else bst_notok pciutils/control-unparseable "unparseable input produced output, so a non-empty case no longer proves the dump parsed: $(_bst_1line "$badout")" fi # 6. ...and that lspci reports a dump it cannot open at all, rather than silently # decoding nothing. This is what separates "the corpus was read" from "the # corpus was missing and every case compared empty to empty". lspci -F "$W/no-such-file.dump" >/dev/null 2>&1 if [ $? -ne 0 ]; then bst_ok pciutils/control-missing-file else bst_notok pciutils/control-missing-file "an absent dump exited 0"; fi # --- The live-bus arm: expected reds, recorded rather than skipped. ----------- # These are the cases the ticket named in advance as acceptable failures. They are # left FAILING rather than skipped so they flip green on their own if this machine # ever grows a PCI bus; a skip would have to be noticed and edited by a human. if [ -e /sys/bus/pci/devices ] || [ -e /proc/bus/pci ]; then check pciutils/live-enumeration lspci check pciutils/setpci-live setpci -s 00:00.0 VENDOR_ID else bst_notok pciutils/live-enumeration "no /sys/bus/pci and no /proc/bus/pci on this machine, so plain lspci has no bus to enumerate. EXPECTED: this is a machine without PCI hardware, not a defect in the port -- the offline arm above is what this port was scoped around." bst_notok pciutils/setpci-live "setpci writes and reads live configuration space, which needs the same absent bus. EXPECTED, and for the same reason." fi rm -rf "$W"