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 ]]
}
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# CLI parsing and exit codes.
load test_helper
setup() { common_setup; }
@test "--help prints the usage and exits 0" {
run skup --help
assert_success
assert_line 'usage: skeptical-updater [options] [command]'
assert_line --partial 'refresh recompute dependency closures for every '"'user'"' root'
assert_line --partial '--dry-run print native commands instead of running mutating ones'
assert_line --partial 'exit status: 0 ok, 1 usage/backend error, 2 aborted'
[[ -z $(cat "$FAKE_LOG") ]]
}
@test "-h is --help, even after a command" {
run skup list -h
assert_success
assert_line 'usage: skeptical-updater [options] [command]'
}
@test "--version and -V print the version" {
run skup --version
assert_success
assert_output 'skeptical-updater 0.1.0'
run skup -V
assert_output 'skeptical-updater 0.1.0'
}
@test "an unknown option is a usage error (exit 1)" {
run skup --frobnicate
assert_failure 1
assert_line 'skeptical-updater: error: unknown option: --frobnicate'
assert_line "Try 'skeptical-updater --help'."
}
@test "an unknown command is a usage error (exit 1)" {
run skup upgrade
assert_failure 1
assert_line 'skeptical-updater: error: unknown command: upgrade'
}
@test "list, trusted and refresh take no arguments" {
for c in list trusted refresh; do
run skup "$c" extra
assert_failure 1
assert_output --partial "'$c' takes no arguments"
done
}
@test "the interactive mode takes no arguments either" {
run skup '' foo
assert_failure 1
}
@test "trust and untrust need at least one package" {
run skup trust
assert_failure 1
assert_output --partial "'trust' needs at least one package name"
run skup untrust
assert_failure 1
assert_output --partial "'untrust' needs at least one package name"
}
@test "info and show need exactly one package" {
run skup info
assert_failure 1
assert_output --partial "'info' needs exactly one package name"
run skup show a b
assert_failure 1
assert_output --partial "'show' needs exactly one package name"
}
@test "invalid package names are rejected before anything runs" {
# shellcheck disable=SC2016 # literal $(id) on purpose
for bad in '-rf' 'a b' 'foo;rm' '../x' 'x/y' '$(id)'; do
run skup trust -- "$bad"
assert_failure 1
assert_output --partial "invalid package name: '$bad'"
done
[[ -z $(cat "$FAKE_LOG") ]]
[[ ! -e $STATE ]]
}
@test "valid package names with + . : ~ @ _ are accepted" {
run skup --backend=apt trust 'libstdc++6' 'libc6:i386' 'g++-12' 'a.b~c' 'x_y@z'
assert_success
run cut -f2 "$STATE_FILE"
assert_output "$(printf '%s\n' "$HEADER" a.b~c g++-12 libc6:i386 libstdc++6 x_y@z)"
}
@test "--backend must name a known backend" {
run skup --backend=portage list
assert_failure 1
assert_output --partial "unknown backend 'portage' (expected apt, dnf, pacman or zypper)"
}
@test "--backend and --state-dir without a value are usage errors" {
run skup list --backend
assert_failure 1
assert_output --partial '--backend needs a value'
run skup list --state-dir
assert_failure 1
assert_output --partial '--state-dir needs a value'
run skup list --state-dir=
assert_failure 1
assert_output --partial 'empty state directory'
}
@test "options may follow the command; --backend NAME as two words" {
run skup list --backend apt --no-refresh
assert_success
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
log_lacks '^apt-get update'
}
@test "--state-dir overrides SKEPTICAL_UPDATER_STATE_DIR" {
run skup --state-dir="$BATS_TEST_TMPDIR/other" trust su-dummy-c
assert_success
[[ -f $BATS_TEST_TMPDIR/other/trusted.tsv ]]
[[ ! -e $STATE ]]
}
@test "-- ends option parsing" {
run skup info -- --all
assert_failure 1
assert_output --partial "invalid package name: '--all'"
}
@test "no ANSI colours when stdout is not a terminal" {
run skup list --no-refresh
assert_success
refute_output --partial $'\e['
}
@test "--no-color and NO_COLOR are accepted" {
run skup --no-color list --no-refresh
assert_success
NO_COLOR=1 run skup list --no-refresh
assert_success
refute_output --partial $'\e['
}
+1
View File
@@ -0,0 +1 @@
su-dummy-orphan
+13
View File
@@ -0,0 +1,13 @@
ii su-dummy-a su-dummy-b
ii su-dummy-b
ii su-dummy-c
ii su-dummy-w su-virtual | su-dummy-alt
ii su-dummy-v su-virtual
rc su-dummy-f su-dummy-c
ii base-files awk
ii mawk libc6 (>= 2.29) awk
ii libc6 libgcc-s1
ii libgcc-s1 gcc-12-base (= 12.2.0-14), libc6 (>= 2.35)
ii gcc-12-base
ii su-dummy-orphan
ii su-dummy-c-data
1 ii su-dummy-a su-dummy-b
2 ii su-dummy-b
3 ii su-dummy-c
4 ii su-dummy-w su-virtual | su-dummy-alt
5 ii su-dummy-v su-virtual
6 rc su-dummy-f su-dummy-c
7 ii base-files awk
8 ii mawk libc6 (>= 2.29) awk
9 ii libc6 libgcc-s1
10 ii libgcc-s1 gcc-12-base (= 12.2.0-14), libc6 (>= 2.35)
11 ii gcc-12-base
12 ii su-dummy-orphan
13 ii su-dummy-c-data
+25
View File
@@ -0,0 +1,25 @@
perl-base:
Installed: 5.36.0-7+deb12u3
Candidate: 5.36.0-7+deb12u4
Version table:
5.36.0-7+deb12u4 500
500 http://deb.debian.org/debian-security bookworm-security/main amd64 Packages
*** 5.36.0-7+deb12u3 500
500 http://deb.debian.org/debian bookworm/main amd64 Packages
100 /var/lib/dpkg/status
su-dummy-c:
Installed: 1.0
Candidate: 2.0
Version table:
2.0 500
500 file:/srv/su-repo ./ Packages
*** 1.0 100
100 /var/lib/dpkg/status
su-dummy-e:
Installed: 1.0
Candidate: 2.0
Version table:
2.0 500
500 file:/srv/su-repo ./ Packages
*** 1.0 100
100 /var/lib/dpkg/status
+1
View File
@@ -0,0 +1 @@
su-dummy-b: su-dummy-a
+3
View File
@@ -0,0 +1,3 @@
gpgv
Reverse Depends:
|apt
+4
View File
@@ -0,0 +1,4 @@
su-dummy-b
Reverse Depends:
su-dummy-a
su-dummy-a
+5
View File
@@ -0,0 +1,5 @@
Package: gpgv
Version: 2.2.40-1.1
Architecture: amd64
Description: GNU privacy guard - signature verification tool
+15
View File
@@ -0,0 +1,15 @@
Package: su-dummy-a
Version: 2.0
Section: misc
Priority: optional
Architecture: all
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
Depends: su-dummy-b (>= 2.0)
Description: dummy package su-dummy-a for skeptical-updater tests
This package only exists to exercise the package manager.
.
It ships one data file and one conffile (version 2.0).
Description-md5: 36da7dcdaf4ec207be2937d1f5dfa408
Filename: ./su-dummy-a_2.0_all.deb
Size: 840
+14
View File
@@ -0,0 +1,14 @@
Package: su-dummy-b
Version: 2.0
Section: misc
Priority: optional
Architecture: all
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
Description: dummy package su-dummy-b for skeptical-updater tests
This package only exists to exercise the package manager.
.
It ships one data file and one conffile (version 2.0).
Description-md5: 36da7dcdaf4ec207be2937d1f5dfa408
Filename: ./su-dummy-b_2.0_all.deb
Size: 840
+14
View File
@@ -0,0 +1,14 @@
Package: su-dummy-c
Version: 2.0
Section: misc
Priority: optional
Architecture: all
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
Description: dummy package su-dummy-c for skeptical-updater tests
This package only exists to exercise the package manager.
.
It ships one data file and one conffile (version 2.0).
Description-md5: 36da7dcdaf4ec207be2937d1f5dfa408
Filename: ./su-dummy-c_2.0_all.deb
Size: 840
+13
View File
@@ -0,0 +1,13 @@
Reading package lists...
Building dependency tree...
Reading state information...
Calculating upgrade...
The following packages will be upgraded:
su-dummy-a su-dummy-b su-dummy-c
3 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Inst su-dummy-b [1.0] (2.0 su-fixture:su-repo [all])
Inst su-dummy-a [1.0] (2.0 su-fixture:su-repo [all])
Inst su-dummy-c [1.0] (2.0 su-fixture:su-repo [all])
Conf su-dummy-b (2.0 su-fixture:su-repo [all])
Conf su-dummy-a (2.0 su-fixture:su-repo [all])
Conf su-dummy-c (2.0 su-fixture:su-repo [all])
+13
View File
@@ -0,0 +1,13 @@
Reading package lists...
Building dependency tree...
Reading state information...
Calculating upgrade...
The following packages have been kept back:
su-dummy-c
The following packages will be upgraded:
su-dummy-a su-dummy-b
2 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
Inst su-dummy-b [1.0] (2.0 su-fixture:su-repo [all])
Inst su-dummy-a [1.0] (2.0 su-fixture:su-repo [all])
Conf su-dummy-b (2.0 su-fixture:su-repo [all])
Conf su-dummy-a (2.0 su-fixture:su-repo [all])
+5
View File
@@ -0,0 +1,5 @@
Reading package lists...
Building dependency tree...
Reading state information...
Calculating upgrade...
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
+26
View File
@@ -0,0 +1,26 @@
NOTE: This is only a simulation!
apt-get needs root privileges for real execution.
Keep also in mind that locking is deactivated,
so don't depend on the relevance to the real current situation!
Reading package lists...
Building dependency tree...
Reading state information...
Calculating upgrade...
The following NEW packages will be installed:
su-dummy-newdep
The following packages have been kept back:
perl-base su-dummy-c su-dummy-e
The following packages will be upgraded:
libaudit-common libaudit1 libc6:i386 libssl3t64 su-dummy-m su-dummy-slow
6 upgraded, 1 newly installed, 0 to remove and 3 not upgraded.
Inst libaudit-common [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [all])
Conf libaudit-common (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [all])
Inst libaudit1 [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [amd64])
Inst libssl3t64 [3.0.13-0ubuntu3.15] (3.0.13-0ubuntu3.16 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
Inst su-dummy-newdep (1.0 su-fixture:su-repo [all])
Inst su-dummy-slow [0.9] (1.0 [all])
Inst su-dummy-m [1.0] (2.0 su-fixture:su-repo [amd64]) [su-dummy-m:amd64 on su-dummy-m:i386] [su-dummy-m:i386 ]
Inst libc6:i386 [2.36-9] (2.36-9+deb12u10 Debian-Security:12/oldstable-security, Debian:12.12/oldstable [i386]) []
Remv su-dummy-f [1.0]
Conf libaudit1 (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [amd64])
Conf libssl3t64 (3.0.13-0ubuntu3.16 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
+10
View File
@@ -0,0 +1,10 @@
gnutls.x86_64 3.8.10-9.el9_8 baseos
kernel-core.x86_64 5.14.0-612.1.el9 baseos
su-dummy-a.noarch 2.0-1 su-dummy
su-dummy-b.noarch 2.0-1 su-dummy
su-dummy-c.x86_64 2.0-1 su-dummy
vim-minimal.x86_64 2:8.2.2637-26.el9_8.23 baseos
Obsoleting Packages
su-dummy-new.noarch 1.0-1 su-dummy
su-dummy-old.noarch 1.0-1 @System
@@ -0,0 +1,6 @@
Upgrades
ca-certificates.noarch 2026.2.90_v9.0.317-1.fc44 updates
curl.x86_64 8.18.0-12.fc44 updates
su-dummy-a.noarch 2.0-1 su-dummy
su-dummy-b.noarch 2.0-1 su-dummy
su-dummy-c.x86_64 2.0-1 su-dummy
@@ -0,0 +1,4 @@
Obsoleting Packages
su-dummy-new.noarch 1.0-1 su-dummy
su-dummy-old.noarch 1.0-1 @System
+28
View File
@@ -0,0 +1,28 @@
Installed Packages
Name : su-dummy-a
Version : 1.0
Release : 1
Architecture : noarch
Size : 15
Source : su-dummy-a-1.0-1.src.rpm
Repository : @System
Summary : skeptical-updater dummy su-dummy-a (v1.0)
License : MIT
Description : Dummy package su-dummy-a version 1.0 for skeptical-updater tests.
:
: Second paragraph of su-dummy-a 1.0 (blank line above on purpose).
Available Upgrades
Name : su-dummy-a
Version : 2.0
Release : 1
Architecture : noarch
Size : 6.3 k
Source : su-dummy-a-2.0-1.src.rpm
Repository : su-dummy
Summary : skeptical-updater dummy su-dummy-a (v2.0)
License : MIT
Description : Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
:
: Second paragraph of su-dummy-a 2.0 (blank line above on purpose).
+5
View File
@@ -0,0 +1,5 @@
NAME="AlmaLinux"
VERSION="9.8 (Olive Jaguar)"
ID="almalinux"
ID_LIKE="rhel centos fedora"
PRETTY_NAME="AlmaLinux 9.8 (Olive Jaguar)"
+4
View File
@@ -0,0 +1,4 @@
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
+4
View File
@@ -0,0 +1,4 @@
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
ID=debian
+6
View File
@@ -0,0 +1,6 @@
NAME="openSUSE Leap"
VERSION="16.0"
ID="opensuse-leap"
ID_LIKE="suse opensuse"
VERSION_ID="16.0"
PRETTY_NAME="openSUSE Leap 16.0"
+5
View File
@@ -0,0 +1,5 @@
NAME="openSUSE Tumbleweed"
ID="opensuse-tumbleweed"
ID_LIKE="opensuse suse"
VERSION_ID="20261007"
PRETTY_NAME="openSUSE Tumbleweed"
+266
View File
@@ -0,0 +1,266 @@
Name : su-dummy-a
Version : 1.0-1
Description : Skeptical-updater dummy package A
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : su-dummy-b
Optional Deps : None
Required By : None
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : su-dummy-b
Version : 1.0-1
Description : Skeptical-updater dummy package B (dependency of A)
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Required By : su-dummy-a
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Installed as a dependency for another package
Install Script : No
Validated By : None
Name : su-dummy-c
Version : 1.0-1
Description : Skeptical-updater dummy package C (independent)
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Required By : None
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : bash
Version : 5.3.20-1
Description : The GNU Bourne Again shell
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : sh
Depends On : readline libreadline.so=8-64 glibc ncurses libncursesw.so=6-64
Optional Deps : None
Required By : None
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : readline
Version : 8.3.001-1
Description : GNU readline library
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : libhistory.so=8-64 libreadline.so=8-64
Depends On : glibc ncurses libncursesw.so=6-64
Optional Deps : None
Required By : bash
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : ncurses
Version : 6.5-4
Description : System V Release 4.0 curses emulation library
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : libncurses++w.so=6-64 libncursesw.so=6-64
Depends On : glibc gcc-libs
Optional Deps : None
Required By : bash readline
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : gcc-libs
Version : 15.2.1-1
Description : Runtime libraries shipped by GCC
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : glibc>=2.27
Optional Deps : None
Required By : ncurses
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : glibc
Version : 2.42-2
Description : GNU C Library
Architecture : x86_64
URL : https://www.gnu.org/software/libc
Licenses : GPL-2.0-or-later LGPL-2.1-or-later
Groups : None
Provides : None
Depends On : linux-api-headers>=4.10 tzdata filesystem
Optional Deps : gd: for memusagestat
perl: for mtrace
Required By : bash gcc-libs ncurses
readline
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 48.71 MiB
Packager : Frederik Schwan <freswa@archlinux.org>
Build Date : Sun Sep 7 10:00:00 2026
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Installed as a dependency for another package
Install Script : Yes
Validated By : Signature
Name : linux-api-headers
Version : 6.16-1
Description : Kernel headers sanitized for use in userspace
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Required By : glibc
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : tzdata
Version : 2026a-1
Description : Sources for time zone and daylight saving time data
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Required By : glibc
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : filesystem
Version : 2025.10.12-1
Description : Base Arch Linux files
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : iana-etc
Optional Deps : None
Required By : glibc
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
Name : iana-etc
Version : 20260920-1
Description : /etc/protocols and /etc/services provided by IANA
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Required By : filesystem
Optional For : None
Conflicts With : None
Replaces : None
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Install Date : Fri Oct 9 08:00:00 2026
Install Reason : Explicitly installed
Install Script : No
Validated By : None
+3
View File
@@ -0,0 +1,3 @@
su-dummy-a 1.0-1 -> 2.0-1
su-dummy-b 1.0-1 -> 2.0-1
su-dummy-c 1.0-1 -> 2.0-1
+6
View File
@@ -0,0 +1,6 @@
ca-certificates-mozilla 3.130-1 -> 3.131-1
expat 2.8.5-1 -> 2.9.0-1 [ignored]
glib2 2.88.3-1 -> 2.90.1-1 [ignored]
su-dummy-a 1.0-1 -> 2.0-1
su-dummy-b 1.0-1 -> 2.0-1
su-dummy-c 1.0-1 -> 2.0-1
+95
View File
@@ -0,0 +1,95 @@
Repository : su-dummy
Name : su-dummy-a
Version : 2.0-1
Description : Skeptical-updater dummy package A
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : su-dummy-b
Optional Deps : None
Conflicts With : None
Replaces : None
Download Size : 380.00 B
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Validated By : None
Repository : su-dummy
Name : su-dummy-b
Version : 2.0-1
Description : Skeptical-updater dummy package B (dependency of A)
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Conflicts With : None
Replaces : None
Download Size : 380.00 B
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Validated By : None
Repository : su-dummy
Name : su-dummy-c
Version : 2.0-1
Description : Skeptical-updater dummy package C (independent)
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Conflicts With : None
Replaces : None
Download Size : 380.00 B
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Validated By : None
Repository : su-dummy
Name : su-dummy-new
Version : 1.0-1
Description : Skeptical-updater dummy package NEW
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Conflicts With : None
Replaces : None
Download Size : 380.00 B
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Validated By : None
Repository : su-dummy2
Name : su-dummy-b
Version : 3.0-1
Description : Skeptical-updater dummy package B (dependency of A)
Architecture : any
URL : https://example.invalid/su-dummy
Licenses : custom
Groups : None
Provides : None
Depends On : None
Optional Deps : None
Conflicts With : None
Replaces : None
Download Size : 380.00 B
Installed Size : 4.00 B
Packager : su-fixture <su-fixture@example.invalid>
Build Date : Tue Nov 14 22:13:20 2023
Validated By : None
+9
View File
@@ -0,0 +1,9 @@
core bash 5.3.20-1 [installed]
core ca-certificates-mozilla 3.131-1 [installed: 3.130-1]
core expat 2.9.0-1 [installed: 2.8.5-1]
core glib2 2.90.1-1 [installed: 2.88.3-1]
extra pacman-contrib 1.13.1-1
su-dummy su-dummy-a 2.0-1 [installed: 1.0-1]
su-dummy su-dummy-b 2.0-1 [installed: 1.0-1]
su-dummy su-dummy-c 2.0-1 [installed: 1.0-1]
su-dummy2 su-dummy-b 3.0-1
+61
View File
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# tests/unit/data/rpm/db.bash - the fake rpm database (sourced by fakes/rpm/rpm).
# Shaped after discovery/rpm.md sections 4-5 (EL9): the fixture packages incl. the
# SU_FIXTURE_EXTRAS su-dummy-d (file dep, versioned dep, rich dep, rpmlib()), plus a small
# glibc/bash base (REQUIRENAME values carry no version, like rpm's; rich deps are whole) whose glibc carries the "(glibc-gconv-extra ... if redhat-rpm-config)" rich dep.
# shellcheck shell=bash disable=SC2034
declare -A ARCH=(
[su-dummy-a]=noarch [su-dummy-b]=noarch [su-dummy-c]=x86_64 [su-dummy-d]=noarch
[bash]=x86_64 [glibc]=x86_64 [glibc-gconv-extra]=x86_64 [glibc-common]=x86_64
[basesystem]=noarch [setup]=noarch [filesystem]=x86_64 [ncurses-libs]=x86_64
[redhat-rpm-config]=noarch [rpm]=x86_64 [gnutls]=x86_64 [vim-minimal]=x86_64
[kernel-core]=x86_64 [ca-certificates]=noarch [curl]=x86_64
)
# installed versions ("epoch:version-release" or "version-release"); kernel-core is installonly (two versions)
declare -A EVR=(
[su-dummy-a]=1.0-1 [su-dummy-b]=1.0-1 [su-dummy-c]=1.0-1 [su-dummy-d]=1.0-1
[bash]=5.1.8-9.el9 [glibc]=2.34-275.el9_8 [glibc-gconv-extra]=2.34-275.el9_8 [glibc-common]=2.34-275.el9_8
[basesystem]=11-13.el9 [setup]=2.13.7-10.el9 [filesystem]=3.16-5.el9 [ncurses-libs]=6.2-12.20210508.el9
[redhat-rpm-config]=210-1.el9 [rpm]=4.16.1.3-40.el9 [gnutls]=3.8.10-4.el9_8 [vim-minimal]=2:8.2.2637-26.el9_8.21
[kernel-core]='5.14.0-570.1.el9 5.14.0-611.5.el9' [ca-certificates]=2025.2.80_v9.0.305-91.el9 [curl]=8.18.0-1.fc44
)
declare -A SUMMARY=(
[su-dummy-a]='skeptical-updater dummy su-dummy-a (v1.0)' [su-dummy-b]='skeptical-updater dummy su-dummy-b (v1.0)'
[su-dummy-c]='skeptical-updater dummy su-dummy-c (v1.0)' [su-dummy-d]='skeptical-updater dummy su-dummy-d (v1.0)'
[bash]='The GNU Bourne Again shell' [glibc]='The GNU libc libraries'
)
declare -A REQUIRES=(
[su-dummy-a]=$'su-dummy-b\nrpmlib(CompressedFileNames)\nrpmlib(FileDigests)'
[su-dummy-b]=$'rpmlib(CompressedFileNames)\nrpmlib(FileDigests)'
[su-dummy-c]=$'rpmlib(CompressedFileNames)'
[su-dummy-d]=$'su-dummy-a\n/bin/sh\nsu-dummy-b\n(su-dummy-c if su-dummy-b)\nrpmlib(CompressedFileNames)'
[bash]=$'/usr/bin/sh\nfilesystem\nlibc.so.6()(64bit)\nlibc.so.6(GLIBC_2.34)(64bit)\nlibtinfo.so.6()(64bit)\nrtld(GNU_HASH)'
[glibc]=$'(glibc-gconv-extra(x86-64) = 2.34-275.el9_8 if redhat-rpm-config)\nbasesystem\nglibc-common'
[glibc-gconv-extra]=$'glibc(x86-64)'
[glibc-common]=$'/usr/bin/sh\nglibc'
[basesystem]=$'filesystem\nsetup'
[setup]=$'system-release'
[filesystem]=$'/bin/sh\nsetup'
[ncurses-libs]=$'libc.so.6()(64bit)\nrtld(GNU_HASH)'
[redhat-rpm-config]=$'rpm\n/usr/bin/sh'
[rpm]=$'/usr/bin/sh\nlibc.so.6()(64bit)'
)
declare -A PROVIDES=(
[su-dummy-a]=su-dummy-a [su-dummy-b]=su-dummy-b [su-dummy-c]=$'su-dummy-c\nsu-dummy-c(x86-64)' [su-dummy-d]=su-dummy-d
[bash]=$'bash\nbash(x86-64)\nconfig(bash)'
[glibc]=$'glibc\nglibc(x86-64)\nlibc.so.6()(64bit)\nlibc.so.6(GLIBC_2.34)(64bit)\nrtld(GNU_HASH)'
[glibc-gconv-extra]=$'glibc-gconv-extra\nglibc-gconv-extra(x86-64)'
[glibc-common]=glibc-common [basesystem]=basesystem [setup]=setup [filesystem]=filesystem
[ncurses-libs]=$'ncurses-libs\nlibtinfo.so.6()(64bit)'
[redhat-rpm-config]=redhat-rpm-config [rpm]=rpm
)
# FAKE_RPM_NO_D=1: su-dummy-d (which requires every dummy) is not installed
declare -A FILES=(
[su-dummy-a]=/usr/share/su-dummy/su-dummy-a [su-dummy-b]=/usr/share/su-dummy/su-dummy-b
[su-dummy-c]=/usr/share/su-dummy/su-dummy-c [su-dummy-d]=/usr/share/su-dummy/su-dummy-d
[bash]=$'/bin/sh\n/usr/bin/bash\n/usr/bin/sh'
[glibc]=/usr/lib64/libc.so.6
)
if [[ -n ${FAKE_RPM_NO_D:-} ]]; then unset 'EVR[su-dummy-d]'; fi
+8
View File
@@ -0,0 +1,8 @@
<?xml version='1.0'?>
<stream>
<message type="info">Loading repository data...</message>
<message type="info">Computing distribution upgrade...</message>
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
</install-summary>
<prompt id="1" type="0">Continue?</prompt>
</stream>
+18
View File
@@ -0,0 +1,18 @@
<?xml version='1.0'?>
<stream>
<message type="info">Loading repository data...</message>
<message type="info">Computing distribution upgrade...</message>
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
<to-upgrade>
<solvable type="package" name="su-dummy-a" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-a"><description>d
more</description></solvable><solvable type="package" name="su-dummy-b" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-b"><description>d
more</description></solvable><solvable type="package" name="su-dummy-c" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-c"><description>d
more</description></solvable><solvable type="product" name="openSUSE-release" edition="20261008-1" arch="noarch" repository="su-dummy" edition-old="20261007-1" arch-old="noarch" summary="s openSUSE-release"><description>d
more</description></solvable></to-upgrade>
</install-summary>
<prompt id="1" type="0">Continue?</prompt>
</stream>
+14
View File
@@ -0,0 +1,14 @@
<?xml version='1.0'?>
<stream>
<message type="info">Loading repository data...</message>
<message type="info">Computing distribution upgrade...</message>
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
<to-remove>
<solvable type="package" name="libcurl-mini4" edition="8.16.0-1" arch="noarch" repository="@System" summary="s libcurl-mini4"><description>d
more</description></solvable></to-remove>
<to-install>
</to-install>
</install-summary>
<prompt id="1" type="0">Continue?</prompt>
</stream>
+20
View File
@@ -0,0 +1,20 @@
<?xml version='1.0'?>
<stream>
<message type="info">Loading repository data...</message>
<message type="info">Computing distribution upgrade...</message>
<install-summary download-size="19047" space-usage-diff="0" packages-to-change="4" need-restart="0" need-reboot="0">
<to-upgrade>
<solvable type="package" name="su-dummy-a" edition="2.0-1" arch="noarch" repository="su-dummy" edition-old="1.0-1" arch-old="noarch" summary="s su-dummy-a"><description>d
more</description></solvable></to-upgrade>
<to-install>
<solvable type="package" name="su-dummy-newdep" edition="1.0-1" arch="noarch" repository="su-dummy" summary="s su-dummy-newdep"><description>d
more</description></solvable></to-install>
<to-remove>
<solvable type="package" name="libcurl-mini4" edition="8.16.0-1" arch="noarch" repository="@System" summary="s libcurl-mini4"><description>d
more</description></solvable></to-remove>
</install-summary>
<prompt id="1" type="0">Continue?</prompt>
</stream>
@@ -0,0 +1,18 @@
Information for package su-dummy-a:
-----------------------------------
Repository : su-dummy
Name : su-dummy-a
Version : 2.0-1
Arch : noarch
Vendor :
Installed Size : 15 B
Installed : Yes
Status : out-of-date (version 1.0-1 installed)
Source package : su-dummy-a-2.0-1.src
Summary : skeptical-updater dummy su-dummy-a (v2.0)
Description :
Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
Second paragraph of su-dummy-a 2.0 (blank line above on purpose).
+15
View File
@@ -0,0 +1,15 @@
<?xml version='1.0'?>
<stream>
<message type="info">Loading repository data...</message>
<message type="info">Reading installed packages...</message>
<update-status version="0.6">
<update-list>
<update kind="package" name="su-dummy-a" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-a (v2.0)</summary><description>Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
Second paragraph of su-dummy-a 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update><update kind="package" name="su-dummy-b" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-b (v2.0)</summary><description>Dummy package su-dummy-b version 2.0 for skeptical-updater tests.
Second paragraph of su-dummy-b 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update><update kind="package" name="su-dummy-c" edition="2.0-1" arch="x86_64" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-c (v2.0)</summary><description>Dummy package su-dummy-c version 2.0 for skeptical-updater tests.
Second paragraph of su-dummy-c 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update></update-list>
</update-status>
</stream>
+9
View File
@@ -0,0 +1,9 @@
<?xml version='1.0'?>
<stream>
<message type="info">Loading repository data...</message>
<message type="info">Reading installed packages...</message>
<update-status version="0.6">
<update-list>
</update-list>
</update-status>
</stream>
+17
View File
@@ -0,0 +1,17 @@
<?xml version='1.0'?>
<stream>
<message type="info">Refreshing service &apos;openSUSE&apos;.</message>
<message type="info">Loading repository data...</message>
<message type="info">Reading installed packages...</message>
<update-status version="0.6">
<update-list>
<update kind="package" name="openSUSE-build-key" edition="1.0-69.1" arch="x86_64" edition-old="1.0-68.1"><summary>The public gpg keys for rpm package signature verification</summary><description>This package contains the gpg keys that are used to sign the
openSUSE distribution.</description><license/><source url="http://download.opensuse.org/update/tumbleweed/" alias="repo-update"/></update><update kind="package" name="su-dummy-a" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-a (v2.0)</summary><description>Dummy package su-dummy-a version 2.0 for skeptical-updater tests.
Second paragraph of su-dummy-a 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update><update kind="package" name="su-dummy-b" edition="2.0-1" arch="noarch" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-b (v2.0)</summary><description>Dummy package su-dummy-b version 2.0 for skeptical-updater tests.
Second paragraph of su-dummy-b 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="openSUSE:repo-oss &amp; co"/></update><update kind="package" name="su-dummy-c" edition="2.0-1" arch="x86_64" edition-old="1.0-1"><summary>skeptical-updater dummy su-dummy-c (v2.0)</summary><description>Dummy package su-dummy-c version 2.0 for skeptical-updater tests.
Second paragraph of su-dummy-c 2.0 (blank line above on purpose).</description><license/><source url="file:/var/lib/su-dummy-repo" alias="su-dummy"/></update></update-list>
</update-status>
</stream>
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# Backend detection (pacman > apt > dnf (else yum) > zypper), --backend override, labels, banner.
# each @test runs in its own subshell: per-test exports are intended
# shellcheck disable=SC2030,SC2031
load test_helper
setup() { common_setup; }
@test "pacman wins over apt, dnf and zypper" {
use_fakes pacman apt dnf zypper rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-arch
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: pacman (Arch Linux)'
log_has 'pacman -Qu'
log_lacks '^(apt-get|dnf|zypper) '
}
@test "apt wins over dnf and zypper" {
use_fakes zypper dnf apt rpm
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
log_has 'apt-get -s --with-new-pkgs upgrade'
}
@test "dnf wins over zypper" {
use_fakes zypper dnf rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-alma9
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: dnf (AlmaLinux 9.8 (Olive Jaguar))'
log_has 'dnf -q check-update --setopt=skip_if_unavailable=0'
}
@test "yum is used when dnf is missing" {
use_fakes zypper yum rpm
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: yum (Debian GNU/Linux 12 (bookworm))'
log_has 'yum -q check-update --setopt=skip_if_unavailable=0'
}
@test "zypper comes last" {
use_fakes zypper rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: zypper (openSUSE Tumbleweed)'
log_has 'zypper --non-interactive --no-refresh --xmlout dist-upgrade --dry-run'
}
@test "pman in PATH: label pman, the commands stay pacman" {
use_fakes pman pacman
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-arch
run skup list
assert_success
assert_line --index 0 'Backend: pman (Arch Linux)'
log_matches '^pacman -Sy --dbpath /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+ --logfile /dev/null$'
log_matches '^pacman --dbpath /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+ -Qu$'
}
@test "--backend overrides the detection" {
use_fakes pacman apt
run skup --backend=apt list --no-refresh
assert_success
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
log_lacks '^pacman '
}
@test "--backend=pman and --backend=yum are aliases of pacman and dnf" {
use_fakes pacman apt dnf rpm
run skup --backend=pman list --no-refresh
assert_success
assert_line --index 0 --partial 'Backend: pacman'
run skup --backend=yum list --no-refresh
assert_success
assert_line --index 0 --partial 'Backend: dnf'
}
@test "the statefile backend column is the backend, not the label" {
use_fakes pman pacman
run skup trust su-dummy-c
assert_success
run cat "$STATE_FILE"
assert_line --regexp "^pacman${T}su-dummy-c${T}user${T}${DATE_RE}\$"
}
@test "no supported package manager: exit 1" {
use_fakes
run skup list --no-refresh
assert_failure 1
assert_output --partial 'no supported package manager found in PATH (pacman, apt-get, dnf/yum, zypper)'
}
@test "a forced backend whose tool is missing: exit 1" {
use_fakes apt
run skup --backend=zypper list --no-refresh
assert_failure 1
assert_output --partial 'zypper backend: zypper not found in PATH'
run skup --backend=pacman list --no-refresh
assert_failure 1
assert_output --partial 'pacman backend: pacman not found in PATH'
run skup --backend=dnf list --no-refresh
assert_failure 1
assert_output --partial 'dnf backend: neither dnf nor yum found in PATH'
}
@test "the banner uses PRETTY_NAME, quoted or not" {
printf "NAME=x\nPRETTY_NAME='Single Quoted 1.0'\n" >"$BATS_TEST_TMPDIR/osr"
SKEPTICAL_UPDATER_OS_RELEASE=$BATS_TEST_TMPDIR/osr run skup list --no-refresh
assert_line --index 0 'Backend: apt (Single Quoted 1.0)'
printf 'PRETTY_NAME=Plain\n' >"$BATS_TEST_TMPDIR/osr"
SKEPTICAL_UPDATER_OS_RELEASE=$BATS_TEST_TMPDIR/osr run skup list --no-refresh
assert_line --index 0 'Backend: apt (Plain)'
}
@test "the banner without os-release shows just the backend" {
SKEPTICAL_UPDATER_OS_RELEASE=$BATS_TEST_TMPDIR/missing run skup list --no-refresh
assert_success
assert_line --index 0 'Backend: apt'
}
@test "trusted needs no package manager at all" {
use_fakes
write_state "$HEADER" "apt${T}x${T}user${T}2026-10-09T09:40:12Z"
run skup trusted
assert_success
assert_line "apt${T}x${T}user${T}2026-10-09T09:40:12Z"
}
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# --dry-run: mutating commands (upgrade, purge, hold/unhold, lock files, statefile writes)
# are printed as "+ <cmd>" instead of executed; read-only queries and (as root) the
# metadata refresh still run.
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'
@test "apt: refresh runs, hold/upgrade/unhold are printed, nothing mutating is executed" {
run skup --dry-run <<<$'2-\n'
assert_success
assert_line 'Refreshing package lists... done.'
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
assert_line '+ apt-mark hold su-dummy-b'
assert_line "+ $APT_UPGRADE"
assert_line '+ apt-mark unhold su-dummy-b'
assert_line 'Dry run: nothing was changed.'
log_lacks '^apt-get -y'
log_lacks '^apt-mark (un)?hold'
run cat "$FAKE_DIR/holds"
assert_output ''
[[ ! -e $STATE ]]
}
@test "apt: a package the admin already held is not printed as held" {
echo su-dummy-c >"$FAKE_DIR/holds"
FAKE_APT_SIM=held run skup --dry-run --no-refresh <<<$'3-\n'
assert_success
refute_line --partial 'apt-mark hold'
refute_line --partial 'apt-mark unhold'
assert_line "+ $APT_UPGRADE"
}
@test "N+ prints the statefile changes and writes nothing; the rows still leave the list" {
run skup --dry-run --no-refresh <<<$'1+\nl\nq'
assert_success
assert_line "+ mkdir -p -m 0755 $STATE"
assert_line "+ $STATE_FILE add: apt su-dummy-a user"
assert_line "+ $STATE_FILE add: apt su-dummy-b dep:su-dummy-a"
assert_line '3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:'
[[ ! -e $STATE ]]
}
@test "Nx prints the purge commands and drops the row" {
run skup --dry-run --no-refresh <<<$'3x\ny\nl\nq'
assert_success
assert_line '+ apt-get -y purge su-dummy-c'
refute_output --partial 'autoremove'
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
log_lacks '^apt-get -y'
}
@test "trust, untrust and refresh print their statefile changes and write nothing" {
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
before=$(cat "$STATE_FILE")
run skup --dry-run trust su-dummy-a
assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line "+ $STATE_FILE add: apt su-dummy-a user"
assert_line "+ $STATE_FILE add: apt su-dummy-b dep:su-dummy-a"
run skup --dry-run untrust su-dummy-c
assert_success
assert_line "+ $STATE_FILE remove: apt su-dummy-c user"
write_state "$HEADER" "apt${T}su-dummy-w${T}user${T}2026-01-01T00:00:00Z"
before=$(cat "$STATE_FILE")
run skup --dry-run refresh
assert_success
assert_line "+ $STATE_FILE add: apt su-dummy-v dep:su-dummy-w"
[[ $(cat "$STATE_FILE") == "$before" ]]
}
@test "pacman: -Su --ignore and -Rns are printed" {
use_fakes pacman
run skup --dry-run --no-refresh <<<$'3x\ny\n2-\n'
assert_success
assert_line '+ pacman -Rns --noconfirm -- su-dummy-c'
assert_line '+ pacman -Su --noconfirm --ignore su-dummy-b'
log_lacks '^pacman -(Su|Syu|Rns) '
}
@test "dnf: upgrade --exclude and remove are printed" {
use_fakes dnf rpm
export FAKE_RPM_NO_D=1
run skup --dry-run --no-refresh <<<$'5x\ny\n4-\n'
assert_success
assert_line '+ dnf -y remove su-dummy-c'
assert_line '+ dnf -y --setopt=skip_if_unavailable=0 --setopt=\*.metadata_expire=-1 upgrade --exclude=su-dummy-b'
log_lacks '^dnf -y'
}
@test "zypper: the private-lock commands are printed, no lock file is written" {
use_fakes zypper rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-leap
mkdir -p "$BATS_TEST_TMPDIR/etc"
printf '[main]\nlocksfile.path = %s\n' "$BATS_TEST_TMPDIR/etc/locks" >"$BATS_TEST_TMPDIR/etc/zypp.conf"
export ZYPP_CONF=$BATS_TEST_TMPDIR/etc/zypp.conf
run skup --dry-run --no-refresh <<<$'2-\n'
assert_success
assert_line "+ # private $BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/zypp.conf: $BATS_TEST_TMPDIR/etc/zypp.conf with locksfile.path = $BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/locks (a copy of $BATS_TEST_TMPDIR/etc/locks)"
assert_line "+ env ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/zypp.conf zypper --non-interactive --quiet addlock su-dummy-b"
assert_line "+ env ZYPP_CONF=$BATS_TEST_TMPDIR/skeptical-updater.XXXXXX/zypp.conf zypper --non-interactive --no-refresh update"
log_lacks 'addlock| update$'
[[ ! -e $BATS_TEST_TMPDIR/etc/locks ]]
run ls "$BATS_TEST_TMPDIR"
refute_output --partial 'skeptical-updater.'
}
@test "as root the refresh really runs; as non-root it is printed" {
run skup --dry-run list
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
: >"$FAKE_LOG"
SKEPTICAL_UPDATER_EUID=1000 run skup --dry-run list
assert_success
assert_line '+ apt-get update -qq -o APT::Update::Error-Mode=any'
log_lacks '^apt-get update'
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake apt-cache (unit tests): show / rdepends / policy from data/apt/
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
pkgs=()
for a in "$@"; do case $a in -*) ;; *) pkgs+=("$a") ;; esac; done
verb=${pkgs[0]}
pkgs=("${pkgs[@]:1}")
case $verb in
show)
f=$FAKE_DATA/apt/show/${pkgs[0]}
if [[ ! -f $f ]] || fake_purged "${pkgs[0]}"; then
echo "E: No packages found" >&2
exit 100
fi
cat "$f"
;;
rdepends)
f=$FAKE_DATA/apt/rdepends/${pkgs[0]}
if [[ -f $f ]]; then cat "$f"; else printf '%s\nReverse Depends:\n' "${pkgs[0]}"; fi
;;
policy)
# print the stanzas of the requested packages
want=" ${pkgs[*]} "
show=0
while IFS= read -r line; do
if [[ $line =~ ^([^ ].*):$ ]]; then
if [[ $want == *" ${BASH_REMATCH[1]} "* ]]; then show=1; else show=0; fi
fi
((show)) && printf '%s\n' "$line"
done <"$FAKE_DATA/apt/policy.txt"
exit 0
;;
*)
echo "fake apt-cache: unhandled: $*" >&2
exit 99
;;
esac
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake apt-get (unit tests). Scenario: FAKE_APT_SIM=fixture|held|real|none|purge (data/apt/sim-*.txt).
# Purge simulation: installed = the "ii" rows of data/apt/dpkg-status.tsv; reverse dependencies from
# data/apt/purge-rdeps.txt; pre-existing orphans from data/apt/autoremovable.txt (added by
# --auto-remove, which is system-wide like the real one); FAKE_APT_AUTO_ORPHANS="pkg:dep ..." are
# the auto-installed dependencies a purge of pkg orphans; FAKE_APT_SIM_EXTRA="pkg:other ..." makes a
# simulated purge of pkg also remove other (a dependent apt-cache rdepends did not report).
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
args=" $* "
D=$FAKE_DATA/apt
installed() { grep -q "^.i *"$'\t'"$1"$'\t' "$D/dpkg-status.tsv" && ! fake_purged "$1"; }
orphans() { while IFS= read -r o; do installed "$o" && printf '%s\n' "$o"; done <"$D/autoremovable.txt"; }
# purge_set [--auto-remove] NAME...: the packages a purge removes, sorted
purge_set() {
local auto=0 n r line
if [[ $1 == --auto-remove ]]; then
auto=1
shift
fi
{
for n in "$@"; do
installed "$n" || continue
echo "$n"
while IFS= read -r line; do
[[ $line == "$n: "* ]] || continue
for r in ${line#*: }; do installed "$r" && echo "$r"; done
done <"$D/purge-rdeps.txt"
if ((auto)); then
for r in ${FAKE_APT_AUTO_ORPHANS:-}; do [[ ${r%%:*} == "$n" ]] && installed "${r#*:}" && echo "${r#*:}"; done
fi
for r in ${FAKE_APT_SIM_EXTRA:-}; do [[ ${r%%:*} == "$n" ]] && installed "${r#*:}" && echo "${r#*:}"; done
done
if ((auto)); then orphans; fi
} | sort -u
}
case $args in
*' update '*)
rc=$(fake_rc FAKE_APT_UPDATE_RC 0)
if ((rc)); then
echo "E: Failed to fetch http://deb.debian.org/debian/dists/bookworm/InRelease Temporary failure resolving 'deb.debian.org'" >&2
echo "E: Some index files failed to download. They have been ignored, or old ones used instead." >&2
fi
exit "$rc"
;;
*' -s '*autoremove*)
while IFS= read -r o; do [[ -n $o ]] && echo "Remv $o [1.0]"; done < <(orphans)
exit 0
;;
*' -s '*purge*)
names=()
auto=()
for a in "$@"; do
case $a in --auto-remove) auto=(--auto-remove) ;; -* | purge) ;; *) names+=("$a") ;; esac
done
rc=$(fake_rc FAKE_APT_SIM_PURGE_RC 0)
if ((rc)); then
echo "E: Unable to correct problems, you have held broken packages." >&2
exit "$rc"
fi
for n in "${names[@]}"; do
if ! grep -q $'\t'"$n"$'\t' "$D/dpkg-status.tsv" && [[ $n != su-dummy-newdep ]]; then
echo "E: Unable to locate package $n" >&2
exit 100
fi
done
while IFS= read -r p; do [[ -n $p ]] && echo "Purg $p [1.0]"; done < <(purge_set "${auto[@]}" "${names[@]}")
exit 0
;;
*' -s '*' upgrade '*)
f=$D/sim-${FAKE_APT_SIM:-fixture}.txt
while IFS= read -r line; do
name=
case $line in Inst\ * | Conf\ *) name=${line#* }; name=${name%% *} ;; esac
if [[ -n $name ]] && fake_purged "$name"; then continue; fi
if [[ $line == ' '* && -f $FAKE_DIR/purged ]]; then
while IFS= read -r p; do line=${line//$p/}; done <"$FAKE_DIR/purged"
fi
printf '%s\n' "$line"
done <"$f"
exit 0
;;
*' upgrade '*)
echo "holds during upgrade: $(sort "$FAKE_DIR/holds" 2>/dev/null | tr '\n' ' ')" >>"$FAKE_LOG"
# which of our lock files does the package manager have open (it must have none)?
if [[ -d /proc/$$/fd ]]; then
n=0
for fd in "/proc/$$/fd"/*; do
[[ $(readlink "$fd" 2>/dev/null) == *.lock ]] && n=$((n + 1))
done
echo "lock fds in the upgrade: $n" >>"$FAKE_LOG"
fi
if [[ -n ${FAKE_APT_UPGRADE_KILL:-} ]]; then
kill -s "$FAKE_APT_UPGRADE_KILL" "$PPID"
sleep 0.2
fi
if [[ -n ${FAKE_KILL_READER:-} ]]; then
# close the applet's stdout: kill the process reading it, then keep writing
pid=$(cat "$FAKE_DIR/reader.pid")
kill -9 "$pid"
while kill -0 "$pid" 2>/dev/null; do sleep 0.05; done
fi
echo "Reading package lists..." 2>/dev/null
echo "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded." 2>/dev/null
exit "$(fake_rc FAKE_APT_UPGRADE_RC 0)"
;;
*' purge '*)
rc=$(fake_rc FAKE_APT_PURGE_RC 0)
if ((rc == 0)); then
for a in "$@"; do case $a in -* | purge) ;; *) fake_mark_purged "$a" ;; esac; done
# FAKE_APT_PURGE_ALSO: packages that disappear although the plan did not name them
read -r -a also <<<"${FAKE_APT_PURGE_ALSO:-}"
((${#also[@]} == 0)) || fake_mark_purged "${also[@]}"
else
echo "E: Held packages were changed and -y was used without --allow-change-held-packages." >&2
fi
exit "$rc"
;;
esac
echo "fake apt-get: unhandled: $*" >&2
exit 99
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake apt-mark (unit tests): hold state in $FAKE_DIR/holds (one name per line).
# FAKE_APT_MARK_LOCKED=N: the first N hold/unhold calls fail like dpkg's busy frontend lock.
# FAKE_APT_MARK_UNHOLD_SIGNAL=SIG: send SIG to the applet before an unhold (a second Ctrl-C).
# FAKE_APT_MARK_UNHOLD_RC=N: unhold fails (nothing released). FAKE_APT_MARK_FAIL_MSG: the message a
# failing hold prints. Every unhold logs whether $STATE_DIR/holds.lock is held by someone.
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
h=$FAKE_DIR/holds
touch "$h"
verb=$1
shift
case $verb in hold | unhold)
if [[ -n ${FAKE_APT_MARK_LOCKED:-} ]]; then
n=$(cat "$FAKE_DIR/locked-calls" 2>/dev/null || echo 0)
if ((n < FAKE_APT_MARK_LOCKED)); then
echo $((n + 1)) >"$FAKE_DIR/locked-calls"
echo "${FAKE_APT_MARK_LOCK_MSG:-dpkg: error: dpkg frontend lock was locked by another process with pid 4242}"
echo "E: Sub-process dpkg --set-selections returned an error code (2)"
echo "E: Executing dpkg failed. Are you root?"
exit 100
fi
fi
;;
esac
case $verb in
showhold) sort "$h" ;;
hold)
for p in "$@"; do
if grep -qxF -- "$p" "$h"; then echo "$p was already set on hold."; else
echo "$p" >>"$h"
echo "$p set on hold."
fi
done
[[ -z ${FAKE_APT_MARK_FAIL_MSG:-} ]] || echo "$FAKE_APT_MARK_FAIL_MSG" >&2
exit "$(fake_rc FAKE_APT_MARK_HOLD_RC 0)"
;;
unhold)
if [[ -n ${SKEPTICAL_UPDATER_STATE_DIR:-} && -e $SKEPTICAL_UPDATER_STATE_DIR/holds.lock ]] && command -v flock >/dev/null; then
if flock -n "$SKEPTICAL_UPDATER_STATE_DIR/holds.lock" true; then l=free; else l=held; fi
echo "holds.lock during unhold: $l" >>"$FAKE_LOG"
fi
if [[ -n ${FAKE_APT_MARK_UNHOLD_RC:-} ]]; then
echo "E: Unable to correct problems" >&2
exit "$FAKE_APT_MARK_UNHOLD_RC"
fi
if [[ -n ${FAKE_APT_MARK_UNHOLD_SIGNAL:-} ]]; then
kill -s "$FAKE_APT_MARK_UNHOLD_SIGNAL" "$(cat "$FAKE_DIR/applet.pid" 2>/dev/null || echo "$PPID")" 2>/dev/null
sleep 0.2
fi
for p in "$@"; do
if grep -qxF -- "$p" "$h"; then
grep -vxF -- "$p" "$h" >"$h.new" || true
mv "$h.new" "$h"
echo "Canceled hold on $p."
else echo "$p was already not on hold."; fi
done
;;
*)
echo "fake apt-mark: unhandled: $verb $*" >&2
exit 99
;;
esac
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
# FAKE_STATE_APPEND / FAKE_STATE_SED: simulate another process writing the statefile meanwhile
st=${SKEPTICAL_UPDATER_STATE_DIR:-}/trusted.tsv
if [[ -n ${FAKE_STATE_APPEND:-} ]]; then printf '%s\n' "$FAKE_STATE_APPEND" >>"$st"; fi
if [[ -n ${FAKE_STATE_SED:-} ]]; then sed -i "$FAKE_STATE_SED" "$st"; fi
case " $* " in
*' -W '*) cat "$FAKE_DATA/apt/dpkg-status.tsv" ;;
*)
echo "fake dpkg-query: unhandled: $*" >&2
exit 99
;;
esac
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake dnf (unit tests). FAKE_DNF_MAJOR=4|5 picks the output dialect; FAKE_DNF_CHECK_RC=0|1 simulates
# "no updates" / an error; FAKE_DNF_CHECK=obsoletes replays a check-update with only an
# "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail;
# removal tables: see the --assumeno remove branch.
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
# shellcheck source=../../data/rpm/db.bash
. "$FAKE_DATA/rpm/db.bash"
fake_log "$@"
D=$FAKE_DATA/dnf
major=${FAKE_DNF_MAJOR:-4}
pos=()
for a in "$@"; do case $a in -*) ;; *) pos+=("$a") ;; esac; done
installed() { [[ -n ${EVR[$1]:-} ]] && ! fake_purged "$1"; }
case " $* " in
*' --version '*) if ((major == 5)); then echo "dnf5 version 5.4.3.0"; else echo "4.14.0"; fi ;;
*' makecache '*)
rc=$(fake_rc FAKE_DNF_MAKECACHE_RC 0)
((rc == 0)) || echo "Error: Failed to download metadata for repo 'su-dummy': Cannot download repomd.xml" >&2
exit "$rc"
;;
*' check-update '*)
rc=$(fake_rc FAKE_DNF_CHECK_RC 100)
f=$D/check-update-dnf$major.txt
[[ ${FAKE_DNF_CHECK:-} != obsoletes ]] || f=$D/check-update-obsoletes.txt
case $rc in
100)
while IFS= read -r line; do
read -r na _ <<<"$line"
[[ -n $na ]] && fake_purged "${na%.*}" && continue
printf '%s\n' "$line"
done <"$f"
;;
1) echo "Error: Failed to download metadata for repo 'baseos'" >&2 ;;
esac
exit "$rc"
;;
*' repoquery '*)
p=${pos[-1]}
case " $* " in
*' --upgrades '*) case $p in su-dummy-[abc]) printf 'skeptical-updater dummy %s (v2.0)\n' "$p"; ((major == 5)) || echo ;; esac ;;
*) case $p in su-dummy-new) printf 'skeptical-updater dummy su-dummy-new (v1.0)\n' ;; esac ;;
esac
exit 0
;;
*' info '*)
p=${pos[-1]}
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
echo "Error: No matching Packages to list" >&2
exit 1
fi
;;
*' --assumeno '*remove*)
# the removal table: the target, its dependents (su-dummy-a for su-dummy-b; FAKE_DNF_DEPENDENT=
# "pkg:other"), its unused dependencies (FAKE_DNF_UNUSED="pkg:dep"); FAKE_DNF_WRAP wraps those rows
p=${pos[-1]}
if [[ -n ${FAKE_DNF_REMOVE_REFUSE:-} ]]; then
echo "Error: Problem: The operation would result in removing the following protected packages: $p"
exit 1
fi
if ! installed "$p"; then
echo "No match for argument: $p"
echo "No packages marked for removal."
echo "Dependencies resolved."
echo "Nothing to do."
echo "Complete!"
exit 0
fi
dependent=() unused=()
[[ $p != su-dummy-b ]] || ! installed su-dummy-a || dependent+=(su-dummy-a)
for r in ${FAKE_DNF_DEPENDENT:-}; do [[ ${r%%:*} == "$p" ]] && dependent+=("${r#*:}"); done
for r in ${FAKE_DNF_UNUSED:-}; do [[ ${r%%:*} == "$p" ]] && unused+=("${r#*:}"); done
row() { # row NAME
if ((major == 5)); then
if [[ -n ${FAKE_DNF_WRAP:-} && $1 != "$p" ]]; then
echo " $1"
echo " noarch 0:1.0-1 <unknown> 15.0 B"
else
echo " $1 noarch 0:1.0-1 <unknown> 15.0 B"
fi
elif [[ -n ${FAKE_DNF_WRAP:-} && $1 != "$p" ]]; then
printf ' %s\n' "$1"
printf ' noarch 1.0-1 @System 15\n'
else
printf ' %-20s noarch 1.0-1 @System 15\n' "$1"
fi
}
if ((major == 5)); then
echo "Package Arch Version Repository Size"
else
echo "Dependencies resolved."
echo "================================================================================"
echo " Package Architecture Version Repository Size"
echo "================================================================================"
fi
echo "Removing:"
row "$p"
if ((${#dependent[@]})); then
echo "Removing dependent packages:"
for r in "${dependent[@]}"; do row "$r"; done
fi
if ((${#unused[@]})); then
echo "Removing unused dependencies:"
for r in "${unused[@]}"; do row "$r"; done
fi
echo
n=$((1 + ${#dependent[@]} + ${#unused[@]}))
if ((major == 5)); then
echo "Transaction Summary:"
echo " Removing: $n packages"
echo "Operation aborted by the user."
else
echo "Transaction Summary"
echo "================================================================================"
echo "Remove $n Packages"
echo
echo "Operation aborted."
fi
exit 1
;;
*' upgrade '*) exit "$(fake_rc FAKE_DNF_UPGRADE_RC 0)" ;;
*' remove '*)
fake_mark_purged "${pos[@]:1}"
exit 0
;;
*)
echo "fake dnf: unhandled: $*" >&2
exit 99
;;
esac
+23
View File
@@ -0,0 +1,23 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# tests/unit/fakes/fakelib.bash - shared by the fake package-manager commands.
# Every fake logs its invocation ("<name> <args...>") to $FAKE_LOG and keeps its mutable
# state (holds, purged packages, ...) under $FAKE_DIR. Replayed outputs live in
# tests/unit/data/<backend>/ and are copied from the samples recorded in discovery/*.md.
# shellcheck shell=bash
FAKE_NAME=${FAKE_AS:-${0##*/}}
: "${FAKE_LOG:?FAKE_LOG not set}" "${FAKE_DIR:?FAKE_DIR not set}" "${FAKE_DATA:?FAKE_DATA not set}"
fake_log() { printf '%s %s\n' "$FAKE_NAME" "$*" >>"$FAKE_LOG"; }
# fake_rc VAR DEFAULT: exit code override from the environment (e.g. FAKE_APT_UPGRADE_RC=100)
fake_rc() {
local v=${!1:-}
printf '%s' "${v:-$2}"
}
# fake_purged NAME: was NAME removed by an earlier fake purge in this test?
fake_purged() { [[ -f $FAKE_DIR/purged ]] && grep -qxF -- "$1" "$FAKE_DIR/purged"; }
fake_mark_purged() { printf '%s\n' "$@" >>"$FAKE_DIR/purged"; }
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake pacman (unit tests). The "real" database is $FAKE_PACMAN_DBPATH (see pacman-conf);
# -Sy --dbpath DIR logs what DIR/sync was seeded with and writes DIR/sync/su-dummy.db
# ("reviewed <n>"; plus a .sig with FAKE_PACMAN_SIG=1); -Su logs the real su-dummy.db it applies.
# FAKE_PACMAN_SUP_RC=1 makes the -Sup pre-check fail like the ignored-dependency trap;
# FAKE_PACMAN_SU_KILL=SIG: -Su signals the applet (Ctrl-C during the upgrade);
# FAKE_PACMAN_STDERR=TEXT: -Sl and -Qu also print TEXT on stderr (an unreadable database).
# -Rs --print: FAKE_PACMAN_RS_REFUSE=1 makes pacman refuse ("breaks dependency");
# FAKE_PACMAN_RS_EXTRA="pkg:other" adds other to the plan of pkg.
# FAKE_PACMAN_QU=fixture|real|none selects the -Qu sample,
# FAKE_PACMAN_SL=empty makes -Sl print nothing, FAKE_PACMAN_SUP_EXTRA="a b" adds packages that
# only -Sup (the real transaction) shows. Every -Q*/-S[il] call records its COLUMNS value, every
# call its --dbpath (or "real") in $FAKE_DIR/pacman-dbpath.
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
D=$FAKE_DATA/pacman
# blocks FILE NAME...: print the blank-line separated blocks whose Name is one of NAME
# (all blocks when no NAME); exit 1 (after printing the others) if a NAME is missing
blocks() {
local f=$1 line block='' name='' rc=0 n
shift
local -A want=() found=()
for n in "$@"; do want[$n]=1; done
while IFS= read -r line || [[ -n $block ]]; do
if [[ -z $line ]]; then
if [[ -n $block ]] && { (($# == 0)) || [[ -n ${want[$name]:-} ]]; } && ! fake_purged "$name"; then
printf '%s\n' "$block"
found[$name]=1
fi
block='' name=''
continue
fi
[[ $line =~ ^Name\ +:\ (.*)$ ]] && name=${BASH_REMATCH[1]}
block+=$line$'\n'
done <"$f"
for n in "$@"; do
if [[ -z ${found[$n]:-} ]]; then
echo "error: package '$n' was not found" >&2
rc=1
fi
done
return "$rc"
}
op='' dbpath=real print=0
targets=()
while (($#)); do
case $1 in
--dbpath)
dbpath=$2
shift
;;
--logfile | --print-format | --ignore) shift ;;
--print) print=1 ;;
--*) ;;
-[A-Z]*) [[ -n $op ]] || op=$1 ;;
-*) ;;
*) targets+=("$1") ;;
esac
shift
done
echo "$op $dbpath" >>"$FAKE_DIR/pacman-dbpath"
case $op in -Q* | -Si | -Sl) echo "$op ${COLUMNS-unset}" >>"$FAKE_DIR/pacman-columns" ;; esac
qu() {
local f=$D/Qu-${FAKE_PACMAN_QU:-fixture}.txt line
[[ -f $f ]] || return 0
while IFS= read -r line; do
fake_purged "${line%% *}" || printf '%s\n' "$line"
done <"$f"
}
case $op in
-Sy)
if [[ $dbpath != real ]]; then
echo "seeded: $(cd "$dbpath/sync" && echo *)" >>"$FAKE_LOG"
rc=$(fake_rc FAKE_PACMAN_SY_RC 0)
((rc)) && exit "$rc"
n=$(($(cat "$FAKE_DIR/sy-count" 2>/dev/null || echo 0) + 1))
echo "$n" >"$FAKE_DIR/sy-count"
echo "reviewed $n" >"$dbpath/sync/su-dummy.db"
[[ -z ${FAKE_PACMAN_SIG:-} ]] || echo "signature $n" >"$dbpath/sync/su-dummy.db.sig"
fi
exit "$(fake_rc FAKE_PACMAN_SY_RC 0)"
;;
-Qu)
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
out=$(qu)
[[ -n $out ]] || exit 1
printf '%s\n' "$out"
;;
-Sup)
if [[ -n ${FAKE_PACMAN_SUP_RC:-} ]]; then
echo ":: unable to satisfy dependency 'su-dummy-b>=2.0' required by su-dummy-a"
echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2
exit "$FAKE_PACMAN_SUP_RC"
fi
qu | while read -r n _ _ _ flag; do [[ $flag == '[ignored]' ]] || echo "$n"; done
for n in ${FAKE_PACMAN_SUP_EXTRA:-}; do echo "$n"; done
;;
-Sl)
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
[[ ${FAKE_PACMAN_SL:-} == empty ]] || cat "$D/Sl.txt"
;;
-Qi) blocks "$D/Qi.txt" "${targets[@]}" ;;
-Si)
t=()
for a in "${targets[@]}"; do t+=("${a#*/}"); done
blocks "$D/Si.txt" "${t[@]}"
;;
-Su | -Syu)
echo "real sync at $op: $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db" 2>/dev/null || echo none) / $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig" 2>/dev/null || echo no-sig)" >>"$FAKE_LOG"
if [[ -e $FAKE_PACMAN_DBPATH/db.lck ]]; then echo "db.lck at $op: yes" >>"$FAKE_LOG"; else echo "db.lck at $op: no" >>"$FAKE_LOG"; fi
if [[ -n ${FAKE_PACMAN_SU_KILL:-} ]]; then
kill -s "$FAKE_PACMAN_SU_KILL" "$PPID"
sleep 0.2
fi
exit "$(fake_rc FAKE_PACMAN_SU_RC 0)"
;;
-Rs)
((print)) || exit 98
tg=${targets[0]}
if [[ -n ${FAKE_PACMAN_RS_REFUSE:-} || $tg == su-dummy-b ]]; then
echo "checking dependencies..."
echo ":: removing $tg breaks dependency '$tg' required by su-dummy-a"
echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2
exit 1
fi
case $tg in
su-dummy-a) printf '%s\n' su-dummy-a su-dummy-b ;; # b was installed as a dependency of a
su-dummy-c | glib2 | expat | ca-certificates-mozilla) echo "$tg" ;;
*)
echo "error: target not found: $tg" >&2
exit 1
;;
esac
for r in ${FAKE_PACMAN_RS_EXTRA:-}; do [[ ${r%%:*} == "$tg" ]] && echo "${r#*:}"; done
;;
-Rns)
rc=$(fake_rc FAKE_PACMAN_RNS_RC 0)
if ((rc)); then echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2; else fake_mark_purged "${targets[@]}"; fi
exit "$rc"
;;
*)
echo "fake pacman: unhandled: $op $*" >&2
exit 99
;;
esac
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake pacman-conf (unit tests): the "real" pacman database lives in $FAKE_PACMAN_DBPATH
case ${1:-} in
DBPath) printf '%s/\n' "${FAKE_PACMAN_DBPATH:?}" ;;
*) exit 1 ;;
esac
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake pman (unit tests): only its presence matters (label "pman")
exit 0
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake rpm (unit tests): answers the queries skeptical-updater makes from data/rpm/db.bash
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
# shellcheck source=../../data/rpm/db.bash
. "$FAKE_DATA/rpm/db.bash"
fake_log "$@"
installed() { [[ -n ${EVR[$1]:-} ]] && ! fake_purged "$1"; }
all=0 whatprov=0 list=0 qf=''
names=()
while (($#)); do
case $1 in
-q) ;;
-qa) all=1 ;;
-ql) list=1 ;;
--whatprovides) whatprov=1 ;;
--qf | --queryformat)
qf=$2
shift
;;
*) names+=("$1") ;;
esac
shift
done
rc=0
if ((all)); then
for p in "${!EVR[@]}"; do
installed "$p" || continue
case $qf in
*'%{NAME}.%{ARCH}'*)
for v in ${EVR[$p]}; do
printf '%s.%s\t%s\n' "$p" "${ARCH[$p]}" "$v"
done
;;
*'%{REQUIRENAME}'*'%{=NAME}'*)
while IFS= read -r r; do [[ -n $r ]] && printf '%s\t%s\n' "$r" "$p"; done <<<"${REQUIRES[$p]:-}"
;;
*) printf '%s\n' "$p" ;;
esac
done
exit 0
fi
if ((whatprov)); then
for cap in "${names[@]}"; do
hit=0
for p in "${!EVR[@]}"; do
installed "$p" || continue
if grep -qxF -- "$cap" <<<"${PROVIDES[$p]:-}"$'\n'"${FILES[$p]:-}"; then
printf '%s\n' "$p"
hit=1
fi
done
if ((!hit)); then
echo "no package provides $cap"
rc=1
fi
done
exit "$rc"
fi
for p in "${names[@]}"; do
if ! installed "$p"; then
echo "package $p is not installed"
rc=1
continue
fi
if ((list)); then
if [[ -n ${FILES[$p]:-} ]]; then printf '%s\n' "${FILES[$p]}"; else echo "(contains no files)"; fi
continue
fi
case $qf in
*'%{REQUIRENAME}'*) [[ -z ${REQUIRES[$p]:-} ]] || printf '%s\n' "${REQUIRES[$p]}" ;;
*'%{PROVIDENAME}'*) [[ -z ${PROVIDES[$p]:-} ]] || printf '%s\n' "${PROVIDES[$p]}" ;;
*'%{SUMMARY}'*) printf '%s\n' "${SUMMARY[$p]:-dummy summary of $p}" ;;
*) printf '%s-%s.%s\n' "$p" "${EVR[$p]##* }" "${ARCH[$p]}" ;;
esac
done
exit "$rc"
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake sudo (unit tests): logs the command it was asked to run, runs nothing
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
exit 0
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake yum (unit tests): yum is the dnf binary on EL9+
FAKE_AS=yum exec "${BASH_SOURCE[0]%/*}/../dnf/dnf" "$@"
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake zypper (unit tests). FAKE_ZYPPER_LU=fixture|real|none picks the list-updates XML,
# FAKE_ZYPPER_DUP=fixture|empty|removes the `dist-upgrade --dry-run` plan (data/zypper/dup-*.xml).
# addlock writes real zypp lock entries into the locksfile.path of $ZYPP_CONF (default
# /etc/zypp/locks is never touched: the fake refuses); update/dist-upgrade log that file.
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "${ZYPP_CONF:+ZYPP_CONF=$ZYPP_CONF }$*"
D=$FAKE_DATA/zypper
locksfile() {
local f=${ZYPP_CONF:-} line l=
[[ -n $f && -r $f ]] || return 1
while IFS= read -r line; do
[[ $line =~ ^[[:space:]]*locksfile\.path[[:space:]]*=[[:space:]]*(.*)$ ]] && l=${BASH_REMATCH[1]}
done <"$f"
[[ -n $l ]] || return 1
printf '%s' "$l"
}
pos=()
for a in "$@"; do case $a in -*) ;; *) pos+=("$a") ;; esac; done
verb=${pos[0]:-}
args=("${pos[@]:1}")
xml_remove() { # the install-summary of `remove --dry-run` for the given names
local n
printf "<?xml version='1.0'?>\n<stream>\n<install-summary download-size=\"0\" space-usage-diff=\"-30\" packages-to-change=\"%s\">\n<to-remove>\n" "$#"
for n in "$@"; do printf '<solvable type="package" name="%s" edition="1.0-1" arch="noarch" repository="@System" summary="dummy %s"><description>d</description></solvable>\n' "$n" "$n"; done
printf '</to-remove>\n</install-summary>\n</stream>\n'
}
dry=0
case " $* " in *' --dry-run '*) dry=1 ;; esac
case $verb in
refresh)
rc=$(fake_rc FAKE_ZYPPER_REFRESH_RC 0)
((rc == 0)) || echo "Could not refresh the repositories because of errors." >&2
exit "$rc"
;;
list-updates) cat "$D/lu-${FAKE_ZYPPER_LU:-fixture}.xml" ;;
info)
p=${args[-1]}
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
echo "package '$p' not found."
exit 104
fi
;;
addlock)
l=$(locksfile) || {
echo "fake zypper: addlock without a private ZYPP_CONF/locksfile.path" >&2
exit 98
}
for p in "${args[@]}"; do printf '\ntype: package\nmatch_type: glob\ncase_sensitive: on\nsolvable_name: %s\n\n' "$p" >>"$l"; done
;;
dist-upgrade | dup | update)
if ((dry)); then
cat "$D/dup-${FAKE_ZYPPER_DUP:-fixture}.xml"
exit 0
fi
if l=$(locksfile); then { echo "--- locks seen by $verb:"; cat "$l"; echo "--- end"; } >>"$FAKE_LOG"; fi
if [[ -n ${ZYPP_CONF:-} && -n ${SKEPTICAL_UPDATER_STATE_DIR:-} && -e $SKEPTICAL_UPDATER_STATE_DIR/zypper.lock ]]; then
if flock -n "$SKEPTICAL_UPDATER_STATE_DIR/zypper.lock" true; then l=free; else l=held; fi
echo "zypper.lock during $verb: $l" >>"$FAKE_LOG"
fi
if [[ -n ${FAKE_ZYPPER_UPGRADE_KILL:-} ]]; then
kill -s "$FAKE_ZYPPER_UPGRADE_KILL" "$PPID"
sleep 0.2
fi
exit "$(fake_rc FAKE_ZYPPER_UPGRADE_RC 0)"
;;
remove)
if ((dry)); then
set -- "${args[@]}"
# FAKE_ZYPPER_CLEAN="pkg:dep" (unneeded dependency) / FAKE_ZYPPER_EXTRA="pkg:other" join the plan
more=()
for r in ${FAKE_ZYPPER_CLEAN:-} ${FAKE_ZYPPER_EXTRA:-}; do [[ ${r%%:*} == "${1:-}" ]] && more+=("${r#*:}"); done
case ${1:-} in
su-dummy-b) xml_remove su-dummy-a su-dummy-b "${more[@]}" ;;
su-dummy-*) xml_remove "$1" "${more[@]}" ;;
*)
echo "'$1' not found in package names. Trying capabilities."
echo "No provider of '$1' found."
exit 104
;;
esac
exit 0
fi
for p in "${args[@]}"; do
case $p in su-dummy-*) ;; *)
echo "'$p' not found in package names. Trying capabilities."
echo "No provider of '$p' found."
exit 104
;;
esac
done
fake_mark_purged "${args[@]}"
;;
*)
echo "fake zypper: unhandled: $*" >&2
exit 99
;;
esac
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# The interactive grammar (stdin is the command channel; no TTY): apt backend, fixture rows
# [1] su-dummy-a [2] su-dummy-b [3] su-dummy-c (su-dummy-a depends on su-dummy-b).
load test_helper
setup() { common_setup; }
UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
@test "the session shows the list and the prompt help; q quits with exit 0, nothing upgraded" {
run skup --no-refresh <<<'q'
assert_success
assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
assert_line ' [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
assert_line '<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever'
assert_line '> q'
assert_line 'Quit: nothing was upgraded.'
log_lacks '^apt-get -y'
log_lacks '^apt-mark (un)?hold'
}
@test "an empty line proceeds: the native upgrade runs, exit 0" {
run skup <<<''
assert_success
assert_line 'Excluded from this run: none'
assert_line 'Upgrade finished.'
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
log_has "$UPGRADE"
}
@test "a whitespace-only line also proceeds" {
run skup --no-refresh <<<' '
assert_success
log_has "$UPGRADE"
}
@test "the exit status of the native upgrade is passed through" {
FAKE_APT_UPGRADE_RC=100 run skup --no-refresh <<<''
assert_failure 100
assert_output --partial 'the upgrade failed: apt exited with status 100'
}
@test "EOF aborts with exit 2 without upgrading" {
run skup --no-refresh <<<'1?'
assert_failure 2
assert_output --partial 'end of input: aborted, nothing was upgraded'
log_lacks '^apt-get -y'
}
@test "EOF straight away (empty stdin) also aborts with exit 2" {
run skup --no-refresh </dev/null
assert_failure 2
log_lacks '^apt-get -y'
}
@test "a last line without a newline is still executed before EOF" {
run skup --no-refresh < <(printf '2-')
assert_failure 2
assert_line 'excluded from this run: su-dummy-b'
}
@test "N? prints the description and the installed packages that depend on it" {
run skup --no-refresh <<<$'2?\nq'
assert_success
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
assert_line ' required by (installed): su-dummy-a'
}
@test "N?? prints the native full info" {
run skup --no-refresh <<<$'1??\nq'
assert_success
assert_line 'Package: su-dummy-a'
assert_line 'Depends: su-dummy-b (>= 2.0)'
log_has 'apt-cache show --no-all-versions su-dummy-a'
}
@test "N- toggles the exclusion and the '-' marker" {
run skup --no-refresh <<<$'2-\nl\n2-\nl\n2-\n'
assert_success
assert_line 'excluded from this run: su-dummy-b'
assert_line 're-included: su-dummy-b'
assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
assert_line 'Excluded from this run: su-dummy-b'
log_has 'apt-mark hold su-dummy-b'
log_has 'apt-mark unhold su-dummy-b'
}
@test "Nx asks y/N on the same stdin; y purges, the row leaves, numbers stay stable" {
run skup --no-refresh <<<$'3x\ny\nl\n3?\nq'
assert_success
assert_line 'Purge su-dummy-c now? [y/N] y'
assert_line 'purged: su-dummy-c'
log_has 'apt-get -y purge su-dummy-c'
log_lacks 'autoremove --purge'
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
[[ $(grep -c '^ \[3\] su-dummy-c' <<<"$output") -eq 1 ]]
assert_output --partial 'no package [3] in the list (l to redraw)'
}
@test "Nx answered with N, n, empty or anything else does not purge" {
for answer in N n '' maybe; do
: >"$FAKE_LOG"
run skup --no-refresh <<<"3x"$'\n'"$answer"$'\nq'
assert_success
assert_line 'not purged: su-dummy-c'
log_lacks '^apt-get -y'
done
}
@test "Nx: a failing purge keeps the row" {
FAKE_APT_PURGE_RC=100 run skup --no-refresh <<<$'3x\nyes\nl\nq'
assert_success
assert_output --partial 'purge of su-dummy-c failed (apt exited 100); the row stays in the list'
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
}
@test "Nx: EOF at the confirmation aborts with exit 2 and purges nothing" {
run skup --no-refresh <<<'3x'
assert_failure 2
log_lacks '^apt-get -y'
}
@test "Nx on a package other installed packages depend on is refused, like pacman -Rns; the row stays" {
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]'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
[[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 2 ]]
log_lacks '^apt-get -y'
}
@test "Nx removes the target with the dependencies only it needed, listed in the prompt" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nl\nq'
assert_success
assert_line 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N] y'
assert_line 'purged: su-dummy-a su-dummy-b'
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
}
@test "Nx: rows the purge took with it unannounced leave the list too (re-listed afterwards)" {
FAKE_APT_PURGE_ALSO=su-dummy-a run skup --no-refresh <<<$'3x\ny\nl\nq'
assert_success
assert_line ' (su-dummy-a is no longer upgradable, removed from the list)'
assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:'
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
}
@test "Nx on a new dependency (old version -, not installed) is refused" {
FAKE_APT_SIM=real run skup --no-refresh <<<$'9x\nq'
assert_success
assert_line 'su-dummy-newdep is not installed (the upgrade would install it as a new dependency): nothing to purge; use 9- to exclude it from this run.'
refute_output --partial '[y/N]'
log_lacks 'purge'
}
@test "Nx refused by the package manager asks nothing" {
FAKE_APT_SIM_PURGE_RC=100 run skup --no-refresh <<<$'3x\nq'
assert_success
assert_output --partial 'E: Unable to correct problems, you have held broken packages.'
assert_output --partial 'cannot purge su-dummy-c: apt refuses (see above); nothing was removed'
refute_output --partial '[y/N]'
log_lacks '^apt-get -y'
}
@test "purging the last packages ends the session cleanly (exit 0)" {
export FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b
run skup --no-refresh <<<$'3x\ny\n1x\ny'
assert_success
assert_line 'purged: su-dummy-c'
assert_line 'purged: su-dummy-a su-dummy-b'
assert_line 'Nothing left to upgrade.'
log_lacks "^apt-get -y -o"
}
@test "purging every listed row keeps the session while trusted (hidden) rows remain" {
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x\ny\nl\nq'
assert_success
assert_line '1 upgradable, 1 trusted (hidden, use --all to show), 0 to review:'
assert_line 'Quit: nothing was upgraded.'
}
@test "N+ trusts with the closure: rows leave, numbers stay, the hidden count grows" {
run skup --no-refresh <<<$'1+\nl\nq'
assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line '3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:'
assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
# [1] and [2] only appear in the first listing
[[ $(grep -c '^ \[1\] su-dummy-a' <<<"$output") -eq 1 ]]
[[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 1 ]]
run cut -f1-3 "$STATE_FILE"
assert_line "apt${T}su-dummy-a${T}user"
assert_line "apt${T}su-dummy-b${T}dep:su-dummy-a"
}
@test "trusting a package re-includes the excluded rows that this trust hides" {
run skup --no-refresh <<<$'2-\n1+\n\n'
assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line ' (su-dummy-b is trusted now: no longer excluded from this run)'
assert_line 'Excluded from this run: none'
log_lacks '^apt-mark hold'
}
@test "with --all a trusted row stays visible, so its exclusion stays too" {
run skup --no-refresh --all <<<$'2-\n1+\n\n'
assert_success
refute_output --partial 'no longer excluded'
assert_line 'Excluded from this run: su-dummy-b'
}
@test "trusted packages are hidden with a count; numbering covers the listed rows only" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh <<<$'1?\nq'
assert_success
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
assert_line ' [1] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
assert_line ' [2] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo'
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
}
@test "--all lists trusted packages too, marked with '='" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh --all <<<$'2+\nl\nq'
assert_success
assert_line '3 upgradable, 1 trusted (marked with =), 2 to review:'
assert_line '=[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
assert_line 'trusted su-dummy-b'
assert_line '3 upgradable, 2 trusted (marked with =), 1 to review:'
assert_line '=[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
}
@test "trusted packages are upgraded silently (never excluded)" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh <<<''
assert_success
assert_line 'Excluded from this run: none'
log_lacks '^apt-mark hold'
log_has "$UPGRADE"
}
@test "everything trusted: 0 to review, the prompt is still offered" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
"apt${T}su-dummy-b${T}user${T}2026-01-01T00:00:00Z" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh <<<''
assert_success
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
log_has "$UPGRADE"
}
@test "*+ trusts every listed package" {
run skup --no-refresh <<<$'*+\nl\nq'
assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line 'trusted su-dummy-b'
assert_line 'trusted su-dummy-c'
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-b${T}user"
}
@test "*- excludes every listed package; *- again re-includes them" {
run skup --no-refresh <<<$'*-\nl\n*-\nl\n*-\n'
assert_success
assert_line 'excluded all 3 listed packages from this run'
assert_line 're-included all 3 listed packages'
assert_line '-[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
assert_line 'Excluded from this run: su-dummy-a su-dummy-b su-dummy-c'
log_has 'apt-mark hold su-dummy-a su-dummy-b su-dummy-c'
}
@test "several tokens on one line run in order" {
run skup --no-refresh <<<$'1? 2- 3?\nl\nq'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
assert_line 'excluded from this run: su-dummy-b'
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
}
@test "an unknown token prints an error, runs nothing from that line and re-prompts" {
run skup --no-refresh <<<$'2- bogus\nl\n1+ 3y\nq'
assert_success
assert_output --partial "unknown command 'bogus' (h for help)"
assert_output --partial "unknown command '3y' (h for help)"
assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
refute_output --partial 'excluded from this run'
[[ ! -e $STATE_FILE ]]
}
@test "numbers that are not in the list are rejected" {
run skup --no-refresh <<<$'9?\n0-\n4x\nq'
assert_success
assert_output --partial 'no package [9] in the list (l to redraw)'
assert_output --partial 'no package [0] in the list (l to redraw)'
assert_output --partial 'no package [4] in the list (l to redraw)'
log_lacks '^apt-get -y'
}
@test "h and ? print the help" {
run skup --no-refresh <<<$'h\n?\nq'
assert_success
[[ $(grep -c 'N?? full native info' <<<"$output") -eq 2 ]]
assert_line 'Several tokens can be given on one line, e.g. "1? 3- 4+".'
}
@test "l redraws the list" {
run skup --no-refresh <<<$'l\nq'
assert_success
[[ $(grep -c '^3 upgradable' <<<"$output") -eq 2 ]]
}
@test "q in the middle of a line stops there (later tokens never run)" {
run skup --no-refresh <<<$'1? q 3x'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
refute_output --partial 'Purge'
}
@test "nothing to upgrade: no prompt, exit 0" {
FAKE_APT_SIM=none run skup --no-refresh </dev/null
assert_success
assert_line 'Nothing to upgrade.'
refute_output --partial '<enter> upgrade'
}
@test "held (kept back) rows can be reviewed like the others" {
echo su-dummy-c >"$FAKE_DIR/holds"
FAKE_APT_SIM=held run skup --no-refresh <<<$'3?\nq'
assert_success
assert_line ' [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]'
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
}
+401
View File
@@ -0,0 +1,401 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# Every backend parser, fed with the real outputs recorded in discovery/*.md
# (replayed by the fakes in tests/unit/fakes/, data in tests/unit/data/).
# each @test runs in its own subshell: per-test exports are intended
# shellcheck disable=SC2030,SC2031
load test_helper
setup() { common_setup; }
# ---- apt ----------------------------------------------------------------------------------------
@test "apt: Inst lines (all the edge cases) and the kept-back block (held, versions from policy)" {
FAKE_APT_SIM=real run skup list --no-refresh
assert_success
assert_line '10 upgradable, 0 trusted (hidden, use --all to show), 10 to review:'
assert_line --regexp '^ \[1\] libaudit-common +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$'
assert_line --regexp '^ \[2\] libaudit1 +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$'
assert_line --regexp '^ \[3\] libc6:i386 +2\.36-9 +-> +2\.36-9\+deb12u10 +Debian-Security:12/oldstable-security, Debian:12\.12/oldstable$'
assert_line --regexp '^ \[4\] libssl3t64 +3\.0\.13-0ubuntu3\.15 +-> +3\.0\.13-0ubuntu3\.16 +Ubuntu:24\.04/noble-updates, Ubuntu:24\.04/noble-security$'
assert_line --regexp '^ \[5\] perl-base +5\.36\.0-7\+deb12u3 +-> +5\.36\.0-7\+deb12u4 +http://deb\.debian\.org/debian-security bookworm-security/main amd64 \[held\]$'
assert_line --regexp '^ \[6\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$'
assert_line --regexp '^ \[7\] su-dummy-e +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$'
assert_line --regexp '^ \[8\] su-dummy-m +1\.0 +-> +2\.0 +su-fixture:su-repo$'
assert_line --regexp '^ \[9\] su-dummy-newdep +- +-> +1\.0 +su-fixture:su-repo$'
assert_line --regexp '^ \[10\] su-dummy-slow +0\.9 +-> +1\.0$'
refute_output --partial 'su-dummy-f'
log_has 'apt-get -s --with-new-pkgs upgrade'
log_has 'apt-cache policy perl-base su-dummy-c su-dummy-e'
}
@test "apt: a held package from the fixture shows up with the held flag" {
FAKE_APT_SIM=held run skup list --no-refresh
assert_success
assert_line --regexp '^ \[1\] su-dummy-a +1\.0 +-> +2\.0 +su-fixture:su-repo$'
assert_line --regexp '^ \[3\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$'
}
@test "apt: nothing to upgrade" {
FAKE_APT_SIM=none run skup list --no-refresh
assert_success
assert_line 'Nothing to upgrade.'
log_lacks '^apt-cache policy'
}
@test "apt: description = candidate Description; reverse deps deduplicated, '|' stripped" {
run skup info su-dummy-b
assert_success
assert_output "$(printf '%s\n' 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' ' required by (installed): su-dummy-a')"
log_has 'apt-cache show --no-all-versions su-dummy-b'
log_has 'apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances su-dummy-b'
run skup info gpgv
assert_success
assert_line 'gpgv: GNU privacy guard - signature verification tool'
assert_line ' required by (installed): apt'
}
@test "apt: the description also comes from Description-<lang> fields (Translation files)" {
mkdir -p "$BATS_TEST_TMPDIR/data/apt/show"
cp -r "$FAKE_DATA"/. "$BATS_TEST_TMPDIR/data/"
sed -i 's/^Description: /Description-en: /' "$BATS_TEST_TMPDIR/data/apt/show/su-dummy-a"
sed -i 's/^Description: /Description-pt_BR: /' "$BATS_TEST_TMPDIR/data/apt/show/su-dummy-b"
FAKE_DATA=$BATS_TEST_TMPDIR/data run skup info su-dummy-a
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
FAKE_DATA=$BATS_TEST_TMPDIR/data run skup info su-dummy-b
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
# Description-md5 is not a description
sed -i '/^Description-en: /d' "$BATS_TEST_TMPDIR/data/apt/show/su-dummy-a"
FAKE_DATA=$BATS_TEST_TMPDIR/data run skup info su-dummy-a
assert_line 'su-dummy-a: (no description available)'
}
@test "apt: info of an unknown package exits 1" {
run skup info no-such-pkg-xyz
assert_failure 1
assert_line 'no-such-pkg-xyz: (no description available: unknown package?)'
}
@test "apt: show is apt-cache show --no-all-versions (the candidate)" {
run skup show su-dummy-a
assert_success
assert_line 'Package: su-dummy-a'
assert_line 'Version: 2.0'
assert_line 'Depends: su-dummy-b (>= 2.0)'
run skup show no-such-pkg-xyz
assert_failure 1
assert_output --partial "no package information for 'no-such-pkg-xyz' (apt exited 100)"
}
# ---- pacman -------------------------------------------------------------------------------------
@test "pacman: -Qu joined with the FIRST -Sl repo; [ignored] => held" {
use_fakes pacman
FAKE_PACMAN_QU=real run skup list --no-refresh
assert_success
assert_line '6 upgradable, 0 trusted (hidden, use --all to show), 6 to review:'
assert_line ' [1] ca-certificates-mozilla 3.130-1 -> 3.131-1 core'
assert_line --regexp '^ \[2\] expat +2\.8\.5-1 +-> +2\.9\.0-1 +core \[held\]$'
assert_line --regexp '^ \[3\] glib2 +2\.88\.3-1 +-> +2\.90\.1-1 +core \[held\]$'
assert_line --regexp '^ \[5\] su-dummy-b +1\.0-1 +-> +2\.0-1 +su-dummy$'
refute_output --partial 'su-dummy2'
}
@test "pacman: rows survive an empty 'pacman -Sl' (no repo column instead of a broken join)" {
use_fakes pacman
FAKE_PACMAN_SL=empty run skup list --no-refresh
assert_success
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1'
}
@test "pacman: packages only the transaction (-Sup) shows become a note" {
use_fakes pacman
FAKE_PACMAN_SUP_EXTRA='su-dummy-new' run skup list --no-refresh
assert_success
assert_line 'Note: the upgrade also installs (new dependencies or replacements): su-dummy-new'
}
@test "pacman: nothing in -Qu but pending work in -Sup: the upgrade is still offered" {
use_fakes pacman
FAKE_PACMAN_QU=none FAKE_PACMAN_SUP_EXTRA='su-dummy-new' run skup --no-refresh <<<''
assert_success
assert_line '0 to review, but pacman reports pending changes (see the notes): <enter> runs the upgrade.'
log_has 'pacman -Su --noconfirm'
}
@test "pacman: nothing upgradable (-Qu exits 1 with no output)" {
use_fakes pacman
FAKE_PACMAN_QU=none run skup list --no-refresh
assert_success
assert_line 'Nothing to upgrade.'
}
@test "pacman: description from -Qi, falling back to -Si for packages that are not installed" {
use_fakes pacman
run skup info su-dummy-a
assert_success
assert_line 'su-dummy-a: Skeptical-updater dummy package A'
assert_line ' required by (installed): nothing'
run skup info su-dummy-new
assert_success
assert_line 'su-dummy-new: Skeptical-updater dummy package NEW'
}
@test "pacman: reverse deps from 'Required By', wrapped continuation lines joined" {
use_fakes pacman
run skup info glibc
assert_success
assert_line 'glibc: GNU C Library'
assert_line ' required by (installed): bash, gcc-libs, ncurses, readline'
run skup info su-dummy-b
assert_line ' required by (installed): su-dummy-a'
}
@test "pacman: the closure resolves provides (sonames, sh) and version constraints" {
use_fakes pacman
run skup trust bash
assert_success
assert_output 'trusted bash (+8 dependencies: filesystem gcc-libs glibc iana-etc linux-api-headers ncurses readline tzdata)'
run skup trust su-dummy-a
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
}
@test "pacman: show prints the installed (-Qi) and the candidate (-Si) records" {
use_fakes pacman
run skup show su-dummy-a
assert_success
assert_line 'Install Reason : Explicitly installed'
assert_line 'Repository : su-dummy'
assert_line 'Version : 2.0-1'
log_has 'pacman -Qi -- su-dummy-a'
log_has 'pacman -Si -- su-dummy-a'
run skup show nope
assert_failure 1
}
@test "pacman: every query runs with COLUMNS=0 (no wrapping)" {
use_fakes pacman
export COLUMNS=20
run skup info glibc
run skup list --no-refresh
run skup trust bash
run grep -v ' 0$' "$FAKE_DIR/pacman-columns"
assert_output ''
}
# ---- dnf ----------------------------------------------------------------------------------------
@test "dnf4: check-update (blank line, padding, Obsoleting section), old version from rpm -qa" {
use_fakes dnf rpm
run skup list --no-refresh
assert_success
assert_line '6 upgradable, 0 trusted (hidden, use --all to show), 6 to review:'
assert_line ' [1] gnutls 3.8.10-4.el9_8 -> 3.8.10-9.el9_8 baseos'
# installonly: the highest installed version is the old one
assert_line --regexp '^ \[2\] kernel-core +5\.14\.0-611\.5\.el9 +-> +5\.14\.0-612\.1\.el9 +baseos$'
assert_line --regexp '^ \[3\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$'
assert_line --regexp '^ \[5\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$'
# epoch kept on both sides
assert_line --regexp '^ \[6\] vim-minimal +2:8\.2\.2637-26\.el9_8\.21 +-> +2:8\.2\.2637-26\.el9_8\.23 +baseos$'
refute_line --regexp '\] su-dummy-new'
refute_output --partial 'su-dummy-old'
refute_output --partial 'Obsoleting'
assert_line 'Note: the upgrade also installs (obsoleting installed packages): su-dummy-new'
}
@test "dnf: check-update exit 100 with only obsoleting packages: 0 rows, but the upgrade is offered" {
use_fakes dnf rpm
FAKE_DNF_CHECK=obsoletes run skup --no-refresh <<<''
assert_success
assert_line '0 upgradable, 0 trusted (hidden, use --all to show), 0 to review:'
assert_line 'Note: the upgrade also installs (obsoleting installed packages): su-dummy-new'
assert_line '0 to review, but dnf reports pending changes (see the notes): <enter> runs the upgrade.'
refute_output --partial 'Nothing to upgrade.'
log_has 'dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade'
}
@test "dnf5: the 'Upgrades' header is skipped" {
use_fakes dnf rpm
FAKE_DNF_MAJOR=5 run skup list --no-refresh
assert_success
assert_line '5 upgradable, 0 trusted (hidden, use --all to show), 5 to review:'
assert_line --regexp '^ \[1\] ca-certificates +2025\.2\.80_v9\.0\.305-91\.el9 +-> +2026\.2\.90_v9\.0\.317-1\.fc44 +updates$'
assert_line --regexp '^ \[2\] curl +8\.18\.0-1\.fc44 +-> +8\.18\.0-12\.fc44 +updates$'
refute_output --partial 'Upgrades'
}
@test "dnf: check-update exit 0 means nothing to upgrade, exit 1 is an error (exit 1)" {
use_fakes dnf rpm
FAKE_DNF_CHECK_RC=0 run skup list --no-refresh
assert_success
assert_line 'Nothing to upgrade.'
FAKE_DNF_CHECK_RC=1 run skup list --no-refresh
assert_failure 1
assert_output --partial 'dnf check-update failed (exit 1)'
assert_output --partial 'could not list the upgradable packages'
}
@test "dnf: description from repoquery (candidate), rpm fallback when not upgradable" {
use_fakes dnf rpm
run skup info su-dummy-a
assert_success
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
run skup info su-dummy-d
assert_success
assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)'
assert_line ' required by (installed): nothing'
FAKE_DNF_MAJOR=5 run skup info su-dummy-a
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
}
@test "rpm reverse deps: provides + owned files joined with every requirement (rich deps too)" {
use_fakes dnf rpm
run skup info su-dummy-b
assert_line ' required by (installed): su-dummy-a, su-dummy-d'
run skup info su-dummy-c
assert_line ' required by (installed): su-dummy-d'
run skup info bash
assert_line ' required by (installed): filesystem, glibc-common, redhat-rpm-config, rpm, su-dummy-d'
}
@test "rpm closure: file deps, rich deps without their condition, rpmlib() dropped" {
use_fakes dnf rpm
run skup trust su-dummy-d
assert_success
assert_output 'trusted su-dummy-d (+11 dependencies: basesystem bash filesystem glibc glibc-common glibc-gconv-extra ncurses-libs setup su-dummy-a su-dummy-b su-dummy-c)'
refute_output --partial 'redhat-rpm-config'
refute_output --partial 'rpmlib'
run skup trust su-dummy-a
assert_output 'trusted su-dummy-a (its installed dependency was already trusted)'
}
@test "dnf4: a removal-table row wrapped onto two lines (long name) is one package" {
use_fakes dnf rpm
export FAKE_RPM_NO_D=1
FAKE_DNF_UNUSED=su-dummy-a:su-dummy-b FAKE_DNF_WRAP=1 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]'
refute_output --partial 'noarch'
log_has 'dnf -y remove su-dummy-a su-dummy-b'
}
@test "dnf5: a wrapped removal-table row is one package 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 FAKE_DNF_WRAP=1 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: show is dnf -q info" {
use_fakes dnf rpm
run skup show su-dummy-a
assert_success
assert_line 'Available Upgrades'
assert_line 'Version : 2.0'
log_has 'dnf -q info su-dummy-a'
}
# ---- zypper -------------------------------------------------------------------------------------
@test "zypper: --xmlout list-updates (one-line XML, multi-line descriptions, last record, entities)" {
use_fakes zypper rpm
FAKE_ZYPPER_LU=real run skup list --no-refresh
assert_success
assert_line '4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:'
assert_line ' [1] openSUSE-build-key 1.0-68.1 -> 1.0-69.1 repo-update'
assert_line --regexp '^ \[2\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$'
assert_line --regexp '^ \[3\] su-dummy-b +1\.0-1 +-> +2\.0-1 +openSUSE:repo-oss & co$'
assert_line --regexp '^ \[4\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$'
log_has 'zypper --non-interactive --xmlout list-updates'
}
@test "zypper on Tumbleweed (root): the review list is the dist-upgrade plan" {
use_fakes zypper rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
run skup list --no-refresh
assert_success
log_has 'zypper --non-interactive --no-refresh --xmlout dist-upgrade --dry-run'
log_lacks 'list-updates'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy'
refute_output --partial 'openSUSE-release'
}
@test "zypper on Tumbleweed: new packages are rows (old -), removals are a note" {
use_fakes zypper rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
FAKE_ZYPPER_DUP=removes run skup list --no-refresh
assert_success
assert_line ' [2] su-dummy-newdep - -> 1.0-1 su-dummy'
assert_line 'Note: the dist-upgrade also removes: libcurl-mini4'
}
@test "zypper on Tumbleweed: a plan with only removals is not 'Nothing to upgrade'" {
use_fakes zypper rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
FAKE_ZYPPER_DUP=onlyremove run skup --no-refresh <<<''
assert_success
assert_line '0 to review, but zypper reports pending changes (see the notes): <enter> runs the upgrade.'
log_has 'zypper --non-interactive --no-refresh dist-upgrade'
FAKE_ZYPPER_DUP=empty run skup --no-refresh </dev/null
assert_success
assert_line 'Nothing to upgrade.'
}
@test "zypper on Tumbleweed as non-root: list-updates with a note (the plan needs root)" {
use_fakes zypper rpm
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
SKEPTICAL_UPDATER_EUID=1000 run skup list --no-refresh
assert_success
log_has 'zypper --non-interactive --xmlout list-updates'
log_lacks 'dist-upgrade'
assert_line 'Note: not root: this is "zypper list-updates"; the dist-upgrade plan (which may differ) needs root'
}
@test "zypper: an empty update list" {
use_fakes zypper rpm
FAKE_ZYPPER_LU=none run skup list --no-refresh
assert_success
assert_line 'Nothing to upgrade.'
}
@test "zypper: description from 'zypper info' Summary, rpm fallback when zypper exits 104" {
use_fakes zypper rpm
run skup info su-dummy-a
assert_success
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
run skup info su-dummy-d
assert_success
assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)'
run skup info nothing-like-this
assert_failure 1
}
@test "zypper: show is zypper info --requires; unknown package => exit 1" {
use_fakes zypper rpm
run skup show su-dummy-a
assert_success
assert_line 'Information for package su-dummy-a:'
log_has 'zypper --non-interactive --quiet info --requires su-dummy-a'
run skup show nothing-like-this
assert_failure 1
assert_output --partial "(zypper exited 104)"
}
@test "zypper: reverse deps and closure use the same pure-rpm code" {
use_fakes zypper rpm
run skup info su-dummy-c
assert_line ' required by (installed): su-dummy-d'
run skup trust su-dummy-a
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
run cut -f1 "$STATE_FILE"
assert_line zypper
}
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# Robustness: the apt hold restore survives a closed stdout and repeated signals, locks and
# merge-on-save of the statefile, an inaccessible state directory.
# each @test runs in its own subshell: per-test exports are intended
# shellcheck disable=SC2030,SC2031
load test_helper
setup() { common_setup; }
# nobody_env: a world-reachable copy of the test environment under /tmp for running the applet as
# nobody (setpriv): $W/bin (tools), $W/fake (log), $W/tmp; sets W
nobody_env() {
((EUID == 0)) || skip "needs root to switch to an unprivileged user"
command -v setpriv >/dev/null 2>&1 || skip "setpriv not available"
W=$(mktemp -d /tmp/su-nobody.XXXXXX)
chmod 0755 "$W"
mkdir -p "$W/bin" "$W/fake" "$W/tmp" "$W/state"
chmod 0777 "$W/fake" "$W/tmp"
for t in "$SYSBIN"/*; do ln -s "$(readlink "$t")" "$W/bin/${t##*/}"; done
: >"$W/fake/log"
chmod 0666 "$W/fake/log"
}
# nobody CMD-ARGS...: run the applet as nobody (no flock in PATH: the mkdir lock fallback)
# shellcheck disable=SC2329 # invoked through `run`
nobody() {
rm -f "$W/bin/flock"
setpriv --reuid=65534 --regid=65534 --clear-groups -- env -u SKEPTICAL_UPDATER_EUID \
PATH="$FAKES/apt:$W/bin" FAKE_DIR="$W/fake" FAKE_LOG="$W/fake/log" TMPDIR="$W/tmp" \
SKEPTICAL_UPDATER_STATE_DIR="$W/state" "$SU_BIN" "$@"
}
@test "a closed stdout (the tee reading it was killed) does not stop the hold restore" {
FAKE_KILL_READER=1 run bash -c '
printf "2-\n\n" | env PATH="$1" "$2" --no-refresh |
{ echo "$BASHPID" >"$3/reader.pid"; while IFS= read -r _; do :; done; }
echo "applet status ${PIPESTATUS[1]}"' _ "$TEST_PATH" "$SU_BIN" "$FAKE_DIR"
assert_line 'applet status 141'
log_has 'apt-mark hold su-dummy-b'
log_has 'apt-mark unhold su-dummy-b'
run cat "$FAKE_DIR/holds"
assert_output ''
[[ ! -e $STATE/holds.added ]]
}
@test "a second signal while the exit handler restores the holds does not abort the restore" {
# 1st TERM during the upgrade starts the exit handler, a 2nd TERM arrives during its apt-mark unhold
FAKE_APT_UPGRADE_KILL=TERM FAKE_APT_MARK_UNHOLD_SIGNAL=TERM \
env PATH="$TEST_PATH" "$SU_BIN" --no-refresh <<<$'2-\n' >"$BATS_TEST_TMPDIR/out" 2>&1 &
pid=$!
echo "$pid" >"$FAKE_DIR/applet.pid"
rc=0
wait "$pid" || rc=$?
[[ $rc -eq 143 ]] || fail "exit status $rc: $(cat "$BATS_TEST_TMPDIR/out")"
log_has 'apt-mark unhold su-dummy-b'
run cat "$FAKE_DIR/holds"
assert_output ''
[[ ! -e $STATE/holds.added ]] || fail "the journal was left behind: $(cat "$STATE/holds.added")"
}
@test "the package manager does not inherit our lock descriptors" {
[[ -d /proc/self/fd ]] || skip "no /proc"
run skup --no-refresh <<<$'2-\n'
assert_success
log_has 'lock fds in the upgrade: 0'
}
@test "statefile writes merge: a row another process added meanwhile is kept" {
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
# while this session computes the closure of su-dummy-a, another process trusts zsh
FAKE_STATE_APPEND="apt${T}zsh${T}user${T}2026-02-02T00:00:00Z" run skup --no-refresh <<<$'1+\nq'
assert_success
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-a${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
assert_line "su-dummy-c${T}user"
assert_line "zsh${T}user"
}
@test "statefile writes merge: a row another process removed meanwhile stays removed" {
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
FAKE_STATE_SED='/su-dummy-c/d' run skup trust su-dummy-a
assert_success
run cut -f2 "$STATE_FILE"
refute_line su-dummy-c
assert_line su-dummy-a
}
@test "statefile writes merge: a removal applies only to the row as this session saw it" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
"apt${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
# refresh drops "stale" (no longer in the closure); meanwhile another process made it a user row
FAKE_STATE_SED='s/^apt\tstale\tdep:su-dummy-a/apt\tstale\tuser/' run skup refresh
assert_success
assert_output 'refreshed the dependency closures of 1 trusted root: 0 added, 1 removed'
run cut -f2,3 "$STATE_FILE"
assert_line "stale${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
}
@test "the statefile lock is flock(1) on trusted.lock, released after the write" {
run skup trust su-dummy-c
assert_success
[[ -f $STATE/trusted.lock ]]
flock -n "$STATE/trusted.lock" true
}
@test "without flock(1) a mkdir lock is used and removed again" {
rm -f "$SYSBIN/flock"
run skup trust su-dummy-c
assert_success
[[ ! -e $STATE/trusted.lock ]]
[[ ! -e $STATE/trusted.lock.d ]]
run cut -f2 "$STATE_FILE"
assert_line su-dummy-c
}
@test "mkdir lock: a stale lock (dead owner) is taken over, a live one is respected" {
rm -f "$SYSBIN/flock"
mkdir -p "$STATE/holds.lock.d"
echo su-dummy-b >"$STATE/holds.added"
echo su-dummy-b >"$FAKE_DIR/holds"
# live owner: this test's own shell
echo "$$" >"$STATE/holds.lock.d/pid"
run skup list --no-refresh
assert_success
assert_output --partial 'another skeptical-updater run is using'
[[ -e $STATE/holds.added ]]
# dead owner
sleep 0 &
dead=$!
wait "$dead"
echo "$dead" >"$STATE/holds.lock.d/pid"
run skup list --no-refresh
assert_success
assert_line 'Released temporary holds left over by an interrupted run: su-dummy-b'
[[ ! -e $STATE/holds.added ]]
[[ ! -e $STATE/holds.lock.d ]]
}
@test "mkdir lock: an owner of another user (kill -0 fails with EPERM) counts as alive" {
nobody_env
chown 65534 "$W/state"
# the lock is held by this test's shell (root): nobody cannot signal it
mkdir "$W/state/trusted.lock.d"
echo "$$" >"$W/state/trusted.lock.d/pid"
chown -R 65534 "$W/state/trusted.lock.d"
SKEPTICAL_UPDATER_LOCK_WAIT=1 run nobody trust su-dummy-c
assert_failure 1
assert_output --partial "another skeptical-updater is writing $W/state/trusted.tsv"
[[ $(cat "$W/state/trusted.lock.d/pid") == "$$" ]]
[[ ! -e $W/state/trusted.tsv ]]
rm -rf "$W"
}
@test "mkdir lock: a stale lock nobody can delete is moved aside (taken over), without a busy loop" {
nobody_env
chown 65534 "$W/state"
sleep 0 &
dead=$!
wait "$dead"
# a dead owner, and the lock directory belongs to root: nobody cannot delete it, only rename it
mkdir "$W/state/trusted.lock.d"
echo "$dead" >"$W/state/trusted.lock.d/pid"
SECONDS=0
SKEPTICAL_UPDATER_LOCK_WAIT=2 run timeout 30 bash -c "$(declare -f nobody); W=$W FAKES=$FAKES SU_BIN=$SU_BIN; nobody trust su-dummy-c"
assert_success
assert_output 'trusted su-dummy-c'
((SECONDS < 20))
[[ ! -e $W/state/trusted.lock.d ]]
rm -rf "$W"
}
@test "mkdir lock: a live lock that never goes away ends in the timeout" {
rm -f "$SYSBIN/flock"
mkdir -p "$STATE/trusted.lock.d"
echo "$$" >"$STATE/trusted.lock.d/pid"
SECONDS=0
SKEPTICAL_UPDATER_LOCK_WAIT=2 run timeout 30 env PATH="$TEST_PATH" "$SU_BIN" trust su-dummy-c
assert_failure 1
assert_output --partial 'another skeptical-updater is writing'
((SECONDS < 20))
[[ $(cat "$STATE/trusted.lock.d/pid") == "$$" ]]
}
@test "mkdir lock: the lock directory appears with its pid, no temporary directory is left behind" {
rm -f "$SYSBIN/flock"
FAKE_STATE_APPEND='' run skup trust su-dummy-c
assert_success
run ls -A "$STATE"
assert_output 'trusted.tsv'
}
@test "mkdir lock: two takeovers of one stale lock - the late one finds the new live lock and leaves it" {
rm -f "$SYSBIN/flock"
mkdir -p "$STATE/holds.lock.d"
sleep 0 &
dead=$!
wait "$dead"
echo "$dead" >"$STATE/holds.lock.d/pid"
echo su-dummy-b >"$STATE/holds.added"
echo su-dummy-b >"$FAKE_DIR/holds"
# "C" sees the dead owner, then pauses before its takeover ...
SKEPTICAL_UPDATER_LOCK_WAIT=2 SKEPTICAL_UPDATER_LOCK_TAKEOVER_PAUSE=2 env PATH="$TEST_PATH" "$SU_BIN" --no-refresh <<<$'2-\n' >"$BATS_TEST_TMPDIR/c.out" 2>&1 &
c=$!
sleep 1
# ... meanwhile "B" took the stale lock over: the lock now belongs to a live process (this shell)
rm -rf "$STATE/holds.lock.d"
mkdir "$STATE/holds.lock.d"
echo "$$" >"$STATE/holds.lock.d/pid"
rc=0
wait "$c" || rc=$?
# C must not have removed B's live lock (it does not get it, and neither do its holds)
[[ $(cat "$STATE/holds.lock.d/pid") == "$$" ]] || fail "the live lock was taken: $(cat "$BATS_TEST_TMPDIR/c.out")"
run ls -A "$STATE"
refute_output --partial 'stale'
rm -rf "$STATE/holds.lock.d"
}
@test "mkdir lock: temporary and renamed lock directories of crashed acquirers are removed" {
rm -f "$SYSBIN/flock"
mkdir -p "$STATE/.trusted.lock.Crash1" "$STATE/.trusted.lock.Crash2" "$STATE/trusted.lock.d.stale.999999" "$STATE/.trusted.lock.Live01"
sleep 0 &
dead=$!
wait "$dead"
echo "$dead" >"$STATE/.trusted.lock.Crash1/pid"
touch -d '2 hours ago' "$STATE/.trusted.lock.Crash2"
echo "$dead" >"$STATE/trusted.lock.d.stale.999999/pid"
echo "$$" >"$STATE/.trusted.lock.Live01/pid"
run skup trust su-dummy-c
assert_success
run ls -A "$STATE"
assert_output "$(printf '%s\n' .trusted.lock.Live01 trusted.tsv)"
}
@test "a state directory that cannot be entered is an error, not 'no trusted packages'" {
((EUID == 0)) || skip "needs root to switch to an unprivileged user"
command -v setpriv >/dev/null 2>&1 || skip "setpriv not available"
w=$(mktemp -d /tmp/su-nobody.XXXXXX)
chmod 0755 "$w"
mkdir -p "$w/bin" "$w/fake" "$w/tmp" "$w/state"
chmod 0777 "$w/fake" "$w/tmp"
for t in "$SYSBIN"/*; do ln -s "$(readlink "$t")" "$w/bin/${t##*/}"; done
: >"$w/fake/log"
chmod 0666 "$w/fake/log"
printf '%s\n' "$HEADER" >"$w/state/trusted.tsv"
chmod 0000 "$w/state"
nobody() {
setpriv --reuid=65534 --regid=65534 --clear-groups -- env -u SKEPTICAL_UPDATER_EUID \
PATH="$FAKES/apt:$w/bin" FAKE_DIR="$w/fake" FAKE_LOG="$w/fake/log" TMPDIR="$w/tmp" \
SKEPTICAL_UPDATER_STATE_DIR="$w/state" "$SU_BIN" "$@"
}
run nobody trusted
assert_failure 1
assert_output --partial "cannot access the state directory $w/state (permission denied)"
run nobody list --no-refresh
assert_failure 1
assert_output --partial "cannot access the state directory $w/state (permission denied)"
# a readable directory with an unreadable statefile
chmod 0755 "$w/state"
chmod 0600 "$w/state/trusted.tsv"
run nobody trusted
assert_failure 1
assert_output --partial "cannot read the statefile $w/state/trusted.tsv (permission denied)"
rm -rf "$w"
}
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# Root handling: what needs root, sudo re-exec with identical arguments, clear failures,
# and the commands that must work unprivileged.
load test_helper
setup() {
common_setup
export SKEPTICAL_UPDATER_EUID=1000
}
@test "the interactive upgrade as non-root without sudo fails clearly (exit 1)" {
run skup <<<''
assert_failure 1
assert_output --partial 'upgrading needs root and sudo is not available; run skeptical-updater as root (or use --dry-run)'
[[ -z $(cat "$FAKE_LOG") ]]
}
@test "list with a refresh needs root; without sudo it fails clearly" {
run skup list
assert_failure 1
assert_output --partial 'refreshing the package lists needs root (use --no-refresh)'
log_lacks 'update'
}
@test "list --no-refresh works unprivileged" {
run skup list --no-refresh
assert_success
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
}
@test "trusted, info and show work unprivileged" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z"
run skup trusted
assert_success
assert_line "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z"
run skup info su-dummy-b
assert_success
assert_line ' required by (installed): su-dummy-a'
run skup show su-dummy-a
assert_success
assert_line 'Package: su-dummy-a'
}
@test "--dry-run works unprivileged: the refresh is printed, not run" {
run skup --dry-run <<<''
assert_success
assert_line 'Refreshing package lists... skipped (not root), the command would be:'
assert_line '+ apt-get update -qq -o APT::Update::Error-Mode=any'
assert_line '+ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade'
log_lacks '^apt-get update'
log_lacks '^apt-get -y'
}
@test "with sudo available, non-root re-execs through sudo: this file itself, same arguments, hidden marker" {
use_fakes apt sudo
run skup --all list
assert_success
log_has "sudo -- $SU_BIN --sudo-reexec --state-dir=$STATE --all list"
log_lacks '^apt-get'
}
@test "the sudo re-exec carries --no-color when NO_COLOR is set" {
use_fakes apt sudo
unset SKEPTICAL_UPDATER_STATE_DIR
NO_COLOR=1 run skup --backend=apt
assert_success
log_has "sudo -- $SU_BIN --sudo-reexec --no-color --backend=apt"
}
@test "an explicit --state-dir is passed through unchanged" {
use_fakes apt sudo
run skup --state-dir=/srv/x list
assert_success
log_has "sudo -- $SU_BIN --sudo-reexec --state-dir=/srv/x list"
}
@test "a copy that is not executable and not in PATH is re-run through bash with its absolute path" {
use_fakes apt sudo
mkdir -p "$BATS_TEST_TMPDIR/copy"
cp "$SU_BIN" "$BATS_TEST_TMPDIR/copy/su-copy"
chmod 0644 "$BATS_TEST_TMPDIR/copy/su-copy"
cd "$BATS_TEST_TMPDIR/copy"
run env PATH="$TEST_PATH" bash su-copy list
assert_success
log_matches "^sudo -- /[^ ]*/bash $BATS_TEST_TMPDIR/copy/su-copy --sudo-reexec --state-dir=$STATE list\$"
}
@test "an executable copy invoked through bash is still re-run directly (sudoers rules match the file)" {
use_fakes apt sudo
mkdir -p "$BATS_TEST_TMPDIR/copy"
cp "$SU_BIN" "$BATS_TEST_TMPDIR/copy/su-copy"
chmod 0755 "$BATS_TEST_TMPDIR/copy/su-copy"
run env PATH="$TEST_PATH" bash "$BATS_TEST_TMPDIR/copy/su-copy" list
assert_success
log_has "sudo -- $BATS_TEST_TMPDIR/copy/su-copy --sudo-reexec --state-dir=$STATE list"
}
@test "after a sudo re-exec that did not make us root, there is no second re-exec" {
use_fakes apt sudo
run skup --sudo-reexec list
assert_failure 1
assert_output --partial 're-executed through sudo but still not running as root (euid 1000); not trying again'
log_lacks '^sudo'
}
@test "--sudo-reexec is only recognised as the first argument and is never forwarded" {
run skup list --sudo-reexec
assert_failure 1
assert_output --partial 'unknown option: --sudo-reexec'
SKEPTICAL_UPDATER_EUID=0 run skup --sudo-reexec list --no-refresh
assert_success
log_has 'apt-get -s --with-new-pkgs upgrade'
log_lacks 'sudo-reexec'
}
@test "trust into a writable state dir does not need root" {
use_fakes apt sudo
run skup trust su-dummy-c
assert_success
assert_output 'trusted su-dummy-c'
log_lacks '^sudo'
}
@test "trust into a non-writable state dir re-execs through sudo" {
ro=$BATS_TEST_DIRNAME/no-such-state-dir
[[ -w $BATS_TEST_DIRNAME ]] && skip "the repository is writable here (the suite mounts it read-only in Docker)"
use_fakes apt sudo
run skup --state-dir="$ro" trust su-dummy-c
assert_success
log_has "sudo -- $SU_BIN --sudo-reexec --state-dir=$ro trust su-dummy-c"
}
@test "--dry-run never re-execs through sudo" {
use_fakes apt sudo
run skup --dry-run list
assert_success
log_lacks '^sudo'
}
@test "as root nothing is re-executed" {
use_fakes apt sudo
SKEPTICAL_UPDATER_EUID=0 run skup list
assert_success
log_lacks '^sudo'
log_has 'apt-get update -qq -o APT::Update::Error-Mode=any'
}
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# The statefile: creation, format, sorting, dedupe, comments, atomic writes, trust/untrust/refresh.
load test_helper
setup() { common_setup; }
@test "trust creates the directory (0755) and the file (0644) with the header" {
run skup trust su-dummy-c
assert_success
assert_output 'trusted su-dummy-c'
[[ $(stat -c %a "$STATE") == 755 ]]
[[ $(stat -c %a "$STATE_FILE") == 644 ]]
run cat "$STATE_FILE"
assert_line --index 0 "$HEADER"
assert_line --index 1 --regexp "^apt${T}su-dummy-c${T}user${T}${DATE_RE}\$"
[[ ${#lines[@]} -eq 2 ]]
}
@test "intermediate directories are created too" {
run skup --state-dir="$BATS_TEST_TMPDIR/a/b/c" trust su-dummy-c
assert_success
[[ -f $BATS_TEST_TMPDIR/a/b/c/trusted.tsv ]]
[[ $(stat -c %a "$BATS_TEST_TMPDIR/a/b/c") == 755 ]]
}
@test "trust writes the user row and dep:<root> rows for the installed closure" {
run skup trust su-dummy-a
assert_success
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
run cat "$STATE_FILE"
assert_line --index 1 --regexp "^apt${T}su-dummy-a${T}user${T}${DATE_RE}\$"
assert_line --index 2 --regexp "^apt${T}su-dummy-b${T}dep:su-dummy-a${T}${DATE_RE}\$"
log_has "dpkg-query -W -f \${db:Status-Abbrev}\\t\${Package}\\t\${Pre-Depends}\\t\${Depends}\\t\${Provides}\\n"
}
@test "the apt closure follows Provides (virtual packages) and alternatives" {
run skup trust su-dummy-w
assert_success
assert_output 'trusted su-dummy-w (+1 dependency: su-dummy-v)'
}
@test "the apt closure walks real chains through virtual packages (base-files -> awk -> mawk)" {
run skup trust base-files
assert_success
assert_output 'trusted base-files (+4 dependencies: gcc-12-base libc6 libgcc-s1 mawk)'
run cut -f2,3 "$STATE_FILE"
assert_line "mawk${T}dep:base-files"
assert_line "gcc-12-base${T}dep:base-files"
}
@test "a not-installed (rc) package is not part of a closure" {
run skup trust su-dummy-f
assert_success
assert_output 'trusted su-dummy-f'
}
@test "records are sorted on (backend, package); comments are kept in order" {
write_state '# my own note' \
"pacman${T}zsh${T}user${T}2026-01-01T00:00:00Z" \
"apt${T}zsh${T}user${T}2026-01-01T00:00:00Z" \
'# second note' \
"apt${T}foo-bar${T}user${T}2026-01-01T00:00:00Z" \
"apt${T}foo${T}dep:zsh${T}2026-01-01T00:00:00Z" \
"dnf${T}abc${T}user${T}2026-01-01T00:00:00Z"
run skup trust su-dummy-c
assert_success
run cat "$STATE_FILE"
assert_line --index 0 '# my own note'
assert_line --index 1 '# second note'
assert_line --index 2 "apt${T}foo${T}dep:zsh${T}2026-01-01T00:00:00Z"
assert_line --index 3 "apt${T}foo-bar${T}user${T}2026-01-01T00:00:00Z"
assert_line --index 4 --regexp "^apt${T}su-dummy-c${T}user${T}"
assert_line --index 5 "apt${T}zsh${T}user${T}2026-01-01T00:00:00Z"
assert_line --index 6 "dnf${T}abc${T}user${T}2026-01-01T00:00:00Z"
assert_line --index 7 "pacman${T}zsh${T}user${T}2026-01-01T00:00:00Z"
[[ ${#lines[@]} -eq 8 ]]
}
@test "duplicates are collapsed: a user row wins over a dep row, else the first one" {
write_state "$HEADER" \
"apt${T}b${T}dep:x${T}2026-01-01T00:00:00Z" \
"apt${T}b${T}user${T}2026-02-02T00:00:00Z" \
"apt${T}c${T}dep:x${T}2026-03-03T00:00:00Z" \
"apt${T}c${T}dep:y${T}2026-04-04T00:00:00Z"
run skup trust su-dummy-c
assert_success
run cat "$STATE_FILE"
assert_line "apt${T}b${T}user${T}2026-02-02T00:00:00Z"
assert_line "apt${T}c${T}dep:x${T}2026-03-03T00:00:00Z"
refute_line --partial 'dep:y'
refute_line --partial "b${T}dep:x"
}
@test "a user row is never downgraded to dep:" {
run skup trust su-dummy-b
run skup trust su-dummy-a
assert_success
assert_output 'trusted su-dummy-a (its installed dependency was already trusted)'
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-b${T}user"
refute_line --partial 'dep:'
}
@test "an existing dep: row is upgraded to user, keeping one row" {
run skup trust su-dummy-a
run skup trust su-dummy-b
assert_success
assert_output 'trusted su-dummy-b'
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-b${T}user"
[[ $(grep -c su-dummy-b "$STATE_FILE") -eq 1 ]]
}
@test "trusting an already trusted package reports it and changes nothing" {
run skup trust su-dummy-c
before=$(cat "$STATE_FILE")
run skup trust su-dummy-c
assert_success
assert_output 'su-dummy-c was already trusted'
[[ $(cat "$STATE_FILE") == "$before" ]]
}
@test "malformed lines are kept verbatim, with a warning" {
write_state "$HEADER" "apt${T}only-two" "apt${T}x${T}weird${T}2026-01-01T00:00:00Z"
run skup trust su-dummy-c
assert_success
assert_output --partial "trusted.tsv:2: malformed line kept verbatim: apt${T}only-two"
assert_output --partial "trusted.tsv:3: malformed line kept verbatim"
run cat "$STATE_FILE"
assert_line "apt${T}only-two"
assert_line "apt${T}x${T}weird${T}2026-01-01T00:00:00Z"
}
@test "blank lines and CRLF line ends are tolerated" {
printf '%s\r\n\r\n%s\r\n\n' "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" >"$BATS_TEST_TMPDIR/st"
mkdir -p "$STATE"
mv "$BATS_TEST_TMPDIR/st" "$STATE_FILE"
run skup list --no-refresh
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
}
@test "writes are atomic (new file renamed over the old one) and leave no temp file" {
run skup trust su-dummy-c
ino1=$(stat -c %i "$STATE_FILE")
run skup trust su-dummy-a
assert_success
ino2=$(stat -c %i "$STATE_FILE")
[[ $ino1 != "$ino2" ]]
run ls -A "$STATE"
assert_output "$(printf '%s\n' trusted.lock trusted.tsv)"
}
@test "a failing write leaves the old file intact" {
run skup trust su-dummy-c
before=$(cat "$STATE_FILE")
# make the state dir read-only (root ignores directory modes: skip then)
chmod 0555 "$STATE"
if [[ -w $STATE ]]; then
chmod 0755 "$STATE"
skip "running as root: directory modes are not enforced"
fi
run skup trust su-dummy-a
chmod 0755 "$STATE"
assert_failure 1
[[ $(cat "$STATE_FILE") == "$before" ]]
}
@test "untrust removes the row for PKG and every dep:PKG row, nothing else" {
write_state "$HEADER" \
"apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
"apt${T}x${T}dep:y${T}2026-01-01T00:00:00Z" \
"apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z" \
"pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
"pacman${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
run skup untrust su-dummy-a
assert_success
assert_output 'untrusted su-dummy-a (2 rows removed)'
run cat "$STATE_FILE"
assert_output "$(printf '%s\n' "$HEADER" \
"apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z" \
"apt${T}x${T}dep:y${T}2026-01-01T00:00:00Z" \
"pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
"pacman${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z")"
}
@test "untrust of a dependency-only package removes its dep row" {
run skup trust su-dummy-a
run skup untrust su-dummy-b
assert_success
assert_output 'untrusted su-dummy-b (1 row removed)'
run cut -f2 "$STATE_FILE"
assert_line su-dummy-a
refute_line su-dummy-b
}
@test "untrust of a package that is not trusted warns, exit 0" {
run skup untrust nope
assert_success
assert_output --partial 'warning: nope is not trusted (no row for backend apt)'
[[ ! -e $STATE_FILE ]]
}
@test "refresh recomputes closures: adds missing rows, drops stale ones, keeps the rest" {
write_state "$HEADER" \
"apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" \
"apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
"apt${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z" \
"apt${T}orphan${T}dep:gone${T}2026-01-01T00:00:00Z" \
"apt${T}su-dummy-w${T}user${T}2026-01-01T00:00:00Z" \
"pacman${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
run skup refresh
assert_success
assert_output 'refreshed the dependency closures of 2 trusted roots: 1 added, 2 removed'
run cat "$STATE_FILE"
assert_line "apt${T}su-dummy-b${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
assert_line --regexp "^apt${T}su-dummy-v${T}dep:su-dummy-w${T}${DATE_RE}\$"
refute_line --partial "apt${T}stale"
refute_line --partial 'orphan'
assert_line "pacman${T}stale${T}dep:su-dummy-a${T}2026-01-01T00:00:00Z"
assert_line "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
}
@test "refresh with no user roots for this backend does nothing" {
write_state "$HEADER" "pacman${T}x${T}user${T}2026-01-01T00:00:00Z"
run skup refresh
assert_success
assert_output 'No trusted root packages for backend apt: nothing to refresh.'
}
@test "trusted prints the statefile verbatim" {
write_state "$HEADER" '# note' "apt${T}x${T}user${T}2026-01-01T00:00:00Z"
run skup trusted
assert_success
assert_output "$(printf '%s\n' "$HEADER" '# note' "apt${T}x${T}user${T}2026-01-01T00:00:00Z")"
}
@test "trusted without a statefile says so, exit 0" {
run skup trusted
assert_success
assert_output "No trusted packages yet ($STATE_FILE does not exist)."
}
@test "only rows of the current backend hide packages" {
write_state "$HEADER" "pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup list --no-refresh
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
assert_line --regexp '^ \[1\] su-dummy-a '
}
@test "the reason does not matter for hiding: dep rows hide too" {
write_state "$HEADER" "apt${T}su-dummy-b${T}dep:whatever${T}2026-01-01T00:00:00Z"
run skup list --no-refresh
assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:'
refute_output --partial 'su-dummy-b'
}
+86
View File
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# tests/unit/test_helper.bash - common setup for the unit tests (sourced via `load test_helper`).
# The applet runs with a hermetic PATH: fake package managers (tests/unit/fakes/<dir>) in front
# of a directory with symlinks to the few system tools it needs. No root, no network, no real
# package manager; everything is written under $BATS_TEST_TMPDIR.
# shellcheck shell=bash
load '../vendor/bats-support/load'
load '../vendor/bats-assert/load'
SU_BIN=$(cd "$BATS_TEST_DIRNAME/../.." && pwd)/skeptical-updater
FAKES=$BATS_TEST_DIRNAME/fakes
export FAKE_DATA=$BATS_TEST_DIRNAME/data
SYS_TOOLS=(bash env cat sort sed grep tr cut join awk mktemp mv mkdir chmod rm cp ln date ls head tail wc touch sleep kill flock readlink timeout stat sha256sum)
common_setup() {
SYSBIN=$BATS_TEST_TMPDIR/sysbin
mkdir -p "$SYSBIN"
local t p
for t in "${SYS_TOOLS[@]}"; do
p=$(type -P "$t") || continue
ln -sf "$p" "$SYSBIN/$t"
done
export FAKE_DIR=$BATS_TEST_TMPDIR/fake
mkdir -p "$FAKE_DIR"
: >"$FAKE_DIR/holds"
export FAKE_LOG=$FAKE_DIR/log
: >"$FAKE_LOG"
export STATE=$BATS_TEST_TMPDIR/state
export STATE_FILE=$STATE/trusted.tsv
export SKEPTICAL_UPDATER_STATE_DIR=$STATE
export SKEPTICAL_UPDATER_EUID=0
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-debian12
export TMPDIR=$BATS_TEST_TMPDIR
export FAKE_PACMAN_DBPATH=$FAKE_DIR/pacman-db
mkdir -p "$FAKE_PACMAN_DBPATH/sync" "$FAKE_PACMAN_DBPATH/local"
unset NO_COLOR ZYPP_CONF COLUMNS SKEPTICAL_UPDATER_LOCK_WAIT
use_fakes apt
}
# use_fakes DIR...: the applet sees only these fake dirs + the system tools
use_fakes() {
local d p=
for d in "$@"; do p+=$FAKES/$d:; done
TEST_PATH=$p$SYSBIN
}
# skup ARGS...: run the applet (use as `run skup ...`, stdin is passed through)
skup() { env PATH="$TEST_PATH" "$SU_BIN" "$@"; }
# shellcheck disable=SC2034 # used by the .bats files
HEADER='# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
# shellcheck disable=SC2034
DATE_RE='[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z'
# shellcheck disable=SC2034
T=$'\t'
# write_state LINE...: create the statefile with exactly these lines
write_state() {
mkdir -p "$STATE"
printf '%s\n' "$@" >"$STATE_FILE"
}
# log_has LINE: the fake log contains exactly this line
log_has() {
grep -qxF -- "$1" "$FAKE_LOG" || {
printf 'expected log line: %s\n--- fake log:\n%s\n' "$1" "$(cat "$FAKE_LOG")" >&2
return 1
}
}
# log_matches REGEX: some fake log line matches
log_matches() {
grep -qE -- "$1" "$FAKE_LOG" || {
printf 'expected a log line matching: %s\n--- fake log:\n%s\n' "$1" "$(cat "$FAKE_LOG")" >&2
return 1
}
}
# log_lacks REGEX: no fake log line matches
log_lacks() {
if grep -qE -- "$1" "$FAKE_LOG"; then
printf 'unexpected log line matching: %s\n--- fake log:\n%s\n' "$1" "$(cat "$FAKE_LOG")" >&2
return 1
fi
}