mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +09:00
The shell translator rewrites test bodies and loses Bats reporting. The unit lane needs complete TAP results and JUnit reports without changing how live-cluster suites run. Sourced production helpers still need a dash pass, and unit test bodies need nounset without exporting shell options into subprocesses. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
703 lines
29 KiB
Bash
703 lines
29 KiB
Bash
#!/usr/bin/env bats
|
|
# -----------------------------------------------------------------------------
|
|
# The ban on EXIT-trap cleanup in hack/*.bats, and the debt each file that still
|
|
# breaks it declares for itself.
|
|
#
|
|
# Why the ban. An EXIT trap inside an @test body replaces the one the bats binary
|
|
# installs for its own bookkeeping, and a test that then FAILS prints no TAP line
|
|
# at all. Not `not ok` -- nothing. The run ends with a warning that it executed
|
|
# fewer tests than it planned, so anyone reading the tail of the output, or
|
|
# grepping it for `not ok`, sees a green suite. Not hypothetical:
|
|
# hack/promote-retag_test.bats reported five passes and no failures while six of
|
|
# its eleven tests printed nothing. Clean up at the end of the test body instead.
|
|
# Both runners set -e, so on failure the cleanup is unreachable and the scratch
|
|
# directory survives for inspection, which is what a failed test wants anyway.
|
|
# Fuller reasoning, and the two exceptions -- a self-contained trap inside a
|
|
# Chainsaw `script` step, and one inside an explicit subshell -- are in
|
|
# docs/agents/e2e-testing.md.
|
|
#
|
|
# The debt belongs to the file that owes it, and not to a central inventory: put
|
|
# the count somewhere else and a change that adds a trap has to edit a suite it
|
|
# otherwise never opens, so two changes sharing no line invalidate each other --
|
|
# both green against their own base, both merging cleanly, red only once the
|
|
# second lands.
|
|
#
|
|
# The declaration is one comment line in the file's leading comment block, under
|
|
# the shebang and above the first line of code:
|
|
#
|
|
# # EXIT-TRAP DEBT: 12
|
|
#
|
|
# There and not merely somewhere, because a .bats file is shell that writes
|
|
# shell: the same line inside a heredoc, a fixture or an expected-output string
|
|
# is data belonging to one test, and honouring it there would let an unrelated
|
|
# fixture excuse a real trap. The leading comment block rather than everything
|
|
# above the first @test, because helper functions and their heredocs sit between
|
|
# the two; a comment block is the region with no interior.
|
|
#
|
|
# It is exact, not a ceiling. Adding a trap fails; removing one fails too, and
|
|
# the fix is to lower the number, or delete the line once it reaches zero. A file
|
|
# with no declaration must hold no traps, which covers the converted files, the
|
|
# files that never had one, and every file added tomorrow.
|
|
#
|
|
# NOT every counted handler is debt. A trap inside an explicit subshell does not
|
|
# replace the bats binary's own, so a test that fails inside `( ... )` still
|
|
# prints its `not ok` -- verified under Bats 1.14.0 against a test-level trap in
|
|
# the same file, where the TAP line vanishes. hack/e2e-test-openapi.bats holds
|
|
# one: it kills a background `kubectl proxy` from a trap inside a subshell, and
|
|
# moving that to the end of the body would leak the process on failure instead of
|
|
# suppressing a report. Its declaration says so, and because the ratchet is
|
|
# exact, REMOVING that trap fails too -- the count protects the construct rather
|
|
# than scheduling it for deletion.
|
|
#
|
|
# The limits are worth stating rather than discovering. The scan is lexical: a
|
|
# signal computed at runtime (`trap cleanup "$sig"`) is invisible, and so is a
|
|
# handler whose quoted action spans physical lines without a backslash, since
|
|
# only backslash continuations are folded. In the other direction the word is
|
|
# counted where no command exists -- inside a heredoc, or in a string a test
|
|
# compares against -- which inflates a file's debt rather than hiding one, and is
|
|
# why the fixture writers below assemble the keyword and the signal from separate
|
|
# arguments. `trap - EXIT`, which DISARMS a handler, scores as an install for the
|
|
# same inflating reason. An exact count catches addition and removal but never
|
|
# SUBSTITUTION: swap the openapi file's subshell trap for a test-level one and
|
|
# the total stays 1, the guard stays green, and that file's "this one is NOT
|
|
# debt" note quietly becomes false.
|
|
#
|
|
# The scan reads .bats and nothing else, so a handler reaching a test body from a
|
|
# sourced .sh is invisible to it. Not theoretical:
|
|
# hack/e2e-chainsaw/_lib/run-kubernetes.sh installs two, each benign for its own
|
|
# reason rather than by design -- cozy_capture_tenant_talos is declared with `(`
|
|
# and so runs in a subshell, and the diagnostics tests DO call it, so flipping
|
|
# its two delimiters to braces reinstates that handler in them with the guard
|
|
# green; run_kubernetes_test is declared with `{`, the shape that would bite, and
|
|
# is benign only because no @test calls it.
|
|
#
|
|
# The marker is anchored at column zero. That is NOT what protects the example
|
|
# above -- the nested `#` does, and relaxing the anchor leaves this file's own
|
|
# audit green. What the anchor buys is that a marker indented inside a function
|
|
# body or a heredoc somewhere else is not read as that file's declaration, and a
|
|
# file that indents its own reports "declares none" and fails loudly rather than
|
|
# passing quietly. Reflowing the example above to column zero would make the
|
|
# guard demand a debt of twelve from a file holding nothing.
|
|
#
|
|
# None of that makes the guard a proof that a file installs no handler; it is a
|
|
# ratchet over a known and common spelling. A suite reporting zero failures still
|
|
# has to have its `1..N` plan reconciled against its `ok` count, which is the
|
|
# check that catches whatever the pattern missed.
|
|
# -----------------------------------------------------------------------------
|
|
|
|
load test_helper
|
|
|
|
# The directory to audit. Under the bats binary that is this file's own
|
|
# location; under cozytest.sh, which does not set BATS_TEST_FILENAME, `$0` is
|
|
# the runner itself -- and the answer comes out the same only because the runner
|
|
# lives in the directory it runs files from. The first test below refuses to
|
|
# pass on an empty enumeration, so a wrong answer here fails rather than audits
|
|
# nothing.
|
|
BATS_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")" && pwd)"
|
|
|
|
# Print a file with comment lines dropped and shell line continuations folded, so
|
|
# the count below matches a COMMAND rather than a physical line. `trap 'rm -rf
|
|
# "$tmp"' \` with `EXIT` on the next line is a working EXIT trap that no
|
|
# single-line pattern sees, and in shell that is where the argument usually sits.
|
|
bnet_fold() {
|
|
grep -v '^[[:space:]]*#' "$1" \
|
|
| awk '{ line = line $0
|
|
if (line ~ /\\$/) { sub(/\\$/, " ", line); next }
|
|
print line; line = "" }
|
|
END { if (line != "") print line }'
|
|
}
|
|
|
|
# What an installed EXIT handler looks like.
|
|
#
|
|
# `0` as well as `EXIT`: POSIX names the exit condition signal 0, and both shells
|
|
# install the identical handler for it, so a pattern that knows only the spelling
|
|
# it has seen tests the spelling and not the construct. `EXIT` anywhere in the
|
|
# signal list rather than only as its last word: `trap ... EXIT HUP` installs the
|
|
# same exit handler plus one more, and anchoring to end-of-line accepts it. A
|
|
# quote around the signal, because `trap cleanup 'EXIT'` installs exactly what
|
|
# `trap cleanup EXIT` installs -- the quote is punctuation, not a different
|
|
# construct. The quote is optional around the SIGNAL only and never after the
|
|
# keyword, so `'trap'` as a shell argument stays unmatched; the fixture writers
|
|
# below depend on that to keep their own test data from reading as real traps.
|
|
#
|
|
# The signal ends at any character that cannot continue an identifier, not at
|
|
# whitespace. `trap ... EXIT; cd "$tmp"` and `(trap ... EXIT)` are both real
|
|
# handlers with punctuation straight after the word, and a terminator demanding
|
|
# whitespace or end-of-line scores them zero -- guard green, no declaration
|
|
# asked for. Excluding the identifier characters is what keeps `EXIT_CODE` from
|
|
# matching, which is the reason the narrower terminator looked right.
|
|
#
|
|
# The signal matches in either case, because the shell accepts it in either
|
|
# case: `trap ... exit` installs the handler `trap ... EXIT` installs, in bash
|
|
# and in dash alike. The folding is spelled into the alternation rather than
|
|
# running the whole pattern case-insensitively, since `trap` is a builtin name
|
|
# and `TRAP ... EXIT` is not a command anyone installs.
|
|
#
|
|
# The keyword needs its left boundary as much as the signal needs its right
|
|
# one, and for the same reason. `bootstrap ` ends in `trap `, so once the
|
|
# terminator accepts punctuation, `talosctl bootstrap -n ... ; do sleep 0.5`
|
|
# scores as a handler on the `0`. That is loud rather than silent, but not
|
|
# harmless: the documented answer to a red guard is to add a debt line, so a
|
|
# file would buy a permanent licence for one real trap in order to quiet a line
|
|
# that has none.
|
|
#
|
|
# Both boundaries are therefore spelled the same way -- any character that
|
|
# cannot be part of an identifier -- and NOT as whitespace. Whitespace on the
|
|
# left drops `tmp=$(mktemp -d);trap ... EXIT` and `(trap ... EXIT; true)`, which
|
|
# are the banned construct behind a semicolon and a paren. The first of those is
|
|
# the sharper lesson: the inventory this replaces carried no left boundary at
|
|
# all and caught it, so a boundary chosen carelessly here would have narrowed
|
|
# coverage while the commit message claimed to widen it.
|
|
BNET_TRAP_RE="(^|[^A-Za-z0-9_])trap[[:space:]].*[[:space:]][\"']?([Ee][Xx][Ii][Tt]|0)[\"']?([^A-Za-z0-9_]|\$)"
|
|
|
|
# How many EXIT traps a file installs.
|
|
#
|
|
# `|| true` because grep -c exits 1 on a count of zero, which is the answer for
|
|
# most of the tree; without it the caller dies under set -e on the first clean
|
|
# file and never reaches the comparison it exists for.
|
|
bnet_count() {
|
|
bnet_fold "$1" | grep -cE "$BNET_TRAP_RE" || true
|
|
}
|
|
|
|
# How many lines install more than one EXIT handler.
|
|
#
|
|
# bnet_count counts matching LINES, so `trap a EXIT; trap b EXIT` counts as one
|
|
# and a handler appended to a line that already carries one arrives without
|
|
# moving the file's total. Deciding where one command ends and the next begins
|
|
# needs a shell parser -- a semicolon inside a handler's own quoted action is not
|
|
# a separator -- and getting that wrong UNDERCOUNTS, which is the one direction a
|
|
# ratchet cannot afford. So refuse the ambiguous line instead of guessing at it.
|
|
# One handler per line is what every file in the tree already does.
|
|
bnet_ambiguous() {
|
|
bnet_fold "$1" | grep -E "$BNET_TRAP_RE" \
|
|
| awk '{ if (gsub(/(^|[^A-Za-z0-9_])trap[[:space:]]/, "&") > 1) n++ } END { print n + 0 }'
|
|
}
|
|
|
|
# The part of a file the declaration may live in: the leading comment block,
|
|
# ending at the first line that is neither a comment nor blank.
|
|
#
|
|
# Traps are counted over the whole file; the declaration is read from the header
|
|
# alone. A .bats file is shell that writes shell -- fixture writers, heredocs of
|
|
# expected output, assertion strings -- and the same line appearing in any of
|
|
# those is data belonging to one test, not a statement about the file. Reading it
|
|
# there would let an unrelated fixture excuse a real trap, and would do it
|
|
# silently, because nothing in that test's own diff looks like a declaration.
|
|
#
|
|
# The leading comment block rather than everything above the first @test: helper
|
|
# functions and their heredocs sit between the two, so stopping at the first
|
|
# @test would still read a line out of one. A comment block cannot contain a
|
|
# heredoc, which makes the narrower rule the one with no interior. It is also
|
|
# where a reviewer opening the file lands, which is the reason the declaration
|
|
# moved out of the central inventory in the first place.
|
|
bnet_header() {
|
|
awk '/^[[:space:]]*#/ || /^[[:space:]]*$/ { print; next } { exit }' "$1"
|
|
}
|
|
|
|
# Report every file in directory $1 whose traps and declaration disagree, one
|
|
# line each, and print nothing when they all agree.
|
|
#
|
|
# Through stdout rather than an exit status: cozytest.sh rewrites a function's
|
|
# closing brace into `return 0`, so a status set in here would be discarded
|
|
# before the caller could read it, and the check would pass unconditionally.
|
|
bnet_audit() {
|
|
find "$1" -name '*.bats' | sort | while IFS= read -r _f; do
|
|
[ -e "$_f" ] || continue
|
|
# Path relative to the audited root, not the basename: the scan recurses,
|
|
# and hack/foo.bats and hack/e2e-apps/foo.bats would otherwise report under
|
|
# the same name.
|
|
_b=${_f#"$1"/}
|
|
_amb=$(bnet_ambiguous "$_f")
|
|
if [ "$_amb" -gt 0 ]; then
|
|
echo "$_b: $_amb line(s) name the trap keyword more than once; keep one handler per line, and the word out of trailing comments, so the count is exact"
|
|
continue
|
|
fi
|
|
_n=$(bnet_count "$_f")
|
|
_decls=$(bnet_header "$_f" | grep -c '^# EXIT-TRAP DEBT:' || true)
|
|
if [ "$_decls" -gt 1 ]; then
|
|
echo "$_b: $_decls debt declarations; a file carries one or none"
|
|
continue
|
|
fi
|
|
if [ "$_decls" -eq 0 ]; then
|
|
if [ "$_n" -ne 0 ]; then
|
|
echo "$_b: holds $_n EXIT trap(s) and declares none; clean up at the end of the test body instead, or -- if the line is data rather than a command -- split the keyword from the signal the way the fixtures below do"
|
|
fi
|
|
continue
|
|
fi
|
|
_d=$(bnet_header "$_f" | sed -n 's/^# EXIT-TRAP DEBT:[[:space:]]*\([0-9][0-9]*\).*/\1/p')
|
|
if [ -z "$_d" ]; then
|
|
echo "$_b: the debt declaration names no count"
|
|
elif [ "$_d" -eq 0 ]; then
|
|
echo "$_b: declares a debt of zero; delete the line instead"
|
|
elif [ "$_n" -ne "$_d" ]; then
|
|
echo "$_b: declares $_d EXIT trap(s), holds $_n"
|
|
fi
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# Fixture writers for the tests below. The word and the signal are assembled from
|
|
# separate arguments on purpose: this file is scanned by the guard it defines, so
|
|
# a fixture spelled out as a literal would be counted as a real trap in it and
|
|
# the guard would demand a declaration covering its own test data.
|
|
#
|
|
# The same constraint reaches the test titles and the failure messages, which are
|
|
# code lines rather than comments and so are read as well. `bnet_add_handler`
|
|
# rather than `bnet_add_trap`, and "a handler naming EXIT" rather than "a trap
|
|
# naming EXIT", because either of the shorter forms puts the word and the signal
|
|
# on one line and is matched. Editing this file means reading its own report
|
|
# afterwards; the first test below is the one that says so.
|
|
bnet_new_fixture() {
|
|
_dir=$1
|
|
printf '%s\n' '#!/usr/bin/env bats' > "$_dir/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_handler() {
|
|
printf "%s 'rm -rf \"\$tmp\"' %s\n" 'trap' "${2:-EXIT}" >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_folded_handler() {
|
|
printf "%s 'rm -rf \"\$tmp\"' \\\\\n%s\n" 'trap' 'EXIT' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_commented_handler() {
|
|
printf "# %s 'rm -rf \"\$tmp\"' %s\n" 'trap' 'EXIT' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
# Put a declaration where a real file puts it: in the leading comment block, just
|
|
# under the shebang, whatever the fixture has appended by now. Rebuilt around
|
|
# line 1 rather than inserted with `sed -i`, whose append syntax differs between
|
|
# the GNU and BSD versions this suite runs under.
|
|
bnet_add_debt() {
|
|
_f=$1/subject.bats
|
|
{ head -n 1 "$_f"; printf '%s %s\n' '# EXIT-TRAP DEBT:' "$2"; tail -n +2 "$_f"; } > "$_f.new"
|
|
mv "$_f.new" "$_f"
|
|
return 0
|
|
}
|
|
|
|
# Put a declaration past the header, where it must NOT be honoured.
|
|
bnet_append_debt() {
|
|
printf '%s %s\n' '# EXIT-TRAP DEBT:' "$2" >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_test_body() {
|
|
printf '%s\n' '@test "something" {' ' :' '}' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_code() {
|
|
printf '%s\n' 'BATS_DIR=.' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_word_containing_trap() {
|
|
printf " timeout 60 sh -ec 'until talosctl %s -n 192.0.2.11; do sleep 0.5; done'\n" \
|
|
'bootstrap' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_indented_debt() {
|
|
printf ' %s %s\n' '# EXIT-TRAP DEBT:' "$2" >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_command_then_handler() {
|
|
printf "a=1;%s 'rm -rf \"\$a\"' %s\n" 'trap' 'EXIT' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_handler_in_parens() {
|
|
printf "(%s 'kill 1' %s; true)\n" 'trap' 'EXIT' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_handler_then_command() {
|
|
printf "%s 'rm -rf \"\$a\"' %s; cd /\n" 'trap' 'EXIT' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_nested_fixture() {
|
|
mkdir -p "$1/nested"
|
|
printf '%s\n' '#!/usr/bin/env bats' > "$1/nested/deep.bats"
|
|
printf "%s 'rm -rf \"\$a\"' %s\n" 'trap' 'EXIT' >> "$1/nested/deep.bats"
|
|
return 0
|
|
}
|
|
|
|
bnet_add_two_handlers_on_one_line() {
|
|
printf "%s 'rm -f \"\$a\"' %s; %s 'rm -f \"\$b\"' %s\n" \
|
|
'trap' 'EXIT' 'trap' 'EXIT' >> "$1/subject.bats"
|
|
return 0
|
|
}
|
|
|
|
@test "every bats file under hack holds exactly the EXIT traps it declares" {
|
|
# The whole point of the guard, run against the tree it governs. Everything
|
|
# below this test exists to prove that this one can fail.
|
|
# Counted with the enumeration bnet_audit actually walks, not a second one:
|
|
# a backstop that asks `ls` whether the `find` found anything answers about
|
|
# the wrong list, and would pass while the audit went vacuously green. That
|
|
# is the failure this file exists to talk about, so it may as well not commit
|
|
# it.
|
|
files=$(find "$BATS_DIR" -name '*.bats' | wc -l)
|
|
[ "$files" -gt 0 ] || { echo "FAIL: found no .bats files to audit at all"; false; }
|
|
|
|
offences=$(bnet_audit "$BATS_DIR")
|
|
if [ -n "$offences" ]; then
|
|
echo "FAIL: EXIT-trap cleanup and the declared debt disagree."
|
|
printf '%s\n' "$offences"
|
|
echo "Clean up at the end of the test body, or correct the file's own"
|
|
echo "'# EXIT-TRAP DEBT:' line; see docs/agents/e2e-testing.md."
|
|
false
|
|
fi
|
|
}
|
|
|
|
@test "a trap with no declaration is reported" {
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: an undeclared trap went unreported: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a declaration matching the file is accepted" {
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_debt "$tmp" 2
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
[ -z "$out" ] || { echo "FAIL: an accurate declaration was reported: $out"; false; }
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a declaration below the real count is reported" {
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_debt "$tmp" 1
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: declares 1 EXIT trap(s), holds 2"*) ;;
|
|
*) echo "FAIL: a grown trap count went unreported: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a declaration above the real count is reported" {
|
|
# The ratchet is exact rather than a ceiling. A file that sheds a trap has to
|
|
# say so, which is what keeps the number from rotting upward and quietly
|
|
# licensing traps somebody removed years earlier.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_debt "$tmp" 3
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: declares 3 EXIT trap(s), holds 1"*) ;;
|
|
*) echo "FAIL: a stale declaration went unreported: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a trap folded across a line continuation is counted" {
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_folded_handler "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a trap whose signal sits on the next line went uncounted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a trap installed on signal zero is counted" {
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp" 0
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: the POSIX spelling of the exit condition went uncounted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a handler naming EXIT ahead of another signal is counted" {
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp" 'EXIT HUP'
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a handler listing EXIT before another signal went uncounted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a trap inside a comment is not counted" {
|
|
# Every converted file explains the ban in prose next to the cleanup it
|
|
# replaced. A check that read the explanation as a violation would push people
|
|
# to delete the explanation, which is the part that stops the next person
|
|
# reinstating the trap.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_commented_handler "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
[ -z "$out" ] || { echo "FAIL: a trap named in a comment was counted: $out"; false; }
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a declared debt of zero is reported" {
|
|
# A file that owes nothing says so by carrying no line, not by carrying a line
|
|
# that says nothing. Otherwise a fully converted file keeps a declaration that
|
|
# reads as an allowance the next reader has no reason to question.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_debt "$tmp" 0
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: declares a debt of zero"*) ;;
|
|
*) echo "FAIL: an empty declaration was accepted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "an indented declaration is not honoured" {
|
|
# The marker is anchored at column zero. Not, as an earlier version of this
|
|
# comment claimed, to protect the header's own `# EXIT-TRAP DEBT: 12` example
|
|
# -- that one is nested behind a second `#` and stays unmatched either way;
|
|
# relaxing the anchor leaves this file's audit green, which is how the claim
|
|
# was caught. What the anchor buys is that a marker indented inside a function
|
|
# body or a heredoc somewhere else is not read as that file's declaration.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_indented_debt "$tmp" 1
|
|
bnet_add_handler "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: an indented declaration was honoured: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a handler preceded by punctuation is counted" {
|
|
# The keyword's left boundary is any character that cannot be part of an
|
|
# identifier, not whitespace specifically. `tmp=$(mktemp -d);trap ... EXIT`
|
|
# and `(trap ... EXIT; true)` both sit straight behind punctuation, and both
|
|
# are the banned construct. Requiring a space would be a REGRESSION -- the
|
|
# inventory this replaces had no left boundary at all and caught the first of
|
|
# them -- so the two boundaries have to be spelled the same way round.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_command_then_handler "$tmp"
|
|
bnet_add_handler_in_parens "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"holds 2 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a handler behind punctuation went uncounted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a word merely ending in trap is not counted" {
|
|
# `bootstrap ` contains `trap `, and once the terminator accepts punctuation
|
|
# the `0` in `sleep 0.5` closes the match: a talosctl bootstrap line scores as
|
|
# a handler. Loud, but not harmless -- the documented answer to a red guard is
|
|
# to add a debt line, so a file would take a permanent licence for one real
|
|
# trap to silence a line that has none. The keyword needs its left boundary
|
|
# for the same reason the signal needed its right one.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_word_containing_trap "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
[ -z "$out" ] || { echo "FAIL: a word ending in trap was counted: $out"; false; }
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a handler naming the signal in lower case is counted" {
|
|
# Signal names are case-insensitive to the shell: `trap ... exit` installs the
|
|
# same handler as `trap ... EXIT`, verified in both bash and dash. Only the
|
|
# SIGNAL is; `trap` itself is a builtin name and stays lower case, which is
|
|
# why the pattern folds case on the signal alternation rather than being run
|
|
# case-insensitively end to end.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp" exit
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a lower-case signal name went uncounted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a handler followed by another command on the same line is counted" {
|
|
# The signal does not have to end the line. `trap ... EXIT; cd "$tmp"` and a
|
|
# handler wrapped in `( ... )` both put punctuation straight after the word,
|
|
# and a terminator that insists on whitespace or end-of-line silently scores
|
|
# them zero -- a real handler, no declaration needed, guard green. Same class
|
|
# as the quoted signal, one keystroke away.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler_then_command "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a handler followed by a semicolon went uncounted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a bats file in a subdirectory is audited too" {
|
|
# The ban covers every bats file under hack/. Two pre-Chainsaw suites once
|
|
# lived one directory down; this synthetic fixture pins recursion after their
|
|
# removal.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_nested_fixture "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"nested/deep.bats: holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a nested bats file went unaudited or lost its path: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a declaration naming no count is reported" {
|
|
# The parser takes digits. A declaration whose count is a word, or missing,
|
|
# parses to nothing, and an unguarded comparison would then treat the file as
|
|
# if it had said something.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_debt "$tmp" "twelve"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: the debt declaration names no count"*) ;;
|
|
*) echo "FAIL: a countless declaration was accepted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a handler whose signal is quoted is counted" {
|
|
# `trap cleanup 'EXIT'` installs the handler `trap cleanup EXIT` installs. A
|
|
# pattern that demands a bare word tests the punctuation rather than the
|
|
# construct, and the quoted form is one keystroke away from whoever wants the
|
|
# guard to look past them.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp" "'EXIT'"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a quoted signal went uncounted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "two handlers sharing one line are reported rather than counted as one" {
|
|
# The count is a count of LINES, so a second handler appended to a line that
|
|
# already carries one arrives free: the file's total does not move and the
|
|
# declaration keeps matching. Rather than guess at command boundaries -- which
|
|
# needs a shell parser, and whose failure mode is an UNDERCOUNT, the direction
|
|
# this guard cannot afford -- refuse the ambiguous line and say so. One handler
|
|
# per line is what the tree already does.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_two_handlers_on_one_line "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: 1 line(s) name the trap keyword more than once"*) ;;
|
|
*) echo "FAIL: a doubled-up line was silently counted once: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a declaration below the leading comment block is not honoured" {
|
|
# The header ends at the first line of code, not at the first @test. A .bats
|
|
# file runs shell before its tests -- helper functions, fixture writers, a
|
|
# heredoc of expected output -- and a line inside any of that is data, not a
|
|
# statement about the file. Ending the header at the first @test would still
|
|
# read it.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_code "$tmp"
|
|
bnet_append_debt "$tmp" 1
|
|
bnet_add_handler "$tmp"
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a declaration past the first code line excused a trap: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a declaration below the first test is not honoured" {
|
|
# Only the header counts, so that a line the file merely CONTAINS -- inside a
|
|
# heredoc, in a fixture a test writes, in an expected-output string -- cannot
|
|
# excuse a real trap. Such a line is written to be read as data by the test
|
|
# around it, and its author has no idea it is also being read as a promise
|
|
# about the file. Narrowing to the header also puts the declaration where a
|
|
# reviewer opening the file sees it, which is the whole reason it moved out of
|
|
# the central inventory.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_test_body "$tmp"
|
|
bnet_append_debt "$tmp" 1
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: holds 1 EXIT trap(s) and declares none"*) ;;
|
|
*) echo "FAIL: a declaration past the first test excused a trap: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a second declaration in one file is reported" {
|
|
# Two declarations make the file's own statement ambiguous, and a parser that
|
|
# silently takes the first would let the losing one drift without ever saying
|
|
# which number is in force.
|
|
tmp=$(mktemp -d)
|
|
bnet_new_fixture "$tmp"
|
|
bnet_add_handler "$tmp"
|
|
bnet_add_debt "$tmp" 1
|
|
bnet_add_debt "$tmp" 9
|
|
|
|
out=$(bnet_audit "$tmp")
|
|
case "$out" in
|
|
*"subject.bats: 2 debt declarations"*) ;;
|
|
*) echo "FAIL: contradictory declarations were accepted: $out"; false ;;
|
|
esac
|
|
rm -rf "$tmp"
|
|
}
|