feat(info): show the install reason and drop the indentation

N? and `info PKG` now print three lines, none indented:

    PKG: <description>
    reason: explicitly installed | installed as a dependency | unknown | not installed
    required by: <installed dependents, comma separated> | nothing

The reason comes from a new <be>_reason backend function: apt-mark
showauto / showmanual (dpkg decides for a package in neither list), the
Install Reason of pacman -Qi, dnf repoquery --installed %{reason} (dnf4
and dnf5 words) and the Installed line of zypper info (rpm plus libzypp's
AutoInstalled when zypper is locked). A row whose old version is "-" is
not installed and the backend is not asked. The "(installed)" qualifier
moves to --help, README and DESIGN.

The rpm E2E fixture and e2e_reset now also mark su-dummy-a and su-dummy-c
as user-installed on dnf, which records plain rpm installs as "unknown",
so 1? and 2? show both reasons on all 12 distros. The verified commands
are recorded in discovery/{apt,pacman,rpm}.md.
This commit is contained in:
bryanpedini committed 2026-10-10 13:32:42 +02:00
1 parent 1354db6c39
commit 71d7f87aa3
21 files changed
+589 -48

No files matched your search

+17 -8
View File
@@ -41,7 +41,7 @@ commands:
trust PKG... add PKG and its installed dependency closure to the statefile trust PKG... add PKG and its installed dependency closure to the statefile
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG) untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
refresh recompute dependency closures for every 'user' root refresh recompute dependency closures for every 'user' root
info PKG one-line description + installed reverse dependencies (as N?) info PKG description, install reason and installed dependents (as N?)
show PKG full native package information (as N??) show PKG full native package information (as N??)
options: options:
@@ -80,7 +80,8 @@ Refreshing package lists... done.
<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever <enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever
> 2? > 2?
su-dummy-b: dummy package su-dummy-b for skeptical-updater tests su-dummy-b: dummy package su-dummy-b for skeptical-updater tests
required by (installed): su-dummy-a reason: installed as a dependency
required by: su-dummy-a
> 1+ > 1+
trusted su-dummy-a (+1 dependency: su-dummy-b) trusted su-dummy-a (+1 dependency: su-dummy-b)
> 3- > 3-
@@ -126,7 +127,7 @@ listed prints an error, runs nothing, and the prompt comes back.
| token | action | | token | action |
|-------|--------| |-------|--------|
| empty line | proceed: run the native upgrade with the current exclusions | | empty line | proceed: run the native upgrade with the current exclusions |
| `N?` | one-line description and the installed packages that depend on it | | `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 (`apt-cache show`, `pacman -Qi` + `-Si`, `dnf info`, `zypper info --requires`) |
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with a `-` marker | | `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 | | `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 |
@@ -220,7 +221,11 @@ upgrade` (Inst lines + the "kept back" block), `pacman -Qu` joined with `pacman
`zypper --xmlout list-updates`, or on Tumbleweed (as root) the real plan `zypper --xmlout list-updates`, or on Tumbleweed (as root) the real plan
`zypper --xmlout --no-refresh dist-upgrade --dry-run`. Reverse dependencies and dependency closures use `zypper --xmlout --no-refresh dist-upgrade --dry-run`. Reverse dependencies and dependency closures use
`apt-cache rdepends` / the dpkg database, `pacman -Qi`, and plain `rpm` queries on `apt-cache rdepends` / the dpkg database, `pacman -Qi`, and plain `rpm` queries on
dnf and zypper systems (see the discovery reports for why). dnf and zypper systems (see the discovery reports for why). The install reason `N?` shows comes
from `apt-mark showauto` / `showmanual`, the `Install Reason` of `pacman -Qi`,
`dnf repoquery --installed --queryformat '%{reason}'` (packages installed with plain `rpm` are
`unknown` to dnf4, user-installed to dnf5) and the `Installed` line of `zypper info`
(`Yes (automatically)`; libzypp's `/var/lib/zypp/AutoInstalled` when zypper is locked).
## Testing ## Testing
@@ -250,7 +255,8 @@ in front of symlinks to the few system tools it needs. The fakes replay the real
outputs recorded in `discovery/*.md` (data in `tests/unit/data/`), keep their state outputs recorded in `discovery/*.md` (data in `tests/unit/data/`), keep their state
(holds, purged packages) under the test's temp dir and log every invocation, so the (holds, purged packages) under the test's temp dir and log every invocation, so the
tests assert the exact commands issued. Covered: CLI parsing and exit codes, backend tests assert the exact commands issued. Covered: CLI parsing and exit codes, backend
detection and override, root handling and the sudo re-exec, every parser, the detection and override, root handling and the sudo re-exec, every parser (install reasons
included: explicitly installed, dependency, unknown and not installed on each backend), the
statefile, the whole interactive grammar, the per-backend exclusion mechanics (apt statefile, the whole interactive grammar, the per-backend exclusion mechanics (apt
hold journal incl. crash recovery and SIGTERM, pacman `--ignore`, dnf `--exclude`, hold journal incl. crash recovery and SIGTERM, pacman `--ignore`, dnf `--exclude`,
zypper private lock file contents incl. drop-ins), the purge plans, the metadata freeze flags, zypper private lock file contents incl. drop-ins), the purge plans, the metadata freeze flags,
@@ -273,9 +279,12 @@ plus `su-dummy-orphan`, an unrelated automatically-installed package nothing dep
with the distro's own repositories disabled (apt `SU_FIXTURE_OFFLINE=1`, pacman with the distro's own repositories disabled (apt `SU_FIXTURE_OFFLINE=1`, pacman
`FIXTURE_ISOLATE=1`, rpm `SU_FIXTURE_ISOLATE=1` after the build tools are installed), `FIXTURE_ISOLATE=1`, rpm `SU_FIXTURE_ISOLATE=1` after the build tools are installed),
so the review list is exactly the three dummies and the result does not depend on so the review list is exactly the three dummies and the result does not depend on
mirror state. Then `tests/e2e/*.bats` exercise the real package manager: banner, mirror state. `su-dummy-b` is recorded as installed as a dependency on every backend (pacman
list, `N?`, `N??`, `N+` and the statefile, `trust`/`untrust`/`refresh`, excluding `--asdeps`, `apt-mark auto`, `dnf mark remove`, zypper's `AutoInstalled`) and `su-dummy-a` as
`su-dummy-b` (with and without a pre-existing hold/lock/IgnorePkg/exclude on 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`,
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 `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 dependency, full upgrade, `Nx` (refused for su-dummy-b, which su-dummy-a needs; su-dummy-a
taking its dependency su-dummy-b along, shown in the prompt, and the unrelated orphan surviving), taking its dependency su-dummy-b along, shown in the prompt, and the unrelated orphan surviving),
+17
View File
@@ -605,3 +605,20 @@ Container start overhead is ~0.2-0.3 s; `docker pull` of the five images took 3-
Everything else compared with `diff` over a 385-line transcript of ~60 commands (update, list, simulate, hold, show/policy/dpkg -s, depends/rdepends, real upgrade, dist-upgrade, purge, autoremove, error paths): identical wording and exit codes across the five images apart from the rows above and checksums/sizes. Specifically identical: `apt list` stderr warning, `Inst`/`Conf`/`Remv`/`Purg` formats, `kept back` block, `apt-mark` messages, `apt-cache depends/rdepends` formats, `E:`/`W:` texts and exit codes (0 / 100), lock message, conffile failure, Essential protection message. Everything else compared with `diff` over a 385-line transcript of ~60 commands (update, list, simulate, hold, show/policy/dpkg -s, depends/rdepends, real upgrade, dist-upgrade, purge, autoremove, error paths): identical wording and exit codes across the five images apart from the rows above and checksums/sizes. Specifically identical: `apt list` stderr warning, `Inst`/`Conf`/`Remv`/`Purg` formats, `kept back` block, `apt-mark` messages, `apt-cache depends/rdepends` formats, `E:`/`W:` texts and exit codes (0 / 100), lock message, conffile failure, Essential protection message.
Pre-existing docker images were not touched (`images-before.tsv` compared: nothing missing or changed); only `debian:12`, `debian:13`, `ubuntu:22.04`, `ubuntu:24.04`, `ubuntu:latest` were pulled and only `docker run --rm` containers were used (one hung throwaway container of mine, caused by a test that re-ran the fixture over a locally modified conffile, was `docker kill`ed). Pre-existing docker images were not touched (`images-before.tsv` compared: nothing missing or changed); only `debian:12`, `debian:13`, `ubuntu:22.04`, `ubuntu:24.04`, `ubuntu:latest` were pulled and only `docker run --rm` containers were used (one hung throwaway container of mine, caused by a test that re-ran the fixture over a locally modified conffile, was `docker kill`ed).
## 13. Item 13 - install reason (`N?` / `info`): `apt-mark showauto` / `showmanual`
Verified 2026-10-10 on debian:12 (apt 2.6.1), debian:13 (3.0.3), ubuntu:22.04 (2.4.14), ubuntu:24.04 (2.8.3) and ubuntu:latest (26.04.1, apt 3.2.0), fixture `SU_FIXTURE_OFFLINE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1`, `LC_ALL=C`. Identical on all five:
```
$ apt-mark showmanual su-dummy-a -> su-dummy-a (rc 0) $ apt-mark showauto su-dummy-a -> (empty, rc 0)
$ apt-mark showmanual su-dummy-b -> (empty, rc 0) $ apt-mark showauto su-dummy-b -> su-dummy-b (rc 0)
$ apt-mark showauto su-dummy-orphan -> su-dummy-orphan $ apt-mark showmanual no-such-pkg -> (empty, rc 0)
$ apt-mark showmanual su-dummy-a su-dummy-b su-dummy-c -> su-dummy-a (only the named packages)
```
* Chosen: `apt-mark showauto PKG` -> dependency; else `apt-mark showmanual PKG` -> explicitly installed; in neither list -> `dpkg-query -W -f '${db:Status-Abbrev}' PKG`: installed (2nd character neither `n` nor `c`) counts as manual, else not installed. No image had an installed package in neither list (installed = manual + auto exactly: debian:12 92 = 2 + 90, ubuntu:24.04 96 = 93 + 3, ...), so that branch is a safety net.
* Read-only: both work as `nobody` (`setpriv --reuid=65534`) and while another process holds dpkg's `lock-frontend` and `lock` (fcntl, like dpkg; checked with `apt-mark hold`, which fails then with `dpkg frontend lock was locked by another process with pid N`, exit 100): 0 s wait, same answers.
* 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.
+17
View File
@@ -568,3 +568,20 @@ pman bugs (verified; relevant if the tool reuses `/etc/pacman.d/ignorepkg`):
`pacman.conf` actually includes the file in `[options]` (silently ineffective, section 6). `pacman.conf` actually includes the file in `[options]` (silently ineffective, section 6).
* The tool should read the effective list with `pacman-conf IgnorePkg` and write with anchored matches * The tool should read the effective list with `pacman-conf IgnorePkg` and write with anchored matches
(`grep -qxF "IgnorePkg = $pkg"`, `sed -i "/^IgnorePkg = ${pkg}\$/d"` after escaping). (`grep -qxF "IgnorePkg = $pkg"`, `sed -i "/^IgnorePkg = ${pkg}\$/d"` after escaping).
## 13. Install reason (`N?` / `info`): `Install Reason` of `pacman -Qi`
Verified 2026-10-10 on archlinux:latest (pacman 7.1.0), fixture `FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1`, `COLUMNS=0 LC_ALL=C pacman -Qi -- PKG`:
```
su-dummy-a Install Reason : Explicitly installed rc 0
su-dummy-b Install Reason : Installed as a dependency for another package rc 0 (-D --asdeps)
su-dummy-c Install Reason : Explicitly installed rc 0
su-dummy-orphan Install Reason : Installed as a dependency for another package rc 0 (-U --asdeps)
no-such-pkg error: package 'no-such-pkg' was not found rc 1 (also for a package only in a sync db)
```
* `pacman -D --asexplicit su-dummy-b` / `--asdeps` flip the line at once. Works as `nobody` and with `db.lck` present (-Qi takes no lock). One line, no wrapping with `COLUMNS=0`.
* `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.
+39
View File
@@ -704,3 +704,42 @@ Inside `proof.sh` (warm containers): forced refresh (`su_refresh`) 0.5-2.6 s (up
* zypper exit codes 100-103, 106, 107 were not reproduced (see 7.2). `opensuse-slowroll`, MicroOS/Aeon (transactional), SLES were not tested. * zypper exit codes 100-103, 106, 107 were not reproduced (see 7.2). `opensuse-slowroll`, MicroOS/Aeon (transactional), SLES were not tested.
* Multi-arch (x86_64 + i686) and installonly kernels were not exercised; the parser keys on `name.arch` and keeps the highest installed `evr` per key for the "old" column. * Multi-arch (x86_64 + i686) and installonly kernels were not exercised; the parser keys on `name.arch` and keeps the highest installed `evr` per key for the "old" column.
* Docker images pulled during this work (all kept, none removed or re-tagged): `almalinux:9`, `almalinux:10`, `rockylinux:9`, `rockylinux/rockylinux:9`, `rockylinux/rockylinux:10`, `quay.io/rockylinux/rockylinux:9`, `quay.io/rockylinux/rockylinux:10`, `opensuse/tumbleweed:latest`, `opensuse/leap:latest`, `fedora:latest`. * Docker images pulled during this work (all kept, none removed or re-tagged): `almalinux:9`, `almalinux:10`, `rockylinux:9`, `rockylinux/rockylinux:9`, `rockylinux/rockylinux:10`, `quay.io/rockylinux/rockylinux:9`, `quay.io/rockylinux/rockylinux:10`, `opensuse/tumbleweed:latest`, `opensuse/leap:latest`, `fedora:latest`.
## 14. Install reason (`N?` / `info`)
Verified 2026-10-10, fixture `SU_FIXTURE_ISOLATE=1 SU_FIXTURE_ORPHAN=1`, `LC_ALL=C`.
### 14.1 DNF: `dnf -q repoquery --installed --queryformat '%{reason}\n' PKG`
Same answers on almalinux:9 and rockylinux/rockylinux:9 (dnf 4.14.0), almalinux:10 and rockylinux/rockylinux:10 (dnf 4.20.0); fedora:latest (Fedora 44, dnf5 5.4.3.0) uses other words:
| state | dnf4 | dnf5 |
|---|---|---|
| installed with plain `rpm -Uvh` (fixture's a, b, c) | `unknown` | `External User` |
| `su-dummy-orphan` (pulled in by `dnf install su-dummy-orphan-parent`) | `dependency` | `Dependency` |
| `dnf install su-dummy-a` pulling in `su-dummy-b` | a `user`, b `dependency` | a `User`, b `Dependency` |
| not installed (in a repo or unknown) | no output, rc 0 | no output, rc 0 |
* `%{reason}` exists on every target, `--qf` and `--queryformat` both work. dnf4 ends the value with a newline, dnf5 does not (several installed versions would run together), so the format carries its own `\n` and the first non-empty line is used. Other values in the code: `weak-dependency`/`Weak Dependency` (dependency), `group`/`Group` (explicit: asked for with its group); dnf5 `External User` counts as user-installed (`repoquery --userinstalled` lists those packages) -> explicit; dnf4 `unknown` -> unknown.
* Fallback `dnf history userinstalled` not needed and not used: dnf5 has no such command (`Unknown argument "userinstalled"`, rc 2), and on dnf4 it lists the fixture's rpm-installed a, b, c (reason `unknown`) as "Packages installed by user", i.e. it would call b explicit.
* Marks: dnf4 `dnf mark remove PKG` -> `dependency`, `dnf mark install PKG` -> `user` (`mark user`/`mark dependency`: invalid choice, rc 2); dnf5 `dnf -y mark dependency|user PKG` (without `-y`: "Operation aborted", rc 1; `mark remove|install`: unknown argument, rc 2). A mark survives a later `dnf upgrade` and an `rpm -Uvh --oldpackage --replacepkgs` back to 1.0 (the e2e reset).
* Works as `nobody` (setpriv / python3 on almalinux:10), with or without `-C`, in 110-150 ms (dnf4) / 45 ms (dnf5), and does not touch the repositories even with the distro repos re-enabled and their metadata expired.
* Fixture consequence: on dnf the rpm-installed a and c would print `reason: unknown`; `SU_FIXTURE_B_AUTO=1` now also runs `dnf mark install su-dummy-a su-dummy-c` (dnf5: `-y mark user`), and `e2e_reset` re-marks a/c user and b dependency.
### 14.2 ZYPPER: `Installed` line of `zypper --non-interactive --no-refresh --quiet info PKG`
opensuse/tumbleweed (zypper 1.14.101, libzypp 17.38.16) and opensuse/leap 16.0 (same versions), identical, fixture with `SU_FIXTURE_B_AUTO=1`:
```
su-dummy-a Installed : Yes rc 0
su-dummy-b Installed : Yes (automatically) rc 0 (listed in /var/lib/zypp/AutoInstalled)
su-dummy-orphan Installed : Yes (automatically) rc 0 (pulled in by zypper install su-dummy-orphan-parent)
su-dummy-orphan-parent Installed : No rc 0 (Status : not installed)
bash Installed : Yes rc 0 (Repository : @System, repos isolated)
no-such-pkg package 'no-such-pkg' not found. rc 104
```
* The block also has `Installed Size : 15 B`: match `^Installed *:` only.
* 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.
+11 -1
View File
@@ -98,7 +98,7 @@ Command grammar (one line, whitespace-separated tokens, each `N` is the number i
| token | action | | token | action |
|--------|---------------------------------------------------------------------------| |--------|---------------------------------------------------------------------------|
| empty line | proceed: run the native upgrade with the current exclusions | | empty line | proceed: run the native upgrade with the current exclusions |
| `N?` | print the one-line description and the list of installed packages that depend on it | | `N?` | print three lines, none indented: `PKG: <one-line description>`, `reason: <r>` with `<r>` = `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: <a, b, ...>` - 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 dump (`apt-cache show` / `pacman -Qi` / `dnf info` / `zypper info --requires`) |
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with `-` marker | | `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 | | `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 |
@@ -202,6 +202,7 @@ Every backend is a set of bash functions with a common prefix. All output is
<be>_describe PKG # stdout: one-line summary <be>_describe PKG # stdout: one-line summary
<be>_full_info PKG # stdout: native full dump (candidate version if available) <be>_full_info PKG # stdout: native full dump (candidate version if available)
<be>_rdepends PKG # stdout: installed packages requiring PKG, one per line <be>_rdepends PKG # stdout: installed packages requiring PKG, one per line
<be>_reason PKG # stdout: explicit | dependency | unknown | not-installed (how PKG got installed)
<be>_closure PKG # stdout: installed recursive dependency closure of PKG (excluding PKG), one per line <be>_closure PKG # stdout: installed recursive dependency closure of PKG (excluding PKG), one per line
<be>_purge_plan PKG # stdout: every package a purge of PKG and its now-unneeded dependencies removes <be>_purge_plan PKG # stdout: every package a purge of PKG and its now-unneeded dependencies removes
# (simulation, no cascade); non-zero = refused # (simulation, no cascade); non-zero = refused
@@ -213,6 +214,15 @@ Every backend is a set of bash functions with a common prefix. All output is
Mutating calls go through `run_cmd` which honours `--dry-run` (prints `+ cmd` to Mutating calls go through `run_cmd` which honours `--dry-run` (prints `+ cmd` to
stdout instead of executing). stdout instead of executing).
`<be>_reason` sources (read-only, unprivileged, verified in `discovery/*.md`): apt
`apt-mark showauto PKG` (dependency) / `apt-mark showmanual PKG` (explicit), a package in neither
list is explicit when dpkg has it installed; pacman the `Install Reason` of `pacman -Qi`; dnf
`repoquery --installed --queryformat '%{reason}'` (`user`/`group`/dnf5 `External User` = explicit,
`dependency`/`weak-dependency` = dependency, dnf4 `unknown` - plain `rpm -i` - stays unknown, no
output = not installed); zypper the `Installed` line of `zypper --no-refresh info`
(`Yes (automatically)` / `Yes` / `No`), and when zypper gives no answer (its lock is busy) `rpm -q`
plus libzypp's `/var/lib/zypp/AutoInstalled`.
## Layout ## Layout
``` ```
+122 -11
View File
@@ -87,7 +87,7 @@ commands:
trust PKG... add PKG and its installed dependency closure to the statefile trust PKG... add PKG and its installed dependency closure to the statefile
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG) untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
refresh recompute dependency closures for every 'user' root refresh recompute dependency closures for every 'user' root
info PKG one-line description + installed reverse dependencies (as N?) info PKG description, install reason and installed dependents (as N?)
show PKG full native package information (as N??) show PKG full native package information (as N??)
options: options:
@@ -102,7 +102,9 @@ options:
interactive commands (one line, whitespace-separated tokens, N = number in brackets): interactive commands (one line, whitespace-separated tokens, N = number in brackets):
<empty line> proceed: run the native upgrade with the current exclusions <empty line> proceed: run the native upgrade with the current exclusions
N? one-line description and installed packages that depend on it 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 dump
N- exclude from this run (toggle); shown with a '-' marker N- exclude from this run (toggle); shown with a '-' marker
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
@@ -122,7 +124,7 @@ EOF
session_help() { session_help() {
cat <<'EOF' cat <<'EOF'
<enter> upgrade now, skipping the rows marked '-' <enter> upgrade now, skipping the rows marked '-'
N? what is it: one-line description + installed packages that need it N? what is it: description, install reason + installed packages that need it
N?? full native info N?? full native info
N- exclude N from this run (toggle) N- exclude N from this run (toggle)
Nx purge N now: lists every package the purge removes, then asks y/N. Nx purge N now: lists every package the purge removes, then asks y/N.
@@ -883,6 +885,41 @@ apt_rdepends() {
apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances "$1" </dev/null 2>/dev/null | apt-cache rdepends --installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances "$1" </dev/null 2>/dev/null |
awk '/^Reverse Depends:/{p=1;next} p{sub(/^[ |]+/,""); print}' | sort -u awk '/^Reverse Depends:/{p=1;next} p{sub(/^[ |]+/,""); print}' | sort -u
} }
# apt_reason PKG: apt's auto flag (discovery/apt.md item 13): listed by `apt-mark showauto` =
# dependency, by `showmanual` = explicit; in neither list but installed (dpkg) = explicit, else not
# installed. Both are read-only: no root, no dpkg lock (verified). apt-mark prints native packages
# without their architecture qualifier and treats a name that is no package as a pattern, hence
# the exact-line match.
apt_reason() {
local out st
out=$(apt-mark showauto "$1" </dev/null 2>/dev/null) || {
printf 'unknown\n'
return 0
}
if apt_mark_lists "$1" "$out"; then
printf 'dependency\n'
return 0
fi
out=$(apt-mark showmanual "$1" </dev/null 2>/dev/null) || {
printf 'unknown\n'
return 0
}
if apt_mark_lists "$1" "$out"; then
printf 'explicit\n'
return 0
fi
# ${db:Status-Abbrev}: 2nd character = state; n(ot-installed) and c(onfig-files) = not installed
st=$(dpkg-query -W -f '${db:Status-Abbrev}' "$1" </dev/null 2>/dev/null) || st=
case ${st:1:1} in
'' | n | c) printf 'not-installed\n' ;;
*) printf 'explicit\n' ;;
esac
}
apt_mark_lists() { # apt_mark_lists PKG OUTPUT: does OUTPUT (apt-mark show*) name PKG (name:arch too)?
local o=$'\n'$2$'\n'
[[ $o == *$'\n'"$1"$'\n'* ]] && return 0
[[ $1 == *:* && $o == *$'\n'"${1%:*}"$'\n'* ]]
}
apt_closure() { apt_closure() {
dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' </dev/null 2>/dev/null | dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' </dev/null 2>/dev/null |
awk -F'\t' -v roots="$1" "$AWK_APT_CLOSURE" | awk -v r="$1" '$0 != r' | sort -u awk -F'\t' -v roots="$1" "$AWK_APT_CLOSURE" | awk -v r="$1" '$0 != r' | sort -u
@@ -1197,6 +1234,22 @@ pacman_rdepends() {
read -r -a words <<<"$out" || true read -r -a words <<<"$out" || true
for w in ${words[@]+"${words[@]}"}; do [[ $w == None ]] || printf '%s\n' "$w"; done | sort -u for w in ${words[@]+"${words[@]}"}; do [[ $w == None ]] || printf '%s\n' "$w"; done | sort -u
} }
# pacman_reason PKG: the "Install Reason" of `pacman -Qi` (discovery/pacman.md section 13); -Qi
# fails (exit 1, "package 'x' was not found") for a package that is not installed
pacman_reason() {
local out rc=0
out=$(pm_field -Qi 'Install Reason' "$1") || rc=$?
out=${out%%$'\n'*}
if ((rc)) && [[ -z $out ]]; then
printf 'not-installed\n'
return 0
fi
case ${out#*$'\t'} in
'Explicitly installed') printf 'explicit\n' ;;
'Installed as a dependency for another package') printf 'dependency\n' ;;
*) printf 'unknown\n' ;;
esac
}
# discovery/pacman/lib.sh pm_closure (forward mode): one pacman -Qi dump + awk BFS, roots excluded # discovery/pacman/lib.sh pm_closure (forward mode): one pacman -Qi dump + awk BFS, roots excluded
pacman_closure() { pacman_closure() {
@@ -1565,6 +1618,25 @@ dnf_describe() {
dnf_full_info() { "$DNF" -q info "$1" </dev/null; } dnf_full_info() { "$DNF" -q info "$1" </dev/null; }
dnf_rdepends() { rpm_rdeps "$1"; } dnf_rdepends() { rpm_rdeps "$1"; }
dnf_closure() { rpm_closure "$1"; } dnf_closure() { rpm_closure "$1"; }
# dnf_reason PKG: dnf's own record, `repoquery --installed --queryformat %{reason}` (discovery/rpm.md
# section 14): dnf4 user/dependency/weak-dependency/group/unknown (unknown = installed with plain rpm),
# dnf5 User/Dependency/Weak Dependency/Group/External User (= installed with plain rpm, which dnf5
# counts as user-installed). A group member was asked for with its group: explicit. No output = not
# installed. Works unprivileged and never touches the repositories (verified).
dnf_reason() {
local out l=
out=$("$DNF" -q repoquery --installed --queryformat $'%{reason}\n' "$1" </dev/null 2>/dev/null) || {
printf 'unknown\n'
return 0
}
while IFS= read -r l; do [[ -z $l ]] || break; done <<<"$out"
case $l in
user | User | group | Group | 'External User') printf 'explicit\n' ;;
dependency | Dependency | weak-dependency | 'Weak Dependency') printf 'dependency\n' ;;
'') printf 'not-installed\n' ;;
*) printf 'unknown\n' ;;
esac
}
# dnf_purge_plan PKG: the "Removing..." sections of `dnf --assumeno remove PKG` (dnf4 and dnf5 # dnf_purge_plan PKG: the "Removing..." sections of `dnf --assumeno remove PKG` (dnf4 and dnf5
# tables; --assumeno always exits 1, so success = a "Transaction Summary" or "Nothing to do") # tables; --assumeno always exits 1, so success = a "Transaction Summary" or "Nothing to do")
dnf_purge_plan() { dnf_purge_plan() {
@@ -1723,6 +1795,35 @@ zypper_describe() {
zypper_full_info() { zypper --non-interactive --quiet info --requires "$1" </dev/null; } zypper_full_info() { zypper --non-interactive --quiet info --requires "$1" </dev/null; }
zypper_rdepends() { rpm_rdeps "$1"; } zypper_rdepends() { rpm_rdeps "$1"; }
zypper_closure() { rpm_closure "$1"; } zypper_closure() { rpm_closure "$1"; }
# zypper_reason PKG: the "Installed" line of `zypper info` - "Yes (automatically)", "Yes" or "No"
# (discovery/rpm.md section 14). Without an answer from zypper (exit 7: another zypper holds the
# lock, root only; exit 104: unknown name) rpm and libzypp's own AutoInstalled list decide.
# SKEPTICAL_UPDATER_ZYPP_ROOT is a path prefix for the test-suite.
zypper_reason() {
local out rc=0 l auto=${SKEPTICAL_UPDATER_ZYPP_ROOT:-}/var/lib/zypp/AutoInstalled
out=$(zypper --non-interactive --no-refresh --quiet info "$1" </dev/null 2>/dev/null) || rc=$?
if ((rc == 0)); then
while IFS= read -r l; do
[[ $l =~ ^Installed[[:space:]]*:[[:space:]]*(.*)$ ]] || continue # not "Installed Size :"
case ${BASH_REMATCH[1]} in
'Yes (automatically)') printf 'dependency\n' ;;
Yes) printf 'explicit\n' ;;
No) printf 'not-installed\n' ;;
*) printf 'unknown\n' ;;
esac
return 0
done <<<"$out"
fi
if ! rpm -q "$1" </dev/null >/dev/null 2>&1; then
printf 'not-installed\n'
elif [[ ! -r $auto ]]; then
printf 'unknown\n'
elif grep -qxF -e "$1" -- "$auto"; then
printf 'dependency\n'
else
printf 'explicit\n'
fi
}
zypper_purge_plan() { zypper_purge_plan() {
local xml rc=0 sec name rest local xml rc=0 sec name rest
xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$1" </dev/null 2>&1) || rc=$? xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$1" </dev/null 2>&1) || rc=$?
@@ -1945,22 +2046,32 @@ print_list() {
fi fi
} }
print_info() { # print_info PKG: description + installed reverse dependencies (N? / info) # print_info PKG [OLD]: description, install reason and the installed packages that depend on PKG
local desc rd rc=0 # (N? / info). OLD "-" (a row of a new dependency) is not installed: the backend is not asked.
print_info() {
local desc rd reason rc=0
desc=$(be describe "$1") || rc=$? desc=$(be describe "$1") || rc=$?
if ((rc)); then if ((rc)); then
say "$1: (no description available: unknown package?)" say "$1: (no description available: unknown package?)"
else else
say "$C_BOLD$1$C_RESET: $desc" say "$C_BOLD$1$C_RESET: $desc"
fi fi
if [[ ${2:-} == - ]]; then
reason=not-installed
else
reason=$(be reason "$1" 2>/dev/null) || reason=unknown
fi
case $reason in
explicit) say 'reason: explicitly installed' ;;
dependency) say 'reason: installed as a dependency' ;;
not-installed) say 'reason: not installed' ;;
*) say 'reason: unknown' ;;
esac
rd=$(be rdepends "$1") || rd='' rd=$(be rdepends "$1") || rd=''
rd=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ') rd=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
rd=${rd% } rd=${rd% }
if [[ -n $rd ]]; then rd=${rd// /, }
say " required by (installed): ${rd// /, }" say "required by: ${rd:-nothing}"
else
say ' required by (installed): nothing'
fi
return "$rc" return "$rc"
} }
@@ -2248,7 +2359,7 @@ session_line() {
fi fi
i=$REPLY i=$REPLY
case $op in case $op in
'?') print_info "${R_NAME[i]}" || true ;; '?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
'??') print_full_info "${R_NAME[i]}" || true ;; '??') print_full_info "${R_NAME[i]}" || true ;;
-) -)
if ((R_EXCL[i])); then if ((R_EXCL[i])); then
+9 -2
View File
@@ -31,7 +31,10 @@
# so it is a pre-existing unneeded package (dnf repoquery --unneeded / zypper packages --unneeded). # so it is a pre-existing unneeded package (dnf repoquery --unneeded / zypper packages --unneeded).
# #
# Optional (SU_FIXTURE_B_AUTO=1): su-dummy-b counts as installed as a dependency (dnf: `dnf mark # Optional (SU_FIXTURE_B_AUTO=1): su-dummy-b counts as installed as a dependency (dnf: `dnf mark
# remove`, zypper: /var/lib/zypp/AutoInstalled), like the pacman fixture's --asdeps. # remove`, zypper: /var/lib/zypp/AutoInstalled), like the pacman fixture's --asdeps; on dnf
# su-dummy-a and su-dummy-c also count as installed by the user (`dnf mark install`): dnf4 reports
# packages installed with plain rpm as reason "unknown" (dnf5: "External User"). zypper needs
# nothing for them: a package that is not in AutoInstalled is user-installed.
# #
# Re-runnable: re-running resets everything to the "1.0 installed, 2.0 available" state. # Re-runnable: re-running resets everything to the "1.0 installed, 2.0 available" state.
# #
@@ -198,7 +201,11 @@ fi
if [ "${SU_FIXTURE_B_AUTO:-0}" = 1 ]; then if [ "${SU_FIXTURE_B_AUTO:-0}" = 1 ]; then
case $BACKEND in case $BACKEND in
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;; zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
dnf|yum) "$BACKEND" -q mark remove su-dummy-b >/dev/null ;; dnf|yum)
# dnf4 spelling first, then dnf5's (which asks for confirmation)
"$BACKEND" -q mark remove su-dummy-b >/dev/null 2>&1 || "$BACKEND" -q -y mark dependency su-dummy-b >/dev/null
"$BACKEND" -q mark install su-dummy-a su-dummy-c >/dev/null 2>&1 || "$BACKEND" -q -y mark user su-dummy-a su-dummy-c >/dev/null
;;
esac esac
log "su-dummy-b marked as installed as a dependency" log "su-dummy-b marked as installed as a dependency"
fi fi
+6 -2
View File
@@ -132,10 +132,14 @@ e2e_reset() {
rpm -Uvh --oldpackage --replacepkgs /var/tmp/su-fixture/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm >/dev/null 2>&1 rpm -Uvh --oldpackage --replacepkgs /var/tmp/su-fixture/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm >/dev/null 2>&1
;; ;;
esac esac
# su-dummy-b is a dependency of su-dummy-a (installed as such), on every backend # su-dummy-b is a dependency of su-dummy-a (installed as such), on every backend; su-dummy-a and
# su-dummy-c were installed by the user (dnf records plain rpm installs as reason unknown)
case $BE in case $BE in
apt) apt-mark auto su-dummy-orphan su-dummy-b >/dev/null ;; apt) apt-mark auto su-dummy-orphan su-dummy-b >/dev/null ;;
dnf) dnf -q mark remove su-dummy-b >/dev/null 2>&1 ;; dnf)
dnf -q mark remove su-dummy-b >/dev/null 2>&1 || dnf -q -y mark dependency su-dummy-b >/dev/null 2>&1
dnf -q mark install su-dummy-a su-dummy-c >/dev/null 2>&1 || dnf -q -y mark user su-dummy-a su-dummy-c >/dev/null 2>&1
;;
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;; zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
esac esac
[[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-orphan) == 1.0 ]] || { [[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-orphan) == 1.0 ]] || {
+22 -5
View File
@@ -32,14 +32,27 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl
refute_line --regexp '^.\[4\]' refute_line --regexp '^.\[4\]'
} }
@test "N? prints the description; su-dummy-b is required by su-dummy-a" { @test "N? prints the description, the install reason and who requires it" {
local -a lines_=()
local i a=-1 b=-1
run skeptical-updater --no-refresh <<<$'1?\n2?\nq' run skeptical-updater --no-refresh <<<$'1?\n2?\nq'
assert_success assert_success
assert_line --regexp '^su-dummy-a: .*([Dd]ummy|skeptical-updater).*$' assert_line --regexp '^su-dummy-a: .*([Dd]ummy|skeptical-updater).*$'
assert_line --regexp '^su-dummy-b: .*([Dd]ummy|skeptical-updater).*$' assert_line --regexp '^su-dummy-b: .*([Dd]ummy|skeptical-updater).*$'
# [1] su-dummy-a has no installed reverse dependency, [2] su-dummy-b is needed by su-dummy-a # [1] su-dummy-a was installed explicitly and nothing needs it; [2] su-dummy-b was installed as
assert_line ' required by (installed): nothing' # a dependency of su-dummy-a - the two lines right under each description, no indentation
assert_line ' required by (installed): su-dummy-a' mapfile -t lines_ <<<"$output"
for i in "${!lines_[@]}"; do
case ${lines_[i]} in
'su-dummy-a: '*) a=$i ;;
'su-dummy-b: '*) b=$i ;;
esac
done
[[ ${lines_[a + 1]} == 'reason: explicitly installed' && ${lines_[a + 2]} == 'required by: nothing' ]] ||
fail "1? (su-dummy-a): got '${lines_[a + 1]}' / '${lines_[a + 2]}'"
[[ ${lines_[b + 1]} == 'reason: installed as a dependency' && ${lines_[b + 2]} == 'required by: su-dummy-a' ]] ||
fail "2? (su-dummy-b): got '${lines_[b + 1]}' / '${lines_[b + 2]}'"
refute_output --partial 'required by (installed)'
assert_line 'Quit: nothing was upgraded.' assert_line 'Quit: nothing was upgraded.'
} }
@@ -250,7 +263,11 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl
assert_line --partial "$BE"$'\t'"su-dummy-c"$'\t'"user" assert_line --partial "$BE"$'\t'"su-dummy-c"$'\t'"user"
run asuser skeptical-updater info su-dummy-b run asuser skeptical-updater info su-dummy-b
assert_success assert_success
assert_line ' required by (installed): su-dummy-a' assert_line 'reason: installed as a dependency'
assert_line 'required by: su-dummy-a'
run asuser skeptical-updater info su-dummy-a
assert_success
assert_line 'reason: explicitly installed'
run asuser skeptical-updater --dry-run --no-refresh <<<'' run asuser skeptical-updater --dry-run --no-refresh <<<''
assert_success assert_success
assert_line --regexp "$(native_upgrade_regex)" assert_line --regexp "$(native_upgrade_regex)"
+6
View File
@@ -0,0 +1,6 @@
gcc-12-base
libc6
libgcc-s1
mawk
su-dummy-b
su-dummy-orphan
+7
View File
@@ -51,6 +51,13 @@ declare -A PROVIDES=(
[ncurses-libs]=$'ncurses-libs\nlibtinfo.so.6()(64bit)' [ncurses-libs]=$'ncurses-libs\nlibtinfo.so.6()(64bit)'
[redhat-rpm-config]=redhat-rpm-config [rpm]=rpm [redhat-rpm-config]=redhat-rpm-config [rpm]=rpm
) )
# what dnf4 records as the install reason (`repoquery --installed --qf %{reason}`; the fake dnf
# speaks dnf5's words with FAKE_DNF_MAJOR=5): plain `rpm -i` leaves "unknown"
declare -A REASON=(
[su-dummy-a]=user [su-dummy-b]=dependency [su-dummy-c]=unknown [su-dummy-d]=user
[bash]=user [glibc]=dependency [glibc-common]=dependency [glibc-gconv-extra]=weak-dependency
[basesystem]=group [setup]=dependency [filesystem]=dependency
)
# FAKE_RPM_NO_D=1: su-dummy-d (which requires every dummy) is not installed # FAKE_RPM_NO_D=1: su-dummy-d (which requires every dummy) is not installed
declare -A FILES=( declare -A FILES=(
[su-dummy-a]=/usr/share/su-dummy/su-dummy-a [su-dummy-b]=/usr/share/su-dummy/su-dummy-b [su-dummy-a]=/usr/share/su-dummy/su-dummy-a [su-dummy-b]=/usr/share/su-dummy/su-dummy-b
@@ -0,0 +1,19 @@
Information for package su-dummy-b:
-----------------------------------
Repository : su-dummy
Name : su-dummy-b
Version : 2.0-1
Arch : noarch
Vendor :
Installed Size : 15 B
Installed : Yes (automatically)
Status : out-of-date (version 1.0-1 installed)
Source package : su-dummy-b-2.0-1.src
Summary : skeptical-updater dummy su-dummy-b (v2.0)
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).
@@ -0,0 +1,19 @@
Information for package su-dummy-orphan-parent:
-----------------------------------------------
Repository : su-dummy
Name : su-dummy-orphan-parent
Version : 1.0-1
Arch : noarch
Vendor :
Installed Size : 27 B
Installed : No
Status : not installed
Source package : su-dummy-orphan-parent-1.0-1.src
Summary : skeptical-updater dummy su-dummy-orphan-parent (v1.0)
Description :
Dummy package su-dummy-orphan-parent version 1.0 for skeptical-updater tests.
Second paragraph of su-dummy-orphan-parent 1.0 (blank line above on purpose).
+26
View File
@@ -7,6 +7,10 @@
# FAKE_APT_MARK_UNHOLD_SIGNAL=SIG: send SIG to the applet before an unhold (a second Ctrl-C). # FAKE_APT_MARK_UNHOLD_SIGNAL=SIG: send SIG to the applet before an unhold (a second Ctrl-C).
# FAKE_APT_MARK_UNHOLD_RC=N: unhold fails (nothing released). FAKE_APT_MARK_FAIL_MSG: the message a # FAKE_APT_MARK_UNHOLD_RC=N: unhold fails (nothing released). FAKE_APT_MARK_FAIL_MSG: the message a
# failing hold prints. Every unhold logs whether $STATE_DIR/holds.lock is held by someone. # failing hold prints. Every unhold logs whether $STATE_DIR/holds.lock is held by someone.
# showauto/showmanual: installed = the "?i" rows of data/apt/dpkg-status.tsv (not purged), auto = those
# listed in data/apt/auto.txt, manual = the others; FAKE_APT_MARK_NEITHER="pkg ..." are in neither
# list, FAKE_APT_MARK_SHOW_RC=N makes both fail. Like the real ones they print only the named
# packages (all of them without a name), native packages without their ":arch" qualifier.
# shellcheck source=../fakelib.bash # shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash" . "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@" fake_log "$@"
@@ -27,8 +31,30 @@ case $verb in hold | unhold)
fi fi
;; ;;
esac esac
# show_marked auto|manual NAME...
show_marked() {
local want=$1 n st name mark
shift
local -A pick=()
for n in "$@"; do pick[${n%:all}]=1; done
if [[ -n ${FAKE_APT_MARK_SHOW_RC:-} ]]; then
echo "E: Unable to determine the package states" >&2
exit "$FAKE_APT_MARK_SHOW_RC"
fi
while IFS=$'\t' read -r st name _; do
[[ $st == ?i* ]] || continue
fake_purged "$name" && continue
[[ " ${FAKE_APT_MARK_NEITHER:-} " == *" $name "* ]] && continue
if grep -qxF -- "$name" "$FAKE_DATA/apt/auto.txt"; then mark=auto; else mark=manual; fi
[[ $mark == "$want" ]] || continue
if (($# == 0)) || [[ -n ${pick[$name]:-} || -n ${pick[$name:amd64]:-} ]]; then printf '%s\n' "$name"; fi
done <"$FAKE_DATA/apt/dpkg-status.tsv" | sort
}
case $verb in case $verb in
showhold) sort "$h" ;; showhold) sort "$h" ;;
showauto) show_marked auto "$@" ;;
showmanual) show_marked manual "$@" ;;
hold) hold)
for p in "$@"; do for p in "$@"; do
if grep -qxF -- "$p" "$h"; then echo "$p was already set on hold."; else if grep -qxF -- "$p" "$h"; then echo "$p was already set on hold."; else
+26 -2
View File
@@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini # Copyright (C) 2026 Bryan Joshua Pedini
# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures # 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)
# shellcheck source=../fakelib.bash # shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash" . "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@" fake_log "$@"
@@ -10,7 +11,30 @@ fake_log "$@"
st=${SKEPTICAL_UPDATER_STATE_DIR:-}/trusted.tsv st=${SKEPTICAL_UPDATER_STATE_DIR:-}/trusted.tsv
if [[ -n ${FAKE_STATE_APPEND:-} ]]; then printf '%s\n' "$FAKE_STATE_APPEND" >>"$st"; fi if [[ -n ${FAKE_STATE_APPEND:-} ]]; then printf '%s\n' "$FAKE_STATE_APPEND" >>"$st"; fi
if [[ -n ${FAKE_STATE_SED:-} ]]; then sed -i "$FAKE_STATE_SED" "$st"; fi if [[ -n ${FAKE_STATE_SED:-} ]]; then sed -i "$FAKE_STATE_SED" "$st"; fi
case " $* " in args=" $* "
names=()
while (($#)); do
case $1 in
-f) shift ;;
-*) ;;
*) names+=("$1") ;;
esac
shift
done
if ((${#names[@]})); then
rc=0
for n in "${names[@]}"; do
st=$(awk -F'\t' -v n="${n%:amd64}" '$2 == n { print $1; exit }' "$FAKE_DATA/apt/dpkg-status.tsv")
if [[ -z $st ]] || fake_purged "${n%:amd64}"; then
echo "dpkg-query: no packages found matching $n" >&2
rc=1
continue
fi
printf '%s' "$st"
done
exit "$rc"
fi
case $args in
*' -W '*) cat "$FAKE_DATA/apt/dpkg-status.tsv" ;; *' -W '*) cat "$FAKE_DATA/apt/dpkg-status.tsv" ;;
*) *)
echo "fake dpkg-query: unhandled: $*" >&2 echo "fake dpkg-query: unhandled: $*" >&2
+24 -1
View File
@@ -5,7 +5,9 @@
# fake dnf (unit tests). FAKE_DNF_MAJOR=4|5 picks the output dialect; FAKE_DNF_CHECK_RC=0|1 simulates # fake dnf (unit tests). FAKE_DNF_MAJOR=4|5 picks the output dialect; FAKE_DNF_CHECK_RC=0|1 simulates
# "no updates" / an error; FAKE_DNF_CHECK=obsoletes replays a check-update with only an # "no updates" / an error; FAKE_DNF_CHECK=obsoletes replays a check-update with only an
# "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail; # "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail;
# removal tables: see the --assumeno remove branch. # 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.
# shellcheck source=../fakelib.bash # shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash" . "${BASH_SOURCE[0]%/*}/../fakelib.bash"
# shellcheck source=../../data/rpm/db.bash # shellcheck source=../../data/rpm/db.bash
@@ -41,7 +43,28 @@ case " $* " in
;; ;;
*' repoquery '*) *' repoquery '*)
p=${pos[-1]} p=${pos[-1]}
rc=$(fake_rc FAKE_DNF_REPOQUERY_RC 0)
if ((rc)); then
echo "Error: rpmdb open failed" >&2
exit "$rc"
fi
case " $* " in case " $* " in
*' --installed '*'%{reason}'*)
installed "$p" || exit 0
r=${REASON[$p]:-unknown}
if ((major == 5)); then
case $r in
user) r=User ;;
dependency) r=Dependency ;;
weak-dependency) r='Weak Dependency' ;;
group) r=Group ;;
unknown) r='External User' ;;
esac
fi
[[ ${FAKE_DNF_REASON:-} != "$p="* ]] || r=${FAKE_DNF_REASON#*=}
printf '%s\n' "$r"
((major == 5)) || echo
;;
*' --upgrades '*) case $p in su-dummy-[abc]) printf 'skeptical-updater dummy %s (v2.0)\n' "$p"; ((major == 5)) || echo ;; esac ;; *' --upgrades '*) case $p in su-dummy-[abc]) printf 'skeptical-updater dummy %s (v2.0)\n' "$p"; ((major == 5)) || echo ;; esac ;;
*) case $p in su-dummy-new) printf 'skeptical-updater dummy su-dummy-new (v1.0)\n' ;; esac ;; *) case $p in su-dummy-new) printf 'skeptical-updater dummy su-dummy-new (v1.0)\n' ;; esac ;;
esac esac
+14 -1
View File
@@ -10,6 +10,7 @@
# FAKE_PACMAN_STDERR=TEXT: -Sl and -Qu also print TEXT on stderr (an unreadable database). # FAKE_PACMAN_STDERR=TEXT: -Sl and -Qu also print TEXT on stderr (an unreadable database).
# -Rs --print: FAKE_PACMAN_RS_REFUSE=1 makes pacman refuse ("breaks dependency"); # -Rs --print: FAKE_PACMAN_RS_REFUSE=1 makes pacman refuse ("breaks dependency");
# FAKE_PACMAN_RS_EXTRA="pkg:other" adds other to the plan of pkg. # FAKE_PACMAN_RS_EXTRA="pkg:other" adds other to the plan of pkg.
# FAKE_PACMAN_REASON="pkg=text" replaces the "Install Reason" of pkg in -Qi (empty text: no such line).
# FAKE_PACMAN_QU=fixture|real|none selects the -Qu sample, # FAKE_PACMAN_QU=fixture|real|none selects the -Qu sample,
# FAKE_PACMAN_SL=empty makes -Sl print nothing, FAKE_PACMAN_SUP_EXTRA="a b" adds packages that # FAKE_PACMAN_SL=empty makes -Sl print nothing, FAKE_PACMAN_SUP_EXTRA="a b" adds packages that
# only -Sup (the real transaction) shows. Every -Q*/-S[il] call records its COLUMNS value, every # only -Sup (the real transaction) shows. Every -Q*/-S[il] call records its COLUMNS value, every
@@ -107,7 +108,19 @@ case $op in
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2 [[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
[[ ${FAKE_PACMAN_SL:-} == empty ]] || cat "$D/Sl.txt" [[ ${FAKE_PACMAN_SL:-} == empty ]] || cat "$D/Sl.txt"
;; ;;
-Qi) blocks "$D/Qi.txt" "${targets[@]}" ;; -Qi)
if [[ -z ${FAKE_PACMAN_REASON:-} ]]; then
blocks "$D/Qi.txt" "${targets[@]}"
exit
fi
rc=0
out=$(blocks "$D/Qi.txt" "${targets[@]}") || rc=$?
awk -v n="${FAKE_PACMAN_REASON%%=*}" -v r="${FAKE_PACMAN_REASON#*=}" '
/^Name +: / { cur = $0; sub(/^Name +: /, "", cur) }
/^Install Reason +: / && cur == n { if (r != "") print "Install Reason : " r; next }
{ print }' <<<"$out"
exit "$rc"
;;
-Si) -Si)
t=() t=()
for a in "${targets[@]}"; do t+=("${a#*/}"); done for a in "${targets[@]}"; do t+=("${a#*/}"); done
+7
View File
@@ -6,6 +6,8 @@
# FAKE_ZYPPER_DUP=fixture|empty|removes the `dist-upgrade --dry-run` plan (data/zypper/dup-*.xml). # FAKE_ZYPPER_DUP=fixture|empty|removes the `dist-upgrade --dry-run` plan (data/zypper/dup-*.xml).
# addlock writes real zypp lock entries into the locksfile.path of $ZYPP_CONF (default # addlock writes real zypp lock entries into the locksfile.path of $ZYPP_CONF (default
# /etc/zypp/locks is never touched: the fake refuses); update/dist-upgrade log that file. # /etc/zypp/locks is never touched: the fake refuses); update/dist-upgrade log that file.
# info replays data/zypper/info-NAME.txt (exit 104 without one); FAKE_ZYPPER_INFO_RC=7 makes it fail
# like a zypp lock held by another process.
# shellcheck source=../fakelib.bash # shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash" . "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "${ZYPP_CONF:+ZYPP_CONF=$ZYPP_CONF }$*" fake_log "${ZYPP_CONF:+ZYPP_CONF=$ZYPP_CONF }$*"
@@ -43,6 +45,11 @@ refresh)
list-updates) cat "$D/lu-${FAKE_ZYPPER_LU:-fixture}.xml" ;; list-updates) cat "$D/lu-${FAKE_ZYPPER_LU:-fixture}.xml" ;;
info) info)
p=${args[-1]} p=${args[-1]}
if [[ -n ${FAKE_ZYPPER_INFO_RC:-} ]]; then
echo "System management is locked by the application with pid 4242 (zypper)."
echo "Close this application before trying again."
exit "$FAKE_ZYPPER_INFO_RC"
fi
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
echo "package '$p' not found." echo "package '$p' not found."
exit 104 exit 104
+23 -4
View File
@@ -64,11 +64,30 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
assert_line 'excluded from this run: su-dummy-b' assert_line 'excluded from this run: su-dummy-b'
} }
@test "N? prints the description and the installed packages that depend on it" { @test "N? prints the description, the install reason and the installed packages that depend on it" {
run skup --no-refresh <<<$'2?\nq' run skup --no-refresh <<<$'1?\n2?\nq'
assert_success assert_success
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' assert_output --partial "$(printf '%s\n' '> 1?' \
assert_line ' required by (installed): su-dummy-a' 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests' \
'reason: explicitly installed' \
'required by: nothing' \
'> 2?' \
'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' \
'reason: installed as a dependency' \
'required by: su-dummy-a')"
refute_output --partial 'required by (installed)'
}
@test "N? on a new dependency (old version -) says 'not installed' without asking the package manager" {
FAKE_APT_SIM=real run skup --no-refresh <<<$'9?\nq'
assert_success
assert_line --regexp '^ \[9\] su-dummy-newdep +- +-> +1\.0 +su-fixture:su-repo$'
assert_output --partial "$(printf '%s\n' '> 9?' \
'su-dummy-newdep: (no description available: unknown package?)' \
'reason: not installed' \
'required by: nothing')"
log_lacks '^apt-mark show'
log_lacks '^dpkg-query -W -f \$\{db:Status-Abbrev\} '
} }
@test "N?? prints the native full info" { @test "N?? prints the native full info" {
+156 -10
View File
@@ -50,13 +50,43 @@ setup() { common_setup; }
@test "apt: description = candidate Description; reverse deps deduplicated, '|' stripped" { @test "apt: description = candidate Description; reverse deps deduplicated, '|' stripped" {
run skup info su-dummy-b run skup info su-dummy-b
assert_success assert_success
assert_output "$(printf '%s\n' 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' ' required by (installed): su-dummy-a')" 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 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' 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 run skup info gpgv
assert_success assert_success
assert_line 'gpgv: GNU privacy guard - signature verification tool' assert_line 'gpgv: GNU privacy guard - signature verification tool'
assert_line ' required by (installed): apt' 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)" { @test "apt: the description also comes from Description-<lang> fields (Translation files)" {
@@ -141,7 +171,7 @@ setup() { common_setup; }
run skup info su-dummy-a run skup info su-dummy-a
assert_success assert_success
assert_line 'su-dummy-a: Skeptical-updater dummy package A' assert_line 'su-dummy-a: Skeptical-updater dummy package A'
assert_line ' required by (installed): nothing' assert_line 'required by: nothing'
run skup info su-dummy-new run skup info su-dummy-new
assert_success assert_success
assert_line 'su-dummy-new: Skeptical-updater dummy package NEW' assert_line 'su-dummy-new: Skeptical-updater dummy package NEW'
@@ -152,9 +182,32 @@ setup() { common_setup; }
run skup info glibc run skup info glibc
assert_success assert_success
assert_line 'glibc: GNU C Library' assert_line 'glibc: GNU C Library'
assert_line ' required by (installed): bash, gcc-libs, ncurses, readline' assert_line 'required by: bash, gcc-libs, ncurses, readline'
run skup info su-dummy-b run skup info su-dummy-b
assert_line ' required by (installed): su-dummy-a' 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" { @test "pacman: the closure resolves provides (sonames, sh) and version constraints" {
@@ -249,7 +302,7 @@ setup() { common_setup; }
run skup info su-dummy-d run skup info su-dummy-d
assert_success assert_success
assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)' assert_line 'su-dummy-d: skeptical-updater dummy su-dummy-d (v1.0)'
assert_line ' required by (installed): nothing' assert_line 'required by: nothing'
FAKE_DNF_MAJOR=5 run skup info su-dummy-a FAKE_DNF_MAJOR=5 run skup info su-dummy-a
assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)' assert_line 'su-dummy-a: skeptical-updater dummy su-dummy-a (v2.0)'
} }
@@ -257,11 +310,55 @@ setup() { common_setup; }
@test "rpm reverse deps: provides + owned files joined with every requirement (rich deps too)" { @test "rpm reverse deps: provides + owned files joined with every requirement (rich deps too)" {
use_fakes dnf rpm use_fakes dnf rpm
run skup info su-dummy-b run skup info su-dummy-b
assert_line ' required by (installed): su-dummy-a, su-dummy-d' assert_line 'required by: su-dummy-a, su-dummy-d'
run skup info su-dummy-c run skup info su-dummy-c
assert_line ' required by (installed): su-dummy-d' assert_line 'required by: su-dummy-d'
run skup info bash run skup info bash
assert_line ' required by (installed): filesystem, glibc-common, redhat-rpm-config, rpm, su-dummy-d' 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" { @test "rpm closure: file deps, rich deps without their condition, rpmlib() dropped" {
@@ -393,9 +490,58 @@ setup() { common_setup; }
@test "zypper: reverse deps and closure use the same pure-rpm code" { @test "zypper: reverse deps and closure use the same pure-rpm code" {
use_fakes zypper rpm use_fakes zypper rpm
run skup info su-dummy-c run skup info su-dummy-c
assert_line ' required by (installed): su-dummy-d' assert_line 'required by: su-dummy-d'
run skup trust su-dummy-a run skup trust su-dummy-a
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)' assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
run cut -f1 "$STATE_FILE" run cut -f1 "$STATE_FILE"
assert_line zypper 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'
}
+2 -1
View File
@@ -39,7 +39,8 @@ setup() {
assert_line "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z" assert_line "apt${T}su-dummy-a${T}user${T}2026-10-09T09:40:12Z"
run skup info su-dummy-b run skup info su-dummy-b
assert_success assert_success
assert_line ' required by (installed): su-dummy-a' assert_line 'reason: installed as a dependency'
assert_line 'required by: su-dummy-a'
run skup show su-dummy-a run skup show su-dummy-a
assert_success assert_success
assert_line 'Package: su-dummy-a' assert_line 'Package: su-dummy-a'