PASS=0
FAIL=0
# Per-test assertion failure counter.
#
# ~keep `run_test` cannot rely on `set -e` to stop a test function at its first
# failed assertion: invoking the function as the condition of an `if` (or either
# side of `&&`/`||`) disables errexit for the whole dynamic extent of that call.
# A failing assertion's `return 1` therefore does not abort the function, and the
# function's exit status ends up being just its *last* command's — so every
# assertion but the last could fail while the test still reported PASS. Each
# assertion helper records its failure here instead, and `run_test` consults the
# counter alongside the exit status. Adding `set -e` would not fix this; errexit
# is already set and is ignored in exactly this context.
ASSERT_FAILURES=0
# Report one failed assertion and record it. Returns 1 so the calling helper
# still propagates a non-zero status to callers that do check it.
__fail_assertion() {
echo "$1" >&2
ASSERT_FAILURES=$((ASSERT_FAILURES + 1))
return 1
}
assert_equals() {
local actual="$1" expected="$2" label="$3"
if [ "$actual" != "$expected" ]; then
__fail_assertion "FAIL [$label]: expected '$expected', got '$actual'"
fi
}
assert_contains() {
local actual="$1" expected="$2" label="$3"
if [[ "$actual" != *"$expected"* ]]; then
__fail_assertion "FAIL [$label]: expected to contain '$expected'"
fi
}
assert_not_empty() {
local actual="$1" label="$2"
# `jq -r` renders JSON null and empty containers as the literal text "null", "[]" and "{}",
# so a bare emptiness test would pass on them. A legitimate 0 or false still renders as text and passes.
if [ -z "$actual" ] || [ "$actual" = "null" ] || [ "$actual" = "[]" ] || [ "$actual" = "{}" ]; then
__fail_assertion "FAIL [$label]: expected non-empty value"
fi
}
assert_count_min() {
local count="$1" min="$2" label="$3"
if [ "$count" -lt "$min" ]; then
__fail_assertion "FAIL [$label]: expected at least $min elements, got $count"
fi
}
assert_greater_than() {
local val="$1" threshold="$2" label="$3"
if [ "$(echo "$val > $threshold" | bc -l)" != "1" ]; then
__fail_assertion "FAIL [$label]: expected $val > $threshold"
fi
}
assert_greater_than_or_equal() {
local actual="$1" expected="$2" label="$3"
if [ "$actual" -lt "$expected" ]; then
__fail_assertion "FAIL [$label]: expected $actual >= $expected"
fi
}
assert_is_empty() {
local actual="$1" label="$2"
if [ -n "$actual" ]; then
__fail_assertion "FAIL [$label]: expected empty value, got '$actual'"
fi
}
assert_less_than() {
local actual="$1" expected="$2" label="$3"
if [ "$actual" -ge "$expected" ]; then
__fail_assertion "FAIL [$label]: expected $actual < $expected"
fi
}
assert_less_than_or_equal() {
local actual="$1" expected="$2" label="$3"
if [ "$actual" -gt "$expected" ]; then
__fail_assertion "FAIL [$label]: expected $actual <= $expected"
fi
}
assert_not_contains() {
local actual="$1" expected="$2" label="$3"
if [[ "$actual" == *"$expected"* ]]; then
__fail_assertion "FAIL [$label]: expected not to contain '$expected'"
fi
}
# Run one generated test function and record its outcome.
#
# ~keep The test runs to completion (errexit is inert here, see ASSERT_FAILURES
# above) so that every assertion is reported, not just the first. The verdict is
# the disjunction of the function's own exit status and the assertion counter:
# either alone is insufficient — the status misses non-final failed assertions,
# and the counter misses a test that bailed out through a bare `return 1`.
run_test() {
local name="$1"
ASSERT_FAILURES=0
local status=0
"$name" || status=$?
if [ "$status" -eq 0 ] && [ "$ASSERT_FAILURES" -eq 0 ]; then
echo "PASS: $name"
PASS=$((PASS + 1))
else
echo "FAIL: $name"
FAIL=$((FAIL + 1))
fi
}