summaryrefslogtreecommitdiffstats
path: root/test/units/test-control.sh
blob: 4cede74d50e3cf2719ff0d6e10967b816d5ac84b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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 $TESTNAME.<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
}