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
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# Regression tests for build/fetch-buildroot-tarball.sh's retry/verify path.
#
# The script had never been exercised by anything (nothing calls it yet --
# see its own header) or by a test, so its retry-on-mismatch, cleanup, and
# already-verified short-circuit had never actually run. It also fetched with
# no --connect-timeout/--max-time, so a connection that opens and then stalls
# (a blackholed route, a hung proxy) would block forever instead of retrying;
# issue tracked separately.
#
# Runs the real script (copied into a fixture dir so its own HERE-relative pin
# lookup finds a pin we control) with a fake curl first on PATH, so no network
# is used and the outcome of each attempt is exact.
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="$HERE/../../build/fetch-buildroot-tarball.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; }
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
WANT_SHA="$(printf 'good-bytes' | sha256sum | awk '{print $1}')"
# A private copy of the real script plus a pin file we control, in its own
# directory: the script resolves its pin relative to itself, so this is the
# shipping file under test, not a reimplementation of it.
setup() { # setup <case-name> -> prints the fixture dir
local dir="$TMP/$1"
mkdir -p "$dir/bin"
cp "$SCRIPT" "$dir/fetch-buildroot-tarball.sh"
printf '%s buildroot-2025.02.8.tar.xz\n' "$WANT_SHA" \
> "$dir/buildroot-2025.02.8.tar.xz.sha256"
printf '%s' "$dir"
}
run() { # run <dir> <dest> -> stdout+stderr on stdout, $? in $RC
local dir="$1" dest="$2"
OUT="$(PATH="$dir/bin:$PATH" bash "$dir/fetch-buildroot-tarball.sh" "$dest" 2>&1)"
RC=$?
}
# --- case A: destination already matches the pin -> curl never runs -------
dir="$(setup case-a)"
printf 'good-bytes' > "$dir/dest.tar.xz"
cat > "$dir/bin/curl" <<'FAKE'
#!/bin/sh
echo "curl should not have run" >&2
exit 1
FAKE
chmod +x "$dir/bin/curl"
run "$dir" "$dir/dest.tar.xz"
if [ "$RC" -eq 0 ] && printf '%s' "$OUT" | grep -q 'already present and verified'; then
ok "already-verified destination: curl never invoked"
else
bad "already-verified destination: curl never invoked (rc=$RC, out=$OUT)"
fi
# --- case B: fails once, succeeds on retry -> verified, no extra attempts -
dir="$(setup case-b)"
cat > "$dir/bin/curl" <<FAKE
#!/bin/sh
cnt_file="$dir/curl-calls"
n=0
[ -f "\$cnt_file" ] && n=\$(cat "\$cnt_file")
n=\$((n + 1))
echo "\$n" > "\$cnt_file"
out="" prev=""
for a in "\$@"; do
[ "\$prev" = "-o" ] && out="\$a"
prev="\$a"
done
if [ "\$n" -lt 2 ]; then printf 'bad-bytes' > "\$out"
else printf 'good-bytes' > "\$out"
fi
FAKE
chmod +x "$dir/bin/curl"
run "$dir" "$dir/dest.tar.xz"
calls="$(cat "$dir/curl-calls" 2>/dev/null || echo 0)"
if [ "$RC" -eq 0 ] && [ "$calls" -eq 2 ] && printf '%s' "$OUT" | grep -q 'sha256 verified'; then
ok "mismatch then match: verified on attempt 2, stops retrying"
else
bad "mismatch then match: verified on attempt 2, stops retrying (rc=$RC calls=$calls out=$OUT)"
fi
# --- case C: every attempt mismatches -> fails closed, no partial file left
dir="$(setup case-c)"
cat > "$dir/bin/curl" <<'FAKE'
#!/bin/sh
out="" prev=""
for a in "$@"; do
[ "$prev" = "-o" ] && out="$a"
prev="$a"
done
printf 'always-bad' > "$out"
FAKE
chmod +x "$dir/bin/curl"
run "$dir" "$dir/dest.tar.xz"
if [ "$RC" -ne 0 ] && printf '%s' "$OUT" | grep -q 'FATAL: could not fetch'; then
ok "persistent mismatch: exits nonzero with FATAL"
else
bad "persistent mismatch: exits nonzero with FATAL (rc=$RC, out=$OUT)"
fi
if [ ! -e "$dir/dest.tar.xz" ]; then
ok "persistent mismatch: no partial/corrupt tarball left at the destination"
else
bad "persistent mismatch: no partial/corrupt tarball left at the destination"
fi
attempts="$(printf '%s' "$OUT" | grep -c '== fetching')"
if [ "$attempts" -eq 3 ]; then
ok "persistent mismatch: exactly 3 attempts"
else
bad "persistent mismatch: exactly 3 attempts (got $attempts)"
fi
# --- guard: the fetch carries a connect and overall timeout ---------------
# Without these, --retry never fires (it only re-attempts a transfer curl has
# already decided failed) and a connection that opens then stalls blocks
# forever -- exactly the routing failure this workspace sees from some hosts.
if grep -q -- '--connect-timeout' "$SCRIPT" && grep -q -- '--max-time' "$SCRIPT"; then
ok "fetch carries --connect-timeout and --max-time"
else
bad "fetch carries --connect-timeout and --max-time"
fi
if [ "$FAIL" -eq 0 ]; then
echo "ALL FETCH-BUILDROOT-TARBALL TESTS PASSED"
else
echo "FETCH-BUILDROOT-TARBALL TESTS FAILED"
fi
exit "$FAIL"
+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"
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
# Regression tests for the post-build boot.img validation in
# build/mk-bootimg.sh: the FIT metadata %512 check, the per-image
# data-position %512 check (and fdtget's own failure path, issue #22 --
# a missing or erroring fdtget used to be swallowed and treated as nothing
# to check), and the >=4096 embedded-data-FIT check.
#
# Runs the real script end to end with stub mkimage/resource_tool/fdtget so
# the arithmetic is exercised as it actually ships, not copied out and
# re-tested in isolation. The stub mkimage answers the -B capability probe
# and, for the real build invocation, writes a synthetic boot.img whose
# 4-byte big-endian metadata-size word and total length are test-controlled --
# the same field mk-bootimg.sh reads with `od -An -tu4 -j4 -N4 --endian=big`.
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="$HERE/../../build/mk-bootimg.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
STUBS="$TMP/stubs"
mkdir -p "$STUBS"
cat > "$STUBS/mkimage" <<'EOF'
#!/bin/bash
if [ "$#" -eq 0 ]; then
echo "Usage: mkimage [-T type] -l image" >&2
echo " -B => align size in hex for FIT structure and header" >&2
exit 1
fi
out=""
for a in "$@"; do out="$a"; done
python3 -c '
import sys
out, meta, total = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
total = max(total, 8)
data = bytearray(total)
data[4:8] = (meta & 0xffffffff).to_bytes(4, "big")
open(out, "wb").write(bytes(data))
' "$out" "${MKBI_META:-512}" "${MKBI_TOTAL:-8192}"
EOF
chmod +x "$STUBS/mkimage"
cat > "$STUBS/resource_tool" <<'EOF'
#!/bin/bash
img=""
for a in "$@"; do
case "$a" in --image=*) img="${a#--image=}" ;; esac
done
echo stub > "$img"
EOF
chmod +x "$STUBS/resource_tool"
cat > "$STUBS/fdtget" <<'EOF'
#!/bin/bash
# Invoked as: fdtget -t u FILE /images/NAME data-position
node="$4"
name="${node#/images/}"
var="FDTGET_$(printf '%s' "$name" | tr '[:lower:]' '[:upper:]')_POS"
val="${!var-}"
if [ -z "$val" ] || [ "$val" = "FAIL" ]; then
echo "fdtget: FDT_ERR_NOTFOUND, $node data-position" >&2
exit 1
fi
echo "$val"
EOF
chmod +x "$STUBS/fdtget"
KERNEL="$TMP/zImage"; DTB="$TMP/rv1106-warden.dtb"
head -c 4096 /dev/urandom > "$KERNEL"
head -c 512 /dev/urandom > "$DTB"
fails=0
run_case() { # run_case <name> <want_rc:ok|fail> <want_grep>
local name="$1" want_rc="$2" want_grep="$3" out rc
out="$(PATH="$STUBS:$PATH" "$SCRIPT" --kernel "$KERNEL" --dtb "$DTB" \
--resource-tool "$STUBS/resource_tool" --out "$TMP/boot.img" 2>&1)"
rc=$?
if [ "$want_rc" = ok ] && [ "$rc" -ne 0 ]; then
echo "FAIL: $name (expected success, got rc=$rc: $out)"; fails=$((fails + 1)); return
fi
if [ "$want_rc" = fail ] && [ "$rc" -eq 0 ]; then
echo "FAIL: $name (expected failure, script exited 0: $out)"; fails=$((fails + 1)); return
fi
if ! printf '%s' "$out" | grep -qF -- "$want_grep"; then
echo "FAIL: $name (output missing '$want_grep'): $out"; fails=$((fails + 1)); return
fi
echo "PASS: $name"
}
# A well-formed image: metadata 512-aligned and small, every data-position
# 512-aligned. The baseline every failure case below is a single change from.
MKBI_META=512 MKBI_TOTAL=8192 \
FDTGET_FDT_POS=512 FDTGET_KERNEL_POS=1024 FDTGET_RESOURCE_POS=1536 \
run_case "well-formed image accepted" ok "FIT metadata 512 bytes"
# Metadata size itself not a multiple of 512 (the exact value measured from
# the SDK's vendored mkimage 2017.09, see the comment above the probe).
MKBI_META=1064 MKBI_TOTAL=8192 \
FDTGET_FDT_POS=512 FDTGET_KERNEL_POS=1024 FDTGET_RESOURCE_POS=1536 \
run_case "unaligned metadata size rejected" fail "not a multiple of 512"
# One sub-image's data-position not a multiple of 512; the FATAL must name it.
MKBI_META=512 MKBI_TOTAL=8192 \
FDTGET_FDT_POS=512 FDTGET_KERNEL_POS=148 FDTGET_RESOURCE_POS=1536 \
run_case "unaligned data-position rejected" fail "/images/kernel data-position 148 is not 512-aligned"
# fdtget itself fails (not on PATH, or the FIT it just built is malformed).
# Regression for issue #22: this used to be swallowed by `|| true` and
# treated as "nothing to check" instead of a build failure.
MKBI_META=512 MKBI_TOTAL=8192 \
FDTGET_FDT_POS=512 FDTGET_KERNEL_POS=FAIL FDTGET_RESOURCE_POS=1536 \
run_case "fdtget failure fails the build, not skips the check" fail \
"fdtget could not read /images/kernel data-position"
# Metadata swelled to >=4096 bytes: an embedded-data FIT, which this U-Boot
# rejects outright. 4096 is itself a multiple of 512 so this must be caught
# by the second check, not mistaken for the first.
MKBI_META=4096 MKBI_TOTAL=8192 \
FDTGET_FDT_POS=512 FDTGET_KERNEL_POS=1024 FDTGET_RESOURCE_POS=1536 \
run_case "embedded-data FIT rejected" fail "embedded-data FIT"
[ "$fails" -eq 0 ] && echo "All mk-bootimg boot.img validation tests passed." || echo "$fails test(s) failed."
exit "$fails"
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
# Regression test: every offline regression-test script this job ships must
# actually be invoked by the qemu-tools CI job, not just committed.
#
# run-probe-tests.sh (guards issue #17) sat in the tree unwired into
# .github/workflows/ci.yml: it passed by hand but ran nowhere in CI, so a
# regression in the mkimage probe would only have surfaced on the next
# workflow_dispatch kernel build, not on every push/PR. The same gap later
# reopened for six more scripts written the same way, so this now checks
# every one of them (including itself) instead of only the first: it
# isolates the qemu-tools job and checks each script's basename appears in
# its steps.
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
CI_YML="$HERE/../../.github/workflows/ci.yml"
[ -f "$CI_YML" ] || { echo "FAIL: no workflow file at $CI_YML"; exit 1; }
# Isolate the qemu-tools job: from its own header line up to (but not
# including) the next job at the same two-space indent.
job="$(awk '
/^ qemu-tools:/ { inside = 1; print; next }
inside && /^ [A-Za-z0-9_-]+:/ { exit }
inside { print }
' "$CI_YML")"
[ -n "$job" ] || { echo "FAIL: qemu-tools job not found in $CI_YML"; exit 1; }
# Every offline regression-test script the qemu-tools job owns. Add new
# scripts here when they are written, not only when someone remembers to
# wire them in -- that is the failure this test exists to catch.
scripts=(
"tests/mk-bootimg/run-probe-tests.sh"
"qemu/tests/run-sh-args-test.sh"
"qemu/tests/seed-dir.sh"
"qemu/tests/stage-rootfs-perms.sh"
"tests/fetch-vendor/run-fetch-vendor-tests.sh"
"tests/fetch-buildroot-tarball/run-fetch-buildroot-tarball-tests.sh"
"tests/mk-bootimg/run-boot-img-validate-tests.sh"
"tests/mk-bootimg/run-help-tests.sh"
"tests/mk-bootimg/run-ci-wiring-tests.sh"
)
fail=0
for s in "${scripts[@]}"; do
base="$(basename "$s")"
if echo "$job" | grep -q -- "$base"; then
echo "PASS: qemu-tools job invokes $s"
else
echo "FAIL: qemu-tools job never runs $s"
fail=1
fi
done
exit "$fail"
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# Regression test: --help must not spill into build/mk-bootimg.sh's own code.
#
# build/fetch-vendor.sh's --help sliced its own source with a hardcoded line
# range that stopped one line too late, printing "set -uo pipefail" -- the
# first line of code -- as the last line of help text. mk-bootimg.sh's
# --help used the same hardcoded-range shape (currently pointed at the right
# span), so the same slip was one header-comment edit away here too. It now
# uses the same self-terminating awk pattern fetch-vendor.sh was fixed to
# use, so this pins both that it stays in sync as the header grows or
# shrinks and that it matches the header verbatim today.
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="$HERE/../../build/mk-bootimg.sh"
[ -f "$SCRIPT" ] || { echo "FAIL: script not found at $SCRIPT"; exit 1; }
fail=0
HELP_OUT="$(bash "$SCRIPT" --help)"
if printf '%s\n' "$HELP_OUT" | grep -q 'set -euo pipefail'; then
echo "FAIL: --help prints the script's own code"
fail=1
else
echo "PASS: --help does not print the script's own code"
fi
LAST_LINE="$(printf '%s\n' "$HELP_OUT" | tail -1)"
if [ "$LAST_LINE" = "# [--resource-tool PATH]" ]; then
echo "PASS: --help ends on the last header comment line, not past it"
else
echo "FAIL: --help ends on the last header comment line, not past it (got: $LAST_LINE)"
fail=1
fi
FIRST_LINE="$(printf '%s\n' "$HELP_OUT" | head -1)"
if [ "$FIRST_LINE" = "# Package a bootable boot.img from a kernel this SDK built." ]; then
echo "PASS: --help starts after the shebang, not on it"
else
echo "FAIL: --help starts after the shebang, not on it (got: $FIRST_LINE)"
fail=1
fi
exit "$fail"