first commit
This commit is contained in:
commit
1354db6c39
90 files changed
+13123
No files matched your search
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/e2e/entrypoint.sh - runs INSIDE one distro container (started by tests/docker/run-e2e.sh,
|
||||
# as root, repository mounted read-only at /work, SU_DISTRO=<label>):
|
||||
# 1. installs the applet as /usr/local/bin/skeptical-updater
|
||||
# 2. builds the deterministic fixture for the package-manager family (su-dummy-{a,b,c}
|
||||
# 1.0 installed, 2.0 offered by a local repo; the distro's own repos are disabled;
|
||||
# su-dummy-orphan: an unrelated, automatically installed package nothing needs)
|
||||
# 3. runs tests/e2e/*.bats with the vendored bats-core against the REAL package manager
|
||||
# Exit status: the bats status (or 1 if the setup failed).
|
||||
set -euo pipefail
|
||||
export LC_ALL=C DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." # repository root (read-only)
|
||||
log() { printf '== entrypoint: %s\n' "$*"; }
|
||||
die() {
|
||||
printf '== entrypoint: ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $(id -u) -eq 0 ]] || die "must run as root"
|
||||
t0=$SECONDS
|
||||
|
||||
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
|
||||
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
|
||||
elif command -v zypper >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then
|
||||
family=rpm
|
||||
SU_FIXTURE_ISOLATE=1 SU_FIXTURE_ORPHAN=1 SU_FIXTURE_B_AUTO=1 bash tests/e2e/fixtures/rpm.sh
|
||||
else
|
||||
die "no supported package manager in this image"
|
||||
fi
|
||||
export SU_FAMILY=$family
|
||||
# shellcheck source=/dev/null
|
||||
log "${SU_DISTRO:-?}: family=$family, fixture ready after $((SECONDS - t0))s ($(. /etc/os-release && printf '%s' "$PRETTY_NAME"), bash $BASH_VERSION)"
|
||||
|
||||
TMPDIR=$(mktemp -d /tmp/su-e2e.XXXXXX)
|
||||
export TMPDIR
|
||||
|
||||
# bats needs find(1) to expand a directory argument; openSUSE base images may lack it
|
||||
if command -v find >/dev/null 2>&1; then
|
||||
targets=(tests/e2e)
|
||||
else
|
||||
targets=(tests/e2e/*.bats)
|
||||
fi
|
||||
status=0
|
||||
bash tests/vendor/bats-core/bin/bats --tap "${targets[@]}" || status=$?
|
||||
log "${SU_DISTRO:-?}: bats exited $status after $((SECONDS - t0))s"
|
||||
exit "$status"
|
||||
Executable
+251
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# apt.sh - APT e2e fixture for skeptical-updater (Debian/Ubuntu containers).
|
||||
# Copy of discovery/apt/fixture.sh; the only changes are the SU_FIXTURE_STRICT_DEP knob
|
||||
# (su-dummy-a 2.0 depends on plain su-dummy-b by default, like the pacman and rpm
|
||||
# fixtures, so excluding su-dummy-b does not keep su-dummy-a back), SU_FIXTURE_ORPHAN and
|
||||
# SU_FIXTURE_B_AUTO.
|
||||
#
|
||||
# Builds a local *flat* APT repository at /srv/su-repo containing dummy packages
|
||||
# and installs the old (1.0) versions, so that after `apt-get update` the package
|
||||
# manager reports deterministic upgrades:
|
||||
#
|
||||
# su-dummy-a 1.0 -> 2.0 (Depends: su-dummy-b, ships a conffile;
|
||||
# SU_FIXTURE_STRICT_DEP=1: 2.0 Depends: su-dummy-b (>= 2.0))
|
||||
# su-dummy-b 1.0 -> 2.0 (dependency of su-dummy-a)
|
||||
# su-dummy-c 1.0 -> 2.0 (standalone)
|
||||
#
|
||||
# Optional extras (SU_FIXTURE_EXTRA=1) for "kept back" / removal / virtual-package
|
||||
# scenarios:
|
||||
# su-dummy-d 1.0 -> 2.0 2.0 adds a NEW dependency (su-dummy-newdep, not installed)
|
||||
# => plain `upgrade` keeps it back, `--with-new-pkgs upgrade`
|
||||
# and `dist-upgrade` install it
|
||||
# su-dummy-e 1.0 -> 2.0 2.0 Conflicts: su-dummy-f (installed, no 2.0 exists)
|
||||
# => only `dist-upgrade` can do it (removes su-dummy-f) - except with apt 3.x
|
||||
# solver3 (ubuntu:latest = 26.04) which refuses to remove the manually
|
||||
# installed su-dummy-f and keeps su-dummy-e back even for dist-upgrade
|
||||
# su-dummy-f 1.0 installed, never upgradable
|
||||
# su-dummy-w 1.0 -> 2.0 Depends: su-virtual | su-dummy-alt (virtual + alternative)
|
||||
# su-dummy-v 1.0 Provides: su-virtual
|
||||
# (+ su-dummy-newdep 1.0 and su-dummy-alt 1.0: only in the repo, never pre-installed)
|
||||
#
|
||||
# Environment knobs (all optional):
|
||||
# SU_REPO_DIR=/srv/su-repo where the repo is created (wiped first; must match /srv/su-*, /tmp/su-*, /var/tmp/su-*)
|
||||
# SU_OLD_DIR=/srv/su-old where the 1.0 .debs are kept (NOT in the repo)
|
||||
# SU_FIXTURE_INDEXER=shell shell | scanpackages | ftparchive
|
||||
# shell = no extra tools needed (dpkg-deb -f + sha256sum)
|
||||
# scanpackages = dpkg-scanpackages (installs dpkg-dev if missing)
|
||||
# ftparchive = apt-ftparchive (installs apt-utils if missing)
|
||||
# SU_FIXTURE_OFFLINE=1 disable the distro's real sources (hermetic: no network at all)
|
||||
# SU_FIXTURE_EXTRA=1 add the extra scenario packages listed above
|
||||
# SU_FIXTURE_BAD=1 add su-dummy-bad: 1.0 installed, 2.0 has a postinst that fails
|
||||
# (apt-get upgrade then exits 100) - for error-path tests
|
||||
# SU_FIXTURE_STRICT_DEP=1 su-dummy-a 2.0 depends on "su-dummy-b (>= 2.0)" (as in discovery/apt):
|
||||
# holding su-dummy-b then also keeps su-dummy-a back
|
||||
# SU_FIXTURE_ORPHAN=1 also install su-dummy-orphan 1.0 marked automatically installed with
|
||||
# nothing depending on it (a pre-existing autoremove candidate)
|
||||
# SU_FIXTURE_B_AUTO=1 mark su-dummy-b automatically installed (a dependency of su-dummy-a,
|
||||
# like the pacman fixture's --asdeps)
|
||||
#
|
||||
# Must run as root inside a throwaway container, e.g.:
|
||||
# docker run --rm -i -v "$PWD/discovery/apt:/d:ro" debian:12 bash /d/fixture.sh
|
||||
set -euo pipefail
|
||||
export LC_ALL=C DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
REPO_DIR=${SU_REPO_DIR:-/srv/su-repo}
|
||||
OLD_DIR=${SU_OLD_DIR:-/srv/su-old}
|
||||
INDEXER=${SU_FIXTURE_INDEXER:-shell}
|
||||
OFFLINE=${SU_FIXTURE_OFFLINE:-0}
|
||||
EXTRA=${SU_FIXTURE_EXTRA:-0}
|
||||
BAD=${SU_FIXTURE_BAD:-0}
|
||||
STRICT_DEP=${SU_FIXTURE_STRICT_DEP:-0}
|
||||
ORPHAN=${SU_FIXTURE_ORPHAN:-0}
|
||||
B_AUTO=${SU_FIXTURE_B_AUTO:-0}
|
||||
SOURCES_FILE=/etc/apt/sources.list.d/su-repo.list
|
||||
T0=$(date +%s)
|
||||
|
||||
log() { printf '[fixture] %s\n' "$*"; }
|
||||
die() { printf '[fixture] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
command -v apt-get >/dev/null || die "apt-get not found (not a Debian/Ubuntu image?)"
|
||||
command -v dpkg-deb >/dev/null || die "dpkg-deb not found"
|
||||
case $INDEXER in shell|scanpackages|ftparchive) ;; *) die "bad SU_FIXTURE_INDEXER=$INDEXER" ;; esac
|
||||
# the directories are wiped with rm -rf: only accept obviously fixture-owned locations
|
||||
for d in "$REPO_DIR" "$OLD_DIR"; do
|
||||
case $d in /srv/su-*|/tmp/su-*|/var/tmp/su-*) ;; *) die "refusing to wipe '$d' (must match /srv/su-*, /tmp/su-* or /var/tmp/su-*)" ;; esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------- build debs
|
||||
# mkdeb NAME VERSION OUTDIR [extra control line]...
|
||||
# If $MKDEB_POSTINST is set it becomes the body of the package's postinst script.
|
||||
mkdeb() {
|
||||
local name=$1 ver=$2 out=$3 root line
|
||||
shift 3
|
||||
root=$(mktemp -d)
|
||||
mkdir -p "$root/DEBIAN" "$root/usr/share/su-dummy" "$root/etc/su-dummy"
|
||||
{
|
||||
printf 'Package: %s\nVersion: %s\nSection: misc\nPriority: optional\n' "$name" "$ver"
|
||||
printf 'Architecture: all\nMaintainer: Skeptical Updater Fixture <su-fixture@example.invalid>\n'
|
||||
for line in "$@"; do printf '%s\n' "$line"; done
|
||||
printf 'Description: dummy package %s for skeptical-updater tests\n' "$name"
|
||||
printf ' This package only exists to exercise the package manager.\n'
|
||||
printf ' .\n'
|
||||
printf ' It ships one data file and one conffile (version %s).\n' "$ver"
|
||||
} > "$root/DEBIAN/control"
|
||||
echo "$ver" > "$root/usr/share/su-dummy/$name"
|
||||
echo "version=$ver" > "$root/etc/su-dummy/$name.conf"
|
||||
echo "/etc/su-dummy/$name.conf" > "$root/DEBIAN/conffiles"
|
||||
if [ -n "${MKDEB_POSTINST:-}" ]; then
|
||||
printf '#!/bin/sh\n%s\n' "$MKDEB_POSTINST" > "$root/DEBIAN/postinst"
|
||||
chmod 755 "$root/DEBIAN/postinst"
|
||||
fi
|
||||
mkdir -p "$out"
|
||||
# -Zgzip: readable by every dpkg in the supported images; --root-owner-group: files owned by root
|
||||
dpkg-deb --root-owner-group -Zgzip --build "$root" "$out/${name}_${ver}_all.deb" >/dev/null
|
||||
rm -rf "$root"
|
||||
}
|
||||
|
||||
rm -rf "$REPO_DIR" "$OLD_DIR"
|
||||
mkdir -p "$REPO_DIR" "$OLD_DIR"
|
||||
|
||||
# old versions (installed straight away with dpkg, NOT published in the repo)
|
||||
mkdeb su-dummy-a 1.0 "$OLD_DIR" 'Depends: su-dummy-b'
|
||||
mkdeb su-dummy-b 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-c 1.0 "$OLD_DIR"
|
||||
# new versions (published in the repo)
|
||||
if [ "$STRICT_DEP" = 1 ]; then
|
||||
mkdeb su-dummy-a 2.0 "$REPO_DIR" 'Depends: su-dummy-b (>= 2.0)'
|
||||
else
|
||||
mkdeb su-dummy-a 2.0 "$REPO_DIR" 'Depends: su-dummy-b'
|
||||
fi
|
||||
mkdeb su-dummy-b 2.0 "$REPO_DIR"
|
||||
mkdeb su-dummy-c 2.0 "$REPO_DIR"
|
||||
|
||||
if [ "$ORPHAN" = 1 ]; then
|
||||
mkdeb su-dummy-orphan 1.0 "$OLD_DIR" # installed, never upgradable
|
||||
fi
|
||||
|
||||
if [ "$EXTRA" = 1 ]; then
|
||||
mkdeb su-dummy-d 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-d 2.0 "$REPO_DIR" 'Depends: su-dummy-newdep'
|
||||
mkdeb su-dummy-newdep 1.0 "$REPO_DIR" # only in the repo, never pre-installed
|
||||
mkdeb su-dummy-e 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-e 2.0 "$REPO_DIR" 'Conflicts: su-dummy-f'
|
||||
mkdeb su-dummy-f 1.0 "$OLD_DIR"
|
||||
mkdeb su-dummy-v 1.0 "$OLD_DIR" 'Provides: su-virtual'
|
||||
mkdeb su-dummy-alt 1.0 "$REPO_DIR" # alternative target, not installed
|
||||
mkdeb su-dummy-w 1.0 "$OLD_DIR" 'Depends: su-virtual | su-dummy-alt'
|
||||
mkdeb su-dummy-w 2.0 "$REPO_DIR" 'Depends: su-virtual | su-dummy-alt'
|
||||
fi
|
||||
|
||||
if [ "$BAD" = 1 ]; then
|
||||
mkdeb su-dummy-bad 1.0 "$OLD_DIR"
|
||||
# shellcheck disable=SC2016 # the postinst body is expanded by the package's /bin/sh, not here
|
||||
MKDEB_POSTINST='[ "$1" = configure ] && { echo "su-dummy-bad: postinst failing on purpose" >&2; exit 1; }; exit 0' \
|
||||
mkdeb su-dummy-bad 2.0 "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- real sources
|
||||
if [ "$OFFLINE" = 1 ]; then
|
||||
mkdir -p /srv/su-disabled-sources
|
||||
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources /etc/apt/sources.list.d/*.list; do
|
||||
[ -e "$f" ] || continue
|
||||
case $f in "$SOURCES_FILE") continue ;; esac
|
||||
mv "$f" "/srv/su-disabled-sources/$(basename "$f")"
|
||||
done
|
||||
log "OFFLINE: real sources moved to /srv/su-disabled-sources"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- indexing
|
||||
install_tool() { # install_tool PACKAGE : needs the real mirrors
|
||||
[ "$OFFLINE" = 1 ] && die "SU_FIXTURE_INDEXER=$INDEXER needs network to install $1; use shell with OFFLINE"
|
||||
log "installing $1 (needs network)"
|
||||
apt-get update -qq >/dev/null
|
||||
apt-get install -y -qq --no-install-recommends "$1" >/dev/null
|
||||
}
|
||||
|
||||
index_shell() {
|
||||
local f
|
||||
: > Packages
|
||||
for f in ./*.deb; do
|
||||
f=${f#./}
|
||||
dpkg-deb -f "$f" >> Packages
|
||||
printf 'Filename: ./%s\nSize: %s\nMD5sum: %s\nSHA256: %s\n\n' \
|
||||
"$f" "$(stat -c %s "$f")" "$(md5sum < "$f" | cut -d' ' -f1)" "$(sha256sum < "$f" | cut -d' ' -f1)" >> Packages
|
||||
done
|
||||
{
|
||||
printf 'Origin: su-fixture\nLabel: su-fixture\nSuite: su-repo\nCodename: su-repo\n'
|
||||
printf 'Date: %s\n' "$(date -u '+%a, %d %b %Y %H:%M:%S UTC')"
|
||||
printf 'SHA256:\n %s %s Packages\n' "$(sha256sum < Packages | cut -d' ' -f1)" "$(stat -c %s Packages)"
|
||||
} > Release
|
||||
}
|
||||
|
||||
index_scanpackages() {
|
||||
command -v dpkg-scanpackages >/dev/null || install_tool dpkg-dev
|
||||
dpkg-scanpackages --multiversion . /dev/null > Packages 2>/dev/null
|
||||
# dpkg-scanpackages does not write a Release file; make a minimal one
|
||||
{
|
||||
printf 'Origin: su-fixture\nLabel: su-fixture\nSuite: su-repo\nCodename: su-repo\n'
|
||||
printf 'Date: %s\n' "$(date -u '+%a, %d %b %Y %H:%M:%S UTC')"
|
||||
printf 'SHA256:\n %s %s Packages\n' "$(sha256sum < Packages | cut -d' ' -f1)" "$(stat -c %s Packages)"
|
||||
} > Release
|
||||
}
|
||||
|
||||
index_ftparchive() {
|
||||
command -v apt-ftparchive >/dev/null || install_tool apt-utils
|
||||
apt-ftparchive packages . > Packages
|
||||
apt-ftparchive \
|
||||
-o APT::FTPArchive::Release::Origin=su-fixture \
|
||||
-o APT::FTPArchive::Release::Label=su-fixture \
|
||||
-o APT::FTPArchive::Release::Suite=su-repo \
|
||||
-o APT::FTPArchive::Release::Codename=su-repo \
|
||||
release . > Release
|
||||
}
|
||||
|
||||
( cd "$REPO_DIR" && "index_$INDEXER" )
|
||||
log "repo $REPO_DIR indexed with '$INDEXER': $(grep -c '^Package:' "$REPO_DIR/Packages") packages"
|
||||
|
||||
echo "deb [trusted=yes] file:$REPO_DIR ./" > "$SOURCES_FILE"
|
||||
|
||||
# ---------------------------------------------------------------- install old versions
|
||||
# --force-confnew + </dev/null: never block on a conffile prompt if the fixture is re-run in a dirty container
|
||||
dpkg --force-confnew -i "$OLD_DIR"/*.deb >/dev/null </dev/null
|
||||
if [ "$ORPHAN" = 1 ]; then
|
||||
apt-mark auto su-dummy-orphan >/dev/null
|
||||
fi
|
||||
if [ "$B_AUTO" = 1 ]; then
|
||||
apt-mark auto su-dummy-b >/dev/null
|
||||
fi
|
||||
log "installed (old): $(dpkg-query -W -f '${db:Status-Abbrev} ${Package}=${Version}\n' 'su-dummy-*' 2>/dev/null | awk '$1 == "ii" {printf "%s ", $2}')"
|
||||
|
||||
# ---------------------------------------------------------------- refresh + verify
|
||||
# drop stale lists of our own repo first: apt compares mtimes of file: indexes with 1s resolution,
|
||||
# so a repo regenerated within the same second would otherwise be seen as "unchanged"
|
||||
rm -f /var/lib/apt/lists/"$(printf '%s' "$REPO_DIR" | tr / _)"_*
|
||||
apt-get update -qq >/dev/null
|
||||
|
||||
fail=0
|
||||
upg=$(apt list --upgradable 2>/dev/null | awk -F/ '/^su-dummy-/{print $1}' | sort | tr '\n' ' ')
|
||||
for p in su-dummy-a su-dummy-b su-dummy-c; do
|
||||
case " $upg" in *" $p "*) ;; *) log "MISSING from upgradable: $p"; fail=1 ;; esac
|
||||
done
|
||||
sim=$(apt-get -s --with-new-pkgs upgrade 2>/dev/null | awk '/^Inst su-dummy-(a|b|c) /{n++} END{print n+0}')
|
||||
[ "$sim" -ge 3 ] || { log "simulated upgrade only covers $sim of 3 core packages"; fail=1; }
|
||||
[ "$fail" -eq 0 ] || die "fixture verification failed"
|
||||
|
||||
log "upgradable dummy packages: $upg"
|
||||
if [ "$ORPHAN" = 1 ]; then
|
||||
apt-get -s autoremove 2>/dev/null | grep -q '^Remv su-dummy-orphan ' || die "su-dummy-orphan is not an autoremove candidate"
|
||||
log "pre-existing orphan (autoremove candidate): su-dummy-orphan"
|
||||
fi
|
||||
real=$(apt list --upgradable 2>/dev/null | awk -F/ 'NR>1 && $1 !~ /^su-dummy-/{print $1}' | tr '\n' ' ')
|
||||
if [ -n "$real" ]; then
|
||||
log "NOTE: real-world upgradable packages present in this base image: $real"
|
||||
else
|
||||
log "NOTE: no real-world upgradable packages in this base image right now"
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
log "OK in $(( $(date +%s) - T0 ))s (image: $(. /etc/os-release; echo "$PRETTY_NAME"))"
|
||||
Executable
+222
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# 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.)
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# su-dummy-a depends=(su-dummy-b) installed explicitly
|
||||
# su-dummy-b installed as a dependency (--asdeps)
|
||||
# su-dummy-c installed explicitly
|
||||
#
|
||||
# It also pre-wires /etc/pacman.d/ignorepkg (empty) and the matching
|
||||
# "Include" line inside the [options] section of /etc/pacman.conf, i.e. the
|
||||
# same layout that pman expects (IgnorePkg = <pkg> lines).
|
||||
#
|
||||
# Run as root inside an Arch container:
|
||||
# docker run --rm -i -v "$PWD/discovery/pacman:/d:ro" archlinux:latest bash /d/fixture.sh
|
||||
#
|
||||
# Environment (all optional):
|
||||
# FIXTURE_BUILDER tar (default) | makepkg
|
||||
# tar : hand-built .pkg.tar.zst (.PKGINFO + bsdtar). No network, no
|
||||
# fakeroot, no build user. ~0.1 s.
|
||||
# makepkg : real makepkg run as the unprivileged user "su-builder" via
|
||||
# runuser. Needs fakeroot (installed with `pacman -Sy fakeroot`
|
||||
# if missing, so it needs network). Slower.
|
||||
# FIXTURE_ISOLATE 0 (default) | 1
|
||||
# 1 comments out every repo section except [su-dummy], so `pacman -Qu`
|
||||
# reports EXACTLY the three dummy packages (no real upgrades, no network
|
||||
# needed after the fixture is built).
|
||||
# FIXTURE_STRICT_DEP 0 (default) | 1
|
||||
# 1 makes su-dummy-a 2.0 depend on "su-dummy-b>=2.0" (instead of plain
|
||||
# "su-dummy-b"), so excluding su-dummy-b from an upgrade (--ignore /
|
||||
# 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_REPO repo directory (default /srv/su-repo)
|
||||
# FIXTURE_SYNC 0 (default) | 1 -> run `pacman -Sy` at the end
|
||||
#
|
||||
# The fixture deliberately does NOT sync by default: the tool under test is
|
||||
# expected to perform the refresh itself (pacman -Sy), after which
|
||||
# `pacman -Qu` lists su-dummy-{a,b,c} (1.0-1 -> 2.0-1).
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
FIXTURE_BUILDER=${FIXTURE_BUILDER:-tar}
|
||||
FIXTURE_ISOLATE=${FIXTURE_ISOLATE:-0}
|
||||
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_REPO_NAME=su-dummy
|
||||
FIXTURE_EPOCH=1700000000 # fixed build date => reproducible tarballs
|
||||
|
||||
fx_log() { printf 'fixture: %s\n' "$*" >&2; }
|
||||
fx_die() { fx_log "ERROR: $*"; exit 1; }
|
||||
|
||||
# pacman wrapper: stdout dropped, the harmless "database file for X does not
|
||||
# exist" warnings (fresh image, no sync yet) filtered, real errors kept, exit
|
||||
# status preserved.
|
||||
fx_pacman() {
|
||||
local rc=0
|
||||
pacman "$@" 2>&1 >/dev/null | { grep -v "^warning: database file for" >&2 || true; } || rc=$?
|
||||
return $rc
|
||||
}
|
||||
|
||||
# fx_build_tar <outdir> <name> <pkgver> <pkgdesc> [depend...]
|
||||
# Hand-built package: only a .PKGINFO and the payload are required by pacman -U
|
||||
# and repo-add (no .MTREE / .BUILDINFO needed; `pacman -Qk` is happy too).
|
||||
fx_build_tar() {
|
||||
local out=$1 name=$2 ver=$3 desc=$4; shift 4
|
||||
local w payload
|
||||
w=$(mktemp -d)
|
||||
payload="usr/share/su-dummy/$name"
|
||||
mkdir -p "$w/$payload"
|
||||
printf '%s\n' "$ver" >"$w/$payload/VERSION"
|
||||
{
|
||||
printf 'pkgname = %s\n' "$name"
|
||||
printf 'pkgbase = %s\n' "$name"
|
||||
printf 'pkgver = %s-1\n' "$ver"
|
||||
printf 'pkgdesc = %s\n' "$desc"
|
||||
printf 'url = https://example.invalid/su-dummy\n'
|
||||
printf 'builddate = %s\n' "$FIXTURE_EPOCH"
|
||||
printf 'packager = su-fixture <su-fixture@example.invalid>\n'
|
||||
printf 'size = %s\n' "$(wc -c <"$w/$payload/VERSION")"
|
||||
printf 'arch = any\n'
|
||||
printf 'license = custom\n'
|
||||
local d
|
||||
for d in "$@"; do printf 'depend = %s\n' "$d"; done
|
||||
} >"$w/.PKGINFO"
|
||||
(cd "$w" && bsdtar --zstd --uid 0 --gid 0 --uname root --gname root \
|
||||
-cf "$out/$name-$ver-1-any.pkg.tar.zst" .PKGINFO usr)
|
||||
rm -rf "$w"
|
||||
}
|
||||
|
||||
# fx_build_makepkg <outdir> <name> <pkgver> <pkgdesc> [depend...]
|
||||
# Real makepkg. makepkg refuses to run as root, so run it as an unprivileged
|
||||
# user with `runuser -u` (no sudo needed; `su -s /bin/bash -c` also works).
|
||||
fx_build_makepkg() {
|
||||
local out=$1 name=$2 ver=$3 desc=$4; shift 4
|
||||
local w depends="" d
|
||||
for d in "$@"; do depends+="'$d' "; done
|
||||
w=/home/su-builder/build/$name-$ver
|
||||
mkdir -p "$w"
|
||||
cat >"$w/PKGBUILD" <<EOF
|
||||
pkgname=$name
|
||||
pkgver=$ver
|
||||
pkgrel=1
|
||||
pkgdesc='$desc'
|
||||
arch=('any')
|
||||
url='https://example.invalid/su-dummy'
|
||||
license=('custom')
|
||||
options=('!debug' '!strip') # arch=any payload: no debugedit/binutils needed
|
||||
depends=($depends)
|
||||
package() {
|
||||
install -Dm644 /dev/stdin "\$pkgdir/usr/share/su-dummy/\$pkgname/VERSION" <<<"\$pkgver"
|
||||
}
|
||||
EOF
|
||||
chown -R su-builder: "$w"
|
||||
runuser -u su-builder -- env \
|
||||
PKGDEST="$out" SOURCE_DATE_EPOCH="$FIXTURE_EPOCH" \
|
||||
PACKAGER='su-fixture <su-fixture@example.invalid>' \
|
||||
bash -c "cd '$w' && makepkg --force --nodeps --noconfirm --nocolor >/dev/null 2>&1" \
|
||||
|| { runuser -u su-builder -- bash -c "cd '$w' && makepkg --force --nodeps --noconfirm --nocolor" >&2; fx_die "makepkg failed for $name"; }
|
||||
}
|
||||
|
||||
fx_prepare_makepkg() {
|
||||
if ! command -v fakeroot >/dev/null; then
|
||||
fx_log "makepkg builder: installing fakeroot (needs network)"
|
||||
fx_pacman -Sy --noconfirm --needed fakeroot
|
||||
fi
|
||||
id su-builder >/dev/null 2>&1 || useradd --create-home su-builder
|
||||
}
|
||||
|
||||
fx_build() { # fx_build <outdir> <name> <ver> <desc> [dep...]
|
||||
case $FIXTURE_BUILDER in
|
||||
tar) fx_build_tar "$@" ;;
|
||||
makepkg) fx_build_makepkg "$@" ;;
|
||||
*) fx_die "unknown FIXTURE_BUILDER=$FIXTURE_BUILDER" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
fx_main() {
|
||||
set -euo pipefail
|
||||
[ "$(id -u)" -eq 0 ] || fx_die "must run as root"
|
||||
local old new
|
||||
old=$(mktemp -d); new=$FIXTURE_REPO
|
||||
rm -rf "$FIXTURE_REPO"; mkdir -p "$FIXTURE_REPO"
|
||||
|
||||
chmod 755 "$old"
|
||||
if [ "$FIXTURE_BUILDER" = makepkg ]; then
|
||||
fx_prepare_makepkg
|
||||
chown su-builder: "$old" "$new" # PKGDEST dirs must be writable by the build user
|
||||
fi
|
||||
|
||||
# 1.0 packages (installed) and 2.0 packages (offered by the repo)
|
||||
fx_build "$old" su-dummy-a 1.0 'Skeptical-updater dummy package A' su-dummy-b
|
||||
fx_build "$old" su-dummy-b 1.0 'Skeptical-updater dummy package B (dependency of A)'
|
||||
fx_build "$old" su-dummy-c 1.0 'Skeptical-updater dummy package C (independent)'
|
||||
local a2_dep=su-dummy-b
|
||||
[ "$FIXTURE_STRICT_DEP" = 1 ] && a2_dep='su-dummy-b>=2.0'
|
||||
fx_build "$new" su-dummy-a 2.0 'Skeptical-updater dummy package A' "$a2_dep"
|
||||
fx_build "$new" su-dummy-b 2.0 'Skeptical-updater dummy package B (dependency of A)'
|
||||
fx_build "$new" su-dummy-c 2.0 'Skeptical-updater dummy package C (independent)'
|
||||
|
||||
# Local repository database (repo-add ships with pacman)
|
||||
(cd "$FIXTURE_REPO" && repo-add --quiet "$FIXTURE_REPO_NAME.db.tar.gz" ./*.pkg.tar.zst)
|
||||
chmod -R a+rX "$FIXTURE_REPO"; chmod a+rx /srv 2>/dev/null || true
|
||||
|
||||
# pacman.conf: register the repo (appended at the end => lowest priority)
|
||||
if ! grep -q "^\[$FIXTURE_REPO_NAME\]" /etc/pacman.conf; then
|
||||
{
|
||||
printf '\n[%s]\n' "$FIXTURE_REPO_NAME"
|
||||
printf 'SigLevel = Optional TrustAll\n'
|
||||
printf 'Server = file://%s\n' "$FIXTURE_REPO"
|
||||
} >>/etc/pacman.conf
|
||||
fi
|
||||
|
||||
# pman-style ignore file, included from the [options] section
|
||||
# (inserted right after the FIRST "[options]" header; appending at EOF would
|
||||
# land in whatever section happens to be last).
|
||||
: >/etc/pacman.d/ignorepkg
|
||||
if ! grep -q '^Include *= */etc/pacman.d/ignorepkg' /etc/pacman.conf; then
|
||||
sed -i '0,/^\[options\]/s||[options]\nInclude = /etc/pacman.d/ignorepkg|' /etc/pacman.conf
|
||||
fi
|
||||
|
||||
if [ "$FIXTURE_ISOLATE" = 1 ]; then
|
||||
# comment out all repos except [su-dummy]; keep [options] intact
|
||||
awk -v keep="$FIXTURE_REPO_NAME" '
|
||||
/^\[.*\][[:space:]]*$/ { sec=$0; sub(/^\[/,"",sec); sub(/\].*$/,"",sec); active = (sec=="options" || sec==keep) }
|
||||
{ if (sec != "" && !active && $0 !~ /^[[:space:]]*#/ && $0 !~ /^[[:space:]]*$/) print "#" $0; else print }
|
||||
' /etc/pacman.conf >/etc/pacman.conf.new && mv /etc/pacman.conf.new /etc/pacman.conf
|
||||
fi
|
||||
|
||||
# Install the 1.0 packages: b as a dependency, a and c explicit.
|
||||
fx_pacman -U --noconfirm "$old"/su-dummy-a-1.0-1-any.pkg.tar.zst \
|
||||
"$old"/su-dummy-b-1.0-1-any.pkg.tar.zst \
|
||||
"$old"/su-dummy-c-1.0-1-any.pkg.tar.zst
|
||||
fx_pacman -D --asdeps su-dummy-b
|
||||
if [ "$FIXTURE_ORPHAN" = 1 ]; then
|
||||
fx_build "$old" su-dummy-orphan 1.0 'Skeptical-updater dummy package ORPHAN (nothing requires it)'
|
||||
fx_pacman -U --noconfirm --asdeps "$old"/su-dummy-orphan-1.0-1-any.pkg.tar.zst
|
||||
pacman -Qdtq | grep -qx su-dummy-orphan || fx_die "su-dummy-orphan is not an orphan"
|
||||
fx_log "pre-existing orphan: su-dummy-orphan"
|
||||
fi
|
||||
rm -rf "$old"
|
||||
|
||||
if [ "$FIXTURE_SYNC" = 1 ]; then fx_pacman -Sy; 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' ';')"
|
||||
fx_log "repo list: $(pacman-conf --repo-list | tr '\n' ' ')"
|
||||
}
|
||||
|
||||
# Run when executed; do nothing when sourced (so tests can reuse fx_build_tar etc.
|
||||
# without inheriting `set -e`).
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
fx_main "$@"
|
||||
fi
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# rpm.sh - deterministic E2E fixture for RPM distros (dnf: Alma/Rocky/..., zypper: openSUSE)
|
||||
# Copy of discovery/rpm/fixture.sh; the only changes are the SU_FIXTURE_ISOLATE,
|
||||
# SU_FIXTURE_ORPHAN and SU_FIXTURE_B_AUTO knobs.
|
||||
#
|
||||
# Builds three dummy packages with rpmbuild, installs version 1.0 of each, publishes
|
||||
# version 2.0 in a local file:// repo and registers that repo, so that the package
|
||||
# manager reports exactly these upgrades:
|
||||
#
|
||||
# su-dummy-a 1.0-1 -> 2.0-1 (noarch, Requires: su-dummy-b)
|
||||
# su-dummy-b 1.0-1 -> 2.0-1 (noarch)
|
||||
# su-dummy-c 1.0-1 -> 2.0-1 (ARCH-SPECIFIC, so the ".x86_64" suffix path is exercised)
|
||||
#
|
||||
# Optional (SU_FIXTURE_EXTRAS=1):
|
||||
# - installs su-dummy-d 1.0 (NOT upgradable): Requires su-dummy-a, /bin/sh (file dep),
|
||||
# "su-dummy-b >= 1.0" (versioned), "(su-dummy-c if su-dummy-b)" (rich dep) and rpmlib(...)
|
||||
# -> for reverse-dep / dependency-closure tests;
|
||||
# - publishes su-dummy-r 1.0 (Requires su-dummy-s) and su-dummy-s 1.0 in the repo, NOT installed
|
||||
# -> for install/remove/--clean-deps tests.
|
||||
#
|
||||
# Optional (SU_FIXTURE_ISOLATE=1): after the build tools are installed, move the distro's
|
||||
# own repository (and, on zypper, service) definitions to /srv/su-disabled-repos, so that
|
||||
# only the su-dummy repo is left: the upgradable list is exactly su-dummy-{a,b,c} and later
|
||||
# refreshes/upgrades do not depend on the state of the real mirrors.
|
||||
#
|
||||
# Optional (SU_FIXTURE_ORPHAN=1): su-dummy-orphan 1.0 is installed as a dependency (of
|
||||
# su-dummy-orphan-parent, installed through the package manager and then removed with rpm -e),
|
||||
# so it is a pre-existing unneeded package (dnf repoquery --unneeded / zypper packages --unneeded).
|
||||
#
|
||||
# Optional (SU_FIXTURE_B_AUTO=1): su-dummy-b counts as installed as a dependency (dnf: `dnf mark
|
||||
# remove`, zypper: /var/lib/zypp/AutoInstalled), like the pacman fixture's --asdeps.
|
||||
#
|
||||
# Re-runnable: re-running resets everything to the "1.0 installed, 2.0 available" state.
|
||||
#
|
||||
# Usage (inside the container, as root): bash fixture.sh
|
||||
# docker run --rm -i -v <project>/discovery/rpm:/d:ro <image> bash /d/fixture.sh
|
||||
set -euo pipefail
|
||||
export LC_ALL=C
|
||||
|
||||
T0=$SECONDS
|
||||
log() { printf '[fixture] %s\n' "$*" >&2; }
|
||||
die() { printf '[fixture] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||
|
||||
# ---- 1. backend detection ---------------------------------------------------
|
||||
BACKEND=
|
||||
if command -v zypper >/dev/null 2>&1; then BACKEND=zypper
|
||||
elif command -v dnf >/dev/null 2>&1; then BACKEND=dnf
|
||||
elif command -v yum >/dev/null 2>&1; then BACKEND=yum
|
||||
else die "neither zypper nor dnf/yum found"; fi
|
||||
log "backend=$BACKEND"
|
||||
|
||||
TOP=/var/tmp/su-fixture # rpmbuild topdir
|
||||
REPO=/var/lib/su-dummy-repo # file:// repo (only 2.0 packages)
|
||||
SPEC_DIR=$TOP/SPECS
|
||||
|
||||
# ---- 2. build tools ---------------------------------------------------------
|
||||
t=$SECONDS
|
||||
case $BACKEND in
|
||||
zypper) zypper --non-interactive --quiet install --no-recommends rpm-build createrepo_c >/dev/null ;;
|
||||
dnf|yum) "$BACKEND" -y -q install --setopt=install_weak_deps=False rpm-build createrepo_c >/dev/null ;;
|
||||
esac
|
||||
command -v rpmbuild >/dev/null || die "rpmbuild missing after install"
|
||||
command -v createrepo_c >/dev/null || die "createrepo_c missing after install"
|
||||
log "build tools installed in $((SECONDS - t))s"
|
||||
|
||||
# ---- 2b. optional isolation: only the su-dummy repo stays configured ----------
|
||||
if [ "${SU_FIXTURE_ISOLATE:-0}" = 1 ]; then
|
||||
mkdir -p /srv/su-disabled-repos
|
||||
case $BACKEND in
|
||||
zypper) defs=(/etc/zypp/repos.d/*.repo /etc/zypp/services.d/*.service) ;;
|
||||
dnf|yum) defs=(/etc/yum.repos.d/*.repo) ;;
|
||||
esac
|
||||
for f in "${defs[@]}"; do
|
||||
[ -e "$f" ] || continue
|
||||
case ${f##*/} in su-dummy.repo) continue ;; esac
|
||||
mv "$f" "/srv/su-disabled-repos/${f##*/}"
|
||||
done
|
||||
log "ISOLATE: real repository definitions moved to /srv/su-disabled-repos"
|
||||
fi
|
||||
|
||||
# ---- 3. build RPMs ----------------------------------------------------------
|
||||
rm -rf "$TOP" "$REPO"
|
||||
mkdir -p "$TOP"/{BUILD,BUILDROOT,RPMS,SRPMS,SOURCES,SPECS} "$REPO"
|
||||
|
||||
# mkspec <name> <version> <arch|noarch> <extra spec header lines...>
|
||||
mkspec() {
|
||||
local name=$1 ver=$2 arch=$3; shift 3
|
||||
local f=$SPEC_DIR/$name-$ver.spec
|
||||
{
|
||||
echo "Name: $name"
|
||||
echo "Version: $ver"
|
||||
echo "Release: 1"
|
||||
echo "Summary: skeptical-updater dummy $name (v$ver)"
|
||||
echo "License: MIT"
|
||||
[ "$arch" = noarch ] && echo "BuildArch: noarch"
|
||||
local l; for l in "$@"; do echo "$l"; done
|
||||
echo "%global debug_package %{nil}"
|
||||
echo "%description"
|
||||
echo "Dummy package $name version $ver for skeptical-updater tests."
|
||||
echo ""
|
||||
echo "Second paragraph of $name $ver (blank line above on purpose)."
|
||||
echo "%install"
|
||||
echo "mkdir -p %{buildroot}/usr/share/su-dummy"
|
||||
echo "echo '$name $ver' > %{buildroot}/usr/share/su-dummy/$name"
|
||||
echo "%files"
|
||||
echo "/usr/share/su-dummy/$name"
|
||||
} > "$f"
|
||||
printf '%s\n' "$f"
|
||||
}
|
||||
|
||||
build() { rpmbuild -bb --quiet --define "_topdir $TOP" "$1" >/dev/null 2>"$TOP/rpmbuild.err" \
|
||||
|| { cat "$TOP/rpmbuild.err" >&2; die "rpmbuild failed for $1"; }; }
|
||||
|
||||
t=$SECONDS
|
||||
for v in 1.0 2.0; do
|
||||
build "$(mkspec su-dummy-a $v noarch 'Requires: su-dummy-b')"
|
||||
build "$(mkspec su-dummy-b $v noarch)"
|
||||
build "$(mkspec su-dummy-c $v arch)"
|
||||
done
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then
|
||||
build "$(mkspec su-dummy-s 1.0 noarch)" # published in the repo, NOT installed
|
||||
build "$(mkspec su-dummy-r 1.0 noarch 'Requires: su-dummy-s')" # (used to test remove/--clean-deps)
|
||||
build "$(mkspec su-dummy-d 1.0 noarch \
|
||||
'Requires: su-dummy-a' \
|
||||
'Requires: /bin/sh' \
|
||||
'Requires: su-dummy-b >= 1.0' \
|
||||
'Requires: (su-dummy-c if su-dummy-b)' \
|
||||
'Requires: rpmlib(CompressedFileNames)')"
|
||||
fi
|
||||
if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then
|
||||
build "$(mkspec su-dummy-orphan 1.0 noarch)"
|
||||
build "$(mkspec su-dummy-orphan-parent 1.0 noarch 'Requires: su-dummy-orphan')"
|
||||
fi
|
||||
log "rpmbuild done in $((SECONDS - t))s"
|
||||
|
||||
# ---- 4. install 1.0 (rpm directly: distro-agnostic, resets to 1.0 on re-run) ----
|
||||
shopt -s nullglob
|
||||
v1=( "$TOP"/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm )
|
||||
[ "${#v1[@]}" -eq 3 ] || die "expected 3 built 1.0 rpms, got ${#v1[@]}"
|
||||
rpm -Uvh --oldpackage --replacepkgs "${v1[@]}" >/dev/null
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then
|
||||
rpm -Uvh --oldpackage --replacepkgs "$TOP"/RPMS/noarch/su-dummy-d-1.0-1.*.rpm >/dev/null
|
||||
fi
|
||||
|
||||
# ---- 5. publish 2.0 as a local repo -------------------------------------------
|
||||
v2=( "$TOP"/RPMS/*/su-dummy-[abc]-2.0-1.*.rpm )
|
||||
[ "${#v2[@]}" -eq 3 ] || die "expected 3 built 2.0 rpms, got ${#v2[@]}"
|
||||
cp "${v2[@]}" "$REPO"/
|
||||
if [ "${SU_FIXTURE_EXTRAS:-0}" = 1 ]; then cp "$TOP"/RPMS/noarch/su-dummy-[rs]-1.0-1.*.rpm "$REPO"/; fi
|
||||
if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then cp "$TOP"/RPMS/noarch/su-dummy-orphan*-1.0-1.*.rpm "$REPO"/; fi
|
||||
createrepo_c --quiet "$REPO" >/dev/null
|
||||
|
||||
# ---- 6. register repo ---------------------------------------------------------
|
||||
case $BACKEND in
|
||||
zypper)
|
||||
zypper --non-interactive --quiet removerepo su-dummy >/dev/null 2>&1 || true
|
||||
zypper --non-interactive --quiet addrepo --no-gpgcheck --no-check --refresh "file://$REPO" su-dummy >/dev/null
|
||||
t=$SECONDS
|
||||
zypper --non-interactive --quiet refresh su-dummy >/dev/null
|
||||
;;
|
||||
dnf|yum)
|
||||
cat > /etc/yum.repos.d/su-dummy.repo <<REPOEOF
|
||||
[su-dummy]
|
||||
name=skeptical-updater dummy packages
|
||||
baseurl=file://$REPO
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
repo_gpgcheck=0
|
||||
metadata_expire=0
|
||||
REPOEOF
|
||||
t=$SECONDS
|
||||
"$BACKEND" -q makecache --repo su-dummy >/dev/null
|
||||
;;
|
||||
esac
|
||||
log "repo refresh in $((SECONDS - t))s"
|
||||
|
||||
# ---- 6b. optional pre-existing orphan: installed as a dependency, its parent removed ----------
|
||||
if [ "${SU_FIXTURE_ORPHAN:-0}" = 1 ]; then
|
||||
case $BACKEND in
|
||||
zypper) zypper --non-interactive --quiet install su-dummy-orphan-parent >/dev/null ;;
|
||||
dnf|yum) "$BACKEND" -y -q install su-dummy-orphan-parent >/dev/null 2>&1 ;;
|
||||
esac
|
||||
rpm -e su-dummy-orphan-parent
|
||||
case $BACKEND in
|
||||
zypper) unneeded=$(zypper --non-interactive --quiet packages --unneeded 2>/dev/null || true) ;;
|
||||
dnf|yum) unneeded=$("$BACKEND" -q repoquery --unneeded 2>/dev/null || true) ;;
|
||||
esac
|
||||
rpm -q su-dummy-orphan >/dev/null || die "su-dummy-orphan is not installed"
|
||||
printf '%s\n' "$unneeded" | grep -q su-dummy-orphan || die "su-dummy-orphan is not an unneeded package"
|
||||
log "pre-existing orphan (unneeded): su-dummy-orphan"
|
||||
fi
|
||||
|
||||
# ---- 6c. optional: su-dummy-b installed as a dependency (of su-dummy-a) ----------------------
|
||||
if [ "${SU_FIXTURE_B_AUTO:-0}" = 1 ]; then
|
||||
case $BACKEND in
|
||||
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
||||
dnf|yum) "$BACKEND" -q mark remove su-dummy-b >/dev/null ;;
|
||||
esac
|
||||
log "su-dummy-b marked as installed as a dependency"
|
||||
fi
|
||||
|
||||
# ---- 7. verify: the three must be listed as upgradable --------------------------
|
||||
want="su-dummy-a su-dummy-b su-dummy-c"
|
||||
case $BACKEND in
|
||||
zypper) listing=$(zypper --non-interactive --quiet list-updates 2>&1 || true) ;;
|
||||
dnf|yum) listing=$("$BACKEND" -q check-update 2>&1 || true) ;;
|
||||
esac
|
||||
bad=0
|
||||
for p in $want; do
|
||||
printf '%s\n' "$listing" | grep -Eq "(^|[| ])$p([. ]|$)" || { log "NOT listed as upgradable: $p"; bad=1; }
|
||||
done
|
||||
if [ "$bad" -ne 0 ]; then printf '%s\n' "$listing" >&2; die "fixture verification failed"; fi
|
||||
log "OK: su-dummy-{a,b,c} 1.0 installed and 2.0 offered by repo 'su-dummy' (total ${SECONDS}s, since start $((SECONDS - T0))s)"
|
||||
@@ -0,0 +1,226 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# tests/e2e/helpers.bash - shared helpers for the E2E suite (sourced by tests/e2e/*.bats).
|
||||
# Runs as root inside a distro container prepared by tests/e2e/entrypoint.sh.
|
||||
# shellcheck shell=bash
|
||||
|
||||
load '../vendor/bats-support/load'
|
||||
load '../vendor/bats-assert/load'
|
||||
|
||||
STATE_DIR=/var/lib/skeptical-updater
|
||||
STATE_FILE=$STATE_DIR/trusted.tsv
|
||||
FAMILY=${SU_FAMILY:?SU_FAMILY not set (run through tests/e2e/entrypoint.sh)}
|
||||
case $FAMILY in
|
||||
apt | pacman) BE=$FAMILY ;;
|
||||
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
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
pretty_name() { (. /etc/os-release && printf '%s' "$PRETTY_NAME"); }
|
||||
|
||||
# pkg_ver NAME -> upstream version of the installed package (1.0 / 2.0) or "absent"
|
||||
pkg_ver() {
|
||||
local v=''
|
||||
case $BE in
|
||||
apt)
|
||||
v=$(dpkg-query -W -f '${db:Status-Abbrev} ${Version}' "$1" 2>/dev/null) || v=''
|
||||
# ${db:Status-Abbrev}: 1st char = selection (i, h = hold, ...), 2nd = state (i = installed)
|
||||
if [[ ${v:1:1} == i ]]; then v=${v##* }; else v=''; fi
|
||||
;;
|
||||
pacman)
|
||||
v=$(pacman -Q "$1" 2>/dev/null) || v=''
|
||||
v=${v#* }
|
||||
v=${v%-*}
|
||||
;;
|
||||
dnf | zypper) v=$(rpm -q --qf '%{VERSION}' "$1" 2>/dev/null) || v='' ;;
|
||||
esac
|
||||
printf '%s' "${v:-absent}"
|
||||
}
|
||||
|
||||
versions() { printf 'a=%s b=%s c=%s' "$(pkg_ver su-dummy-a)" "$(pkg_ver su-dummy-b)" "$(pkg_ver su-dummy-c)"; }
|
||||
|
||||
# holds_snapshot: the complete native hold/lock/ignore/exclude state, as text
|
||||
holds_snapshot() {
|
||||
case $BE in
|
||||
apt) apt-mark showhold | sort ;;
|
||||
pacman)
|
||||
echo "--- ignorepkg"
|
||||
cat /etc/pacman.d/ignorepkg
|
||||
echo "--- pacman-conf IgnorePkg"
|
||||
pacman-conf IgnorePkg
|
||||
;;
|
||||
dnf) cat /etc/dnf/dnf.conf ;;
|
||||
zypper) if [[ -e /etc/zypp/locks ]]; then cat /etc/zypp/locks; else echo "(no /etc/zypp/locks)"; fi ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# hold_c: a pre-existing, admin-made hold/lock/IgnorePkg/exclude on su-dummy-c
|
||||
hold_c() {
|
||||
case $BE in
|
||||
apt) apt-mark hold su-dummy-c >/dev/null ;;
|
||||
pacman) echo 'IgnorePkg = su-dummy-c' >/etc/pacman.d/ignorepkg ;;
|
||||
dnf) sed -i '/^\[main\]/a excludepkgs=su-dummy-c' /etc/dnf/dnf.conf ;;
|
||||
zypper) zypper --non-interactive --quiet addlock su-dummy-c >/dev/null ;;
|
||||
esac
|
||||
}
|
||||
|
||||
e2e_setup_file() {
|
||||
# remember the pristine configuration once per container
|
||||
case $BE in
|
||||
pacman)
|
||||
# 1.0 packages for e2e_reset (re-running the fixture would rebuild the repo with new
|
||||
# bytes in the same second, leaving the already synced database stale)
|
||||
if [[ ! -d /srv/su-old-pacman ]]; then
|
||||
mkdir -p /srv/su-old-pacman
|
||||
(
|
||||
# shellcheck source=fixtures/pacman.sh
|
||||
. "$BATS_TEST_DIRNAME/fixtures/pacman.sh"
|
||||
fx_build_tar /srv/su-old-pacman su-dummy-a 1.0 'Skeptical-updater dummy package A' su-dummy-b
|
||||
fx_build_tar /srv/su-old-pacman su-dummy-b 1.0 'Skeptical-updater dummy package B (dependency of A)'
|
||||
fx_build_tar /srv/su-old-pacman su-dummy-c 1.0 'Skeptical-updater dummy package C (independent)'
|
||||
)
|
||||
fi
|
||||
;;
|
||||
dnf) [[ -e /srv/su-orig-dnf.conf ]] || cp -p /etc/dnf/dnf.conf /srv/su-orig-dnf.conf ;;
|
||||
zypper)
|
||||
rm -f /etc/zypp/zypp.conf.d/50-su-e2e.conf /etc/zypp/locks-su-e2e
|
||||
if [[ ! -e /srv/su-orig-locks.state ]]; then
|
||||
if [[ -e /etc/zypp/locks ]]; then
|
||||
cp -p /etc/zypp/locks /srv/su-orig-locks
|
||||
echo present >/srv/su-orig-locks.state
|
||||
else
|
||||
echo absent >/srv/su-orig-locks.state
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# e2e_reset: back to "su-dummy-{a,b,c} 1.0 installed, 2.0 available, no holds, no statefile"
|
||||
e2e_reset() {
|
||||
rm -rf -- "$STATE_DIR"
|
||||
case $BE in
|
||||
apt)
|
||||
local -a held=()
|
||||
mapfile -t held < <(apt-mark showhold)
|
||||
((${#held[@]} == 0)) || apt-mark unhold "${held[@]}" >/dev/null
|
||||
dpkg --force-confnew -i /srv/su-old/*.deb </dev/null >/dev/null 2>&1
|
||||
;;
|
||||
pacman)
|
||||
: >/etc/pacman.d/ignorepkg
|
||||
pacman -U --noconfirm /srv/su-old-pacman/*.pkg.tar.zst >/dev/null 2>&1
|
||||
pacman -D --asexplicit su-dummy-a su-dummy-c >/dev/null
|
||||
pacman -D --asdeps su-dummy-b >/dev/null
|
||||
;;
|
||||
dnf | zypper)
|
||||
if [[ $BE == dnf ]]; then
|
||||
cp -p /srv/su-orig-dnf.conf /etc/dnf/dnf.conf
|
||||
else
|
||||
rm -f /etc/zypp/zypp.conf.d/50-su-e2e.conf /etc/zypp/locks-su-e2e
|
||||
fi
|
||||
if [[ $BE == dnf ]]; then
|
||||
:
|
||||
elif [[ $(cat /srv/su-orig-locks.state) == present ]]; then
|
||||
cp -p /srv/su-orig-locks /etc/zypp/locks
|
||||
else
|
||||
rm -f /etc/zypp/locks
|
||||
fi
|
||||
rpm -Uvh --oldpackage --replacepkgs /var/tmp/su-fixture/RPMS/*/su-dummy-[abc]-1.0-1.*.rpm >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
# su-dummy-b is a dependency of su-dummy-a (installed as such), on every backend
|
||||
case $BE in
|
||||
apt) apt-mark auto su-dummy-orphan su-dummy-b >/dev/null ;;
|
||||
dnf) dnf -q mark remove su-dummy-b >/dev/null 2>&1 ;;
|
||||
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
|
||||
esac
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" && $(pkg_ver su-dummy-orphan) == 1.0 ]] || {
|
||||
echo "e2e_reset: unexpected state: $(versions) orphan=$(pkg_ver su-dummy-orphan)" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# the native upgrade command line printed by --dry-run (without exclusions)
|
||||
native_upgrade_regex() {
|
||||
case $BE in
|
||||
apt) printf '%s' '^\+ apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 --with-new-pkgs upgrade$' ;;
|
||||
pacman) printf '%s' '^\+ pacman -Su --noconfirm$' ;; # after the reviewed databases were installed
|
||||
dnf) printf '%s' '^\+ (dnf|yum) -y --setopt=skip_if_unavailable=0 --setopt=\\\*\.metadata_expire=-1 upgrade$' ;;
|
||||
zypper) printf '%s' '^\+ zypper --non-interactive --no-refresh (update|dist-upgrade)$' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# rpm_publish NAME VERSION: build NAME-VERSION-1 (arch-specific for su-dummy-c, like the fixture)
|
||||
# with the fixture's rpmbuild tree and add it to the su-dummy repository
|
||||
rpm_publish() {
|
||||
local name=$1 ver=$2 top=/var/tmp/su-fixture repo=/var/lib/su-dummy-repo spec
|
||||
spec=$top/SPECS/$name-$ver.spec
|
||||
{
|
||||
echo "Name: $name"
|
||||
echo "Version: $ver"
|
||||
echo "Release: 1"
|
||||
echo "Summary: skeptical-updater dummy $name (v$ver)"
|
||||
echo "License: MIT"
|
||||
[[ $name == su-dummy-c ]] || echo "BuildArch: noarch"
|
||||
echo "%global debug_package %{nil}"
|
||||
echo "%description"
|
||||
echo "Dummy package $name version $ver."
|
||||
echo "%install"
|
||||
echo "mkdir -p %{buildroot}/usr/share/su-dummy"
|
||||
echo "echo '$name $ver' > %{buildroot}/usr/share/su-dummy/$name"
|
||||
echo "%files"
|
||||
echo "/usr/share/su-dummy/$name"
|
||||
} >"$spec"
|
||||
rpmbuild -bb --quiet --define "_topdir $top" "$spec" >/dev/null 2>&1
|
||||
cp "$top"/RPMS/*/"$name-$ver"-1.*.rpm "$repo"/
|
||||
createrepo_c --quiet --update "$repo" >/dev/null
|
||||
}
|
||||
rpm_unpublish() {
|
||||
rm -f /var/lib/su-dummy-repo/"$1-$2"-1.*.rpm
|
||||
createrepo_c --quiet --update /var/lib/su-dummy-repo >/dev/null
|
||||
}
|
||||
|
||||
# pacman_sync_snapshot: the real sync database directory, with full timestamps
|
||||
pacman_sync_snapshot() { ls -la --time-style=full-iso /var/lib/pacman/sync 2>&1; }
|
||||
|
||||
# pacman_publish NAME VERSION / pacman_unpublish: add a package to the fixture's [su-dummy] repo,
|
||||
# and rebuild that repo with only the fixture's 2.0 packages again
|
||||
pacman_publish() {
|
||||
(
|
||||
# shellcheck source=fixtures/pacman.sh
|
||||
. "$BATS_TEST_DIRNAME/fixtures/pacman.sh"
|
||||
fx_build_tar /srv/su-repo "$1" "$2" "Skeptical-updater dummy package $1 $2"
|
||||
)
|
||||
(cd /srv/su-repo && repo-add --quiet su-dummy.db.tar.gz "./$1-$2-1-any.pkg.tar.zst")
|
||||
}
|
||||
pacman_unpublish() {
|
||||
(cd /srv/su-repo && rm -f su-dummy.db* su-dummy.files* &&
|
||||
for f in ./*.pkg.tar.zst; do case $f in ./su-dummy-[abc]-2.0-1-any.pkg.tar.zst) ;; *) rm -f "$f" ;; esac; done &&
|
||||
repo-add --quiet su-dummy.db.tar.gz ./su-dummy-a-2.0-1-any.pkg.tar.zst ./su-dummy-b-2.0-1-any.pkg.tar.zst ./su-dummy-c-2.0-1-any.pkg.tar.zst)
|
||||
}
|
||||
|
||||
# session_start ARGS...: run skeptical-updater ARGS in the background with a FIFO as stdin and wait
|
||||
# for its prompt; sets SESSION_PID and SESSION_FD (write the input to it; fd 3 belongs to bats)
|
||||
session_start() {
|
||||
mkfifo "$BATS_TEST_TMPDIR/in"
|
||||
skeptical-updater "$@" <"$BATS_TEST_TMPDIR/in" >"$BATS_TEST_TMPDIR/out" 2>&1 &
|
||||
SESSION_PID=$!
|
||||
exec {SESSION_FD}>"$BATS_TEST_TMPDIR/in"
|
||||
for _ in $(seq 1 600); do
|
||||
grep -q '^<enter> upgrade' "$BATS_TEST_TMPDIR/out" && return 0
|
||||
sleep 0.1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# session_finish INPUT: send INPUT, close stdin, wait; sets SESSION_RC (read by the .bats files)
|
||||
# shellcheck disable=SC2034
|
||||
session_finish() {
|
||||
printf '%s\n' "$1" >&"$SESSION_FD"
|
||||
exec {SESSION_FD}>&-
|
||||
SESSION_RC=0
|
||||
wait "$SESSION_PID" || SESSION_RC=$?
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env bats
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Copyright (C) 2026 Bryan Joshua Pedini
|
||||
|
||||
# E2E: skeptical-updater against the REAL package manager of the container
|
||||
# (apt, pacman, dnf or zypper), on the su-dummy-{a,b,c} fixture: 1.0 installed,
|
||||
# 2.0 available, su-dummy-a depends on su-dummy-b. Every test starts from that
|
||||
# state (e2e_reset) with no statefile and no holds/locks.
|
||||
|
||||
load helpers
|
||||
|
||||
setup_file() { e2e_setup_file; }
|
||||
setup() { e2e_reset; }
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
zypper_verb() { case $(. /etc/os-release && printf '%s' "$ID") in opensuse-tumbleweed | opensuse-slowroll) echo dist-upgrade ;; *) echo update ;; esac; }
|
||||
|
||||
@test "banner shows the detected backend and the distro, refresh succeeds" {
|
||||
run skeptical-updater list
|
||||
assert_success
|
||||
assert_line --index 0 "Backend: $BE ($(pretty_name))"
|
||||
assert_line --index 1 "Refreshing package lists... done."
|
||||
}
|
||||
|
||||
@test "list shows exactly the three dummies 1.0 -> 2.0" {
|
||||
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)? +[^ ]+$'
|
||||
refute_line --regexp '^.\[4\]'
|
||||
}
|
||||
|
||||
@test "N? prints the description; su-dummy-b is required by su-dummy-a" {
|
||||
run skeptical-updater --no-refresh <<<$'1?\n2?\nq'
|
||||
assert_success
|
||||
assert_line --regexp '^su-dummy-a: .*([Dd]ummy|skeptical-updater).*$'
|
||||
assert_line --regexp '^su-dummy-b: .*([Dd]ummy|skeptical-updater).*$'
|
||||
# [1] su-dummy-a has no installed reverse dependency, [2] su-dummy-b is needed by su-dummy-a
|
||||
assert_line ' required by (installed): nothing'
|
||||
assert_line ' required by (installed): su-dummy-a'
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
}
|
||||
|
||||
@test "N?? prints the native full info" {
|
||||
run skeptical-updater --no-refresh <<<$'1??\nq'
|
||||
assert_success
|
||||
assert_line --regexp '^(Package: su-dummy-a|Name +: su-dummy-a)$'
|
||||
}
|
||||
|
||||
@test "N+ writes the user row and the dep row; list hides both and counts them" {
|
||||
run skeptical-updater --no-refresh <<<$'1+\nq'
|
||||
assert_success
|
||||
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
[[ -f $STATE_FILE ]]
|
||||
[[ $(stat -c %a "$STATE_DIR") == 755 ]]
|
||||
run cat "$STATE_FILE"
|
||||
assert_line --index 0 '# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
|
||||
assert_line --index 1 --regexp "^$BE"$'\t'"su-dummy-a"$'\t'"user"$'\t''[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$'
|
||||
assert_line --index 2 --regexp "^$BE"$'\t'"su-dummy-b"$'\t'"dep:su-dummy-a"$'\t''[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$'
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_success
|
||||
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 '
|
||||
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 '
|
||||
}
|
||||
|
||||
@test "trust / trusted / refresh / untrust from the command line" {
|
||||
run skeptical-updater trust su-dummy-a
|
||||
assert_success
|
||||
assert_output 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
|
||||
run skeptical-updater trusted
|
||||
assert_success
|
||||
assert_line --partial "$BE"$'\t'"su-dummy-a"$'\t'"user"$'\t'
|
||||
assert_line --partial "$BE"$'\t'"su-dummy-b"$'\t'"dep:su-dummy-a"$'\t'
|
||||
# refresh recomputes the closure: a removed dep row comes back
|
||||
sed -i '/dep:su-dummy-a/d' "$STATE_FILE"
|
||||
run skeptical-updater refresh
|
||||
assert_success
|
||||
assert_output 'refreshed the dependency closures of 1 trusted root: 1 added, 0 removed'
|
||||
run grep -c $'\tdep:su-dummy-a\t' "$STATE_FILE"
|
||||
assert_output 1
|
||||
run skeptical-updater untrust su-dummy-a
|
||||
assert_success
|
||||
assert_output 'untrusted su-dummy-a (2 rows removed)'
|
||||
run skeptical-updater trusted
|
||||
assert_success
|
||||
assert_output '# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
|
||||
}
|
||||
|
||||
@test "excluding su-dummy-b (2- then enter) upgrades the others" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line 'excluded from this run: su-dummy-b'
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
assert_line 'Upgrade finished.'
|
||||
[[ $(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'
|
||||
}
|
||||
|
||||
@test "a pre-existing hold/lock/IgnorePkg on su-dummy-c survives a run that excludes su-dummy-b" {
|
||||
hold_c
|
||||
before=$(holds_snapshot)
|
||||
[[ -n $before ]]
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_line --regexp '^ \[2\] su-dummy-b '
|
||||
run skeptical-updater <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
[[ $(versions) == "a=2.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
after=$(holds_snapshot)
|
||||
[[ $after == "$before" ]] || fail $'hold/lock state changed.\nbefore:\n'"$before"$'\nafter:\n'"$after"
|
||||
if [[ $BE == apt ]]; then [[ ! -e $STATE_DIR/holds.added ]]; fi
|
||||
}
|
||||
|
||||
@test "trusting a package re-includes its excluded dependency" {
|
||||
run skeptical-updater <<<$'2-\n1+\n'
|
||||
assert_success
|
||||
assert_line ' (su-dummy-b is trusted now: no longer excluded from this run)'
|
||||
assert_line 'Excluded from this run: none'
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
}
|
||||
|
||||
@test "enter upgrades everything" {
|
||||
run skeptical-updater <<<''
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: none'
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
run skeptical-updater list --no-refresh
|
||||
assert_success
|
||||
assert_output --partial 'Nothing to upgrade.'
|
||||
}
|
||||
|
||||
@test "Nx purges su-dummy-c after a y confirmation" {
|
||||
run skeptical-updater --no-refresh <<<$'3x\ny\nl\nq'
|
||||
assert_success
|
||||
assert_line 'Purge su-dummy-c now? [y/N] y'
|
||||
assert_line 'purged: su-dummy-c'
|
||||
assert_line '2 upgradable, 0 trusted (hidden, use --all to show), 2 to review:'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=absent" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
||||
}
|
||||
|
||||
@test "Nx on su-dummy-b is refused: su-dummy-a depends on it; both stay, the row stays listed" {
|
||||
run skeptical-updater --no-refresh <<<$'2x\nl\nq'
|
||||
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"
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
}
|
||||
|
||||
@test "Nx on su-dummy-a removes su-dummy-b with it (its dependency, nothing else needs it); the orphan survives" {
|
||||
run skeptical-updater --no-refresh <<<$'1x\ny\nl\nq'
|
||||
assert_success
|
||||
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:'
|
||||
[[ $(versions) == "a=absent b=absent c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
||||
}
|
||||
|
||||
@test "purging every package ends the session cleanly" {
|
||||
run skeptical-updater --no-refresh <<<$'1x\ny\n3x\ny'
|
||||
assert_success
|
||||
assert_line 'Nothing left to upgrade.'
|
||||
[[ $(versions) == "a=absent b=absent c=absent" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(pkg_ver su-dummy-orphan) == 1.0 ]] || fail "the unrelated orphan was removed"
|
||||
}
|
||||
|
||||
@test "Nx answered with anything but y keeps the package" {
|
||||
run skeptical-updater --no-refresh <<<$'3x\nn\nq'
|
||||
assert_success
|
||||
assert_line 'not purged: su-dummy-c'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
}
|
||||
|
||||
@test "q leaves everything untouched" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater <<<$'2-\n1-\nq'
|
||||
assert_success
|
||||
assert_line 'Quit: nothing was upgraded.'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
[[ $(holds_snapshot) == "$before" ]]
|
||||
[[ ! -e $STATE_FILE ]]
|
||||
}
|
||||
|
||||
@test "EOF aborts with exit 2 and changes nothing" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater <<<'2-'
|
||||
assert_failure 2
|
||||
assert_output --partial 'end of input: aborted, nothing was upgraded'
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
[[ $(holds_snapshot) == "$before" ]]
|
||||
[[ ! -e $STATE_FILE ]]
|
||||
}
|
||||
|
||||
@test "--dry-run: enter prints the native upgrade command and changes nothing" {
|
||||
before=$(holds_snapshot)
|
||||
run skeptical-updater --dry-run <<<''
|
||||
assert_success
|
||||
assert_line --regexp "$(native_upgrade_regex)"
|
||||
assert_line 'Dry run: nothing was changed.'
|
||||
# (3+: trusting su-dummy-a would hide su-dummy-b and so re-include it)
|
||||
run skeptical-updater --dry-run <<<$'2-\n3+\n'
|
||||
assert_success
|
||||
case $BE in
|
||||
apt)
|
||||
assert_line '+ apt-mark hold su-dummy-b'
|
||||
assert_line '+ apt-mark unhold su-dummy-b'
|
||||
;;
|
||||
pacman)
|
||||
assert_line --regexp '^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db /var/lib/pacman/sync/$'
|
||||
assert_line '+ pacman -Su --noconfirm --ignore su-dummy-b'
|
||||
;;
|
||||
dnf) assert_line --regexp '^\+ (dnf|yum) -y --setopt=skip_if_unavailable=0 --setopt=\\\*\.metadata_expire=-1 upgrade --exclude=su-dummy-b$' ;;
|
||||
zypper)
|
||||
assert_line --regexp '^\+ env ZYPP_CONF=.*/zypp\.conf zypper --non-interactive --quiet addlock su-dummy-b$'
|
||||
assert_line --regexp "^\+ env ZYPP_CONF=.*/zypp\.conf zypper --non-interactive --no-refresh $(zypper_verb)\$"
|
||||
;;
|
||||
esac
|
||||
assert_line --partial "+ $STATE_FILE add: $BE su-dummy-c user"
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
[[ $(holds_snapshot) == "$before" ]]
|
||||
[[ ! -e $STATE_DIR ]]
|
||||
}
|
||||
|
||||
@test "read-only commands work unprivileged" {
|
||||
if command -v setpriv >/dev/null 2>&1; then
|
||||
asuser() { setpriv --reuid=65534 --regid=65534 --clear-groups -- env TMPDIR=/tmp "$@"; }
|
||||
elif command -v python3 >/dev/null 2>&1; then # almalinux:10 has neither setpriv nor runuser/su
|
||||
asuser() { python3 -c 'import os, sys; os.setgroups([]); os.setgid(65534); os.setuid(65534); os.execvp(sys.argv[1], sys.argv[1:])' env TMPDIR=/tmp "$@"; }
|
||||
else
|
||||
skip "neither setpriv nor python3 available to drop privileges"
|
||||
fi
|
||||
skeptical-updater trust su-dummy-c >/dev/null
|
||||
run asuser skeptical-updater trusted
|
||||
assert_success
|
||||
assert_line --partial "$BE"$'\t'"su-dummy-c"$'\t'"user"
|
||||
run asuser skeptical-updater info su-dummy-b
|
||||
assert_success
|
||||
assert_line ' required by (installed): su-dummy-a'
|
||||
run asuser skeptical-updater --dry-run --no-refresh <<<''
|
||||
assert_success
|
||||
assert_line --regexp "$(native_upgrade_regex)"
|
||||
run asuser skeptical-updater upgrade-me-please
|
||||
assert_failure 1
|
||||
# a real upgrade without root and without sudo is refused
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
run asuser skeptical-updater <<<''
|
||||
assert_failure 1
|
||||
assert_output --partial 'upgrading needs root and sudo is not available'
|
||||
fi
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]]
|
||||
# a state directory an unprivileged user cannot enter is an error, not "no trusted packages"
|
||||
chmod 0000 /var/lib/skeptical-updater
|
||||
run asuser skeptical-updater trusted
|
||||
chmod 0755 /var/lib/skeptical-updater
|
||||
assert_failure 1
|
||||
assert_output --partial 'cannot access the state directory /var/lib/skeptical-updater (permission denied)'
|
||||
}
|
||||
|
||||
@test "pacman: list, q, end of input and --dry-run never touch the real sync database" {
|
||||
[[ $BE == pacman ]] || skip "pacman only"
|
||||
before=$(pacman_sync_snapshot)
|
||||
run skeptical-updater list
|
||||
assert_success
|
||||
run skeptical-updater <<<'q'
|
||||
assert_success
|
||||
run skeptical-updater <<<'2-'
|
||||
assert_failure 2
|
||||
run skeptical-updater --dry-run <<<''
|
||||
assert_success
|
||||
assert_line 'Checking the reviewed transaction (pacman -Sup)... ok.'
|
||||
assert_line '+ pacman -Su --noconfirm'
|
||||
after=$(pacman_sync_snapshot)
|
||||
[[ $after == "$before" ]] || fail $'the real sync database changed.\nbefore:\n'"$before"$'\nafter:\n'"$after"
|
||||
run ls /tmp
|
||||
refute_output --partial 'skeptical-updater-db.'
|
||||
}
|
||||
|
||||
@test "apt: temporary holds wait for a busy dpkg lock instead of failing" {
|
||||
[[ $BE == apt ]] || skip "apt only"
|
||||
command -v perl >/dev/null 2>&1 || skip "perl not available"
|
||||
# hold dpkg's frontend lock for 4 s (fcntl, as dpkg does), starting now
|
||||
perl -e 'use Fcntl; open(my $f, ">>", "/var/lib/dpkg/lock-frontend") or die; my $l = pack("s s l l i", F_WRLCK, 0, 0, 0, 0); fcntl($f, F_SETLKW, $l) or die; sleep 4;' &
|
||||
locker=$!
|
||||
sleep 0.5
|
||||
run skeptical-updater --no-refresh <<<$'2-\n'
|
||||
wait "$locker" || true
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
[[ $(versions) == "a=2.0 b=1.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ -z $(apt-mark showhold) ]]
|
||||
}
|
||||
|
||||
@test "zypper: a lock whose locksfile.path comes from a zypp.conf.d drop-in is honoured" {
|
||||
[[ $BE == zypper ]] || skip "zypper only"
|
||||
mkdir -p /etc/zypp/zypp.conf.d
|
||||
printf '[main]\nlocksfile.path = /etc/zypp/locks-su-e2e\n' >/etc/zypp/zypp.conf.d/50-su-e2e.conf
|
||||
zypper --non-interactive --quiet addlock su-dummy-c >/dev/null
|
||||
grep -q 'solvable_name: su-dummy-c' /etc/zypp/locks-su-e2e
|
||||
before=$(cat /etc/zypp/locks-su-e2e)
|
||||
run skeptical-updater <<<$'2-\n'
|
||||
assert_success
|
||||
assert_line 'Excluded from this run: su-dummy-b'
|
||||
[[ $(versions) == "a=2.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ $(cat /etc/zypp/locks-su-e2e) == "$before" ]]
|
||||
}
|
||||
|
||||
@test "zypper: the reviewed metadata is applied (no refresh between review and upgrade)" {
|
||||
[[ $BE == zypper ]] || skip "zypper only (libdnf reads file:// repositories live; apt never refreshes on upgrade)"
|
||||
mkfifo "$BATS_TEST_TMPDIR/in"
|
||||
skeptical-updater <"$BATS_TEST_TMPDIR/in" >"$BATS_TEST_TMPDIR/out" 2>&1 &
|
||||
pid=$!
|
||||
exec {w}>"$BATS_TEST_TMPDIR/in" # (fd 3 belongs to bats)
|
||||
for _ in $(seq 1 600); do
|
||||
grep -q '^<enter> upgrade' "$BATS_TEST_TMPDIR/out" && break
|
||||
sleep 0.1
|
||||
done
|
||||
# a newer su-dummy-c appears in the repository after the review
|
||||
rpm_publish su-dummy-c 3.0
|
||||
echo >&"$w"
|
||||
exec {w}>&-
|
||||
rc=0
|
||||
wait "$pid" || rc=$?
|
||||
rpm_unpublish su-dummy-c 3.0
|
||||
[[ $rc -eq 0 ]] || fail "exit $rc: $(cat "$BATS_TEST_TMPDIR/out")"
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=2.0" ]] || fail "versions after the run: $(versions)"
|
||||
}
|
||||
|
||||
@test "pacman: package databases synced elsewhere during the review: refused (exit 2), nothing changed" {
|
||||
[[ $BE == pacman ]] || skip "pacman only"
|
||||
session_start
|
||||
sleep 1
|
||||
pacman_publish su-dummy-c 3.0
|
||||
pacman -Sy >/dev/null 2>&1 # another terminal: the system's database is now newer than the reviewed one
|
||||
synced=$(sha256sum /var/lib/pacman/sync/su-dummy.db | cut -d' ' -f1)
|
||||
session_finish ''
|
||||
pacman_unpublish
|
||||
pacman -Sy >/dev/null 2>&1
|
||||
[[ $SESSION_RC -eq 2 ]] || fail "exit $SESSION_RC: $(cat "$BATS_TEST_TMPDIR/out")"
|
||||
grep -q 'package databases changed since the review (su-dummy); run skeptical-updater again' "$BATS_TEST_TMPDIR/out"
|
||||
[[ $(versions) == "a=1.0 b=1.0 c=1.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ ! -e /var/lib/pacman/db.lck ]]
|
||||
[[ -n $synced ]]
|
||||
}
|
||||
|
||||
@test "pacman: <enter> installs exactly the reviewed metadata (the real sync DB becomes the reviewed one)" {
|
||||
[[ $BE == pacman ]] || skip "pacman only"
|
||||
# the system's database is older than what the review will see (su-dummy-c 3.0) ...
|
||||
# (1 s apart from the previous repo rebuild: pacman compares database mtimes in seconds)
|
||||
sleep 1
|
||||
pacman_publish su-dummy-c 3.0
|
||||
session_start
|
||||
reviewed=$(sha256sum /tmp/skeptical-updater-db."$SESSION_PID".*/sync/su-dummy.db | cut -d' ' -f1)
|
||||
# ... and su-dummy-c 4.0 appears in the repository after the review
|
||||
sleep 1
|
||||
pacman_publish su-dummy-c 4.0
|
||||
session_finish ''
|
||||
pacman_unpublish
|
||||
[[ $SESSION_RC -eq 0 ]] || fail "exit $SESSION_RC: $(cat "$BATS_TEST_TMPDIR/out")"
|
||||
grep -qE '^\+ cp -p -- /tmp/skeptical-updater-db\.[0-9]+\.[A-Za-z0-9]+/sync/su-dummy\.db /var/lib/pacman/sync/$' "$BATS_TEST_TMPDIR/out"
|
||||
[[ -n $reviewed ]]
|
||||
[[ $(sha256sum /var/lib/pacman/sync/su-dummy.db | cut -d' ' -f1) == "$reviewed" ]] || fail "the system's su-dummy.db is not the reviewed one"
|
||||
[[ $(versions) == "a=2.0 b=2.0 c=3.0" ]] || fail "versions after the run: $(versions)"
|
||||
[[ ! -e /var/lib/pacman/db.lck ]]
|
||||
run ls /tmp
|
||||
refute_output --partial 'skeptical-updater-db.'
|
||||
}
|
||||
Reference in new issue
Block a user