# 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 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 '^ 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=$? }