The update conflict tests stub sudo and pacman, but omarchy-update-pacman now puts systemd-run between them, so on a systemd-booted host the tests would reach for the real system manager. Stub systemd-run to drop the wrapper's options and run the command, and cover the helper's own invocation composition in a new test. Raised by codex review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015zcqENR1UbhuwC1v5u3Wop
285 lines
10 KiB
Bash
Executable File
285 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(dirname "$0")/base-test.sh"
|
|
|
|
test_tmp=$(mktemp -d)
|
|
trap 'rm -rf "$test_tmp"' EXIT
|
|
|
|
stub_bin="$test_tmp/bin"
|
|
mkdir -p "$stub_bin"
|
|
|
|
cat >"$stub_bin/sudo" <<'STUB'
|
|
#!/bin/bash
|
|
exec "$@"
|
|
STUB
|
|
|
|
# omarchy-update-pacman wraps the transaction in a real PID 1 scope; the tests
|
|
# must stay inside the fixture, so drop the wrapper's options and run the command.
|
|
cat >"$stub_bin/systemd-run" <<'STUB'
|
|
#!/bin/bash
|
|
while [[ $1 == -* ]]; do shift; done
|
|
exec "$@"
|
|
STUB
|
|
|
|
# Fails the first -Syu with the report under test, then succeeds unless the case
|
|
# asked for the retry to fail too.
|
|
cat >"$stub_bin/pacman" <<'STUB'
|
|
#!/bin/bash
|
|
if [[ $1 == -Qo ]]; then
|
|
# Anything in OWNED_PATHS has a package behind it; everything else is unowned.
|
|
[[ " $OWNED_PATHS " == *" $2 "* ]]
|
|
exit $?
|
|
fi
|
|
|
|
attempt=$(($(cat "$PACMAN_ATTEMPTS") + 1))
|
|
echo "$attempt" >"$PACMAN_ATTEMPTS"
|
|
if ((attempt == 1)); then
|
|
cat "$CONFLICT_REPORT" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -n ${RETRY_FAILS:-} ]]; then
|
|
# Optionally commit the file first, as a partial transaction would.
|
|
[[ -n ${RETRY_INSTALLS:-} ]] && echo "packaged" >"$RETRY_INSTALLS"
|
|
echo "error: failed to retrieve some files" >&2
|
|
exit 1
|
|
fi
|
|
echo "upgrade complete"
|
|
STUB
|
|
|
|
chmod +x "$stub_bin/sudo" "$stub_bin/systemd-run" "$stub_bin/pacman"
|
|
|
|
replaced="$test_tmp/replaced"
|
|
|
|
run_update() {
|
|
OMARCHY_REPLACED_DIR="$replaced" \
|
|
RETRY_FAILS="${RETRY_FAILS:-}" \
|
|
RETRY_INSTALLS="${RETRY_INSTALLS:-}" \
|
|
PACMAN_ATTEMPTS="$test_tmp/attempts" \
|
|
CONFLICT_REPORT="$test_tmp/report" \
|
|
OWNED_PATHS="${OWNED_PATHS:-}" \
|
|
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
|
bash "$ROOT/bin/omarchy-update-system-pkgs"
|
|
}
|
|
|
|
# $1 blamed package, $2 path, $3 optional owning package.
|
|
write_report() {
|
|
echo 0 >"$test_tmp/attempts"
|
|
{
|
|
echo "error: failed to commit transaction (conflicting files)"
|
|
echo "$1: $2 exists in filesystem${3:+ (owned by $3)}"
|
|
} >"$test_tmp/report"
|
|
}
|
|
|
|
# Raw conflict lines, for reports the recovery must refuse wholesale.
|
|
write_raw_report() {
|
|
echo 0 >"$test_tmp/attempts"
|
|
{
|
|
echo "error: failed to commit transaction (conflicting files)"
|
|
printf '%s\n' "$@"
|
|
} >"$test_tmp/report"
|
|
}
|
|
|
|
work="$test_tmp/work"
|
|
fresh_work() {
|
|
rm -rf "$work" "$replaced"
|
|
mkdir -p "$work"
|
|
}
|
|
|
|
# An unowned path one of the packages is taking over.
|
|
fresh_work
|
|
stray="$work/omarchy-fcitx5.service"
|
|
echo "stray content" >"$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
|
fail "an unowned file conflict is not resolved"
|
|
[[ ! -e $stray ]] ||
|
|
fail "the file is left in pacman's way"
|
|
pass "a file pacman is taking over is moved out of its way"
|
|
|
|
# Kept out of the directory it came from, where SDDM and systemd-sleep read
|
|
# every file and every executable respectively.
|
|
[[ -z $(ls -A "$work") ]] ||
|
|
fail "something is left in the directory the replaced file came from"
|
|
grep -qx "stray content" "$replaced$stray" ||
|
|
fail "the replaced file is destroyed rather than kept out of the way"
|
|
pass "the replaced file is quarantined outside the directory it came from"
|
|
|
|
# A real fight between packages, not Omarchy's leftovers. pacman appends
|
|
# "(owned by ...)" here.
|
|
fresh_work
|
|
echo "theirs" >"$stray"
|
|
write_report omarchy-settings-dev "$stray" someone-else
|
|
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "a file owned by another package is silently taken"
|
|
fi
|
|
[[ -e $stray && ! -e $replaced$stray ]] ||
|
|
fail "a file owned by another package is silently taken"
|
|
pass "a conflict owned by another package stops the upgrade instead of being taken"
|
|
|
|
# Report reads unowned, database disagrees: the parse is never the only thing
|
|
# between a retry and another package's file.
|
|
fresh_work
|
|
echo "theirs" >"$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
if OWNED_PATHS="$stray" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "pacman -Qo is not consulted before moving a file"
|
|
fi
|
|
[[ -e $stray && ! -e $replaced$stray ]] ||
|
|
fail "pacman -Qo is not consulted before moving a file"
|
|
pass "an owned path is left alone even when the report reads as unowned"
|
|
|
|
# A name prefix is not a namespace; only the packages that own system paths.
|
|
fresh_work
|
|
echo "stray" >"$stray"
|
|
write_report omarchy-chromium-bin "$stray"
|
|
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "an optional omarchy-prefixed package gets its conflicts auto-resolved"
|
|
fi
|
|
pass "only the packages that own system paths get their conflicts resolved"
|
|
|
|
# Not Omarchy's conflict to resolve.
|
|
fresh_work
|
|
echo "stray" >"$stray"
|
|
write_report some-other-pkg "$stray"
|
|
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "a conflict from an unrelated package is auto-resolved"
|
|
fi
|
|
pass "a conflict from a non-Omarchy package is left for a human"
|
|
|
|
# The path is used literally, so glob characters in a name mean nothing.
|
|
fresh_work
|
|
globby="$work/omarchy-[1].conf"
|
|
echo "globby" >"$globby"
|
|
write_report omarchy-settings-dev "$globby"
|
|
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
|
fail "a path whose name contains glob characters is not resolved"
|
|
[[ -f "$replaced$globby" && ! -e "$globby" ]] ||
|
|
fail "a path whose name contains glob characters is treated as a pattern"
|
|
pass "a path whose name would act as a glob is moved literally"
|
|
|
|
# A leftover directory is cleared the same way a file is.
|
|
fresh_work
|
|
conflict_dir="$work/omarchy-dir"
|
|
mkdir -p "$conflict_dir"
|
|
write_report omarchy-settings-dev "$conflict_dir"
|
|
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
|
fail "a conflicting directory is not cleared out of pacman's way"
|
|
[[ -d "$replaced$conflict_dir" && ! -e $conflict_dir ]] ||
|
|
fail "a conflicting directory is left in place"
|
|
pass "a conflicting directory is moved away"
|
|
|
|
# A space is legal in a package path; the parse must not truncate it.
|
|
fresh_work
|
|
spaced="$work/omarchy theme.conf"
|
|
echo "spaced" >"$spaced"
|
|
write_report omarchy-settings-dev "$spaced"
|
|
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
|
fail "a conflicting path containing a space is not resolved"
|
|
[[ -f "$replaced$spaced" && ! -e "$spaced" ]] ||
|
|
fail "a conflicting path containing a space is truncated"
|
|
pass "a conflicting path containing a space is parsed whole"
|
|
|
|
# An earlier quarantined copy is the more original one, and might not be ours.
|
|
fresh_work
|
|
echo "current" >"$stray"
|
|
mkdir -p "$replaced$work"
|
|
echo "from an earlier run" >"$replaced$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
|
fail "a conflict with an existing quarantined copy is not resolved"
|
|
grep -qrx "from an earlier run" "$replaced" ||
|
|
fail "an existing quarantined copy is destroyed to make room for a new one"
|
|
pass "an existing quarantined copy survives a later run needing the same name"
|
|
|
|
# mv without -T would move the source inside an existing destination directory.
|
|
fresh_work
|
|
echo "ours" >"$stray"
|
|
mkdir -p "$replaced$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
|
fail "a conflict whose destination is a directory is not resolved"
|
|
[[ -f "$replaced$stray" ]] ||
|
|
fail "the leftover was moved inside the existing destination directory"
|
|
pass "an existing destination directory is replaced, not moved into"
|
|
|
|
# One healable conflict beside one that is not. Moving only the first leaves the
|
|
# retry blocked by the second, and that config inactive for nothing.
|
|
fresh_work
|
|
echo "ours" >"$stray"
|
|
write_raw_report \
|
|
"omarchy-settings-dev: $stray exists in filesystem" \
|
|
"some-package: $work/theirs exists in filesystem (owned by other-package)"
|
|
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "an upgrade with an unhealable conflict reports success"
|
|
fi
|
|
[[ -e $stray && ! -e $replaced$stray ]] ||
|
|
fail "a healable conflict is moved even though another conflict dooms the retry"
|
|
pass "nothing moves unless every reported conflict is healable"
|
|
|
|
# The retry can still fail for an unrelated reason. Leave nothing inactive.
|
|
fresh_work
|
|
echo "ours" >"$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
if RETRY_FAILS=1 run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "a failed retry reports success"
|
|
fi
|
|
[[ -f $stray ]] ||
|
|
fail "a failed retry leaves the file moved away and inactive"
|
|
grep -qx "ours" "$stray" ||
|
|
fail "the restored file is not the original content"
|
|
[[ $(cat "$test_tmp/attempts") == 2 ]] ||
|
|
fail "the handler and the upgrade re-invoke each other instead of stopping"
|
|
pass "a failed retry puts the files back, without re-invoking the handler"
|
|
|
|
# A retry that failed after committing the files has nothing to restore, and
|
|
# should not announce a restore it is not doing.
|
|
fresh_work
|
|
echo "ours" >"$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
if RETRY_FAILS=1 RETRY_INSTALLS="$stray" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "a failed retry reports success"
|
|
fi
|
|
grep -qi "restoring" "$test_tmp/out" "$test_tmp/err" &&
|
|
fail "a restore is announced when no file is put back"
|
|
grep -qx "packaged" "$stray" ||
|
|
fail "the file pacman installed was overwritten by the restore"
|
|
pass "no restore is announced when there is nothing to put back"
|
|
|
|
# A dangling symlink reads as absent to -e, so a relative link that no longer
|
|
# resolves from inside the quarantine must still be recognised and put back.
|
|
fresh_work
|
|
ln -s ./neighbour "$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
if RETRY_FAILS=1 run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "a failed retry reports success"
|
|
fi
|
|
[[ -L $stray ]] ||
|
|
fail "a symlink that dangles from the quarantine is never restored"
|
|
pass "a dangling symlink is restored rather than stranded in the quarantine"
|
|
|
|
# The handler acts on a pacman report; an old or hand-written one would move
|
|
# live files aside for an upgrade that is not happening.
|
|
fresh_work
|
|
echo "ours" >"$stray"
|
|
write_report omarchy-settings-dev "$stray"
|
|
if PATH="$stub_bin:$ROOT/bin:$PATH" OMARCHY_REPLACED_DIR="$replaced" \
|
|
bash "$ROOT/bin/omarchy-update-system-pkgs-when-conflicted" "$test_tmp/report" \
|
|
>"$test_tmp/out" 2>"$test_tmp/err"; then
|
|
fail "the handler acts on a report handed to it outside an update"
|
|
fi
|
|
[[ -f $stray ]] ||
|
|
fail "the handler moved a live file when run outside an update"
|
|
pass "the handler refuses a report handed to it outside an update"
|
|
|
|
# The happy path must not pay for any of this.
|
|
fresh_work
|
|
: >"$test_tmp/report"
|
|
echo 1 >"$test_tmp/attempts"
|
|
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
|
fail "a clean upgrade fails"
|
|
[[ $(cat "$test_tmp/attempts") == 2 ]] ||
|
|
fail "a clean upgrade runs more than one pacman transaction"
|
|
pass "a clean upgrade runs a single pacman transaction"
|