#!/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: 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 ]] }