Files
skeptical-updater/tests/e2e/helpers.bash
T
bryanpedini 71d7f87aa3 feat(info): show the install reason and drop the indentation
N? and `info PKG` now print three lines, none indented:

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

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

The rpm E2E fixture and e2e_reset now also mark su-dummy-a and su-dummy-c
as user-installed on dnf, which records plain rpm installs as "unknown",
so 1? and 2? show both reasons on all 12 distros. The verified commands
are recorded in discovery/{apt,pacman,rpm}.md.
2026-10-10 13:32:42 +02:00

231 lines
8.5 KiB
Bash

# 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; su-dummy-a and
# su-dummy-c were installed by the user (dnf records plain rpm installs as reason unknown)
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 || dnf -q -y mark dependency su-dummy-b >/dev/null 2>&1
dnf -q mark install su-dummy-a su-dummy-c >/dev/null 2>&1 || dnf -q -y mark user su-dummy-a su-dummy-c >/dev/null 2>&1
;;
zypper) grep -qx su-dummy-b /var/lib/zypp/AutoInstalled 2>/dev/null || echo su-dummy-b >>/var/lib/zypp/AutoInstalled ;;
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=$?
}