summaryrefslogtreecommitdiffstats
path: root/test/units/test-control.sh
diff options
context:
space:
mode:
Diffstat (limited to 'test/units/test-control.sh')
-rw-r--r--test/units/test-control.sh166
1 files changed, 166 insertions, 0 deletions
diff --git a/test/units/test-control.sh b/test/units/test-control.sh
new file mode 100644
index 0000000..0a1611b
--- /dev/null
+++ b/test/units/test-control.sh
@@ -0,0 +1,166 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# shellcheck shell=bash
+
+if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
+ echo >&2 "This file should not be executed directly"
+ exit 1
+fi
+
+declare -i _CHILD_PID=0
+_PASSED_TESTS=()
+
+# Like trap, but passes the signal name as the first argument
+_trap_with_sig() {
+ local fun="${1:?}"
+ local sig
+ shift
+
+ for sig in "$@"; do
+ # shellcheck disable=SC2064
+ trap "$fun $sig" "$sig"
+ done
+}
+
+# Propagate the caught signal to the current child process
+_handle_signal() {
+ local sig="${1:?}"
+
+ if [[ $_CHILD_PID -gt 0 ]]; then
+ echo "Propagating signal $sig to child process $_CHILD_PID"
+ kill -s "$sig" "$_CHILD_PID"
+ fi
+}
+
+# In order to make the _handle_signal() stuff above work, we have to execute
+# each script asynchronously, since bash won't execute traps until the currently
+# executed command finishes. This, however, introduces another issue regarding
+# how bash's wait works. Quoting:
+#
+# When bash is waiting for an asynchronous command via the wait builtin,
+# the reception of a signal for which a trap has been set will cause the wait
+# builtin to return immediately with an exit status greater than 128,
+# immediately after which the trap is executed.
+#
+# In other words - every time we propagate a signal, wait returns with
+# 128+signal, so we have to wait again - repeat until the process dies.
+_wait_harder() {
+ local pid="${1:?}"
+
+ while kill -0 "$pid" &>/dev/null; do
+ wait "$pid" || :
+ done
+
+ wait "$pid"
+}
+
+_show_summary() {(
+ set +x
+
+ if [[ ${#_PASSED_TESTS[@]} -eq 0 ]]; then
+ echo >&2 "No tests were executed, this is most likely an error"
+ exit 1
+ fi
+
+ printf "PASSED TESTS: %3d:\n" "${#_PASSED_TESTS[@]}"
+ echo "------------------"
+ for t in "${_PASSED_TESTS[@]}"; do
+ echo "$t"
+ done
+)}
+
+# Like run_subtests, but propagate specified signals to the subtest script
+run_subtests_with_signals() {
+ local subtests=("${0%.sh}".*.sh)
+ local subtest
+
+ if [[ "${#subtests[@]}" -eq 0 ]]; then
+ echo >&2 "No subtests found for file $0"
+ exit 1
+ fi
+
+ if [[ "$#" -eq 0 ]]; then
+ echo >&2 "No signals to propagate were specified"
+ exit 1
+ fi
+
+ _trap_with_sig _handle_signal "$@"
+
+ for subtest in "${subtests[@]}"; do
+ if [[ -n "${TEST_MATCH_SUBTEST:-}" ]] && ! [[ "$subtest" =~ $TEST_MATCH_SUBTEST ]]; then
+ echo "Skipping $subtest (not matching '$TEST_MATCH_SUBTEST')"
+ continue
+ fi
+
+ : "--- $subtest BEGIN ---"
+ SECONDS=0
+ "./$subtest" &
+ _CHILD_PID=$!
+ if ! _wait_harder "$_CHILD_PID"; then
+ echo "Subtest $subtest failed"
+ return 1
+ fi
+
+ _PASSED_TESTS+=("$subtest")
+ : "--- $subtest END (${SECONDS}s) ---"
+ done
+
+ _show_summary
+}
+
+# Run all subtests (i.e. files named as testsuite-<testid>.<subtest_name>.sh)
+run_subtests() {
+ local subtests=("${0%.sh}".*.sh)
+ local subtest
+
+ if [[ "${#subtests[@]}" -eq 0 ]]; then
+ echo >&2 "No subtests found for file $0"
+ exit 1
+ fi
+
+ for subtest in "${subtests[@]}"; do
+ if [[ -n "${TEST_MATCH_SUBTEST:-}" ]] && ! [[ "$subtest" =~ $TEST_MATCH_SUBTEST ]]; then
+ echo "Skipping $subtest (not matching '$TEST_MATCH_SUBTEST')"
+ continue
+ fi
+
+ : "--- $subtest BEGIN ---"
+ SECONDS=0
+ if ! "./$subtest"; then
+ echo "Subtest $subtest failed"
+ return 1
+ fi
+
+ _PASSED_TESTS+=("$subtest")
+ : "--- $subtest END (${SECONDS}s) ---"
+ done
+
+ _show_summary
+}
+
+# Run all test cases (i.e. functions prefixed with testcase_ in the current namespace)
+run_testcases() {
+ local testcase testcases
+
+ # Create a list of all functions prefixed with testcase_
+ mapfile -t testcases < <(declare -F | awk '$3 ~ /^testcase_/ {print $3;}')
+
+ if [[ "${#testcases[@]}" -eq 0 ]]; then
+ echo >&2 "No test cases found, this is most likely an error"
+ exit 1
+ fi
+
+ for testcase in "${testcases[@]}"; do
+ if [[ -n "${TEST_MATCH_TESTCASE:-}" ]] && ! [[ "$testcase" =~ $TEST_MATCH_TESTCASE ]]; then
+ echo "Skipping $testcase (not matching '$TEST_MATCH_TESTCASE')"
+ continue
+ fi
+
+ : "+++ $testcase BEGIN +++"
+ # Note: the subshell here is used purposefully, otherwise we might
+ # unexpectedly inherit a RETURN trap handler from the called
+ # function and call it for the second time once we return,
+ # causing a "double-free"
+ ("$testcase")
+ : "+++ $testcase END +++"
+ done
+}