#!/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; }