Test: Exercise updates through the real command
This commit is contained in:
@@ -37,7 +37,8 @@ tmp="$(mktemp -d -t panama-update-contract.XXXXXX)"
|
|||||||
trap 'rm -rf "$tmp"' EXIT
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
|
STAGE_NAMES=(install-packages link-dotfiles link-skills link-user change-settings
|
||||||
link-vicinae-scripts setup-identity install-hardware)
|
link-vicinae-scripts setup-server link-server setup-identity
|
||||||
|
install-hardware)
|
||||||
|
|
||||||
# A PANAMA_PATH that looks enough like the real one for install to run, and
|
# A PANAMA_PATH that looks enough like the real one for install to run, and
|
||||||
# records what it was asked to do instead of doing it.
|
# records what it was asked to do instead of doing it.
|
||||||
@@ -108,14 +109,13 @@ EOF
|
|||||||
# Run the fixture installer and echo what ran, one stage per line.
|
# Run the fixture installer and echo what ran, one stage per line.
|
||||||
run_install() {
|
run_install() {
|
||||||
local root="$1"; shift
|
local root="$1"; shift
|
||||||
|
local status=0
|
||||||
: >"$root/ran"
|
: >"$root/ran"
|
||||||
PATH="$root/shim:$PATH" \
|
PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
|
||||||
PANAMA_PATH="$root" \
|
XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
|
||||||
PANAMA_RAN="$root/ran" \
|
>"$root/out" 2>&1 || status=$?
|
||||||
XDG_STATE_HOME="$root/state" \
|
|
||||||
bash "$root/install" "$@" >"$root/out" 2>&1
|
|
||||||
printf '%s' "$?" >"$root/rc"
|
|
||||||
cat "$root/ran"
|
cat "$root/ran"
|
||||||
|
return "$status"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 1. The interview never runs on an upgrade ────────────────────────────────
|
# ── 1. The interview never runs on an upgrade ────────────────────────────────
|
||||||
@@ -146,12 +146,28 @@ done
|
|||||||
|
|
||||||
# A stage added to STAGES without a decision about which path owns it shows up
|
# A stage added to STAGES without a decision about which path owns it shows up
|
||||||
# here, because this list is written down twice on purpose.
|
# here, because this list is written down twice on purpose.
|
||||||
mapfile -t declared < <(python3 - "$installer" <<'PY'
|
declared=()
|
||||||
import re, sys
|
if declared_output="$(python3 - "$installer" <<'PY'
|
||||||
line = next(l for l in open(sys.argv[1], encoding="utf-8") if l.startswith("STAGES="))
|
import re
|
||||||
print("\n".join(re.findall(r"[\w-]+", line)[1:]))
|
import sys
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for line in open(sys.argv[1], encoding="utf-8"):
|
||||||
|
match = re.match(r'^\s*STAGES=\((.*)\)\s*$', line)
|
||||||
|
if match and "$" not in match.group(1):
|
||||||
|
found = True
|
||||||
|
print("\n".join(re.findall(r"[\w-]+", match.group(1))))
|
||||||
|
if not found:
|
||||||
|
print("install has no STAGES assignment", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
PY
|
PY
|
||||||
)
|
)"; then
|
||||||
|
while IFS= read -r stage; do
|
||||||
|
[[ -n "$stage" ]] && declared+=("$stage")
|
||||||
|
done <<<"$declared_output"
|
||||||
|
else
|
||||||
|
note 'could not read the install STAGES assignment'
|
||||||
|
fi
|
||||||
for stage in "${declared[@]}"; do
|
for stage in "${declared[@]}"; do
|
||||||
printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \
|
printf '%s\n' "${STAGE_NAMES[@]}" | grep -qx "$stage" \
|
||||||
|| note "install declares a stage this contract has never heard of: $stage"
|
|| note "install declares a stage this contract has never heard of: $stage"
|
||||||
@@ -177,7 +193,10 @@ grep -qx 'install-packages' <<<"$ran_changed" \
|
|||||||
|
|
||||||
# A failing stage must not record the hash, or the failure is hidden forever.
|
# A failing stage must not record the hash, or the failure is hidden forever.
|
||||||
build_fixture "$tmp/c" 1
|
build_fixture "$tmp/c" 1
|
||||||
run_install "$tmp/c" --upgrade >/dev/null
|
install_status=0
|
||||||
|
run_install "$tmp/c" --upgrade >/dev/null || install_status=$?
|
||||||
|
[[ "$install_status" -eq 1 ]] \
|
||||||
|
|| note "install --upgrade returned $install_status instead of the failing stage status"
|
||||||
if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then
|
if [[ -r "$tmp/c/state/panama/packages-hash" ]]; then
|
||||||
note 'install-packages failed but its hash was recorded, so it will never be retried'
|
note 'install-packages failed but its hash was recorded, so it will never be retried'
|
||||||
fi
|
fi
|
||||||
@@ -191,62 +210,120 @@ grep -qx 'install-packages' <<<"$ran_full" \
|
|||||||
|
|
||||||
# ── 4. A conflicted pop never leaves markers in a live config ────────────────
|
# ── 4. A conflicted pop never leaves markers in a live config ────────────────
|
||||||
#
|
#
|
||||||
# Two halves. The first checks that git still behaves the way the design
|
# The fixture also covers a clean fast-forward, installer status propagation,
|
||||||
# depends on; the second checks that panama acts on it. Neither is worth much
|
# and the boundary between update and sync before forcing the conflict below.
|
||||||
# without the other.
|
|
||||||
|
|
||||||
conflict="$tmp/conflict"
|
# Each fixture has the same three repositories as a real update: a bare remote,
|
||||||
mkdir -p "$conflict"
|
# a clone that publishes upstream changes, and the machine clone being updated.
|
||||||
(
|
build_cli_fixture() (
|
||||||
set -e
|
set -e
|
||||||
cd "$conflict"
|
local root="$1"
|
||||||
git init -q up && cd up
|
rm -rf "$root"
|
||||||
git config user.email contract@panama && git config user.name contract
|
mkdir -p "$root"
|
||||||
printf 'one\n' >f; git add -A; git commit -qm one
|
git init -q --bare "$root/origin.git"
|
||||||
cd "$conflict"; git clone -q up work; cd work
|
git clone -q "$root/origin.git" "$root/upstream" 2>/dev/null
|
||||||
git config user.email contract@panama && git config user.name contract
|
git -C "$root/upstream" config user.email contract@panama
|
||||||
cd "$conflict/up"; printf 'upstream\n' >f; git commit -qam two
|
git -C "$root/upstream" config user.name contract
|
||||||
cd "$conflict/work"; printf 'local\n' >f
|
|
||||||
git stash push --include-untracked -m contract >/dev/null
|
|
||||||
git pull -q --ff-only
|
|
||||||
git stash pop >/dev/null 2>&1 && exit 3 # a conflict was the point
|
|
||||||
git reset -q --hard HEAD
|
|
||||||
[[ -n "$(git stash list)" ]] || exit 4 # the stash must survive
|
|
||||||
grep -q '<<<<<<<' f && exit 5 # and no markers may remain
|
|
||||||
exit 0
|
|
||||||
) >/dev/null 2>&1
|
|
||||||
case $? in
|
|
||||||
0) ;;
|
|
||||||
3) note 'the conflict fixture did not conflict, so this check proves nothing' ;;
|
|
||||||
4) note 'git no longer keeps the stash after a conflicted pop; panama update would lose work' ;;
|
|
||||||
5) note 'git reset --hard left conflict markers behind' ;;
|
|
||||||
*) note 'the stash conflict fixture could not be built' ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# panama update must act on that: reset the tree rather than leave the markers.
|
mkdir -p "$root/upstream/bin"
|
||||||
body="$(sed -n '/^cmd_update()/,/^}/p' "$panama")"
|
cp "$panama" "$root/upstream/bin/panama"
|
||||||
if [[ -z "$body" ]]; then
|
cat >"$root/upstream/install" <<'EOF'
|
||||||
note 'bin/panama has no cmd_update to check'
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"${PANAMA_UPDATE_FIXTURE_LOG:?}"
|
||||||
|
exit "${PANAMA_UPDATE_INSTALL_RC:-0}"
|
||||||
|
EOF
|
||||||
|
chmod +x "$root/upstream/bin/panama" "$root/upstream/install"
|
||||||
|
printf 'one\n' >"$root/upstream/f"
|
||||||
|
git -C "$root/upstream" add -A
|
||||||
|
git -C "$root/upstream" commit -qm initial
|
||||||
|
git -C "$root/upstream" push -qu origin HEAD
|
||||||
|
|
||||||
|
git clone -q "$root/origin.git" "$root/machine"
|
||||||
|
git -C "$root/machine" config user.email contract@panama
|
||||||
|
git -C "$root/machine" config user.name contract
|
||||||
|
)
|
||||||
|
|
||||||
|
advance_upstream() (
|
||||||
|
set -e
|
||||||
|
local root="$1" file="$2" contents="$3"
|
||||||
|
printf '%s\n' "$contents" >"$root/upstream/$file"
|
||||||
|
git -C "$root/upstream" add "$file"
|
||||||
|
git -C "$root/upstream" commit -qm "update $file"
|
||||||
|
git -C "$root/upstream" push -q
|
||||||
|
)
|
||||||
|
|
||||||
|
clean="$tmp/clean-update"
|
||||||
|
if build_cli_fixture "$clean" && advance_upstream "$clean" release new; then
|
||||||
|
machine_before="$(git -C "$clean/machine" rev-parse HEAD)"
|
||||||
|
upstream_after="$(git -C "$clean/upstream" rev-parse HEAD)"
|
||||||
|
[[ "$machine_before" != "$upstream_after" ]] \
|
||||||
|
|| note 'the clean update fixture started current, so it cannot prove a fast-forward'
|
||||||
|
|
||||||
|
: >"$clean/install.log"
|
||||||
|
update_status=0
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
||||||
|
"$clean/machine/bin/panama" update >"$clean/update.out" 2>&1 \
|
||||||
|
|| update_status=$?
|
||||||
|
[[ "$update_status" -eq 0 ]] \
|
||||||
|
|| note "panama update failed on a clean clone with status $update_status"
|
||||||
|
[[ "$(git -C "$clean/machine" rev-parse HEAD)" == "$upstream_after" ]] \
|
||||||
|
|| note 'panama update did not fast-forward the clean machine clone'
|
||||||
|
grep -qx -- '--upgrade' "$clean/install.log" \
|
||||||
|
|| note 'panama update did not invoke the installer with --upgrade'
|
||||||
|
|
||||||
|
: >"$clean/install.log"
|
||||||
|
update_status=0
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" PANAMA_UPDATE_INSTALL_RC=23 \
|
||||||
|
"$clean/machine/bin/panama" update >"$clean/failing-update.out" 2>&1 \
|
||||||
|
|| update_status=$?
|
||||||
|
[[ "$update_status" -eq 23 ]] \
|
||||||
|
|| note "panama update returned $update_status instead of installer status 23"
|
||||||
|
grep -qx -- '--upgrade' "$clean/install.log" \
|
||||||
|
|| note 'the failing update did not reach the fixture installer'
|
||||||
|
|
||||||
|
: >"$clean/install.log"
|
||||||
|
printf 'local sync\n' >"$clean/machine/synced"
|
||||||
|
sync_status=0
|
||||||
|
printf 'y\ncontract sync\n' \
|
||||||
|
| PANAMA_UPDATE_FIXTURE_LOG="$clean/install.log" \
|
||||||
|
"$clean/machine/bin/panama" sync >"$clean/sync.out" 2>&1 \
|
||||||
|
|| sync_status=$?
|
||||||
|
[[ "$sync_status" -eq 0 ]] \
|
||||||
|
|| note "panama sync failed in the local fixture with status $sync_status"
|
||||||
|
[[ ! -s "$clean/install.log" ]] \
|
||||||
|
|| note 'panama sync invoked the installer; sync and update are separate jobs'
|
||||||
else
|
else
|
||||||
grep -q 'git stash push' <<<"$body" \
|
note 'the clean update fixture could not be built'
|
||||||
|| note 'cmd_update does not stash local changes, so a pull can fail on a dirty tree'
|
|
||||||
grep -q 'git reset --hard' <<<"$body" \
|
|
||||||
|| note 'cmd_update does not reset after a failed pop, so conflict markers reach ~/.config'
|
|
||||||
grep -q 'git pull --ff-only' <<<"$body" \
|
|
||||||
|| note 'cmd_update does not pull with --ff-only'
|
|
||||||
grep -q -- '--upgrade' <<<"$body" \
|
|
||||||
|| note 'cmd_update does not hand off to install --upgrade, so it would ask the interview'
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The two verbs stay separate: sync must never run the installer.
|
# A conflicted stash pop must not leave markers in the checkout. The failed pop
|
||||||
sync_body="$(sed -n '/^cmd_sync()/,/^}/p' "$panama")"
|
# keeps the stash, so the local version remains recoverable after the reset.
|
||||||
if [[ -z "$sync_body" ]]; then
|
conflict="$tmp/conflict-update"
|
||||||
note 'bin/panama has no cmd_sync, so the git workflow lost its home'
|
if build_cli_fixture "$conflict"; then
|
||||||
|
printf 'local\n' >"$conflict/machine/f"
|
||||||
|
if advance_upstream "$conflict" f upstream; then
|
||||||
|
: >"$conflict/install.log"
|
||||||
|
conflict_status=0
|
||||||
|
PANAMA_UPDATE_FIXTURE_LOG="$conflict/install.log" \
|
||||||
|
"$conflict/machine/bin/panama" update >"$conflict/update.out" 2>&1 \
|
||||||
|
|| conflict_status=$?
|
||||||
|
[[ "$conflict_status" -eq 0 ]] \
|
||||||
|
|| note "panama update failed while recovering a stash conflict with status $conflict_status"
|
||||||
|
[[ "$(<"$conflict/machine/f")" == upstream ]] \
|
||||||
|
|| note 'panama update did not reset the conflicted file to the upstream version'
|
||||||
|
if git -C "$conflict/machine" grep -qE '^(<<<<<<<|=======|>>>>>>>)' -- .; then
|
||||||
|
note 'panama update left conflict markers in the machine checkout'
|
||||||
|
fi
|
||||||
|
[[ -n "$(git -C "$conflict/machine" stash list)" ]] \
|
||||||
|
|| note 'panama update dropped the stash after its conflicted pop'
|
||||||
|
recovered="$(git -C "$conflict/machine" show 'stash@{0}:f' 2>/dev/null)"
|
||||||
|
[[ "$recovered" == local ]] \
|
||||||
|
|| note 'the stash left by panama update does not contain the local version'
|
||||||
|
else
|
||||||
|
note 'the conflict fixture could not publish its upstream edit'
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
grep -q 'install' <<<"$sync_body" \
|
note 'the conflict update fixture could not be built'
|
||||||
&& note 'cmd_sync runs the installer; committing and updating are separate jobs'
|
|
||||||
grep -q 'git stash' <<<"$sync_body" \
|
|
||||||
&& note 'cmd_sync stashes, which the commit-before-pull order exists to avoid'
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( ${#findings[@]} > 0 )); then
|
if (( ${#findings[@]} > 0 )); then
|
||||||
|
|||||||
Reference in New Issue
Block a user