. "$BST_LIB" # BST-TIMEOUT: 900 # 65 oracle comparisons plus the split probe and the # # informational cases -- ~80 execs of a 3 MB multi-call # # binary. Provisional; retuned from the first # # ==BST-SUITE lvm2 elapsed=Ns== measurement. # lvm2 -- the CONFIGURATION AND METADATA PARSER arm, compared BYTE FOR BYTE # against a native x86-64 oracle of the same version. # # WHAT THIS MACHINE CAN AND CANNOT TEST. lvm2 manages logical volumes through the # kernel's device-mapper, and this kernel has none: there is no /dev/mapper/control # and nothing behind its ioctls. Anything that must talk to dm therefore cannot # work here, and those cases are recorded as expected reds rather than skipped. # # What CAN run is the arm the port was scoped around, and it is not a small one: # lvm2's whole command set is driven by a TEXT configuration and a TEXT on-disk # metadata format, both of which are read from a file. Parsing, validating and # re-emitting them is 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 BYTES, including exit status and including the byte offset in a parse # error. # # WHY A COMPARISON RATHER THAN ASSERTIONS. Weaker checks do not survive contact # with the failure they exist to catch. "lvmconfig ran" and "the output mentions # devices" are both satisfied by a build that mis-parsed every value it printed, # and a config dump is exactly the shape of output where that is invisible: 267 # plausible-looking lines, one of them wrong. # # WHERE THE CORPUS COMES FROM. Upstream's own where it exists -- conf/example.conf # as lvm2's configure generates it, and the eight conf/*.profile files it ships -- # read out of ${S} at build time so they are the corpus THIS version carries and # cannot drift from the code they exercise. Authored only where upstream ships # nothing: the nine malformed stanzas (lvm2 has no corpus of bad configs) and the # volume-group backup record. bst_ok lvm2/suite-canary # first line: absorbs brintos/linux#96, which drops it require lvm lvm2/present T=/usr/libexec/lvm2-tests if [ ! -d "$T/cases" ] || [ ! -d "$T/sys" ]; then bst_notok lvm2/corpus "no oracle corpus at $T -- lvm2-tests is not installed, so every comparison in this suite is unrunnable" exit 0 fi W=${TMPDIR:-/tmp}/bst-lvm2.$$ if ! mkdir -p "$W" 2>/dev/null; then bst_notok lvm2/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 # about the ones never reached -- a clean-looking run that is simply shorter. # # The copy is also NEEDED rather than merely safe: lvm writes a lock file under # its own system directory for the reporting commands, and the rootfs is # read-only. if ! cp -r "$T/cases" "$W/cases" 2>/dev/null || ! cp -r "$T/sys" "$W/sys" 2>/dev/null; then bst_notok lvm2/stage "cannot copy the corpus from $T to $W" rm -rf "$W"; exit 0 fi # --- 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 the # trailing-newline control at the end proves the pair actually does. if command -v cmp >/dev/null 2>&1; then LV_CMP=1; else LV_CMP=; fi lv_same() { # lv_same if [ -n "$LV_CMP" ]; then cmp -s "$1" "$2" else [ "$(cat "$1")" = "$(cat "$2")" ] && [ "$(wc -c < "$1")" = "$(wc -c < "$2")" ] fi } # --- The normaliser, which must be the ORACLE'S, line for line. --------------- # Five substitutions, each of which the two arms would otherwise disagree on for a # build that is entirely correct: # 1. the non-root WARNING -- on the oracle's stderr, never on the target's. # 2. LVM_SYSTEM_DIR -- lvmconfig prints it back as config/profile_dir. # 3. the compiled-in sbindir -- global/fsadm_executable carries it, and it is # the native sysroot on one arm and /usr/sbin on the other. DERIVED FROM THE # TOOL, not from where the binary sits: those are different paths, and # folding the binary's own directory silently leaves the value in place. # 4. TZ=UTC -- vgcfgrestore renders Backup Time through localtime. # 5. LC_ALL=C -- sort order and number formatting. # 6. lvm's own fd-leak checker: under bitbake's pseudo the oracle gets three # "File descriptor NN (...) leaked on lvm invocation" lines per case and the # target gets none. Dropped on BOTH arms so the filter cannot itself become # an asymmetry. TARGET_SBIN=$(TZ=UTC LC_ALL=C lvm lvmconfig --type full 2>/dev/null \ | sed -n 's|^[[:space:]]*fsadm_executable="\(.*\)/fsadm"$|\1|p' | head -1) if [ -z "$TARGET_SBIN" ]; then bst_notok lvm2/sbindir "cannot read global/fsadm_executable back from lvmconfig --type full; the path fold cannot be built and every comparison below would be a comparison of two different paths" rm -rf "$W"; exit 0 fi bst_ok lvm2/sbindir lv_normalise() { # $1 = sysdir to fold; stdin -> stdout sed -e '/WARNING: Running as a non-root user/d' \ -e '/leaked on .* invocation\./d' \ -e "s|$1|@SYSDIR@|g" \ -e "s|$TARGET_SBIN|@SBINDIR@|g" } # --- The comparisons. --------------------------------------------------------- # One case per directory: cmd (argv, one word per line), sysdir (which staged # system directory to use), overlay (whether the reporting overlay is needed), # and the expected out/err/rc. # # The case list is written to a FILE and iterated from there. A `for d in # "$W"/cases/*` glob would put 65 paths on nothing, but the argv ceiling # (__wasm32_args_buf, 8192 bytes -- brintos/hardwarejs#15) is close enough at this # corpus size to be worth not walking towards, and the file form is what the # next package with 500 cases will need anyway. ( cd "$W/cases" && ls ) > "$W/caselist" 2>/dev/null n_cmp=0; n_bad=0 # BST-REDIRECT-OK: $W is under $TMPDIR, written by this suite three lines above. while IFS= read -r name; do [ -n "$name" ] || continue d="$W/cases/$name" [ -f "$d/cmd" ] || continue key=$(cat "$d/sysdir"); ov=$(cat "$d/overlay"); want_rc=$(cat "$d/rc") sd="$W/sys/$key" mkdir -p "$sd/lock" 2>/dev/null # Rebuild the argv, expanding @SYSDIR@ to THIS arm's staged directory. The # oracle expanded it to its own, so no absolute path from either machine ever # reaches the other's command line. set -- while IFS= read -r a; do set -- "$@" "$(printf '%s' "$a" | sed "s|@SYSDIR@|$sd|g")" done < "$d/cmd" if [ "$ov" = 1 ]; then TZ=UTC LC_ALL=C LVM_SYSTEM_DIR="$sd" lvm "$@" \ --config "devices{filter=[\"r|.*|\"] obtain_device_list_from_udev=0 hints=\"none\" use_devicesfile=0} global{use_lvmpolld=0 locking_dir=\"$sd/lock\"}" \ > "$W/o" 2> "$W/e" else TZ=UTC LC_ALL=C LVM_SYSTEM_DIR="$sd" lvm "$@" > "$W/o" 2> "$W/e" fi got_rc=$? lv_normalise "$sd" < "$W/o" > "$W/out" lv_normalise "$sd" < "$W/e" > "$W/err" n_cmp=$((n_cmp + 1)) if [ "$got_rc" != "$want_rc" ]; then n_bad=$((n_bad + 1)) bst_notok "lvm2/$name" "exit status $got_rc, oracle says $want_rc; first output line: $(_bst_1line "$(head -1 "$W/out" "$W/err" 2>/dev/null | tr '\n' ' ')")" elif ! lv_same "$d/out" "$W/out"; then n_bad=$((n_bad + 1)) bst_notok "lvm2/$name" "stdout differs from the x86-64 oracle: $(_bst_1line "$(diff "$d/out" "$W/out" 2>/dev/null | head -4 | tr '\n' ' ')")" elif ! lv_same "$d/err" "$W/err"; then n_bad=$((n_bad + 1)) # ATTRIBUTED RED, NOT A FILTERED ONE. Eleven reporting commands differ from # the oracle by exactly one stderr line and nothing else, and that line is # lvm2 correctly reporting a platform capability this guest lacks. Both # arms below are bst_notok, so nothing here can turn a red green -- what # the split buys is that the reader is told WHICH red it is. Removing the # line instead would make eleven cases pass and delete the regression # signal that flips them green on their own when brintos/linux#85 lands. grep -v '^ *Failed to set up async io, using sync io\.$' "$W/err" > "$W/err.noaio" if lv_same "$d/err" "$W/err.noaio"; then bst_notok "lvm2/$name" "expected red (brintos/linux#85): the ONLY stderr difference from the x86-64 oracle is lvm2's async-io fallback notice. io_setup(2) returns -38 ENOSYS here -- the AIO family (206-210) is absent from the wasm32 syscall table although CONFIG_AIO=y -- so bcache falls back to sync io and says so. stdout, exit status and every other stderr line are byte-identical" else bst_notok "lvm2/$name" "stderr differs from the x86-64 oracle: $(_bst_1line "$(diff "$d/err" "$W/err" 2>/dev/null | head -4 | tr '\n' ' ')")" fi else bst_ok "lvm2/$name" fi done < "$W/caselist" # A corpus that shrank to nothing would report a clean run of zero comparisons, # which is the failure that hides best. if [ "$n_cmp" -ge 40 ]; then bst_ok lvm2/case-count else bst_notok lvm2/case-count "only $n_cmp oracle comparisons ran; the corpus should carry at least 40" fi # --- THE SPLIT: the question the ticket said to check rather than assume. ----- # Re-measured HERE, on the shipped wasm32 binary. `lvm` is a MULTI-CALL binary: # its 59 subcommands are one executable, so "does the package work" is not one # question but 59, and the split is which of them can reach a documented result # with no device-mapper. # # The obvious probe -- run each with no arguments and grep stderr for # /dev/mapper/control -- is wrong in both directions at once, and both were # observed while writing this. With a devices/dir that does not exist it reports # ZERO, because "Ignoring : No such file or directory." is emitted first and # swamps the line that matters. With a plausible sandbox but a non-writable lock # directory it reports THREE while quietly misfiling twelve reporting commands as # device-dependent when they are merely unable to take the global lock. n_cmds=0; n_dm=0; dmlist= ( cd "$W" && lvm help ) 2>&1 | sed -n 's/^ \([a-z][a-z0-9]*\) *[A-Z].*/\1/p' | sort -u > "$W/cmds" # BST-REDIRECT-OK: $W/cmds is written by this suite on the line above, under $TMPDIR. while IFS= read -r c; do [ -n "$c" ] || continue n_cmds=$((n_cmds + 1)) e=$(TZ=UTC LC_ALL=C LVM_SYSTEM_DIR="$W/sys/ref" lvm "$c" \ --config "devices{filter=[\"r|.*|\"] obtain_device_list_from_udev=0 hints=\"none\" use_devicesfile=0} global{use_lvmpolld=0 locking_dir=\"$W/sys/ref/lock\"}" \ 2>&1 >/dev/null &1) case "$o" in *"LVM version:"*) bst_ok lvm2/version-string ;; *) bst_notok lvm2/version-string "lvm version printed no LVM version line: $(_bst_1line "$o")" ;; esac case "$o" in */dev/mapper/control*) bst_notok lvm2/dm-control "expected red (brintos/linux#104): /dev/mapper/control is absent on this kernel, so lvm cannot read the driver version -- $(_bst_1line "$o")" ;; *) bst_ok lvm2/dm-control ;; esac if command -v dmsetup >/dev/null 2>&1; then o=$(dmsetup version 2>&1) case "$o" in *"Library version:"*) bst_ok lvm2/dmsetup-library-version ;; *) bst_notok lvm2/dmsetup-library-version "dmsetup version printed no library version: $(_bst_1line "$o")" ;; esac o=$(dmsetup targets 2>&1); rc=$? if [ "$rc" = 0 ]; then bst_notok lvm2/dmsetup-targets "dmsetup targets succeeded on a kernel with no device-mapper, which cannot be right: $(_bst_1line "$o")" else bst_notok lvm2/dmsetup-targets "expected red (brintos/linux#104): dmsetup targets needs /dev/mapper/control, absent on this kernel -- $(_bst_1line "$o")" fi else bst_notok lvm2/dmsetup-present "dmsetup is not on the image; the libdevmapper package did not install its tool" fi # --- Controls: a comparator with a bug in it passes everything. --------------- # 65 green comparisons look identical whether the comparator works or not, so # three cases prove it can fail. Each MUST report not ok; a green here is the # finding. printf 'x\n' > "$W/c1"; printf 'y\n' > "$W/c2" if lv_same "$W/c1" "$W/c2"; then bst_notok lvm2/control-differs "the comparator called two different files equal, so every comparison above is meaningless" else bst_ok lvm2/control-differs fi printf 'x\n' > "$W/c3"; printf 'x' > "$W/c4" if lv_same "$W/c3" "$W/c4"; then bst_notok lvm2/control-trailing-newline "the comparator ignored a trailing-newline difference; \$(cat) alone would, which is why the byte count is there" else bst_ok lvm2/control-trailing-newline fi # And one that proves the NORMALISER is actually substituting, rather than the two # arms happening to agree because neither path was ever present. n=$(printf '%s\n' "$TARGET_SBIN/fsadm" | lv_normalise "$W/sys/ref") if [ "$n" = "@SBINDIR@/fsadm" ]; then bst_ok lvm2/control-normaliser else bst_notok lvm2/control-normaliser "the sbindir fold did not fire: '$TARGET_SBIN/fsadm' normalised to '$n', not '@SBINDIR@/fsadm'" fi rm -rf "$W"