pave

run.test

#!/bin/bash
## Tests for `pave run`: phase and host file selection, failure semantics, chains, reports.

# shellcheck source=tests/testlib.bash
source "${BASH_SOURCE[0]%/*}/testlib.bash"

# ---- file selection: common file, then this host's file, and no other host's ----

G="$scratch/select"
goals "$G/manual.bash" <<<'echo common-ran'
goals "$G/boot.bash" <<<'echo boot-ran'
goals "$G/hosts/testhost/manual.bash" <<<'echo host-ran'
goals "$G/hosts/otherhost/manual.bash" <<<'echo otherhost-ran'
pv run "$G" manual
assert_eq "selection: exit 0" 0 "$rc"
assert_eq "selection: common then host, nothing else" $'common-ran\nhost-ran' "$out"

# ---- top-level failures are recorded and the file carries on ----

G="$scratch/toplevel"
goals "$G/manual.bash" <<'EOF'
false
echo after-first
x=$(exit 3)
echo after-cmdsubst
false
EOF
pv run "$G" manual
assert_eq "toplevel: exit 7" 7 "$rc"
assert_has "toplevel: failure located" "pave: FAIL: exit 1 at ./manual.bash:1"
assert_has "toplevel: carried on after failure" "after-first"
assert_has "toplevel: cmdsubst failure located" "pave: FAIL: exit 3 at ./manual.bash:3"
assert_has "toplevel: carried on after cmdsubst" "after-cmdsubst"
assert_has "toplevel: last-line failure located" "pave: FAIL: exit 1 at ./manual.bash:5"
assert_count "toplevel: each failure recorded once" "pave: FAIL" 3
assert_has "toplevel: summary counts" "pave: manual: 0 changed, 3 failed"

# ---- chains stop at their first failure; the file carries on after them ----

G="$scratch/chains"
goals "$G/manual.bash" <<'EOF'
( chain first
	echo first-a
	false
	echo first-b-SHOULD-NOT-RUN
)
echo between
helper() {
	echo helper-a
	false
	echo helper-b-SHOULD-NOT-RUN
}
( chain viafunc
	helper
	echo viafunc-SHOULD-NOT-RUN
)
inner() {
	( chain inner
		false
	)
}
( chain outer
	inner
	echo outer-SHOULD-NOT-RUN
)
( chain ok
	echo ok-a
	true
	echo ok-b
)
EOF
pv run "$G" manual
assert_eq "chains: exit 7" 7 "$rc"
assert_lacks "chains: nothing after a failure ran" "SHOULD-NOT-RUN"
assert_has "chains: failure named and located" "pave: FAIL: chain first: exit 1 at ./manual.bash:3"
assert_has "chains: file carried on" "between"
assert_has "chains: failure inside a called function stops the chain" "helper-a"
assert_has "chains: nested failure recorded at the innermost chain" "pave: FAIL: chain inner: exit 1"
assert_count "chains: each failed chain recorded once" "pave: FAIL" 3
assert_has "chains: a chain that doesn't fail runs to the end" "ok-b"

# ---- chain outside a subshell is refused, and doesn't switch on errexit ----

G="$scratch/badchain"
goals "$G/manual.bash" <<'EOF'
chain oops
false
echo still-running
( chain )
EOF
pv run "$G" manual
assert_eq "bad chain: exit 7" 7 "$rc"
assert_has "bad chain: refusal explained" "chain oops: not inside ( ... )"
assert_has "bad chain: refusal recorded" "pave: FAIL: exit 1 at ./manual.bash:1"
assert_has "bad chain: errexit stayed off" "still-running"
assert_has "bad chain: nameless chain refused" "usage: ( chain NAME ... )"

# ---- whole-file aborts: unset variables, syntax errors ----

G="$scratch/aborts"
goals "$G/manual.bash" <<'EOF'
echo before-unset
echo "$NO_SUCH_VARIABLE"
echo after-unset-SHOULD-NOT-RUN
EOF
goals "$G/hosts/testhost/manual.bash" <<'EOF'
echo syntax-SHOULD-NOT-RUN
if then fi
EOF
pv run "$G" manual
assert_eq "aborts: exit 7" 7 "$rc"
assert_has "aborts: ran up to the unset variable" "before-unset"
assert_lacks "aborts: nothing ran after an abort, or in a file with a syntax error" "SHOULD-NOT-RUN"
assert_has "aborts: unset variable aborts the file" "pave: FAIL: ./manual.bash: aborted"
assert_has "aborts: syntax error skips the file" "pave: FAIL: ./hosts/testhost/manual.bash: syntax error"

# ---- usage errors, and misnamed phase files ----

pv run "$scratch/select" sesion
assert_eq "unknown phase argument: exit 4" 4 "$rc"
assert_has "unknown phase argument: lists phases" "boot session login manual"
pv run "$scratch/no-such-dir" manual
assert_eq "missing goals dir: exit 5" 5 "$rc"

G="$scratch/misnamed"
goals "$G/manual.bash" <<<'echo manual-ran'
goals "$G/hosts/testhost/sesion.bash" <<<'true'
pv run "$G" manual
assert_eq "misnamed phase file: exit 6" 6 "$rc"
assert_has "misnamed phase file: named" "pave: FAIL: ./hosts/testhost/sesion.bash: not named for a phase"
assert_has "misnamed phase file: goals still ran" "manual-ran"

# ---- the environment goal files run in ----

G="$scratch/env"
goals "$G/manual.bash" <<'EOF'
echo "cwd=$PWD"
if read -r line; then echo stdin-had-data; else echo stdin-empty; fi
EOF
out=$(echo surprise | "$PAVE" run "$G" manual 2>&1)
assert_has "env: cwd is the goals dir" "cwd=$G"
assert_has "env: stdin is empty" "stdin-empty"

# ---- reports, and quiet convergence ----

G="$scratch/quiet"
T="$scratch/quiet-target"
goals "$G/manual.bash" <<EOF
ensure_file "$T" 644 hello
EOF
pv run "$G" manual
assert_has "report: first run reports the change" "pave: changed: created $T"
assert_has "report: first run prints a summary" "pave: manual: 1 changed, 0 failed"
pv run "$G" manual
assert_eq "report: converged run exits 0" 0 "$rc"
assert_eq "report: converged run prints nothing" "" "$out"
report="$HOME/.local/state/pave/manual.report"
assert "report: written under the XDG state dir" test -f "$report"
assert_eq "report: holds the summary even when quiet" \
	"pave: manual: 0 changed, 0 failed (goals: $G, host: testhost)" "$(<"$report")"
assert "report: no scratch files left behind" \
	test "$(cd "${report%/*}" && printf '%s ' .[!.]* *)" = ".[!.]* manual.report "

XDG_STATE_HOME="$scratch/state" "$PAVE" run "$G" manual
assert "report: XDG_STATE_HOME honored" test -f "$scratch/state/pave/manual.report"

pv run "$scratch/select" session
assert_eq "no goal files for a phase: exit 0" 0 "$rc"
assert_eq "no goal files for a phase: quiet" "" "$out"

verdict

tests/run.test · 5.7 KiB · 7240887182