. "$BST_LIB" # BST-TIMEOUT: 750 # 58 cases -- 40 corpus replays with 57 fw_printenv/fw_setenv # execs between them, plus the deliverable and control cases. # MEASURED at elapsed=145s on hz1 (2026-08-24, # ==BST-SUITE libubootenv elapsed=145s cases=58), declared at # 5x that. The default 60 would trip on the case loop alone, # and the 600 this first carried was only 4x -- too close for a # suite that copies a 288 kB corpus onto a virtiofs root and # then execs 57 times, on a build host it shares. # libubootenv -- fw_printenv/fw_setenv driven against an environment FILE and # compared, in both directions, against a native x86-64 oracle. # # WHAT THIS MACHINE CAN AND CANNOT TEST. These tools exist to read and rewrite a # U-Boot environment held on a flash device, and this machine has no flash. That # would be crippling for most BSP tools and is not here: fw_env.config names the # storage by PATH, and upstream documents a plain file as a legal answer (the VFAT # case). So a reference environment image standing in for the device exercises # everything the tool does except the MTD ioctls -- the config parse, the layout # choice, the crc32 verify, the key=value parse, the sort, the format, the # re-serialise, the crc32 recompute, and the redundant copy selection. The MTD # lock/unlock ioctls are the only arm left uncovered, and they are #ifdef'd behind # a device this port does not claim to support. # # WHY A COMPARISON RATHER THAN ASSERTIONS. Decoding and re-encoding an environment # is arithmetic on bytes with one pread and one pwrite around it, so a correct # wasm32 build must print exactly what a native build of the same version prints # on the same input. Every weaker form fails in a way that matters: "fw_printenv # listed bootcmd" passes on a build that then writes an environment U-Boot will # reject, and a checked-in expected output is a snapshot of one version that rots # into a false red the day poky bumps libubootenv -- and is then re-blessed from # whatever the target printed, which is the comparison inverted. The expectation # is generated at build time by libubootenv-native, the SAME recipe version with # the same two patches, and installed as /usr/libexec/libubootenv-tests. # # AND WHY THE BLOB IS COMPARED, NOT ONLY THE OUTPUT. The read path and the write # path share the parse but not the serialise, and the crc32 is only ever exercised # by the writer. A suite that wrote and then read back would go green on a build # whose crc is wrong the same way in both directions -- which is exactly what a # width or byte-order mistake in `*(uint32_t *)image` looks like. The environment # blob is the only artifact that leaves the process, so it is the one compared. # # THE TWO LAYOUTS. fw_env.config with one device line is the single layout (crc, # then data); with two it is the redundant one (crc, one flag byte, then data), so # the crc covers one byte fewer at a different offset and the writer alternates # copies while incrementing the flag. Both are in the corpus on purpose: the # redundant layout is where the offset and the copy choice can be got wrong, and # the single one is the control that says whether a divergence is about the flag # byte or about the crc in general. # # THE CONTROLS AT THE END ARE NOT DECORATION. A comparator with a bug in it passes # everything, and 40 green comparisons look identical either way. Each control # takes a known-good oracle answer, changes exactly one thing, and requires the # comparison to say so -- and each reports whether the mutation actually applied, # so a control that posed no question is a named failure rather than a quiet pass. bst_ok libubootenv/suite-canary # first line: absorbs brintos/linux#96, which drops it require fw_printenv libubootenv/present T=/usr/libexec/libubootenv-tests if [ ! -f "$T/oracle-info" ] || [ ! -d "$T/cases" ]; then bst_notok libubootenv/corpus "no oracle corpus at $T -- libubootenv-tests is not installed, so every comparison in this suite is unrunnable" exit 0 fi W=${TMPDIR:-/tmp}/bst-libubootenv.$$ if ! mkdir -p "$W" 2>/dev/null; then bst_notok libubootenv/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. # The copy is needed anyway: every case mutates its own env.bin, and the rootfs is # read-only at browser runtime. if ! cp -r "$T/cases" "$W/cases" || ! cp -r "$T/env" "$W/env" || ! cp -r "$T/cfg" "$W/cfg"; then bst_notok libubootenv/stage "cannot copy the corpus from $T 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_envsize=$(sed -n 's/^envsize //p' "$T/oracle-info") # --- The comparator. --------------------------------------------------------- # cmp is byte-exact and is what compares the BINARY blobs, so unlike the text-only # case there is no string fallback for those; the fallback below covers the # transcripts on a stripped image. `$(cat)` drops trailing newlines on both sides, # so the byte count is what closes that hole -- and control-trailing-newline proves # the pair actually does. if command -v cmp >/dev/null 2>&1; then LU_CMP=1; else LU_CMP=; fi lu_same() { # lu_same if [ -n "$LU_CMP" ]; then cmp -s "$1" "$2" else [ "$(cat "$1")" = "$(cat "$2")" ] && [ "$(wc -c < "$1")" = "$(wc -c < "$2")" ] fi } # --- The deliverable itself. ------------------------------------------------- check libubootenv/fw-setenv test -x /usr/bin/fw_setenv check libubootenv/lib-static test -f /usr/lib/libubootenv.a check libubootenv/header test -f /usr/include/libuboot.h check libubootenv/pkgconfig test -f /usr/lib/pkgconfig/libubootenv.pc # The decision this port turns on, asked of the TARGET rather than of the build. # A shared libubootenv cannot link on this arm at all, so one appearing here would # mean the image is carrying something no consumer could have used. bst_lib_shape libubootenv/no-shared "/usr/lib/libubootenv.so*" # fw_setenv is upstream's argv[0] dispatch of the SAME binary. Asserting the two # names differ in behaviour is what proves the dispatch survived packaging: an # image that installed two copies of fw_printenv under both names would pass every # read case in the corpus and silently never write anything. lu_h=$(fw_setenv -h 2>&1) case "$lu_h" in *"Usage fw_setenv"*) bst_ok libubootenv/argv0-dispatch ;; *) bst_notok libubootenv/argv0-dispatch "fw_setenv -h does not identify itself as fw_setenv, so the argv[0] dispatch did not survive install: $(_bst_1line "$lu_h")" ;; esac ver=$(fw_printenv -V 2>&1) case "$ver" in *"$info_pv"*) bst_ok libubootenv/version ;; *) bst_notok libubootenv/version "target fw_printenv is not the oracle's version $info_pv: $(_bst_1line "$ver")" ;; esac # --- The compiled-in default config path. ------------------------------------ # Every case below names its config with -c, because the corpus must (each side # resolves its own scratch directory). That leaves the question the ticket # actually asked -- does the tool find /etc/fw_env.config when not told? -- # unasked by the corpus, so it is asked here, where it is a property worth having. # Same command twice, once letting the binary find its own config and once naming # it: identical output is the only outcome consistent with the compiled-in # DEFAULT_CFG_FILE resolving to the installed file. if [ -f /etc/fw_env.config ]; then bst_ok libubootenv/etc-config fw_printenv > "$W/def.out" 2>&1 fw_printenv -c /etc/fw_env.config > "$W/exp.out" 2>&1 if lu_same "$W/def.out" "$W/exp.out"; then bst_ok libubootenv/default-config-path else bst_notok libubootenv/default-config-path "fw_printenv finds a different config on its own than at /etc/fw_env.config, so the compiled-in DEFAULT_CFG_FILE does not resolve to the installed file -- default: $(_bst_1line "$(head -2 "$W/def.out")") explicit: $(_bst_1line "$(head -2 "$W/exp.out")")" fi # ...and that the pair above is not two equally empty answers. A fw_printenv # that resolved nothing exits non-zero and prints only its complaint, which # satisfies "the two agree" perfectly. case "$(cat "$W/def.out")" in *bootcmd=*) bst_ok libubootenv/default-config-reads ;; *) bst_notok libubootenv/default-config-reads "the default config path produced no environment, so the agreement above is between two failures: $(_bst_1line "$(cat "$W/def.out")")" ;; esac else bst_notok libubootenv/etc-config "no /etc/fw_env.config -- the compiled-in default config path cannot be exercised on a read-only rootfs without one" bst_notok libubootenv/default-config-path "no /etc/fw_env.config" bst_notok libubootenv/default-config-reads "no /etc/fw_env.config" fi # --- The case loop. ---------------------------------------------------------- # The argv is READ FROM THE CORPUS, not re-typed here, and the same `--` # separator rule the oracle applied is applied again -- so a case added to # libubootenv-oracle.sh reaches the target without editing this file, and the two # cannot drift. lu_flush() { lu_k=$((lu_k + 1)) printf '==RUN %d %s==\n' "$lu_k" "$lu_pending" >> "$W/o" ( cd "$W/run" && "$lu_pending" -c fw_env.config "$@" ) >> "$W/o" 2>&1 printf '%s\n' "$?" >> "$W/rcs" } lu_run() { # lu_run ; writes $W/o, $W/rcs and leaves the blob at $W/run/env.bin lu_id=$1 lu_base=$(cat "$W/cases/$lu_id.base") lu_cfg=$(cat "$W/cases/$lu_id.cfg") lu_pending=$(cat "$W/cases/$lu_id.tool") rm -rf "$W/run"; mkdir -p "$W/run" || return 1 cp "$W/cfg/$lu_cfg" "$W/run/fw_env.config" || return 1 cp "$W/env/$lu_base.env" "$W/run/env.bin" || return 1 cp "$W/env/initial.env" "$W/run/initial.env" || return 1 cp "$W/env/set-script.txt" "$W/run/set-script.txt" || return 1 : > "$W/o"; : > "$W/rcs" lu_k=0 set -- while IFS= read -r a; do case "$a" in --fw_printenv|--fw_setenv) lu_flush "$@" lu_pending=${a#--} set -- ;; *) set -- "$@" "$a" ;; esac # The loop must stay here rather than move into a helper: it builds the # positional parameters lu_flush then runs. $W is the $TMPDIR copy staged at # the top of this file for exactly this reason. # BST-REDIRECT-OK: reads the $TMPDIR corpus copy, never the rootfs done < "$W/cases/$lu_id.argv" lu_flush "$@" return 0 } ( 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)) if ! lu_run "$id"; then bst_notok "libubootenv/$id" "could not stage the case working directory" continue fi diff= lu_same "$W/cases/$id.out" "$W/o" || diff=output lu_same "$W/cases/$id.rc" "$W/rcs" || diff="${diff:+$diff,}rc(oracle $(tr '\n' ' ' < "$W/cases/$id.rc")target $(tr '\n' ' ' < "$W/rcs"))" lu_same "$W/cases/$id.blob" "$W/run/env.bin" || diff="${diff:+$diff,}blob" if [ -z "$diff" ]; then bst_ok "libubootenv/$id" else bst_notok "libubootenv/$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 libubootenv/case-count "$info_cases" "$n_all" # --- Controls on the comparator itself. -------------------------------------- ctl=$W/cases/s-all.out blobctl=$W/cases/s-set-new.blob if [ -f "$ctl" ] && [ -f "$blobctl" ]; then cp "$ctl" "$W/ctl.orig"; cp "$blobctl" "$W/ctlb.orig" # Whether a mutation APPLIED is asked with grep, never with the comparator the # control exists to test. Asking lu_same both questions makes them one # question: a mutation that changed nothing leaves the files identical, # lu_same says "same", and the control reports a blind comparator when the # truth is that it posed no question at all. # 1. One character, in the middle of a variable VALUE. if ! grep -q 'distro_bootcmd' "$W/ctl.orig"; then bst_notok libubootenv/control-one-char "the reference transcript holds no 'distro_bootcmd' to change, so this control posed no question -- which itself means the fixture is not the one the oracle wrote" else sed 's/distro_bootcmd/distro_bootcmX/' "$W/ctl.orig" > "$W/ctl.1" if lu_same "$W/ctl.orig" "$W/ctl.1"; then bst_notok libubootenv/control-one-char "the comparator cannot see a changed variable value" else bst_ok libubootenv/control-one-char fi fi # 2. The TRAILING NEWLINE alone -- the one the `$(cat)` fallback comparator is # structurally blind to, which is why lu_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.2" if lu_same "$W/ctl.orig" "$W/ctl.2"; then bst_notok libubootenv/control-trailing-newline "the comparator cannot see a missing trailing newline" else bst_ok libubootenv/control-trailing-newline fi # 3. A DROPPED LINE. A transcript truncated in transport would otherwise # compare equal over the part that arrived. sed '2d' "$W/ctl.orig" > "$W/ctl.3" if lu_same "$W/ctl.orig" "$W/ctl.3"; then bst_notok libubootenv/control-dropped-line "the comparator cannot see a dropped line" else bst_ok libubootenv/control-dropped-line fi # 4. ONE BYTE of an expected BLOB, deep in the zero tail the crc covers. This # is the control for the half of the comparison the three text mutations # above cannot reach, and it is the half this port turns on: without it, # "the blob matched" is an unexamined claim about a binary file. dd if="$W/ctlb.orig" of="$W/ctlb.1" bs=1 count=3000 2>/dev/null printf 'Z' >> "$W/ctlb.1" dd if="$W/ctlb.orig" of="$W/ctlb.1" bs=1 skip=3001 seek=3001 conv=notrunc 2>/dev/null if [ "$(wc -c < "$W/ctlb.1")" != "$(wc -c < "$W/ctlb.orig")" ]; then bst_notok libubootenv/control-one-blob-byte "the mutated blob is a different LENGTH from the original, so this control is testing truncation rather than the single byte it means to" elif lu_same "$W/ctlb.orig" "$W/ctlb.1"; then bst_notok libubootenv/control-one-blob-byte "the comparator cannot see one changed byte in the environment image, so every blob comparison above is vacuous" else bst_ok libubootenv/control-one-blob-byte fi else bst_notok libubootenv/control-one-char "no $ctl to mutate" bst_notok libubootenv/control-trailing-newline "no $ctl to mutate" bst_notok libubootenv/control-dropped-line "no $ctl to mutate" bst_notok libubootenv/control-one-blob-byte "no $blobctl to mutate" fi # 5. The control that establishes what a MATCH means. Every case would compare # equal if fw_printenv printed nothing on every input, so one input is posed # that must produce a non-empty, specific answer. case "$(cat "$W/cases/s-all.out")" in *"bootcmd=run distro_bootcmd"*) bst_ok libubootenv/control-nonempty ;; *) bst_notok libubootenv/control-nonempty "the reference transcript for the full dump does not contain the environment, so a matching case no longer proves anything was read: $(_bst_1line "$(cat "$W/cases/s-all.out")")" ;; esac # 6. ...and that a corrupt crc really is REJECTED on this target, stated once in # its own right rather than only as one of the forty comparisons. This is the # question the ticket named: a tool that answered from the damaged bytes, or # that silently returned defaults without saying so, would be a worse failure # than one that crashed. rm -rf "$W/run"; mkdir -p "$W/run" cp "$W/cfg/single.cfg" "$W/run/fw_env.config" cp "$W/env/single-badcrc.env" "$W/run/env.bin" bad=$( cd "$W/run" && fw_printenv -c fw_env.config -f absent.env 2>&1 ) badrc=$? case "$bad" in *bootcmd=*) bst_notok libubootenv/crc-enforced "a damaged environment still answered bootcmd, so the stored crc is not verified on this build and the corpus's five damaged cases are vacuous: $(_bst_1line "$bad")" ;; *"Cannot read environment"*) if [ "$badrc" -eq 0 ]; then bst_notok libubootenv/crc-enforced "the damaged environment was reported unreadable but fw_printenv still exited 0, so a caller has no way to tell" else bst_ok libubootenv/crc-enforced fi ;; *) bst_notok libubootenv/crc-enforced "a damaged environment produced neither the variables nor a complaint (rc=$badrc): $(_bst_1line "$bad")" ;; esac rm -rf "$W"