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.
This commit is contained in:
bryanpedini committed 2026-10-10 14:24:39 +02:00
1 parent 296c77e319
commit 875c8294bf
30 files changed
+857 -45

No files matched your search

+208 -14
View File
@@ -111,17 +111,70 @@ setup() { common_setup; }
assert_line 'no-such-pkg-xyz: (no description available: unknown package?)'
}
@test "apt: show is apt-cache show --no-all-versions (the candidate)" {
@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 'Package: su-dummy-a'
assert_line 'Version: 2.0'
assert_line 'Depends: su-dummy-b (>= 2.0)'
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" {
@@ -219,15 +272,73 @@ setup() { common_setup; }
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
}
@test "pacman: show prints the installed (-Qi) and the candidate (-Si) records" {
@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 su-dummy-a
run skup show glib2
assert_success
assert_line 'Install Reason : Explicitly installed'
assert_line 'Repository : su-dummy'
assert_line 'Version : 2.0-1'
log_has 'pacman -Qi -- su-dummy-a'
log_has 'pacman -Si -- su-dummy-a'
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
}
@@ -238,6 +349,7 @@ setup() { common_setup; }
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 ''
}
@@ -391,12 +503,93 @@ setup() { common_setup; }
log_has 'dnf -y remove su-dummy-a su-dummy-b'
}
@test "dnf: show is dnf -q info" {
@test "dnf4: show merges info --installed with info --upgrades (headings dropped)" {
use_fakes dnf rpm
run skup show su-dummy-a
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'
assert_line 'Version : 2.0'
log_has 'dnf -q info su-dummy-a'
}
@@ -481,6 +674,7 @@ setup() { common_setup; }
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