Files
coredesktop/tests/update.bats
T
davidandClaude Opus 5.5 54f9e12526
shellcheck / shellcheck (push) Failing after 17s
Log each install and update, and keep only the recent logs
Each run's output now goes to ~/.local/state/coredesktop/logs/, so what
failed can be read after the terminal is gone. In a terminal, update runs
itself under script(1), so everything it runs still sees a terminal, then
strips color codes and progress redraws from the log; otherwise it uses
tee. install.sh, usually piped into bash, uses tee, and runs configure
straight on the terminal, outside the log. Both name the log when
something fails. 95-cleanup.sh keeps the newest 10 update logs and 3
install logs.

The sudo keepalive loops no longer share the logged output, so their
sleep can't hold the log open after the run ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:00:31 -07:00

220 lines
6.2 KiB
Bash

#!/usr/bin/env bats
load test_helper
# Each test runs the real update script from a fake CoreDesktop checkout in the
# test's HOME, with fake hooks in updates.d/ that log to $RAN when they run.
setup() {
# A checkout with nothing but the update script and the files it installs
common_setup
CD="$HOME/.local/share/coredesktop"
mkdir -p "$CD/updates.d" "$HOME/.local/bin"
cp "$REPO/update" "$CD/update"
for script in refresh sibling version configure audit; do
echo '#!/bin/bash' > "$CD/$script"
done
export RAN="$BATS_TEST_TMPDIR/ran"
unset UPDATE_REEXECED UPDATE_PULL_FAILED UPDATE_FORCE UPDATE_LOG
stub clear
stub git 'exit "${GIT_STATUS:-0}"'
stub sudo 'exit "${SUDO_STATUS:-0}"'
stub df 'printf "Avail\n%s\n" "${DF_AVAIL:-104857600}"'
# Let go of bats' output pipes so the sudo keepalive's leftover sleep
# doesn't keep the test waiting
stub sleep 'exec > /dev/null 2>&1 3>&-; exec /usr/bin/sleep "$@"'
}
# hook NAME BODY: add updates.d/NAME.sh with BODY as its run_update
hook() {
printf '#!/bin/bash\nrun_update() {\n%s\n}\n' "$2" > "$CD/updates.d/$1.sh"
}
# plain_hook NAME BODY: a hook that does its work as it's sourced, with no
# run_update, like updates.d/12-migrations.sh
plain_hook() {
printf '#!/bin/bash\n%s\n' "$2" > "$CD/updates.d/$1.sh"
}
ran() {
[ "$(cat "$RAN" 2> /dev/null)" = "$(printf '%s\n' "$@")" ]
}
@test "update pulls, installs its scripts, and runs every hook in order" {
hook 20-b 'echo b >> "$RAN"'
hook 10-a 'echo a >> "$RAN"'
run "$CD/update"
[ "$status" -eq 0 ]
[[ "$output" == *"Done."* ]]
called git -C "$CD" pull --ff-only
ran a b
for script in update refresh sibling version configure audit; do
[ -x "$HOME/.local/bin/$script" ]
done
}
@test "update pulls only once, before re-running its new copy" {
hook 10-a 'echo a >> "$RAN"'
run "$CD/update"
[ "$status" -eq 0 ]
[ "$(grep -c '^git ' "$STUB_LOG")" -eq 1 ]
ran a
}
@test "a failing hook doesn't stop the hooks after it" {
hook 10-a 'echo a >> "$RAN"'
hook 20-b 'echo b >> "$RAN"; return 1'
hook 30-c 'echo c >> "$RAN"'
run "$CD/update"
[ "$status" -eq 1 ]
[[ "$output" == *"Done, with errors in: 20-b.sh"* ]]
ran a b c
}
@test "a failing hook without run_update fails the update too" {
plain_hook 10-a 'echo a >> "$RAN"; false'
plain_hook 20-b 'echo b >> "$RAN"'
run "$CD/update"
[ "$status" -eq 1 ]
[[ "$output" == *"Done, with errors in: 10-a.sh"* ]]
ran a b
}
@test "a hook without run_update that succeeds doesn't fail the update" {
plain_hook 10-a 'echo a >> "$RAN"'
run "$CD/update"
[ "$status" -eq 0 ]
[[ "$output" == *"Done."* ]]
ran a
}
@test "a hook that exits doesn't end the update" {
hook 10-a 'echo a >> "$RAN"; exit 0'
hook 20-b 'echo b >> "$RAN"'
run "$CD/update"
[ "$status" -eq 0 ]
ran a b
}
@test "a hook using set -e fails at its first failing command" {
hook 10-a 'set -e; echo a >> "$RAN"; false; echo after >> "$RAN"'
hook 20-b 'echo b >> "$RAN"'
run "$CD/update"
[ "$status" -eq 1 ]
[[ "$output" == *"errors in: 10-a.sh"* ]]
ran a b
}
@test "a hook's is_installed and functions don't leak into the next hook" {
printf '%s\n' '#!/bin/bash' 'is_installed() { false; }' \
'run_update() { echo a >> "$RAN"; }' > "$CD/updates.d/10-a.sh"
# 10-a's is_installed must not stop 20-b, and its run_update must not run
# again for 30-c, which has none
printf '%s\n' '#!/bin/bash' 'run_update() { echo b >> "$RAN"; }' \
> "$CD/updates.d/20-b.sh"
printf '%s\n' '#!/bin/bash' 'true' > "$CD/updates.d/30-c.sh"
run "$CD/update"
[ "$status" -eq 0 ]
ran b
}
@test "update runs the hooks on disk when git pull fails, and reports it" {
export GIT_STATUS=1
hook 10-a 'echo a >> "$RAN"'
run "$CD/update"
[ "$status" -eq 1 ]
[[ "$output" == *"could not pull"* ]]
[[ "$output" == *"errors in: git pull"* ]]
ran a
}
@test "update stops before any hook when / is low on space" {
export DF_AVAIL=1048576
hook 10-a 'echo a >> "$RAN"'
run "$CD/update"
[ "$status" -eq 1 ]
[[ "$output" == *"needs at least 10 GiB"* ]]
[ ! -e "$RAN" ]
}
@test "UPDATE_FORCE skips the free space check" {
export DF_AVAIL=1048576 UPDATE_FORCE=1
hook 10-a 'echo a >> "$RAN"'
run "$CD/update"
[ "$status" -eq 0 ]
ran a
}
@test "update stops when sudo fails" {
export SUDO_STATUS=1
hook 10-a 'echo a >> "$RAN"'
run "$CD/update"
[ "$status" -eq 1 ]
[ ! -e "$RAN" ]
}
@test "update logs its output to one file, across re-running its new copy" {
hook 10-a 'echo "hook a ran"'
run "$CD/update"
[ "$status" -eq 0 ]
logs=("$HOME"/.local/state/coredesktop/logs/update-*.log)
[ "${#logs[@]}" -eq 1 ]
grep -q "Updating CoreDesktop" "${logs[0]}"
grep -q "hook a ran" "${logs[0]}"
grep -q "^Done\.$" "${logs[0]}"
[ "$(stat -c %a "$HOME/.local/state/coredesktop/logs")" = 700 ]
}
@test "when a hook fails, update says where its log is" {
hook 10-a 'return 1'
run "$CD/update"
[ "$status" -eq 1 ]
[[ "$output" == *"The full output is in $HOME/.local/state/coredesktop/logs/update-"*".log"* ]]
}
@test "clean_log leaves only the text of a terminal session's log" {
eval "$(sed -n '/^clean_log() {/,/^}/p' "$REPO/update")"
log="$BATS_TEST_TMPDIR/session.log"
printf '%s\n' 'Script started on 2026-11-08 10:00:00-07:00 [COMMAND="update"]' > "$log"
printf '\033]0;title\007\033[1;32mgreen\033[0m Ünïcode █\r\n' >> "$log"
printf '10%%\r50%%\r100%%\r\n' >> "$log"
printf '\033[H\033[2J\033[3Jdone\r\n' >> "$log"
printf '%s\n' 'Script done on 2026-11-08 10:01:00-07:00 [COMMAND_EXIT_CODE="0"]' >> "$log"
clean_log "$log"
[ "$(cat "$log")" = "$(printf 'green Ünïcode █\n100%%\ndone')" ]
}
@test "an update from before 2026-10-19 that copies this one over itself hands over to it" {
# 013acac's update: sudo cp overwrites the file bash is still reading, and
# bash reads on in the new one from where it was
[ "$(/usr/bin/git -C "$REPO" rev-parse --is-shallow-repository)" = false ] \
|| skip "needs update's history"
/usr/bin/git -C "$REPO" show 013acac:update > "$HOME/.local/bin/update"
chmod +x "$HOME/.local/bin/update"
stub sudo '[ "$1" = cp ] && exec "$@"; exit 0'
hook 10-a 'echo a >> "$RAN"'
run "$HOME/.local/bin/update"
[ "$status" -eq 0 ]
[[ "$output" != *"command not found"* ]]
[[ "$output" == *"Done."* ]]
ran a
}