Files
T
2026-10-10 10:37:53 +02:00

111 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# smoke.sh - proves that the command recipes documented in discovery/apt.md work, on top of fixture.sh.
# docker run --rm -i --network none -v "$PWD/discovery/apt:/d:ro" debian:12 bash /d/smoke.sh
# Runs fixture.sh in hermetic mode (SU_FIXTURE_OFFLINE=1, SU_FIXTURE_EXTRA=1) and then asserts.
set -uo pipefail
export LC_ALL=C DEBIAN_FRONTEND=noninteractive
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
fails=0
ok() { printf 'PASS %s\n' "$1"; }
bad() { printf 'FAIL %s\n got: %s\n expected: %s\n' "$1" "$2" "$3"; fails=$((fails + 1)); }
check() { # check LABEL GOT EXPECTED
if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "$2" "$3"; fi
}
ver() { dpkg-query -W -f '${Version}' "$1" 2>/dev/null; }
SU_FIXTURE_OFFLINE=1 SU_FIXTURE_EXTRA=1 bash "$HERE/fixture.sh" >/dev/null || { echo "fixture failed"; exit 2; }
echo "image: $(. /etc/os-release; echo "$PRETTY_NAME") apt: $(apt-get --version | head -1)"
# ---- 1. refresh: exit code is 0 and the unreachable-source case is made fatal by Error-Mode=any
apt-get update -qq -o APT::Update::Error-Mode=any >/dev/null 2>&1; check "update exit code" "$?" "0"
# ---- 2. upgradable list (Inst parse) + kept-back parse + apt list cross-check
list=$(apt-get -s --with-new-pkgs upgrade 2>/dev/null | awk -f "$HERE/parse-inst.awk" | sort)
check "Inst parse su-dummy-a" "$(printf '%s\n' "$list" | grep '^su-dummy-a')" "$(printf 'su-dummy-a\t1.0\t2.0\tsu-fixture:su-repo')"
check "Inst parse new package (old=-)" "$(printf '%s\n' "$list" | grep '^su-dummy-newdep')" "$(printf 'su-dummy-newdep\t-\t1.0\tsu-fixture:su-repo')"
kept=$(apt-get -s --with-new-pkgs upgrade 2>/dev/null | awk '/^The following packages have been kept back:/{k=1;next} k&&/^ /{for(i=1;i<=NF;i++)print $i;next} {k=0}' | tr '\n' ' ')
check "kept-back parse" "$kept" "su-dummy-e "
alist=$(apt list --upgradable 2>/dev/null | sed -nE 's#^([^/]+)/([^ ]+) ([^ ]+) ([^ ]+) \[upgradable from: ([^]]+)\]$#\1\t\5\t\3\t\2#p' | grep '^su-dummy-a')
check "apt list parse" "$alist" "$(printf 'su-dummy-a\t1.0\t2.0\tsu-repo')"
# ---- 3. description
desc=$(apt-cache show --no-all-versions su-dummy-a | awk '
/^Description(-[a-z][a-z](_[A-Z][A-Z])?)?:/ { p=1; sub(/^[^:]*: ?/,""); print; next }
p && /^ / { l=substr($0,2); print (l=="." ? "" : l); next }
p { exit }')
check "description first line" "$(printf '%s\n' "$desc" | head -1)" "dummy package su-dummy-a for skeptical-updater tests"
check "description line count" "$(printf '%s\n' "$desc" | wc -l | tr -d ' ')" "4"
# ---- 4. rdepends / closure
RD="--installed --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances"
rd=$(apt-cache rdepends $RD su-dummy-b | awk '/^Reverse Depends:/{p=1;next} p{sub(/^[ |]+/,""); print}' | sort -u | tr '\n' ' ')
check "rdepends su-dummy-b" "$rd" "su-dummy-a "
NI="--no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances"
installed_names() { dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\n' | awk -F'\t' 'substr($1,2,1) !~ /[nc]/ {print $2}' | sort -u; }
clos=$(comm -12 <(apt-cache depends --recurse $NI su-dummy-w | awk '/^[^ <]/' | sort -u) <(installed_names) | tr '\n' ' ')
check "closure su-dummy-w via virtual (no --installed)" "$clos" "su-dummy-v su-dummy-w "
clos_flag=$(apt-cache depends --recurse --installed $NI su-dummy-w | awk '/^[^ <]/' | sort -u | tr '\n' ' ')
check "closure su-dummy-w WITH --installed (documented defect: provider lost)" "$clos_flag" "su-dummy-w "
clos_dpkg=$(dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' | awk -F'\t' -v roots="su-dummy-w" -f "$HERE/closure-dpkg.awk" | sort -u | tr '\n' ' ')
check "closure su-dummy-w via dpkg awk" "$clos_dpkg" "su-dummy-v su-dummy-w "
# real package: base-files Pre-Depends on the virtual 'awk' (provided by the installed mawk)
real_dpkg=$(dpkg-query -W -f '${db:Status-Abbrev}\t${Package}\t${Pre-Depends}\t${Depends}\t${Provides}\n' | awk -F'\t' -v roots="base-files" -f "$HERE/closure-dpkg.awk" | sort -u | tr '\n' ' ')
case " $real_dpkg" in *" mawk "*) ok "closure-dpkg base-files reaches mawk through virtual awk" ;; *) bad "closure-dpkg base-files reaches mawk" "$real_dpkg" "... mawk ..." ;; esac
real_flag=$(apt-cache depends --recurse --installed $NI base-files | awk '/^[^ <]/' | sort -u | tr '\n' ' ')
case " $real_flag" in *" mawk "*) bad "apt-cache --recurse --installed base-files lacks mawk (documented defect)" "$real_flag" "no mawk" ;; *) ok "apt-cache --recurse --installed base-files lacks mawk (documented defect)" ;; esac
# ---- 5. hold / exclusion recipe + upgrade + restore
SU_STATE_DIR=$(mktemp -d); SU_HOLDS_ADDED=$SU_STATE_DIR/holds.added
su_holds_begin() {
mkdir -p "$SU_STATE_DIR" || return 1
local held p; held=$(apt-mark showhold) || return 1
: > "$SU_HOLDS_ADDED"
for p in "$@"; do
grep -qxF -- "$p" <<<"$held" && continue
echo "$p" >> "$SU_HOLDS_ADDED"
apt-mark hold "$p" >/dev/null || return 1
done
}
su_holds_restore() {
[ -f "$SU_HOLDS_ADDED" ] || return 0
apt-mark showhold | sort -u | comm -12 - <(sort -u "$SU_HOLDS_ADDED") | xargs -r apt-mark unhold >/dev/null
rm -f "$SU_HOLDS_ADDED"
}
apt-mark hold su-dummy-alt >/dev/null # pre-existing admin hold: must survive untouched
before=$(apt-mark showhold | sort | tr '\n' ' ')
su_holds_begin su-dummy-c su-dummy-alt
check "hold set during run" "$(apt-mark showhold | sort | tr '\n' ' ')" "su-dummy-alt su-dummy-c "
apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -o DPkg::Lock::Timeout=60 \
--with-new-pkgs upgrade >/dev/null 2>&1; check "upgrade exit code" "$?" "0"
check "held pkg skipped (c)" "$(ver su-dummy-c)" "1.0"
check "a upgraded" "$(ver su-dummy-a)" "2.0"; check "b upgraded" "$(ver su-dummy-b)" "2.0"
check "new dep installed via --with-new-pkgs" "$(ver su-dummy-newdep)" "1.0"
check "kept back e untouched" "$(ver su-dummy-e)" "1.0"
su_holds_restore
check "hold set restored exactly" "$(apt-mark showhold | sort | tr '\n' ' ')" "$before"
# crash recovery: journal survives, admin edits in between are respected
su_holds_begin su-dummy-b su-dummy-e; apt-mark hold su-dummy-w >/dev/null; apt-mark unhold su-dummy-alt >/dev/null # (no restore = simulated kill -9)
su_holds_restore
check "crash recovery releases only our holds, keeps admin edits" "$(apt-mark showhold | sort | tr '\n' ' ')" "su-dummy-w "
apt-mark unhold su-dummy-w >/dev/null
apt-get -y --with-new-pkgs upgrade >/dev/null 2>&1; check "second upgrade exit code" "$?" "0"
check "c upgraded after restore" "$(ver su-dummy-c)" "2.0"
noop=$(apt-get -y --with-new-pkgs upgrade 2>&1); rc=$?
check "nothing-to-do exit code" "$rc" "0"
check "nothing-to-do summary" "$(printf '%s\n' "$noop" | grep -E '^[0-9]+ upgraded')" "0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded."
# ---- 6. purge + autoremove
apt-mark auto su-dummy-b >/dev/null
apt-get -y purge su-dummy-a >/dev/null 2>&1; check "purge exit code" "$?" "0"
check "a purged (no dpkg status)" "$(dpkg-query -W -f '${db:Status-Abbrev}' su-dummy-a 2>/dev/null)" ""
check "autoremove --purge candidate" "$(apt-get -s autoremove --purge | awk '/^Purg /{print $2}' | tr '\n' ' ')" "su-dummy-b "
apt-get -y autoremove --purge >/dev/null 2>&1; check "autoremove exit code" "$?" "0"
check "b gone" "$(dpkg-query -W -f '${db:Status-Abbrev}' su-dummy-b 2>/dev/null)" ""
check "conffile of b gone" "$(ls /etc/su-dummy/su-dummy-b.conf 2>&1 | grep -c 'No such file')" "1"
rm -rf "$SU_STATE_DIR"
[ "$fails" -eq 0 ] && echo "ALL PASSED" || { echo "$fails FAILED"; exit 1; }