diff options
Diffstat (limited to 'test/units/test-control.sh')
-rw-r--r-- | test/units/test-control.sh | 166 |
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 +} |