N?? and `show PKG` now print the installed record in the package
manager's own key/value layout, after one line explaining the format:
# lines starting with > come from the upstream candidate: a differing
value, or a field the installed record lacks
After each field whose candidate value differs comes the candidate's
line prefixed with "> "; fields only the candidate has are appended the
same way; installed-only fields stay; identical fields print once.
Multi-line values (pacman Optional Deps, dnf Description, RFC-822
continuation lines) are compared as blocks and a differing candidate
block gets "> " on every line. A package that is not installed shows the
candidate alone ("# not installed: this is the upstream candidate").
Merged where the backend gives comparable records for both sides:
- pacman: -Qi vs -Si REPO/PKG, REPO = the first repository -Sl lists
- apt: dpkg-query -s (Status, Conffiles, the long description; richer
than apt-cache show PKG=<installed>, which gives the archive record
when that version is still listed) vs apt-cache show --no-all-versions;
Description-xx pairs with Description and a one-line candidate
Description equal to the installed first line counts as identical
(the archives ship the long text in Translation files)
- dnf: info --installed vs info --upgrades (--available lists other
architectures first and nothing for an up-to-date package); not
installed: --available; dnf5's differently padded keys are re-padded
zypper keeps `zypper info --requires`: rpm -qi and zypper info share
almost no keys and zypper cannot print the installed record itself.
No candidate, or not exactly one record per side, keeps the old output.
Formats verified in containers and recorded in discovery/*.md. Unit
tests replay recorded pacman, apt, dnf4 and dnf5 records; the E2E suite
checks 2?? on su-dummy-b on all 12 distros.
742 lines
31 KiB
Bash
742 lines
31 KiB
Bash
#!/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' 'reason: installed as a dependency' 'required by: 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: apt'
|
|
}
|
|
|
|
@test "apt: install reason from apt-mark showauto / showmanual, dpkg for a package in neither list" {
|
|
run skup info su-dummy-a
|
|
assert_success
|
|
assert_output "$(printf '%s\n' 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests' 'reason: explicitly installed' 'required by: nothing')"
|
|
log_has 'apt-mark showauto su-dummy-a'
|
|
log_has 'apt-mark showmanual su-dummy-a'
|
|
log_lacks '^dpkg-query -W -f \$\{db:Status-Abbrev\} '
|
|
run skup info su-dummy-b
|
|
assert_line 'reason: installed as a dependency'
|
|
# apt-mark prints a native package without the ":arch" it was asked for
|
|
run skup info libc6:amd64
|
|
assert_line 'reason: installed as a dependency'
|
|
# installed but in neither list: manual
|
|
FAKE_APT_MARK_NEITHER=su-dummy-c run skup info su-dummy-c
|
|
assert_success
|
|
assert_line 'reason: explicitly installed'
|
|
# shellcheck disable=SC2016 # dpkg-query's own ${...} format, literally
|
|
log_has 'dpkg-query -W -f ${db:Status-Abbrev} su-dummy-c'
|
|
# removed with its configuration files kept (rc), or unknown to dpkg: not installed
|
|
run skup info su-dummy-f
|
|
assert_line 'reason: not installed'
|
|
run skup info gpgv
|
|
assert_success
|
|
assert_line 'reason: not installed'
|
|
# apt-mark cannot tell
|
|
FAKE_APT_MARK_SHOW_RC=100 run skup info su-dummy-a
|
|
assert_success
|
|
assert_line 'reason: unknown'
|
|
}
|
|
|
|
@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 merges dpkg's installed record (dpkg-query -s) with the candidate (apt-cache show)" {
|
|
# changed Version, local-only Status/Conffiles, identical fields once, a differing multi-line
|
|
# Description (prefixed on every line), candidate-only fields appended
|
|
run skup show su-dummy-b
|
|
assert_success
|
|
assert_output "$(cat <<'EOF'
|
|
# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks
|
|
Package: su-dummy-b
|
|
Status: install ok installed
|
|
Priority: optional
|
|
Section: misc
|
|
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
|
|
Architecture: all
|
|
Version: 1.0
|
|
> Version: 2.0
|
|
Conffiles:
|
|
/etc/su-dummy/su-dummy-b.conf 607e2d3a119dfe832d937ce579ef1079
|
|
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 1.0).
|
|
> 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
|
|
EOF
|
|
)"
|
|
log_has 'dpkg-query -s -- su-dummy-b'
|
|
log_has 'apt-cache show --no-all-versions su-dummy-b'
|
|
run skup show su-dummy-a
|
|
assert_success
|
|
assert_line 'Depends: su-dummy-b'
|
|
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)"
|
|
}
|
|
|
|
@test "apt: a one-line candidate Description (the archives' short form) equals the installed long one" {
|
|
# real debian:12 records: perl-base 5.36.0-7+deb12u3 installed, +deb12u4 from bookworm-security
|
|
run skup show perl-base
|
|
assert_success
|
|
[[ $(grep -c '^Description: minimal Perl system$' <<<"$output") -eq 1 ]]
|
|
refute_line --partial '> Description:'
|
|
assert_output --partial "$(printf '%s\n' 'Installed-Size: 7639' '> Installed-Size: 7643')"
|
|
assert_output --partial "$(printf '%s\n' 'Version: 5.36.0-7+deb12u3' '> Version: 5.36.0-7+deb12u4')"
|
|
[[ $(grep -c '^Breaks: ' <<<"$output") -eq 1 ]]
|
|
refute_line --regexp '^> (Breaks|Pre-Depends|Essential|Source):'
|
|
assert_line --index -1 '> SHA256: d7d1943aec9597629bf73075efcc5ef6dc9bda96d78e80d843156ecd448478b8'
|
|
}
|
|
|
|
@test "apt: show of a package that is not installed prints the candidate alone, with a note" {
|
|
run skup show zsh
|
|
assert_success
|
|
assert_line --index 0 '# not installed: this is the upstream candidate'
|
|
assert_line --index 1 'Package: zsh'
|
|
assert_line --index 3 'Version: 5.9-4+b15'
|
|
refute_line --partial '> '
|
|
log_has 'dpkg-query -s -- zsh'
|
|
}
|
|
|
|
# ---- 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: 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: bash, gcc-libs, ncurses, readline'
|
|
run skup info su-dummy-b
|
|
assert_line 'required by: su-dummy-a'
|
|
}
|
|
|
|
@test "pacman: install reason from the 'Install Reason' of -Qi; -Qi failing = not installed" {
|
|
use_fakes pacman
|
|
run skup info su-dummy-a
|
|
assert_success
|
|
assert_output "$(printf '%s\n' 'su-dummy-a: Skeptical-updater dummy package A' 'reason: explicitly installed' 'required by: nothing')"
|
|
run skup info su-dummy-b
|
|
assert_success
|
|
assert_output "$(printf '%s\n' 'su-dummy-b: Skeptical-updater dummy package B (dependency of A)' 'reason: installed as a dependency' 'required by: su-dummy-a')"
|
|
run skup info glibc
|
|
assert_line 'reason: installed as a dependency'
|
|
# in a sync database only
|
|
run skup info su-dummy-new
|
|
assert_success
|
|
assert_line 'reason: not installed'
|
|
# a value this tool does not know, or no such field
|
|
FAKE_PACMAN_REASON='su-dummy-c=Installed for a reason pacman 9 invented' run skup info su-dummy-c
|
|
assert_success
|
|
assert_line 'reason: unknown'
|
|
FAKE_PACMAN_REASON='su-dummy-c=' run skup info su-dummy-c
|
|
assert_line 'reason: unknown'
|
|
log_has 'pacman -Qi -- su-dummy-c'
|
|
}
|
|
|
|
@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 merges -Qi with -Si of the candidate's repository: '>' lines for what differs" {
|
|
# real archlinux records (glib2 2.88.3-1 installed, core 2.90.1-1), one Optional Deps entry
|
|
# added to the candidate: changed fields, identical ones once, -Qi-only fields kept, a differing
|
|
# multi-line block, -Si-only fields appended
|
|
use_fakes pacman
|
|
run skup show glib2
|
|
assert_success
|
|
assert_output "$(cat <<'EOF'
|
|
# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks
|
|
Name : glib2
|
|
Version : 2.88.3-1
|
|
> Version : 2.90.1-1
|
|
Description : Low level core library
|
|
Architecture : x86_64
|
|
URL : https://gitlab.gnome.org/GNOME/glib
|
|
Licenses : LGPL-2.1-or-later
|
|
Groups : None
|
|
Provides : libglib-2.0.so=0-64 libgio-2.0.so=0-64 libgirepository-2.0.so=0-64 libgmodule-2.0.so=0-64 libgobject-2.0.so=0-64 libgthread-2.0.so=0-64
|
|
Depends On : bash glibc libffi libsysprof-capture pcre2 util-linux-libs zlib libffi.so=8-64 libmount.so=1-64
|
|
Optional Deps : dconf: GSettings storage backend
|
|
glib2-devel: development tools
|
|
gvfs: most gio functionality
|
|
> Optional Deps : dconf: GSettings storage backend
|
|
> glib2-devel: development tools
|
|
> gvfs: most gio functionality
|
|
> python: gdbus-codegen
|
|
Required By : libsecret pinentry
|
|
Optional For : None
|
|
Conflicts With : None
|
|
Replaces : None
|
|
Installed Size : 37.89 MiB
|
|
> Installed Size : 38.39 MiB
|
|
Packager : Jan Alexander Steffens (heftig) <heftig@archlinux.org>
|
|
Build Date : Wed Jul 29 14:57:16 2026
|
|
> Build Date : Tue Oct 6 16:19:12 2026
|
|
Install Date : Sun Oct 4 00:00:00 2026
|
|
Install Reason : Installed as a dependency for another package
|
|
Install Script : No
|
|
Validated By : Signature
|
|
> Validated By : SHA-256 Sum Signature
|
|
> Repository : core
|
|
> Download Size : 5.12 MiB
|
|
EOF
|
|
)"
|
|
log_has 'pacman -Qi -- glib2'
|
|
log_has 'pacman -Si -- core/glib2'
|
|
}
|
|
|
|
@test "pacman: the candidate comes from the first repository -Sl lists; no -Si record = the plain -Qi" {
|
|
use_fakes pacman
|
|
run skup show su-dummy-b
|
|
assert_success
|
|
assert_output --partial "$(printf '%s\n' 'Version : 1.0-1' '> Version : 2.0-1')"
|
|
assert_line 'Required By : su-dummy-a'
|
|
refute_output --partial '3.0-1'
|
|
log_has 'pacman -Si -- su-dummy/su-dummy-b'
|
|
# bash: in the sync list, but no -Si record - the output stays what it was (-Qi, then -Si)
|
|
run skup show bash
|
|
assert_success
|
|
assert_line --index 0 'Name : bash'
|
|
refute_line --partial '# lines starting with >'
|
|
# not installed: the candidate alone
|
|
run skup show su-dummy-new
|
|
assert_success
|
|
assert_line --index 0 '# not installed: this is the upstream candidate'
|
|
assert_line --index 1 'Repository : su-dummy'
|
|
refute_line --partial '> '
|
|
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 skup show glib2
|
|
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: 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: su-dummy-a, su-dummy-d'
|
|
run skup info su-dummy-c
|
|
assert_line 'required by: su-dummy-d'
|
|
run skup info bash
|
|
assert_line 'required by: filesystem, glibc-common, redhat-rpm-config, rpm, su-dummy-d'
|
|
}
|
|
|
|
@test "dnf4: install reason from repoquery --installed %{reason}; plain rpm -i = unknown" {
|
|
use_fakes dnf rpm
|
|
run skup info su-dummy-a
|
|
assert_success
|
|
assert_output "$(printf '%s\n' 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)' 'reason: explicitly installed' 'required by: su-dummy-d')"
|
|
log_has 'dnf -q repoquery --installed --queryformat %{reason}'
|
|
run skup info su-dummy-b
|
|
assert_line 'reason: installed as a dependency'
|
|
run skup info su-dummy-c
|
|
assert_line 'reason: unknown'
|
|
# weak-dependency counts as a dependency, a group member as asked for
|
|
run skup info glibc-gconv-extra
|
|
assert_line 'reason: installed as a dependency'
|
|
run skup info basesystem
|
|
assert_line 'reason: explicitly installed'
|
|
# only in a repository: repoquery --installed prints nothing
|
|
run skup info su-dummy-new
|
|
assert_success
|
|
assert_line 'su-dummy-new: skeptical-updater dummy su-dummy-new (v1.0)'
|
|
assert_line 'reason: not installed'
|
|
# repoquery failing: unknown
|
|
FAKE_DNF_REPOQUERY_RC=1 run skup info su-dummy-a
|
|
assert_success
|
|
assert_line 'reason: unknown'
|
|
}
|
|
|
|
@test "dnf5: install reason in dnf5's words (External User = plain rpm -i, counted as user-installed)" {
|
|
use_fakes dnf rpm
|
|
export FAKE_DNF_MAJOR=5
|
|
run skup info su-dummy-a
|
|
assert_success
|
|
assert_line 'reason: explicitly installed'
|
|
run skup info su-dummy-b
|
|
assert_line 'reason: installed as a dependency'
|
|
run skup info su-dummy-c
|
|
assert_line 'reason: explicitly installed'
|
|
run skup info glibc-gconv-extra
|
|
assert_line 'reason: installed as a dependency'
|
|
FAKE_DNF_REASON='su-dummy-c=None' run skup info su-dummy-c
|
|
assert_line 'reason: unknown'
|
|
run skup info su-dummy-new
|
|
assert_line 'reason: not installed'
|
|
}
|
|
|
|
@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 "dnf4: show merges info --installed with info --upgrades (headings dropped)" {
|
|
use_fakes dnf rpm
|
|
run skup show su-dummy-b
|
|
assert_success
|
|
assert_output "$(cat <<'EOF'
|
|
# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks
|
|
Name : su-dummy-b
|
|
Version : 1.0
|
|
> Version : 2.0
|
|
Release : 1
|
|
Architecture : noarch
|
|
Size : 15
|
|
> Size : 6.5 k
|
|
Source : su-dummy-b-1.0-1.src.rpm
|
|
> Source : su-dummy-b-2.0-1.src.rpm
|
|
Repository : @System
|
|
> Repository : su-dummy
|
|
Summary : skeptical-updater dummy su-dummy-b (v1.0)
|
|
> Summary : skeptical-updater dummy su-dummy-b (v2.0)
|
|
License : MIT
|
|
Description : Dummy package su-dummy-b version 1.0 for skeptical-updater tests.
|
|
:
|
|
: Second paragraph of su-dummy-b 1.0 (blank line above on purpose).
|
|
> 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).
|
|
EOF
|
|
)"
|
|
log_has 'dnf -q info --installed su-dummy-b'
|
|
log_has 'dnf -q info --upgrades su-dummy-b'
|
|
# real almalinux:9 records: an identical multi-line Description once, "From repo" (installed only) kept
|
|
run skup show glibc
|
|
assert_success
|
|
assert_output --partial "$(printf '%s\n' 'Release : 275.el9_8' '> Release : 283.el9_8')"
|
|
assert_line 'From repo : baseos'
|
|
[[ $(grep -c '^Description : The glibc package contains' <<<"$output") -eq 1 ]]
|
|
[[ $(grep -c 'Linux system will not function\.$' <<<"$output") -eq 1 ]]
|
|
}
|
|
|
|
@test "dnf5: show merges records whose keys are padded differently (re-padded, extra fields appended)" {
|
|
use_fakes dnf rpm
|
|
FAKE_DNF_MAJOR=5 run skup show su-dummy-b
|
|
assert_success
|
|
assert_output "$(cat <<'EOF'
|
|
# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks
|
|
Name : su-dummy-b
|
|
Epoch : 0
|
|
Version : 1.0
|
|
> Version : 2.0
|
|
Release : 1
|
|
Architecture : noarch
|
|
Installed size : 15.0 B
|
|
Source : su-dummy-b-1.0-1.src.rpm
|
|
> Source : su-dummy-b-2.0-1.src.rpm
|
|
From repository : <unknown>
|
|
Summary : skeptical-updater dummy su-dummy-b (v1.0)
|
|
> Summary : skeptical-updater dummy su-dummy-b (v2.0)
|
|
URL :
|
|
License : MIT
|
|
Description : Dummy package su-dummy-b version 1.0 for skeptical-updater tests.
|
|
:
|
|
: Second paragraph of su-dummy-b 1.0 (blank line above on purpose).
|
|
> 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).
|
|
Vendor : <NULL>
|
|
> Vendor :
|
|
> Download size : 6.6 KiB
|
|
> Repository : su-dummy
|
|
EOF
|
|
)"
|
|
}
|
|
|
|
@test "dnf: not installed = info --available alone; not one record per side = the plain dnf -q info" {
|
|
use_fakes dnf rpm
|
|
run skup show zsh
|
|
assert_success
|
|
assert_line --index 0 '# not installed: this is the upstream candidate'
|
|
assert_line --index 1 'Name : zsh'
|
|
refute_line 'Available Packages'
|
|
log_has 'dnf -q info --available zsh'
|
|
# two installed records (e.g. two architectures): nothing to pair, the output stays dnf -q info
|
|
FAKE_DNF_INFO_TWICE=installed run skup show su-dummy-a
|
|
assert_success
|
|
refute_line --partial '# lines starting with >'
|
|
assert_line 'Installed Packages'
|
|
assert_line 'Available Upgrades'
|
|
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:'
|
|
refute_line --partial '# lines starting with >'
|
|
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: 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
|
|
}
|
|
|
|
@test "zypper: install reason from the Installed line of zypper info (not 'Installed Size')" {
|
|
use_fakes zypper rpm
|
|
run skup info su-dummy-a
|
|
assert_success
|
|
assert_output "$(printf '%s\n' 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)' 'reason: explicitly installed' 'required by: su-dummy-d')"
|
|
log_has 'zypper --non-interactive --no-refresh --quiet info su-dummy-a'
|
|
run skup info su-dummy-b
|
|
assert_success
|
|
assert_output "$(printf '%s\n' 'su-dummy-b: skeptical-updater dummy su-dummy-b (v2.0)' 'reason: installed as a dependency' 'required by: su-dummy-a, su-dummy-d')"
|
|
# only in a repository
|
|
run skup info su-dummy-orphan-parent
|
|
assert_success
|
|
assert_line 'su-dummy-orphan-parent: skeptical-updater dummy su-dummy-orphan-parent (v1.0)'
|
|
assert_line 'reason: not installed'
|
|
# unknown to zypper (exit 104) and to rpm
|
|
run skup info nothing-like-this
|
|
assert_failure 1
|
|
assert_line 'reason: not installed'
|
|
}
|
|
|
|
@test "zypper: without an answer from zypper (lock held: exit 7) rpm + libzypp's AutoInstalled decide" {
|
|
use_fakes zypper rpm
|
|
export SKEPTICAL_UPDATER_ZYPP_ROOT=$BATS_TEST_TMPDIR/zroot FAKE_ZYPPER_INFO_RC=7
|
|
# no readable AutoInstalled: the tool cannot tell
|
|
run skup info su-dummy-b
|
|
assert_success
|
|
assert_line 'su-dummy-b: skeptical-updater dummy su-dummy-b (v1.0)'
|
|
assert_line 'reason: unknown'
|
|
mkdir -p "$SKEPTICAL_UPDATER_ZYPP_ROOT/var/lib/zypp"
|
|
printf '# AutoInstalled generated Sat Oct 10 11:14:20 2026\n#\nsu-dummy-b\npattern:minimal_base\n#\n' >"$SKEPTICAL_UPDATER_ZYPP_ROOT/var/lib/zypp/AutoInstalled"
|
|
run skup info su-dummy-b
|
|
assert_line 'reason: installed as a dependency'
|
|
run skup info su-dummy-a
|
|
assert_line 'reason: explicitly installed'
|
|
run skup info su-dummy-orphan-parent
|
|
assert_line 'reason: not installed'
|
|
}
|
|
|
|
@test "zypper on Tumbleweed: N? on a new dependency (old -) says 'not installed' without asking" {
|
|
use_fakes zypper rpm
|
|
export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed
|
|
FAKE_ZYPPER_DUP=removes run skup --no-refresh <<<$'2?\nq'
|
|
assert_success
|
|
assert_line ' [2] su-dummy-newdep - -> 1.0-1 su-dummy'
|
|
assert_line 'reason: not installed'
|
|
assert_line 'required by: nothing'
|
|
log_lacks 'zypper --non-interactive --no-refresh --quiet info su-dummy-newdep'
|
|
}
|