primitives.test
#!/bin/bash
## Tests for the goal primitives: ensure_file, ensure_link, ensure_block, write_knob.
## Each is exercised through `pave run`, the way goal files use them.
# shellcheck source=tests/testlib.bash
source "${BASH_SOURCE[0]%/*}/testlib.bash"
G="$scratch/goals"
T="$scratch/t"
mkdir -p "$T"
## converge writes a manual-phase goal file and runs it.
##
## - @stdin the goal file.
converge() {
goals "$G/manual.bash"
pv run "$G" manual
}
# ---- ensure_file ----
converge <<EOF
ensure_file "$T/deep/er/f" 640 "line one
line two"
EOF
assert_has "ensure_file: creation reported" "pave: changed: created $T/deep/er/f"
assert_eq "ensure_file: content plus one newline" $'line one\nline two\n.' "$(exact "$T/deep/er/f")"
assert_eq "ensure_file: mode" 640 "$(stat -c %a "$T/deep/er/f")"
pv run "$G" manual
assert_eq "ensure_file: converged is quiet" "" "$out"
printf 'line one\nline two\n\n' >"$T/deep/er/f"
pv run "$G" manual
assert_has "ensure_file: an extra trailing newline is drift" "pave: changed: rewrote $T/deep/er/f"
chmod 600 "$T/deep/er/f"
pv run "$G" manual
assert_has "ensure_file: mode drift fixed alone" "pave: changed: chmod 640 $T/deep/er/f (was 600)"
assert_count "ensure_file: mode drift doesn't rewrite" "rewrote" 0
mkdir "$T/adir"
ln -s elsewhere "$T/alink"
converge <<EOF
ensure_file "$T/adir" 644 x
ensure_file "$T/alink" 644 x
ensure_file relative/path 644 x
ensure_file "$T/badmode" rw x
ensure_file "$T/toofew" 644
EOF
assert_eq "ensure_file refusals: exit 7" 7 "$rc"
assert_has "ensure_file: refuses a directory" "pave: error: $T/adir exists and is not a regular file"
assert_has "ensure_file: refuses a symlink" "pave: error: $T/alink exists and is not a regular file"
assert_has "ensure_file: refuses a relative path" "pave: error: 'relative/path' is not an absolute path"
assert_has "ensure_file: refuses a non-octal mode" "mode 'rw' is not octal"
assert_has "ensure_file: refuses wrong arg count" "usage: ensure_file PATH MODE CONTENT"
assert_count "ensure_file: each refusal recorded" "pave: FAIL" 5
assert_eq "ensure_file: symlink untouched" elsewhere "$(readlink "$T/alink")"
# ---- ensure_link ----
converge <<EOF
ensure_link ../target "$T/links/l"
EOF
assert_has "ensure_link: creation reported" "pave: changed: linked $T/links/l -> ../target"
assert_eq "ensure_link: exact target text" ../target "$(readlink "$T/links/l")"
pv run "$G" manual
assert_eq "ensure_link: converged is quiet" "" "$out"
ln -sfn /wrong "$T/links/l"
pv run "$G" manual
assert_has "ensure_link: repoint reported" "pave: changed: repointed $T/links/l -> ../target (was /wrong)"
assert_eq "ensure_link: repointed" ../target "$(readlink "$T/links/l")"
touch "$T/plainfile"
converge <<EOF
ensure_link x "$T/plainfile"
EOF
assert_has "ensure_link: refuses a non-symlink" "pave: error: $T/plainfile exists and is not a symlink"
assert "ensure_link: non-symlink untouched" test -f "$T/plainfile" -a ! -L "$T/plainfile"
# ---- ensure_block ----
B="$T/rc"
printf '#!/bin/sh -e\necho existing\n\nexit 0\n' >"$B"
chmod 751 "$B"
converge <<EOF
ensure_block "$B" top first "echo one
echo two"
EOF
assert_has "ensure_block: addition reported" "pave: changed: added block 'first' to $B"
assert_eq "ensure_block: top goes after the shebang" \
$'#!/bin/sh -e\n# >>> pave:first >>>\necho one\necho two\n# <<< pave:first <<<\necho existing\n\nexit 0' "$(<"$B")"
assert_eq "ensure_block: mode kept" 751 "$(stat -c %a "$B")"
snap=$(exact "$B")
pv run "$G" manual
assert_eq "ensure_block: converged is quiet" "" "$out"
assert_eq "ensure_block: converged file byte-identical" "$snap" "$(exact "$B")"
converge <<EOF
ensure_block "$B" top first "echo changed"
ensure_block "$B" bottom second "echo last"
EOF
assert_has "ensure_block: update reported" "pave: changed: updated block 'first' in $B"
assert_eq "ensure_block: updated in place, second at bottom, rest kept" \
$'#!/bin/sh -e\n# >>> pave:first >>>\necho changed\n# <<< pave:first <<<\necho existing\n\nexit 0\n# >>> pave:second >>>\necho last\n# <<< pave:second <<<' "$(<"$B")"
converge <<EOF
ensure_block "$B" top first "echo changed"
ensure_block "$B" top second "echo last"
EOF
assert_eq "ensure_block: moved to the requested end" \
$'#!/bin/sh -e\n# >>> pave:second >>>\necho last\n# <<< pave:second <<<\n# >>> pave:first >>>\necho changed\n# <<< pave:first <<<\necho existing\n\nexit 0' "$(<"$B")"
converge <<EOF
ensure_block "$T/fresh/rc" bottom b "x"
EOF
assert_eq "ensure_block: creates a missing file" $'# >>> pave:b >>>\nx\n# <<< pave:b <<<' "$(<"$T/fresh/rc")"
: >"$T/empty"
converge <<EOF
ensure_block "$T/empty" top b "x"
EOF
assert_eq "ensure_block: an empty file gains no blank line" $'# >>> pave:b >>>\nx\n# <<< pave:b <<<' "$(<"$T/empty")"
printf 'keep\n# >>> pave:b >>>\nhalf a block\n' >"$T/corrupt"
converge <<EOF
ensure_block "$T/corrupt" top b "x"
ensure_block "$T/empty" middle b "x"
EOF
assert_has "ensure_block: refuses a block with no end marker" "block 'b' has a begin marker but no end marker"
assert_eq "ensure_block: corrupt file untouched" $'keep\n# >>> pave:b >>>\nhalf a block' "$(<"$T/corrupt")"
assert_has "ensure_block: refuses an unknown placement" "placement must be top or bottom"
# ---- write_knob (against plain files; real sysfs can't be faked) ----
printf 'old\n' >"$T/knob"
converge <<EOF
write_knob "$T/knob" new
EOF
assert_has "write_knob: change reported with before and after" "pave: changed: $T/knob: old -> new (wrote new)"
pv run "$G" manual
assert_eq "write_knob: unchanged read-back is quiet" "" "$out"
converge <<EOF
write_knob "$T/no-such-knob" 1
EOF
assert_eq "write_knob: missing file fails" 7 "$rc"
# ---- primitives fail correctly even where errexit is off: in conditions ----
touch "$T/notadir"
converge <<EOF
if ensure_file "$T/notadir/f" 644 x; then echo WRONG-file; fi
if ensure_link x "$T/notadir/l"; then echo WRONG-link; fi
if ensure_block "$T/notadir/b" top b x; then echo WRONG-block; fi
if write_knob "$T/notadir/k" 1; then echo WRONG-knob; fi
( chain stops
ensure_file "$T/notadir/f" 644 x
echo WRONG-chain
)
EOF
assert_lacks "primitives: report failure inside conditions and chains" "WRONG"
assert_count "primitives: only the chain's failure is recorded" "pave: FAIL" 1
# An unreadable file is a failure where every later step would succeed, so only the guard can stop it.
# (Root reads anything, so this case only means something for other users.)
if [ "$EUID" -ne 0 ]; then
printf 'secret\n' >"$T/unreadable"
chmod 000 "$T/unreadable"
converge <<EOF
if ensure_file "$T/unreadable" 000 x; then echo WRONG-unreadable; fi
if ensure_block "$T/unreadable" top b x; then echo WRONG-unreadable; fi
EOF
assert_lacks "primitives: unreadable file fails, even in a condition" "WRONG"
assert_has "primitives: unreadable file explained" "pave: error: $T/unreadable is not readable"
chmod 600 "$T/unreadable"
assert_eq "primitives: unreadable file untouched" secret "$(<"$T/unreadable")"
fi
verdict
tests/primitives.test · 6.8 KiB · 7240887182