Files
skeptical-updater/tests/unit/parsers.bats
T
bryanpedini 875c8294bf feat(show): merge the installed record with the upstream candidate
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.
2026-10-10 14:24:39 +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_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'
}