diff --git a/README.md b/README.md index 9dffb62..1024048 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ commands: untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG) refresh recompute dependency closures for every 'user' root info PKG description, install reason and installed dependents (as N?) - show PKG full native package information (as N??) + show PKG installed record merged with the upstream candidate (as N??) options: --backend=NAME force apt|dnf|pacman|zypper (default: detect pacman > apt > dnf/yum > zypper) @@ -128,7 +128,7 @@ listed prints an error, runs nothing, and the prompt comes back. |-------|--------| | empty line | proceed: run the native upgrade with the current exclusions | | `N?` | what is it: the one-line description, `reason:` how it got installed (`explicitly installed`, `installed as a dependency`, `unknown` when the package manager cannot tell, `not installed` for a new dependency) and `required by:` the packages that depend on it, or `nothing`. That list covers installed packages only | -| `N??` | full native info (`apt-cache show`, `pacman -Qi` + `-Si`, `dnf info`, `zypper info --requires`) | +| `N??` | full native info: the installed record, with the upstream candidate merged in as lines starting with `>` (see below). zypper shows `zypper info --requires` as it is | | `N-` | exclude from this run (toggle; `N-` again re-includes); shown with a `-` marker | | `Nx` | purge now, with its now-unneeded dependencies: the purge is simulated first and the prompt lists everything it removes (`Purge su-dummy-a now? This also removes: su-dummy-b [y/N]`); the answer is read from the same stdin; the rows of every removed package leave the list. `x` refuses when other installed packages depend on the target, like `pacman -Rns`; purge the dependents first (`not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first`). Rows with old version `-` (new dependencies, not installed) cannot be purged | | `N+` | trust forever: write it and its installed dependency closure to the statefile; the row leaves the list. Rows this hides are re-included if they were excluded (a trusted package is upgraded) | @@ -142,6 +142,47 @@ Several tokens may share a line (`1? 3- 4+`); they run left to right. With `--al trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the session ends by itself (exit 0). +`N??` (and `show PKG`) prints the installed record in the package manager's own layout +(`dpkg-query -s`, `pacman -Qi`, `dnf info --installed`), preceded by one `#` line saying how to read +it. Right after each field whose value differs in the upstream candidate (`apt-cache show`, +`pacman -Si` from the repository pacman would use, `dnf info --upgrades`) comes the candidate's line, +prefixed with `>`; fields only the candidate has (download size, checksums, repository) follow at the +end the same way, and fields only the installed record has (install reason, conffiles, +`Required By`) stay as they are. Identical fields are printed once; multi-line values (descriptions, pacman +`Optional Deps`, apt continuation lines) are compared as blocks and, when they differ, every line of +the candidate's block gets the `>`. A package that is not installed shows the candidate alone. When +the two records cannot be paired (zypper, whose `rpm -qi` and `zypper info` share almost no keys; no +candidate; several installed architectures) the plain native output is shown. On Debian 12, with +`su-dummy-b` 1.0 installed and 2.0 available: + +``` +> 2?? +# 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 +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: 27e6732d8f16131f7512e1d4f1855cc3 +> Filename: ./su-dummy-b_2.0_all.deb +> Size: 824 +> MD5sum: 62274690c5324af5419515e9f430226b +> SHA256: e1c87480693d98770356f1dc55616dc416f7e3c3f71dc3967e94dec77bb36c16 +``` + Under the rows, `Note:` lines report pending work that is not a row: new dependencies or replacements only the pacman transaction (`-Sup`) shows, packages dnf installs to obsolete others, packages a Tumbleweed `dist-upgrade` removes. With no row but such work, the list @@ -277,7 +318,9 @@ signal during the exit handler, the holds lock kept while a restore failed, jour zypper configuration replayed only when no other run holds their lock, the pacman pre-check / database install / `-Su` sequence and stale throwaway databases, statefile locking and merge, the `mkdir` lock fallback (EPERM owner alive, no busy loop), an inaccessible state directory, the -sudo re-exec of the file itself and its loop guard), and the prompt on a real pty (`script`): +sudo re-exec of the file itself and its loop guard), the `N??` merge on recorded pacman, apt, dnf4 +and dnf5 records (changed, identical, installed-only, candidate-only and multi-line fields, apt's +short candidate descriptions, dnf5's padding, the fallbacks), and the prompt on a real pty (`script`): cursor editing, Up/Down history recall, the stashed line, the bell at both ends, no history file. About 30 seconds. @@ -296,7 +339,9 @@ mirror state. `su-dummy-b` is recorded as installed as a dependency on every bac `--asdeps`, `apt-mark auto`, `dnf mark remove`, zypper's `AutoInstalled`) and `su-dummy-a` as explicitly installed (dnf needs `dnf mark install`: it records plain `rpm` installs as `unknown`). Then `tests/e2e/*.bats` exercise the real package manager: banner, list, `N?` (the -reason and `required by:` of both), `N??`, `N+` and the statefile, `trust`/`untrust`/`refresh`, +reason and `required by:` of both), `N??` (su-dummy-b's installed 1.0 record with `> Version`, and +`> Repository` on pacman and dnf; zypper's plain `zypper info`), `N+` and the statefile, +`trust`/`untrust`/`refresh`, excluding `su-dummy-b` (with and without a pre-existing hold/lock/IgnorePkg/exclude on `su-dummy-c`, which must survive byte for byte), trusting re-including an excluded dependency, full upgrade, `Nx` (refused for su-dummy-b, which su-dummy-a needs; su-dummy-a diff --git a/discovery/apt.md b/discovery/apt.md index 5af45f3..6e9067a 100644 --- a/discovery/apt.md +++ b/discovery/apt.md @@ -622,3 +622,15 @@ $ apt-mark showmanual su-dummy-a su-dummy-b su-dummy-c -> su-dummy-a (only t * A removed package with its configuration files kept (`rc`) is in neither list: not installed. A package installed with `dpkg -i` is manual (also `su-dummy-c` reinstalled with `dpkg -i` after `dpkg -r`). * apt-mark prints a native package without its architecture qualifier (`apt-mark showauto libc6:amd64` -> `libc6`) and nothing for an arch that is not installed (`libc6:i386` -> empty); a name that is not a package is treated as a pattern (`apt-mark showmanual 'su-dummy-?'` and `'su-dummy-.'` -> `su-dummy-a`), exact names win (`libapt-pkg6.0`, `libbz2-1.0`, `libdb5.3` answered for themselves). Hence the tool matches the output line by line against `PKG` (or `PKG` without `:arch`). * Fixture: `su-dummy-a`/`-c` are manual (`dpkg -i`), `su-dummy-b` is auto with `SU_FIXTURE_B_AUTO=1` (`apt-mark auto su-dummy-b`); no change needed. + +## 14. Item 14 - full info (`N??` / `show`): `dpkg-query -s` merged with `apt-cache show --no-all-versions` + +Verified 2026-10-10 on debian:12 (apt 2.6.1, dpkg 1.21.23), `LC_ALL=C`: the fixture (`SU_FIXTURE_OFFLINE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1`) and the real archive after `apt-get update` (`perl-base` 5.36.0-7+deb12u3 installed, +deb12u4 in bookworm-security; `bash`, up to date). + +* Installed record, two candidates compared: + * `dpkg-query -s PKG`: Package, Essential, Status, Priority, Section, Installed-Size, Maintainer, Architecture, Multi-Arch, Source, Version, Replaces, Provides, (Pre-)Depends, Recommends, Suggests, Breaks, Conflicts, Conffiles (multi-line), Description (short + long, always: dpkg keeps it), Homepage. rc 1 and `package 'x' is not installed and no information is available` for an unknown name; a removed package with config files left still answers (`Status: deinstall ok config-files`). + * `apt-cache show PKG=`: when that version is only in dpkg's status (the fixture's 1.0, installed with `dpkg -i`) it is the same record plus a computed `Description-md5`; when the version is still in the lists (bash) it is the **archive's** record instead: no Status, no Conffiles, only the short description (Debian ships the long text in Translation files), plus Tag, Filename, Size, MD5sum, SHA256. + * Chosen: `dpkg-query -s` - the record of what is installed, with its local fields, independent of the lists. `Status` third word `not-installed`/`config-files` = not installed. +* Candidate: `apt-cache show --no-all-versions PKG` (as before). Same RFC-822 layout (`Key: value`, continuation lines start with a space, ` .` = empty line), keys in another order. Against the installed record: Version, Installed-Size and changed relations differ; Status, Conffiles, Multi-Arch (sometimes) only installed; Description-md5, Tag, Filename, Size, MD5sum, SHA256 only in the candidate. +* Description: on the real archive the candidate's `Description` is one line (perl-base: `Description: minimal Perl system`) while dpkg has the long text; with Translation files downloaded it is `Description-en:` with the long text (see item 3). So `Description-xx` pairs with `Description`, and a one-line candidate equal to the installed first line counts as the same description. The fixture's local repository carries the long text in `Packages`, so there the two blocks really differ (`(version 1.0)` vs `(version 2.0)`). +* Several installed architectures of one name (M-A: same) give several `dpkg-query -s` records: not paired, the plain candidate is shown. diff --git a/discovery/pacman.md b/discovery/pacman.md index df6592a..27cfb8f 100644 --- a/discovery/pacman.md +++ b/discovery/pacman.md @@ -585,3 +585,33 @@ no-such-pkg error: package 'no-such-pkg' was not found * `pacman -Qi sh` answers for the provider (`bash`), like the description and `Required By` already do. * Mapping: `Explicitly installed` -> explicit, `Installed as a dependency for another package` -> dependency, -Qi failing -> not installed, anything else (or no such line) -> unknown. * Fixture: `su-dummy-b` is installed `--asdeps`, `a`/`c` explicitly; `e2e_reset` re-applies both after its `pacman -U`. No change needed. + +## 14. Full info (`N??` / `show`): `-Qi` merged with `-Si REPO/PKG` + +Verified 2026-10-10 on archlinux:latest (pacman 7.1.0), `COLUMNS=0 LC_ALL=C`: the fixture (`FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1`) and a real upgradable package after `pacman -Sy` (`glib2` 2.88.3-1 installed, core 2.90.1-1). + +``` +$ pacman -Qi su-dummy-b $ pacman -Si su-dummy/su-dummy-b +Name : su-dummy-b Repository : su-dummy +Version : 1.0-1 Name : su-dummy-b +Description : Skeptical-updater dummy ... Version : 2.0-1 +Architecture : any Description : Skeptical-updater dummy ... +URL : https://example.invalid/... Architecture : any +Licenses : custom URL / Licenses / Groups / Provides / Depends On / +Groups / Provides / Depends On / Optional Deps Optional Deps / Conflicts With / Replaces +Required By : su-dummy-a Download Size : 388.00 B +Optional For : None Installed Size : 4.00 B +Conflicts With / Replaces Packager : su-fixture <...> +Installed Size : 4.00 B Build Date : Tue Nov 14 22:13:20 2023 +Packager : su-fixture <...> Validated By : SHA-256 Sum +Build Date : Tue Nov 14 22:13:20 2023 +Install Date : Sat Oct 10 11:55:39 2026 +Install Reason : Installed as a dependency for another package +Install Script : No +Validated By : None +``` + +* Both are one record per target (blank line after it), every key padded to the same column (`:` at column 17 in both, C locale), so the keys pair one to one. `-Qi` only: `Required By`, `Optional For`, `Install Date`, `Install Reason`, `Install Script`. `-Si` only: `Repository`, `Download Size`. Both, usually different: `Version`, `Installed Size`, `Build Date`, `Validated By` (`None`/`Signature` installed, `SHA-256 Sum Signature` in the repo), `Depends On`/`Provides`/`Optional Deps` when they changed. +* Multi-line: only `Optional Deps` (continuation lines indented to the value column, one entry each - glib2: `dconf: ...` / `glib2-devel: ...` / `gvfs: ...`); with `COLUMNS=0` nothing else wraps (`Provides` of glib2, 6 entries, stays one line). +* The candidate is `-Si REPO/PKG` with REPO = the first repository `-Sl` lists PKG in (pacman's choice, the join `pacman_upgradable` uses); plain `-Si PKG` prints one record per repository that has PKG. +* Missing sides: `-Qi` of a package only in a sync database fails (not installed: the candidate alone); a local-only package has no `-Si` record (the plain `-Qi` stays). diff --git a/discovery/rpm.md b/discovery/rpm.md index 2a441b2..72abdd3 100644 --- a/discovery/rpm.md +++ b/discovery/rpm.md @@ -743,3 +743,40 @@ no-such-pkg package 'no-such-pkg' not found. rc 104 * Cross-check: the flag is exactly membership in `/var/lib/zypp/AutoInstalled` (bare package names, plus `pattern:...` lines and `#` comments; 0644 root, world-readable). Deleting `su-dummy-b` from it turns the line into `Yes` immediately; an `rpm -Uvh --replacepkgs` reinstall keeps it; `zypper install su-dummy-a` (rpm -e'd first) installs b with `Yes (automatically)` and adds it to the file; naming b on the command line (`zypper update su-dummy-a su-dummy-b`) drops the flag (b becomes user-installed). * Works as `nobody`. As root, while another process owns the zypp lock (`/run/zypp.pid` with a live pid), `zypper info` exits 7 ("System management is locked by the application with pid N (sleep)") - as `nobody` it still answers. Fallback in that case (and for exit 104): `rpm -q PKG` fails -> not installed; else membership in AutoInstalled -> dependency / explicit; unreadable -> unknown. * Fixture: a/c (plain rpm, not in AutoInstalled) are `Yes`, b is `Yes (automatically)` with `SU_FIXTURE_B_AUTO=1`; `e2e_reset` re-adds b to AutoInstalled. No change needed. + +## 15. Full info (`N??` / `show`): installed record merged with the candidate + +Verified 2026-10-10, fixture `SU_FIXTURE_ISOLATE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1`, `LC_ALL=C`, plus real packages on almalinux:9 (`glibc` 2.34-275.el9_8 installed, 283.el9_8 in baseos; `bash`, up to date; `zsh`, not installed). + +### 15.1 DNF: `dnf -q info --installed PKG` vs `dnf -q info --upgrades PKG` + +dnf4 (almalinux:9 4.14.0, almalinux:10 4.20.0 - identical apart from sizes): one heading line (`Installed Packages`, `Available Upgrades`, `Available Packages`), then `Key : value` records, `:` at column 14 on both sides, `Description` continued by ` : text` lines (an empty paragraph is ` : `), a blank line after each record; `Size : 15 ` keeps trailing blanks. Same keys on both sides (Name, Version, Release, Architecture, Size, Source, Repository, `From repo` (installed only, when dnf installed it), Summary, URL, License, Description): Version/Release, Size (installed size vs download size, same key), Source, Repository (`@System` vs the repo id) and changed Summary/Description differ. + +dnf5 (fedora:latest, 5.4.3.0): headings `Installed packages` / `Available upgrades` / `Available packages`; installed keys Name, Epoch, Version, Release, Architecture, `Installed size`, Source, `From repository`, Summary, URL, License, Description, Vendor (`:` at column 17); the candidate has `Download size`, `Installed size`, `Repository` instead of `From repository` (`:` at column 16). `Installed size` is the same value with different padding; `Vendor` is `` installed and empty in the repo for the fixture. + +* `--upgrades`, not `--available`, for an installed package: on almalinux:9 `info --available glibc` prints the **i686** build first and then x86_64 (both not installed), and nothing for the up-to-date bash (`Error: No matching Packages to list`, rc 1); `--upgrades glibc` prints exactly the x86_64 candidate. Not installed (`zsh`): `--installed` fails, `--available` prints the record. +* Both rc 1 with no match (dnf4 `Error: No matching Packages to list`, dnf5 `No matching packages to list`, stderr). + +### 15.2 ZYPPER: not merged + +``` +$ rpm -qi su-dummy-b $ zypper -n --no-refresh info su-dummy-b +Name : su-dummy-b Information for package su-dummy-b: +Version : 1.0 ----------------------------------- +Release : 1 Repository : su-dummy +Architecture: noarch Name : su-dummy-b +Install Date: Sat Oct 10 12:02:01 2026 Version : 2.0-1 +Group : Unspecified Arch : noarch +Size : 15 Vendor : +License : MIT Installed Size : 15 B +Signature : (none) Installed : Yes (automatically) +Source RPM : su-dummy-b-1.0-1.src.rpm Status : out-of-date (version 1.0-1 installed) +Build Date : Sat Oct 10 12:02:01 2026 Source package : su-dummy-b-2.0-1.src +Build Host : 17756d8ad56d Summary : skeptical-updater dummy su-dummy-b (v2.0) +Summary : skeptical-updater ... Description : +Description : Dummy package su-dummy-b version 2.0 ... +Dummy package su-dummy-b version 1.0 ... +Distribution: (none) +``` + +* Only Name and Summary pair directly. Version (version vs version-release), Arch/Architecture, Size (bytes) vs `Installed Size` (`15 B`), `Source RPM` vs `Source package` (different file names), the description layout (rpm: unindented lines after `Description :`; zypper: indented 4) would all need translating, and zypper cannot print the installed record in its own layout (`zypper info -r @System PKG`: `Repository '@System' not found by its alias, number, or URI.`, rc 3). `N??` stays `zypper info --requires` (opensuse/tumbleweed, zypper 1.14.101). diff --git a/docs/DESIGN.md b/docs/DESIGN.md index fee82b4..c4383e1 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -99,7 +99,7 @@ Command grammar (one line, whitespace-separated tokens, each `N` is the number i |--------|---------------------------------------------------------------------------| | empty line | proceed: run the native upgrade with the current exclusions | | `N?` | print three lines, none indented: `PKG: `, `reason: ` with `` = `explicitly installed`, `installed as a dependency`, `unknown` (the backend cannot tell) or `not installed` (also, without asking the backend, for a row whose old version is `-`), and `required by: ` - the reverse dependencies, installed packages only - or `required by: nothing` | -| `N??` | print the full native info dump (`apt-cache show` / `pacman -Qi` / `dnf info` / `zypper info --requires`) | +| `N??` | print the full native info: the installed record merged with the upstream candidate (see "Full info" below); zypper: `zypper info --requires` as it is | | `N-` | exclude from this run (toggle; `N-` again re-includes); shown with `-` marker | | `Nx` | purge it now, following pacman's `-Rns` principle on every backend: if any installed package still depends on it, refuse and print who (the reverse dependencies `N?` shows) - never cascade; otherwise simulate the native purge of it together with its now-unneeded dependencies, show that removal set ("This also removes: ...") and ask `y/N`. As a safety check, a simulated set containing anything that is neither the target nor in its installed dependency closure is refused too. Only a purge that ran removes rows (of every package it removed). A row whose old version is `-` (a new dependency, not installed) cannot be purged | | `N+` | trust it: write it and its installed dependency closure to the statefile; the row leaves the list (with `--all` it stays, marked `=`). Rows that this trust hides are re-included if they were excluded | @@ -126,6 +126,33 @@ history by itself). readline draws the prompt and the line on stderr, as bash do stdin keeps the plain `IFS= read -r` with the prompt on stdout and each line echoed after it, so piped runs and their logs are unchanged. +Full info (`N??`, `show PKG`). The first line is `# lines starting with > come from the upstream +candidate: a differing value, or a field the installed record lacks`; then the installed record in +its native key/value layout, as the base. After each field whose candidate value differs, the +candidate's line follows, prefixed with `> ` (same key formatting); fields only the candidate has +are appended at the end the same way; fields only the installed record has stay; identical fields +print once. A field starts on a line that begins with a non-blank character and holds a `:` (the +key is the text before it); lines beginning with a blank continue it, so multi-line values (pacman +`Optional Deps`, dnf `Description`, RFC-822 continuation lines) are compared as blocks, every line +trimmed, and a differing candidate block gets `> ` on every line. `Description-xx` (apt Translation +files) pairs with `Description`; on apt a one-line candidate `Description` equal to the first +installed line counts as identical (the archives carry only the short description, the long text +is in the Translation files). When both records align their `:` in one column but in different +ones (dnf5: 16 installed, 15 available), the candidate lines are re-padded to the installed one's. + +| backend | installed record | upstream candidate | +|---------|------------------|--------------------| +| pacman | `pacman -Qi PKG` | `pacman -Si REPO/PKG`, REPO = the first repository `pacman -Sl` lists PKG in (as for the review list) | +| apt | `dpkg-query -s PKG` (Status, Conffiles, always the long description; `apt-cache show PKG=` gives the archive's record when that version is still listed: no Status/Conffiles, short description only); a `Status` of `not-installed`/`config-files` = not installed | `apt-cache show --no-all-versions PKG` | +| dnf | `dnf -q info --installed PKG` | `dnf -q info --upgrades PKG` (installed architecture only; `--available` also lists other architectures and, for an up-to-date package, other builds); not installed: `--available` | +| zypper | - | - (`rpm -qi` and `zypper info` share almost no keys - Version vs version-release, Arch, byte sizes, source names, description layout - and zypper cannot print the installed record by itself: `zypper info -r @System` is refused) | + +A package that is not installed (a row whose old version is `-` is not asked) shows the candidate +alone under `# not installed: this is the upstream candidate`. Anything else - no candidate (an +up-to-date dnf package, a local-only pacman package), not exactly one record on each side (two +installed architectures), zypper - prints `_full_info` unchanged. `dnf info` headings +("Installed Packages", ...) are dropped. Verified formats: `discovery/{pacman,apt,rpm}.md`. + Flags shown after a row: `[held]` = the package manager will not upgrade it on its own: on apt a hold (`apt-mark hold`) or a package apt keeps back (the "kept back" block of `apt-get -s --with-new-pkgs upgrade`), on pacman an `IgnorePkg` (`[ignored]` in `pacman -Qu`). @@ -213,6 +240,8 @@ Every backend is a set of bash functions with a common prefix. All output is # plus "#" lines for pending work that is not a row _describe PKG # stdout: one-line summary _full_info PKG # stdout: native full dump (candidate version if available) +_installed_record PKG # optional (pacman, apt, dnf): stdout: ONE native record of the installed package +_candidate_record PKG installed|not-installed # optional, with the above: ONE native record of the candidate _rdepends PKG # stdout: installed packages requiring PKG, one per line _reason PKG # stdout: explicit | dependency | unknown | not-installed (how PKG got installed) _closure PKG # stdout: installed recursive dependency closure of PKG (excluding PKG), one per line diff --git a/skeptical-updater b/skeptical-updater index 2e447bf..cb2bdaa 100755 --- a/skeptical-updater +++ b/skeptical-updater @@ -91,7 +91,7 @@ commands: untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG) refresh recompute dependency closures for every 'user' root info PKG description, install reason and installed dependents (as N?) - show PKG full native package information (as N??) + show PKG installed record merged with the upstream candidate (as N??) options: --backend=NAME force apt|dnf|pacman|zypper (default: detect pacman > apt > dnf/yum > zypper) @@ -108,7 +108,8 @@ interactive commands (one line, whitespace-separated tokens, N = number in brack N? what is it: the description, 'reason:' (explicitly installed, installed as a dependency, unknown or not installed) and 'required by:', the packages that depend on it - installed packages only - N?? full native info dump + N?? full native info: the installed record; lines starting with '>' come + from the upstream candidate (differing values, fields it adds) N- exclude from this run (toggle); shown with a '-' marker Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused when other installed packages depend on it, like pacman -Rns @@ -128,7 +129,7 @@ session_help() { cat <<'EOF' upgrade now, skipping the rows marked '-' N? what is it: description, install reason + installed packages that need it - N?? full native info + N?? full native info: installed record, '>' lines from the upstream candidate N- exclude N from this run (toggle) Nx purge N now: lists every package the purge removes, then asks y/N. x refuses when other installed packages depend on the target, like @@ -884,6 +885,18 @@ apt_describe() { printf '%s\n' "$out" } apt_full_info() { apt-cache show --no-all-versions "$1" `, which gives the archive's record when that version is still listed: +# no Status, no Conffiles, often only the short description); a package dpkg merely remembers +# (removed, config files left) is not installed. apt_candidate_record PKG: the candidate. +apt_installed_record() { + local out st + out=$(dpkg-query -s -- "$1" /dev/null) || return 1 + st=$(sed -n 's/^Status: //p' <<<"$out") + case ${st##* } in '' | not-installed | config-files) return 1 ;; esac + printf '%s\n' "$out" +} +apt_candidate_record() { apt-cache show --no-all-versions "$1" /dev/null; } apt_rdepends() { apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances "$1" /dev/null | awk '/^Reverse Depends:/{p=1;next} p{sub(/^[ |]+/,""); print}' | sort -u @@ -1229,6 +1242,15 @@ pacman_full_info() { _pm -Si -- "$1" 2>/dev/null && ok=0 return "$ok" } +# pacman_installed_record PKG: -Qi. pacman_candidate_record PKG: -Si of the repository pacman would +# take it from (the first one `-Sl` lists it in, as in pacman_upgradable) +pacman_installed_record() { _pm -Qi -- "$1" 2>/dev/null; } +pacman_candidate_record() { + local repo + repo=$(_pm -Sl 2>/dev/null | awk -v p="$1" '$2 == p && r == "" { r = $1 } END { if (r != "") print r }') || repo='' + [[ -n $repo ]] || return 1 + _pm -Si -- "$repo/$1" 2>/dev/null +} pacman_rdepends() { local out w local -a words=() @@ -1619,6 +1641,16 @@ dnf_describe() { return 1 } dnf_full_info() { "$DNF" -q info "$1" /dev/null | sed '/^[^[:space:]][^:]*$/d'; } +dnf_candidate_record() { + local w=--available + [[ ${2:-} != installed ]] || w=--upgrades + "$DNF" -q info "$w" "$1" /dev/null | sed '/^[^[:space:]][^:]*$/d' +} dnf_rdepends() { rpm_rdeps "$1"; } dnf_closure() { rpm_closure "$1"; } # dnf_reason PKG: dnf's own record, `repoquery --installed --queryformat %{reason}` (discovery/rpm.md @@ -2078,8 +2110,91 @@ print_info() { return "$rc" } -print_full_info() { # print_full_info PKG (N?? / show); 1 if the package manager knows nothing - local rc=0 +# N?? / show: the installed record as the base, merged with the upstream candidate. Backends that +# give comparable key/value records for both sides define _installed_record PKG and +# _candidate_record PKG installed|not-installed (one native record each, no output = none): +# pacman, apt, dnf. zypper does not (rpm -qi and zypper info share almost no keys, and zypper cannot +# print the installed record by itself) and keeps _full_info, as does any package for which +# either side is missing or is not exactly one record. +readonly SU_MERGE_NOTE='# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks' +readonly SU_CANDIDATE_NOTE='# not installed: this is the upstream candidate' +# merge: each base field is printed as it is; when the candidate's value differs, the candidate's +# line(s) follow, prefixed with "> "; fields only the candidate has are appended the same way. A +# field starts on a line beginning with a non-blank character that holds a ":" (key = the text +# before it, trailing blanks dropped; a heading without ":" before the record is skipped); lines +# beginning with a blank continue it (pacman Optional Deps, dnf Description, RFC-822 continuation +# lines), so multi-line values are compared - and prefixed - as blocks, every line trimmed. A blank +# line ends a record: exit 3 unless each side holds exactly one. Description-xx (apt Translation +# files) pairs with Description. apt=1: the archives carry only the short description of the +# candidate (the long text is in the Translation files), so a one-line candidate Description equal +# to the first installed line is the same description. When both records align every ":" in one +# column (pacman, dnf) but in different ones (dnf5 pads installed and available keys differently), +# the candidate lines are re-padded to the installed record's column. +# shellcheck disable=SC2016 # awk program, not shell +readonly AWK_MERGE_RECORDS=' +function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t\r]+$/, "", s); return s } +function repad(l, i, k) { + if (shift == 0) return l + if (l ~ /^[^ \t]/) { i = index(l, ":"); k = substr(l, 1, i - 1); sub(/[ \t]+$/, "", k); return sprintf("%-" (col[1] - 1) "s", k) substr(l, i) } + if (shift > 0) return sprintf("%" shift "s", "") l + return substr(l, 1 - shift) +} +function pfx(s, n, a, m) { n = split(s, a, "\n"); for (m = 1; m <= n; m++) print "> " repad(a[m]) } +FNR == 1 { side = (FILENAME == ARGV[1]) ? 1 : 2; cur = "" } +/^[ \t]*$/ { if (nk[side]) ended[side] = 1; next } +/^[^ \t]/ { + i = index($0, ":") + if (i == 0) { if (nk[side]) bad = 1; next } + if (ended[side]) { bad = 1; next } + if (!nk[side]) col[side] = i + else if (col[side] != i) col[side] = -1 + k = substr($0, 1, i - 1); sub(/[ \t]+$/, "", k) + if (k ~ /^Description-[a-z][a-z](_[A-Z][A-Z])?$/) k = "Description" + if (++seen[side, k] > 1) k = k "#" seen[side, k] + key[side, ++nk[side]] = k + raw[side, k] = $0; val[side, k] = trim(substr($0, i + 1)); first[side, k] = val[side, k]; nl[side, k] = 1 + cur = k + next +} +{ + if (cur == "" || ended[side]) { bad = 1; next } + raw[side, cur] = raw[side, cur] "\n" $0; val[side, cur] = val[side, cur] "\n" trim($0); nl[side, cur]++ +} +END { + if (bad || !nk[1] || !nk[2]) exit 3 + shift = (col[1] > 0 && col[2] > 0) ? col[1] - col[2] : 0 + for (j = 1; j <= nk[1]; j++) { + k = key[1, j] + print raw[1, k] + if (!((2, k) in raw) || val[1, k] == val[2, k]) continue + if (apt && k == "Description" && nl[2, k] == 1 && val[2, k] == first[1, k]) continue + pfx(raw[2, k]) + } + for (j = 1; j <= nk[2]; j++) { k = key[2, j]; if (!((1, k) in raw)) pfx(raw[2, k]) } +}' + +print_full_info() { # print_full_info PKG [OLD] (N?? / show); 1 if the package manager knows nothing + local rc=0 inst='' cand='' out apt=0 + if declare -F "${BACKEND}_installed_record" >/dev/null; then + [[ $BACKEND != apt ]] || apt=1 + # a row whose old version is "-" is a new dependency: not installed, nothing to ask + [[ ${2:-} == - ]] || inst=$(be installed_record "$1") || inst='' + if [[ -n $inst ]]; then + cand=$(be candidate_record "$1" installed) || cand='' + if [[ -n $cand ]] && out=$(awk -v apt="$apt" "$AWK_MERGE_RECORDS" <(printf '%s\n' "$inst") <(printf '%s\n' "$cand")); then + say "$SU_MERGE_NOTE" + say "$out" + return 0 + fi + else + cand=$(be candidate_record "$1" not-installed) || cand='' + if [[ -n $cand ]]; then + say "$SU_CANDIDATE_NOTE" + say "$cand" + return 0 + fi + fi + fi be full_info "$1" || rc=$? ((rc == 0)) && return 0 err "no package information for '$1' ($(be label) exited $rc)" @@ -2369,7 +2484,7 @@ session_line() { i=$REPLY case $op in '?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;; - '??') print_full_info "${R_NAME[i]}" || true ;; + '??') print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;; -) if ((R_EXCL[i])); then R_EXCL[i]=0 diff --git a/tests/e2e/skeptical-updater.bats b/tests/e2e/skeptical-updater.bats index db04358..634c671 100644 --- a/tests/e2e/skeptical-updater.bats +++ b/tests/e2e/skeptical-updater.bats @@ -56,10 +56,33 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl assert_line 'Quit: nothing was upgraded.' } -@test "N?? prints the native full info" { - run skeptical-updater --no-refresh <<<$'1??\nq' +@test "N?? shows the installed record merged with the upstream candidate (zypper: zypper info)" { + run skeptical-updater --no-refresh <<<$'2??\nq' assert_success - assert_line --regexp '^(Package: su-dummy-a|Name +: su-dummy-a)$' + if [[ $BE == zypper ]]; then # no comparable records: the plain zypper info --requires + assert_line 'Information for package su-dummy-b:' + refute_line --partial '# lines starting with >' + return 0 + fi + assert_line '# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks' + case $BE in + apt) + assert_line 'Package: su-dummy-b' + assert_line 'Status: install ok installed' + assert_output --partial "$(printf '%s\n' 'Version: 1.0' '> Version: 2.0')" + assert_line --regexp '^> Filename: .*su-dummy-b_2\.0_all\.deb$' + ;; + pacman) + assert_output --partial "$(printf '%s\n' 'Version : 1.0-1' '> Version : 2.0-1')" + assert_line 'Install Reason : Installed as a dependency for another package' + assert_line '> Repository : su-dummy' + ;; + dnf) + assert_output --partial "$(printf '%s\n' 'Version : 1.0' '> Version : 2.0')" + assert_output --partial "$(printf '%s\n' 'Repository : @System' '> Repository : su-dummy')" + ;; + esac + [[ $(grep -c '^> ' <<<"$output") -ge 2 ]] } @test "N+ writes the user row and the dep row; list hides both and counts them" { diff --git a/tests/unit/data/apt/show/perl-base b/tests/unit/data/apt/show/perl-base new file mode 100644 index 0000000..2500d75 --- /dev/null +++ b/tests/unit/data/apt/show/perl-base @@ -0,0 +1,21 @@ +Package: perl-base +Source: perl +Version: 5.36.0-7+deb12u4 +Installed-Size: 7643 +Maintainer: Niko Tyni +Architecture: amd64 +Replaces: libfile-path-perl (<< 2.18), libfile-temp-perl (<< 0.2311), libio-socket-ip-perl (<< 0.41), libscalar-list-utils-perl (<< 1:1.62), libsocket-perl (<< 2.033), libxsloader-perl (<< 0.31), perl (<< 5.10.1-12), perl-modules (<< 5.20.1-3) +Provides: libfile-path-perl (= 2.18), libfile-temp-perl (= 0.2311), libio-socket-ip-perl (= 0.41), libscalar-list-utils-perl (= 1:1.62), libsocket-perl (= 2.033), libxsloader-perl (= 0.31), perlapi-5.36.0 +Suggests: perl, sensible-utils +Conflicts: defoma (<< 0.11.12), doc-base (<< 0.10.3), mono-gac (<< 2.10.8.1-3), safe-rm (<< 0.8), update-inetd (<< 4.41) +Breaks: amanda-common (<< 1:3.3.9-2), backuppc (<< 4.4.0-7~), bucardo (<< 5.5.0-1.1), debconf (<< 1.5.61), dh-haskell (<< 0.3), duck (<< 0.14.1), intltool (<< 0.51.0-4), kio-perldoc (<< 20.04.1-1), latexml (<< 0.8.4-2), libdevel-mat-dumper-perl (<< 0.42-3), libencode-arabic-perl (<< 14.2-2), libexception-class-perl (<< 1.42), libfile-path-perl (<< 2.18), libfile-spec-perl (<< 3.8400), libfile-temp-perl (<< 0.2311), libio-socket-ip-perl (<< 0.41), libmp3-tag-perl (<< 1.13-1.2), libsbuild-perl (<< 0.67.0-1), libscalar-list-utils-perl (<< 1:1.62), libsocket-perl (<< 2.033), libxsloader-perl (<< 0.31), mailagent (<< 1:3.1-81-2), perl (<< 5.36.0~), perl-modules (<< 5.36.0~), pod2pdf (<< 0.42-5.1), slic3r (<< 1.2.9+dfsg-6.1), slic3r-prusa (<< 1.37.0+dfsg-1.1), texinfo (<< 6.1.0.dfsg.1-8) +Description: minimal Perl system +Description-md5: 6f98ca50727514fe6b19872d5ede03ec +Homepage: http://dev.perl.org/perl5/ +Essential: yes +Pre-Depends: libc6 (>= 2.35), libcrypt1 (>= 1:4.1.0), dpkg (>= 1.17.17) +Section: perl +Priority: required +Filename: pool/updates/main/p/perl/perl-base_5.36.0-7+deb12u4_amd64.deb +Size: 1609596 +SHA256: d7d1943aec9597629bf73075efcc5ef6dc9bda96d78e80d843156ecd448478b8 diff --git a/tests/unit/data/apt/show/zsh b/tests/unit/data/apt/show/zsh new file mode 100644 index 0000000..df3a392 --- /dev/null +++ b/tests/unit/data/apt/show/zsh @@ -0,0 +1,21 @@ +Package: zsh +Source: zsh (5.9-4) +Version: 5.9-4+b15 +Installed-Size: 2461 +Maintainer: Debian Zsh Maintainers +Architecture: amd64 +Depends: zsh-common (= 5.9-4), libc6 (>= 2.34), libcap2 (>= 1:2.10), libtinfo6 (>= 6), debianutils (>= 5.3-1~) +Recommends: libc6 (>= 2.35), libgdbm6 (>= 1.16), libncursesw6 (>= 6), libpcre3 +Suggests: zsh-doc +Description: shell with lots of features +Description-md5: a129d6b2d23d2d5d3a6b822d3f8f856d +Homepage: https://www.zsh.org/ +Tag: devel::interpreter, implemented-in::c, interface::shell, + network::client, protocol::ftp, role::program, scope::utility +Section: shells +Priority: optional +Filename: pool/main/z/zsh/zsh_5.9-4+b15_amd64.deb +Size: 907512 +MD5sum: 831903f59bf1ed613a5beeef9fc787ad +SHA256: 8362cc8bbb530d393950522dc8a9df5a860a61227d62ff669a7524056e6289a8 + diff --git a/tests/unit/data/apt/status/perl-base b/tests/unit/data/apt/status/perl-base new file mode 100644 index 0000000..d9ce7fe --- /dev/null +++ b/tests/unit/data/apt/status/perl-base @@ -0,0 +1,24 @@ +Package: perl-base +Essential: yes +Status: install ok installed +Priority: required +Section: perl +Installed-Size: 7639 +Maintainer: Niko Tyni +Architecture: amd64 +Source: perl +Version: 5.36.0-7+deb12u3 +Replaces: libfile-path-perl (<< 2.18), libfile-temp-perl (<< 0.2311), libio-socket-ip-perl (<< 0.41), libscalar-list-utils-perl (<< 1:1.62), libsocket-perl (<< 2.033), libxsloader-perl (<< 0.31), perl (<< 5.10.1-12), perl-modules (<< 5.20.1-3) +Provides: libfile-path-perl (= 2.18), libfile-temp-perl (= 0.2311), libio-socket-ip-perl (= 0.41), libscalar-list-utils-perl (= 1:1.62), libsocket-perl (= 2.033), libxsloader-perl (= 0.31), perlapi-5.36.0 +Pre-Depends: libc6 (>= 2.35), libcrypt1 (>= 1:4.1.0), dpkg (>= 1.17.17) +Suggests: perl, sensible-utils +Breaks: amanda-common (<< 1:3.3.9-2), backuppc (<< 4.4.0-7~), bucardo (<< 5.5.0-1.1), debconf (<< 1.5.61), dh-haskell (<< 0.3), duck (<< 0.14.1), intltool (<< 0.51.0-4), kio-perldoc (<< 20.04.1-1), latexml (<< 0.8.4-2), libdevel-mat-dumper-perl (<< 0.42-3), libencode-arabic-perl (<< 14.2-2), libexception-class-perl (<< 1.42), libfile-path-perl (<< 2.18), libfile-spec-perl (<< 3.8400), libfile-temp-perl (<< 0.2311), libio-socket-ip-perl (<< 0.41), libmp3-tag-perl (<< 1.13-1.2), libsbuild-perl (<< 0.67.0-1), libscalar-list-utils-perl (<< 1:1.62), libsocket-perl (<< 2.033), libxsloader-perl (<< 0.31), mailagent (<< 1:3.1-81-2), perl (<< 5.36.0~), perl-modules (<< 5.36.0~), pod2pdf (<< 0.42-5.1), slic3r (<< 1.2.9+dfsg-6.1), slic3r-prusa (<< 1.37.0+dfsg-1.1), texinfo (<< 6.1.0.dfsg.1-8) +Conflicts: defoma (<< 0.11.12), doc-base (<< 0.10.3), mono-gac (<< 2.10.8.1-3), safe-rm (<< 0.8), update-inetd (<< 4.41) +Description: minimal Perl system + Perl is a scripting language used in many system scripts and utilities. + . + This package provides a Perl interpreter and the small subset of the + standard run-time library required to perform basic tasks. For a full + Perl installation, install "perl" (and its dependencies, "perl-modules-5.36" + and "perl-doc"). +Homepage: http://dev.perl.org/perl5/ diff --git a/tests/unit/data/apt/status/su-dummy-a b/tests/unit/data/apt/status/su-dummy-a new file mode 100644 index 0000000..ad3fd28 --- /dev/null +++ b/tests/unit/data/apt/status/su-dummy-a @@ -0,0 +1,14 @@ +Package: su-dummy-a +Status: install ok installed +Priority: optional +Section: misc +Maintainer: Skeptical Updater Fixture +Architecture: all +Version: 1.0 +Depends: su-dummy-b +Conffiles: + /etc/su-dummy/su-dummy-a.conf 607e2d3a119dfe832d937ce579ef1079 +Description: dummy package su-dummy-a for skeptical-updater tests + This package only exists to exercise the package manager. + . + It ships one data file and one conffile (version 1.0). diff --git a/tests/unit/data/apt/status/su-dummy-b b/tests/unit/data/apt/status/su-dummy-b new file mode 100644 index 0000000..caeabdc --- /dev/null +++ b/tests/unit/data/apt/status/su-dummy-b @@ -0,0 +1,13 @@ +Package: su-dummy-b +Status: install ok installed +Priority: optional +Section: misc +Maintainer: Skeptical Updater Fixture +Architecture: all +Version: 1.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). diff --git a/tests/unit/data/apt/status/su-dummy-c b/tests/unit/data/apt/status/su-dummy-c new file mode 100644 index 0000000..3d008f3 --- /dev/null +++ b/tests/unit/data/apt/status/su-dummy-c @@ -0,0 +1,13 @@ +Package: su-dummy-c +Status: install ok installed +Priority: optional +Section: misc +Maintainer: Skeptical Updater Fixture +Architecture: all +Version: 1.0 +Conffiles: + /etc/su-dummy/su-dummy-c.conf 607e2d3a119dfe832d937ce579ef1079 +Description: dummy package su-dummy-c for skeptical-updater tests + This package only exists to exercise the package manager. + . + It ships one data file and one conffile (version 1.0). diff --git a/tests/unit/data/dnf/info-available-zsh-dnf4.txt b/tests/unit/data/dnf/info-available-zsh-dnf4.txt new file mode 100644 index 0000000..4c9c1b4 --- /dev/null +++ b/tests/unit/data/dnf/info-available-zsh-dnf4.txt @@ -0,0 +1,17 @@ +Available Packages +Name : zsh +Version : 5.8 +Release : 9.el9 +Architecture : x86_64 +Size : 2.9 M +Source : zsh-5.8-9.el9.src.rpm +Repository : baseos +Summary : Powerful interactive shell +URL : http://zsh.sourceforge.net/ +License : MIT +Description : The zsh shell is a command interpreter usable as an interactive login + : shell and as a shell script command processor. Zsh resembles the ksh + : shell (the Korn shell), but includes many enhancements. Zsh supports + : command line editing, built-in spelling correction, programmable + : command completion, shell functions (with autoloading), a history + : mechanism, and more. diff --git a/tests/unit/data/dnf/info-installed-glibc-dnf4.txt b/tests/unit/data/dnf/info-installed-glibc-dnf4.txt new file mode 100644 index 0000000..3920d9e --- /dev/null +++ b/tests/unit/data/dnf/info-installed-glibc-dnf4.txt @@ -0,0 +1,19 @@ +Installed Packages +Name : glibc +Version : 2.34 +Release : 275.el9_8 +Architecture : x86_64 +Size : 6.2 M +Source : glibc-2.34-275.el9_8.src.rpm +Repository : @System +From repo : baseos +Summary : The GNU libc libraries +URL : http://www.gnu.org/software/glibc/ +License : LGPL-2.1-or-later AND SunPro AND LGPL-2.1-or-later WITH GCC-exception-2.0 AND BSD-3-Clause AND GPL-2.0-or-later AND LGPL-2.1-or-later WITH GNU-compiler-exception AND GPL-2.0-only AND ISC AND LicenseRef-Fedora-Public-Domain AND HPND AND CMU-Mach AND LGPL-2.0-or-later AND Unicode-3.0 AND GFDL-1.1-or-later AND GPL-1.0-or-later AND FSFUL AND MIT AND Inner-Net-2.0 AND X11 AND GPL-2.0-or-later WITH GCC-exception-2.0 AND GFDL-1.3-only AND GFDL-1.1-only AND GPL-3.0-or-later AND GPL-3.0-or-later WITH Autoconf-exception-generic-3.0 AND GPL-3.0-or-later WITH Texinfo-exception +Description : The glibc package contains standard libraries which are used by + : multiple programs on the system. In order to save disk space and + : memory, as well as to make upgrading easier, common system code is + : kept in one place and shared between programs. This particular package + : contains the most important sets of shared libraries: the standard C + : library and the standard math library. Without these two libraries, a + : Linux system will not function. diff --git a/tests/unit/data/dnf/info-installed-su-dummy-a-dnf4.txt b/tests/unit/data/dnf/info-installed-su-dummy-a-dnf4.txt new file mode 100644 index 0000000..53627a5 --- /dev/null +++ b/tests/unit/data/dnf/info-installed-su-dummy-a-dnf4.txt @@ -0,0 +1,13 @@ +Installed Packages +Name : su-dummy-a +Version : 1.0 +Release : 1 +Architecture : noarch +Size : 15 +Source : su-dummy-a-1.0-1.src.rpm +Repository : @System +Summary : skeptical-updater dummy su-dummy-a (v1.0) +License : MIT +Description : Dummy package su-dummy-a version 1.0 for skeptical-updater tests. + : + : Second paragraph of su-dummy-a 1.0 (blank line above on purpose). diff --git a/tests/unit/data/dnf/info-installed-su-dummy-b-dnf4.txt b/tests/unit/data/dnf/info-installed-su-dummy-b-dnf4.txt new file mode 100644 index 0000000..54bf97f --- /dev/null +++ b/tests/unit/data/dnf/info-installed-su-dummy-b-dnf4.txt @@ -0,0 +1,13 @@ +Installed Packages +Name : su-dummy-b +Version : 1.0 +Release : 1 +Architecture : noarch +Size : 15 +Source : su-dummy-b-1.0-1.src.rpm +Repository : @System +Summary : skeptical-updater dummy su-dummy-b (v1.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). diff --git a/tests/unit/data/dnf/info-installed-su-dummy-b-dnf5.txt b/tests/unit/data/dnf/info-installed-su-dummy-b-dnf5.txt new file mode 100644 index 0000000..03556b8 --- /dev/null +++ b/tests/unit/data/dnf/info-installed-su-dummy-b-dnf5.txt @@ -0,0 +1,16 @@ +Installed packages +Name : su-dummy-b +Epoch : 0 +Version : 1.0 +Release : 1 +Architecture : noarch +Installed size : 15.0 B +Source : su-dummy-b-1.0-1.src.rpm +From repository : +Summary : skeptical-updater dummy su-dummy-b (v1.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). +Vendor : diff --git a/tests/unit/data/dnf/info-upgrades-glibc-dnf4.txt b/tests/unit/data/dnf/info-upgrades-glibc-dnf4.txt new file mode 100644 index 0000000..9fc4307 --- /dev/null +++ b/tests/unit/data/dnf/info-upgrades-glibc-dnf4.txt @@ -0,0 +1,18 @@ +Available Upgrades +Name : glibc +Version : 2.34 +Release : 283.el9_8 +Architecture : x86_64 +Size : 2.0 M +Source : glibc-2.34-283.el9_8.src.rpm +Repository : baseos +Summary : The GNU libc libraries +URL : http://www.gnu.org/software/glibc/ +License : LGPL-2.1-or-later AND SunPro AND LGPL-2.1-or-later WITH GCC-exception-2.0 AND BSD-3-Clause AND GPL-2.0-or-later AND LGPL-2.1-or-later WITH GNU-compiler-exception AND GPL-2.0-only AND ISC AND LicenseRef-Fedora-Public-Domain AND HPND AND CMU-Mach AND LGPL-2.0-or-later AND Unicode-3.0 AND GFDL-1.1-or-later AND GPL-1.0-or-later AND FSFUL AND MIT AND Inner-Net-2.0 AND X11 AND GPL-2.0-or-later WITH GCC-exception-2.0 AND GFDL-1.3-only AND GFDL-1.1-only AND GPL-3.0-or-later AND GPL-3.0-or-later WITH Autoconf-exception-generic-3.0 AND GPL-3.0-or-later WITH Texinfo-exception +Description : The glibc package contains standard libraries which are used by + : multiple programs on the system. In order to save disk space and + : memory, as well as to make upgrading easier, common system code is + : kept in one place and shared between programs. This particular package + : contains the most important sets of shared libraries: the standard C + : library and the standard math library. Without these two libraries, a + : Linux system will not function. diff --git a/tests/unit/data/dnf/info-upgrades-su-dummy-a-dnf4.txt b/tests/unit/data/dnf/info-upgrades-su-dummy-a-dnf4.txt new file mode 100644 index 0000000..d08823d --- /dev/null +++ b/tests/unit/data/dnf/info-upgrades-su-dummy-a-dnf4.txt @@ -0,0 +1,13 @@ +Available Upgrades +Name : su-dummy-a +Version : 2.0 +Release : 1 +Architecture : noarch +Size : 6.3 k +Source : su-dummy-a-2.0-1.src.rpm +Repository : su-dummy +Summary : skeptical-updater dummy su-dummy-a (v2.0) +License : MIT +Description : Dummy package su-dummy-a version 2.0 for skeptical-updater tests. + : + : Second paragraph of su-dummy-a 2.0 (blank line above on purpose). diff --git a/tests/unit/data/dnf/info-upgrades-su-dummy-b-dnf4.txt b/tests/unit/data/dnf/info-upgrades-su-dummy-b-dnf4.txt new file mode 100644 index 0000000..fb2e4f2 --- /dev/null +++ b/tests/unit/data/dnf/info-upgrades-su-dummy-b-dnf4.txt @@ -0,0 +1,13 @@ +Available Upgrades +Name : su-dummy-b +Version : 2.0 +Release : 1 +Architecture : noarch +Size : 6.5 k +Source : su-dummy-b-2.0-1.src.rpm +Repository : su-dummy +Summary : skeptical-updater dummy su-dummy-b (v2.0) +License : MIT +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). diff --git a/tests/unit/data/dnf/info-upgrades-su-dummy-b-dnf5.txt b/tests/unit/data/dnf/info-upgrades-su-dummy-b-dnf5.txt new file mode 100644 index 0000000..4af4243 --- /dev/null +++ b/tests/unit/data/dnf/info-upgrades-su-dummy-b-dnf5.txt @@ -0,0 +1,17 @@ +Available upgrades +Name : su-dummy-b +Epoch : 0 +Version : 2.0 +Release : 1 +Architecture : noarch +Download size : 6.6 KiB +Installed size : 15.0 B +Source : su-dummy-b-2.0-1.src.rpm +Repository : su-dummy +Summary : skeptical-updater dummy su-dummy-b (v2.0) +URL : +License : MIT +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 : diff --git a/tests/unit/data/pacman/Qi.txt b/tests/unit/data/pacman/Qi.txt index 311db3f..5f668f1 100644 --- a/tests/unit/data/pacman/Qi.txt +++ b/tests/unit/data/pacman/Qi.txt @@ -264,3 +264,27 @@ Install Reason : Explicitly installed Install Script : No Validated By : None + +Name : glib2 +Version : 2.88.3-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 +Required By : libsecret pinentry +Optional For : None +Conflicts With : None +Replaces : None +Installed Size : 37.89 MiB +Packager : Jan Alexander Steffens (heftig) +Build Date : Wed Jul 29 14:57:16 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 diff --git a/tests/unit/data/pacman/Si.txt b/tests/unit/data/pacman/Si.txt index 4f521de..109dae5 100644 --- a/tests/unit/data/pacman/Si.txt +++ b/tests/unit/data/pacman/Si.txt @@ -93,3 +93,25 @@ Packager : su-fixture Build Date : Tue Nov 14 22:13:20 2023 Validated By : None + +Repository : core +Name : glib2 +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 + python: gdbus-codegen +Conflicts With : None +Replaces : None +Download Size : 5.12 MiB +Installed Size : 38.39 MiB +Packager : Jan Alexander Steffens (heftig) +Build Date : Tue Oct 6 16:19:12 2026 +Validated By : SHA-256 Sum Signature diff --git a/tests/unit/data/pacman/Sl.txt b/tests/unit/data/pacman/Sl.txt index 75e26ae..f3a4ae0 100644 --- a/tests/unit/data/pacman/Sl.txt +++ b/tests/unit/data/pacman/Sl.txt @@ -6,4 +6,5 @@ extra pacman-contrib 1.13.1-1 su-dummy su-dummy-a 2.0-1 [installed: 1.0-1] su-dummy su-dummy-b 2.0-1 [installed: 1.0-1] su-dummy su-dummy-c 2.0-1 [installed: 1.0-1] +su-dummy su-dummy-new 1.0-1 su-dummy2 su-dummy-b 3.0-1 diff --git a/tests/unit/fakes/apt/dpkg-query b/tests/unit/fakes/apt/dpkg-query index 5bccbb5..47d41e4 100755 --- a/tests/unit/fakes/apt/dpkg-query +++ b/tests/unit/fakes/apt/dpkg-query @@ -2,8 +2,9 @@ # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2026 Bryan Joshua Pedini -# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures, and -# `-W -f '${db:Status-Abbrev}' NAME` (the status of one package; purged = unknown to dpkg) +# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures, +# `-W -f '${db:Status-Abbrev}' NAME` (the status of one package; purged = unknown to dpkg) and +# `-s NAME` (dpkg's record: data/apt/status/NAME; none or purged = not installed) # shellcheck source=../fakelib.bash . "${BASH_SOURCE[0]%/*}/../fakelib.bash" fake_log "$@" @@ -21,6 +22,15 @@ while (($#)); do esac shift done +if [[ $args == *' -s '* ]]; then + n=${names[0]%:amd64} + if [[ ! -f $FAKE_DATA/apt/status/$n ]] || fake_purged "$n"; then + echo "dpkg-query: package '$n' is not installed and no information is available" >&2 + exit 1 + fi + cat "$FAKE_DATA/apt/status/$n" + exit 0 +fi if ((${#names[@]})); then rc=0 for n in "${names[@]}"; do diff --git a/tests/unit/fakes/dnf/dnf b/tests/unit/fakes/dnf/dnf index 453ebc4..c83a1af 100755 --- a/tests/unit/fakes/dnf/dnf +++ b/tests/unit/fakes/dnf/dnf @@ -7,7 +7,9 @@ # "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail; # removal tables: see the --assumeno remove branch. `repoquery --installed --qf %{reason}`: REASON of # data/rpm/db.bash (dnf5's words with FAKE_DNF_MAJOR=5), FAKE_DNF_REASON="pkg=raw value" overrides -# it, FAKE_DNF_REPOQUERY_RC=N makes repoquery fail. +# it, FAKE_DNF_REPOQUERY_RC=N makes repoquery fail. `info --installed|--upgrades|--available PKG` +# replays data/dnf/info--PKG-dnf.txt (none: no match); FAKE_DNF_INFO_TWICE= +# prints that block twice (two installed architectures, ...). Plain `info PKG`: data/dnf/info-PKG.txt. # shellcheck source=../fakelib.bash . "${BASH_SOURCE[0]%/*}/../fakelib.bash" # shellcheck source=../../data/rpm/db.bash @@ -72,10 +74,23 @@ case " $* " in ;; *' info '*) p=${pos[-1]} - if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else - echo "Error: No matching Packages to list" >&2 + f=$D/info-$p.txt + w='' + case " $* " in + *' --installed '*) w=installed ;; + *' --upgrades '*) w=upgrades ;; + *' --available '*) w=available ;; + esac + [[ -z $w ]] || f=$D/info-$w-$p-dnf$major.txt + if [[ ! -f $f ]] || { [[ $w == installed ]] && fake_purged "$p"; }; then + if ((major == 5)); then echo "No matching packages to list" >&2; else echo "Error: No matching Packages to list" >&2; fi exit 1 fi + cat "$f" + if [[ -n $w && ${FAKE_DNF_INFO_TWICE:-} == "$w" ]]; then + echo + sed '1d' "$f" + fi ;; *' --assumeno '*remove*) # the removal table: the target, its dependents (su-dummy-a for su-dummy-b; FAKE_DNF_DEPENDENT= diff --git a/tests/unit/fakes/pacman/pacman b/tests/unit/fakes/pacman/pacman index 407fe9a..fa28cc9 100755 --- a/tests/unit/fakes/pacman/pacman +++ b/tests/unit/fakes/pacman/pacman @@ -20,23 +20,26 @@ fake_log "$@" D=$FAKE_DATA/pacman -# blocks FILE NAME...: print the blank-line separated blocks whose Name is one of NAME -# (all blocks when no NAME); exit 1 (after printing the others) if a NAME is missing +# blocks FILE NAME...: print the blank-line separated blocks whose Name is one of NAME, or whose +# Repository/Name is (-Si repo/name); all blocks when no NAME; exit 1 (after printing the others) +# if a NAME is missing blocks() { - local f=$1 line block='' name='' rc=0 n + local f=$1 line block='' name='' repo='' rc=0 n shift local -A want=() found=() for n in "$@"; do want[$n]=1; done while IFS= read -r line || [[ -n $block ]]; do if [[ -z $line ]]; then - if [[ -n $block ]] && { (($# == 0)) || [[ -n ${want[$name]:-} ]]; } && ! fake_purged "$name"; then + if [[ -n $block ]] && { (($# == 0)) || [[ -n ${want[$name]:-} || -n ${want[$repo/$name]:-} ]]; } && ! fake_purged "$name"; then printf '%s\n' "$block" found[$name]=1 + found[$repo/$name]=1 fi - block='' name='' + block='' name='' repo='' continue fi [[ $line =~ ^Name\ +:\ (.*)$ ]] && name=${BASH_REMATCH[1]} + [[ $line =~ ^Repository\ +:\ (.*)$ ]] && repo=${BASH_REMATCH[1]} block+=$line$'\n' done <"$f" for n in "$@"; do @@ -121,11 +124,7 @@ case $op in { print }' <<<"$out" exit "$rc" ;; --Si) - t=() - for a in "${targets[@]}"; do t+=("${a#*/}"); done - blocks "$D/Si.txt" "${t[@]}" - ;; +-Si) blocks "$D/Si.txt" "${targets[@]}" ;; -Su | -Syu) echo "real sync at $op: $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db" 2>/dev/null || echo none) / $(cat "$FAKE_PACMAN_DBPATH/sync/su-dummy.db.sig" 2>/dev/null || echo no-sig)" >>"$FAKE_LOG" if [[ -e $FAKE_PACMAN_DBPATH/db.lck ]]; then echo "db.lck at $op: yes" >>"$FAKE_LOG"; else echo "db.lck at $op: no" >>"$FAKE_LOG"; fi diff --git a/tests/unit/interactive.bats b/tests/unit/interactive.bats index e3d7b33..f2a90ab 100644 --- a/tests/unit/interactive.bats +++ b/tests/unit/interactive.bats @@ -90,14 +90,25 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc log_lacks '^dpkg-query -W -f \$\{db:Status-Abbrev\} ' } -@test "N?? prints the native full info" { +@test "N?? prints the installed record merged with the candidate" { run skup --no-refresh <<<$'1??\nq' assert_success + assert_line '# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks' assert_line 'Package: su-dummy-a' - assert_line 'Depends: su-dummy-b (>= 2.0)' + assert_line 'Version: 1.0' + assert_line '> Version: 2.0' + assert_line '> Depends: su-dummy-b (>= 2.0)' + log_has 'dpkg-query -s -- su-dummy-a' log_has 'apt-cache show --no-all-versions su-dummy-a' } +@test "N?? on a new dependency (old version -) never looks for an installed record" { + FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq' + assert_success + log_lacks '^dpkg-query -s' + log_has 'apt-cache show --no-all-versions su-dummy-newdep' +} + @test "N- toggles the exclusion and the '-' marker" { run skup --no-refresh <<<$'2-\nl\n2-\nl\n2-\n' assert_success diff --git a/tests/unit/parsers.bats b/tests/unit/parsers.bats index 620213c..926b27b 100644 --- a/tests/unit/parsers.bats +++ b/tests/unit/parsers.bats @@ -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 +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) +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 : +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 : +> 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