On the pacman backend, an installed AUR helper (paru, else yay;
--aur-helper=paru|yay|none, --no-aur) adds the AUR updates to the review
list. The helper never runs as root: when the tool is root through sudo it
runs as SUDO_USER (runuser -u, else sudo -u, with that user's HOME, from
that home); unprivileged it runs as is; a root login gets no AUR, with a
warning. The banner names the helper ("+ AUR via paru").
- listing: <helper> -Qua (also with --no-refresh), rows with repository
"aur"; [ignored] is shown [held]; a failing -Qua warns and the session
goes on with the repository rows
- every row has a marker column after the number: "*" for AUR rows, a
space otherwise (on every backend)
- N?? merges pacman -Qi with <helper> -Sia; N?, N+, Nx stay with pacman;
N- excludes through the helper's --ignore
- <enter>: the repository step exactly as before, then <helper> -Sua
--ignore <excluded> as the invoking user (--noconfirm only when stdin is
not a terminal); pacman is skipped when only AUR rows are left; a helper
failure is reported and its status returned; --dry-run prints it
- other backends never run a helper; --aur-helper is ignored with a warning
paru 2.1.0 and yay 13.0.1 formats verified in discovery/pacman.md section
15 (proof: discovery/pacman/aur.sh). Unit tests replay the recorded output
(fake paru, yay, runuser, getent); the archlinux E2E run installs the
foreign su-dummy-aur and drives a fake paru through a fake sudo built on
passwordless su for wheel.
788 lines
32 KiB
Bash
788 lines
32 KiB
Bash
#!/usr/bin/env bats
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
# The exact native commands issued per backend: refresh, upgrade + per-run exclusion
|
|
# (apt hold journal, pacman --ignore, dnf --exclude, zypper private locks), purge plans.
|
|
|
|
# each @test runs in its own subshell: per-test exports are intended
|
|
# shellcheck disable=SC2030,SC2031
|
|
|
|
load test_helper
|
|
|
|
setup() { common_setup; }
|
|
|
|
APT_UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
|
|
DNF_UPGRADE='dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade'
|
|
|
|
# ---- apt ----------------------------------------------------------------------------------------
|
|
|
|
@test "apt: refresh is apt-get update -qq -o APT::Update::Error-Mode=any" {
|
|
run skup list
|
|
assert_success
|
|
assert_line 'Refreshing package lists... done.'
|
|
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
|
|
}
|
|
|
|
@test "apt: a failed refresh warns and goes on with the cached lists" {
|
|
FAKE_APT_UPDATE_RC=100 run skup list
|
|
assert_success
|
|
assert_line 'Refreshing package lists... FAILED.'
|
|
assert_output --partial "E: Failed to fetch http://deb.debian.org/debian/dists/bookworm/InRelease"
|
|
assert_output --partial 'warning: metadata refresh failed (apt-get update -qq -o APT::Update::Error-Mode=any exited 100); continuing with the cached package lists'
|
|
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
|
|
}
|
|
|
|
@test "apt: the upgrade command line" {
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
log_has "$APT_UPGRADE"
|
|
assert_line "+ $APT_UPGRADE"
|
|
}
|
|
|
|
@test "apt: exclusions are temporary holds; a package the admin held is never held nor released by us" {
|
|
echo su-dummy-c >"$FAKE_DIR/holds"
|
|
FAKE_APT_SIM=held run skup --no-refresh <<<$'2-\n3-\n'
|
|
assert_success
|
|
assert_line 'Excluded from this run: su-dummy-b su-dummy-c'
|
|
log_has 'apt-mark showhold'
|
|
log_has 'apt-mark hold su-dummy-b'
|
|
log_has 'holds during upgrade: su-dummy-b su-dummy-c '
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
log_lacks '^apt-mark (un)?hold .*su-dummy-c'
|
|
run cat "$FAKE_DIR/holds"
|
|
assert_output 'su-dummy-c'
|
|
[[ ! -e $STATE/holds.added ]]
|
|
# order: hold, upgrade, unhold
|
|
run grep -nE '^apt-mark (un)?hold|^apt-get -y' "$FAKE_LOG"
|
|
assert_line --index 0 --partial 'apt-mark hold su-dummy-b'
|
|
assert_line --index 1 --partial "$APT_UPGRADE"
|
|
assert_line --index 2 --partial 'apt-mark unhold su-dummy-b'
|
|
}
|
|
|
|
@test "apt: the holds are written to the journal before apt-mark hold runs" {
|
|
# a hold that fails half-way: the journal still lets the restore release it
|
|
FAKE_APT_MARK_HOLD_RC=100 run skup --no-refresh <<<$'1-\n2-\n'
|
|
assert_failure 1
|
|
assert_output --partial 'could not place the temporary holds'
|
|
log_has 'apt-mark unhold su-dummy-a su-dummy-b'
|
|
run cat "$FAKE_DIR/holds"
|
|
assert_output ''
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
@test "apt: holds are released even when the upgrade fails" {
|
|
FAKE_APT_UPGRADE_RC=100 run skup --no-refresh <<<$'2-\n'
|
|
assert_failure 100
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
run cat "$FAKE_DIR/holds"
|
|
assert_output ''
|
|
}
|
|
|
|
@test "apt: holds are released when the run is killed (SIGTERM) during the upgrade" {
|
|
FAKE_APT_UPGRADE_KILL=TERM run skup --no-refresh <<<$'2-\n'
|
|
assert_failure 143
|
|
assert_output --partial 'interrupted'
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
run cat "$FAKE_DIR/holds"
|
|
assert_output ''
|
|
[[ ! -e $STATE/holds.added ]]
|
|
}
|
|
|
|
@test "apt: holds are released on SIGQUIT and SIGHUP too" {
|
|
FAKE_APT_UPGRADE_KILL=QUIT run skup --no-refresh <<<$'2-\n'
|
|
assert_failure 131
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
: >"$FAKE_LOG"
|
|
FAKE_APT_UPGRADE_KILL=HUP run skup --no-refresh <<<$'2-\n'
|
|
assert_failure 129
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
run cat "$FAKE_DIR/holds"
|
|
assert_output ''
|
|
}
|
|
|
|
@test "apt: a journal left by a crashed run is replayed at the start of the next root run (even a list)" {
|
|
printf '%s\n' su-dummy-b su-dummy-c >"$FAKE_DIR/holds" # b leaked by the crash, c is the admin's
|
|
mkdir -p "$STATE"
|
|
printf '%s\n' su-dummy-b su-dummy-e >"$STATE/holds.added" # e: leaked, but the admin released it since
|
|
run skup list --no-refresh
|
|
assert_success
|
|
assert_line 'Released temporary holds left over by an interrupted run: su-dummy-b'
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
log_lacks 'unhold .*su-dummy-(c|e)'
|
|
run cat "$FAKE_DIR/holds"
|
|
assert_output 'su-dummy-c'
|
|
[[ ! -e $STATE/holds.added ]]
|
|
# the replay happens before the listing
|
|
run grep -nE '^apt-mark unhold|^apt-get -s' "$FAKE_LOG"
|
|
assert_line --index 0 --partial 'apt-mark unhold su-dummy-b'
|
|
}
|
|
|
|
@test "apt: a journal whose holds are all gone is just removed" {
|
|
mkdir -p "$STATE"
|
|
echo su-dummy-e >"$STATE/holds.added"
|
|
run skup list --no-refresh
|
|
assert_success
|
|
assert_line 'Removed a stale hold journal of an interrupted run (nothing was still held).'
|
|
log_lacks 'unhold'
|
|
[[ ! -e $STATE/holds.added ]]
|
|
}
|
|
|
|
@test "apt: no replay as non-root or under --dry-run" {
|
|
mkdir -p "$STATE"
|
|
echo su-dummy-b >"$STATE/holds.added"
|
|
echo su-dummy-b >"$FAKE_DIR/holds"
|
|
run skup --dry-run list --no-refresh
|
|
assert_success
|
|
SKEPTICAL_UPDATER_EUID=1000 run skup list --no-refresh
|
|
assert_success
|
|
log_lacks 'unhold'
|
|
[[ -e $STATE/holds.added ]]
|
|
}
|
|
|
|
@test "apt: the replay leaves the journal alone while another run holds its lock" {
|
|
mkdir -p "$STATE"
|
|
echo su-dummy-b >"$STATE/holds.added"
|
|
echo su-dummy-b >"$FAKE_DIR/holds"
|
|
exec 9>>"$STATE/holds.lock"
|
|
flock 9
|
|
run skup list --no-refresh
|
|
exec 9>&-
|
|
assert_success
|
|
assert_output --partial "another skeptical-updater run is using $STATE/holds.added; leaving its temporary holds alone."
|
|
log_lacks 'unhold'
|
|
[[ -e $STATE/holds.added ]]
|
|
}
|
|
|
|
@test "apt: apt-mark retries while dpkg's frontend lock is busy (it ignores DPkg::Lock::Timeout)" {
|
|
FAKE_APT_MARK_LOCKED=2 run skup --no-refresh <<<$'2-\n'
|
|
assert_success
|
|
[[ $(grep -c '^apt-mark hold su-dummy-b' "$FAKE_LOG") -eq 3 ]]
|
|
log_has 'apt-mark unhold su-dummy-b'
|
|
run cat "$FAKE_DIR/holds"
|
|
assert_output ''
|
|
}
|
|
|
|
@test "apt: when the restore fails, the holds lock stays held for the exit handler's retry" {
|
|
FAKE_APT_MARK_UNHOLD_RC=100 run skup --no-refresh <<<$'2-\n'
|
|
assert_failure 1
|
|
assert_output --partial 'could not release the temporary holds (su-dummy-b); they are recorded in'
|
|
[[ $(grep -c '^holds.lock during unhold: ' "$FAKE_LOG") -eq 2 ]]
|
|
log_lacks '^holds.lock during unhold: free'
|
|
[[ -e $STATE/holds.added ]]
|
|
}
|
|
|
|
@test "apt: the dpkg 1.22+ wording of the busy lock is retried as well" {
|
|
FAKE_APT_MARK_LOCKED=1 FAKE_APT_MARK_LOCK_MSG='dpkg: error: dpkg frontend lock was locked by /usr/bin/perl process with pid 3297' \
|
|
run skup --no-refresh <<<$'2-\n'
|
|
assert_success
|
|
[[ $(grep -c '^apt-mark hold su-dummy-b' "$FAKE_LOG") -eq 2 ]]
|
|
}
|
|
|
|
@test "apt: an apt-mark failure whose message merely contains 'lock' (libblockdev) is not retried" {
|
|
SECONDS=0
|
|
FAKE_APT_MARK_HOLD_RC=100 FAKE_APT_MARK_FAIL_MSG='E: Unable to locate package libblockdev-crypto3' run skup --no-refresh <<<$'2-\n'
|
|
assert_failure 1
|
|
[[ $(grep -c '^apt-mark hold' "$FAKE_LOG") -eq 1 ]]
|
|
((SECONDS < 20))
|
|
}
|
|
|
|
@test "apt: Nx on a package others depend on is refused (apt-cache rdepends gate), nothing simulated or removed" {
|
|
run skup --no-refresh <<<$'2x\nl\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks 'purge'
|
|
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
|
|
}
|
|
|
|
@test "apt: purge plan = the simulated purge minus pre-existing orphans; the target's own orphaned dependencies go too" {
|
|
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nq'
|
|
assert_success
|
|
log_has 'apt-get -s autoremove'
|
|
log_has 'apt-get -s purge --auto-remove su-dummy-a'
|
|
log_has 'apt-get -s purge su-dummy-a su-dummy-b'
|
|
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
|
run grep -E '^apt-get -y' "$FAKE_LOG"
|
|
assert_output 'apt-get -y purge su-dummy-a su-dummy-b'
|
|
refute_output --partial 'su-dummy-orphan'
|
|
log_lacks 'autoremove --purge|-y autoremove'
|
|
}
|
|
|
|
@test "apt: belt and braces - a simulated purge that also removes a non-dependency is refused" {
|
|
FAKE_APT_SIM_EXTRA=su-dummy-c:su-dummy-w run skup --no-refresh <<<$'3x\nq'
|
|
assert_success
|
|
assert_output --partial 'cannot purge su-dummy-c: apt would also remove su-dummy-w, which su-dummy-c does not depend on (dependents of it?); nothing was removed'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks '^apt-get -y'
|
|
}
|
|
|
|
# ---- pacman -------------------------------------------------------------------------------------
|
|
|
|
REAL_SYNC_RE='/pacman-db/sync'
|
|
|
|
@test "pacman: the refresh syncs a throwaway --dbpath (pid in its name); the real sync database is never touched" {
|
|
use_fakes pacman
|
|
echo "old" >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
run skup list
|
|
assert_success
|
|
run grep -E '^-Sy ' "$FAKE_DIR/pacman-dbpath"
|
|
assert_output --regexp '^-Sy /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+$'
|
|
db=${output#-Sy }
|
|
run grep -E '^-(Qu|Sl|Sup) ' "$FAKE_DIR/pacman-dbpath"
|
|
refute_output --partial ' real'
|
|
assert_output --partial "-Qu $db"
|
|
[[ ! -e $db ]]
|
|
log_has "pacman -Sy --dbpath $db --logfile /dev/null"
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
|
}
|
|
|
|
@test "pacman: the throwaway database is seeded with the system's databases AND their signatures" {
|
|
use_fakes pacman
|
|
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
echo old-sig >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig"
|
|
run skup list
|
|
assert_success
|
|
log_has 'seeded: su-dummy.db su-dummy.db.sig'
|
|
}
|
|
|
|
@test "pacman: q, EOF and --dry-run never touch the real sync database" {
|
|
use_fakes pacman
|
|
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
run skup <<<'q'
|
|
assert_success
|
|
run skup <<<'1?'
|
|
assert_failure 2
|
|
run skup --dry-run <<<''
|
|
assert_success
|
|
assert_line 'Checking the reviewed transaction (pacman -Sup)... ok.'
|
|
assert_line --regexp "^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db .*$REAL_SYNC_RE/\$"
|
|
assert_line '+ pacman -Su --noconfirm'
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
|
log_lacks '^pacman -Su '
|
|
run grep -E ' real$' "$FAKE_DIR/pacman-dbpath"
|
|
refute_output --regexp '^-S(y|u|yu) '
|
|
run ls /tmp
|
|
refute_output --partial 'skeptical-updater-db.'
|
|
}
|
|
|
|
@test "pacman: <enter> checks the reviewed transaction, installs the reviewed .db and .db.sig, then runs -Su" {
|
|
use_fakes pacman
|
|
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
FAKE_PACMAN_SIG=1 run skup <<<$'1-\n3-\n'
|
|
assert_success
|
|
# the order: sync the throwaway db, pre-check against it, -Su on the installed copy
|
|
run grep -nE '^pacman (-Sy|-Sup|-Su|--dbpath)|^real sync' "$FAKE_LOG"
|
|
assert_output --regexp 'pacman -Sy --dbpath /tmp/skeptical-updater-db\.'
|
|
assert_output --regexp 'pacman -Sup --noconfirm --ignore su-dummy-a,su-dummy-c --dbpath /tmp/skeptical-updater-db\.[^ ]+ --print-format %n %v'
|
|
log_has 'pacman -Su --noconfirm --ignore su-dummy-a,su-dummy-c'
|
|
log_has 'real sync at -Su: reviewed 1 / signature 1'
|
|
log_lacks '^pacman -Syu'
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == "reviewed 1" ]]
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig") == "signature 1" ]]
|
|
run ls -A "$FAKE_PACMAN_DBPATH/sync"
|
|
assert_output "$(printf '%s\n' su-dummy.db su-dummy.db.sig)"
|
|
}
|
|
|
|
@test "pacman: a failing pre-check (ignored-dependency trap) changes nothing and exits with its status" {
|
|
use_fakes pacman
|
|
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
FAKE_PACMAN_SUP_RC=1 run skup <<<$'2-\n'
|
|
assert_failure 1
|
|
assert_line 'Checking the reviewed transaction (pacman -Sup)... FAILED.'
|
|
assert_output --partial ":: unable to satisfy dependency 'su-dummy-b>=2.0' required by su-dummy-a"
|
|
assert_output --partial 'the reviewed upgrade cannot be done (pacman -Sup exited 1): nothing was changed'
|
|
assert_output --partial 'hint: pacman aborts the whole transaction under --noconfirm'
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
|
log_lacks '^pacman -Su '
|
|
log_lacks '^real sync'
|
|
}
|
|
|
|
@test "pacman: a failing -Su after the databases were installed warns how to finish" {
|
|
use_fakes pacman
|
|
FAKE_PACMAN_SU_RC=1 run skup <<<''
|
|
assert_failure 1
|
|
assert_output --partial "the system's pacman sync database now holds the reviewed metadata but 'pacman -Su' did not complete: it is newer than the installed packages. Finish the upgrade with 'pacman -Su' (or run skeptical-updater again)"
|
|
[[ $(grep -c "did not complete" <<<"$output") -eq 1 ]]
|
|
}
|
|
|
|
@test "pacman: an existing db.lck (another pacman) changes nothing and is left alone" {
|
|
use_fakes pacman
|
|
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
: >"$FAKE_PACMAN_DBPATH/db.lck"
|
|
run skup <<<''
|
|
assert_failure 1
|
|
assert_output --partial "the pacman database is locked by another process ($FAKE_PACMAN_DBPATH/db.lck); nothing was changed"
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
|
[[ -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
|
log_lacks '^pacman -Su '
|
|
}
|
|
|
|
@test "pacman: db.lck is ours during the install and gone before -Su runs" {
|
|
use_fakes pacman
|
|
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
touch -d '2 hours ago' "$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
run skup <<<''
|
|
assert_success
|
|
log_has 'db.lck at -Su: no'
|
|
[[ ! -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
|
}
|
|
|
|
@test "pacman: a system database that changed since the review and is newer: refused (exit 2), nothing changed" {
|
|
use_fakes pacman
|
|
echo "synced elsewhere" >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
touch -d '+1 hour' "$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
run skup <<<''
|
|
assert_failure 2
|
|
assert_output --partial 'package databases changed since the review (su-dummy); run skeptical-updater again'
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == "synced elsewhere" ]]
|
|
[[ ! -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
|
log_lacks '^pacman -Su '
|
|
}
|
|
|
|
@test "pacman: an identical system database is not copied again" {
|
|
use_fakes pacman
|
|
echo "reviewed 1" >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
touch -d '2 hours ago' "$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
before=$(stat -c %Y "$FAKE_PACMAN_DBPATH/sync/su-dummy.db")
|
|
run skup <<<''
|
|
assert_success
|
|
assert_line "The system's package databases are the reviewed ones already."
|
|
refute_line --regexp '^\+ cp '
|
|
[[ $(stat -c %Y "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == "$before" ]]
|
|
log_has 'pacman -Su --noconfirm'
|
|
}
|
|
|
|
@test "pacman: the install is all-or-nothing: a failed copy renames nothing" {
|
|
use_fakes pacman
|
|
echo old >"$FAKE_PACMAN_DBPATH/sync/su-dummy.db"
|
|
rm -f "$SYSBIN/cp"
|
|
FAKE_PACMAN_SIG=1 run skup <<<''
|
|
assert_failure 1
|
|
assert_output --partial 'cannot copy the reviewed package databases into'
|
|
[[ $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db") == old ]]
|
|
[[ ! -e $FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig ]]
|
|
run ls -A "$FAKE_PACMAN_DBPATH/sync"
|
|
assert_output 'su-dummy.db'
|
|
[[ ! -e $FAKE_PACMAN_DBPATH/db.lck ]]
|
|
log_lacks '^pacman -Su '
|
|
}
|
|
|
|
@test "pacman: Ctrl-C during -Su after the install: the exit handler says how to finish" {
|
|
use_fakes pacman
|
|
FAKE_PACMAN_SU_KILL=INT run skup <<<''
|
|
assert_failure 130
|
|
assert_output --partial "the system's pacman sync database now holds the reviewed metadata but 'pacman -Su' did not complete"
|
|
assert_output --partial "Finish the upgrade with 'pacman -Su'"
|
|
}
|
|
|
|
@test "pacman: errors pacman prints about the throwaway database are shown in the listing" {
|
|
use_fakes pacman
|
|
FAKE_PACMAN_STDERR="error: could not open database su-dummy: invalid or corrupted database (PGP signature)" run skup list
|
|
assert_success
|
|
assert_output --partial 'warning: pacman: error: could not open database su-dummy: invalid or corrupted database (PGP signature)'
|
|
FAKE_PACMAN_STDERR="warning: database file for 'core' does not exist (use '-Sy' to download)" run skup list
|
|
refute_output --partial 'warning: pacman:'
|
|
}
|
|
|
|
@test "pacman: --no-refresh (or a failed refresh) upgrades with plain -Su" {
|
|
use_fakes pacman
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
log_has 'pacman -Su --noconfirm'
|
|
log_lacks '^pacman -Sup --noconfirm'
|
|
: >"$FAKE_LOG"
|
|
FAKE_PACMAN_SY_RC=1 run skup <<<''
|
|
assert_success
|
|
assert_line 'Refreshing package lists... FAILED.'
|
|
log_has 'pacman -Su --noconfirm'
|
|
log_lacks '^pacman -Sup --noconfirm'
|
|
}
|
|
|
|
@test "pacman: a throwaway database that cannot be set up is reported; no refresh, plain -Su" {
|
|
use_fakes pacman
|
|
rm -f "$SYSBIN/ln"
|
|
run skup <<<''
|
|
assert_success
|
|
assert_output --partial 'cannot set up the temporary pacman database /tmp/skeptical-updater-db.'
|
|
assert_output --partial '(chmod/ln/mkdir failed): not refreshing'
|
|
log_lacks '^pacman -Sy '
|
|
log_has 'pacman -Su --noconfirm'
|
|
}
|
|
|
|
@test "pacman: throwaway databases of dead runs are removed at the start of a root run, a live run's stays" {
|
|
use_fakes pacman
|
|
sleep 0 &
|
|
dead=$!
|
|
wait "$dead"
|
|
mkdir -p "/tmp/skeptical-updater-db.$dead.Stale1/sync" "/tmp/skeptical-updater-db.$$.Live01"
|
|
ln -s "$FAKE_PACMAN_DBPATH/local" "/tmp/skeptical-updater-db.$dead.Stale1/local"
|
|
run skup list --no-refresh
|
|
assert_success
|
|
assert_line "Removed the temporary pacman database of an interrupted run (/tmp/skeptical-updater-db.$dead.Stale1)."
|
|
[[ ! -e /tmp/skeptical-updater-db.$dead.Stale1 ]]
|
|
[[ -d /tmp/skeptical-updater-db.$$.Live01 ]]
|
|
[[ -d $FAKE_PACMAN_DBPATH/local ]] # the symlink target is untouched
|
|
rm -rf "/tmp/skeptical-updater-db.$$.Live01"
|
|
SKEPTICAL_UPDATER_EUID=1000 run skup list --no-refresh
|
|
refute_output --partial 'Removed the temporary pacman database'
|
|
}
|
|
|
|
@test "pacman: Nx on a package others depend on is refused (Required By), nothing removed" {
|
|
use_fakes pacman
|
|
run skup --no-refresh <<<$'2x\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks '^pacman -R'
|
|
}
|
|
|
|
@test "pacman: purge plan from -Rs --print (no cascade), purge with -Rns" {
|
|
use_fakes pacman
|
|
run skup --no-refresh <<<$'1x\ny\nq'
|
|
assert_success
|
|
log_has 'pacman -Rs --print --print-format %n -- su-dummy-a'
|
|
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
|
log_has 'pacman -Rns --noconfirm -- su-dummy-a su-dummy-b'
|
|
log_lacks 'Rsc|Rnsc'
|
|
}
|
|
|
|
@test "pacman: when pacman itself refuses (breaks dependency) its message is shown and nothing is asked" {
|
|
use_fakes pacman
|
|
FAKE_PACMAN_RS_REFUSE=1 run skup --no-refresh <<<$'3x\nq'
|
|
assert_success
|
|
assert_output --partial ":: removing su-dummy-c breaks dependency 'su-dummy-c' required by su-dummy-a"
|
|
assert_output --partial 'cannot purge su-dummy-c: pacman refuses (see above); nothing was removed'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks '^pacman -Rns'
|
|
}
|
|
|
|
@test "pacman: belt and braces - a plan with a non-dependency is refused" {
|
|
use_fakes pacman
|
|
FAKE_PACMAN_RS_EXTRA=su-dummy-c:expat run skup --no-refresh <<<$'3x\nq'
|
|
assert_success
|
|
assert_output --partial 'cannot purge su-dummy-c: pacman would also remove expat, which su-dummy-c does not depend on'
|
|
log_lacks '^pacman -Rns'
|
|
}
|
|
@test "pacman: a failed upgrade with exclusions explains the --noconfirm dependency trap" {
|
|
use_fakes pacman
|
|
FAKE_PACMAN_SU_RC=1 run skup --no-refresh <<<$'2-\n'
|
|
assert_failure 1
|
|
assert_output --partial 'hint: pacman aborts the whole transaction under --noconfirm when an ignored package is needed at a newer version'
|
|
}
|
|
|
|
@test "pacman: IgnorePkg rows ([ignored]) are not added to --ignore (pacman applies them itself)" {
|
|
use_fakes pacman
|
|
FAKE_PACMAN_QU=real run skup --no-refresh <<<''
|
|
assert_success
|
|
log_has 'pacman -Su --noconfirm'
|
|
}
|
|
|
|
# ---- dnf ----------------------------------------------------------------------------------------
|
|
|
|
@test "dnf: refresh is dnf -q --refresh --setopt=skip_if_unavailable=0 makecache" {
|
|
use_fakes dnf rpm
|
|
run skup list
|
|
assert_success
|
|
log_has 'dnf -q --refresh --setopt=skip_if_unavailable=0 makecache'
|
|
}
|
|
|
|
@test "dnf: a failed refresh warns and goes on" {
|
|
use_fakes dnf rpm
|
|
FAKE_DNF_MAKECACHE_RC=1 run skup list
|
|
assert_success
|
|
assert_line 'Refreshing package lists... FAILED.'
|
|
assert_output --partial 'warning: metadata refresh failed'
|
|
}
|
|
|
|
@test "dnf: exclusions are --exclude=a,b; the reviewed metadata is frozen (*.metadata_expire=-1)" {
|
|
use_fakes dnf rpm
|
|
run skup --no-refresh <<<$'3-\n4-\n'
|
|
assert_success
|
|
assert_line 'Excluded from this run: su-dummy-a su-dummy-b'
|
|
log_has "$DNF_UPGRADE --exclude=su-dummy-a,su-dummy-b"
|
|
assert_line "+ dnf -y --setopt=skip_if_unavailable=0 --setopt=\\*.metadata_expire=-1 upgrade --exclude=su-dummy-a,su-dummy-b"
|
|
}
|
|
|
|
@test "dnf: no exclusion => no --exclude" {
|
|
use_fakes dnf rpm
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
log_has "$DNF_UPGRADE"
|
|
}
|
|
|
|
@test "dnf: Nx on a package others depend on is refused (rpm reverse dependencies), nothing removed" {
|
|
use_fakes dnf rpm
|
|
export FAKE_RPM_NO_D=1
|
|
run skup --no-refresh <<<$'4x\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks 'remove'
|
|
}
|
|
|
|
@test "dnf: purge plan from dnf --assumeno remove (dnf4 table, unused dependencies), then dnf -y remove" {
|
|
use_fakes dnf rpm
|
|
export FAKE_RPM_NO_D=1
|
|
FAKE_DNF_UNUSED=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'3x\ny\nl\nq'
|
|
assert_success
|
|
log_has 'dnf --assumeno remove su-dummy-a'
|
|
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
|
log_has 'dnf -y remove su-dummy-a su-dummy-b'
|
|
[[ $(grep -c '\[4\] su-dummy-b' <<<"$output") -eq 1 ]]
|
|
[[ $(grep -c '\[3\] su-dummy-a' <<<"$output") -eq 1 ]]
|
|
}
|
|
|
|
@test "dnf: belt and braces - a removal table with a non-dependency is refused" {
|
|
use_fakes dnf rpm
|
|
export FAKE_RPM_NO_D=1
|
|
FAKE_DNF_DEPENDENT=su-dummy-c:bash run skup --no-refresh <<<$'5x\nq'
|
|
assert_success
|
|
assert_output --partial 'cannot purge su-dummy-c: dnf would also remove bash, which su-dummy-c does not depend on'
|
|
log_lacks '^dnf -y remove'
|
|
}
|
|
@test "dnf5: the purge plan table is parsed as well" {
|
|
use_fakes dnf rpm
|
|
export FAKE_RPM_NO_D=1
|
|
FAKE_DNF_MAJOR=5 FAKE_DNF_UNUSED=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'3x\ny\nq'
|
|
assert_success
|
|
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
|
log_has 'dnf -y remove su-dummy-a su-dummy-b'
|
|
}
|
|
@test "dnf: a refused removal (protected package) asks nothing and removes nothing" {
|
|
use_fakes dnf rpm
|
|
export FAKE_RPM_NO_D=1
|
|
FAKE_DNF_REMOVE_REFUSE=1 run skup --no-refresh <<<$'5x\nq'
|
|
assert_success
|
|
assert_output --partial 'Error: Problem: The operation would result in removing the following protected packages: su-dummy-c'
|
|
assert_output --partial 'cannot purge su-dummy-c: dnf refuses (see above); nothing was removed'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks '^dnf -y remove'
|
|
}
|
|
|
|
@test "dnf: a failed upgrade with exclusions explains the dnf4 excluded-dependency rule" {
|
|
use_fakes dnf rpm
|
|
FAKE_DNF_UPGRADE_RC=1 run skup --no-refresh <<<$'4-\n'
|
|
assert_failure 1
|
|
assert_output --partial "hint: dnf4 refuses to upgrade a package whose required newer dependency is excluded (exit 1)"
|
|
}
|
|
|
|
@test "yum fallback: the same commands through yum" {
|
|
use_fakes yum rpm
|
|
run skup <<<$'4-\n'
|
|
assert_success
|
|
log_has 'yum -q --refresh --setopt=skip_if_unavailable=0 makecache'
|
|
log_has 'yum -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade --exclude=su-dummy-b'
|
|
}
|
|
|
|
# ---- zypper -------------------------------------------------------------------------------------
|
|
|
|
zypp_conf() { # a "real" zypp.conf + locks file with one user lock, in the test dir
|
|
mkdir -p "$BATS_TEST_TMPDIR/etc"
|
|
printf '[main]\n# locksfile.path = /etc/zypp/locks\nlocksfile.path = %s\n' "$BATS_TEST_TMPDIR/etc/locks" >"$BATS_TEST_TMPDIR/etc/zypp.conf"
|
|
printf '\ntype: package\nmatch_type: glob\ncase_sensitive: on\nsolvable_name: su-dummy-c\ncomment: user lock\n\n' >"$BATS_TEST_TMPDIR/etc/locks"
|
|
export ZYPP_CONF=$BATS_TEST_TMPDIR/etc/zypp.conf
|
|
}
|
|
|
|
@test "zypper: refresh is zypper --non-interactive --quiet refresh" {
|
|
use_fakes zypper rpm
|
|
run skup list
|
|
assert_success
|
|
log_has 'zypper --non-interactive --quiet refresh'
|
|
}
|
|
|
|
@test "zypper: a failed refresh (exit 4) warns and goes on" {
|
|
use_fakes zypper rpm
|
|
FAKE_ZYPPER_REFRESH_RC=4 run skup list
|
|
assert_success
|
|
assert_output --partial 'Could not refresh the repositories because of errors.'
|
|
assert_output --partial 'exited 4); continuing with the cached package lists'
|
|
}
|
|
|
|
@test "zypper: Leap (ID=opensuse-leap) upgrades with update --no-refresh" {
|
|
use_fakes zypper rpm
|
|
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
log_has 'zypper --non-interactive --no-refresh update'
|
|
}
|
|
|
|
@test "zypper: Tumbleweed (ID=opensuse-tumbleweed) upgrades with dist-upgrade --no-refresh" {
|
|
use_fakes zypper rpm
|
|
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
log_has 'zypper --non-interactive --no-refresh dist-upgrade'
|
|
}
|
|
|
|
@test "zypper: exclusions are locks in a private ZYPP_CONF seeded with the real locks; the real lock file is untouched" {
|
|
use_fakes zypper rpm
|
|
zypp_conf
|
|
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
|
|
before=$(cksum <"$BATS_TEST_TMPDIR/etc/locks")
|
|
run skup --no-refresh <<<$'2-\n'
|
|
assert_success
|
|
run grep -E '^zypper ZYPP_CONF=.*/skeptical-updater\.' "$FAKE_LOG"
|
|
assert_line --index 0 --regexp "^zypper ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater\.[A-Za-z0-9]+/zypp\.conf --non-interactive --quiet addlock su-dummy-b\$"
|
|
assert_line --index 1 --regexp "^zypper ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater\.[A-Za-z0-9]+/zypp\.conf --non-interactive --no-refresh update\$"
|
|
# the private lock file seen by the upgrade: the user's lock + ours
|
|
run sed -n '/^--- locks seen by update:/,/^--- end/p' "$FAKE_LOG"
|
|
assert_line 'solvable_name: su-dummy-c'
|
|
assert_line 'comment: user lock'
|
|
assert_line 'solvable_name: su-dummy-b'
|
|
# real locks unchanged, private directory and its marker removed
|
|
[[ $(cksum <"$BATS_TEST_TMPDIR/etc/locks") == "$before" ]]
|
|
run grep -c 'solvable_name' "$BATS_TEST_TMPDIR/etc/locks"
|
|
assert_output 1
|
|
run ls "$BATS_TEST_TMPDIR"
|
|
refute_output --partial 'skeptical-updater.'
|
|
[[ ! -e $STATE/zypper-private ]]
|
|
}
|
|
|
|
@test "zypper: commented locksfile.path lines are ignored; the private one (last [main]) is used" {
|
|
use_fakes zypper rpm
|
|
zypp_conf
|
|
run skup --no-refresh <<<$'1-\n'
|
|
assert_success
|
|
run sed -n '/^--- locks seen by/,/^--- end/p' "$FAKE_LOG"
|
|
assert_line 'comment: user lock'
|
|
assert_line 'solvable_name: su-dummy-a'
|
|
}
|
|
|
|
@test "zypper: zypp.conf.d drop-ins are part of the private config; a drop-in locksfile.path is honoured" {
|
|
use_fakes zypper rpm
|
|
r=$BATS_TEST_TMPDIR/root
|
|
mkdir -p "$r/usr/etc/zypp/zypp.conf.d" "$r/etc/zypp/zypp.conf.d"
|
|
printf '[main]\n# vendor file\n' >"$r/usr/etc/zypp/zypp.conf"
|
|
printf '[main]\nlocksfile.path = /nonexistent/vendor-dropin-locks\n' >"$r/usr/etc/zypp/zypp.conf.d/10-vendor.conf"
|
|
printf '[main]\nsolver.onlyRequires = true\nlocksfile.path = %s\n' "$r/admin-locks" >"$r/etc/zypp/zypp.conf.d/50-admin.conf"
|
|
printf '\ntype: package\nmatch_type: glob\ncase_sensitive: on\nsolvable_name: su-dummy-c\ncomment: drop-in lock\n\n' >"$r/admin-locks"
|
|
export SKEPTICAL_UPDATER_ZYPP_ROOT=$r
|
|
run skup --no-refresh <<<$'1-\n'
|
|
assert_success
|
|
run sed -n '/^--- locks seen by/,/^--- end/p' "$FAKE_LOG"
|
|
assert_line 'comment: drop-in lock'
|
|
assert_line 'solvable_name: su-dummy-a'
|
|
[[ $(grep -c solvable_name "$r/admin-locks") -eq 1 ]]
|
|
}
|
|
|
|
@test "zypper: with a drop-in, the dry run names every file of the private configuration" {
|
|
use_fakes zypper rpm
|
|
r=$BATS_TEST_TMPDIR/root
|
|
mkdir -p "$r/etc/zypp/zypp.conf.d"
|
|
printf '[main]\n' >"$r/etc/zypp/zypp.conf"
|
|
printf '[main]\nlocksfile.path = %s\n' "$r/l" >"$r/etc/zypp/zypp.conf.d/50-admin.conf"
|
|
SKEPTICAL_UPDATER_ZYPP_ROOT=$r run skup --dry-run --no-refresh <<<$'1-\n'
|
|
assert_success
|
|
assert_line --regexp "^\+ # private .*/zypp\.conf: $r/etc/zypp/zypp\.conf \+ $r/etc/zypp/zypp\.conf\.d/50-admin\.conf with locksfile\.path = .*/locks \(a copy of $r/l\)\$"
|
|
}
|
|
|
|
@test "zypper: no exclusion => plain zypper --non-interactive --no-refresh VERB, no private config" {
|
|
use_fakes zypper rpm
|
|
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
|
|
run skup --no-refresh <<<''
|
|
assert_success
|
|
log_lacks 'ZYPP_CONF='
|
|
log_lacks 'addlock'
|
|
}
|
|
|
|
@test "zypper: Nx on a package others depend on is refused (rpm reverse dependencies), nothing removed" {
|
|
use_fakes zypper rpm
|
|
export FAKE_RPM_NO_D=1
|
|
run skup --no-refresh <<<$'2x\nq'
|
|
assert_success
|
|
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
|
|
refute_output --partial '[y/N]'
|
|
log_lacks 'remove'
|
|
}
|
|
|
|
@test "zypper: purge plan from remove --dry-run --xmlout (--clean-deps), then remove --clean-deps" {
|
|
use_fakes zypper rpm
|
|
export FAKE_RPM_NO_D=1
|
|
FAKE_ZYPPER_CLEAN=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nq'
|
|
assert_success
|
|
log_has 'zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps su-dummy-a'
|
|
assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]'
|
|
log_has 'zypper --non-interactive --no-refresh remove --clean-deps su-dummy-a su-dummy-b'
|
|
}
|
|
|
|
@test "zypper: belt and braces - a removal plan with a non-dependency is refused" {
|
|
use_fakes zypper rpm
|
|
export FAKE_RPM_NO_D=1
|
|
FAKE_ZYPPER_EXTRA=su-dummy-c:bash run skup --no-refresh <<<$'3x\nq'
|
|
assert_success
|
|
assert_output --partial 'cannot purge su-dummy-c: zypper would also remove bash, which su-dummy-c does not depend on'
|
|
log_lacks 'remove --clean-deps su-dummy-c'
|
|
}
|
|
@test "zypper: a package zypper does not know (exit 104) is not purged, nothing is asked" {
|
|
use_fakes zypper rpm
|
|
FAKE_ZYPPER_LU=real run skup --no-refresh <<<$'1x\nq'
|
|
assert_success
|
|
assert_output --partial "zypper: no installed package provides 'openSUSE-build-key' (exit 104)"
|
|
assert_output --partial 'cannot purge openSUSE-build-key: zypper refuses (see above); nothing was removed'
|
|
refute_output --partial '[y/N]'
|
|
}
|
|
|
|
@test "zypper: exit 4 from the upgrade is passed through, with a hint" {
|
|
use_fakes zypper rpm
|
|
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
|
FAKE_ZYPPER_UPGRADE_RC=4 run skup --no-refresh <<<''
|
|
assert_failure 4
|
|
assert_output --partial "hint: zypper hit a dependency/vendor problem that --non-interactive cancels (exit 4); run 'zypper dist-upgrade' by hand"
|
|
}
|
|
|
|
@test "zypper: an unreadable zypp.conf with exclusions is an error, nothing is upgraded" {
|
|
use_fakes zypper rpm
|
|
export ZYPP_CONF=$BATS_TEST_TMPDIR/missing.conf
|
|
run skup --no-refresh <<<$'1-\n'
|
|
assert_failure 1
|
|
assert_output --partial 'cannot read the zypp configuration'
|
|
log_lacks ' (update|dist-upgrade)$'
|
|
}
|
|
|
|
@test "zypper: a private configuration left by an interrupted run is removed at the next start" {
|
|
use_fakes zypper rpm
|
|
mkdir -p "$STATE" "$BATS_TEST_TMPDIR/skeptical-updater.Leftov"
|
|
: >"$BATS_TEST_TMPDIR/skeptical-updater.Leftov/zypp.conf"
|
|
echo "$BATS_TEST_TMPDIR/skeptical-updater.Leftov" >"$STATE/zypper-private"
|
|
run skup list --no-refresh
|
|
assert_success
|
|
assert_line "Removed the private zypper configuration of an interrupted run ($BATS_TEST_TMPDIR/skeptical-updater.Leftov)."
|
|
[[ ! -e $BATS_TEST_TMPDIR/skeptical-updater.Leftov ]]
|
|
[[ ! -e $STATE/zypper-private ]]
|
|
}
|
|
|
|
@test "zypper: the replay leaves a running upgrade's private configuration alone" {
|
|
use_fakes zypper rpm
|
|
mkdir -p "$STATE" "$BATS_TEST_TMPDIR/skeptical-updater.Running"
|
|
echo "$BATS_TEST_TMPDIR/skeptical-updater.Running" >"$STATE/zypper-private"
|
|
exec 9>>"$STATE/zypper.lock"
|
|
flock 9
|
|
run skup info su-dummy-a
|
|
exec 9>&-
|
|
assert_success
|
|
assert_line 'Note: another skeptical-updater run is using its private zypper configuration; leaving it alone.'
|
|
[[ -d $BATS_TEST_TMPDIR/skeptical-updater.Running ]]
|
|
[[ -e $STATE/zypper-private ]]
|
|
}
|
|
|
|
@test "zypper: the upgrade holds zypper.lock while its private configuration exists" {
|
|
use_fakes zypper rpm
|
|
zypp_conf
|
|
run skup --no-refresh <<<$'1-\n'
|
|
assert_success
|
|
log_has 'zypper.lock during update: held'
|
|
flock -n "$STATE/zypper.lock" true
|
|
}
|
|
|
|
@test "zypper: the private configuration is removed when the upgrade is killed" {
|
|
use_fakes zypper rpm
|
|
zypp_conf
|
|
# an upgrade that is killed (SIGTERM to the applet) in the middle
|
|
FAKE_ZYPPER_UPGRADE_KILL=TERM run skup --no-refresh <<<$'1-\n'
|
|
assert_failure 143
|
|
run ls "$BATS_TEST_TMPDIR"
|
|
refute_output --partial 'skeptical-updater.'
|
|
[[ ! -e $STATE/zypper-private ]]
|
|
}
|