#!/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 -> 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 -> 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" <> "$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 { 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"