When update or install.sh fails and there's a terminal, ask whether to send its log to the CoreDesktop team, with the option of reading exactly what would be sent first. Nothing is sent without a yes, and nothing is asked without a terminal. The new report command sends a log later (report, report FILE) or shows what would be sent (report --view). Before sending, the log is cleaned of terminal codes, and the home folder, user name, host name, email addresses, IPv4 addresses and Tailscale names are replaced. CoreDesktop's version, the kernel's, the failed steps and sibling versions go along with it. The request format the receiver on federated.sh implements is described at the top of report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
6.9 KiB
Bash
240 lines
6.9 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 report; 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 report; 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 "when a step fails, update offers to send its log, naming what failed" {
|
|
printf '#!/bin/bash\nprintf "%%s\\n" "$@" > "$BATS_TEST_TMPDIR/report-args"\n' > "$CD/report"
|
|
hook 10-a 'return 1'
|
|
hook 20-b 'return 1'
|
|
|
|
run "$CD/update"
|
|
[ "$status" -eq 1 ]
|
|
log=("$HOME"/.local/state/coredesktop/logs/update-*.log)
|
|
[ "$(cat "$BATS_TEST_TMPDIR/report-args")" = "$(printf '%s\n' --offer "${log[0]}" 10-a.sh 20-b.sh)" ]
|
|
}
|
|
|
|
@test "update doesn't offer to send its log when nothing failed" {
|
|
printf '#!/bin/bash\ntouch "$BATS_TEST_TMPDIR/report-ran"\n' > "$CD/report"
|
|
hook 10-a 'true'
|
|
|
|
run "$CD/update"
|
|
[ "$status" -eq 0 ]
|
|
[ ! -e "$BATS_TEST_TMPDIR/report-ran" ]
|
|
}
|
|
|
|
@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
|
|
}
|