test: seal the fake PATH instead of prepending it

The old harness put .tests/fakebin at the front of $PATH, which fails OPEN: a
package manager with no fake fell through to the real one, and an audit of this
suite invoked the host's actual `brew`. The twelve one-line fakes are replaced
by a single dispatcher every fake symlinks to, plus seal.sh, which builds the
WHOLE of $PATH for a run — real binaries for the pure tools, fakes for anything
that installs, downloads or needs root, and command-not-found for everything
else. Runs go under `env -i` so they inherit nothing.

Two audits ship with it: any `have`/`find_tool` probe the seal has never heard
of is an error, and the four absolute paths find_tool probes are written to
.shadowed so a test needing a tool to be genuinely absent can say so.

The dispatcher also fails on demand (FAKE_FAIL), which is what makes the
installers' failure paths reachable at all — the apt and npm one-at-a-time
retries, note_fail, and cmd_install's non-zero exit had all been unreachable,
and all three survived being deleted outright.

_curl stands in for every remote the installer talks to and for nothing else:
an unrecognised URL is a failed download, so a typo'd host shows up as the
failure it would really be. The fzf fake records its argv, which is what makes
the picker's bindings testable.
This commit is contained in:
bcherb2
2026-08-21 22:34:30 -04:00
parent 1ea6b492eb
commit 042320208e
15 changed files with 335 additions and 43 deletions
+96
View File
@@ -0,0 +1,96 @@
# Sourced by _fake when it is invoked as `curl`. Stands in for every remote the
# installer talks to, and for nothing else: a URL this file does not recognise
# is a failed download, not a silent success, so a typo'd host or a moved path
# shows up as the failure it would really be.
#
# Knobs, all set by the caller:
# FAKE_BOOT_BLOB file whose contents the bootstrap endpoint returns on 200
# FAKE_BOOT_PW the password that gets a 200; anything else gets a 401
# FAKE_BOOT_CODE force a code (404, 000, 500 ...) whatever the password
# FAKE_BWS_SUMS match (default) | mismatch | absent
# FAKE_FAIL=curl:deb | curl:chezmoi | curl:fzf fail just that one download
_fb=$(dirname "$0") # the fakebin directory
url=; out=; prev=; readcfg=0
for a in "$@"; do
case $prev in -o) out=$a ;; -K) [ "$a" = - ] && readcfg=1 ;; esac
case $a in http://*|https://*|get.chezmoi.io|*.io|*.sh) [ -z "$url" ] && url=$a ;; esac
prev=$a
done
cfg=
[ "$readcfg" = 1 ] && cfg=$(cat) # always drain: the writer is a pipe
emit() { if [ -n "$out" ]; then cat > "$out"; else cat; fi; }
# A tiny installer script, printed the way get.chezmoi.io and astral.sh print
# theirs. It installs the FAKE of the same name, so whatever it drops behaves
# like every other fake in this directory.
installer() { # $1 tool, $2 default dir
printf '%s\n' \
"b=$2" \
'while [ $# -gt 0 ]; do case $1 in -b) b=$2; shift 2 ;; *) shift ;; esac; done' \
"mkdir -p \"\$b\" && ln -sf $_fb/_fake \"\$b/$1\" && echo \"installed $1 to \$b\""
}
case $url in
*get.chezmoi.io*)
[ "$mode" = chezmoi ] && exit 22
installer chezmoi "\$HOME/.local/bin"; exit 0 ;;
*astral.sh/uv/install.sh*)
installer uv "\$HOME/.local/bin"; exit 0 ;;
*go.dev/VERSION*)
echo go1.99.0; exit 0 ;;
*api.github.com*)
echo ' "browser_download_url": "https://example.invalid/fake_amd64.deb"'; exit 0 ;;
# ------------------------------------------------------------------- fzf ----
# Only the real release path answers. The tarball is built here from the
# version in the URL, so `dotup preflight` reporting "0.74.2" is evidence that
# the pinned version travelled through the URL and into the binary -- not that
# a fixture happened to say so.
https://github.com/junegunn/fzf/releases/download/*)
[ "$mode" = fzf ] && exit 22
f=${url##*/} # fzf-0.74.2-linux_amd64.tar.gz
v=${f#fzf-}; v=${v%%-*}
d=$(mktemp -d); printf '#!/bin/sh\ncase ${1:-} in --version) echo "%s (fake)" ;; esac\nexit 0\n' "$v" > "$d/fzf"
chmod 755 "$d/fzf"; tar -czf - -C "$d" fzf; rm -rf "$d"; exit 0 ;;
# ------------------------------------------------------------------- bws ----
*/sdk-sm/releases/download/*bws-sha256-checksums-*)
case ${FAKE_BWS_SUMS:-match} in
absent) exit 22 ;;
mismatch) s=0000000000000000000000000000000000000000000000000000000000000000 ;;
*) s=$(printf 'PK-fake-bws-zip\n' | sha256sum | cut -d' ' -f1) ;;
esac
v=${url##*bws-sha256-checksums-}; v=${v%.txt}
for t in x86_64-unknown-linux-musl aarch64-unknown-linux-musl macos-universal; do
printf '%s bws-%s-%s.zip\n' "$s" "$t" "$v"
done | emit
exit 0 ;;
*/sdk-sm/releases/download/*.zip)
printf 'PK-fake-bws-zip\n' | emit; exit 0 ;;
# -------------------------------------------------------------- bootstrap ---
*/bootstrap.env)
pw=$(printf '%s\n' "$cfg" | sed -n 's/^user = "[^:]*:\(.*\)"$/\1/p' | head -1)
code=${FAKE_BOOT_CODE:-}
if [ -z "$code" ]; then
if [ "$pw" = "${FAKE_BOOT_PW:-}" ]; then code=200; else code=401; fi
fi
if [ "$code" = 200 ]; then cat "${FAKE_BOOT_BLOB:?FAKE_BOOT_BLOB unset}"; fi
printf '\n%s' "$code"
[ "$code" = 000 ] && exit 7
exit 0 ;;
esac
# --------------------------------------------------------------- anything ---
# A download to a file still has to produce the file; a download of something
# this stand-in has never heard of is a failure, which is the honest answer.
case $url in
*.deb|*.tar.gz|*.tgz)
[ "$mode" = deb ] && exit 22
[ -n "$out" ] && { : > "$out"; exit 0; }
exit 0 ;;
esac
[ -n "$out" ] && { : > "$out"; exit 0; }
exit 22
+130
View File
@@ -0,0 +1,130 @@
#!/bin/sh
# One dispatcher; every fake in this directory is a symlink to it.
#
# The old version of this file did two things wrong, and both let real bugs
# through:
#
# it always succeeded so no test could ever reach an installer's failure
# path -- the apt and npm one-at-a-time retries, the
# "did not install" report, and cmd_install's non-zero
# exit were all unreachable, and all three survived
# being deleted outright.
# it failed OPEN the fakes were PREPENDED to $PATH, so a package
# manager with no fake here fell through to the real
# one. An audit of this suite invoked the host's real
# `brew`. A fake that is missing must be an ERROR, not
# a silent hand-off to the machine.
#
# The second is fixed by seal.sh, which builds a directory holding exactly the
# tools a run may touch -- fakes for anything that installs, downloads or needs
# root, real binaries for the pure ones -- and runs dotup under `env -i` with
# that directory as the WHOLE of PATH. Anything not listed is command-not-found.
#
# ---------------------------------------------------------------- failure ---
# FAKE_FAIL is a comma-separated list of injections:
#
# FAKE_FAIL=brew brew exits 1 every time
# FAKE_FAIL=apt-get:batch apt-get fails only when asked for >1 package,
# which is what a real "unable to locate package"
# inside a batch looks like: the batch dies and
# the one-at-a-time retry is the only thing that
# saves the other thirty.
# FAKE_FAIL=npm:batch the same shape for npm.
# FAKE_FAIL=curl:deb curl fails only for the .deb download.
#
# Several may be combined: FAKE_FAIL=brew,apt-get:batch
set -u
me=${0##*/}
printf '%s %s\n' "$me" "$*" >> "${DOTUP_TEST_LOG:?DOTUP_TEST_LOG unset}"
# The mode this run injects for THIS tool: "always", a tool-specific word, or
# empty for "behave".
mode=
_oldifs=$IFS
IFS=,
for _e in ${FAKE_FAIL:-}; do
case $_e in
"$me") mode=always; break ;;
"$me":*) mode=${_e#*:}; break ;;
esac
done
IFS=$_oldifs
[ "$mode" = always ] && exit 1
# Count the package-ish arguments of a batch call: everything after the
# subcommand that is not a flag and not the flag's value.
count_pkgs() {
n=0
for a in "$@"; do
case $a in
-*|install|update|show|get|tool|-g|-y|--*) continue ;;
*=*) continue ;;
esac
n=$((n + 1))
done
printf '%s\n' "$n"
}
case $me in
apt-get)
case ${1:-} in update) exit 0 ;; esac
[ "$mode" = batch ] && [ "$(count_pkgs "$@")" -gt 1 ] && exit 1
exit 0 ;;
npm)
[ "$mode" = batch ] && [ "$(count_pkgs "$@")" -gt 1 ] && exit 1
exit 0 ;;
apt-cache)
# Stand in for a real apt cache. Two names are special, and both are real
# behaviours of stock Ubuntu that the installer has to tell apart:
#
# gh genuinely absent -- `apt-cache policy` prints nothing. The
# manifest's real example of a name that falls through to brew.
# docker-ce present in the cache but with `Candidate: (none)`, because
# something else Conflicts/Replaces it. `apt-cache show` exits
# 0 for this, which is why the old probe kept it in the batch
# and let apt refuse all thirty packages at once.
case ${1:-} in
policy)
case ${2:-} in
gh) exit 0 ;;
docker-ce) printf '%s:\n Installed: (none)\n Candidate: (none)\n' "$2"; exit 0 ;;
esac
printf '%s:\n Installed: (none)\n Candidate: 1.0-fake\n' "$2"; exit 0 ;;
esac
case ${2:-} in gh) exit 100 ;; esac
printf 'Package: %s\n' "${2:-}"; exit 0 ;;
unzip)
# `unzip -oq <zip> -d <dir>` -- produce the one member ensure_bws looks for.
d=; prev=
for a in "$@"; do [ "$prev" = -d ] && { d=$a; break; }; prev=$a; done
[ -n "$d" ] || exit 1
mkdir -p "$d" && printf '#!/bin/sh\necho "bws 2.1.0"\n' > "$d/bws" \
&& chmod 755 "$d/bws" && exit 0
exit 1 ;;
dpkg)
# install_deb fills %a in a gh: asset name from this.
case ${1:-} in --print-architecture) echo amd64 ;; esac
exit 0 ;;
chezmoi)
# `chezmoi init --apply --source DIR -c CFG URL`. Produce the shape the
# caller checks for -- a source tree with a .git in it -- and NOTHING else,
# at the caller's umask, so a missing `chmod -R go-rwx` is visible as a mode.
src=; prev=
for a in "$@"; do [ "$prev" = --source ] && { src=$a; break; }; prev=$a; done
[ -n "$src" ] || exit 0
mkdir -p "$src/.git" && : > "$src/.git/config" && : > "$src/README" || exit 1
exit 0 ;;
curl) . "$(dirname "$0")/_curl" ;;
fzf)
# The picker's own fzf, and the only fake here whose ARGV is the thing
# under test. `--bind` strings are built across a dozen continued lines, so
# one stray comment between them silently truncates the command and the
# picker draws with two binds instead of seven. Dump argv one argument per
# line -- a bind that lost its continuation cannot masquerade as one that
# survived -- and behave like an accept: read the piped rows, print nothing.
case ${1:-} in --version) echo "${FAKE_FZF_VERSION:-9.9.9} (fake)"; exit 0 ;; esac
[ -n "${FAKE_FZF_ARGV:-}" ] && printf '%s\n' "$@" > "$FAKE_FZF_ARGV"
cat > /dev/null
exit "${FAKE_FZF_RC:-0}" ;;
esac
exit 0
-32
View File
@@ -1,32 +0,0 @@
#!/bin/sh
# Every fake in this directory is a symlink to this file. It records the call
# and succeeds, so the installer can be driven end to end without a single
# package being installed, on this machine or in a container.
#
# `sudo` is faked too, and deliberately does NOT exec its argument: a run as
# root in a container would otherwise reach the real `rm -rf /opt/nvim`.
printf '%s %s\n' "${0##*/}" "$*" >> "${DOTUP_TEST_LOG:?DOTUP_TEST_LOG unset}"
case ${0##*/} in
apt-cache)
# Stand in for a real apt cache: everything is known except `gh`, which is
# genuinely absent from stock Ubuntu and is the manifest's real example of a
# name that has to fall through to brew.
case $2 in gh) exit 100 ;; esac
printf 'Package: %s\n' "$2"; exit 0 ;;
curl)
# Enough shape for the two bespoke handlers that parse a response.
for a in "$@"; do
case $a in
*go.dev/VERSION*) echo go1.99.0; exit 0 ;;
*api.github.com*) echo ' "browser_download_url": "https://example.invalid/fake_amd64.deb"'; exit 0 ;;
esac
done
prev=
for a in "$@"; do
[ "$prev" = "-o" ] && { : > "$a"; exit 0; }
prev=$a
done
exit 0 ;;
esac
exit 0
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
-1
View File
@@ -1 +0,0 @@
_log
Executable
+109
View File
@@ -0,0 +1,109 @@
#!/bin/sh
# Build a SEALED bin directory: the whole of $PATH for a run under test.
#
# sh seal.sh <dir> [--with name ...] [--without name ...]
#
# The old harness prepended .tests/fakebin to the caller's $PATH. That fails
# OPEN: a package manager with no fake here simply fell through to the real one,
# and an audit of this suite invoked the host's actual `brew`. The seal fails
# CLOSED. It holds exactly two kinds of thing --
#
# REAL pure, local, side-effect-free tools dotup is entitled to use: awk,
# sed, install, sha256sum, tar. Faking these would weaken the test;
# `sha256sum` in particular has to be the real one or the checksum
# comparison is checking a fixture against itself.
# FAKE anything that installs, downloads, or needs root. Symlinks to _fake.
#
# -- and nothing else. A tool that is neither is command-not-found, which is a
# visible failure rather than a silent hand-off to the machine.
#
# One hole cannot be closed from here and is reported rather than hidden:
# find_tool probes /usr/local/bin, /usr/local/go/bin, /home/linuxbrew/... and
# /opt/homebrew/bin by ABSOLUTE path, so on a developer box those directories
# can satisfy a lookup no matter what PATH says. The seal therefore ships a fake
# for every name that is shadowed there -- `command -v` is tried first, so the
# fake wins -- and writes the shadow list to <dir>/.shadowed so a test that
# needs a tool to be genuinely ABSENT can say so instead of quietly passing.
set -eu
# gzip is here because tar shells out to it: without it `tar xz` dies with
# "gzip: not found" -- which is the seal working, and is how it was found.
REAL='sh awk grep sed sort cut tr head tail id uname mkdir cp mv rm cat
dirname basename chmod mktemp find install sha256sum stty tar gzip ln
touch true false env expr wc'
# Deliberately NOT here by default, each for a reason a test depends on:
# sudo absent means SUDO='' and apt-get is invoked directly, which is the
# only way FAKE_FAIL=apt-get can reach the installer at all.
# fzf absent is what makes ensure_fzf actually fetch one.
# bws absent is what makes the private tier actually install it.
FAKE='curl apt-get apt-cache brew npm uv snap flatpak xcode-select
git dpkg unzip chezmoi'
OPTIONAL='sudo fzf bws nvim go node'
dir=${1:?usage: seal.sh <dir> [--with name ...] [--without name ...]}; shift
# Everything the seal KNOWS about, fixed before --without takes anything out: a
# hole punched on purpose is still an accounted-for tool, and must not trip the
# coverage audit below.
KNOWN="$REAL $FAKE $OPTIONAL"
mode=
for a in "$@"; do
case $a in
--with) mode=with ;;
--without) mode=without ;;
*) case $mode in
with) FAKE="$FAKE $a"; KNOWN="$KNOWN $a" ;;
without) FAKE=$(printf '%s\n' $FAKE | grep -vx "$a" | tr '\n' ' ') ;;
*) echo "seal.sh: stray argument $a" >&2; exit 2 ;;
esac ;;
esac
done
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
FB=$here/fakebin
[ -x "$FB/_fake" ] || { echo "seal.sh: no $FB/_fake" >&2; exit 1; }
rm -rf "$dir"; mkdir -p "$dir"
ln -s "$FB/_fake" "$dir/_fake"
ln -s "$FB/_curl" "$dir/_curl"
# Resolved by looking, not by `command -v`: an interactive shell reports
# aliases and functions, and a symlink to a bare name is a loop.
miss=
for t in $REAL; do
found=
for bd in /usr/bin /bin /usr/local/bin /sbin /usr/sbin; do
[ -x "$bd/$t" ] && { ln -sf "$bd/$t" "$dir/$t"; found=1; break; }
done
[ -n "$found" ] || miss="$miss $t"
done
[ -z "$miss" ] || { echo "seal.sh: host is missing real tools:$miss" >&2; exit 1; }
for t in $FAKE; do ln -sf "$dir/_fake" "$dir/$t"; done
# ------------------------------------------------------------- the audits ---
# 1. Anything dotup probes for must be accounted for. A new `have foo` with no
# fake would otherwise fall through to the host the moment someone adds one.
D=${DOTUP_SEAL_TARGET:-$here/../dot_local/bin/executable_dotup}
probes=$(grep -v '^[[:space:]]*#' "$D" \
| grep -oE '\b(have|find_tool) [a-z][a-z0-9-]*' \
| awk '{print $2}' | sort -u || :)
unknown=
for p in $probes; do
case " $KNOWN " in *" $p "*) ;; *) unknown="$unknown $p" ;; esac
done
[ -z "$unknown" ] || {
echo "seal.sh: dotup probes for tools the seal has never heard of:$unknown" >&2
echo " add them to REAL, FAKE or OPTIONAL -- do not let them fall through" >&2
exit 1; }
# 2. The absolute-path hole, reported rather than papered over. $KNOWN, not
# $FAKE: a name taken out with --without is exactly the name a test wants to
# be absent, so it is the one that most needs checking.
: > "$dir/.shadowed"
for t in $KNOWN; do
for bd in /usr/local/bin /usr/local/go/bin /home/linuxbrew/.linuxbrew/bin /opt/homebrew/bin; do
[ -x "$bd/$t" ] || continue
[ -e "$dir/$t" ] || printf '%s\t%s\n' "$t" "$bd/$t" >> "$dir/.shadowed"
done
done
printf '%s\n' "$dir"