Files
skeptical-updater/tests/unit/parsers.bats
T
bryanpedini 3af159e09a fix(show): star marker, aligned gutter and smarter AUR merge
N?? / show marks the upstream candidate with "*" instead of ">" (the
prompt, which read as a command in logs), and every line but the note
gets a two-character gutter: "  " for the installed record, "* " for the
candidate, so keys and colons line up. The plain native fallback (zypper,
unmergeable records) and the not-installed candidate use the same gutter.

pacman records (-Qi with -Si or the AUR helper's -Sia) are now compared
the way they are meant:
- values unwrapped (continuation lines joined, runs of blanks as one):
  yay wraps Depends On / Optional Deps at 80 columns even without a
  terminal unless COLUMNS=0, which the helper now gets for -Qua and -Sia
- Depends On, Provides, Conflicts With, Make/Check Deps and the other
  lists as sets of exact tokens in any order (a constraint is a change)
- Optional Deps by package names ("name: description [installed]" in
  -Qi, bare names from the AUR)
- a differing value is one line like -Qi, Optional Deps one entry per
  line; candidate-only fields sit next to their counterpart (Repository
  after Version, AUR URL after URL, Maintainer after Packager, ...)

Verified on recorded claude-desktop records (paru 2.1.0, yay 13.0.1, the
installed 2.9939.4-1 from the AUR's own .SRCINFO); discovery/pacman.md
section 15.2.
2026-10-10 16:01:51 +02:00

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_output --partial "$(printf '%s\n' ' Depends: su-dummy-b' '* 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: lines starting with * come from the upstream candidate (all of them)'
assert_line --index 1 '* Package: zsh'
assert_line --index 3 '* Version: 5.9-4+b15'
refute_line --regexp '^ '
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, in place" {
# 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
# Optional Deps block (one entry per line), -Si-only fields next to their counterparts
# (Repository after Version, Download Size after Installed Size); every line in the gutter
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
* Repository : core
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
* Download Size : 5.12 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
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: lines starting with * come from the upstream candidate (all of them)'
assert_line --index 1 '* Repository : su-dummy'
refute_line --regexp '^ '
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: lines starting with * come from the upstream candidate (all of them)'
assert_line --index 1 '* Name : zsh'
refute_line --partial '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'
}