. "$BST_LIB" # BST-TIMEOUT: 900 # 144 corpus comparisons plus ~15 structural cases, one # sdparm exec apiece. Set from a measurement, not a guess -- # see the ==BST-SUITE line this suite emits and the design doc # for the elapsed it was declared against. The default 60s # would trip inside the enumerate matrix alone. # sdparm -- the DECODER and TABLE arms, compared BYTE FOR BYTE against a native # x86-64 oracle of the same version. # # WHAT THIS MACHINE CAN AND CANNOT TEST. sdparm gets and sets SCSI mode pages on a # device, and there is no SCSI device here: no /dev/sg*, no /dev/sd*, nothing # behind SG_IO. The device-driven forms are therefore probed at the end of this # file and skipped on what the probe FOUND -- not on a hardcoded assumption -- so # they run for real the day this machine gains a device. # # What can run is most of what sdparm is. It carries the SPC/SBC/SSC/SMC/MMC # mode-page and VPD tables in sdparm_data.c and exposes them two device-free ways: # # --enumerate prints the table: 50 generic mode pages, 16 transport tables, # 8 vendor tables, every field's byte:bit:width and description. # --inhex=FN decodes a mode-page or VPD RESPONSE read from a FILE, through # the same table and the same extraction the device path uses. # # WHY A COMPARISON RATHER THAN ASSERTIONS. Both forms are deterministic across # architectures, so a correct wasm32 build must print exactly what a native build # of the SAME VERSION prints on the SAME BYTES. Weaker assertions do not survive # contact with the failure they are meant to catch: "sdparm --enumerate ran", "the # output mentions WCE" and "it printed 540 lines" are all satisfied by a table # dump that mislabelled every field on the page. # # AND THE --inhex HALF IS THE HALF THAT IS ACTUALLY AT RISK. --enumerate walks # static tables and prints what they say; a ported table dump can be right for the # same reason a `cat` is. --inhex extracts multi-byte big-endian values out of a # buffer and prints them, which is the one place an ILP32 target and an LP64 build # host can legitimately disagree. A suite that dumped tables and stopped would # have exercised none of it, and would have passed on a build that could not # decode a single response correctly. # # WHERE THE CORPUS COMES FROM. sdparm ships no reference buffers, so the oracle # SYNTHESIZES one MODE SENSE(10) response per enumerated page from the field # extents that page itself declares, filled with a deterministic non-zero pattern. # It is derived at build time from the binary under test, so it cannot drift from # the version it exercises. The fill is not zeros on purpose: a zero-filled page # decodes to a screen of zeros, which is also what a broken extractor prints. # # THE TWO SHELL HELPERS ARE HERE TO TEST BASH, NOT sdparm. sas_disk_blink and # scsi_ch_swp are #!/bin/bash and are not compiled, so they are byte-identical on # both sides and a divergence in what they print is a divergence in the bash that # ran them. "bash is in the image" is a weaker claim than "bash on wasm32 executes # these and produces the same bytes as bash on x86-64". # # AND THE CONTROLS AT THE END ARE NOT DECORATION. A comparator with a bug in it # passes everything, and 144 green comparisons look identical either way. bst_ok sdparm/suite-canary # first line: absorbs brintos/linux#96, which drops it require sdparm sdparm/present T=/usr/libexec/sdparm-tests if [ ! -f "$T/oracle-info" ] || [ ! -d "$T/cases" ]; then bst_notok sdparm/corpus "no oracle corpus at $T -- sdparm-tests is not installed, so every comparison in this suite is unrunnable" exit 0 fi W=${TMPDIR:-/tmp}/bst-sdparm.$$ if ! mkdir -p "$W" 2>/dev/null; then bst_notok sdparm/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 || ! cp -r "$T/inhex" "$W/inhex" 2>/dev/null; then bst_notok sdparm/stage "cannot copy the corpus from $T to $W" rm -rf "$W"; exit 0 fi for f in oracle-info no-field-pages.list manifest.sha256 oracle-report.txt; do cp "$T/$f" "$W/$f" 2>/dev/null done info_pv=$(sed -n 's/^pv //p' "$W/oracle-info") info_cases=$(sed -n 's/^cases //p' "$W/oracle-info") info_buffers=$(sed -n 's/^buffers //p' "$W/oracle-info") info_pages=$(sed -n 's/^pages //p' "$W/oracle-info") info_modebuf=$(sed -n 's/^mode-buffers //p' "$W/oracle-info") info_transports=$(sed -n 's/^transports //p' "$W/oracle-info") info_vendors=$(sed -n 's/^vendors //p' "$W/oracle-info") info_helpers=$(sed -n 's/^helpers //p' "$W/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 SD_CMP=1; else SD_CMP=; fi sd_same() { # sd_same if [ -n "$SD_CMP" ]; then cmp -s "$1" "$2" else [ "$(cat "$1")" = "$(cat "$2")" ] && [ "$(wc -c < "$1")" = "$(wc -c < "$2")" ] fi } # --- The binary is the one the oracle is about. ------------------------------ # Every comparison below rests on "the same program, two architectures". A # version skew would make all 144 of them a comparison of two different programs, # and would show up as 144 reds with no indication of why. tgt_pv=$(sdparm --version 2>&1 | sed -n 's/^version: *\([0-9.]*\).*/\1/p') check_eq sdparm/version "$info_pv" "$tgt_pv" # --- The rest of the deliverable is actually installed. ---------------------- check sdparm/binary-installed test -x /usr/bin/sdparm check sdparm/bash-present test -x /bin/bash n_help=0; n_helpbad=0; helpbad= for h in sas_disk_blink scsi_ch_swp; do n_help=$((n_help + 1)) [ -x "/usr/bin/$h" ] || { n_helpbad=$((n_helpbad + 1)); helpbad="$helpbad $h"; } done check_eq sdparm/helper-count "$info_helpers" "$n_help" if [ "$n_helpbad" -eq 0 ]; then bst_ok sdparm/helpers-installed else bst_notok sdparm/helpers-installed "$n_helpbad of $n_help shell helpers are not executable:$helpbad" fi # --- The corpus arrived intact. ---------------------------------------------- # Every comparison below is against these bytes, so a corpus truncated in # transport would make the suite pass over whatever survived. The manifest is fed # on STDIN, which is what keeps ~490 filenames off the argv -- the guest's # argument buffer is 8192 bytes and an exec that exceeds it wedges rather than # errors (brintos/hardwarejs#15). if command -v sha256sum >/dev/null 2>&1; then if grep -v '^#' "$W/manifest.sha256" | ( cd "$T" && sha256sum -c --quiet ) >"$W/sha.err" 2>&1; then bst_ok sdparm/corpus-identity else bst_notok sdparm/corpus-identity "the installed corpus does not match the manifest the oracle wrote: $(_bst_1line "$(head -3 "$W/sha.err")")" fi else bst_skip sdparm/corpus-identity "no sha256sum on this image, so the corpus bytes cannot be pinned to the oracle's" fi check_eq sdparm/buffer-count "$info_buffers" "$(find "$W/inhex" -type f | wc -l)" # --- The table the port is scoped around, re-measured on the shipped binary. -- # The oracle counted these from the NATIVE binary. Counting them again here is # what makes them a claim about the wasm32 build rather than about the build host, # and the three are independent: a table can lose pages while keeping transports. tgt_pages=$(sdparm -e 2>/dev/null | sed -n 's/^ [^ ][^ ]* *0x.*/x/p' | wc -l) tgt_tr=$(sdparm -e -t 99 2>&1 | sed -n 's/^ [a-z0-9]* *0x\([0-9a-f][0-9a-f]\) *.*/\1/p' | sort -u | wc -l) tgt_ven=$(sdparm -e -M 999 2>&1 | sed -n 's/^ [a-z0-9]* *0x\([0-9a-f][0-9a-f]\) *.*/\1/p' | sort -u | wc -l) check_eq sdparm/page-count "$info_pages" "$(echo $tgt_pages)" check_eq sdparm/transport-count "$info_transports" "$(echo $tgt_tr)" check_eq sdparm/vendor-count "$info_vendors" "$(echo $tgt_ven)" # --- The comparison. --------------------------------------------------------- # The argv is READ FROM THE CORPUS, not re-typed here -- so a case added to the # oracle reaches the target without editing this file, and the two cannot drift. # Each case runs from the buffer directory and names its tool and its buffer by # BARE NAME, which is what makes the argv byte-identical to the oracle's: sdparm # echoes both back in its own error text ("sdparm: unrecognized option", # "Unable to open no-such-buffer.hex"), so an absolute path on either side would # diverge those cases for a reason that is not the decoder. sd_run() { # sd_run ; sets $sd_rc, writes $W/o sd_dir=$1; sd_id=$2 set -- # $sd_dir is always "$W/cases", the $TMPDIR copy staged at the top of this # file precisely so this loop does not read from the virtiofs root # (brintos/linux#73) -- 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, and a subshell would discard # them. # BST-REDIRECT-OK: reads the $TMPDIR corpus copy, never the rootfs while IFS= read -r a; do set -- "$@" "$a"; done < "$sd_dir/$sd_id.argv" sd_tool=$1; shift ( cd "$W/inhex" && "$sd_tool" "$@" ) > "$W/o" 2>&1 sd_rc=$? } ( cd "$W/cases" && find . -name '*.argv' | sed 's|^\./||; s|\.argv$||' | sort ) > "$W/list" n_all=0 while IFS= read -r id; do [ -n "$id" ] || continue n_all=$((n_all + 1)) sd_run "$W/cases" "$id" arc=$sd_rc erc=$(cat "$W/cases/$id.rc") diff= sd_same "$W/cases/$id.out" "$W/o" || diff=output [ "$arc" = "$erc" ] || diff="${diff:+$diff,}rc(oracle $erc, target $arc)" if [ -z "$diff" ]; then bst_ok "sdparm/$id" else bst_notok "sdparm/$id" "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 sdparm/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. Whether the mutation APPLIED is asked with grep, never # with the comparator under test: asking sd_same both questions makes them one # question, and a mutation that changed nothing then reports a blind comparator # when the truth is that it posed no question at all. ctl=$W/cases/ih-ca.out if [ ! -f "$ctl" ]; then bst_notok sdparm/control-corpus "the corpus holds no ih-ca case to mutate, so none of the comparator controls below could be posed" else cp "$ctl" "$W/ctl.orig" # 1. One character of a decoded FIELD NAME -- the table-lookup half. if ! grep -q 'WCE' "$W/ctl.orig"; then bst_notok sdparm/control-one-char "the caching-page decode holds no 'WCE' to change, so this control posed no question -- which itself means the decode produced no field names" else sed 's/WCE/WCF/' "$W/ctl.orig" > "$W/ctl.1" if sd_same "$W/ctl.orig" "$W/ctl.1"; then bst_notok sdparm/control-one-char "the comparator cannot see a changed field name" else bst_ok sdparm/control-one-char fi fi # 2. One digit of a decoded multi-byte VALUE. The values come out of the # buffer parser, which is the half the name mutation above cannot reach -- # and it is the half an ILP32/LP64 disagreement would break. if ! grep -q '38860' "$W/ctl.orig"; then bst_notok sdparm/control-one-value "the caching-page decode holds no DPTL 38860 to change, so this control posed no question -- the synthesized buffer did not decode to the value it was built to produce" else sed 's/38860/38861/' "$W/ctl.orig" > "$W/ctl.2" if sd_same "$W/ctl.orig" "$W/ctl.2"; then bst_notok sdparm/control-one-value "the comparator cannot see a changed decoded value" else bst_ok sdparm/control-one-value fi fi # 3. The TRAILING NEWLINE alone -- the one the `$(cat)` fallback comparator is # structurally blind to, which is why sd_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 sd_same "$W/ctl.orig" "$W/ctl.3"; then bst_notok sdparm/control-trailing-newline "the comparator cannot see a missing trailing newline" else bst_ok sdparm/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 sd_same "$W/ctl.orig" "$W/ctl.4"; then bst_notok sdparm/control-dropped-line "the comparator cannot see a dropped line" else bst_ok sdparm/control-dropped-line fi # 5. The EXIT STATUS is compared separately from the bytes, so it needs its # own control: a suite that only diffed stdout would pass a build whose # error paths all returned 0. if [ "$(cat "$W/cases/err-nofile.rc")" = "$(cat "$W/cases/version.rc")" ]; then bst_notok sdparm/control-rc-distinct "the oracle recorded the same exit status for --version and for a missing --inhex file, so the rc half of every comparison asks nothing" else bst_ok sdparm/control-rc-distinct fi fi # --- The device-driven arm: PROBED, not assumed. ----------------------------- # --inquiry, --get and --set need a real SCSI device. This kernel has no block # layer and no SCSI, so they are expected to be unreachable -- but they are # skipped on what this probe FINDS, with the evidence recorded, so the day the # machine gains a device the very same cases run the real assertion instead. sd_dev= for d in /dev/sg0 /dev/sg1 /dev/sg2 /dev/sda /dev/sdb /dev/sr0; do [ -e "$d" ] && { sd_dev=$d; break; } done if [ -z "$sd_dev" ]; then for d in /sys/class/scsi_generic/* /sys/class/scsi_device/*; do [ -e "$d" ] || continue n=$(basename "$d") [ -e "/dev/$n" ] && { sd_dev=/dev/$n; break; } done fi if [ -z "$sd_dev" ]; then sd_ev="no /dev/sg[0-2], no /dev/sd[ab], no /dev/sr0" if [ -d /sys/class/scsi_generic ]; then sd_ev="$sd_ev; /sys/class/scsi_generic exists and holds $(ls /sys/class/scsi_generic 2>/dev/null | wc -l) entries" else sd_ev="$sd_ev; /sys/class/scsi_generic absent" fi [ -d /sys/class/scsi_device ] || sd_ev="$sd_ev; /sys/class/scsi_device absent" bst_skip sdparm/device-inquiry "probe found no SCSI device -- $sd_ev" bst_skip sdparm/device-all "probe found no SCSI device -- $sd_ev" bst_skip sdparm/device-get-wce "probe found no SCSI device -- $sd_ev" else check_out sdparm/device-inquiry "VPD" sdparm --inquiry "$sd_dev" check sdparm/device-all sdparm --all "$sd_dev" check sdparm/device-get-wce sdparm --get=WCE "$sd_dev" fi rm -rf "$W"