176 lines
13 KiB
Bash
Executable File
176 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
# Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
# proof.sh - runs lib-rpm.sh against the fixture and asserts the behaviour documented in rpm.md.
|
|
# docker run --rm -i -v <project>/discovery/rpm:/d:ro <image> bash /d/proof.sh
|
|
# Prints PASS/FAIL lines + sample output; exit code = number of failures.
|
|
export LC_ALL=C
|
|
D=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
now() { printf '%s' "${EPOCHREALTIME/./}"; } # microseconds
|
|
T_START=$(now)
|
|
FAILS=0
|
|
pass() { printf 'PASS %s\n' "$1"; }
|
|
fail() { printf 'FAIL %s\n' "$1"; FAILS=$((FAILS+1)); }
|
|
check() { local name=$1; shift; if "$@" >/dev/null 2>&1; then pass "$name"; else fail "$name"; fi; }
|
|
eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1: got [$2] want [$3]"; fi; }
|
|
exec 3>&1 # fd 3 = real stdout, so tm's timing is not swallowed by redirections
|
|
tm() { local t0=$(now); "$@"; local rc=$?; printf ' (took %d ms)\n' $(( ($(now)-t0)/1000 )) >&3; return $rc; }
|
|
sec() { printf '\n== %s\n' "$*"; }
|
|
|
|
sec "fixture"
|
|
t0=$(now); SU_FIXTURE_EXTRAS=1 bash "$D/fixture.sh" 2>&1 | sed 's/^/ /' | tail -4; [ "${PIPESTATUS[0]}" -eq 0 ] || { echo "FIXTURE FAILED"; exit 99; }
|
|
FIXTURE_MS=$(( ($(now)-t0)/1000 ))
|
|
. "$D/lib-rpm.sh"
|
|
. /etc/os-release
|
|
BACKEND=$(su_backend)
|
|
echo "image=$PRETTY_NAME ID=$ID backend=$BACKEND dnf_major=$([ $BACKEND = dnf ] && su_dnf_major || echo -) zypper_verb=$([ $BACKEND = zypper ] && su_zypper_verb || echo -)"
|
|
|
|
sec "1. refresh"
|
|
t0=$(now); su_refresh; rc=$?; REFRESH_MS=$(( ($(now)-t0)/1000 )); eq "su_refresh exit code" "$rc" 0; echo " (took $REFRESH_MS ms)"
|
|
|
|
sec "2. upgradable list (name old new repo arch)"
|
|
t0=$(now); su_upgradable > /tmp/up.tsv; rc=$?; LIST_MS=$(( ($(now)-t0)/1000 )); eq "su_upgradable exit code" "$rc" 0
|
|
echo " (took $LIST_MS ms; $(wc -l </tmp/up.tsv) rows) first 5 rows:"; head -5 /tmp/up.tsv | cat -A | sed 's/^/ /'
|
|
for p in a b c; do
|
|
arch=noarch; [ $p = c ] && arch=$(rpm -q --qf '%{ARCH}' su-dummy-c)
|
|
grep -q -x -F "su-dummy-$p"$'\t'"1.0-1"$'\t'"2.0-1"$'\t'"su-dummy"$'\t'"$arch" /tmp/up.tsv && pass "row su-dummy-$p 1.0-1 -> 2.0-1 [su-dummy] $arch" || fail "row su-dummy-$p missing"
|
|
done
|
|
check "no su-dummy-d (not upgradable)" test "$(grep -c '^su-dummy-d' /tmp/up.tsv)" -eq 0
|
|
check "no '.arch' suffix in name column" test "$(cut -f1 /tmp/up.tsv | grep -c '\.\(noarch\|x86_64\|aarch64\)$')" -eq 0
|
|
|
|
sec "3. summary + description (candidate if upgradable, else installed)"
|
|
su_describe su-dummy-a > /tmp/d_a.txt; echo "--- su-dummy-a (candidate):"; sed 's/^/ /' /tmp/d_a.txt
|
|
eq "candidate summary" "$(head -n1 /tmp/d_a.txt)" "skeptical-updater dummy su-dummy-a (v2.0)"
|
|
check "candidate description has 2nd paragraph" grep -q 'Second paragraph of su-dummy-a 2.0' /tmp/d_a.txt
|
|
check "paragraph break preserved" test "$(sed -n 3p /tmp/d_a.txt)" = ""
|
|
su_describe su-dummy-d > /tmp/d_d.txt; echo "--- su-dummy-d (installed only):"; sed 's/^/ /' /tmp/d_d.txt
|
|
eq "installed-only summary" "$(head -n1 /tmp/d_d.txt)" "skeptical-updater dummy su-dummy-d (v1.0)"
|
|
rpm -qi su-dummy-a | sed -n '/^Summary/p' | sed 's/^/ rpm -qi: /'
|
|
|
|
sec "4. installed reverse dependencies"
|
|
eq "rdeps su-dummy-b" "$(su_rdeps su-dummy-b | tr '\n' ' ')" "su-dummy-a su-dummy-d "
|
|
eq "rdeps su-dummy-a" "$(su_rdeps su-dummy-a | tr '\n' ' ')" "su-dummy-d "
|
|
eq "rdeps su-dummy-c (only via rich dep (c if b))" "$(su_rdeps su-dummy-c | tr '\n' ' ')" "su-dummy-d "
|
|
eq "rdeps su-dummy-d (leaf)" "$(su_rdeps su-dummy-d | tr '\n' ' ')" ""
|
|
n_mine=$(su_rdeps bash | wc -l); n_naive=$(rpm -q --qf '%{NAME}\n' --whatrequires bash 2>/dev/null | sort -u | wc -l)
|
|
echo " bash: su_rdeps=$n_mine naive 'rpm -q --whatrequires bash'=$n_naive"
|
|
provider_sh=$(rpm -q --whatprovides --qf '%{NAME}\n' /bin/sh | head -n1)
|
|
check "su_rdeps($provider_sh) finds su-dummy-d via file dep /bin/sh" bash -c ". $D/lib-rpm.sh; su_rdeps $provider_sh | grep -qx su-dummy-d"
|
|
[ "$n_mine" -gt "$n_naive" ] && pass "su_rdeps(bash) > naive rpm --whatrequires" || fail "su_rdeps(bash) not > naive"
|
|
if [ "$BACKEND" = dnf ]; then
|
|
dnf -q repoquery --installed --whatrequires bash --queryformat $'%{name}\n' 2>/dev/null | sed '/^$/d' | sort -u > /tmp/native_rd.txt
|
|
su_rdeps bash > /tmp/mine_rd.txt; eq "su_rdeps(bash) == dnf repoquery --installed --whatrequires" "$(cat /tmp/mine_rd.txt | tr '\n' ' ')" "$(cat /tmp/native_rd.txt | tr '\n' ' ')"
|
|
else
|
|
zypper -q -n search --installed-only -t package --requires-pkg bash 2>/dev/null | grep -E '^[iv ][+ ]? *\|' | cut -d'|' -f2 | tr -d ' ' | grep -v -x Name | sort -u > /tmp/native_rd.txt
|
|
su_rdeps bash > /tmp/mine_rd.txt; su_rdeps_recursive bash > /tmp/mine_rrd.txt
|
|
echo " zypper se --requires-pkg bash : $(wc -l </tmp/native_rd.txt) pkgs"; echo " su_rdeps bash (direct) : $(wc -l </tmp/mine_rd.txt) pkgs: $(tr '\n' ' ' </tmp/mine_rd.txt)"
|
|
echo " su_rdeps_recursive bash : $(wc -l </tmp/mine_rrd.txt) pkgs"
|
|
echo " native minus recursive : $(LC_ALL=C comm -23 /tmp/native_rd.txt /tmp/mine_rrd.txt | tr '\n' ' ')"
|
|
fi
|
|
eq "su_rdeps_recursive(su-dummy-b)" "$(su_rdeps_recursive su-dummy-b | tr '\n' ' ')" "su-dummy-a su-dummy-d "
|
|
eq "su_rdeps_recursive(su-dummy-d) (leaf)" "$(su_rdeps_recursive su-dummy-d | tr '\n' ' ')" ""
|
|
|
|
sec "5. recursive installed dependency closure"
|
|
tm su_closure su-dummy-d > /tmp/clo_d.txt
|
|
echo " closure(su-dummy-d): $(wc -l </tmp/clo_d.txt) pkgs: $(head -c 300 /tmp/clo_d.txt | tr '\n' ' ')..."
|
|
for p in su-dummy-a su-dummy-b su-dummy-c glibc; do grep -qx "$p" /tmp/clo_d.txt && pass "closure(d) contains $p" || fail "closure(d) missing $p"; done
|
|
provider_sh=$(rpm -q --whatprovides --qf '%{NAME}\n' /bin/sh | head -n1); grep -qx "$provider_sh" /tmp/clo_d.txt && pass "closure(d) contains $provider_sh (provider of file dep /bin/sh)" || fail "closure(d) missing $provider_sh"
|
|
! grep -qx 'su-dummy-d' /tmp/clo_d.txt && pass "closure(d) excludes root" || fail "closure(d) contains root"
|
|
grep -q 'rpmlib' /tmp/clo_d.txt && fail "closure contains rpmlib" || pass "no rpmlib(...) leakage"
|
|
eq "closure(su-dummy-a)" "$(su_closure su-dummy-a | tr '\n' ' ')" "su-dummy-b "
|
|
if [ "$BACKEND" = dnf ] && [ "$(su_dnf_major)" = 4 ]; then
|
|
dnf -q repoquery --installed --requires --resolve --recursive --queryformat $'%{name}\n' su-dummy-d 2>/dev/null | sed '/^$/d' | sort -u > /tmp/clo_native.txt
|
|
eq "closure(d) == dnf4 repoquery --requires --resolve --recursive" "$(tr '\n' ' ' </tmp/clo_d.txt)" "$(tr '\n' ' ' </tmp/clo_native.txt)"
|
|
fi
|
|
if [ "$BACKEND" = dnf ] && [ "$(su_dnf_major)" = 5 ]; then
|
|
dnf -q repoquery --installed --requires --resolve --recursive su-dummy-d >/dev/null 2>&1; echo " dnf5 repoquery --requires --resolve --recursive -> rc=$? (unsupported)"
|
|
fi
|
|
|
|
sec "6. upgrade with per-run exclusion (a,b excluded) -- c must upgrade, a,b must stay"
|
|
if [ $BACKEND = dnf ]; then RESTRICT=(--disablerepo='*' --enablerepo=su-dummy); else RESTRICT=(); fi
|
|
LOCKS_BEFORE=$( [ -e /etc/zypp/locks ] && cksum < /etc/zypp/locks || echo absent )
|
|
tm su_upgrade --exclude su-dummy-a --exclude su-dummy-b -- "${RESTRICT[@]}" > /tmp/up_ex.log 2>&1; rc=$?
|
|
eq "exit code of excluded upgrade" "$rc" 0
|
|
eq "versions after excluded upgrade" "$(rpm -q --qf '%{NAME}=%{VERSION} ' su-dummy-a su-dummy-b su-dummy-c)" "su-dummy-a=1.0 su-dummy-b=1.0 su-dummy-c=2.0 "
|
|
if [ $BACKEND = zypper ]; then
|
|
LOCKS_AFTER=$( [ -e /etc/zypp/locks ] && cksum < /etc/zypp/locks || echo absent )
|
|
eq "/etc/zypp/locks untouched ($LOCKS_BEFORE)" "$LOCKS_AFTER" "$LOCKS_BEFORE"
|
|
check "no leftover temp zypp dirs" bash -c 'ls /tmp | grep -q "^tmp\." && exit 1 || exit 0'
|
|
fi
|
|
su_upgradable | cut -f1,2,3 | grep '^su-dummy' | sed 's/^/ still upgradable: /'
|
|
check "a and b still listed as upgradable" bash -c ". $D/lib-rpm.sh; su_upgradable | cut -f1 | grep -c '^su-dummy-[ab]$' | grep -qx 2"
|
|
echo "--- now without exclusions"
|
|
tm su_upgrade -- "${RESTRICT[@]}" > /tmp/up_all.log 2>&1; rc=$?; eq "exit code of plain upgrade" "$rc" 0
|
|
eq "versions after full upgrade" "$(rpm -q --qf '%{NAME}=%{VERSION} ' su-dummy-a su-dummy-b su-dummy-c)" "su-dummy-a=2.0 su-dummy-b=2.0 su-dummy-c=2.0 "
|
|
check "nothing left for su-dummy-*" test "$(su_upgradable | grep -c '^su-dummy')" -eq 0
|
|
|
|
sec "7. remove (+ unneeded deps)"
|
|
if [ $BACKEND = dnf ]; then dnf -y -q install su-dummy-r >/dev/null 2>&1; else zypper -n -q install su-dummy-r >/dev/null 2>&1; fi
|
|
eq "installed r and s" "$(rpm -q --qf '%{NAME} ' su-dummy-r su-dummy-s)" "su-dummy-r su-dummy-s "
|
|
su_remove su-dummy-r >/tmp/rm.log 2>&1; rc=$?; eq "remove exit code" "$rc" 0
|
|
eq "r AND its auto-installed dep s removed" "$(rpm -q su-dummy-r su-dummy-s 2>&1 | sed 's/ is not installed//;s/^package //' | tr '\n' ' ')" "su-dummy-r su-dummy-s "
|
|
su_remove su-dummy-a >/tmp/rm2.log 2>&1
|
|
eq "rpm-installed dep b NOT removed with a (not marked auto-installed)" "$(rpm -q su-dummy-b)" "su-dummy-b-2.0-1.noarch"
|
|
su_remove no-such-pkg-xyz >/tmp/rm3.log 2>&1; echo " remove of unknown pkg -> exit $?"
|
|
|
|
sec "8. fixture re-run resets state"
|
|
SU_FIXTURE_EXTRAS=1 bash "$D/fixture.sh" >/dev/null 2>&1; su_upgradable > /tmp/up2.tsv
|
|
check "after re-run a,b,c upgradable again" test "$(grep -c '^su-dummy-[abc] 1.0-1 2.0-1' /tmp/up2.tsv)" -eq 3
|
|
|
|
sec "9. plan preview (display only) incl. an obsoleting package"
|
|
TOP=/var/tmp/su-fixture; REPO=/var/lib/su-dummy-repo
|
|
mkp() { # mkp name ver [extra spec header lines...] -> builds noarch rpm, echoes path
|
|
local n=$1 v=$2; shift 2; local f=$TOP/SPECS/$n-$v.spec
|
|
{ echo "Name: $n"; echo "Version: $v"; echo "Release: 1"; echo "Summary: dummy $n"; echo "License: MIT"; echo "BuildArch: noarch"
|
|
local l; for l in "$@"; do echo "$l"; done
|
|
echo '%description'; echo "d"; echo '%install'; echo 'mkdir -p %{buildroot}/usr/share/su-dummy'; echo "echo $n > %{buildroot}/usr/share/su-dummy/$n"
|
|
echo '%files'; echo "/usr/share/su-dummy/$n"; } > "$f"
|
|
rpmbuild -bb --quiet --define "_topdir $TOP" "$f" >/dev/null 2>&1; ls "$TOP"/RPMS/noarch/$n-$v-1.*.rpm
|
|
}
|
|
rpm -Uvh "$(mkp su-dummy-old 1.0)" >/dev/null
|
|
cp "$(mkp su-dummy-new 1.0 'Obsoletes: su-dummy-old < 2' 'Provides: su-dummy-old = 2')" "$REPO"/ && createrepo_c --quiet --update "$REPO" >/dev/null
|
|
su_refresh
|
|
su_plan --exclude su-dummy-a > /tmp/plan.tsv; rc=$?; eq "su_plan exit code" "$rc" 0
|
|
echo " su_plan --exclude su-dummy-a (section name old new repo arch replaces), su-dummy rows only:"; grep -E $'\tsu-dummy-' /tmp/plan.tsv | sed 's/^/ /'
|
|
check "plan: b upgrade 1.0-1 -> 2.0-1" grep -q -x -F $'upgrade\tsu-dummy-b\t1.0-1\t2.0-1\tsu-dummy\tnoarch\t-' /tmp/plan.tsv
|
|
check "plan: excluded su-dummy-a is absent" test "$(grep -c $'\tsu-dummy-a\t' /tmp/plan.tsv)" -eq 0
|
|
check "plan: obsoleting install of su-dummy-new is visible" grep -q $'^install\tsu-dummy-new\t' /tmp/plan.tsv
|
|
echo " (upgradable list does NOT show the obsoleter:) $(su_upgradable | cut -f1 | grep -c '^su-dummy-new$') row(s) for su-dummy-new"
|
|
rpm -e su-dummy-old >/dev/null 2>&1
|
|
|
|
if [ $BACKEND = zypper ]; then
|
|
sec "10. zypper lock save/restore recipe (su_zypper_excluding_snapshot)"
|
|
zypper -n -q addlock -m "user lock" su-dummy-d >/dev/null
|
|
sum() { [ -e /etc/zypp/locks ] && { cksum < /etc/zypp/locks; stat -c %Y /etc/zypp/locks; } | tr '\n' ' ' || echo absent; }
|
|
B=$(sum); echo " before: $B"
|
|
su_zypper_excluding_snapshot su-dummy-a su-dummy-b -- "$(su_zypper_verb)" --dry-run > /tmp/snap.log 2>&1; rc=$?
|
|
eq "snapshot recipe exit code (dry-run)" "$rc" 0
|
|
eq "locks file byte-identical + same mtime after normal run" "$(sum)" "$B"
|
|
check "user lock still present afterwards" grep -q 'solvable_name: su-dummy-d' /etc/zypp/locks
|
|
check "temporary locks are gone" test "$(grep -c 'solvable_name: su-dummy-[ab]$' /etc/zypp/locks)" -eq 0
|
|
echo "--- why not addlock + removelock: a pre-existing user lock on the same name is destroyed"
|
|
zypper -n -q addlock su-dummy-a >/dev/null # the 'user' lock on su-dummy-a
|
|
zypper -n -q removelock su-dummy-a >/dev/null
|
|
check "NAIVE removelock destroyed the user's own lock on su-dummy-a (why we snapshot)" test "$(grep -c 'solvable_name: su-dummy-a$' /etc/zypp/locks)" -eq 0
|
|
zypper -n -q addlock su-dummy-a >/dev/null; B3=$(sum)
|
|
su_zypper_excluding_snapshot su-dummy-a -- "$(su_zypper_verb)" --dry-run >/dev/null 2>&1
|
|
eq "snapshot recipe keeps pre-existing lock on su-dummy-a" "$(sum)" "$B3"
|
|
echo "--- file absent before => absent after (removelock would leave a 0-byte file)"
|
|
rm -f /etc/zypp/locks
|
|
su_zypper_excluding_snapshot su-dummy-a -- "$(su_zypper_verb)" --dry-run >/dev/null 2>&1
|
|
eq "locks file absent after" "$(sum)" absent
|
|
echo "--- SIGTERM in the middle of the run => trap restores"
|
|
zypper -n -q addlock -m "user lock" su-dummy-d >/dev/null; B4=$(sum)
|
|
zypper() { if [ "$3" = "$(su_zypper_verb)" ] || [ "$2" = "$(su_zypper_verb)" ]; then kill -TERM $BASHPID; sleep 5; else command zypper "$@"; fi; }
|
|
su_zypper_excluding_snapshot su-dummy-a su-dummy-b -- "$(su_zypper_verb)" > /dev/null 2>&1; rc=$?
|
|
unset -f zypper
|
|
eq "killed run exit code (143 = 128+SIGTERM)" "$rc" 143
|
|
eq "locks restored after SIGTERM" "$(sum)" "$B4"
|
|
fi
|
|
|
|
TOTAL_MS=$(( ($(now)-T_START)/1000 ))
|
|
printf '\n== timings: fixture=%d ms refresh=%d ms list=%d ms total=%d ms\n' "$FIXTURE_MS" "$REFRESH_MS" "$LIST_MS" "$TOTAL_MS"
|
|
printf '== RESULT: %d failure(s)\n' "$FAILS"
|
|
exit "$FAILS"
|