first commit

This commit is contained in:
bryanpedini committed 2026-10-10 10:37:53 +02:00
commit 1354db6c39
90 files changed
+13123

No files matched your search

+787
View File
@@ -0,0 +1,787 @@
#!/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 ]]
}