diff options
| -rwxr-xr-x | installer/archangel | 103 | ||||
| -rw-r--r-- | installer/lib/btrfs.sh | 11 | ||||
| -rw-r--r-- | tests/unit/test_archangel.bats | 398 | ||||
| -rw-r--r-- | tests/unit/test_btrfs.bats | 34 |
4 files changed, 532 insertions, 14 deletions
diff --git a/installer/archangel b/installer/archangel index 065772c..56290d9 100755 --- a/installer/archangel +++ b/installer/archangel @@ -1374,16 +1374,87 @@ cleanup() { info "Cleanup complete." } -# Trap target for ERR / INT / TERM during install_zfs and +# The trap set that routes a mid-install failure to install_failure_cleanup. +# EXIT is the one that does the work. Every install step is a function, and +# bash never delivers an ERR trap to a command failing inside a called +# function, so errexit used to end the script with the cleanup unrun. The +# `|| error "..."` shape leaves through a plain `exit 1`, which no ERR trap +# sees either. Both left /mnt mounted and the pool imported for the next +# attempt to trip over (2026-08-06, 2026-09-12). ERR still fires for a +# failure directly in install_zfs/install_btrfs, and INT and TERM cover +# Ctrl-C and a kill. The success path disarms all four before its own +# cleanup, whose non-zero exits are expected. +# +# I deliberately don't turn on errtrace (set -E) to widen ERR. It hands the +# trap to command substitutions too, so a masked failing `$(...)` ran the +# whole cleanup inside that subshell and unmounted /mnt while the install +# carried on in the parent. +arm_failure_trap() { + trap 'install_failure_cleanup' ERR INT TERM EXIT +} + +# How many one-second tries the failure cleanup gives a busy pool export or +# LUKS close. When a signal interrupts pacstrap, pacman's children in the +# chroot can still be exiting as the cleanup runs, so the first export fails +# busy and a manual one seconds later succeeds (VM, 2026-09-13). +CLEANUP_BUSY_ATTEMPTS="${CLEANUP_BUSY_ATTEMPTS:-15}" + +# Run a command until it succeeds, up to <attempts> times, sleeping one +# second between tries. Returns 1 if the last try still fails. +# +# Usage: retry_busy <attempts> <command> [args...] +retry_busy() { + local attempts="$1" i + shift + for ((i = 1; i <= attempts; i++)); do + "$@" && return 0 + ((i < attempts)) && sleep 1 + done + return 1 +} + +# Close this install's LUKS mappings and succeed only once none is left. +# close_luks_container swallows cryptsetup's errors, so a busy close has to +# be detected by looking for the mapping afterwards. +close_luks_and_confirm() { + btrfs_close_encryption 2>/dev/null + ! luks_mappings_open +} + +disarm_failure_trap() { + trap - ERR INT TERM EXIT +} + +# Trap target for ERR / INT / TERM / EXIT during install_zfs and # install_btrfs. Captures the failing exit code first, disarms the -# trap to prevent recursion, clears sensitive variables, and -# dispatches to the right per-filesystem cleanup before exiting via -# error(). All cleanup steps swallow their own errors — partial -# state is expected when this fires mid-install, so individual tool -# failures are not fatal. +# trap to prevent recursion (its own error() exit would re-fire EXIT), +# clears sensitive variables, and dispatches to the right +# per-filesystem cleanup before exiting via error(). All cleanup +# steps swallow their own errors — partial state is expected when +# this fires mid-install, so individual tool failures are not fatal. install_failure_cleanup() { local exit_code=$? - trap - ERR INT TERM + disarm_failure_trap + + # A SIGTERM sent to the whole process group (kill -- -PGID, timeout, a + # service stop) also kills the tee init_logging put on stdout. With the + # tee gone, the first warn below raised SIGPIPE and killed the shell + # before anything was unmounted. Ctrl-C doesn't hit this, because bash + # starts a process substitution with SIGINT ignored. Ignore SIGPIPE, and + # turn errexit off so a failed write to the dead pipe can't end the + # cleanup either. `local -` scopes that to this function: the cleanup + # normally leaves through error(), but a caller that gets control back + # (the unit tests) would otherwise keep running with errexit off. + local - + trap '' PIPE + set +e + + # disarm_failure_trap put INT and TERM back to their defaults, so a + # second Ctrl-C or SIGTERM during the busy retries below (several silent + # seconds, easy to read as a hang) killed the cleanup halfway and left + # the pool imported. Ignore both until the cleanup exits. It's bounded + # by CLEANUP_BUSY_ATTEMPTS, and SIGKILL still stops it. + trap '' INT TERM ROOT_PASSWORD="" ZFS_PASSPHRASE="" @@ -1402,7 +1473,7 @@ install_failure_cleanup() { umount "$EFI_DIR" 2>/dev/null || umount -l "$EFI_DIR" 2>/dev/null || true umount -R "$MNTPOINT" 2>/dev/null || umount -R -l "$MNTPOINT" 2>/dev/null || true if zpool list "$POOL_NAME" >/dev/null 2>&1; then - zpool export "$POOL_NAME" 2>/dev/null \ + retry_busy "$CLEANUP_BUSY_ATTEMPTS" zpool export "$POOL_NAME" 2>/dev/null \ || zpool export -f "$POOL_NAME" 2>/dev/null \ || true fi @@ -1410,7 +1481,13 @@ install_failure_cleanup() { btrfs) umount "$EFI_DIR" 2>/dev/null || umount -l "$EFI_DIR" 2>/dev/null || true btrfs_cleanup 2>/dev/null || true - btrfs_close_encryption 2>/dev/null || true + # btrfs_cleanup only unmounts the subvolumes it mounted. A + # pacstrap interrupted mid-transaction can leave its /proc, /sys + # and /dev bind mounts under the root, which keep it busy, so the + # LUKS mapping can't close. Same recursive-then-lazy fallback as + # the ZFS branch, before closing encryption. + umount -R "$MNTPOINT" 2>/dev/null || umount -R -l "$MNTPOINT" 2>/dev/null || true + retry_busy "$CLEANUP_BUSY_ATTEMPTS" close_luks_and_confirm || true ;; esac @@ -1531,7 +1608,7 @@ install_zfs() { # cleanup (unmount /mnt + export pool) so the live ISO is left in a # state where the user can re-run the installer without manual # intervention. - trap 'install_failure_cleanup' ERR INT TERM + arm_failure_trap partition_disks create_zfs_pool @@ -1553,7 +1630,7 @@ install_zfs() { # Disarm the failure trap before the success-path cleanup. The # success cleanup may emit non-zero exit codes that we don't want # to interpret as "installation failed". - trap - ERR INT TERM + disarm_failure_trap cleanup print_summary } @@ -1565,7 +1642,7 @@ install_zfs() { install_btrfs() { # Arm the failure trap before any destructive operation. See the # matching block in install_zfs() for the rationale. - trap 'install_failure_cleanup' ERR INT TERM + arm_failure_trap local btrfs_devices=() @@ -1605,7 +1682,7 @@ install_btrfs() { # Disarm the failure trap before the success-path cleanup. See # the matching block in install_zfs() for the rationale. - trap - ERR INT TERM + disarm_failure_trap # Cleanup btrfs_cleanup diff --git a/installer/lib/btrfs.sh b/installer/lib/btrfs.sh index 67c96a0..96ad29e 100644 --- a/installer/lib/btrfs.sh +++ b/installer/lib/btrfs.sh @@ -153,6 +153,17 @@ close_luks_containers() { done } +# Succeed when any of this install's LUKS mappings is still open. Names +# follow get_luks_devices: the bare LUKS_MAPPER_NAME for the first disk, +# then LUKS_MAPPER_NAME1, 2, ... MAPPER_DIR is overridable for tests. +luks_mappings_open() { + local dir="${MAPPER_DIR:-/dev/mapper}" m + for m in "$dir/$LUKS_MAPPER_NAME" "$dir/$LUKS_MAPPER_NAME"[0-9]*; do + [[ -e "$m" ]] && return 0 + done + return 1 +} + # Get list of opened LUKS mapper devices get_luks_devices() { local count="$1" diff --git a/tests/unit/test_archangel.bats b/tests/unit/test_archangel.bats index 983bfd2..508a062 100644 --- a/tests/unit/test_archangel.bats +++ b/tests/unit/test_archangel.bats @@ -20,6 +20,16 @@ setup() { # shellcheck disable=SC1091 source "${BATS_TEST_DIRNAME}/../../installer/archangel" UNATTENDED=true + # Tests that call install_failure_cleanup in this process would + # otherwise run the real disarm_failure_trap, which clears bats' own + # EXIT trap, and bats reports a failing assertion from that trap: the + # failure would vanish instead of printing "not ok". The trap arming + # itself is tested in a child bash, which sources the real functions. + disarm_failure_trap() { :; } + # Point the LUKS-mapping check at an empty directory, so a machine with + # a real /dev/mapper/cryptroot can't leak into the cleanup tests. + MAPPER_DIR="$BATS_TEST_TMPDIR/mapper" + mkdir -p "$MAPPER_DIR" } ############################# @@ -136,9 +146,212 @@ setup() { } ############################# +# Failure trap arming +############################# +# arm_failure_trap / disarm_failure_trap own the trap set that routes a +# mid-install failure to install_failure_cleanup. These run in a child +# bash (via `run bash -c`) because trap inheritance is a property of +# the live shell. Before the EXIT trap, a failure inside any install step +# ended the script through errexit without the cleanup running, and an +# `exit 1` from error() reached no ERR trap at all. Both were how the +# 2026-08-06 and 2026-09-12 retries found a still-mounted disk. + +@test "arm_failure_trap fires the cleanup for a command failing inside a function" { + run bash -c 'source "$1" + install_failure_cleanup() { disarm_failure_trap; echo CLEANUP-RAN; exit 7; } + arm_failure_trap + f() { false; } + f + echo NOT-REACHED' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" + [ "$status" -eq 7 ] + [[ "$output" == *"CLEANUP-RAN"* ]] + [[ "$output" != *"NOT-REACHED"* ]] +} + +@test "arm_failure_trap fires the cleanup when error() exits from inside a function" { + run bash -c 'source "$1" + install_failure_cleanup() { disarm_failure_trap; echo CLEANUP-RAN; exit 7; } + arm_failure_trap + f() { false || error "pacstrap failed"; } + f + echo NOT-REACHED' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" + [ "$status" -eq 7 ] + [[ "$output" == *"CLEANUP-RAN"* ]] + [[ "$output" != *"NOT-REACHED"* ]] +} + +@test "arm_failure_trap fires the cleanup for a failing pipeline inside a function" { + run bash -c 'source "$1" + install_failure_cleanup() { disarm_failure_trap; echo CLEANUP-RAN; exit 7; } + arm_failure_trap + f() { printf "\n" | false; } + f + echo NOT-REACHED' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" + [ "$status" -eq 7 ] + [[ "$output" == *"CLEANUP-RAN"* ]] + [[ "$output" != *"NOT-REACHED"* ]] +} + +@test "disarm_failure_trap lets the success path exit without running the cleanup" { + run bash -c 'source "$1" + install_failure_cleanup() { disarm_failure_trap; echo CLEANUP-RAN; exit 7; } + arm_failure_trap + f() { true; } + f + disarm_failure_trap + echo SUCCESS' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" + [ "$status" -eq 0 ] + [[ "$output" == *"SUCCESS"* ]] + [[ "$output" != *"CLEANUP-RAN"* ]] +} + +@test "arm_failure_trap fires the cleanup when the install is sent TERM" { + run bash -c 'source "$1" + install_failure_cleanup() { disarm_failure_trap; echo CLEANUP-RAN; exit 7; } + arm_failure_trap + f() { kill -TERM $$; sleep 2; echo NOT-REACHED; } + f' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" + [ "$status" -eq 7 ] + [[ "$output" == *"CLEANUP-RAN"* ]] + [[ "$output" != *"NOT-REACHED"* ]] +} + +# A failing command substitution whose status is masked (an argument to +# echo, a `local` declaration, a `|| true`) is not an install failure. With +# errtrace on, bash ran the whole cleanup inside the substitution's +# subshell, unmounting /mnt while the install carried on in the parent. +@test "a masked failing command substitution does not run the cleanup" { + local marker="$BATS_TEST_TMPDIR/cleanup-ran" + run bash -c 'source "$1"; M="$2" + install_failure_cleanup() { disarm_failure_trap; echo ran >> "$M"; exit 7; } + arm_failure_trap + f() { echo "$(false)" >/dev/null; local y; y=$(false) || true; echo STEP-DONE; } + f + disarm_failure_trap + echo SUCCESS' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" "$marker" + [ "$status" -eq 0 ] + [[ "$output" == *"STEP-DONE"* ]] + [[ "$output" == *"SUCCESS"* ]] + [ ! -e "$marker" ] +} + +# A SIGTERM to the whole process group also reaches the tee init_logging +# puts on stdout. With the tee gone, the cleanup's first warn raised +# SIGPIPE and killed the shell before anything was unmounted: a Btrfs +# install TERM'd mid-pacstrap in the VM on 2026-09-13 was left with /mnt +# and the LUKS mapping still open. Ctrl-C's SIGINT leaves the tee alive +# (bash starts a process substitution with SIGINT ignored), so the INT +# test guards the ordinary Ctrl-C path rather than the SIGPIPE fix. +# +# The child starts through `env --default-signal=INT` because bats runs it +# in the background, and a background job in a non-interactive shell +# begins with SIGINT ignored, which bash then refuses to trap. Without the +# reset, the INT case would pass by never delivering the signal at all. +signal_group_with_logging_tee() { + local sig="$1" d="$2" + env --default-signal=INT setsid bash -c 'source "$1"; D="$2" + FILESYSTEM=zfs; POOL_NAME=zroot + umount() { echo "umount $*" >> "$D/calls"; } + zpool() { echo "zpool $*" >> "$D/calls"; [[ "$1" == list ]]; } + exec > >(tee -a "$D/tee.log") 2>&1 + arm_failure_trap + grep SigIgn /proc/$$/status > "$D/sigign" + echo $$ > "$D/armed" + sleep 30' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" "$d" </dev/null >/dev/null 2>&1 & + + local i pid="" + for i in $(seq 1 100); do + [ -s "$d/armed" ] && pid=$(cat "$d/armed") && break + sleep 0.1 + done + [ -n "$pid" ] || return 1 + kill "-$sig" -- "-$pid" + for i in $(seq 1 100); do + kill -0 "$pid" 2>/dev/null || break + sleep 0.1 + done + kill -0 "$pid" 2>/dev/null && kill -KILL -- "-$pid" + return 0 +} + +@test "install_failure_cleanup finishes when SIGTERM to the group also killed its logging tee" { + local d="$BATS_TEST_TMPDIR" + signal_group_with_logging_tee TERM "$d" + grep -qx "zpool export zroot" "$d/calls" +} + +@test "install_failure_cleanup finishes when Ctrl-C's SIGINT reaches the whole group" { + local d="$BATS_TEST_TMPDIR" + signal_group_with_logging_tee INT "$d" + # Guard against the vacuous pass: SIGINT (mask bit 0x2) must not be + # ignored in the child, or the signal never arrived. + local mask + mask=$(awk '{print $2}' "$d/sigign") + [ $(( 0x$mask & 0x2 )) -eq 0 ] + grep -qx "zpool export zroot" "$d/calls" +} + +# The busy retries can keep the cleanup silent for several seconds, and a +# second Ctrl-C or SIGTERM in that window used to kill it halfway: the +# export never finished and the pool stayed imported. +@test "a second SIGTERM during the busy-retry window doesn't stop the cleanup" { + local d="$BATS_TEST_TMPDIR" + env --default-signal=INT setsid bash -c 'source "$1"; D="$2" + FILESYSTEM=zfs; POOL_NAME=zroot; CLEANUP_BUSY_ATTEMPTS=6 + umount() { :; } + zpool() { + [[ "$1" == list ]] && return 0 + if [[ "$*" == "export zroot" ]]; then + echo try >> "$D/exports" + [ "$(wc -l < "$D/exports")" -ge 4 ] && { echo exported >> "$D/done"; return 0; } + return 1 + fi + [[ "$*" == "export -f zroot" ]] && echo forced >> "$D/done" + return 0 + } + arm_failure_trap + echo $$ > "$D/armed" + sleep 30' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" "$d" </dev/null >/dev/null 2>&1 & + + local i pid="" + for i in $(seq 1 100); do + [ -s "$d/armed" ] && pid=$(cat "$d/armed") && break + sleep 0.1 + done + [ -n "$pid" ] + kill -TERM -- "-$pid" + for i in $(seq 1 50); do + [ -s "$d/exports" ] && break + sleep 0.1 + done + [ -s "$d/exports" ] + kill -TERM -- "-$pid" 2>/dev/null || true + for i in $(seq 1 100); do + kill -0 "$pid" 2>/dev/null || break + sleep 0.1 + done + kill -0 "$pid" 2>/dev/null && kill -KILL -- "-$pid" + + [ "$(cat "$d/done" 2>/dev/null)" = "exported" ] +} + +@test "install_failure_cleanup runs exactly once when its own exit re-fires the trap" { + run bash -c 'source "$1" + FILESYSTEM=zfs; POOL_NAME=zroot + umount() { :; } + zpool() { return 1; } + arm_failure_trap + f() { false; } + f' _ "${BATS_TEST_DIRNAME}/../../installer/archangel" + [ "$status" -eq 1 ] + [ "$(grep -c 'cleaning up' <<<"$output")" -eq 1 ] + [[ "$output" == *"system cleaned up"* ]] +} + +############################# # install_failure_cleanup ############################# -# install_failure_cleanup is the trap target for ERR / INT / TERM +# install_failure_cleanup is the trap target for ERR / INT / TERM / EXIT # during install_zfs and install_btrfs. It clears sensitive vars, # dispatches on FILESYSTEM, and exits non-zero. Tests use function # overrides to capture which system tools the cleanup invokes; the @@ -146,6 +359,143 @@ setup() { # btrfs_close_encryption) are deliberately VM-tested per # testing-strategy.org. +############################# +# retry_busy +############################# +# Teardown after an interrupted pacstrap races pacman's children in the +# chroot, which can still be exiting: the first export or LUKS close fails +# busy and succeeds a moment later. retry_busy gives it a bounded window. + +@test "retry_busy runs a command that succeeds once, without sleeping" { + CALLS=() + sleep() { CALLS+=("sleep $*"); } + ok() { CALLS+=("ok"); return 0; } + retry_busy 5 ok + [ "${#CALLS[@]}" -eq 1 ] + [ "${CALLS[0]}" = "ok" ] +} + +@test "retry_busy succeeds when the command first succeeds on the last attempt" { + CALLS=(); TRIES=0 + sleep() { CALLS+=("sleep $*"); } + flaky() { TRIES=$((TRIES + 1)); [ "$TRIES" -ge 3 ]; } + retry_busy 3 flaky + [ "$TRIES" -eq 3 ] + [ "${#CALLS[@]}" -eq 2 ] +} + +@test "retry_busy returns 1 after the last attempt, sleeping only between attempts" { + CALLS=(); TRIES=0 + sleep() { CALLS+=("sleep $*"); } + never() { TRIES=$((TRIES + 1)); return 1; } + run retry_busy 4 never + [ "$status" -eq 1 ] + retry_busy 4 never || true + [ "$TRIES" -eq 4 ] + [ "${#CALLS[@]}" -eq 3 ] +} + +# bats fails a test through errexit. The cleanup turns errexit off for +# itself, and before `local -` scoped that, every assertion after a direct +# call except the last one stopped being able to fail. +@test "install_failure_cleanup leaves the caller's errexit on" { + FILESYSTEM=zfs + POOL_NAME=zroot + umount() { :; } + zpool() { return 1; } + warn() { :; } + error() { return 1; } + + install_failure_cleanup || true + + # Capture the flags, then restore errexit before asserting. If the + # cleanup leaked errexit-off, a bare assertion here couldn't fail. + local flags="$-" + set -e + [[ "$flags" == *e* ]] +} + +@test "install_failure_cleanup ZFS path retries the export while the pool is still busy" { + FILESYSTEM=zfs + POOL_NAME=zroot + CALLS=(); EXPORTS=0 + umount() { :; } + zpool() { + CALLS+=("zpool $*") + [[ "$1" == list ]] && return 0 + if [[ "$*" == "export zroot" ]]; then + EXPORTS=$((EXPORTS + 1)) + [ "$EXPORTS" -ge 3 ] + return + fi + return 0 + } + sleep() { CALLS+=("sleep $*"); } + warn() { :; } + error() { return 1; } + + install_failure_cleanup || true + + [ "$EXPORTS" -eq 3 ] + [[ " ${CALLS[*]} " != *" zpool export -f zroot "* ]] +} + +@test "install_failure_cleanup ZFS path forces the export once the busy window runs out" { + FILESYSTEM=zfs + POOL_NAME=zroot + CLEANUP_BUSY_ATTEMPTS=4 + CALLS=(); EXPORTS=0; SLEEPS=0 + umount() { :; } + zpool() { + CALLS+=("zpool $*") + [[ "$1" == list ]] && return 0 + [[ "$*" == "export zroot" ]] && { EXPORTS=$((EXPORTS + 1)); return 1; } + return 0 + } + sleep() { SLEEPS=$((SLEEPS + 1)); } + warn() { :; } + error() { return 1; } + + install_failure_cleanup || true + + [ "$EXPORTS" -eq 4 ] + [ "$SLEEPS" -eq 3 ] + [[ " ${CALLS[*]} " == *" zpool export -f zroot "* ]] +} + +@test "install_failure_cleanup Btrfs path retries closing LUKS until the mapping is gone" { + FILESYSTEM=btrfs + CLOSES=0 + umount() { :; } + btrfs_cleanup() { :; } + btrfs_close_encryption() { CLOSES=$((CLOSES + 1)); } + luks_mappings_open() { [ "$CLOSES" -lt 3 ]; } + sleep() { :; } + warn() { :; } + error() { return 1; } + + install_failure_cleanup || true + + [ "$CLOSES" -eq 3 ] +} + +@test "install_failure_cleanup Btrfs path stops retrying the LUKS close after the busy window" { + FILESYSTEM=btrfs + CLEANUP_BUSY_ATTEMPTS=4 + CLOSES=0 + umount() { :; } + btrfs_cleanup() { :; } + btrfs_close_encryption() { CLOSES=$((CLOSES + 1)); } + luks_mappings_open() { return 0; } + sleep() { :; } + warn() { :; } + error() { return 1; } + + install_failure_cleanup || true + + [ "$CLOSES" -eq 4 ] +} + @test "install_failure_cleanup clears sensitive variables before exiting" { FILESYSTEM=zfs POOL_NAME=zroot @@ -218,6 +568,52 @@ setup() { [[ " ${CALLS[*]} " != *" zpool"* ]] } +# btrfs_cleanup unmounts only the subvolumes it mounted, one level at a +# time. A pacstrap interrupted mid-transaction can leave its /proc, /sys +# and /dev bind mounts under /mnt, which keep the root busy, so the LUKS +# mapping can't close and the retry's disk_in_use still sees a mountpoint. +@test "install_failure_cleanup Btrfs path unmounts the target root recursively before closing LUKS" { + FILESYSTEM=btrfs + CALLS=() + + umount() { CALLS+=("umount $*"); return 0; } + zpool() { CALLS+=("zpool $*"); return 0; } + btrfs_cleanup() { CALLS+=("btrfs_cleanup"); } + btrfs_close_encryption() { CALLS+=("btrfs_close_encryption"); } + warn() { :; } + error() { CALLS+=("error"); return 1; } + + install_failure_cleanup || true + + local i recursive=-1 close=-1 + for i in "${!CALLS[@]}"; do + [[ "${CALLS[$i]}" == "umount -R /mnt" ]] && recursive=$i + [[ "${CALLS[$i]}" == "btrfs_close_encryption" ]] && close=$i + done + [ "$recursive" -ge 0 ] + [ "$close" -gt "$recursive" ] +} + +@test "install_failure_cleanup Btrfs path falls back to a lazy recursive unmount when the root is busy" { + FILESYSTEM=btrfs + CALLS=() + + umount() { + CALLS+=("umount $*") + [[ "$*" == "-R /mnt" ]] && return 32 + return 0 + } + btrfs_cleanup() { CALLS+=("btrfs_cleanup"); } + btrfs_close_encryption() { CALLS+=("btrfs_close_encryption"); } + warn() { :; } + error() { CALLS+=("error"); return 1; } + + install_failure_cleanup || true + + [[ " ${CALLS[*]} " == *" umount -R -l /mnt "* ]] + [[ " ${CALLS[*]} " == *" btrfs_close_encryption "* ]] +} + @test "install_failure_cleanup ZFS path skips zpool export when pool not imported" { FILESYSTEM=zfs POOL_NAME=zroot diff --git a/tests/unit/test_btrfs.bats b/tests/unit/test_btrfs.bats index 15bf141..6205311 100644 --- a/tests/unit/test_btrfs.bats +++ b/tests/unit/test_btrfs.bats @@ -89,3 +89,37 @@ setup() { run parse_btrfs_subvol_opts "@x" "nodatacow,nosuid" [ "$output" = "subvol=@x,noatime,space_cache=v2,discard=async,nodatacow,nosuid,nodev" ] } + +############################# +# luks_mappings_open +############################# +# The failure cleanup can't trust close_luks_container's exit status (it +# swallows errors), so it checks whether a mapping still exists. Names +# follow get_luks_devices: the bare LUKS_MAPPER_NAME, then a numeric suffix. + +@test "luks_mappings_open succeeds when the bare mapping exists" { + MAPPER_DIR="$BATS_TEST_TMPDIR/mapper"; mkdir -p "$MAPPER_DIR" + touch "$MAPPER_DIR/$LUKS_MAPPER_NAME" + run luks_mappings_open + [ "$status" -eq 0 ] +} + +@test "luks_mappings_open succeeds when only a suffixed mapping exists" { + MAPPER_DIR="$BATS_TEST_TMPDIR/mapper"; mkdir -p "$MAPPER_DIR" + touch "$MAPPER_DIR/${LUKS_MAPPER_NAME}1" + run luks_mappings_open + [ "$status" -eq 0 ] +} + +@test "luks_mappings_open fails when no mapping exists" { + MAPPER_DIR="$BATS_TEST_TMPDIR/mapper"; mkdir -p "$MAPPER_DIR" + run luks_mappings_open + [ "$status" -eq 1 ] +} + +@test "luks_mappings_open ignores unrelated mappings" { + MAPPER_DIR="$BATS_TEST_TMPDIR/mapper"; mkdir -p "$MAPPER_DIR" + touch "$MAPPER_DIR/control" "$MAPPER_DIR/vg-root" "$MAPPER_DIR/${LUKS_MAPPER_NAME}-old" + run luks_mappings_open + [ "$status" -eq 1 ] +} |
