Files
bcherb2 a001406a33 test: container lab for the two-tier apply
A disposable ubuntu container, a fake private tier and a fake bootstrap
endpoint, so the whole documented path — chezmoi init --apply, dotup pick,
dotup private, cmp apply, dotsecrets — can run end to end without touching a
real machine or a real credential. The fake tier mirrors the real one's
structure (seven secrets and one alias) because dotsecrets is copied verbatim
and the "8 exports, not 7" assertion depends on that cardinality; its ids are
sequential and obviously synthetic.

check-verbatim.sh keeps the fake tier's copies of shipped files honest, and
snapshot.sh records file modes so a 644 where a 600 belongs is a diff.
2026-08-21 22:34:53 -04:00

311 lines
13 KiB
Bash

#!/bin/bash
# What a real user actually gets.
#
# The picker's ^a is `preset defaults`, and Enter is `install`. This scenario is
# that keystroke pair with nobody at the keyboard: tick the defaults, resolve
# every ticked row through dotup's own `resolve`, run dotup's own installer, and
# then ask the machine -- not the log -- whether each package is there.
#
# The log is not evidence. `apt-get install -y` prints a great deal and still
# leaves you without the package; a batch that fails silently retries one at a
# time and the second failure scrolls past the first. So every row is verified
# against the filesystem or the package database afterwards, and anything that
# did not land is RE-RUN ON ITS OWN through dotup, which does three things at
# once: it gets the exact command dotup would use, the exact exit code, and the
# exact first line of the error, with no other package's output interleaved.
#
# The re-run also settles the question a single pass cannot: a package that
# fails in the batch and installs on its own was a flake or a batch fault, not
# an unavailable package. Those come back as FLAKE-RECOVERED and are reported
# separately, so a transient 503 from a mirror is never filed as a bug.
#
# Output: a tab-separated table on stdout, one row per selected package.
# STATUS KEY CHANNEL ARG ISORC ERROR
set -u
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo " ok $*"; }
STATE=$HOME/.config/dotfiles
SEL=$STATE/selected
DOTUP=$HOME/.local/bin/dotup
LOG=/tmp/dotup-lab
mkdir -p "$LOG"
# ------------------------------------------------------------------ preamble -
echo "-- preamble --"
# ISSUE-1: the documented one-liner used to die on a stock box, because chezmoi's
# six git-repo externals need git and the image has none. Try it UNAIDED first --
# if the repo now supplies git itself, installing it here would hide that and
# hide any future regression. Only fall back to the hand-install, loudly, if the
# documented command still cannot stand on its own.
if sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >"$LOG/init.log" 2>&1; then
if grep -q 'installing git' "$LOG/init.log"; then
ok "ISSUE-1 fixed upstream: the repo installed git itself"
else
ok "chezmoi init --apply succeeded (git was already satisfied)"
fi
else
echo " NOTE ISSUE-1 still open: init --apply failed on a box with no git"
sudo apt-get update -qq >/dev/null 2>&1
sudo apt-get install -y -qq git >/dev/null 2>&1 || fail "could not install git"
echo " NOTE installed git by hand and retried -- see ISSUE-1"
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >"$LOG/init.log" 2>&1 \
|| { tail -20 "$LOG/init.log"; fail "chezmoi init --apply"; }
fi
[ -x "$DOTUP" ] || fail "no dotup at $DOTUP"
ok "public tier applied; dotup at $DOTUP"
# The tree under test moves while this suite is being written, so pin which
# build produced the table below. Two runs that disagree are only interesting
# if they ran the same code.
echo " dotup sha256 $(sha256sum "$DOTUP" | cut -c1-16) manifest sha256 $(sha256sum "$HOME/.local/share/dotup/packages.tsv" | cut -c1-16)"
# A user on a desktop has a display, and `preset defaults` ticks the gui rows
# only when one is present -- so ^a on a headless box selects a DIFFERENT set.
# Say which of the two we are testing instead of inheriting whatever the
# container happens to be.
export DISPLAY=:99
# --------------------------------------------------------------- selection --
echo "-- selection: ^a (preset defaults) --"
"$DOTUP" preset defaults || fail "preset defaults"
nsel=$(grep -c . "$SEL")
nsafe=$(awk -F'\t' '!/^[#@]/ && NF>=3 && $3=="safe"' "$HOME/.local/share/dotup/packages.tsv" | wc -l)
ngui=$(awk -F'\t' '!/^[#@]/ && NF>=3 && $3=="gui"' "$HOME/.local/share/dotup/packages.tsv" | wc -l)
echo " manifest: safe=$nsafe gui=$ngui -> expected $((nsafe + ngui))"
echo " selected: $nsel"
[ "$nsel" -eq $((nsafe + ngui)) ] || fail "defaults preset did not tick safe+gui"
ok "$nsel packages ticked"
# -------------------------------------------------------------- resolution --
# dotup's own resolver, one key at a time, so the table below is what dotup
# decided rather than what this scenario guessed.
echo "-- resolution (dotup resolve) --"
: > "$LOG/resolved.tsv"
while read -r key; do
[ -n "$key" ] || continue
line=$("$DOTUP" resolve "$key" 2>/dev/null)
IFS=$'\t' read -r ch arg <<<"$line"
[ -n "${arg:-}" ] || arg="-"
printf '%s\t%s\t%s\n' "$key" "${ch:-?}" "$arg" >> "$LOG/resolved.tsv"
done < "$SEL"
echo " channel spread:"
awk -F'\t' '{c[$2]++} END{for(k in c) printf " %-12s %d\n", k, c[k]}' "$LOG/resolved.tsv" | sort
"$DOTUP" plan >"$LOG/plan.log" 2>&1 || :
"$DOTUP" --print --yes install >"$LOG/print.log" 2>&1 || :
ok "plan and dry-run captured"
# ----------------------------------------------------------------- install --
echo "-- install: dotup --yes install (this takes a while) --"
t0=$(date +%s)
"$DOTUP" --yes install >"$LOG/install.log" 2>&1
irc=$?
t1=$(date +%s)
echo " exit $irc after $((t1 - t0))s, $(wc -l <"$LOG/install.log") lines of log"
echo "-- what dotup itself said did not install --"
sed -n '/did not install/,$p' "$LOG/install.log" | sed 's/\x1b\[[0-9;]*m//g' | sed 's/^/ | /'
# ------------------------------------------------------------ verification --
# Ask the machine, not the log.
UV=$(command -v uv 2>/dev/null || echo "$HOME/.local/bin/uv")
dpkg_ok() { [ "$(dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null)" = installed ]; }
# The same search path dotup's own find_tool uses, and for the same reason: a
# tool installed a moment ago is not on this process's PATH. Checking only PATH
# and ~/.local/bin reported chezmoi missing when get.chezmoi.io had put it in
# ~/bin -- a false failure against dotup for a fault in the check.
have_bin() {
local c
command -v "$1" >/dev/null 2>&1 && return 0
for c in "$HOME/.local/bin/$1" "$HOME/bin/$1" "$HOME/.npm-global/bin/$1" \
"/usr/local/bin/$1" "/usr/local/go/bin/$1" \
"/home/linuxbrew/.linuxbrew/bin/$1" "/opt/homebrew/bin/$1"; do
[ -x "$c" ] && return 0
done
return 1
}
npm_ok() {
local root
root=$(npm root -g 2>/dev/null) || return 1
[ -n "$root" ] && [ -e "$root/$1" ]
}
verify() {
local key=$1 ch=$2 arg=$3 n
# The bespoke channels land somewhere only their handler knows about.
case $key in
core/neovim) [ -x /opt/nvim/bin/nvim ]; return $? ;;
core/go) [ -x /usr/local/go/bin/go ]; return $? ;;
core/chezmoi) have_bin chezmoi; return $? ;;
core/uv) have_bin uv; return $? ;;
apps/chrome) dpkg_ok google-chrome-stable; return $? ;;
apps/ghostty) dpkg_ok ghostty; return $? ;;
esac
case $ch in
apt) for n in $arg; do dpkg_ok "$n" || return 1; done; return 0 ;;
brew) command -v brew >/dev/null 2>&1 || return 1
brew list --formula "$arg" >/dev/null 2>&1; return $? ;;
npm) for n in $arg; do npm_ok "$n" || return 1; done; return 0 ;;
uv) [ -x "$UV" ] || return 1
"$UV" tool list 2>/dev/null | grep -q "^$arg "; return $? ;;
# Both of these are bounded on purpose. `snap list` does not fail fast when
# snapd is installed but not running -- it blocks trying to reach a daemon
# systemd never started -- and a verification step that can hang forever is
# not a verification step.
snap) command -v snap >/dev/null 2>&1 || return 1
timeout 20 snap list "$arg" >/dev/null 2>&1; return $? ;;
flatpak) command -v flatpak >/dev/null 2>&1 || return 1
timeout 20 flatpak --user info "$arg" >/dev/null 2>&1 && return 0
timeout 20 flatpak info "$arg" >/dev/null 2>&1; return $? ;;
*) return 1 ;;
esac
}
# Re-run ONE key through dotup, with its own state directory so the selection is
# exactly that key. Everything -- resolution, channel dispatch, the command
# string, sudo -- is dotup's, so what comes back is dotup's behaviour for that
# package in isolation and not this scenario's reconstruction of it.
isolate() {
local key=$1 d rc
d=$(mktemp -d)
printf '%s\n' "$key" >"$d/selected"
: >"$d/expanded"
DOTUP_STATE=$d "$DOTUP" --yes install >"$LOG/iso.$(printf '%s' "$key" | tr / _).log" 2>&1
rc=$?
rm -rf "$d"
return $rc
}
isolog() { printf '%s/iso.%s.log' "$LOG" "$(printf '%s' "$1" | tr / _)"; }
# The first line that looks like a diagnosis, falling back to the last thing
# said. apt says "E:", npm says "npm error", brew and snap just say it.
firstline() {
local f=$1 l
l=$(sed 's/\x1b\[[0-9;]*m//g' "$f" 2>/dev/null \
| grep -m1 -aE 'E: |error|Error|ERROR|Unable to locate|not found|No such|Permission denied|refus|missing|unavailable|failed|Failed' )
[ -n "$l" ] || l=$(sed 's/\x1b\[[0-9;]*m//g' "$f" 2>/dev/null | grep -a . | tail -1)
printf '%s' "$l" | sed 's/^[[:space:]+]*//' | cut -c1-150
}
echo
echo "-- verification and per-package isolation --"
: > "$LOG/table.tsv"
while IFS=$'\t' read -r key ch arg; do
if verify "$key" "$ch" "$arg"; then
printf 'OK\t%s\t%s\t%s\t-\t-\n' "$key" "$ch" "$arg" >> "$LOG/table.tsv"
continue
fi
isolate "$key"; rc=$?
if verify "$key" "$ch" "$arg"; then
printf 'FLAKE-RECOVERED\t%s\t%s\t%s\t%s\t%s\n' \
"$key" "$ch" "$arg" "$rc" "$(firstline "$(isolog "$key")")" >> "$LOG/table.tsv"
else
printf 'FAIL\t%s\t%s\t%s\t%s\t%s\n' \
"$key" "$ch" "$arg" "$rc" "$(firstline "$(isolog "$key")")" >> "$LOG/table.tsv"
fi
done < "$LOG/resolved.tsv"
# --------------------------------------------------------------- the table --
echo
echo "===== RESULT TABLE ====="
printf 'STATUS\tKEY\tCHANNEL\tARG\tISORC\tERROR\n'
sort -k1,1r -k2,2 "$LOG/table.tsv"
echo "===== END RESULT TABLE ====="
echo
echo "===== COUNTS ====="
awk -F'\t' '{s[$1]++; if($1!="OK") c[$3]++} END{
for (k in s) printf "%-16s %d\n", k, s[k]
printf "\nfailures by channel:\n"
for (k in c) printf " %-10s %d\n", k, c[k] }' "$LOG/table.tsv"
printf 'total%12s %d\n' "" "$(grep -c . "$LOG/table.tsv")"
echo "===== END COUNTS ====="
echo
echo "===== RUNTIME SMOKE ====="
# Installed is not the same as usable, and the difference is not visible in any
# install log. apt's nodejs on 24.04 is 18.19.1; every npm row in this manifest
# declares node>=20. npm 9 only WARNS about a failed engine check, so the
# install exits 0 and leaves a tool that cannot start. A table that stopped at
# "the files are on disk" would score that as a success.
smoke() {
local b=$1 p="" c out rc first
shift
p=$(command -v "$b" 2>/dev/null) || p=""
if [ -z "$p" ]; then
for c in "$HOME/.local/bin/$b" "$HOME/bin/$b" "$HOME/.npm-global/bin/$b" \
"/usr/local/bin/$b" "/usr/local/go/bin/$b"; do
[ -x "$c" ] && { p=$c; break; }
done
fi
[ -n "$p" ] || { printf 'ABSENT\t%s\t-\t-\n' "$b"; return; }
out=$("$p" "$@" 2>&1); rc=$?
first=$(printf '%s' "$out" | sed 's/\x1b\[[0-9;]*m//g' | grep -a . | head -1 | cut -c1-110)
if [ "$rc" -eq 0 ]; then printf 'RUNS\t%s\t%s\t%s\n' "$b" "$rc" "$first"
else printf 'BROKEN\t%s\t%s\t%s\n' "$b" "$rc" "$first"; fi
}
printf 'STATUS\tBINARY\tRC\tFIRSTLINE\n'
smoke node --version
smoke npm --version
smoke nvim --version
smoke go version
smoke uv --version
smoke chezmoi --version
smoke rg --version
smoke fdfind --version
smoke batcat --version
smoke eza --version
smoke gh --version
smoke mmdc --version
smoke codex --version
smoke pi --version
smoke specify --help
smoke btop --version
smoke lazygit --version
smoke bw --version
echo "===== END RUNTIME SMOKE ====="
echo
echo "===== ISOLATION DETAIL (non-OK rows) ====="
awk -F'\t' '$1!="OK"{print $2}' "$LOG/table.tsv" | while read -r key; do
echo "--- $key ---"
sed 's/\x1b\[[0-9;]*m//g' "$(isolog "$key")" 2>/dev/null | grep -a . | tail -14 | sed 's/^/ /'
done
echo "===== END ISOLATION DETAIL ====="
echo
echo "===== DOTUP PLAN ====="
sed 's/\x1b\[[0-9;]*m//g' "$LOG/plan.log" | sed 's/^/ /'
echo "===== END DOTUP PLAN ====="
echo
echo "===== DRY RUN (dotup --print install) ====="
sed 's/\x1b\[[0-9;]*m//g' "$LOG/print.log" | sed 's/^/ /'
echo "===== END DRY RUN ====="
# A dry run reports what WOULD happen. It must never invent a failure that
# exists only because nothing ran. core/brew probed `have brew` after an
# install step that --print had merely printed, so a perfectly clean plan
# reported "brew still not found after installing it".
#
# This assertion has to live in a container. On a developer box `find_tool`
# probes /home/linuxbrew/.linuxbrew/bin by ABSOLUTE path and finds a real brew,
# so `have brew` succeeds and the bug cannot reproduce -- the same assertion in
# the fast suite passed with the guard deliberately removed.
invented=$(sed 's/\x1b\[[0-9;]*m//g' "$LOG/print.log" \
| sed -n '/did not install/,$p' | grep -E '^[[:space:]]+[a-z]+/[a-z]' || true)
if [ -n "$invented" ]; then
printf '%s\n' "$invented" | sed 's/^/ | /'
fail "the dry run reported failures for packages it never tried to install"
fi
ok "a dry run invents no failures"
nfail=$(awk -F'\t' '$1=="FAIL"' "$LOG/table.tsv" | wc -l)
echo
echo "MANIFEST-INSTALL: $nsel selected, $nfail did not install"
exit 0