qemu and build: review fixes across the rig driver, boot script, and fetch helpers

Bounded waits and validated arguments in run.sh and ui-drive.sh, a seeded
settings directory and root-only staged rootfs permissions with their own
tests, qmp.py and imgtools.py hardening, the fetch scripts checking what they
download, and ASCII typography throughout. Each fix carries its test under
qemu/tests or tests/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
This commit is contained in:
Noah
2026-09-09 19:17:54 -06:00
co-authored by Claude Fable 5.1
parent bda6c6c633
commit 2b6e8a2098
24 changed files with 1823 additions and 137 deletions
+304
View File
@@ -0,0 +1,304 @@
#!/usr/bin/env bash
# Regression and coverage tests for build/fetch-vendor.sh.
#
# The script had no test of any kind before this: not the --check/--fetch
# state machine (MISSING/OK/DRIFTED, the lvgl/luckfox-pico path mapping, the
# "locally modified" annotation), not --help, not the clone stall guard. Two
# concrete regressions motivate the first two cases:
#
# - --help sliced its own source with a hardcoded line range that stopped
# one line too late, so it printed "set -uo pipefail" -- the first line
# of code -- as the last line of help text.
# - git clone ran with no bound on a stalled transfer: a dead peer or a
# wedged proxy mid-clone (the luckfox-pico tree alone is ~21 GB) hung the
# script forever with no way for a caller to tell "still working" from
# "wedged". The fix sets GIT_HTTP_LOW_SPEED_LIMIT/TIME so a stalled
# transfer aborts while a merely slow one is left alone.
#
# --help runs the shipping script directly. The --fetch/--check cases run a
# copy of it in a private fixture dir (its manifest lookup is relative to
# itself, so a copy is how its own HERE-relative resolution can be pointed at
# a manifest we control) against local, throwaway origin repos -- no network,
# and the real git binary does the work throughout (a thin logging wrapper
# only intercepts "clone" to record the env it saw, then execs straight
# through), so these exercise the shipping script's actual git calls.
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="$HERE/../../build/fetch-vendor.sh"
[ -f "$SCRIPT" ] || { echo "FAIL: script not found at $SCRIPT"; exit 1; }
FAIL=0
ok() { printf '[PASS] %s\n' "$1"; }
bad() { printf '[FAIL] %s\n' "$1"; FAIL=1; }
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
GITC() { git -c user.email=t@t.invalid -c user.name=t "$@"; }
# mkrepo1 <dir> -> one commit on main, prints its hash
mkrepo1() {
local dir="$1"
git init -q -b main "$dir"
printf 'v1\n' > "$dir/file.txt"
git -C "$dir" add file.txt
GITC -C "$dir" commit -q -m v1
git -C "$dir" rev-parse HEAD
}
# mkrepo2 <dir> -> two commits on main, prints "C1 C2" (C1 older)
mkrepo2() {
local dir="$1" c1 c2
git init -q -b main "$dir"
printf 'v1\n' > "$dir/file.txt"
git -C "$dir" add file.txt
GITC -C "$dir" commit -q -m v1
c1="$(git -C "$dir" rev-parse HEAD)"
printf 'v2\n' > "$dir/file.txt"
git -C "$dir" add file.txt
GITC -C "$dir" commit -q -m v2
c2="$(git -C "$dir" rev-parse HEAD)"
printf '%s %s\n' "$c1" "$c2"
}
# ---------------------------------------------------------------------------
# Regression: --help must not spill into the script's own code.
# ---------------------------------------------------------------------------
HELP_OUT="$(bash "$SCRIPT" --help)"
if printf '%s\n' "$HELP_OUT" | grep -q 'set -uo pipefail'; then
bad "--help does not print the script's own code"
else
ok "--help does not print the script's own code"
fi
LAST_LINE="$(printf '%s\n' "$HELP_OUT" | tail -1)"
if [ "$LAST_LINE" = "# someone's debugging session, not something to silently throw away." ]; then
ok "--help ends on the last comment line, not past it"
else
bad "--help ends on the last comment line, not past it (got: $LAST_LINE)"
fi
# ---------------------------------------------------------------------------
# Regression: the clone stall guard reaches git clone's environment.
# ---------------------------------------------------------------------------
REALGIT="$(command -v git)"
FAKEBIN="$WORK/fakebin"
mkdir -p "$FAKEBIN"
CAPTURE="$WORK/clone-env.txt"
cat > "$FAKEBIN/git" <<FAKEGIT
#!/bin/sh
if [ "\$1" = "clone" ]; then
printf '%s %s\n' "\$GIT_HTTP_LOW_SPEED_LIMIT" "\$GIT_HTTP_LOW_SPEED_TIME" >> "$CAPTURE"
fi
exec "$REALGIT" "\$@"
FAKEGIT
chmod +x "$FAKEBIN/git"
ORIGIN_GUARD="$WORK/origin-guard"
GUARD_C="$(mkrepo1 "$ORIGIN_GUARD")"
MANIFEST_GUARD="$WORK/manifest-guard"
printf 'widget\t%s\t%s\tguard test tree\n' "$ORIGIN_GUARD" "$GUARD_C" > "$MANIFEST_GUARD"
# fetch-vendor.sh finds its manifest next to itself, so give it a private
# fixture dir carrying a copy of the real script alongside our manifest.
GUARD_FIXTURE="$WORK/guard-fixture"
mkdir -p "$GUARD_FIXTURE"
cp "$SCRIPT" "$GUARD_FIXTURE/fetch-vendor.sh"
cp "$MANIFEST_GUARD" "$GUARD_FIXTURE/vendor.manifest"
: > "$CAPTURE"
PATH="$FAKEBIN:$PATH" bash "$GUARD_FIXTURE/fetch-vendor.sh" --fetch "$WORK/vendor-default" \
> "$WORK/guard-default.log" 2>&1
if [ "$(cat "$CAPTURE")" = "1000 60" ]; then
ok "clone runs with the default low-speed guard (1000 bytes/sec, 60s)"
else
bad "clone runs with the default low-speed guard (got: $(cat "$CAPTURE" 2>/dev/null))"
fi
: > "$CAPTURE"
PATH="$FAKEBIN:$PATH" WARDEN_VENDOR_LOW_SPEED_LIMIT=5 WARDEN_VENDOR_LOW_SPEED_TIME=9 \
bash "$GUARD_FIXTURE/fetch-vendor.sh" --fetch "$WORK/vendor-override" \
> "$WORK/guard-override.log" 2>&1
if [ "$(cat "$CAPTURE")" = "5 9" ]; then
ok "the low-speed guard is overridable"
else
bad "the low-speed guard is overridable (got: $(cat "$CAPTURE" 2>/dev/null))"
fi
# ---------------------------------------------------------------------------
# Coverage: the --check/--fetch state machine and the name-to-path mapping.
# ---------------------------------------------------------------------------
ORIGIN_WIDGET="$WORK/origin-widget"
read -r WIDGET_C1 WIDGET_C2 <<< "$(mkrepo2 "$ORIGIN_WIDGET")"
ORIGIN_LVGL="$WORK/origin-lvgl"
LVGL_C="$(mkrepo1 "$ORIGIN_LVGL")"
ORIGIN_SDK="$WORK/origin-sdk"
SDK_C="$(mkrepo1 "$ORIGIN_SDK")"
MANIFEST="$WORK/vendor.manifest"
write_manifest() { # write_manifest <widget-commit>
{
printf 'widget\t%s\t%s\tgeneric tree, default path mapping\n' "$ORIGIN_WIDGET" "$1"
printf 'lvgl\t%s\t%s\tlvgl name maps under ui/lvgl\n' "$ORIGIN_LVGL" "$LVGL_C"
printf 'luckfox-pico\t%s\t%s\tluckfox-pico name maps under sdk\n' "$ORIGIN_SDK" "$SDK_C"
} > "$MANIFEST"
}
STATE_FIXTURE="$WORK/state-fixture"
mkdir -p "$STATE_FIXTURE"
cp "$SCRIPT" "$STATE_FIXTURE/fetch-vendor.sh"
VENDOR_DIR="$WORK/vendor"
write_manifest "$WIDGET_C1"
cp "$MANIFEST" "$STATE_FIXTURE/vendor.manifest"
# Case 1: nothing cloned yet -> --check reports MISSING for all three, rc=1.
OUT="$(bash "$STATE_FIXTURE/fetch-vendor.sh" --check "$VENDOR_DIR")"; RC=$?
if [ "$RC" -ne 0 ] \
&& printf '%s\n' "$OUT" | grep -q '^MISSING widget' \
&& printf '%s\n' "$OUT" | grep -q '^MISSING lvgl' \
&& printf '%s\n' "$OUT" | grep -q '^MISSING luckfox-pico'; then
ok "--check reports MISSING and rc=1 when nothing is cloned"
else
bad "--check reports MISSING and rc=1 when nothing is cloned (rc=$RC)"
fi
# Case 2: --fetch clones each tree under its mapped path and checks out the pin.
OUT="$(bash "$STATE_FIXTURE/fetch-vendor.sh" --fetch "$VENDOR_DIR")"; RC=$?
if [ "$RC" -eq 0 ] \
&& [ -e "$VENDOR_DIR/widget/.git" ] \
&& [ -e "$VENDOR_DIR/ui/lvgl/.git" ] \
&& [ -e "$VENDOR_DIR/sdk/.git" ]; then
ok "--fetch clones lvgl under ui/lvgl and luckfox-pico under sdk"
else
bad "--fetch clones lvgl under ui/lvgl and luckfox-pico under sdk (rc=$RC)"
fi
if [ "$(git -C "$VENDOR_DIR/widget" rev-parse HEAD 2>/dev/null)" = "$WIDGET_C1" ]; then
ok "--fetch checks out the manifest-pinned commit"
else
bad "--fetch checks out the manifest-pinned commit"
fi
# Case 3: a checkout sitting at its pin --check's clean, rc=0.
OUT="$(bash "$STATE_FIXTURE/fetch-vendor.sh" --check "$VENDOR_DIR")"; RC=$?
if [ "$RC" -eq 0 ] && printf '%s\n' "$OUT" | grep -q "^OK widget ${WIDGET_C1:0:12}$"; then
ok "--check reports OK with no suffix for a clean checkout at the pin"
else
bad "--check reports OK with no suffix for a clean checkout at the pin"
fi
# Case 4: manifest moves to a commit the checkout is not on -> DRIFTED,
# rc=1, and the checkout itself is left untouched (never reset).
write_manifest "$WIDGET_C2"
cp "$MANIFEST" "$STATE_FIXTURE/vendor.manifest"
OUT="$(bash "$STATE_FIXTURE/fetch-vendor.sh" --check "$VENDOR_DIR")"; RC=$?
if [ "$RC" -ne 0 ] \
&& printf '%s\n' "$OUT" | grep -q "^DRIFTED widget want ${WIDGET_C2:0:12} have ${WIDGET_C1:0:12}"; then
ok "--check reports DRIFTED when HEAD does not match the pin"
else
bad "--check reports DRIFTED when HEAD does not match the pin"
fi
if [ "$(git -C "$VENDOR_DIR/widget" rev-parse HEAD 2>/dev/null)" = "$WIDGET_C1" ]; then
ok "a drifted checkout is reported, never reset"
else
bad "a drifted checkout is reported, never reset"
fi
# Case 5: back at the pin but with an uncommitted local change -> OK, but
# annotated, and still rc=0 (a dirty vendor tree is expected, not a failure).
write_manifest "$WIDGET_C1"
cp "$MANIFEST" "$STATE_FIXTURE/vendor.manifest"
echo "local debugging change" >> "$VENDOR_DIR/widget/file.txt"
OUT="$(bash "$STATE_FIXTURE/fetch-vendor.sh" --check "$VENDOR_DIR")"; RC=$?
if [ "$RC" -eq 0 ] && printf '%s\n' "$OUT" | grep -q "^OK widget ${WIDGET_C1:0:12} (locally modified)$"; then
ok "--check reports OK (locally modified) for a dirty checkout at the pin, rc=0"
else
bad "--check reports OK (locally modified) for a dirty checkout at the pin, rc=0"
fi
# ---------------------------------------------------------------------------
# Case 6: git clone fails (bad origin) -> reported, rc=1, no directory left
# behind for that tree, and -- the actual regression this guards -- the loop
# still reaches the remaining manifest entries and reports exactly one
# failure line for widget, not a second "FAILED to check out" once the
# clone's own continue has fired.
# ---------------------------------------------------------------------------
ORIGIN_BAD="$WORK/no-such-origin"
CLONEFAIL_FIXTURE="$WORK/clonefail-fixture"
mkdir -p "$CLONEFAIL_FIXTURE"
cp "$SCRIPT" "$CLONEFAIL_FIXTURE/fetch-vendor.sh"
{
printf 'widget\t%s\t%s\tbad origin, clone must fail\n' "$ORIGIN_BAD" "$WIDGET_C1"
printf 'lvgl\t%s\t%s\tlvgl name maps under ui/lvgl\n' "$ORIGIN_LVGL" "$LVGL_C"
printf 'luckfox-pico\t%s\t%s\tluckfox-pico name maps under sdk\n' "$ORIGIN_SDK" "$SDK_C"
} > "$CLONEFAIL_FIXTURE/vendor.manifest"
CLONEFAIL_DIR="$WORK/vendor-clonefail"
OUT="$(bash "$CLONEFAIL_FIXTURE/fetch-vendor.sh" --fetch "$CLONEFAIL_DIR" 2>&1)"; RC=$?
FAILED_COUNT="$(printf '%s\n' "$OUT" | grep -c 'FAILED to clone widget')"
if [ "$RC" -ne 0 ] && [ "$FAILED_COUNT" -eq 1 ] \
&& ! printf '%s\n' "$OUT" | grep -q 'FAILED to check out'; then
ok "--fetch reports FAILED to clone once and rc=1 for a bad origin"
else
bad "--fetch reports FAILED to clone once and rc=1 for a bad origin (rc=$RC, count=$FAILED_COUNT)"
fi
if [ ! -e "$CLONEFAIL_DIR/widget/.git" ]; then
ok "a failed clone leaves no checkout behind for that tree"
else
bad "a failed clone leaves no checkout behind for that tree"
fi
if [ -e "$CLONEFAIL_DIR/ui/lvgl/.git" ] && [ -e "$CLONEFAIL_DIR/sdk/.git" ]; then
ok "a clone failure on one tree does not stop the remaining trees from being fetched"
else
bad "a clone failure on one tree does not stop the remaining trees from being fetched"
fi
# ---------------------------------------------------------------------------
# Case 7: git checkout fails (pinned commit missing from the origin) ->
# reported, rc=1, the remaining trees still get fetched, and the checkout is
# left wherever the failed checkout left it -- a later --check must report
# that as DRIFTED, never mistake it for success.
# ---------------------------------------------------------------------------
BOGUS_COMMIT="deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
CHECKOUTFAIL_FIXTURE="$WORK/checkoutfail-fixture"
mkdir -p "$CHECKOUTFAIL_FIXTURE"
cp "$SCRIPT" "$CHECKOUTFAIL_FIXTURE/fetch-vendor.sh"
{
printf 'widget\t%s\t%s\tcommit missing from the origin, checkout must fail\n' "$ORIGIN_WIDGET" "$BOGUS_COMMIT"
printf 'lvgl\t%s\t%s\tlvgl name maps under ui/lvgl\n' "$ORIGIN_LVGL" "$LVGL_C"
printf 'luckfox-pico\t%s\t%s\tluckfox-pico name maps under sdk\n' "$ORIGIN_SDK" "$SDK_C"
} > "$CHECKOUTFAIL_FIXTURE/vendor.manifest"
CHECKOUTFAIL_DIR="$WORK/vendor-checkoutfail"
OUT="$(bash "$CHECKOUTFAIL_FIXTURE/fetch-vendor.sh" --fetch "$CHECKOUTFAIL_DIR" 2>&1)"; RC=$?
if [ "$RC" -ne 0 ] && printf '%s\n' "$OUT" | grep -q "FAILED to check out $BOGUS_COMMIT"; then
ok "--fetch reports FAILED to check out for a commit missing from the origin"
else
bad "--fetch reports FAILED to check out for a commit missing from the origin (rc=$RC)"
fi
# The regression this guards: if the checkout failure's own "continue" were
# ever dropped, the same loop iteration falls through into the have-vs-pin
# comparison below and prints a bogus DRIFTED/OK line for widget in this same
# --fetch run, on top of the FAILED line above.
if ! printf '%s\n' "$OUT" | grep -qE '^(DRIFTED {2}|OK {7})widget'; then
ok "a checkout failure does not fall through to a DRIFTED/OK line in the same run"
else
bad "a checkout failure does not fall through to a DRIFTED/OK line in the same run"
fi
if [ -e "$CHECKOUTFAIL_DIR/widget/.git" ] \
&& [ -e "$CHECKOUTFAIL_DIR/ui/lvgl/.git" ] && [ -e "$CHECKOUTFAIL_DIR/sdk/.git" ]; then
ok "a checkout failure on one tree does not stop the remaining trees from being fetched"
else
bad "a checkout failure on one tree does not stop the remaining trees from being fetched"
fi
OUT="$(bash "$CHECKOUTFAIL_FIXTURE/fetch-vendor.sh" --check "$CHECKOUTFAIL_DIR" 2>&1)"; RC=$?
if [ "$RC" -ne 0 ] && printf '%s\n' "$OUT" | grep -q "^DRIFTED widget want ${BOGUS_COMMIT:0:12}"; then
ok "a later --check reports the failed checkout as DRIFTED, never as success"
else
bad "a later --check reports the failed checkout as DRIFTED, never as success"
fi
[ "$FAIL" -eq 0 ] && echo "All fetch-vendor tests passed." || echo "Some fetch-vendor tests failed."
exit "$FAIL"