rig/test/install-lifecycle.sh

172 lines
7.7 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# The install LIFECYCLE, driven end to end against a tree install.sh itself
# produced (#106) — the four beats box and cast already run in CI, which rig,
# the repo whose headline claim is convergence, ran nowhere:
#
# 1. install from THIS checkout (RIG_INSTALL_SOURCE — the local channel)
# 2. assert what landed (layout, current, the PATH chain)
# 3. a converging re-run (an EMPTY DIFF, never an exit code)
# 4. uninstall --all (ending in the absence assert)
#
# test/cli.sh drives the same verbs against throwaway roots; this suite runs
# them in the environment cli.sh deliberately fakes — the real default paths
# under the runner's own $HOME. Run: bash test/install-lifecycle.sh (CI's
# `install:` job). RIG_HOME/RIG_BIN redirect the roots for a local run; the
# refusal below explains when you need them.
#
# Deliberately no `set -e` — a failing beat is data, and the summary is the
# verdict (the test/release.sh harness shape).
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT" || exit 1
PASS=0 FAIL=0
# check <desc> <want_exit> <want_substr> <cmd...>
# Runs cmd, asserts exit code and (if non-empty) that combined output
# contains want_substr.
check() {
local desc="$1" want="$2" substr="$3"; shift 3
local out rc
out="$("$@" 2>&1)"; rc=$?
if [ "$rc" -ne "$want" ]; then
echo "FAIL: $desc — exit $rc, wanted $want"
printf '%s\n' "$out" | sed 's/^/ /'
FAIL=$((FAIL + 1)); return
fi
if [ -n "$substr" ] && ! printf '%s' "$out" | grep -qF -e "$substr"; then
echo "FAIL: $desc — output missing '$substr'"
printf '%s\n' "$out" | sed 's/^/ /'
FAIL=$((FAIL + 1)); return
fi
echo "ok: $desc"; PASS=$((PASS + 1))
}
# The roots install.sh will use, computed by ITS rules (install.sh:55-60), so
# every assert below points at what the installer actually touched.
DEST="${RIG_HOME:-$HOME/.local/share/rig}"
if [ "$(id -u)" -eq 0 ]; then
BINDIR="${RIG_BIN:-/usr/local/bin}"
else
BINDIR="${RIG_BIN:-$HOME/.local/bin}"
fi
# Beat 4 REMOVES the install at those roots, so a rig that already lives there
# is a refusal, not a fixture — this suite must never eat an operator's
# install. CI runners are clean; a workstation run points the roots at
# something disposable.
if [ -e "$DEST" ] || [ -L "$DEST" ] || [ -e "$BINDIR/rig" ] || [ -L "$BINDIR/rig" ]; then
echo "install-lifecycle: a rig install already exists ($DEST or $BINDIR/rig)" >&2
echo "install-lifecycle: refusing to drive the lifecycle over it — re-run against scratch roots:" >&2
echo " W=\$(mktemp -d); RIG_HOME=\$W/rig RIG_BIN=\$W/bin bash test/install-lifecycle.sh" >&2
exit 2
fi
VER="$(cat "$ROOT/VERSION")"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
# tree_state <root> — what "changed nothing" must mean: every file's bytes,
# every path's type and mode, every symlink's target. Beat 3 captures this
# before and after the re-run and diffs the two.
tree_state() {
(cd "$1" || return 1
find . -type f -exec sha256sum {} + | LC_ALL=C sort
find . -type l -printf '%p -> %l\n' | LC_ALL=C sort
find . -printf '%y %m %p\n' | LC_ALL=C sort)
}
diff_state() { # diff_state <capture> <root> — how the tree drifted, by name
tree_state "$2" | diff "$1" -
}
no_residue() { # no_residue <path>... — 0 iff every path is GONE: file, dir OR
local p bad=0 # symlink. `! -e` alone follows the link and cannot see it
for p in "$@"; do # dangling — the residue a broken uninstall actually leaves.
if [ -e "$p" ] || [ -L "$p" ]; then echo "still present: $p"; bad=1; fi
done
return "$bad"
}
# --- instrument honesty ------------------------------------------------------
# The diff and the absence assert must be able to FAIL, or beats 3 and 4 prove
# nothing — so break each one against a scratch tree first, on every run
# (the test/drill.sh doctrine: mechanical, not a one-off claim in a PR).
SCR="$WORK/scr"; mkdir -p "$SCR/tree/bin"
echo content > "$SCR/tree/bin/rig"
ln -s bin/rig "$SCR/tree/link"
tree_state "$SCR/tree" > "$SCR/cap"
check "honesty: an untouched tree reads as zero drift" 0 "" \
diff_state "$SCR/cap" "$SCR/tree"
echo drift >> "$SCR/tree/bin/rig"
check "honesty: a mutated file is drift, named" 1 "bin/rig" \
diff_state "$SCR/cap" "$SCR/tree"
tree_state "$SCR/tree" > "$SCR/cap"
ln -sfn ../elsewhere "$SCR/tree/link"
check "honesty: a retargeted symlink is drift" 1 "elsewhere" \
diff_state "$SCR/cap" "$SCR/tree"
tree_state "$SCR/tree" > "$SCR/cap"
touch "$SCR/tree/leftover"
check "honesty: an ADDED file is drift (what a non-convergent installer leaves)" 1 "leftover" \
diff_state "$SCR/cap" "$SCR/tree"
# The beat-4 distinction, demonstrated: `test ! -e` PASSES on a dangling
# symlink (it follows the link), so on its own it would certify a broken
# uninstall clean — only `! -L` sees the corpse.
ln -s "$SCR/nowhere" "$SCR/dangling-rig"
check "honesty: test ! -e cannot see a dangling symlink (the lie)" 0 "" \
test ! -e "$SCR/dangling-rig"
check "honesty: the absence assert can (! -L is the catch)" 1 "still present" \
no_residue "$SCR/dangling-rig"
rm "$SCR/dangling-rig"
check "honesty: a really-gone path passes the absence assert" 0 "" \
no_residue "$SCR/dangling-rig"
# --- beat 1: install from THIS checkout --------------------------------------
# RIG_INSTALL_SOURCE is the supported local channel (its contract — dir,
# tarball, loud refusal, no silent download fallback — is test/release.sh's);
# in CI $ROOT is $GITHUB_WORKSPACE, so what lands is the code under review.
b1() { RIG_INSTALL_SOURCE="$ROOT" bash "$ROOT/install.sh"; }
check "beat 1: install.sh installs this checkout" 0 "done" b1
# --- beat 2: assert what landed ----------------------------------------------
check "beat 2: the tree landed in versions/$VER" 0 "" \
test -x "$DEST/versions/$VER/bin/rig"
check "beat 2: current points at versions/$VER" 0 "versions/$VER" \
readlink "$DEST/current"
check "beat 2: the PATH symlink rides the chain" 0 "$DEST/current/bin/rig" \
readlink "$BINDIR/rig"
check "beat 2: ...and resolves into versions/ (cast's assert)" 0 "/versions/$VER/bin/rig" \
readlink -f "$BINDIR/rig"
check "beat 2: rig --version answers through the whole chain" 0 "rig $VER" \
"$BINDIR/rig" --version
check "beat 2: INSTALLED_FROM names the local source" 0 "local:$ROOT" \
cat "$DEST/versions/$VER/INSTALLED_FROM"
# --- beat 3: the converging re-run -------------------------------------------
# "Ran twice without crashing" is the self-deception this beat exists to
# refuse (#106): the assert is an empty diff of captured state, plus current
# still pointing where it did.
tree_state "$DEST" > "$WORK/before"
CUR_BEFORE="$(readlink "$DEST/current")"
check "beat 3: the re-run is a no-op that says so" 0 "already installed" b1
check "beat 3: ...and changed NOTHING — the diff is the verdict" 0 "" \
diff_state "$WORK/before" "$DEST"
check "beat 3: current did not move" 0 "" \
test "$(readlink "$DEST/current")" = "$CUR_BEFORE"
# --- beat 4: uninstall --all, ending in the absence assert -------------------
check "beat 4: uninstall --all removes the whole install" 0 "uninstalled" \
"$BINDIR/rig" uninstall --all --force
check "beat 4: zero residue at the install root" 0 "" no_residue "$DEST"
check "beat 4: zero residue on PATH — not even a dangling symlink" 0 "" \
no_residue "$BINDIR/rig"
# The doctrine spelled out as its two distinct asserts (#106): -e for
# presence, -L for the dangling link -e cannot see.
check "beat 4: test ! -e on the PATH entry" 0 "" test ! -e "$BINDIR/rig"
check "beat 4: test ! -L on the PATH entry" 0 "" test ! -L "$BINDIR/rig"
check "beat 4: test ! -e on the install root" 0 "" test ! -e "$DEST"
check "beat 4: test ! -L on the install root" 0 "" test ! -L "$DEST"
echo "---"
echo "$PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]