a001406a33
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.
311 lines
13 KiB
Bash
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
|