diff --git a/README.md b/README.md index 1024048..911430a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ upgradable packages, lets the user interrogate each one, and only then runs the native upgrade. Packages the user has explicitly acknowledged are remembered in a statefile and never questioned again. It is one file with no runtime dependencies beyond bash >= 4.3 and the distro's package manager, and it never reimplements -package management: every action shells out to the native tool. +package management: every action shells out to the native tool. On Arch Linux and +its derivatives, AUR packages join the review when `paru` or `yay` is installed +(see [AUR packages](#aur-packages-arch-linux-paru-or-yay)). The specification is [docs/DESIGN.md](docs/DESIGN.md); the per-family command research it is built on lives in [discovery/](discovery/). @@ -27,7 +29,9 @@ on first use, not by `make install`; `make uninstall` leaves it in place. Runtime needs: bash >= 4.3, the package manager, coreutils, sed and grep (plus awk, which the apt and pacman backends use and which those distros always ship). `sudo` is optional: when the tool needs root and you are not root, it re-runs itself -through `sudo` with the same arguments, or fails with a clear message. +through `sudo` with the same arguments, or fails with a clear message. On the pacman +backend `paru` or `yay` is optional too (AUR packages); the helper is run as the user +who invoked `sudo`, with `runuser` (util-linux, always there on Arch) or else `sudo -u`. ## Usage @@ -51,6 +55,9 @@ options: --dry-run print native commands instead of running mutating ones --state-dir=DIR override /var/lib/skeptical-updater (env: SKEPTICAL_UPDATER_STATE_DIR) --no-color disable ANSI colours (also honours NO_COLOR and non-tty stdout) + --aur-helper=NAME pacman backend: list and upgrade AUR packages with paru or yay, or + none (default: paru, else yay, when installed; other backends ignore it) + --no-aur same as --aur-helper=none -h, --help this help -V, --version print the version ``` @@ -60,7 +67,8 @@ otherwise the exit status of the package manager's upgrade step. What needs root: the metadata refresh, the upgrade, a purge, and writing a state directory you cannot write. `--dry-run`, `list --no-refresh`, `trusted`, `info` and -`show` work unprivileged (as long as the state directory is readable). +`show` work unprivileged (as long as the state directory is readable); there an AUR +helper simply runs as you. ### Example session @@ -73,9 +81,9 @@ Backend: apt (Debian GNU/Linux 12 (bookworm)) Refreshing package lists... done. 3 upgradable, 0 trusted (hidden, use --all to show), 3 to review: - [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo - [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo - [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held] + [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo + [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo + [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held] upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever > 2? @@ -89,7 +97,7 @@ excluded from this run: su-dummy-c > l 3 upgradable, 2 trusted (hidden, use --all to show), 1 to review: --[3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held] +-[3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held] > Excluded from this run: su-dummy-c + apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade @@ -115,7 +123,8 @@ upgrade; end of input without one aborts with exit status 2). `[held]` marks packages the package manager will not upgrade on its own: apt holds and "kept back" packages, pacman `IgnorePkg` (`[ignored]` in `pacman -Qu`). dnf `exclude=`/versionlock and zypper locks hide packages from the native listing, -so they do not appear at all. +so they do not appear at all. The column right after the number is a `*` for an AUR +package (pacman backend with paru or yay, see below) and a space on every other row. ### Command grammar @@ -188,6 +197,73 @@ replacements only the pacman transaction (`-Sup`) shows, packages dnf installs t others, packages a Tumbleweed `dist-upgrade` removes. With no row but such work, the list says "0 to review, but ... reports pending changes" and `` still runs the upgrade. +### AUR packages (Arch Linux: paru or yay) + +On the pacman backend (Arch and its derivatives, `pman` included) AUR packages are reviewed +with the rest when an AUR helper is installed: `paru`, else `yay`. `--aur-helper=paru|yay` +picks one (an error if it is not installed), `--aur-helper=none` or `--no-aur` turns the AUR +off. The banner says which helper is in use, the AUR rows get a `*` right after the number +and `aur` as their repository: + +``` +$ skeptical-updater (as a normal user: it re-runs itself through sudo) +Backend: pacman (Arch Linux) + AUR via paru +Refreshing package lists... done. +4 upgradable, 0 trusted (hidden, use --all to show), 4 to review: + + [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy + [2]* su-dummy-aur 1.0-1 -> 2.0-1 aur + [3] su-dummy-b 1.0-1 -> 2.0-1 su-dummy + [4] su-dummy-c 1.0-1 -> 2.0-1 su-dummy + + upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever +> 3- +excluded from this run: su-dummy-b +> +Excluded from this run: su-dummy-b +Checking the reviewed transaction (pacman -Sup)... ok. ++ cp -p -- /tmp/skeptical-updater-db.4242.AbC123/sync/su-dummy.db /var/lib/pacman/sync/ ++ pacman -Su --noconfirm --ignore su-dummy-b +... ++ runuser -u alice -- env HOME=/home/alice paru -Sua --ignore su-dummy-b +... +Upgrade finished. +``` + +(`pman` installed: `Backend: pman (Arch Linux) + AUR via paru`.) + +- **Listing**: `paru -Qua` / `yay -Qua` (`name old -> new`). The helper asks the AUR every time, + so `--no-refresh` lists the AUR rows too; when it fails (no network, the AUR is down) its + message is shown with `aur: paru -Qua failed, AUR packages not listed` and the session goes on + with the repository rows. A package in `IgnorePkg` is listed `[held]` by paru and left out by + yay. Devel (`-git`) packages are listed when the helper checks them (paru does by default, yay + with `--devel`), with `latest-commit` as the new version. A name a repository has wins over an + AUR row (a package that moved from the AUR into a repository). +- **`N?`, `N+`, `Nx`** work as for any installed package, through pacman (`pacman -Qi`, + the statefile with backend `pacman`, `pacman -Rns`). **`N??`** merges `pacman -Qi` with the + AUR record (`paru -Sia` / `yay -Sia`), `>` lines as usual: `> Version`, `> Repository : aur`, + `> Maintainer`, `> Votes`, ... (`show PKG` does the same for a package in no repository). +- **`N-`** excludes an AUR row through the helper's `--ignore`. +- **``** upgrades the repository packages first, exactly as without the AUR (pre-check, + the reviewed databases, `pacman -Su`), then, if AUR rows are left to upgrade, runs + `paru -Sua --ignore ` (or yay). The helper runs only after a successful + repository step; if it fails, its exit status is the tool's (`the AUR upgrade failed: paru + exited with status 1 (the repository packages were upgraded)`). When no repository row is + left to upgrade, pacman is not run at all, only the helper. `--dry-run` prints the helper's + command line. +- **Which user**: AUR helpers must not run as root (paru refuses to build as root, makepkg too). + When the tool is root because of its own `sudo` re-exec (or any `sudo`: `SUDO_USER` names a + non-root user), every helper command runs as that user: `runuser -u "$SUDO_USER" -- env + HOME= paru ...` from that home (`sudo -u` when `runuser` is missing). Run + unprivileged (`list --no-refresh`, `info`, `show`, `--dry-run`) the helper runs as you. A root + login without `SUDO_USER` gets no AUR, with a warning (`aur: paru is never run as root and + there is no invoking user ...`). +- **PKGBUILD review**: when stdin is a terminal the helper gets it and asks as usual - paru's + PKGBUILD review, yay's diff menu, and `sudo` asking for your password for its `pacman -U` if + the sudo timestamp has expired meanwhile. When stdin is not a terminal (a piped session) the + helper runs with `--noconfirm`: nothing is reviewed, and its `sudo pacman -U` must work without + a password. + ### Line editing and history When stdin is a terminal, the prompt is read with bash's readline and its defaults: Left/Right @@ -263,12 +339,14 @@ Permanent holds belong to the package manager (`apt-mark hold`, `IgnorePkg`, |---------|---------|---------|-------------------|-------| | apt | `apt-get update -qq -o APT::Update::Error-Mode=any` | `apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade` | temporary `apt-mark hold` of the excluded packages that are *not already held*, journalled in `holds.added` before holding; afterwards exactly those are released (also on failure, Ctrl-C, TERM, HUP, QUIT, a second signal during the restore, or a closed stdout; a journal left by `kill -9` is replayed at the start of the next root run). Holds you placed are never touched. `apt-mark` ignores `DPkg::Lock::Timeout`, so it is retried for up to 60 s while dpkg's lock is busy. | plan: `apt-get -s purge --auto-remove PKG` minus what `apt-get -s autoremove` already listed (`--auto-remove` is system-wide), then `apt-get -y purge` of exactly that set: PKG and the dependencies this purge orphans; no `autoremove` | | pacman / pman | `pacman -Sy --dbpath ` (with `local` linked to the real local database, seeded with the system's `*.db` and `*.db.sig`): listing, `q`, end of input and `--dry-run` never touch `/var/lib/pacman/sync`; errors pacman prints about it are shown | check `pacman -Sup --noconfirm --ignore ... --dbpath ` (on failure: nothing changed, its exit status), install the reviewed `*.db`/`*.db.sig` into `/var/lib/pacman/sync` under `db.lck`, all-or-nothing, never over a newer system database (exit 2), then `pacman -Su --noconfirm` - exactly the reviewed metadata, no second sync; with `--no-refresh` plain `pacman -Su` | `--ignore a,b` | plan: `pacman -Rs --print`; then `pacman -Rns --noconfirm -- ...` | +| AUR (pacman backend, paru or yay) | none needed: `-Qua` asks the AUR every time | after the repository step: `paru -Sua` (`--noconfirm` when stdin is not a terminal), as the user who invoked sudo | the helper's `--ignore a,b` (all excluded rows) | through pacman, like any installed package | | dnf / yum | `dnf -q --refresh --setopt=skip_if_unavailable=0 makecache` | `dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 upgrade` (the reviewed metadata; the repo glob is needed because a repo's own `metadata_expire` overrides the main one) | `--exclude=a,b` (adds to `exclude=` from the configuration) | plan: `dnf --assumeno remove PKG`; then `dnf -y remove ...` | | zypper | `zypper --non-interactive --quiet refresh` | `zypper --non-interactive --no-refresh update` (Leap/SLE) or `dist-upgrade` (Tumbleweed/Slowroll, by `ID=` in os-release) | locks in a *private* `ZYPP_CONF`: the effective configuration (main zypp.conf plus the `zypp.conf.d` drop-ins of `/usr/etc` and `/etc`) with `locksfile.path` pointing at a copy of the effective lock file (also when a drop-in sets it), plus `addlock` of the exclusions; deleted afterwards: the real lock file is never written | plan: `zypper --xmlout remove --dry-run --clean-deps PKG`; then `zypper --non-interactive --no-refresh remove --clean-deps ...` | Every package-manager call runs with `LC_ALL=C` (and `DEBIAN_FRONTEND=noninteractive`; `COLUMNS=0` for pacman), with stdin from `/dev/null` so that it can never eat the -commands you pipe in. The upgradable list comes from `apt-get -s --with-new-pkgs +commands you pipe in - except an AUR helper's upgrade on a terminal, which gets the terminal for +its own prompts. The upgradable list comes from `apt-get -s --with-new-pkgs upgrade` (Inst lines + the "kept back" block), `pacman -Qu` joined with `pacman -Sl`, `dnf -q check-update` (+ `rpm -qa` for the installed versions) and `zypper --xmlout list-updates`, or on Tumbleweed (as root) the real plan @@ -303,8 +381,8 @@ runs a subset of distros; `E2E_FAIL_FAST=1` stops at the first failing distro. No root, no network, no real package manager. The applet runs with a hermetic `PATH`: the fakes in `tests/unit/fakes//` (`apt-get`, `apt-cache`, -`dpkg-query`, `apt-mark`, `pacman`, `pman`, `dnf`, `yum`, `rpm`, `zypper`, `sudo`) -in front of symlinks to the few system tools it needs. The fakes replay the real +`dpkg-query`, `apt-mark`, `pacman`, `pman`, `dnf`, `yum`, `rpm`, `zypper`, `sudo`, and for the +AUR `paru`, `yay`, `runuser`, `getent`) 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 (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 @@ -320,9 +398,14 @@ database install / `-Su` sequence and stale throwaway databases, statefile locki 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), 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. +short candidate descriptions, dnf5's padding, the fallbacks), the prompt on a real pty (`script`): +cursor editing, Up/Down history recall, the stashed line, the bell at both ends, no history file, +and the AUR (`tests/unit/aur.bats`, on recorded paru 2.1.0 and yay 13.0.1 output): helper detection +and overrides, the user the helper runs as (`runuser -u` / `sudo -u` with `SUDO_USER`, unprivileged, +a root login), the `*` column on pacman and apt rows, `-Qua` parsing (`[ignored]`, yay's age suffix +and root warning, devel packages, failures), `N?`/`N??`/`N-`/`N+`/`Nx` on AUR rows, the upgrade order +and arguments, an AUR-only upgrade, a failing helper, `--dry-run`, and `--noconfirm` only without a +terminal. About 35 seconds. ### End-to-end tests (`tests/e2e/`) @@ -354,6 +437,20 @@ review is not installed), apt holds waiting for a busy dpkg lock, zypper honouring a lock file set in a drop-in and applying the reviewed metadata although a newer package appeared after the review. +`tests/e2e/aur.bats` covers the AUR. On archlinux the fixture (`FIXTURE_AUR=1`) also installs +`su-dummy-aur` 1.0 with `pacman -U` (a foreign package: in no repository) and keeps its 2.0 +package file, creates the user `su-tester` and makes `su` passwordless for the `wheel` group; +the tests install `fixtures/fake-paru.sh` as `/usr/local/bin/paru` (it emulates paru's `-Qua`, +`-Sia` and `-Sua` for that package, offline, refusing to install as root like paru) and +`fixtures/fake-sudo.sh` as `/usr/local/bin/sudo` (the image has no sudo: `su`, with `SUDO_USER` +set like sudo does). The applet runs as `su-tester` and re-executes itself through that sudo, or +as root with `SUDO_USER=su-tester`: the `*` row with repository `aur`, paru running as su-tester +with its HOME, `N?` and `N??` on it, `N-` keeping it at 1.0 while the repository packages upgrade, +a plain `` upgrading it to 2.0 after them, an AUR-only upgrade that leaves pacman alone, +`--dry-run`, an unprivileged `list --no-refresh`, a root login (warning, no AUR) and `--no-aur`. +On the other distros it checks the space column and that no AUR helper ever runs, even with +`--aur-helper=paru` (ignored with a warning). + The matrix (`tests/docker/images.txt`): | label | image | backend | @@ -442,6 +539,26 @@ From the discovery reports (`discovery/*.md`), all reproduced there: the current closures of all your trusted packages wholesale. A package shared by several trusted roots is recorded under the first one only, so `untrust` of that root drops it until the next `refresh`. +- **AUR** (pacman backend with paru or yay): + - The helper is run as the user named by `SUDO_USER` only: a root login (no `SUDO_USER`, or + `doas`, `run0`, `pkexec`) lists and upgrades no AUR package, with a warning. + - A piped session upgrades AUR packages with `--noconfirm`: no PKGBUILD review, and the + helper's own `sudo pacman -U` must not ask for a password (NOPASSWD, or a sudo timestamp that + is still valid). Review AUR updates on a terminal, or exclude their rows. On a terminal the + helper may ask for your password again if its sudo timestamp expired during a long review. + - The AUR listing needs the network on every run (also with `--no-refresh`); without it the + session goes on with the repository rows (warning). `N??` on an AUR row asks the AUR too. + - The helper decides what is foreign from the system's sync database, not from the reviewed + throwaway copy; a package that moved from the AUR into a repository is listed from the + repository (its AUR row is dropped). + - What the helper reports is what you get: yay leaves `IgnorePkg` packages out (paru lists them + `[held]`), devel packages are listed only when the helper checks them (paru by default, yay + with `--devel`) and show `latest-commit` as the new version; the helper's own configuration + (`paru.conf`, `yay --save`) applies to its upgrade. + - The AUR upgrade is one helper run after `pacman -Su`: a helper failure leaves the repository + packages upgraded (reported, its exit status returned). The E2E suite drives a fake paru + (offline); the formats of real paru 2.1.0 and yay 13.0.1 are verified in + `discovery/pacman.md` section 15. - The upgradable lists show what the native listing shows: new dependencies are rows on apt and on Tumbleweed (`-` as the old version); on pacman and dnf new dependencies, replacements and obsoletes only appear as a note, and removals only on Tumbleweed (note); diff --git a/discovery/pacman.md b/discovery/pacman.md index 27cfb8f..d1034ed 100644 --- a/discovery/pacman.md +++ b/discovery/pacman.md @@ -9,6 +9,7 @@ Companion files (all in `discovery/pacman/`): | `fixture.sh` | E2E fixture: dummy packages `su-dummy-{a,b,c}` 1.0 installed, 2.0 offered by a local repo | | `lib.sh` | proven bash helpers (`pm_upgradable`, `pm_field`, `pm_closure`, `pm_plan`, ...) | | `checks.sh` | executable proof of this document (`ALL CHECKS PASSED` on `archlinux:latest` and `archlinux:base-devel`) | +| `aur.sh` | proof of section 15 (paru and yay output formats; **needs network**: Arch mirrors, the AUR, GitHub) | Re-run the proof: @@ -30,6 +31,7 @@ docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/che | per-run exclusion | `pacman -Su --noconfirm --ignore a,b` (or repeated `--ignore`); globs work | | upgrade | `pacman -Su --noconfirm` (`-Syu` if you did not refresh before) | | purge | `pacman -Rns --noconfirm -- pkg...` | +| AUR (paru / yay, section 15) | list ` -Qua`, record ` -Sia PKG`, upgrade ` -Sua [--noconfirm] [--ignore a,b]`, never as root | Top gotchas (details below): @@ -615,3 +617,127 @@ Validated By : None * 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). + +## 15. AUR helpers: paru and yay (`-Qua`, `-Sia`, `-Sua`) + +Verified 2026-10-10 on archlinux:latest (pacman 7.1.0 / libalpm 16.0.1) with **paru 2.1.0** and **yay 13.0.1**, +both built from the AUR, run as the unprivileged user `bob` (wheel, NOPASSWD sudo) with stdin from `/dev/null`, +stdout piped, `LC_ALL=C`. `paru-bin` 2.1.0 does not start on this pacman (`paru: error while loading shared +libraries: libalpm.so.15: cannot open shared object file`, exit 127: it links libalpm 15), so paru was built from +the `paru` package (cargo); yay is `yay-bin`. Outdated AUR package: `pfetch` 1.10.0-1 (built from an older commit +of its AUR git), 1.11.0-1 in the AUR; devel package: `pfetch-git`; foreign package that is not in the AUR: +`su-local-only` (hand-built). Proof: `discovery/pacman/aur.sh` (needs network): + +``` +docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/aur.sh +``` + +### 15.1 `-Qua`: the AUR upgrades + +``` +$ paru -Qua $ yay -Qua +pfetch 1.10.0-1 -> 1.11.0-1 pfetch 1.10.0-1 -> 1.11.0-1 [122d13h] +[exit 0] [exit 0] +``` + +* Format `name SP old SP "->" SP new [SP extra]`, like `pacman -Qu`; no colour when piped (both). yay 13 appends the + time since the package's `Last Modified` in the AUR (`[122d13h]`: 9 Jun 2026 to 10 Oct 2026); paru nothing. + `paru -Qu` / `yay -Qu` print the same line here (repository and AUR upgrades together). +* `IgnorePkg = pfetch`: paru prints `pfetch 1.10.0-1 -> 1.11.0-1 [ignored]` (exit 0); **yay leaves the package + out** (no output, exit 1). +* Nothing to upgrade: **exit 1, nothing on stdout or stderr** (both, with and without `--devel`). +* The AUR unreachable (`127.0.0.1 aur.archlinux.org` in `/etc/hosts`): **exit 1 with the error on stderr**: + + ``` + paru: error: error sending request for url (https://aur.archlinux.org/rpc): error trying to connect: tcp connect + error: Connection refused (os error 111): ... Connection refused (os error 111) (one line) + yay: -> 1 error occurred: + * request failed: Get "https://aur.archlinux.org/rpc?arg%5B%5D=paru&...&type=info&v=5": dial tcp + 127.0.0.1:443: connect: connection refused (then 2 blank lines) + ``` + + So exit 1 with an empty stderr = nothing to upgrade; anything else = failure. +* A foreign package that is not in the AUR is not mentioned by `-Qua` (only `-Sua` says `:: packages not in the + AUR: su-local-only` / ` -> Packages not in AUR: su-local-only`). Foreign = in no sync database: with the + repositories of the E2E fixture commented out, every base package is foreign to the helper. +* Devel (VCS) packages, after `paru -S pfetch-git`, `paru --gendb`, `yay -Y --gendb` and replacing the commit ids + recorded in `~/.local/state/paru/devel.toml` / `~/.cache/yay/vcs.json` by older ones: paru checks them **by + default** (`paru -Qua`, with or without `--devel`; `-Sua` prints `:: Looking for devel upgrades...`), yay only + with `--devel` (or `yay -Y --devel --save`). The new version is the literal `latest-commit`: + + ``` + $ paru -Qua $ yay -Qua --devel ($ yay -Qua: exit 1, nothing) + pfetch-git r432.a906ff8-1 -> latest-commit pfetch-git r432.a906ff8-1 -> latest-commit [2299d6h] + ``` + +* As root: `paru -Qua` works (same output); `yay -Qua` works but prints `^[[1m^[[33m -> ^[[0m^[[0mAvoid running + yay as root/sudo.` on **stdout**. `paru -Sua` as root: `error: can't install AUR package as root` (exit 1); + `yay -Sua` as root fails as well (exit 1). The helpers must run as a normal user. + +### 15.2 `-Sia PKG`: the AUR record + +`paru -Si pfetch`, `paru -Sia pfetch` and `paru -Si --aur pfetch` print the same record; `yay -Si pfetch` asks the +repositories only (`error: package 'pfetch' was not found` / ` -> exit status 1`, exit 1), `yay -Sia pfetch` gives +the AUR record. Both are pacman-style, one record followed by a blank line: + +``` +$ paru -Sia pfetch $ yay -Sia pfetch (keys padded to 30) +Repository : aur Repository : aur +Name : pfetch Name / Version / Description / URL / Licenses / +Version : 1.11.0-1 Groups / Provides / Depends On / Optional Deps / +Description : A pretty system information tool ... Make Deps / Check Deps / Conflicts With / +URL : https://github.com/Un1q32/pfetch Replaces : None +AUR URL : https://aur.archlinux.org/packages/pfetch AUR URL : https://aur... +Groups : None First Submitted : Sat 16 Jan 2021 09:13:08 AM UTC +Licenses : MIT Keywords : fetch +Provides : pfetch Last Modified : Tue 09 Jun 2026 10:43:50 PM UTC +Depends On : None Maintainer : yigits +Make Deps : None Popularity : 0.103440 +Check Deps : None Votes : 48 +Optional Deps : None Out-of-date : No +Conflicts With : pfetch-git +Maintainer : yigits +Votes : 48 +Popularity : 0.10344 +First Submitted : Sat, 16 Jan 2021 09:13:08 +Last Modified : Tue, 9 Jun 2026 22:43:50 +Out Of Date : No +``` + +* Comparable with `pacman -Qi pfetch` (section 14): one record per side, every `:` in one column per side (paru: 17, + like pacman; yay: 31, re-padded by the merge), keys spelled like pacman's. Both, here identical: `Name`, + `Description`, `URL`, `Licenses`, `Groups`, `Provides`, `Depends On`, `Optional Deps`, `Conflicts With` (yay also + `Replaces`); different: `Version`. `-Qi` only: `Architecture`, `Required By`, `Optional For`, `Installed Size`, + `Packager`, `Build Date`, `Install Date`, `Install Reason`, `Install Script`, `Validated By`. AUR only: + `Repository` (`aur`), `AUR URL`, `Make Deps`, `Check Deps`, `Maintainer`, `Votes`, `Popularity`, `First + Submitted`, `Last Modified`, `Out Of Date` (yay: `Out-of-date`, plus `Keywords`). +* Not in the AUR: paru `error: package 'no-such-package-zzz' was not found`, yay ` -> No AUR package found for + no-such-package-zzz`, exit 1. AUR unreachable: the `-Qua` errors above, exit 1. + +### 15.3 `-Sua`: the AUR upgrade + +* `--ignore` covers AUR packages (both): `paru -Sua --noconfirm --ignore pfetch` -> ` there is nothing to do`, stderr + `warning: pfetch: ignoring package upgrade (1.10.0-1 => 1.11.0-1)`, exit 0; `yay -Sua --noconfirm --ignore pfetch` + -> ` -> pfetch: ignoring package upgrade (1.10.0-1 => 1.11.0-1)` and ` there is nothing to do` (stdout), exit 0. + Names that are not installed (`--ignore su-dummy-x,su-dummy-y`) are accepted and pfetch is upgraded. +* `--noconfirm`, stdin `/dev/null`: both upgrade without asking (exit 0). paru still prints `:: Proceed to review? + [Y/n]:` and answers it; yay's menus (`Packages to exclude`, `Packages to cleanBuild?`, `Diffs to show?`) take + their default (none). The helper installs with `sudo pacman -U` itself (here NOPASSWD; otherwise sudo asks for + the user's password on the terminal). +* **Without `--noconfirm`, stdin `/dev/null`**: paru takes the defaults at end of input and upgrades (printing the + PKGBUILD as its review); **yay exits 1** at its first menu (`:: 1 package to upgrade/install.` ... `Excluding + packages may cause partial upgrades`), nothing upgraded. So `--noconfirm` whenever stdin is not a terminal; on a + terminal the helper keeps its prompts (paru's PKGBUILD review, yay's diff menu). +* Devel: `paru -Sua --noconfirm` rebuilt `pfetch-git` for the `latest-commit` row (`warning: pfetch-git-r432.a906ff8-1 + is up to date -- reinstalling`, since only the recorded commit was faked). +* `runuser -u bob -- env` (as root) sets `HOME=/home/bob`, `USER`, `LOGNAME` and `MAIL` (PAM) and keeps `PATH` and + the working directory; `runuser` is in util-linux (the base image has it, `sudo` it has not). + +### 15.4 Chosen + +| need | command (as the invoking user: `runuser -u "$SUDO_USER" -- env HOME=`, else `sudo -u ...`; unprivileged runs as they are; a root login without `SUDO_USER`: no AUR) | +|------|---------| +| helper | `--aur-helper=paru\|yay\|none` / `--no-aur`, else `paru`, else `yay` | +| list | ` -Qua`: rows `name old new aur` (flag `aur`, `[ignored]` -> held); a name `pacman -Qu` lists wins; exit 1 + empty stderr = none, other failures: warn, no AUR rows | +| `N??` | `pacman -Qi` merged with ` -Sia PKG` (section 14's merge) | +| upgrade | after the repository step: ` -Sua [--noconfirm when stdin is not a terminal] [--ignore ]` | diff --git a/discovery/pacman/aur.sh b/discovery/pacman/aur.sh new file mode 100644 index 0000000..3a51779 --- /dev/null +++ b/discovery/pacman/aur.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# discovery/pacman/aur.sh - verify the output formats and exit codes of paru and yay that the +# AUR support of skeptical-updater relies on (-Qua, -Si/-Sia, -Sua --ignore, devel packages, +# [ignored], root refusal, AUR unreachable). NEEDS NETWORK (Arch mirrors, aur.archlinux.org, +# github.com for pfetch-git). Run as root in a throwaway container: +# docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/aur.sh +set -uo pipefail +export LC_ALL=C + +U=bob +H=/home/$U +step() { printf '\n######## %s\n' "$*"; } +# as_user CMD...: run CMD as the unprivileged user, from its home, with its HOME +as_user() { (cd "$H" && runuser -u "$U" -- env HOME="$H" "$@"); } +# show [-r] CMD...: print the command, its stdout, its stderr (prefixed "2> ") and its exit status; +# as the user unless -r; escape sequences are made visible (cat -v) +show() { + local root=0 rc=0 o e + [[ $1 == -r ]] && { root=1; shift; } + o=$(mktemp) e=$(mktemp) + printf '$ %s%s\n' "$( ((root)) && echo '(root) ')" "$*" + if ((root)); then "$@" >"$o" 2>"$e" "$o" 2>"$e" /' "$e" | cat -v + printf '[exit %s]\n' "$rc" + rm -f "$o" "$e" +} + +step "setup: base-devel git sudo, user $U (wheel, NOPASSWD)" +pacman -Syu --noconfirm --needed base-devel git sudo >/dev/null 2>&1 || { echo "pacman -Syu failed"; exit 1; } +useradd -m -G wheel "$U" +echo "$U ALL=(ALL:ALL) NOPASSWD: ALL" >/etc/sudoers.d/$U +chmod 0440 /etc/sudoers.d/$U + +# aur_build NAME [older]: clone NAME from the AUR and makepkg -si it; "older" checks out the newest +# commit whose .SRCINFO pkgver differs from the current one first (an outdated install) +aur_build() { + as_user bash -c ' + set -e + cd ~ && git clone -q "https://aur.archlinux.org/$1.git" && cd "$1" + if [ "${2:-}" = older ]; then + cur=$(sed -n "s/^\tpkgver = //p" .SRCINFO) + for c in $(git log --format=%h -- .SRCINFO); do + v=$(git show "$c:.SRCINFO" | sed -n "s/^\tpkgver = //p") + if [ "$v" != "$cur" ]; then git checkout -q "$c"; break; fi + done + fi + makepkg -si --noconfirm >/dev/null 2>&1 + ' _ "$@" || { echo "aur_build $* failed"; exit 1; } +} +# paru-bin 2.1.0 links libalpm.so.15 and does not start with pacman 7.1 (libalpm.so.16): +# "error while loading shared libraries: libalpm.so.15" (verified) - build paru from source +aur_build "${PARU_PKG:-paru}" +aur_build yay-bin +aur_build pfetch older +show paru --version +show yay --version +show -r pacman -Qm + +step "runuser -u: the environment it sets" +show -r runuser -u "$U" -- env +show -r runuser -u "$U" -- id + +step "a foreign package that is not in the AUR (local build)" +w=$(mktemp -d) +mkdir -p "$w/usr/share/su-local-only" +echo 1.0 >"$w/usr/share/su-local-only/VERSION" +printf 'pkgname = su-local-only\npkgbase = su-local-only\npkgver = 1.0-1\npkgdesc = local only\narch = any\nsize = 4\n' >"$w/.PKGINFO" +(cd "$w" && bsdtar --zstd -cf /tmp/su-local-only-1.0-1-any.pkg.tar.zst .PKGINFO usr) +pacman -U --noconfirm /tmp/su-local-only-1.0-1-any.pkg.tar.zst >/dev/null 2>&1 +show -r pacman -Qm + +step "-Qua (piped, stdin /dev/null)" +show paru -Qua +show yay -Qua +show paru -Qu +show yay -Qu + +step "-Qua with IgnorePkg = pfetch" +sed -i '0,/^\[options\]/s||[options]\nIgnorePkg = pfetch|' /etc/pacman.conf +show paru -Qua +show yay -Qua +sed -i '/^IgnorePkg = pfetch$/d' /etc/pacman.conf + +step "-Si / -Sia of an AUR package, compared with -Qi" +show -r env COLUMNS=0 pacman -Qi pfetch +show paru -Si pfetch +show paru -Sia pfetch +show paru -Si --aur pfetch +show yay -Si pfetch +show yay -Sia pfetch +show paru -Sia no-such-package-zzz +show yay -Sia no-such-package-zzz +show paru -Si bash + +step "-Qua as root" +show -r paru -Qua +show -r yay -Qua + +step "AUR unreachable (aur.archlinux.org -> 127.0.0.1)" +cp /etc/hosts /tmp/hosts.orig +echo '127.0.0.1 aur.archlinux.org' >>/etc/hosts +show paru -Qua +show yay -Qua +show paru -Sia pfetch +show yay -Sia pfetch +cat /tmp/hosts.orig >/etc/hosts + +# reset_pfetch: back to the outdated pfetch built at the start (makepkg left the package file) +reset_pfetch() { pacman -U --noconfirm "$H"/pfetch/pfetch-[0-9]*.pkg.tar.zst >/dev/null 2>&1; pacman -Q pfetch; } + +step "-Sua as root (pfetch outdated)" +show -r paru -Sua --noconfirm +show -r yay -Sua --noconfirm +show -r pacman -Q pfetch + +step "-Sua without --noconfirm, stdin /dev/null (no terminal)" +show paru -Sua +show -r pacman -Q pfetch +reset_pfetch +show yay -Sua +show -r pacman -Q pfetch +reset_pfetch + +step "-Sua --ignore (non-interactive: --noconfirm, stdin /dev/null)" +show paru -Sua --noconfirm --ignore pfetch +show -r pacman -Q pfetch +show yay -Sua --noconfirm --ignore pfetch +show -r pacman -Q pfetch +show paru -Sua --noconfirm --ignore su-dummy-x,su-dummy-y +show -r pacman -Q pfetch +reset_pfetch +show yay -Sua --noconfirm +show -r pacman -Q pfetch + +step "devel (-git) packages: pfetch-git (conflicts with pfetch, which goes first)" +show -r pacman -Rns --noconfirm pfetch +show paru -S --noconfirm pfetch-git +show -r pacman -Q pfetch-git +show paru --gendb +show yay -Y --gendb +for f in "$H/.local/state/paru/devel.toml" "$H/.cache/yay/vcs.json"; do + echo "--- $f"; cat "$f"; echo + sed -i -E 's/[0-9a-f]{40}/0000000000000000000000000000000000000000/g' "$f" + echo "--- $f with every commit id replaced by zeros (an older upstream state)"; cat "$f"; echo +done +show paru -Qua +show paru -Qua --devel +show yay -Qua +show yay -Qua --devel +show paru -Sua --noconfirm +show -r pacman -Q pfetch-git +show paru -Sua --noconfirm --devel +show -r pacman -Q pfetch-git + +step "-Qua with nothing to upgrade" +show paru -Qua +show yay -Qua +show paru -Qua --devel +show yay -Qua --devel +echo "ALL DONE" diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c4383e1..6729303 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -20,13 +20,44 @@ Order (first match wins, overridable with `--backend=`): | backend | detected by | notes | |----------|-----------------------------|--------------------------------------------| -| `pacman` | `pacman` in PATH | `pman` present => label shown as `pman`, same commands; honor pacman `IgnorePkg` (`pacman -Qu` marks them `[ignored]`); the refresh syncs a throwaway `--dbpath` copy; at `` its reviewed databases are installed and applied with `pacman -Su` | +| `pacman` | `pacman` in PATH | `pman` present => label shown as `pman`, same commands; honor pacman `IgnorePkg` (`pacman -Qu` marks them `[ignored]`); the refresh syncs a throwaway `--dbpath` copy; at `` its reviewed databases are installed and applied with `pacman -Su`; AUR packages through `paru` (else `yay`) when installed, see "AUR" below | | `apt` | `apt-get` in PATH | Debian/Ubuntu | | `dnf` | `dnf` in PATH (else `yum`) | dnf4 (EL9) and dnf5 (EL10) differences hidden in the backend | | `zypper` | `zypper` in PATH | `dist-upgrade` on Tumbleweed (ID=opensuse-tumbleweed), `update` on Leap; on Tumbleweed the review list is the `dist-upgrade --dry-run` plan | Each backend implements the same function set (see "Backend contract"). +### AUR (pacman backend only) + +| helper | detected by | overridden by | +|--------|-------------|---------------| +| `paru` | `paru` in PATH (first) | `--aur-helper=paru` (an error when it is not in PATH) | +| `yay` | `yay` in PATH (when there is no paru) | `--aur-helper=yay` (idem) | +| none | neither installed | `--aur-helper=none`, `--no-aur` | + +The helper is detected with the backend (before any sudo re-exec). The banner then reads +`Backend: pman (Arch Linux) + AUR via paru`. The other backends never look a helper up: +`--aur-helper=paru|yay` is accepted there and ignored with a warning (`--aur-helper=paru is +ignored: AUR helpers only apply to the pacman backend ...`), `--no-aur`/`--aur-helper=none` silently. + +The helper never runs as root (paru refuses to install AUR packages as root, makepkg refuses to +build as root). Decided once, after the root handling (the banner decides it; the listing runs in +subshells): + +| we are | the helper runs | +|--------|-----------------| +| not root (`list --no-refresh`, `info`, `show`, `--dry-run`) | as we are: `paru ARGS` | +| root, `SUDO_USER` names a user whose uid (`getent passwd`, else `/etc/passwd`) is not 0 - our sudo re-exec, or any sudo | as that user, from that user's home: `runuser -u "$SUDO_USER" -- env HOME= paru ARGS`, `sudo -u "$SUDO_USER" -- env HOME= ...` when there is no runuser | +| root without `SUDO_USER` (a root login), `SUDO_USER` root or unknown, neither runuser nor sudo | not at all: no AUR row, one warning (`aur: paru is never run as root and there is no invoking user (SUDO_USER is not set): AUR packages are neither listed nor upgraded ...`) | + +Helper commands (formats verified in `discovery/pacman.md` section 15, paru 2.1.0 and yay 13.0.1): + +| need | command | notes | +|------|---------|-------| +| list | ` -Qua` | `name old -> new`, then ` [ignored]` (paru; yay leaves `IgnorePkg` packages out) or yay's age (` [122d13h]`, ignored); devel packages show `latest-commit` as the new version; escape sequences and other lines are dropped. Exit 1 with nothing on stdout and stderr = nothing to upgrade; any other failure: the helper's stderr lines as warnings, then `aur: -Qua failed, AUR packages not listed`, and no AUR row. Asked on every run, also with `--no-refresh` (the helper always compares with the AUR RPC) | +| `N??` candidate | ` -Sia PKG` | for a package in no repository (`pacman -Sl`); paru's record aligns its keys like `pacman -Qi`, yay's are wider (re-padded by the merge) | +| upgrade | ` -Sua [--noconfirm] [--ignore a,b]` | see "After ``" | + ## Statefile Directory: `/var/lib/skeptical-updater/` (override: `SKEPTICAL_UPDATER_STATE_DIR`). @@ -85,9 +116,9 @@ Backend: apt (Debian GNU/Linux 12) Refreshing package lists... done. 12 upgradable, 9 trusted (hidden, use --all to show), 3 to review: - [1] nodejs 18.19.0+dfsg-6 -> 18.20.4+dfsg-1 bookworm-security - [2] libnode108 18.19.0+dfsg-6 -> 18.20.4+dfsg-1 bookworm-security - [3] zsh 5.9-4 -> 5.9-4+deb12u1 bookworm + [1] nodejs 18.19.0+dfsg-6 -> 18.20.4+dfsg-1 bookworm-security + [2] libnode108 18.19.0+dfsg-6 -> 18.20.4+dfsg-1 bookworm-security + [3] zsh 5.9-4 -> 5.9-4+deb12u1 bookworm upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever > 1? @@ -108,6 +139,19 @@ Command grammar (one line, whitespace-separated tokens, each `N` is the number i | `q` | quit without upgrading (exit 0) | | `h`, `?` | help | +List format: a marker column (`-` excluded, `=` trusted with `--all`, else a space), the number in +brackets right-aligned to the widest one, then one column that is `*` for an AUR row and a space for +every other row (on every backend: the columns stay aligned), a space, then name, old version, `->`, +new version, repository (`aur` for AUR rows) and the flags (`[held]`): + +``` + [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy +-[2]* su-dummy-aur 1.0-1 -> 2.0-1 aur +``` + +On AUR rows `N?`, `N+` and `Nx` act through pacman like on any installed package; `N??` merges +`pacman -Qi` with ` -Sia PKG`; `N-` excludes the row through the helper's `--ignore`. + Numbers are stable for the whole session (removed rows leave a gap, never renumber). All tokens of a line are validated first: an unknown token or a number that is not listed prints an error, runs nothing of that line and re-prompts. EOF on stdin aborts without @@ -142,7 +186,7 @@ ones (dnf5: 16 installed, 15 available), the candidate lines are re-padded to th | 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) | +| pacman | `pacman -Qi PKG` | `pacman -Si REPO/PKG`, REPO = the first repository `pacman -Sl` lists PKG in (as for the review list); a package in no repository: the AUR record, ` -Sia PKG`, when a helper is in use (should it not merge, `pacman -Qi` is followed by the AUR record with every line marked `> `) | | 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) | @@ -188,6 +232,17 @@ After ``: plain `pacman -Su` applies the existing databases. Throwaway databases carry their owner's pid in the name (`/tmp/skeptical-updater-db..XXXXXX`); those of runs that are gone are removed at the start of the next root run. + AUR rows (pacman backend): pacman gets `--ignore` with the excluded repository rows only. When + no repository row is left to upgrade (every one excluded or `[held]`, no pending note) but AUR + rows are, (a)-(c) are skipped ("No repository package to upgrade: pacman is not run, only + paru."). Otherwise (a)-(c) run first, and only after they succeeded (d) + ` -Sua --ignore ` runs as the invoking user (see + "AUR"), when an AUR row that is neither excluded nor `[held]` remains: with stdin a terminal the + helper gets it and asks as usual (paru's PKGBUILD review, yay's menus, its sudo password); + otherwise `--noconfirm` and stdin from `/dev/null` (yay exits 1 at its first menu on a closed + stdin). A helper failure is reported as `the AUR upgrade failed: paru exited with status N (the + repository packages were upgraded)` and its status is the exit status; `--dry-run` prints the + helper's command line. 4. Restore holds/locks, exit with the package manager's exit code. Trusted packages are upgraded silently along with everything else; they are only @@ -215,6 +270,8 @@ options: --dry-run print native commands instead of running mutating ones --state-dir=DIR override /var/lib/skeptical-updater --no-color disable ANSI colours (also honours NO_COLOR, non-tty stdout) + --aur-helper=NAME pacman backend: paru|yay|none (default: paru, else yay, when installed) + --no-aur same as --aur-helper=none --version, --help ``` @@ -236,7 +293,8 @@ Every backend is a set of bash functions with a common prefix. All output is ``` _refresh # update metadata; non-fatal on failure (warn) -_upgradable # stdout: nameoldnewrepoflags (flags: "" | "held"; old "-" = new package) +_upgradable # stdout: nameoldnewrepoflags (flags: space-separated words: + # "held", "aur" (pacman: an AUR row, shown with "*"); old "-" = new package) # 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) @@ -248,7 +306,8 @@ Every backend is a set of bash functions with a common prefix. All output is _purge_plan PKG # stdout: every package a purge of PKG and its now-unneeded dependencies removes # (simulation, no cascade); non-zero = refused _purge PKG... # non-interactive native purge/remove of exactly the planned packages -_upgrade EXCLUDED... # non-interactive native upgrade skipping EXCLUDED (handles hold/lock save+restore) +_upgrade EXCLUDED... # non-interactive native upgrade skipping EXCLUDED (handles hold/lock save+restore; + # pacman: then the AUR helper, see "After ") _label # "apt" / "pman" / ... ``` diff --git a/skeptical-updater b/skeptical-updater index cb2bdaa..a0871ff 100755 --- a/skeptical-updater +++ b/skeptical-updater @@ -52,6 +52,7 @@ opt_dry_run=0 opt_color=1 opt_state_dir= opt_state_dir_set=0 +opt_aur= # AUR helper (pacman backend): '' = detect paru, then yay; paru | yay | none cmd= cmd_args=() @@ -100,6 +101,9 @@ options: --dry-run print native commands instead of running mutating ones --state-dir=DIR override /var/lib/skeptical-updater (env: SKEPTICAL_UPDATER_STATE_DIR) --no-color disable ANSI colours (also honours NO_COLOR and non-tty stdout) + --aur-helper=NAME pacman backend: list and upgrade AUR packages with paru or yay, or + none (default: paru, else yay, when installed; other backends ignore it) + --no-aur same as --aur-helper=none -h, --help this help -V, --version print the version @@ -120,6 +124,10 @@ interactive commands (one line, whitespace-separated tokens, N = number in brack h, ? this help end of input abort without upgrading (exit 2) +A '*' right after the number marks an AUR package (pacman backend with paru or yay): +upgrades the repository packages with pacman first, then the AUR ones with the helper, run as +the user who invoked sudo; on a terminal the helper asks as usual (PKGBUILD review). + exit status: 0 ok, 1 usage/backend error, 2 aborted, otherwise the package manager's exit status from the upgrade step. EOF @@ -136,7 +144,7 @@ session_help() { pacman -Rns; purge the dependents first N+ trust N (and its installed dependencies) forever *+ *- trust / exclude every listed package (*- again re-includes them) - l redraw the list + l redraw the list (a '*' after the number marks an AUR package) q quit without upgrading h ? this help Several tokens can be given on one line, e.g. "1? 3- 4+". @@ -171,6 +179,16 @@ parse_args() { opt_state_dir_set=1 shift ;; + --aur-helper=*) + opt_aur=${a#*=} + [[ -n $opt_aur ]] || usage_error "--aur-helper needs a value" + ;; + --aur-helper) + (($#)) || usage_error "--aur-helper needs a value" + opt_aur=$1 + shift + ;; + --no-aur) opt_aur=none ;; --all) opt_all=1 ;; --no-refresh) opt_refresh=0 ;; --dry-run) opt_dry_run=1 ;; @@ -209,6 +227,10 @@ parse_args() { *) usage_error "unknown backend '$opt_backend' (expected apt, dnf, pacman or zypper)" ;; esac fi + case $opt_aur in + '' | paru | yay | none) ;; + *) usage_error "unknown AUR helper '$opt_aur' (expected paru, yay or none)" ;; + esac if ((opt_state_dir_set)); then STATE_DIR=$opt_state_dir else @@ -391,7 +413,9 @@ fmt_cmd() { # the command line, shell-quoted where needed # run_child CMD...: run CMD with stdin from /dev/null and our lock descriptors closed (a daemon # started by a maintainer script must not inherit, and so keep, our flock). -run_child() { +run_child() { run_child_stdin "$@" /dev/null) || line='' + elif [[ -r /etc/passwd ]]; then + while IFS= read -r x; do + if [[ ${x%%:*} == "$1" ]]; then + line=$x + break + fi + done /dev/null || cd /; fi + run_child ${AUR_RUN[@]+"${AUR_RUN[@]}"} "$AUR_HELPER" "$@" + ) +} + +# aur_upgradable: AUR rows (nameoldnewauraur[ held]) from ` -Qua`: +# "name old -> new", then " [ignored]" (paru; yay leaves IgnorePkg packages out) or yay 13's time +# since the AUR change (" [122d13h]"). The helper asks the AUR RPC every time (no refresh needed). +# Exit 1 with nothing on stdout and stderr = nothing to upgrade; any other failure (the AUR is +# unreachable) is warned about with the helper's messages, and no AUR row is listed. +aur_upgradable() { + local out rc=0 errf err='' l + errf=$(mktemp "${TMPDIR:-/tmp}/$SU_PROG.XXXXXX") || errf=/dev/null + out=$(aur_exec -Qua 2>"$errf") || rc=$? + if [[ $errf != /dev/null ]]; then + err=$(cat -- "$errf") + rm -f -- "$errf" + fi + if ((rc)); then + [[ $rc -ne 1 || -n $out || -n ${err//[[:space:]]/} ]] || return 0 + while IFS= read -r l; do [[ -z ${l//[[:space:]]/} ]] || warn "$AUR_HELPER: $l"; done <<<"$err" + warn "aur: $AUR_HELPER -Qua failed, AUR packages not listed" + return 0 + fi + awk '{ gsub(/\033\[[0-9;]*m/, "") } + NF >= 4 && $3 == "->" && $1 ~ /^[A-Za-z0-9_+@][A-Za-z0-9_+.@-]*$/ { + printf "%s\t%s\t%s\taur\taur%s\n", $1, $2, $4, (($5 == "[ignored]") ? " held" : "") + }' <<<"$out" +} + +# aur_info PKG: the AUR record of PKG, pacman-style "Key : Value" lines (paru: keys padded like +# pacman's, yay: wider; one record either way). -Sia: plain `yay -Si` only asks the repositories. +aur_info() { aur_exec -Sia "$1" 2>/dev/null; } + +# aur_upgrade EXCLUDED...: ` -Sua [--noconfirm] [--ignore a,b]` as the invoking user. With a +# terminal on stdin the helper gets it and asks as usual (its PKGBUILD review); otherwise +# --noconfirm, stdin from /dev/null. --dry-run prints the command line. +aur_upgrade() { + local -a c=(${AUR_RUN[@]+"${AUR_RUN[@]}"} "$AUR_HELPER" -Sua) + [[ -t 0 ]] || c+=(--noconfirm) + if (($#)); then c+=(--ignore "$(join_by , "$@")"); fi + if ((opt_dry_run)); then + say "+ $(fmt_cmd "${c[@]}")" + return 0 + fi + printf '%s+ %s%s\n' "$C_DIM" "$(fmt_cmd "${c[@]}")" "$C_RESET" 2>/dev/null || true + ( + if [[ -n $AUR_HOME ]]; then cd -- "$AUR_HOME" 2>/dev/null || cd /; fi + if [[ -t 0 ]]; then run_child_stdin "${c[@]}"; else run_child "${c[@]}"; fi + ) +} + # discovery/pacman/lib.sh pm_upgradable: -Qu joined with the first -Sl repo; [ignored] => held. # FILENAME == ARGV[1] (not FNR == NR): correct also when `pacman -Sl` prints nothing. pacman_upgradable() { @@ -1191,6 +1363,13 @@ pacman_upgradable() { ' <(printf '%s\n' "$sl") <(printf '%s\n' "$quraw")) [[ -z $qu ]] || printf '%s\n' "$qu" while IFS= read -r n; do [[ -n $n ]] && listed[${n%%$'\t'*}]=1; done <<<"$qu" + # AUR rows; a name the repositories list wins (a package that moved from the AUR into a + # repository, while the system's sync database the helper reads does not know that yet) + if aur_prepare; then + while IFS= read -r n; do + [[ -n $n && -z ${listed[${n%%$'\t'*}]+x} ]] && printf '%s\n' "$n" + done < <(aur_upgradable) + fi # what the upgrade really installs: new dependencies and replacements are not in -Qu sup=$(_pm -Sup --print-format '%n' 2>>"$errf") || sup='' while IFS= read -r n; do @@ -1236,20 +1415,36 @@ pacman_describe() { [[ -n $out ]] || return 1 printf '%s\n' "$out" } +# pacman_full_info PKG: -Qi and -Si as pacman prints them (each record ends with a blank line). A +# package in no repository: the AUR record when a helper is in use, every line marked with "> " +# (reached only when it does not merge with -Qi - paru's and yay's records do) pacman_full_info() { - local ok=1 - _pm -Qi -- "$1" 2>/dev/null && ok=0 - _pm -Si -- "$1" 2>/dev/null && ok=0 + local ok=1 qi si aur='' l + qi=$(_pm -Qi -- "$1" 2>/dev/null) && ok=0 + si=$(_pm -Si -- "$1" 2>/dev/null) && ok=0 + if [[ -z $si ]] && aur_prepare; then aur=$(aur_info "$1") || aur=''; fi + [[ -z $aur ]] || say "# lines starting with > are the AUR record ($AUR_HELPER -Sia) as it prints it" + [[ -z $qi ]] || printf '%s\n\n' "$qi" + [[ -z $si ]] || printf '%s\n\n' "$si" + if [[ -n $aur ]]; then + while IFS= read -r l; do printf '> %s\n' "$l"; done <<<"$aur" + ok=0 + fi 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) +# take it from (the first one `-Sl` lists it in, as in pacman_upgradable); in no repository, the +# AUR record when a helper is in use 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 + if [[ -n $repo ]]; then + _pm -Si -- "$repo/$1" 2>/dev/null + return + fi + aur_prepare || return 1 + aur_info "$1" } pacman_rdepends() { local out w @@ -1406,7 +1601,38 @@ pacman_install_dbs() { return 0 } +# pacman_upgrade EXCLUDED...: the repository packages with pacman (pacman_upgrade_repo), then - when +# AUR rows remain to upgrade - the AUR packages with the helper (`-Sua --ignore EXCLUDED`), only after +# a successful repository step. With nothing for pacman to do but AUR rows to upgrade, pacman is not +# run at all. pacman_upgrade() { + local i n rc=0 n_repo=0 n_aur=0 ran=0 + local -a repo_ex=() + local -A aur=() + for ((i = 0; i < ${#R_NAME[@]}; i++)); do + ((R_GONE[i] == 0)) || continue + if ((R_AUR[i])); then aur[${R_NAME[i]}]=1; fi + ((R_EXCL[i] == 0)) && [[ ${R_FLAGS[i]} != held ]] || continue + if ((R_AUR[i])); then n_aur=$((n_aur + 1)); else n_repo=$((n_repo + 1)); fi + done + for n in "$@"; do [[ -n ${aur[$n]+x} ]] || repo_ex+=("$n"); done + if ((n_repo || !n_aur || ${#R_NOTES[@]})); then + pacman_upgrade_repo ${repo_ex[@]+"${repo_ex[@]}"} || return $? + ran=1 + else + say "No repository package to upgrade: pacman is not run, only $AUR_HELPER." + fi + ((n_aur)) || return 0 + aur_upgrade "$@" || rc=$? + if ((rc)); then + UPGRADE_FAILED_MSG="the AUR upgrade failed: $AUR_HELPER exited with status $rc" + ((ran == 0)) || UPGRADE_FAILED_MSG+=" (the repository packages were upgraded)" + fi + return "$rc" +} + +# pacman_upgrade_repo EXCLUDED...: the reviewed repository transaction (see the top of this section) +pacman_upgrade_repo() { local rc=0 real tmp out local -a ign=() if (($#)); then ign=(--ignore "$(join_by , "$@")"); fi @@ -1982,27 +2208,33 @@ zypper_upgrade() { # commands # =================================================================================================== banner() { - local pretty + local pretty via='' pretty=$(os_release_field PRETTY_NAME) - say "${C_BOLD}Backend: $(be label)${pretty:+ ($pretty)}${C_RESET}" + # pacman: the AUR helper in use (decided here, once: the listing runs in subshells) + if [[ $BACKEND == pacman ]] && aur_prepare quiet; then via=" + AUR via $AUR_HELPER"; fi + say "${C_BOLD}Backend: $(be label)${pretty:+ ($pretty)}$via${C_RESET}" + if [[ $BACKEND == pacman ]]; then aur_warn_pending; fi } maybe_refresh() { if ((opt_refresh)); then be refresh || true; fi; } # rows of the review list: R_* arrays indexed 0..n-1, sorted by name; R_NOTES: what the backend # reports beyond the rows (new dependencies, removals, pending changes it does not list) -R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_NUM=() R_GONE=() R_EXCL=() R_NOTES=() +# R_FLAGS: what is shown in brackets ("held"); R_AUR: 1 for an AUR package (flag "aur" from the +# backend; shown as a "*" right after the number) +R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_AUR=() R_NUM=() R_GONE=() R_EXCL=() R_NOTES=() N_IDX=() # number -> row index N_MAX=0 load_rows() { - local out rc=0 line name old new repo flags i n=0 + local out rc=0 line name old new repo flags i n=0 w aur shown + local -a fw=() out=$(be upgradable | sort -t $'\t' -k1,1 -u) || rc=$? if ((rc)); then err "could not list the upgradable packages ($(be label) exited $rc)" exit 1 fi - R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_NUM=() R_GONE=() R_EXCL=() N_IDX=() R_NOTES=() + R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_AUR=() R_NUM=() R_GONE=() R_EXCL=() N_IDX=() R_NOTES=() while IFS= read -r line; do [[ -n $line ]] || continue if [[ $line == '#'* ]]; then @@ -2011,7 +2243,12 @@ load_rows() { fi IFS=$US read -r name old new repo flags <<<"${line//$'\t'/$US}" [[ -n $name ]] || continue - R_NAME+=("$name") R_OLD+=("${old:--}") R_NEW+=("${new:--}") R_REPO+=("${repo:--}") R_FLAGS+=("$flags") + aur=0 shown='' + read -r -a fw <<<"$flags" || true + for w in ${fw[@]+"${fw[@]}"}; do + if [[ $w == aur ]]; then aur=1; else shown+=${shown:+ }$w; fi + done + R_NAME+=("$name") R_OLD+=("${old:--}") R_NEW+=("${new:--}") R_REPO+=("${repo:--}") R_FLAGS+=("$shown") R_AUR+=("$aur") R_GONE+=(0) R_EXCL+=(0) done <<<"$out" for ((i = 0; i < ${#R_NAME[@]}; i++)); do @@ -2039,7 +2276,7 @@ row_visible() { # row_visible IDX } print_list() { - local i total=0 trusted=0 review=0 namew=4 oldw=3 neww=3 numw marker line flags repo note + local i total=0 trusted=0 review=0 namew=4 oldw=3 neww=3 numw marker aurm line flags repo note for ((i = 0; i < ${#R_NAME[@]}; i++)); do ((R_GONE[i] == 0)) || continue total=$((total + 1)) @@ -2065,11 +2302,14 @@ print_list() { elif is_trusted "${R_NAME[i]}"; then marker="${C_DIM}=${C_RESET}" fi + # an AUR package: "*" right after the bracket, every other row a space there + aurm=' ' + ((R_AUR[i] == 0)) || aurm="${C_YELLOW}*${C_RESET}" repo=${R_REPO[i]} [[ $repo != - ]] || repo= flags= [[ -z ${R_FLAGS[i]} ]] || flags=" ${C_YELLOW}[${R_FLAGS[i]}]${C_RESET}" - printf -v line '%s%*s %s%-*s%s %-*s -> %s%-*s%s %s%s' "$marker" "$numw" "[${R_NUM[i]}]" \ + printf -v line '%s%*s%s %s%-*s%s %-*s -> %s%-*s%s %s%s' "$marker" "$numw" "[${R_NUM[i]}]" "$aurm" \ "$C_BOLD" "$namew" "${R_NAME[i]}" "$C_RESET" "$oldw" "${R_OLD[i]}" \ "$C_GREEN" "$neww" "${R_NEW[i]}" "$C_RESET" "$repo" "$flags" line=${line%"${line##*[! ]}"} @@ -2445,6 +2685,7 @@ session_toggle_all() { } SESSION_PROCEED=0 +UPGRADE_FAILED_MSG= # set by _upgrade when the failing step is not the package manager's own # session_line LINE: validate every token first (a typo never executes half a line), then run them session_line() { @@ -2553,6 +2794,8 @@ cmd_interactive() { be upgrade ${excl[@]+"${excl[@]}"} || rc=$? if ((rc == 0)); then if ((opt_dry_run)); then say "Dry run: nothing was changed."; else say "Upgrade finished."; fi + elif [[ -n $UPGRADE_FAILED_MSG ]]; then + err "$UPGRADE_FAILED_MSG" else err "the upgrade failed: $(be label) exited with status $rc" upgrade_hint "$rc" ${excl[@]+"${excl[@]}"} @@ -2590,6 +2833,9 @@ main() { fi detect_backend ensure_root + if [[ $BACKEND != pacman && ($opt_aur == paru || $opt_aur == yay) ]]; then + warn "--aur-helper=$opt_aur is ignored: AUR helpers only apply to the pacman backend (Arch Linux and derivatives)" + fi recover_leftovers case $cmd in '') cmd_interactive ;; diff --git a/tests/e2e/aur.bats b/tests/e2e/aur.bats new file mode 100644 index 0000000..f6b04b2 --- /dev/null +++ b/tests/e2e/aur.bats @@ -0,0 +1,180 @@ +#!/usr/bin/env bats +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# E2E: AUR packages. pacman backend (archlinux): su-dummy-aur 1.0 is a foreign package (fixture +# FIXTURE_AUR=1), the fake paru (fixtures/fake-paru.sh) offers it at 2.0 and installs it through +# the fake sudo (fixtures/fake-sudo.sh: su, passwordless for wheel). The applet runs either as the +# test user su-tester, re-executing itself through that sudo, or as root with SUDO_USER=su-tester +# (what the re-exec leaves behind). Rows: [1] su-dummy-a [2]* su-dummy-aur [3] su-dummy-b +# [4] su-dummy-c. Other backends: the marker column is a space and no AUR helper ever runs. + +load helpers + +PARU_LOG=/tmp/su-aur-paru.log +AUR_OLD=/srv/su-aur/su-dummy-aur-1.0-1-any.pkg.tar.zst + +setup_file() { + e2e_setup_file + if [[ $BE == pacman ]]; then + install -m 0755 "$BATS_TEST_DIRNAME/fixtures/fake-paru.sh" /usr/local/bin/paru + install -m 0755 "$BATS_TEST_DIRNAME/fixtures/fake-sudo.sh" /usr/local/bin/sudo + else + # a paru that only records that it ran (it never may: AUR helpers are for pacman only) + printf '#!/bin/sh\necho "$*" >>%s\n' "$PARU_LOG" >/usr/local/bin/paru + chmod 0755 /usr/local/bin/paru + fi +} +teardown_file() { + rm -f /usr/local/bin/paru "$PARU_LOG" + if [[ $BE == pacman ]]; then rm -f /usr/local/bin/sudo; fi +} +setup() { + e2e_reset + if [[ $BE == pacman ]]; then + pacman -U --noconfirm "$AUR_OLD" >/dev/null 2>&1 + [[ $(pkg_ver su-dummy-aur) == 1.0 ]] + fi + : >"$PARU_LOG" + chmod 0666 "$PARU_LOG" +} + +# as_tester ARGS...: run skeptical-updater as the unprivileged su-tester (its TMPDIR: the suite's +# is root's); a run that needs root re-executes itself through the fake sudo +as_tester() { runuser -u su-tester -- env TMPDIR=/tmp HOME=/home/su-tester skeptical-updater "$@"; } +pacman_only() { [[ $BE == pacman ]] || skip "pacman backend only"; } + +@test "AUR: the row is listed with * and repo aur; paru ran as the invoking user after the sudo re-exec" { + pacman_only + run as_tester list + assert_success + assert_line --index 0 "Backend: pacman ($(pretty_name)) + AUR via paru" + assert_line 'Refreshing package lists... done.' + assert_line '4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:' + assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' + assert_line ' [2]* su-dummy-aur 1.0-1 -> 2.0-1 aur' + assert_line ' [3] su-dummy-b 1.0-1 -> 2.0-1 su-dummy' + assert_line ' [4] su-dummy-c 1.0-1 -> 2.0-1 su-dummy' + run cat "$PARU_LOG" + assert_output 'su-tester HOME=/home/su-tester PWD=/home/su-tester -Qua' +} + +@test "AUR: N? and N?? on the AUR row (pacman -Qi; merged with paru -Sia)" { + pacman_only + SUDO_USER=su-tester run skeptical-updater --no-refresh <<<$'2?\n2??\nq' + assert_success + assert_output --partial "$(printf '%s\n' \ + 'su-dummy-aur: Skeptical-updater dummy package AUR (foreign: in no repository)' \ + 'reason: explicitly installed' \ + 'required by: nothing')" + assert_line '# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks' + assert_output --partial "$(printf '%s\n' 'Version : 1.0-1' '> Version : 2.0-1')" + assert_line 'Install Reason : Explicitly installed' + assert_line '> Repository : aur' + assert_line '> Maintainer : su-fixture' + assert_line '> AUR URL : https://aur.archlinux.org/packages/su-dummy-aur' + run grep -c 'su-tester HOME=/home/su-tester PWD=/home/su-tester -Sia su-dummy-aur' "$PARU_LOG" + assert_output 1 +} + +@test "AUR: N- keeps su-dummy-aur at 1.0 while the repository packages upgrade; paru is not run" { + pacman_only + SUDO_USER=su-tester run skeptical-updater <<<$'2-\n' + assert_success + assert_line 'Excluded from this run: su-dummy-aur' + assert_line 'Upgrade finished.' + [[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)" + [[ $(pkg_ver su-dummy-aur) == 1.0 ]] || fail "su-dummy-aur: $(pkg_ver su-dummy-aur)" + run grep -c -- '-Sua' "$PARU_LOG" + assert_output 0 +} + +@test "AUR: a plain upgrades su-dummy-aur to 2.0 with paru, after the repository packages" { + pacman_only + run as_tester <<<'' + assert_success + assert_line 'Excluded from this run: none' + assert_line '+ runuser -u su-tester -- env HOME=/home/su-tester paru -Sua --noconfirm' + assert_line 'Upgrade finished.' + [[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)" + [[ $(pkg_ver su-dummy-aur) == 2.0 ]] || fail "su-dummy-aur: $(pkg_ver su-dummy-aur)" + # the repository step came first: su-dummy-a was 2.0 already when paru -Sua started + run grep -A1 -- ' -Sua --noconfirm$' "$PARU_LOG" + assert_line --index 0 'su-tester HOME=/home/su-tester PWD=/home/su-tester -Sua --noconfirm' + assert_line --index 1 ' (at -Sua: su-dummy-a 2.0-1)' + run skeptical-updater list --no-refresh --no-aur + assert_output --partial 'Nothing to upgrade.' + SUDO_USER=su-tester run skeptical-updater list --no-refresh + assert_output --partial 'Nothing to upgrade.' +} + +@test "AUR: only the AUR row left to upgrade: pacman is not run, only paru" { + pacman_only + # (the files only: `..` changes with the db.lck of paru's pacman -U) + before=$(ls -l --time-style=full-iso /var/lib/pacman/sync) + SUDO_USER=su-tester run skeptical-updater <<<$'1- 3- 4-\n' + assert_success + assert_line 'No repository package to upgrade: pacman is not run, only paru.' + assert_line '+ runuser -u su-tester -- env HOME=/home/su-tester paru -Sua --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c' + refute_line --regexp '^\+ pacman ' + [[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)" + [[ $(pkg_ver su-dummy-aur) == 2.0 ]] || fail "su-dummy-aur: $(pkg_ver su-dummy-aur)" + [[ $(ls -l --time-style=full-iso /var/lib/pacman/sync) == "$before" ]] || fail "the sync database changed although pacman was not run" +} + +@test "AUR: --dry-run prints paru's command line (as the invoking user) and changes nothing" { + pacman_only + SUDO_USER=su-tester run skeptical-updater --dry-run --no-refresh <<<$'3-\n' + assert_success + assert_line '+ pacman -Su --noconfirm --ignore su-dummy-b' + assert_line '+ runuser -u su-tester -- env HOME=/home/su-tester paru -Sua --noconfirm --ignore su-dummy-b' + assert_line 'Dry run: nothing was changed.' + [[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-aur) == 1.0 ]] + run grep -c -- '-Sua' "$PARU_LOG" + assert_output 0 +} + +@test "AUR: unprivileged list --no-refresh runs paru directly, as that user" { + pacman_only + run as_tester list --no-refresh + assert_success + assert_line ' [2]* su-dummy-aur 1.0-1 -> 2.0-1 aur' + run cat "$PARU_LOG" + assert_output --regexp '^su-tester HOME=/home/su-tester PWD=[^ ]+ -Qua$' +} + +@test "AUR: a root login (no SUDO_USER): skipped with a warning, the repository rows listed, paru never run" { + pacman_only + run skeptical-updater list --no-refresh + assert_success + assert_line --index 0 "Backend: pacman ($(pretty_name))" + assert_line --index 1 'skeptical-updater: warning: aur: paru is never run as root and there is no invoking user (SUDO_USER is not set): AUR packages are neither listed nor upgraded (run skeptical-updater through sudo from your own account, or use --no-aur)' + assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' + refute_output --partial 'su-dummy-aur' + [[ ! -s $PARU_LOG ]] +} + +@test "AUR: --no-aur lists the repository rows only and never runs paru" { + pacman_only + run as_tester --no-aur list --no-refresh + assert_success + assert_line --index 0 "Backend: pacman ($(pretty_name))" + assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' + refute_output --partial 'su-dummy-aur' + [[ ! -s $PARU_LOG ]] +} + +@test "other backends: the marker column is a space; no AUR helper is ever run, --aur-helper is ignored" { + [[ $BE != pacman ]] || skip "not the pacman backend" + run skeptical-updater list --no-refresh + assert_success + assert_line --regexp '^ \[1\] su-dummy-a +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$' + run skeptical-updater --aur-helper=paru list --no-refresh + assert_success + assert_line "skeptical-updater: warning: --aur-helper=paru is ignored: AUR helpers only apply to the pacman backend (Arch Linux and derivatives)" + assert_line --regexp '^ \[3\] su-dummy-c ' + run skeptical-updater --dry-run --no-refresh <<<'' + assert_success + refute_output --partial 'paru' + [[ ! -s $PARU_LOG ]] || fail "paru ran: $(cat "$PARU_LOG")" +} diff --git a/tests/e2e/entrypoint.sh b/tests/e2e/entrypoint.sh index 87eca70..640d2b5 100755 --- a/tests/e2e/entrypoint.sh +++ b/tests/e2e/entrypoint.sh @@ -27,8 +27,9 @@ install -m 0755 skeptical-updater /usr/local/bin/skeptical-updater if command -v pacman >/dev/null 2>&1; then family=pacman - # FIXTURE_SYNC=1: a system whose sync database was synced once (--no-refresh lists from it) - FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1 bash tests/e2e/fixtures/pacman.sh + # FIXTURE_SYNC=1: a system whose sync database was synced once (--no-refresh lists from it); + # FIXTURE_AUR=1: the foreign package su-dummy-aur and the user su-tester for tests/e2e/aur.bats + FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 FIXTURE_SYNC=1 FIXTURE_AUR=1 bash tests/e2e/fixtures/pacman.sh elif command -v apt-get >/dev/null 2>&1; then family=apt SU_FIXTURE_OFFLINE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1 bash tests/e2e/fixtures/apt.sh diff --git a/tests/e2e/fixtures/fake-paru.sh b/tests/e2e/fixtures/fake-paru.sh new file mode 100755 index 0000000..c6bc8dd --- /dev/null +++ b/tests/e2e/fixtures/fake-paru.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# Fake paru for the AUR E2E tests (tests/e2e/aur.bats installs it as /usr/local/bin/paru on the +# pacman backend): emulates paru 2.1.0 (discovery/pacman.md section 15) for su-dummy-aur, the +# foreign package of the fixture (FIXTURE_AUR=1), whose 2.0 "AUR build" is +# /srv/su-aur/su-dummy-aur-2.0-1-any.pkg.tar.zst. No network. +# -Qua "su-dummy-aur 1.0-1 -> 2.0-1" (+ " [ignored]" when IgnorePkg names it) while an +# older version is installed; otherwise nothing, exit 1 +# -Sia NAME paru's AUR record of su-dummy-aur 2.0-1 (pacman-style "Key : Value" lines) +# -Sua [--noconfirm] [--ignore a,b] +# installs the 2.0 package with `sudo pacman -U --noconfirm`, unless su-dummy-aur is +# ignored or up to date; refuses to run as root like paru ("can't install AUR package +# as root") +# Every call is appended to /tmp/su-aur-paru.log as " HOME= PWD= "; -Sua +# also logs the installed su-dummy-a then (the repository step must have upgraded it before). +set -uo pipefail +LOG=/tmp/su-aur-paru.log +PKG=su-dummy-aur NEW=2.0-1 FILE=/srv/su-aur/su-dummy-aur-2.0-1-any.pkg.tar.zst +printf '%s HOME=%s PWD=%s %s\n' "$(id -un)" "${HOME-}" "$PWD" "$*" >>"$LOG" + +installed() { pacman -Q "$PKG" 2>/dev/null | cut -d' ' -f2; } +ignored_by_conf() { pacman-conf IgnorePkg 2>/dev/null | grep -qx "$PKG"; } + +case ${1:-} in +-Qua) + v=$(installed) + [[ -n $v && $v != "$NEW" ]] || exit 1 + if ignored_by_conf; then echo "$PKG $v -> $NEW [ignored]"; else echo "$PKG $v -> $NEW"; fi + ;; +-Sia) + if [[ ${2:-} != "$PKG" ]]; then + echo "error: package '${2:-}' was not found" >&2 + exit 1 + fi + cat </dev/null))" >>"$LOG" + echo ':: Looking for devel upgrades...' + if ((EUID == 0)); then + echo ':: Resolving dependencies...' + echo "error: can't install AUR package as root" >&2 + exit 1 + fi + v=$(installed) + if [[ ,$ign, == *,"$PKG",* ]] || ignored_by_conf; then + echo " there is nothing to do" + echo "warning: $PKG: ignoring package upgrade ($v => $NEW)" >&2 + exit 0 + fi + if [[ -z $v || $v == "$NEW" ]]; then + echo " there is nothing to do" + exit 0 + fi + printf '\nAur (1) Old Version New Version Make Only\naur/%s %s %s No\n\n' "$PKG" "$v" "$NEW" + sudo pacman -U --noconfirm "$FILE" + ;; +*) + echo "fake paru: unhandled: $*" >&2 + exit 99 + ;; +esac diff --git a/tests/e2e/fixtures/fake-sudo.sh b/tests/e2e/fixtures/fake-sudo.sh new file mode 100755 index 0000000..b775d3b --- /dev/null +++ b/tests/e2e/fixtures/fake-sudo.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# Fake sudo for the AUR E2E tests (tests/e2e/aur.bats installs it as /usr/local/bin/sudo on the +# pacman backend): archlinux:latest has no sudo and the E2E run is offline. Runs +# "sudo [--] COMMAND..." as root through su, which the fixture (FIXTURE_AUR=1) makes passwordless +# for the members of wheel (pam_wheel trust), with the variables sudo sets for the command: +# SUDO_USER, SUDO_UID and SUDO_GID. Used for the applet's own re-exec and by the fake paru. +[[ ${1:-} != -- ]] || shift +if (($# == 0)); then + echo "usage: sudo [--] command..." >&2 + exit 1 +fi +# shellcheck disable=SC2016 # $1... belong to the root shell started by su +exec su -s /bin/bash root -c 'exec env SUDO_USER="$1" SUDO_UID="$2" SUDO_GID="$3" "${@:4}"' -- sudo "$(id -un)" "$(id -u)" "$(id -g)" "$@" diff --git a/tests/e2e/fixtures/pacman.sh b/tests/e2e/fixtures/pacman.sh index 772f9bf..545926b 100755 --- a/tests/e2e/fixtures/pacman.sh +++ b/tests/e2e/fixtures/pacman.sh @@ -3,8 +3,9 @@ # Copyright (C) 2026 Bryan Joshua Pedini # Deterministic pacman fixture for skeptical-updater E2E tests. -# (tests/e2e/fixtures/pacman.sh: copy of discovery/pacman/fixture.sh plus the FIXTURE_ORPHAN -# knob; tests/e2e/entrypoint.sh runs it with FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1.) +# (tests/e2e/fixtures/pacman.sh: copy of discovery/pacman/fixture.sh plus the FIXTURE_ORPHAN and +# FIXTURE_AUR knobs; tests/e2e/entrypoint.sh runs it with FIXTURE_ISOLATE=1 FIXTURE_ORPHAN=1 +# FIXTURE_SYNC=1 FIXTURE_AUR=1.) # # Creates three dummy packages installed at 1.0 and a local pacman repo # ([su-dummy], file:///srv/su-repo) that offers them at 2.0: @@ -37,6 +38,11 @@ # IgnorePkg) while upgrading su-dummy-a fails with unsatisfied deps. # FIXTURE_ORPHAN 0 (default) | 1 -> also install su-dummy-orphan 1.0 --asdeps with nothing # requiring it (a pre-existing orphan, `pacman -Qdtq` lists it) +# FIXTURE_AUR 0 (default) | 1 -> also install su-dummy-aur 1.0, a foreign package (in no +# repository: `pacman -Qm` lists it), and keep its 2.0 package file in +# /srv/su-aur for the fake paru of tests/e2e/aur.bats; create the user +# su-tester (group wheel) and make `su` passwordless for wheel (pam_wheel +# trust), which the fake sudo of those tests relies on (the image has no sudo) # FIXTURE_REPO repo directory (default /srv/su-repo) # FIXTURE_SYNC 0 (default) | 1 -> run `pacman -Sy` at the end # @@ -52,6 +58,7 @@ FIXTURE_STRICT_DEP=${FIXTURE_STRICT_DEP:-0} FIXTURE_REPO=${FIXTURE_REPO:-/srv/su-repo} FIXTURE_SYNC=${FIXTURE_SYNC:-0} FIXTURE_ORPHAN=${FIXTURE_ORPHAN:-0} +FIXTURE_AUR=${FIXTURE_AUR:-0} FIXTURE_REPO_NAME=su-dummy FIXTURE_EPOCH=1700000000 # fixed build date => reproducible tarballs @@ -208,7 +215,24 @@ fx_main() { fi rm -rf "$old" + if [ "$FIXTURE_AUR" = 1 ]; then + mkdir -p /srv/su-aur + [ "$FIXTURE_BUILDER" != makepkg ] || chown su-builder: /srv/su-aur + fx_build /srv/su-aur su-dummy-aur 1.0 'Skeptical-updater dummy package AUR (foreign: in no repository)' + fx_build /srv/su-aur su-dummy-aur 2.0 'Skeptical-updater dummy package AUR (foreign: in no repository)' + chmod -R a+rX /srv/su-aur + fx_pacman -U --noconfirm /srv/su-aur/su-dummy-aur-1.0-1-any.pkg.tar.zst + id su-tester >/dev/null 2>&1 || useradd --create-home --groups wheel su-tester + sed -i 's/^#\(auth[[:space:]]\+sufficient[[:space:]]\+pam_wheel\.so trust use_uid\)/\1/' /etc/pam.d/su + grep -q '^auth[[:space:]]\+sufficient[[:space:]]\+pam_wheel\.so trust use_uid' /etc/pam.d/su || + fx_die "could not enable pam_wheel trust in /etc/pam.d/su" + fi + if [ "$FIXTURE_SYNC" = 1 ]; then fx_pacman -Sy; fi + if [ "$FIXTURE_AUR" = 1 ]; then + pacman -Qmq | grep -qx su-dummy-aur || fx_die "su-dummy-aur is not a foreign package" + fx_log "foreign (AUR) package: su-dummy-aur 1.0; user su-tester (wheel, passwordless su)" + fi fx_log "builder=$FIXTURE_BUILDER isolate=$FIXTURE_ISOLATE repo=$FIXTURE_REPO" fx_log "installed: $(pacman -Q su-dummy-a su-dummy-b su-dummy-c 2>/dev/null | tr '\n' ';')" diff --git a/tests/e2e/helpers.bash b/tests/e2e/helpers.bash index d27e63d..19c9663 100644 --- a/tests/e2e/helpers.bash +++ b/tests/e2e/helpers.bash @@ -17,7 +17,7 @@ rpm) if command -v zypper >/dev/null 2>&1; then BE=zypper; else BE=dnf; fi ;; *) BE=unknown ;; esac export STATE_DIR STATE_FILE FAMILY BE -unset SKEPTICAL_UPDATER_STATE_DIR NO_COLOR +unset SKEPTICAL_UPDATER_STATE_DIR NO_COLOR SUDO_USER # shellcheck source=/dev/null pretty_name() { (. /etc/os-release && printf '%s' "$PRETTY_NAME"); } diff --git a/tests/e2e/skeptical-updater.bats b/tests/e2e/skeptical-updater.bats index 634c671..4561552 100644 --- a/tests/e2e/skeptical-updater.bats +++ b/tests/e2e/skeptical-updater.bats @@ -26,9 +26,9 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl run skeptical-updater list --no-refresh assert_success assert_line "3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:" - assert_line --regexp '^ \[1\] su-dummy-a +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$' - assert_line --regexp '^ \[2\] su-dummy-b +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$' - assert_line --regexp '^ \[3\] su-dummy-c +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$' + assert_line --regexp '^ \[1\] su-dummy-a +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$' + assert_line --regexp '^ \[2\] su-dummy-b +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$' + assert_line --regexp '^ \[3\] su-dummy-c +1\.0(-1)? +-> +2\.0(-1)? +[^ ]+$' refute_line --regexp '^.\[4\]' } @@ -100,13 +100,13 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl assert_line "3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:" refute_output --partial 'su-dummy-a' refute_output --partial 'su-dummy-b' - assert_line --regexp '^ \[1\] su-dummy-c ' + assert_line --regexp '^ \[1\] su-dummy-c ' run skeptical-updater list --no-refresh --all assert_success assert_line "3 upgradable, 2 trusted (marked with =), 1 to review:" - assert_line --regexp '^=\[1\] su-dummy-a ' - assert_line --regexp '^=\[2\] su-dummy-b ' - assert_line --regexp '^ \[3\] su-dummy-c ' + assert_line --regexp '^=\[1\] su-dummy-a ' + assert_line --regexp '^=\[2\] su-dummy-b ' + assert_line --regexp '^ \[3\] su-dummy-c ' } @test "trust / trusted / refresh / untrust from the command line" { @@ -142,7 +142,7 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl [[ $(versions) == "a=2.0 b=1.0 c=2.0" ]] || fail "versions after the run: $(versions)" [[ $(holds_snapshot) == "$before" ]] || fail "hold/lock state changed: $(holds_snapshot)" run skeptical-updater list --no-refresh - assert_line --regexp '^ \[1\] su-dummy-b +1\.0' + assert_line --regexp '^ \[1\] su-dummy-b +1\.0' } @test "a pre-existing hold/lock/IgnorePkg on su-dummy-c survives a run that excludes su-dummy-b" { @@ -150,7 +150,7 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl before=$(holds_snapshot) [[ -n $before ]] run skeptical-updater list --no-refresh - assert_line --regexp '^ \[2\] su-dummy-b ' + assert_line --regexp '^ \[2\] su-dummy-b ' run skeptical-updater <<<$'2-\n' assert_success assert_line 'Excluded from this run: su-dummy-b' @@ -193,7 +193,7 @@ zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbl assert_success assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first' refute_output --partial '[y/N]' - [[ $(grep -c '^ \[2\] su-dummy-b ' <<<"$output") -eq 2 ]] || fail "row 2 left the list" + [[ $(grep -c '^ \[2\] su-dummy-b ' <<<"$output") -eq 2 ]] || fail "row 2 left the list" [[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)" } diff --git a/tests/unit/aur.bats b/tests/unit/aur.bats new file mode 100644 index 0000000..2abaa29 --- /dev/null +++ b/tests/unit/aur.bats @@ -0,0 +1,551 @@ +#!/usr/bin/env bats +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# AUR packages on the pacman backend, through the fake paru/yay (tests/unit/fakes/paru) replaying +# the output recorded in discovery/pacman.md section 15: helper detection and overrides, the user +# the helper runs as (runuser/sudo -u, SUDO_USER, a root login), the "*" marker column, -Qua +# parsing and failures, N? / N?? / N- / N+ / Nx on an AUR row, and the upgrade (repository +# packages with pacman first, then `-Sua --ignore ...`; --noconfirm only without a terminal). +# Rows with the fake paru: [1]* pfetch [2] su-dummy-a [3]* su-dummy-aur [4] su-dummy-b +# [5] su-dummy-c (pfetch and su-dummy-aur are foreign: in no repository). + +# each @test runs in its own subshell: per-test exports are intended +# shellcheck disable=SC2030,SC2031 + +load test_helper + +setup() { + common_setup + use_fakes paru pacman runuser getent + export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-arch + # root through our sudo re-exec, invoked by alice (fake getent: uid 1000, home below) + export SUDO_USER=alice + ALICE_HOME=$FAKE_DIR/home/alice + mkdir -p "$ALICE_HOME" + AS_ALICE="runuser -u alice -- env HOME=$ALICE_HOME" +} + +ROW1=' [1]* pfetch 1.10.0-1 -> 1.11.0-1 aur' +ROW2=' [2] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' +ROW3=' [3]* su-dummy-aur 1.0-1 -> 2.0-1 aur' +ROW4=' [4] su-dummy-b 1.0-1 -> 2.0-1 su-dummy' +ROW5=' [5] su-dummy-c 1.0-1 -> 2.0-1 su-dummy' + +# upgrade_steps: the upgrade's steps from the fake log, in order (throwaway --dbpath normalised) +upgrade_steps() { + grep -E '^(pacman -Sup --noconfirm|pacman -Su |real sync at -Su|(runuser|sudo) -u .* -Sua|paru -Sua|yay -Sua)' "$FAKE_LOG" | + sed -E 's#--dbpath /tmp/skeptical-updater-db\.[^ ]+#--dbpath DB#' +} + +# ---- detection and overrides ------------------------------------------------------------------- + +@test "detection: paru first; the banner names the helper" { + use_fakes paru yay pacman runuser getent + run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via paru' + log_has 'paru -Qua' + log_lacks '^yay' +} + +@test "detection: yay when paru is not installed" { + use_fakes yay pacman runuser getent + run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via yay' + log_has "$AS_ALICE yay -Qua" + assert_line "$ROW1" + assert_line "$ROW3" +} + +@test "pman: the banner reads pman (Arch Linux) + AUR via paru" { + use_fakes pman paru pacman runuser getent + run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pman (Arch Linux) + AUR via paru' +} + +@test "--aur-helper=yay picks yay although paru is installed" { + use_fakes paru yay pacman runuser getent + run skup --aur-helper=yay list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via yay' + log_has 'yay -Qua' + log_lacks '^paru' + run skup --aur-helper yay list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via yay' +} + +@test "--no-aur and --aur-helper=none: no helper runs, no AUR row, plain banner" { + for opt in --no-aur --aur-helper=none; do + : >"$FAKE_LOG" + run skup "$opt" list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux)' + assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' + assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' + refute_output --partial 'pfetch' + log_lacks '^(paru|yay|runuser)' + done +} + +@test "no helper installed: no AUR, plain banner, every row still has the marker column" { + use_fakes pacman runuser getent + run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux)' + assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' + log_lacks '^runuser' +} + +@test "--aur-helper names a helper that is not installed: error (exit 1), nothing runs" { + use_fakes paru pacman runuser getent + run skup --aur-helper=yay list --no-refresh + assert_failure 1 + assert_output --partial 'error: --aur-helper=yay: yay not found in PATH' + [[ -z $(cat "$FAKE_LOG") ]] +} + +@test "--aur-helper: an unknown or empty value is a usage error" { + run skup --aur-helper=trizen list + assert_failure 1 + assert_output --partial "unknown AUR helper 'trizen' (expected paru, yay or none)" + run skup --aur-helper= list + assert_failure 1 + assert_output --partial '--aur-helper needs a value' + run skup list --aur-helper + assert_failure 1 + assert_output --partial '--aur-helper needs a value' + [[ -z $(cat "$FAKE_LOG") ]] +} + +@test "apt: no helper is ever run, even with paru in PATH; --aur-helper is ignored with a warning" { + use_fakes apt paru runuser getent + export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-debian12 + run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))' + assert_line ' [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo' + refute_output --partial 'aur' + run skup --aur-helper=paru list --no-refresh + assert_success + assert_output --partial 'warning: --aur-helper=paru is ignored: AUR helpers only apply to the pacman backend (Arch Linux and derivatives)' + run skup --no-aur list --no-refresh + assert_success + refute_output --partial 'warning' + log_lacks '^(paru|yay|runuser)' +} + +# ---- the user the helper runs as --------------------------------------------------------------- + +@test "root through sudo: the helper runs as SUDO_USER with runuser -u, that user's HOME, from that home" { + run skup list --no-refresh + assert_success + log_has "$AS_ALICE paru -Qua" + log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=notty" +} + +@test "root through sudo without runuser: sudo -u USER -- env HOME=..." { + use_fakes paru pacman sudo getent + run skup list --no-refresh + assert_success + log_has "sudo -u alice -- env HOME=$ALICE_HOME paru -Qua" + log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=notty" + assert_line "$ROW1" +} + +@test "unprivileged (list --no-refresh): the helper runs directly, as we are" { + export SKEPTICAL_UPDATER_EUID=1000 + unset SUDO_USER + run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via paru' + log_has 'paru -Qua' + log_lacks '^(runuser|sudo)' + assert_line "$ROW1" +} + +@test "a root login (no SUDO_USER): AUR skipped with a warning after the banner; repository rows listed" { + unset SUDO_USER + run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux)' + assert_line --index 1 'skeptical-updater: warning: aur: paru is never run as root and there is no invoking user (SUDO_USER is not set): AUR packages are neither listed nor upgraded (run skeptical-updater through sudo from your own account, or use --no-aur)' + assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' + [[ $(grep -c 'warning: aur:' <<<"$output") -eq 1 ]] + log_lacks '^(paru|runuser)' +} + +@test "SUDO_USER root (or another uid 0 name) or an unknown user: AUR skipped with a warning" { + for u in root toor; do + SUDO_USER=$u run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux)' + assert_output --partial "warning: aur: paru is never run as root and the invoking user ($u) is root" + done + SUDO_USER=nosuchuser run skup list --no-refresh + assert_success + assert_output --partial "warning: aur: the invoking user 'nosuchuser' (SUDO_USER) is unknown" + log_lacks '^(paru|runuser)' +} + +@test "a root login: the interactive upgrade runs pacman only, no helper" { + unset SUDO_USER + run skup --no-refresh <<<'' + assert_success + log_has 'pacman -Su --noconfirm' + log_lacks '^(paru|runuser)' +} + +# ---- listing ------------------------------------------------------------------------------------ + +@test "the * marker: right after the bracket of AUR rows, a space there on the others; columns aligned" { + run skup list --no-refresh + assert_success + assert_line '5 upgradable, 0 trusted (hidden, use --all to show), 5 to review:' + assert_output --partial "$(printf '%s\n' "$ROW1" "$ROW2" "$ROW3" "$ROW4" "$ROW5")" +} + +@test "the marker column stays aligned with the - and = markers" { + write_state "$HEADER" "pacman${T}su-dummy-aur${T}user${T}2026-10-09T09:40:12Z" + run skup --all --no-refresh <<<$'1-\nl\nq' + assert_success + assert_line '-[1]* pfetch 1.10.0-1 -> 1.11.0-1 aur' + assert_line '=[3]* su-dummy-aur 1.0-1 -> 2.0-1 aur' + assert_line ' [2] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' +} + +@test "--no-refresh still lists the AUR rows (the helper always asks the AUR)" { + run skup list --no-refresh + assert_success + log_lacks '^pacman -Sy' + log_has "$AS_ALICE paru -Qua" + assert_line "$ROW3" +} + +@test "with a refresh too: AUR rows next to the rows of the reviewed databases" { + run skup list + assert_success + assert_line 'Refreshing package lists... done.' + assert_line "$ROW1" + assert_line "$ROW2" +} + +@test "-Qua failing (AUR unreachable): the helper's message and a warning; repository rows listed" { + FAKE_AUR_QUA=fail run skup list --no-refresh + assert_success + assert_line --index 0 'Backend: pacman (Arch Linux) + AUR via paru' + assert_output --partial 'warning: paru: error: error sending request for url (https://aur.archlinux.org/rpc): error trying to connect' + assert_output --partial 'warning: aur: paru -Qua failed, AUR packages not listed' + assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' + refute_output --partial '*' +} + +@test "yay -Qua failing: its multi-line message is shown without the blank lines" { + use_fakes yay pacman runuser getent + FAKE_AUR_QUA=fail run skup list --no-refresh + assert_success + assert_output --partial 'warning: yay: -> 1 error occurred:' + assert_output --partial 'dial tcp 127.0.0.1:443: connect: connection refused' + assert_output --partial 'warning: aur: yay -Qua failed, AUR packages not listed' + refute_line 'skeptical-updater: warning: yay: ' +} + +@test "-Qua with nothing to upgrade (exit 1, silent) is not a failure" { + FAKE_AUR_QUA=none run skup list --no-refresh + assert_success + refute_output --partial 'warning' + assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' +} + +@test "paru marks IgnorePkg packages [ignored] (shown [held]); yay leaves them out" { + FAKE_AUR_QUA=ignored run skup list --no-refresh + assert_success + assert_line ' [1]* pfetch 1.10.0-1 -> 1.11.0-1 aur [held]' + use_fakes yay pacman runuser getent + FAKE_AUR_QUA=ignored run skup list --no-refresh + assert_success + refute_output --partial 'pfetch' + assert_line ' [2]* su-dummy-aur 1.0-1 -> 2.0-1 aur' +} + +@test "yay: the age suffix ([122d13h]) is no flag; its colored root warning on stdout is no row" { + use_fakes yay pacman runuser getent + for q in fixture noise; do + FAKE_AUR_QUA=$q run skup list --no-refresh + assert_success + assert_output --partial "$(printf '%s\n' "$ROW1" "$ROW2" "$ROW3")" + refute_output --partial 'Avoid' + refute_output --partial '122d13h' + done +} + +@test "devel packages: the new version is latest-commit (paru by default, yay with --devel)" { + for h in paru yay; do + use_fakes "$h" pacman runuser getent + FAKE_AUR_QUA=devel run skup list --no-refresh + assert_success + assert_line --regexp '^ \[1\]\* pfetch-git +r432\.a906ff8-1 +-> +latest-commit +aur$' + done +} + +@test "a name a repository lists wins over the AUR row (moved from the AUR into a repository)" { + FAKE_AUR_QUA=dup run skup list --no-refresh + assert_success + assert_line --regexp '^ \[3\] su-dummy-b +1\.0-1 +-> +2\.0-1 +su-dummy$' + refute_output --partial '9.0-1' +} + +# ---- N? N?? N+ Nx on an AUR row --------------------------------------------------------------- + +@test "N? on an AUR row: description, install reason and dependents from pacman -Qi" { + run skup --no-refresh <<<$'1?\nq' + assert_success + assert_output --partial "$(printf '%s\n' '> 1?' \ + 'pfetch: A pretty system information tool written in POSIX sh.' \ + 'reason: explicitly installed' \ + 'required by: nothing')" + log_lacks 'paru -Si' +} + +@test "N?? on an AUR row: pacman -Qi merged with paru -Sia (same key column)" { + run skup --no-refresh <<<$'1??\nq' + assert_success + log_has "$AS_ALICE paru -Sia pfetch" + assert_line '# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks' + assert_output --partial "$(printf '%s\n' 'Version : 1.10.0-1' '> Version : 1.11.0-1')" + assert_line 'Install Reason : Explicitly installed' + assert_line 'Description : A pretty system information tool written in POSIX sh.' + [[ $(count_lines 'Description : A pretty system information tool written in POSIX sh.') -eq 1 ]] + assert_output --partial "$(printf '%s\n' \ + '> Repository : aur' \ + '> AUR URL : https://aur.archlinux.org/packages/pfetch' \ + '> Make Deps : None' \ + '> Check Deps : None' \ + '> Maintainer : yigits' \ + '> Votes : 48' \ + '> Popularity : 0.10344' \ + '> First Submitted : Sat, 16 Jan 2021 09:13:08' \ + '> Last Modified : Tue, 9 Jun 2026 22:43:50' \ + '> Out Of Date : No')" +} + +@test "N?? with yay: its wider key column is re-padded to pacman's" { + use_fakes yay pacman runuser getent + run skup --no-refresh <<<$'1??\nq' + assert_success + log_has "$AS_ALICE yay -Sia pfetch" + assert_output --partial "$(printf '%s\n' 'Version : 1.10.0-1' '> Version : 1.11.0-1')" + assert_line '> Repository : aur' + assert_line '> Keywords : fetch' + assert_line '> First Submitted : Sat 16 Jan 2021 09:13:08 AM UTC' + assert_line '> Out-of-date : No' + refute_output --regexp 'Repository {10,}:' +} + +@test "N?? when the AUR has no record of it: the installed record alone" { + run skup --no-refresh <<<$'3??\nq' + assert_success + assert_line 'Name : su-dummy-aur' + assert_line 'Install Reason : Explicitly installed' + refute_line --regexp '^> [A-Z#]' + log_has "$AS_ALICE paru -Sia su-dummy-aur" +} + +@test "N?? when the AUR record does not merge: -Qi, then the AUR record with every line marked >" { + FAKE_AUR_SIA=twice run skup --no-refresh <<<$'1??\nq' + assert_success + assert_line '# lines starting with > are the AUR record (paru -Sia) as it prints it' + refute_line --partial '# lines starting with > come from the upstream candidate' + assert_line 'Version : 1.10.0-1' + [[ $(count_lines '> Version : 1.11.0-1') -eq 2 ]] + [[ $(count_lines '> Repository : aur') -eq 2 ]] + assert_line 'Install Reason : Explicitly installed' +} + +@test "show PKG for an AUR package: the same merge, the helper decided on the spot" { + run skup show pfetch + assert_success + assert_line '> Repository : aur' + assert_output --partial "$(printf '%s\n' 'Version : 1.10.0-1' '> Version : 1.11.0-1')" + export SKEPTICAL_UPDATER_EUID=1000 + unset SUDO_USER + : >"$FAKE_LOG" + run skup show pfetch + assert_success + assert_line '> Maintainer : yigits' + log_has 'paru -Sia pfetch' + log_lacks '^runuser' +} + +@test "show of an AUR package as a root login: one warning, the installed record" { + unset SUDO_USER + run skup show pfetch + assert_success + assert_line 'Name : pfetch' + refute_line --regexp '^> ' + [[ $(grep -c 'warning: aur:' <<<"$output") -eq 1 ]] + log_lacks '^paru' +} + +@test "N+ on an AUR row trusts it like any installed package (backend pacman)" { + run skup --no-refresh <<<$'1+\nl\nq' + assert_success + assert_line 'trusted pfetch' + assert_line '5 upgradable, 1 trusted (hidden, use --all to show), 4 to review:' + run cat "$STATE_FILE" + assert_line --regexp "^pacman${T}pfetch${T}user${T}${DATE_RE}\$" +} + +@test "Nx on an AUR row purges it with pacman -Rns; the row leaves the list" { + run skup --no-refresh <<<$'1x\ny\nl\nq' + assert_success + log_has 'pacman -Rs --print --print-format %n -- pfetch' + assert_line 'Purge pfetch now? [y/N] y' + log_has 'pacman -Rns --noconfirm -- pfetch' + assert_line '4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:' + [[ $(grep -c '^ \[1\]\* pfetch' <<<"$output") -eq 1 ]] || fail "row 1 is still listed after the purge" + assert_line ' [3]* su-dummy-aur 1.0-1 -> 2.0-1 aur' +} + +# ---- the upgrade -------------------------------------------------------------------------------- + +@test ": the repository step first (pre-check, databases, -Su), then the helper as the invoking user" { + run skup <<<'' + assert_success + assert_line 'Upgrade finished.' + run upgrade_steps + assert_output "$(printf '%s\n' \ + 'pacman -Sup --noconfirm --dbpath DB --print-format %n %v' \ + 'pacman -Su --noconfirm' \ + 'real sync at -Su: reviewed 1 / no-sig' \ + "$AS_ALICE paru -Sua --noconfirm" \ + 'paru -Sua --noconfirm')" + log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=notty" +} + +@test "N- on an AUR row: the helper's --ignore; pacman never sees AUR names" { + run skup --no-refresh <<<$'1-\n' + assert_success + assert_line 'Excluded from this run: pfetch' + log_has 'pacman -Su --noconfirm' + assert_line "+ $AS_ALICE paru -Sua --noconfirm --ignore pfetch" + log_has 'paru -Sua --noconfirm --ignore pfetch' + log_lacks '^pacman .*--ignore' +} + +@test "N- on a repository row: pacman --ignore and the helper's --ignore both get it" { + run skup --no-refresh <<<$'2-\n1-\n' + assert_success + log_has 'pacman -Su --noconfirm --ignore su-dummy-a' + log_has "$AS_ALICE paru -Sua --noconfirm --ignore pfetch,su-dummy-a" +} + +@test "every AUR row excluded: the helper is not run" { + run skup --no-refresh <<<$'1-\n3-\n' + assert_success + log_has 'pacman -Su --noconfirm' + log_lacks '-Sua' +} + +@test "only AUR rows left to upgrade: pacman is not run, only the helper" { + run skup <<<$'2- 4- 5-\n' + assert_success + assert_line 'No repository package to upgrade: pacman is not run, only paru.' + log_lacks '^pacman -Su' + log_lacks '^pacman -Sup --noconfirm' + log_lacks '^real sync' + log_has "$AS_ALICE paru -Sua --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c" + assert_line 'Upgrade finished.' +} + +@test "no AUR row at all: exactly the repository upgrade as before" { + FAKE_AUR_QUA=none run skup --no-refresh <<<'' + assert_success + log_has 'pacman -Su --noconfirm' + log_lacks '-Sua' +} + +@test "trusted (hidden) AUR rows are upgraded by the helper too" { + write_state "$HEADER" "pacman${T}pfetch${T}user${T}2026-10-09T09:40:12Z" "pacman${T}su-dummy-aur${T}user${T}2026-10-09T09:40:12Z" + # listed: [1] su-dummy-a [2] su-dummy-b [3] su-dummy-c (both AUR rows trusted: hidden) + run skup --no-refresh <<<$'1- 2- 3-\n' + assert_success + assert_line 'No repository package to upgrade: pacman is not run, only paru.' + log_has "$AS_ALICE paru -Sua --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c" +} + +@test "an [ignored] AUR row alone does not make pacman stand aside" { + FAKE_AUR_QUA=ignored run skup --no-refresh <<<$'2- 3- 4- 5-\n' + assert_success + log_has 'pacman -Su --noconfirm --ignore su-dummy-a,su-dummy-b,su-dummy-c' + log_lacks '-Sua' +} + +@test "the helper failing after a successful repository step: reported, its exit status returned" { + FAKE_AUR_SUA_RC=1 run skup --no-refresh <<<'' + assert_failure 1 + log_has 'pacman -Su --noconfirm' + assert_output --partial 'error: the AUR upgrade failed: paru exited with status 1 (the repository packages were upgraded)' + refute_output --partial 'hint:' + refute_output --partial 'Upgrade finished.' +} + +@test "the helper failing in an AUR-only upgrade: its exit status" { + FAKE_AUR_SUA_RC=4 run skup --no-refresh <<<$'2- 4- 5-\n' + assert_failure 4 + assert_output --partial 'error: the AUR upgrade failed: paru exited with status 4' + refute_output --partial 'repository packages were upgraded' +} + +@test "a failing repository step: the helper is not run" { + FAKE_PACMAN_SU_RC=1 run skup --no-refresh <<<'' + assert_failure 1 + assert_output --partial 'the upgrade failed: pacman exited with status 1' + log_lacks '-Sua' + FAKE_PACMAN_SUP_RC=1 run skup <<<'2-'$'\n' + assert_failure 1 + log_lacks '-Sua' +} + +@test "--dry-run prints the helper's command line (root through sudo: as the invoking user)" { + run skup --dry-run --no-refresh <<<$'1-\n' + assert_success + assert_line '+ pacman -Su --noconfirm' + assert_line "+ $AS_ALICE paru -Sua --noconfirm --ignore pfetch" + assert_line 'Dry run: nothing was changed.' + log_lacks '-Sua' + log_lacks '^pacman -Su ' +} + +@test "--dry-run unprivileged: the helper's plain command line" { + export SKEPTICAL_UPDATER_EUID=1000 + unset SUDO_USER + run skup --dry-run <<<'' + assert_success + assert_line '+ paru -Sua --noconfirm' + log_lacks '-Sua' + log_lacks '^(runuser|sudo)' +} + +@test "on a terminal: no --noconfirm, the helper gets the terminal (its PKGBUILD review)" { + pty_skup $'3-\r\r' --no-refresh + assert_success + log_has "$AS_ALICE paru -Sua --ignore su-dummy-aur" + log_has "paru as: HOME=$ALICE_HOME PWD=$ALICE_HOME stdin=tty" + log_lacks 'noconfirm --ignore su-dummy-aur' + log_has 'pacman -Su --noconfirm' +} + +@test "help and --help mention the * marker and the options" { + run skup --no-refresh <<<$'h\nq' + assert_success + assert_line " l redraw the list (a '*' after the number marks an AUR package)" + run skup --help + assert_success + assert_line --partial '--aur-helper=NAME pacman backend: list and upgrade AUR packages with paru or yay, or' + assert_line --partial '--no-aur same as --aur-helper=none' + assert_output --partial "A '*' right after the number marks an AUR package" +} diff --git a/tests/unit/backends.bats b/tests/unit/backends.bats index d47f29f..d9159af 100644 --- a/tests/unit/backends.bats +++ b/tests/unit/backends.bats @@ -193,7 +193,7 @@ DNF_UPGRADE='dnf -y --setopt=skip_if_unavailable=0 --setopt=*.metadata_expire=-1 assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first' refute_output --partial '[y/N]' log_lacks 'purge' - assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' } @test "apt: purge plan = the simulated purge minus pre-existing orphans; the target's own orphaned dependencies go too" { @@ -529,8 +529,8 @@ REAL_SYNC_RE='/pacman-db/sync' log_has 'dnf --assumeno remove su-dummy-a' assert_output --partial 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N]' log_has 'dnf -y remove su-dummy-a su-dummy-b' - [[ $(grep -c '\[4\] su-dummy-b' <<<"$output") -eq 1 ]] - [[ $(grep -c '\[3\] su-dummy-a' <<<"$output") -eq 1 ]] + [[ $(grep -c '\[4\] su-dummy-b' <<<"$output") -eq 1 ]] + [[ $(grep -c '\[3\] su-dummy-a' <<<"$output") -eq 1 ]] } @test "dnf: belt and braces - a removal table with a non-dependency is refused" { diff --git a/tests/unit/data/aur/Qua-fail-paru.err b/tests/unit/data/aur/Qua-fail-paru.err new file mode 100644 index 0000000..3c9afad --- /dev/null +++ b/tests/unit/data/aur/Qua-fail-paru.err @@ -0,0 +1 @@ +error: error sending request for url (https://aur.archlinux.org/rpc): error trying to connect: tcp connect error: Connection refused (os error 111): error trying to connect: tcp connect error: Connection refused (os error 111): tcp connect error: Connection refused (os error 111): Connection refused (os error 111) diff --git a/tests/unit/data/aur/Qua-fail-yay.err b/tests/unit/data/aur/Qua-fail-yay.err new file mode 100644 index 0000000..d63db60 --- /dev/null +++ b/tests/unit/data/aur/Qua-fail-yay.err @@ -0,0 +1,4 @@ + -> 1 error occurred: + * request failed: Get "https://aur.archlinux.org/rpc?arg%5B%5D=paru&arg%5B%5D=pfetch&arg%5B%5D=su-dummy-aur&type=info&v=5": dial tcp 127.0.0.1:443: connect: connection refused + + diff --git a/tests/unit/data/aur/Qua-paru-devel.txt b/tests/unit/data/aur/Qua-paru-devel.txt new file mode 100644 index 0000000..f50a971 --- /dev/null +++ b/tests/unit/data/aur/Qua-paru-devel.txt @@ -0,0 +1 @@ +pfetch-git r432.a906ff8-1 -> latest-commit diff --git a/tests/unit/data/aur/Qua-paru-dup.txt b/tests/unit/data/aur/Qua-paru-dup.txt new file mode 100644 index 0000000..db6419d --- /dev/null +++ b/tests/unit/data/aur/Qua-paru-dup.txt @@ -0,0 +1,2 @@ +pfetch 1.10.0-1 -> 1.11.0-1 +su-dummy-b 1.0-1 -> 9.0-1 diff --git a/tests/unit/data/aur/Qua-paru-fixture.txt b/tests/unit/data/aur/Qua-paru-fixture.txt new file mode 100644 index 0000000..9eb67a3 --- /dev/null +++ b/tests/unit/data/aur/Qua-paru-fixture.txt @@ -0,0 +1,2 @@ +pfetch 1.10.0-1 -> 1.11.0-1 +su-dummy-aur 1.0-1 -> 2.0-1 diff --git a/tests/unit/data/aur/Qua-paru-ignored.txt b/tests/unit/data/aur/Qua-paru-ignored.txt new file mode 100644 index 0000000..0877f26 --- /dev/null +++ b/tests/unit/data/aur/Qua-paru-ignored.txt @@ -0,0 +1,2 @@ +pfetch 1.10.0-1 -> 1.11.0-1 [ignored] +su-dummy-aur 1.0-1 -> 2.0-1 diff --git a/tests/unit/data/aur/Qua-paru-noise.txt b/tests/unit/data/aur/Qua-paru-noise.txt new file mode 100644 index 0000000..9eb67a3 --- /dev/null +++ b/tests/unit/data/aur/Qua-paru-noise.txt @@ -0,0 +1,2 @@ +pfetch 1.10.0-1 -> 1.11.0-1 +su-dummy-aur 1.0-1 -> 2.0-1 diff --git a/tests/unit/data/aur/Qua-yay-devel.txt b/tests/unit/data/aur/Qua-yay-devel.txt new file mode 100644 index 0000000..04d1749 --- /dev/null +++ b/tests/unit/data/aur/Qua-yay-devel.txt @@ -0,0 +1 @@ +pfetch-git r432.a906ff8-1 -> latest-commit [2299d6h] diff --git a/tests/unit/data/aur/Qua-yay-dup.txt b/tests/unit/data/aur/Qua-yay-dup.txt new file mode 100644 index 0000000..36acbfe --- /dev/null +++ b/tests/unit/data/aur/Qua-yay-dup.txt @@ -0,0 +1,2 @@ +pfetch 1.10.0-1 -> 1.11.0-1 [122d13h] +su-dummy-b 1.0-1 -> 9.0-1 [1d2h] diff --git a/tests/unit/data/aur/Qua-yay-fixture.txt b/tests/unit/data/aur/Qua-yay-fixture.txt new file mode 100644 index 0000000..c32a93f --- /dev/null +++ b/tests/unit/data/aur/Qua-yay-fixture.txt @@ -0,0 +1,2 @@ +pfetch 1.10.0-1 -> 1.11.0-1 [122d13h] +su-dummy-aur 1.0-1 -> 2.0-1 [3d4h] diff --git a/tests/unit/data/aur/Qua-yay-ignored.txt b/tests/unit/data/aur/Qua-yay-ignored.txt new file mode 100644 index 0000000..1b681bf --- /dev/null +++ b/tests/unit/data/aur/Qua-yay-ignored.txt @@ -0,0 +1 @@ +su-dummy-aur 1.0-1 -> 2.0-1 [3d4h] diff --git a/tests/unit/data/aur/Qua-yay-noise.txt b/tests/unit/data/aur/Qua-yay-noise.txt new file mode 100644 index 0000000..44cc82a --- /dev/null +++ b/tests/unit/data/aur/Qua-yay-noise.txt @@ -0,0 +1,3 @@ + -> Avoid running yay as root/sudo. +pfetch 1.10.0-1 -> 1.11.0-1 [122d13h] +su-dummy-aur 1.0-1 -> 2.0-1 [3d4h] diff --git a/tests/unit/data/aur/Sia-paru-pfetch.txt b/tests/unit/data/aur/Sia-paru-pfetch.txt new file mode 100644 index 0000000..4f71091 --- /dev/null +++ b/tests/unit/data/aur/Sia-paru-pfetch.txt @@ -0,0 +1,21 @@ +Repository : aur +Name : pfetch +Version : 1.11.0-1 +Description : A pretty system information tool written in POSIX sh. +URL : https://github.com/Un1q32/pfetch +AUR URL : https://aur.archlinux.org/packages/pfetch +Groups : None +Licenses : MIT +Provides : pfetch +Depends On : None +Make Deps : None +Check Deps : None +Optional Deps : None +Conflicts With : pfetch-git +Maintainer : yigits +Votes : 48 +Popularity : 0.10344 +First Submitted : Sat, 16 Jan 2021 09:13:08 +Last Modified : Tue, 9 Jun 2026 22:43:50 +Out Of Date : No + diff --git a/tests/unit/data/aur/Sia-yay-pfetch.txt b/tests/unit/data/aur/Sia-yay-pfetch.txt new file mode 100644 index 0000000..42b411c --- /dev/null +++ b/tests/unit/data/aur/Sia-yay-pfetch.txt @@ -0,0 +1,23 @@ +Repository : aur +Name : pfetch +Version : 1.11.0-1 +Description : A pretty system information tool written in POSIX sh. +URL : https://github.com/Un1q32/pfetch +Licenses : MIT +Groups : None +Provides : pfetch +Depends On : None +Optional Deps : None +Make Deps : None +Check Deps : None +Conflicts With : pfetch-git +Replaces : None +AUR URL : https://aur.archlinux.org/packages/pfetch +First Submitted : Sat 16 Jan 2021 09:13:08 AM UTC +Keywords : fetch +Last Modified : Tue 09 Jun 2026 10:43:50 PM UTC +Maintainer : yigits +Popularity : 0.103440 +Votes : 48 +Out-of-date : No + diff --git a/tests/unit/data/pacman/Qi.txt b/tests/unit/data/pacman/Qi.txt index 5f668f1..a5a3460 100644 --- a/tests/unit/data/pacman/Qi.txt +++ b/tests/unit/data/pacman/Qi.txt @@ -288,3 +288,47 @@ Install Date : Sun Oct 4 00:00:00 2026 Install Reason : Installed as a dependency for another package Install Script : No Validated By : Signature + +Name : pfetch +Version : 1.10.0-1 +Description : A pretty system information tool written in POSIX sh. +Architecture : any +URL : https://github.com/Un1q32/pfetch +Licenses : MIT +Groups : None +Provides : pfetch +Depends On : None +Optional Deps : None +Required By : None +Optional For : None +Conflicts With : pfetch-git +Replaces : None +Installed Size : 81.50 KiB +Packager : Unknown Packager +Build Date : Sat Oct 10 12:42:02 2026 +Install Date : Sat Oct 10 12:42:04 2026 +Install Reason : Explicitly installed +Install Script : No +Validated By : None + +Name : su-dummy-aur +Version : 1.0-1 +Description : Skeptical-updater dummy package AUR (foreign: in no repository) +Architecture : any +URL : https://example.invalid/su-dummy +Licenses : custom +Groups : None +Provides : None +Depends On : None +Optional Deps : None +Required By : None +Optional For : None +Conflicts With : None +Replaces : None +Installed Size : 4.00 B +Packager : su-fixture +Build Date : Tue Nov 14 22:13:20 2023 +Install Date : Fri Oct 9 08:00:00 2026 +Install Reason : Explicitly installed +Install Script : No +Validated By : None diff --git a/tests/unit/fakes/getent/getent b/tests/unit/fakes/getent/getent new file mode 100755 index 0000000..4073695 --- /dev/null +++ b/tests/unit/fakes/getent/getent @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# fake getent (unit tests): "getent passwd NAME" for alice (uid 1000, home $FAKE_DIR/home/alice), +# root and toor (uid 0); exit 2 (no such key) for anyone else +[[ ${1:-} == passwd ]] || exit 1 +case ${2:-} in +alice) echo "alice:x:1000:1000:Alice:${FAKE_DIR:?}/home/alice:/bin/bash" ;; +root) echo "root:x:0:0::/root:/bin/bash" ;; +toor) echo "toor:x:0:0:root by another name:/root:/bin/bash" ;; +*) exit 2 ;; +esac diff --git a/tests/unit/fakes/pacman/pacman b/tests/unit/fakes/pacman/pacman index fa28cc9..aeab990 100755 --- a/tests/unit/fakes/pacman/pacman +++ b/tests/unit/fakes/pacman/pacman @@ -145,7 +145,7 @@ case $op in fi case $tg in su-dummy-a) printf '%s\n' su-dummy-a su-dummy-b ;; # b was installed as a dependency of a - su-dummy-c | glib2 | expat | ca-certificates-mozilla) echo "$tg" ;; + su-dummy-c | glib2 | expat | ca-certificates-mozilla | pfetch | su-dummy-aur) echo "$tg" ;; # (the last two: foreign, AUR) *) echo "error: target not found: $tg" >&2 exit 1 diff --git a/tests/unit/fakes/paru/paru b/tests/unit/fakes/paru/paru new file mode 100755 index 0000000..44bf12e --- /dev/null +++ b/tests/unit/fakes/paru/paru @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# fake paru (unit tests; fakes/yay/yay runs this file as yay): replays the output of paru 2.1.0 +# and yay 13.0.1 recorded in discovery/pacman.md section 15 (tests/unit/data/aur/) and logs every +# call, followed by " as: HOME=... PWD=... stdin=tty|notty" (where and how it runs). +# FAKE_AUR_QUA selects what -Qua prints: fixture (default: pfetch and su-dummy-aur), ignored +# (pfetch in IgnorePkg: paru marks it [ignored], yay leaves it out), devel (a -git package whose +# upstream moved: "-> latest-commit"), dup (an AUR row for su-dummy-b, which a repository has), +# noise (yay's colored root warning on stdout), none (nothing to upgrade: exit 1, no output) or +# fail (the AUR is unreachable: exit 1 with the recorded error). Packages purged by the fake pacman +# are not listed. -Sia PKG prints data/aur/Sia--PKG.txt (else the helper's not-found +# error, exit 1), twice with FAKE_AUR_SIA=twice (a record that cannot be merged); -Sua exits +# FAKE_AUR_SUA_RC (default 0). +# shellcheck source=../fakelib.bash +. "${BASH_SOURCE[0]%/*}/../fakelib.bash" +fake_log "$@" +if [[ -t 0 ]]; then stdin='tty'; else stdin='notty'; fi +printf '%s as: HOME=%s PWD=%s stdin=%s\n' "$FAKE_NAME" "${HOME-}" "$PWD" "$stdin" >>"$FAKE_LOG" +D=$FAKE_DATA/aur + +case ${1:-} in +-Qua) + case ${FAKE_AUR_QUA:-fixture} in + none) exit 1 ;; + fail) + cat "$D/Qua-fail-$FAKE_NAME.err" >&2 + exit 1 + ;; + esac + out=$( + while IFS= read -r line; do + fake_purged "${line%% *}" || printf '%s\n' "$line" + done <"$D/Qua-$FAKE_NAME-${FAKE_AUR_QUA:-fixture}.txt" + ) + [[ -n $out ]] || exit 1 + printf '%s\n' "$out" + ;; +-Sia) + if [[ -f $D/Sia-$FAKE_NAME-${2:-}.txt ]]; then + cat "$D/Sia-$FAKE_NAME-$2.txt" + [[ ${FAKE_AUR_SIA:-} != twice ]] || cat "$D/Sia-$FAKE_NAME-$2.txt" + exit 0 + fi + if [[ $FAKE_NAME == yay ]]; then + echo " -> No AUR package found for ${2:-}" >&2 + else + echo "error: package '${2:-}' was not found" >&2 + fi + exit 1 + ;; +-Sua) exit "$(fake_rc FAKE_AUR_SUA_RC 0)" ;; +*) + echo "fake $FAKE_NAME: unhandled: $*" >&2 + exit 99 + ;; +esac diff --git a/tests/unit/fakes/runuser/runuser b/tests/unit/fakes/runuser/runuser new file mode 100755 index 0000000..861041c --- /dev/null +++ b/tests/unit/fakes/runuser/runuser @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# fake runuser (unit tests): logs "runuser -u USER -- CMD..." and runs CMD (as the same user) +# shellcheck source=../fakelib.bash +. "${BASH_SOURCE[0]%/*}/../fakelib.bash" +fake_log "$@" +if [[ ${1:-} != -u || ${3:-} != -- ]]; then + echo "fake runuser: unexpected arguments: $*" >&2 + exit 98 +fi +shift 3 +exec "$@" diff --git a/tests/unit/fakes/sudo/sudo b/tests/unit/fakes/sudo/sudo index 5d3761e..6cfa3e9 100755 --- a/tests/unit/fakes/sudo/sudo +++ b/tests/unit/fakes/sudo/sudo @@ -2,8 +2,13 @@ # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2026 Bryan Joshua Pedini -# fake sudo (unit tests): logs the command it was asked to run, runs nothing +# fake sudo (unit tests): logs the command it was asked to run; runs nothing (the re-exec of the +# applet), except "sudo -u USER -- CMD...", which runs CMD (as the same user: an AUR helper) # shellcheck source=../fakelib.bash . "${BASH_SOURCE[0]%/*}/../fakelib.bash" fake_log "$@" +if [[ ${1:-} == -u && ${3:-} == -- ]]; then + shift 3 + exec "$@" +fi exit 0 diff --git a/tests/unit/fakes/yay/yay b/tests/unit/fakes/yay/yay new file mode 100755 index 0000000..54c4777 --- /dev/null +++ b/tests/unit/fakes/yay/yay @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (C) 2026 Bryan Joshua Pedini + +# fake yay (unit tests): the fake paru, replaying yay's recorded output instead +FAKE_AS=yay exec "${BASH_SOURCE[0]%/*}/../paru/paru" "$@" diff --git a/tests/unit/interactive.bats b/tests/unit/interactive.bats index f2a90ab..d614841 100644 --- a/tests/unit/interactive.bats +++ b/tests/unit/interactive.bats @@ -16,7 +16,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_success assert_line --index 0 'Backend: apt (Debian GNU/Linux 12 (bookworm))' assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' - assert_line ' [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo' assert_line ' upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever' assert_line '> q' assert_line 'Quit: nothing was upgraded.' @@ -81,7 +81,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc @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_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' \ @@ -114,8 +114,8 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_success assert_line 'excluded from this run: su-dummy-b' assert_line 're-included: su-dummy-b' - assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' - assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' assert_line 'Excluded from this run: su-dummy-b' log_has 'apt-mark hold su-dummy-b' log_has 'apt-mark unhold su-dummy-b' @@ -129,8 +129,8 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc log_has 'apt-get -y purge su-dummy-c' log_lacks 'autoremove --purge' assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:' - assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' - [[ $(grep -c '^ \[3\] su-dummy-c' <<<"$output") -eq 1 ]] + assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + [[ $(grep -c '^ \[3\] su-dummy-c' <<<"$output") -eq 1 ]] assert_output --partial 'no package [3] in the list (l to redraw)' } @@ -148,7 +148,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc FAKE_APT_PURGE_RC=100 run skup --no-refresh <<<$'3x\nyes\nl\nq' assert_success assert_output --partial 'purge of su-dummy-c failed (apt exited 100); the row stays in the list' - assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' } @test "Nx: EOF at the confirmation aborts with exit 2 and purges nothing" { @@ -163,7 +163,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first' refute_output --partial '[y/N]' assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' - [[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 2 ]] + [[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 2 ]] log_lacks '^apt-get -y' } @@ -173,7 +173,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_line 'Purge su-dummy-a now? This also removes: su-dummy-b [y/N] y' assert_line 'purged: su-dummy-a su-dummy-b' assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:' - assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' } @test "Nx: rows the purge took with it unannounced leave the list too (re-listed afterwards)" { @@ -181,7 +181,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_success assert_line ' (su-dummy-a is no longer upgradable, removed from the list)' assert_line '1 upgradable, 0 trusted (hidden, use --all to show), 1 to review:' - assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' } @test "Nx on a new dependency (old version -, not installed) is refused" { @@ -224,10 +224,10 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_success assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)' assert_line '3 upgradable, 2 trusted (hidden, use --all to show), 1 to review:' - assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [3] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' # [1] and [2] only appear in the first listing - [[ $(grep -c '^ \[1\] su-dummy-a' <<<"$output") -eq 1 ]] - [[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 1 ]] + [[ $(grep -c '^ \[1\] su-dummy-a' <<<"$output") -eq 1 ]] + [[ $(grep -c '^ \[2\] su-dummy-b' <<<"$output") -eq 1 ]] run cut -f1-3 "$STATE_FILE" assert_line "apt${T}su-dummy-a${T}user" assert_line "apt${T}su-dummy-b${T}dep:su-dummy-a" @@ -254,8 +254,8 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc run skup --no-refresh <<<$'1?\nq' assert_success assert_line '3 upgradable, 1 trusted (hidden, use --all to show), 2 to review:' - assert_line ' [1] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' - assert_line ' [2] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [1] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [2] su-dummy-c 1.0 -> 2.0 su-fixture:su-repo' assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests' } @@ -264,11 +264,11 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc run skup --no-refresh --all <<<$'2+\nl\nq' assert_success assert_line '3 upgradable, 1 trusted (marked with =), 2 to review:' - assert_line '=[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo' - assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line '=[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' assert_line 'trusted su-dummy-b' assert_line '3 upgradable, 2 trusted (marked with =), 1 to review:' - assert_line '=[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line '=[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' } @test "trusted packages are upgraded silently (never excluded)" { @@ -305,7 +305,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_success assert_line 'excluded all 3 listed packages from this run' assert_line 're-included all 3 listed packages' - assert_line '-[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo' + assert_line '-[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo' assert_line 'Excluded from this run: su-dummy-a su-dummy-b su-dummy-c' log_has 'apt-mark hold su-dummy-a su-dummy-b su-dummy-c' } @@ -316,7 +316,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests' assert_line 'excluded from this run: su-dummy-b' assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests' - assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line '-[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' } @test "an unknown token prints an error, runs nothing from that line and re-prompts" { @@ -324,7 +324,7 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc assert_success assert_output --partial "unknown command 'bogus' (h for help)" assert_output --partial "unknown command '3y' (h for help)" - assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' + assert_line ' [2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo' refute_output --partial 'excluded from this run' [[ ! -e $STATE_FILE ]] } @@ -369,6 +369,6 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc echo su-dummy-c >"$FAKE_DIR/holds" FAKE_APT_SIM=held run skup --no-refresh <<<$'3?\nq' assert_success - assert_line ' [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]' + assert_line ' [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]' assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests' } diff --git a/tests/unit/parsers.bats b/tests/unit/parsers.bats index 926b27b..dd1f0e0 100644 --- a/tests/unit/parsers.bats +++ b/tests/unit/parsers.bats @@ -18,16 +18,16 @@ setup() { common_setup; } FAKE_APT_SIM=real run skup list --no-refresh assert_success assert_line '10 upgradable, 0 trusted (hidden, use --all to show), 10 to review:' - assert_line --regexp '^ \[1\] libaudit-common +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$' - assert_line --regexp '^ \[2\] libaudit1 +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$' - assert_line --regexp '^ \[3\] libc6:i386 +2\.36-9 +-> +2\.36-9\+deb12u10 +Debian-Security:12/oldstable-security, Debian:12\.12/oldstable$' - assert_line --regexp '^ \[4\] libssl3t64 +3\.0\.13-0ubuntu3\.15 +-> +3\.0\.13-0ubuntu3\.16 +Ubuntu:24\.04/noble-updates, Ubuntu:24\.04/noble-security$' - assert_line --regexp '^ \[5\] perl-base +5\.36\.0-7\+deb12u3 +-> +5\.36\.0-7\+deb12u4 +http://deb\.debian\.org/debian-security bookworm-security/main amd64 \[held\]$' - assert_line --regexp '^ \[6\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$' - assert_line --regexp '^ \[7\] su-dummy-e +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$' - assert_line --regexp '^ \[8\] su-dummy-m +1\.0 +-> +2\.0 +su-fixture:su-repo$' - assert_line --regexp '^ \[9\] su-dummy-newdep +- +-> +1\.0 +su-fixture:su-repo$' - assert_line --regexp '^ \[10\] su-dummy-slow +0\.9 +-> +1\.0$' + assert_line --regexp '^ \[1\] libaudit-common +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$' + assert_line --regexp '^ \[2\] libaudit1 +1:3\.1\.2-2\.1build1\.1 +-> +1:3\.1\.2-2\.1ubuntu0\.1 +Ubuntu:24\.04/noble-updates$' + assert_line --regexp '^ \[3\] libc6:i386 +2\.36-9 +-> +2\.36-9\+deb12u10 +Debian-Security:12/oldstable-security, Debian:12\.12/oldstable$' + assert_line --regexp '^ \[4\] libssl3t64 +3\.0\.13-0ubuntu3\.15 +-> +3\.0\.13-0ubuntu3\.16 +Ubuntu:24\.04/noble-updates, Ubuntu:24\.04/noble-security$' + assert_line --regexp '^ \[5\] perl-base +5\.36\.0-7\+deb12u3 +-> +5\.36\.0-7\+deb12u4 +http://deb\.debian\.org/debian-security bookworm-security/main amd64 \[held\]$' + assert_line --regexp '^ \[6\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$' + assert_line --regexp '^ \[7\] su-dummy-e +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$' + assert_line --regexp '^ \[8\] su-dummy-m +1\.0 +-> +2\.0 +su-fixture:su-repo$' + assert_line --regexp '^ \[9\] su-dummy-newdep +- +-> +1\.0 +su-fixture:su-repo$' + assert_line --regexp '^ \[10\] su-dummy-slow +0\.9 +-> +1\.0$' refute_output --partial 'su-dummy-f' log_has 'apt-get -s --with-new-pkgs upgrade' log_has 'apt-cache policy perl-base su-dummy-c su-dummy-e' @@ -36,8 +36,8 @@ setup() { common_setup; } @test "apt: a held package from the fixture shows up with the held flag" { FAKE_APT_SIM=held run skup list --no-refresh assert_success - assert_line --regexp '^ \[1\] su-dummy-a +1\.0 +-> +2\.0 +su-fixture:su-repo$' - assert_line --regexp '^ \[3\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$' + assert_line --regexp '^ \[1\] su-dummy-a +1\.0 +-> +2\.0 +su-fixture:su-repo$' + assert_line --regexp '^ \[3\] su-dummy-c +1\.0 +-> +2\.0 +file:/srv/su-repo \./ \[held\]$' } @test "apt: nothing to upgrade" { @@ -182,10 +182,10 @@ EOF FAKE_PACMAN_QU=real run skup list --no-refresh assert_success assert_line '6 upgradable, 0 trusted (hidden, use --all to show), 6 to review:' - assert_line ' [1] ca-certificates-mozilla 3.130-1 -> 3.131-1 core' - assert_line --regexp '^ \[2\] expat +2\.8\.5-1 +-> +2\.9\.0-1 +core \[held\]$' - assert_line --regexp '^ \[3\] glib2 +2\.88\.3-1 +-> +2\.90\.1-1 +core \[held\]$' - assert_line --regexp '^ \[5\] su-dummy-b +1\.0-1 +-> +2\.0-1 +su-dummy$' + assert_line ' [1] ca-certificates-mozilla 3.130-1 -> 3.131-1 core' + assert_line --regexp '^ \[2\] expat +2\.8\.5-1 +-> +2\.9\.0-1 +core \[held\]$' + assert_line --regexp '^ \[3\] glib2 +2\.88\.3-1 +-> +2\.90\.1-1 +core \[held\]$' + assert_line --regexp '^ \[5\] su-dummy-b +1\.0-1 +-> +2\.0-1 +su-dummy$' refute_output --partial 'su-dummy2' } @@ -194,7 +194,7 @@ EOF FAKE_PACMAN_SL=empty run skup list --no-refresh assert_success assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' - assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1' + assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1' } @test "pacman: packages only the transaction (-Sup) shows become a note" { @@ -361,14 +361,14 @@ EOF run skup list --no-refresh assert_success assert_line '6 upgradable, 0 trusted (hidden, use --all to show), 6 to review:' - assert_line ' [1] gnutls 3.8.10-4.el9_8 -> 3.8.10-9.el9_8 baseos' + assert_line ' [1] gnutls 3.8.10-4.el9_8 -> 3.8.10-9.el9_8 baseos' # installonly: the highest installed version is the old one - assert_line --regexp '^ \[2\] kernel-core +5\.14\.0-611\.5\.el9 +-> +5\.14\.0-612\.1\.el9 +baseos$' - assert_line --regexp '^ \[3\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$' - assert_line --regexp '^ \[5\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$' + assert_line --regexp '^ \[2\] kernel-core +5\.14\.0-611\.5\.el9 +-> +5\.14\.0-612\.1\.el9 +baseos$' + assert_line --regexp '^ \[3\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$' + assert_line --regexp '^ \[5\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$' # epoch kept on both sides - assert_line --regexp '^ \[6\] vim-minimal +2:8\.2\.2637-26\.el9_8\.21 +-> +2:8\.2\.2637-26\.el9_8\.23 +baseos$' - refute_line --regexp '\] su-dummy-new' + assert_line --regexp '^ \[6\] vim-minimal +2:8\.2\.2637-26\.el9_8\.21 +-> +2:8\.2\.2637-26\.el9_8\.23 +baseos$' + refute_line --regexp '\] +su-dummy-new' refute_output --partial 'su-dummy-old' refute_output --partial 'Obsoleting' assert_line 'Note: the upgrade also installs (obsoleting installed packages): su-dummy-new' @@ -390,8 +390,8 @@ EOF FAKE_DNF_MAJOR=5 run skup list --no-refresh assert_success assert_line '5 upgradable, 0 trusted (hidden, use --all to show), 5 to review:' - assert_line --regexp '^ \[1\] ca-certificates +2025\.2\.80_v9\.0\.305-91\.el9 +-> +2026\.2\.90_v9\.0\.317-1\.fc44 +updates$' - assert_line --regexp '^ \[2\] curl +8\.18\.0-1\.fc44 +-> +8\.18\.0-12\.fc44 +updates$' + assert_line --regexp '^ \[1\] ca-certificates +2025\.2\.80_v9\.0\.305-91\.el9 +-> +2026\.2\.90_v9\.0\.317-1\.fc44 +updates$' + assert_line --regexp '^ \[2\] curl +8\.18\.0-1\.fc44 +-> +8\.18\.0-12\.fc44 +updates$' refute_output --partial 'Upgrades' } @@ -600,10 +600,10 @@ EOF FAKE_ZYPPER_LU=real run skup list --no-refresh assert_success assert_line '4 upgradable, 0 trusted (hidden, use --all to show), 4 to review:' - assert_line ' [1] openSUSE-build-key 1.0-68.1 -> 1.0-69.1 repo-update' - assert_line --regexp '^ \[2\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$' - assert_line --regexp '^ \[3\] su-dummy-b +1\.0-1 +-> +2\.0-1 +openSUSE:repo-oss & co$' - assert_line --regexp '^ \[4\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$' + assert_line ' [1] openSUSE-build-key 1.0-68.1 -> 1.0-69.1 repo-update' + assert_line --regexp '^ \[2\] su-dummy-a +1\.0-1 +-> +2\.0-1 +su-dummy$' + assert_line --regexp '^ \[3\] su-dummy-b +1\.0-1 +-> +2\.0-1 +openSUSE:repo-oss & co$' + assert_line --regexp '^ \[4\] su-dummy-c +1\.0-1 +-> +2\.0-1 +su-dummy$' log_has 'zypper --non-interactive --xmlout list-updates' } @@ -615,7 +615,7 @@ EOF log_has 'zypper --non-interactive --no-refresh --xmlout dist-upgrade --dry-run' log_lacks 'list-updates' assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' - assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' + assert_line ' [1] su-dummy-a 1.0-1 -> 2.0-1 su-dummy' refute_output --partial 'openSUSE-release' } @@ -624,7 +624,7 @@ EOF export SKEPTICAL_UPDATER_OS_RELEASE=$FAKE_DATA/os-release-tumbleweed FAKE_ZYPPER_DUP=removes run skup list --no-refresh assert_success - assert_line ' [2] su-dummy-newdep - -> 1.0-1 su-dummy' + assert_line ' [2] su-dummy-newdep - -> 1.0-1 su-dummy' assert_line 'Note: the dist-upgrade also removes: libcurl-mini4' } @@ -734,7 +734,7 @@ EOF 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 ' [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' diff --git a/tests/unit/statefile.bats b/tests/unit/statefile.bats index 5e0656f..d86ea33 100644 --- a/tests/unit/statefile.bats +++ b/tests/unit/statefile.bats @@ -249,7 +249,7 @@ setup() { common_setup; } write_state "$HEADER" "pacman${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z" run skup list --no-refresh assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:' - assert_line --regexp '^ \[1\] su-dummy-a ' + assert_line --regexp '^ \[1\] su-dummy-a ' } @test "the reason does not matter for hiding: dep rows hide too" { diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index 8d01203..210443b 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -36,7 +36,7 @@ common_setup() { export TMPDIR=$BATS_TEST_TMPDIR export FAKE_PACMAN_DBPATH=$FAKE_DIR/pacman-db mkdir -p "$FAKE_PACMAN_DBPATH/sync" "$FAKE_PACMAN_DBPATH/local" - unset NO_COLOR ZYPP_CONF COLUMNS SKEPTICAL_UPDATER_LOCK_WAIT + unset NO_COLOR ZYPP_CONF COLUMNS SKEPTICAL_UPDATER_LOCK_WAIT SUDO_USER use_fakes apt }