xref: /aosp_15_r20/external/bazelbuild-rules_android/test/bashunit/unittest.bash (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
1*9e965d6fSRomain Jobredeaux#!/bin/bash
2*9e965d6fSRomain Jobredeaux#
3*9e965d6fSRomain Jobredeaux# Copyright 2015 The Bazel Authors. All rights reserved.
4*9e965d6fSRomain Jobredeaux#
5*9e965d6fSRomain Jobredeaux# Licensed under the Apache License, Version 2.0 (the "License");
6*9e965d6fSRomain Jobredeaux# you may not use this file except in compliance with the License.
7*9e965d6fSRomain Jobredeaux# You may obtain a copy of the License at
8*9e965d6fSRomain Jobredeaux#
9*9e965d6fSRomain Jobredeaux#    http://www.apache.org/licenses/LICENSE-2.0
10*9e965d6fSRomain Jobredeaux#
11*9e965d6fSRomain Jobredeaux# Unless required by applicable law or agreed to in writing, software
12*9e965d6fSRomain Jobredeaux# distributed under the License is distributed on an "AS IS" BASIS,
13*9e965d6fSRomain Jobredeaux# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*9e965d6fSRomain Jobredeaux# See the License for the specific language governing permissions and
15*9e965d6fSRomain Jobredeaux# limitations under the License.
16*9e965d6fSRomain Jobredeaux#
17*9e965d6fSRomain Jobredeaux# Common utility file for Bazel shell tests
18*9e965d6fSRomain Jobredeaux#
19*9e965d6fSRomain Jobredeaux# unittest.bash: a unit test framework in Bash.
20*9e965d6fSRomain Jobredeaux#
21*9e965d6fSRomain Jobredeaux# A typical test suite looks like so:
22*9e965d6fSRomain Jobredeaux#
23*9e965d6fSRomain Jobredeaux#   ------------------------------------------------------------------------
24*9e965d6fSRomain Jobredeaux#   #!/bin/bash
25*9e965d6fSRomain Jobredeaux#
26*9e965d6fSRomain Jobredeaux#   source path/to/unittest.bash || exit 1
27*9e965d6fSRomain Jobredeaux#
28*9e965d6fSRomain Jobredeaux#   # Test that foo works.
29*9e965d6fSRomain Jobredeaux#   function test_foo() {
30*9e965d6fSRomain Jobredeaux#     foo >$TEST_log || fail "foo failed";
31*9e965d6fSRomain Jobredeaux#     expect_log "blah" "Expected to see 'blah' in output of 'foo'."
32*9e965d6fSRomain Jobredeaux#   }
33*9e965d6fSRomain Jobredeaux#
34*9e965d6fSRomain Jobredeaux#   # Test that bar works.
35*9e965d6fSRomain Jobredeaux#   function test_bar() {
36*9e965d6fSRomain Jobredeaux#     bar 2>$TEST_log || fail "bar failed";
37*9e965d6fSRomain Jobredeaux#     expect_not_log "ERROR" "Unexpected error from 'bar'."
38*9e965d6fSRomain Jobredeaux#     ...
39*9e965d6fSRomain Jobredeaux#     assert_equals $x $y
40*9e965d6fSRomain Jobredeaux#   }
41*9e965d6fSRomain Jobredeaux#
42*9e965d6fSRomain Jobredeaux#   run_suite "Test suite for blah"
43*9e965d6fSRomain Jobredeaux#   ------------------------------------------------------------------------
44*9e965d6fSRomain Jobredeaux#
45*9e965d6fSRomain Jobredeaux# Each test function is considered to pass iff fail() is not called
46*9e965d6fSRomain Jobredeaux# while it is active.  fail() may be called directly, or indirectly
47*9e965d6fSRomain Jobredeaux# via other assertions such as expect_log().  run_suite must be called
48*9e965d6fSRomain Jobredeaux# at the very end.
49*9e965d6fSRomain Jobredeaux#
50*9e965d6fSRomain Jobredeaux# A test suite may redefine functions "set_up" and/or "tear_down";
51*9e965d6fSRomain Jobredeaux# these functions are executed before and after each test function,
52*9e965d6fSRomain Jobredeaux# respectively.  Similarly, "cleanup" and "timeout" may be redefined,
53*9e965d6fSRomain Jobredeaux# and these function are called upon exit (of any kind) or a timeout.
54*9e965d6fSRomain Jobredeaux#
55*9e965d6fSRomain Jobredeaux# The user can pass --test_filter to blaze test to select specific tests
56*9e965d6fSRomain Jobredeaux# to run with Bash globs. A union of tests matching any of the provided globs
57*9e965d6fSRomain Jobredeaux# will be run. Additionally the user may define TESTS=(test_foo test_bar ...) to
58*9e965d6fSRomain Jobredeaux# specify a subset of test functions to execute, for example, a working set
59*9e965d6fSRomain Jobredeaux# during debugging. By default, all functions called test_* will be executed.
60*9e965d6fSRomain Jobredeaux#
61*9e965d6fSRomain Jobredeaux# This file provides utilities for assertions over the output of a
62*9e965d6fSRomain Jobredeaux# command.  The output of the command under test is directed to the
63*9e965d6fSRomain Jobredeaux# file $TEST_log, and then the expect_log* assertions can be used to
64*9e965d6fSRomain Jobredeaux# test for the presence of certain regular expressions in that file.
65*9e965d6fSRomain Jobredeaux#
66*9e965d6fSRomain Jobredeaux# The test framework is responsible for restoring the original working
67*9e965d6fSRomain Jobredeaux# directory before each test.
68*9e965d6fSRomain Jobredeaux#
69*9e965d6fSRomain Jobredeaux# The order in which test functions are run is not defined, so it is
70*9e965d6fSRomain Jobredeaux# important that tests clean up after themselves.
71*9e965d6fSRomain Jobredeaux#
72*9e965d6fSRomain Jobredeaux# Each test will be run in a new subshell.
73*9e965d6fSRomain Jobredeaux#
74*9e965d6fSRomain Jobredeaux# Functions named __* are not intended for use by clients.
75*9e965d6fSRomain Jobredeaux#
76*9e965d6fSRomain Jobredeaux# This framework implements the "test sharding protocol".
77*9e965d6fSRomain Jobredeaux#
78*9e965d6fSRomain Jobredeaux
79*9e965d6fSRomain Jobredeaux[[ -n "$BASH_VERSION" ]] ||
80*9e965d6fSRomain Jobredeaux  { echo "unittest.bash only works with bash!" >&2; exit 1; }
81*9e965d6fSRomain Jobredeaux
82*9e965d6fSRomain Jobredeauxexport BAZEL_SHELL_TEST=1
83*9e965d6fSRomain Jobredeaux
84*9e965d6fSRomain JobredeauxDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
85*9e965d6fSRomain Jobredeaux
86*9e965d6fSRomain Jobredeaux# Load the environment support utilities.
87*9e965d6fSRomain Jobredeauxsource "${DIR}/unittest_utils.sh" || { echo "unittest_utils.sh not found" >&2; exit 1; }
88*9e965d6fSRomain Jobredeaux
89*9e965d6fSRomain Jobredeaux#### Global variables:
90*9e965d6fSRomain Jobredeaux
91*9e965d6fSRomain JobredeauxTEST_name=""                    # The name of the current test.
92*9e965d6fSRomain Jobredeaux
93*9e965d6fSRomain JobredeauxTEST_log=$TEST_TMPDIR/log       # The log file over which the
94*9e965d6fSRomain Jobredeaux                                # expect_log* assertions work.  Must
95*9e965d6fSRomain Jobredeaux                                # be absolute to be robust against
96*9e965d6fSRomain Jobredeaux                                # tests invoking 'cd'!
97*9e965d6fSRomain Jobredeaux
98*9e965d6fSRomain JobredeauxTEST_passed="true"              # The result of the current test;
99*9e965d6fSRomain Jobredeaux                                # failed assertions cause this to
100*9e965d6fSRomain Jobredeaux                                # become false.
101*9e965d6fSRomain Jobredeaux
102*9e965d6fSRomain Jobredeaux# These variables may be overridden by the test suite:
103*9e965d6fSRomain Jobredeaux
104*9e965d6fSRomain JobredeauxTESTS=()                        # A subset or "working set" of test
105*9e965d6fSRomain Jobredeaux                                # functions that should be run.  By
106*9e965d6fSRomain Jobredeaux                                # default, all tests called test_* are
107*9e965d6fSRomain Jobredeaux                                # run.
108*9e965d6fSRomain Jobredeaux
109*9e965d6fSRomain Jobredeaux_TEST_FILTERS=()                # List of globs to use to filter the tests.
110*9e965d6fSRomain Jobredeaux                                # If non-empty, all tests matching at least one
111*9e965d6fSRomain Jobredeaux                                # of the globs are run and test list provided in
112*9e965d6fSRomain Jobredeaux                                # the arguments is ignored if present.
113*9e965d6fSRomain Jobredeaux
114*9e965d6fSRomain Jobredeaux__in_tear_down=0                # Indicates whether we are in `tear_down` phase
115*9e965d6fSRomain Jobredeaux                                # of test. Used to avoid re-entering `tear_down`
116*9e965d6fSRomain Jobredeaux                                # on failures within it.
117*9e965d6fSRomain Jobredeaux
118*9e965d6fSRomain Jobredeauxif (( $# > 0 )); then
119*9e965d6fSRomain Jobredeaux  (
120*9e965d6fSRomain Jobredeaux    IFS=':'
121*9e965d6fSRomain Jobredeaux    echo "WARNING: Passing test names in arguments (--test_arg) is deprecated, please use --test_filter='$*' instead." >&2
122*9e965d6fSRomain Jobredeaux  )
123*9e965d6fSRomain Jobredeaux
124*9e965d6fSRomain Jobredeaux  # Legacy behavior is to ignore missing regexp, but with errexit
125*9e965d6fSRomain Jobredeaux  # the following line fails without || true.
126*9e965d6fSRomain Jobredeaux  # TODO(dmarting): maybe we should revisit the way of selecting
127*9e965d6fSRomain Jobredeaux  # test with that framework (use Bazel's environment variable instead).
128*9e965d6fSRomain Jobredeaux  TESTS=($(for i in "$@"; do echo $i; done | grep ^test_ || true))
129*9e965d6fSRomain Jobredeaux  if (( ${#TESTS[@]} == 0 )); then
130*9e965d6fSRomain Jobredeaux    echo "WARNING: Arguments do not specify tests!" >&2
131*9e965d6fSRomain Jobredeaux  fi
132*9e965d6fSRomain Jobredeauxfi
133*9e965d6fSRomain Jobredeaux# TESTBRIDGE_TEST_ONLY contains the value of --test_filter, if any. We want to
134*9e965d6fSRomain Jobredeaux# preferentially use that instead of $@ to determine which tests to run.
135*9e965d6fSRomain Jobredeauxif [[ ${TESTBRIDGE_TEST_ONLY:-} != "" ]]; then
136*9e965d6fSRomain Jobredeaux  if (( ${#TESTS[@]} != 0 )); then
137*9e965d6fSRomain Jobredeaux    echo "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg" >&2
138*9e965d6fSRomain Jobredeaux    TESTS=()
139*9e965d6fSRomain Jobredeaux  fi
140*9e965d6fSRomain Jobredeaux  # Split TESTBRIDGE_TEST_ONLY on colon and store it in `_TEST_FILTERS` array.
141*9e965d6fSRomain Jobredeaux  IFS=':' read -r -a _TEST_FILTERS <<< "$TESTBRIDGE_TEST_ONLY"
142*9e965d6fSRomain Jobredeauxfi
143*9e965d6fSRomain Jobredeaux
144*9e965d6fSRomain JobredeauxTEST_verbose="true"             # Whether or not to be verbose.  A
145*9e965d6fSRomain Jobredeaux                                # command; "true" or "false" are
146*9e965d6fSRomain Jobredeaux                                # acceptable.  The default is: true.
147*9e965d6fSRomain Jobredeaux
148*9e965d6fSRomain JobredeauxTEST_script="$0"                # Full path to test script
149*9e965d6fSRomain Jobredeaux# Check if the script path is absolute, if not prefix the PWD.
150*9e965d6fSRomain Jobredeauxif [[ ! "$TEST_script" = /* ]]; then
151*9e965d6fSRomain Jobredeaux  TEST_script="${PWD}/$0"
152*9e965d6fSRomain Jobredeauxfi
153*9e965d6fSRomain Jobredeaux
154*9e965d6fSRomain Jobredeaux
155*9e965d6fSRomain Jobredeaux#### Internal functions
156*9e965d6fSRomain Jobredeaux
157*9e965d6fSRomain Jobredeauxfunction __show_log() {
158*9e965d6fSRomain Jobredeaux    echo "-- Test log: -----------------------------------------------------------"
159*9e965d6fSRomain Jobredeaux    [[ -e $TEST_log ]] && cat "$TEST_log" || echo "(Log file did not exist.)"
160*9e965d6fSRomain Jobredeaux    echo "------------------------------------------------------------------------"
161*9e965d6fSRomain Jobredeaux}
162*9e965d6fSRomain Jobredeaux
163*9e965d6fSRomain Jobredeaux# Usage: __pad <title> <pad-char>
164*9e965d6fSRomain Jobredeaux# Print $title padded to 80 columns with $pad_char.
165*9e965d6fSRomain Jobredeauxfunction __pad() {
166*9e965d6fSRomain Jobredeaux    local title=$1
167*9e965d6fSRomain Jobredeaux    local pad=$2
168*9e965d6fSRomain Jobredeaux    # Ignore the subshell error -- `head` closes the fd before reading to the
169*9e965d6fSRomain Jobredeaux    # end, therefore the subshell will get SIGPIPE while stuck in `write`.
170*9e965d6fSRomain Jobredeaux    {
171*9e965d6fSRomain Jobredeaux        echo -n "${pad}${pad} ${title} "
172*9e965d6fSRomain Jobredeaux        printf "%80s" " " | tr ' ' "$pad"
173*9e965d6fSRomain Jobredeaux    } | head -c 80 || true
174*9e965d6fSRomain Jobredeaux    echo
175*9e965d6fSRomain Jobredeaux}
176*9e965d6fSRomain Jobredeaux
177*9e965d6fSRomain Jobredeaux#### Exported functions
178*9e965d6fSRomain Jobredeaux
179*9e965d6fSRomain Jobredeaux# Usage: init_test ...
180*9e965d6fSRomain Jobredeaux# Deprecated.  Has no effect.
181*9e965d6fSRomain Jobredeauxfunction init_test() {
182*9e965d6fSRomain Jobredeaux    :
183*9e965d6fSRomain Jobredeaux}
184*9e965d6fSRomain Jobredeaux
185*9e965d6fSRomain Jobredeaux
186*9e965d6fSRomain Jobredeaux# Usage: set_up
187*9e965d6fSRomain Jobredeaux# Called before every test function.  May be redefined by the test suite.
188*9e965d6fSRomain Jobredeauxfunction set_up() {
189*9e965d6fSRomain Jobredeaux    :
190*9e965d6fSRomain Jobredeaux}
191*9e965d6fSRomain Jobredeaux
192*9e965d6fSRomain Jobredeaux# Usage: tear_down
193*9e965d6fSRomain Jobredeaux# Called after every test function.  May be redefined by the test suite.
194*9e965d6fSRomain Jobredeauxfunction tear_down() {
195*9e965d6fSRomain Jobredeaux    :
196*9e965d6fSRomain Jobredeaux}
197*9e965d6fSRomain Jobredeaux
198*9e965d6fSRomain Jobredeaux# Usage: cleanup
199*9e965d6fSRomain Jobredeaux# Called upon eventual exit of the test suite.  May be redefined by
200*9e965d6fSRomain Jobredeaux# the test suite.
201*9e965d6fSRomain Jobredeauxfunction cleanup() {
202*9e965d6fSRomain Jobredeaux    :
203*9e965d6fSRomain Jobredeaux}
204*9e965d6fSRomain Jobredeaux
205*9e965d6fSRomain Jobredeaux# Usage: timeout
206*9e965d6fSRomain Jobredeaux# Called upon early exit from a test due to timeout.
207*9e965d6fSRomain Jobredeauxfunction timeout() {
208*9e965d6fSRomain Jobredeaux    :
209*9e965d6fSRomain Jobredeaux}
210*9e965d6fSRomain Jobredeaux
211*9e965d6fSRomain Jobredeaux# Usage: testenv_set_up
212*9e965d6fSRomain Jobredeaux# Called prior to set_up. For use by testenv.sh.
213*9e965d6fSRomain Jobredeauxfunction testenv_set_up() {
214*9e965d6fSRomain Jobredeaux    :
215*9e965d6fSRomain Jobredeaux}
216*9e965d6fSRomain Jobredeaux
217*9e965d6fSRomain Jobredeaux# Usage: testenv_tear_down
218*9e965d6fSRomain Jobredeaux# Called after tear_down. For use by testenv.sh.
219*9e965d6fSRomain Jobredeauxfunction testenv_tear_down() {
220*9e965d6fSRomain Jobredeaux    :
221*9e965d6fSRomain Jobredeaux}
222*9e965d6fSRomain Jobredeaux
223*9e965d6fSRomain Jobredeaux# Usage: fail <message> [<message> ...]
224*9e965d6fSRomain Jobredeaux# Print failure message with context information, and mark the test as
225*9e965d6fSRomain Jobredeaux# a failure.  The context includes a stacktrace including the longest sequence
226*9e965d6fSRomain Jobredeaux# of calls outside this module.  (We exclude the top and bottom portions of
227*9e965d6fSRomain Jobredeaux# the stack because they just add noise.)  Also prints the contents of
228*9e965d6fSRomain Jobredeaux# $TEST_log.
229*9e965d6fSRomain Jobredeauxfunction fail() {
230*9e965d6fSRomain Jobredeaux    __show_log >&2
231*9e965d6fSRomain Jobredeaux    echo "${TEST_name} FAILED: $*." >&2
232*9e965d6fSRomain Jobredeaux    # Keep the original error message if we fail in `tear_down` after a failure.
233*9e965d6fSRomain Jobredeaux    [[ "${TEST_passed}" == "true" ]] && echo "$@" >"$TEST_TMPDIR"/__fail
234*9e965d6fSRomain Jobredeaux    TEST_passed="false"
235*9e965d6fSRomain Jobredeaux    __show_stack
236*9e965d6fSRomain Jobredeaux    # Cleanup as we are leaving the subshell now
237*9e965d6fSRomain Jobredeaux    __run_tear_down_after_failure
238*9e965d6fSRomain Jobredeaux    exit 1
239*9e965d6fSRomain Jobredeaux}
240*9e965d6fSRomain Jobredeaux
241*9e965d6fSRomain Jobredeauxfunction __run_tear_down_after_failure() {
242*9e965d6fSRomain Jobredeaux    # Skip `tear_down` after a failure in `tear_down` to prevent infinite
243*9e965d6fSRomain Jobredeaux    # recursion.
244*9e965d6fSRomain Jobredeaux    (( __in_tear_down )) && return
245*9e965d6fSRomain Jobredeaux    __in_tear_down=1
246*9e965d6fSRomain Jobredeaux    echo -e "\nTear down:\n" >&2
247*9e965d6fSRomain Jobredeaux    tear_down
248*9e965d6fSRomain Jobredeaux    testenv_tear_down
249*9e965d6fSRomain Jobredeaux}
250*9e965d6fSRomain Jobredeaux
251*9e965d6fSRomain Jobredeaux# Usage: warn <message>
252*9e965d6fSRomain Jobredeaux# Print a test warning with context information.
253*9e965d6fSRomain Jobredeaux# The context includes a stacktrace including the longest sequence
254*9e965d6fSRomain Jobredeaux# of calls outside this module.  (We exclude the top and bottom portions of
255*9e965d6fSRomain Jobredeaux# the stack because they just add noise.)
256*9e965d6fSRomain Jobredeauxfunction warn() {
257*9e965d6fSRomain Jobredeaux    __show_log >&2
258*9e965d6fSRomain Jobredeaux    echo "${TEST_name} WARNING: $1." >&2
259*9e965d6fSRomain Jobredeaux    __show_stack
260*9e965d6fSRomain Jobredeaux
261*9e965d6fSRomain Jobredeaux    if [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then
262*9e965d6fSRomain Jobredeaux      echo "${TEST_name} WARNING: $1." >> "$TEST_WARNINGS_OUTPUT_FILE"
263*9e965d6fSRomain Jobredeaux    fi
264*9e965d6fSRomain Jobredeaux}
265*9e965d6fSRomain Jobredeaux
266*9e965d6fSRomain Jobredeaux# Usage: show_stack
267*9e965d6fSRomain Jobredeaux# Prints the portion of the stack that does not belong to this module,
268*9e965d6fSRomain Jobredeaux# i.e. the user's code that called a failing assertion.  Stack may not
269*9e965d6fSRomain Jobredeaux# be available if Bash is reading commands from stdin; an error is
270*9e965d6fSRomain Jobredeaux# printed in that case.
271*9e965d6fSRomain Jobredeaux__show_stack() {
272*9e965d6fSRomain Jobredeaux    local i=0
273*9e965d6fSRomain Jobredeaux    local trace_found=0
274*9e965d6fSRomain Jobredeaux
275*9e965d6fSRomain Jobredeaux    # Skip over active calls within this module:
276*9e965d6fSRomain Jobredeaux    while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} == "${BASH_SOURCE[0]}" ]]; do
277*9e965d6fSRomain Jobredeaux       (( ++i ))
278*9e965d6fSRomain Jobredeaux    done
279*9e965d6fSRomain Jobredeaux
280*9e965d6fSRomain Jobredeaux    # Show all calls until the next one within this module (typically run_suite):
281*9e965d6fSRomain Jobredeaux    while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} != "${BASH_SOURCE[0]}" ]]; do
282*9e965d6fSRomain Jobredeaux        # Read online docs for BASH_LINENO to understand the strange offset.
283*9e965d6fSRomain Jobredeaux        # Undefined can occur in the BASH_SOURCE stack apparently when one exits from a subshell
284*9e965d6fSRomain Jobredeaux        echo "${BASH_SOURCE[i]:-"Unknown"}:${BASH_LINENO[i - 1]:-"Unknown"}: in call to ${FUNCNAME[i]:-"Unknown"}" >&2
285*9e965d6fSRomain Jobredeaux        (( ++i ))
286*9e965d6fSRomain Jobredeaux        trace_found=1
287*9e965d6fSRomain Jobredeaux    done
288*9e965d6fSRomain Jobredeaux
289*9e965d6fSRomain Jobredeaux    (( trace_found )) || echo "[Stack trace not available]" >&2
290*9e965d6fSRomain Jobredeaux}
291*9e965d6fSRomain Jobredeaux
292*9e965d6fSRomain Jobredeaux# Usage: expect_log <regexp> [error-message]
293*9e965d6fSRomain Jobredeaux# Asserts that $TEST_log matches regexp.  Prints the contents of
294*9e965d6fSRomain Jobredeaux# $TEST_log and the specified (optional) error message otherwise, and
295*9e965d6fSRomain Jobredeaux# returns non-zero.
296*9e965d6fSRomain Jobredeauxfunction expect_log() {
297*9e965d6fSRomain Jobredeaux    local pattern=$1
298*9e965d6fSRomain Jobredeaux    local message=${2:-Expected regexp "$pattern" not found}
299*9e965d6fSRomain Jobredeaux    grep -sq -- "$pattern" $TEST_log && return 0
300*9e965d6fSRomain Jobredeaux
301*9e965d6fSRomain Jobredeaux    fail "$message"
302*9e965d6fSRomain Jobredeaux    return 1
303*9e965d6fSRomain Jobredeaux}
304*9e965d6fSRomain Jobredeaux
305*9e965d6fSRomain Jobredeaux# Usage: expect_log_warn <regexp> [error-message]
306*9e965d6fSRomain Jobredeaux# Warns if $TEST_log does not match regexp.  Prints the contents of
307*9e965d6fSRomain Jobredeaux# $TEST_log and the specified (optional) error message on mismatch.
308*9e965d6fSRomain Jobredeauxfunction expect_log_warn() {
309*9e965d6fSRomain Jobredeaux    local pattern=$1
310*9e965d6fSRomain Jobredeaux    local message=${2:-Expected regexp "$pattern" not found}
311*9e965d6fSRomain Jobredeaux    grep -sq -- "$pattern" $TEST_log && return 0
312*9e965d6fSRomain Jobredeaux
313*9e965d6fSRomain Jobredeaux    warn "$message"
314*9e965d6fSRomain Jobredeaux    return 1
315*9e965d6fSRomain Jobredeaux}
316*9e965d6fSRomain Jobredeaux
317*9e965d6fSRomain Jobredeaux# Usage: expect_log_once <regexp> [error-message]
318*9e965d6fSRomain Jobredeaux# Asserts that $TEST_log contains one line matching <regexp>.
319*9e965d6fSRomain Jobredeaux# Prints the contents of $TEST_log and the specified (optional)
320*9e965d6fSRomain Jobredeaux# error message otherwise, and returns non-zero.
321*9e965d6fSRomain Jobredeauxfunction expect_log_once() {
322*9e965d6fSRomain Jobredeaux    local pattern=$1
323*9e965d6fSRomain Jobredeaux    local message=${2:-Expected regexp "$pattern" not found exactly once}
324*9e965d6fSRomain Jobredeaux    expect_log_n "$pattern" 1 "$message"
325*9e965d6fSRomain Jobredeaux}
326*9e965d6fSRomain Jobredeaux
327*9e965d6fSRomain Jobredeaux# Usage: expect_log_n <regexp> <count> [error-message]
328*9e965d6fSRomain Jobredeaux# Asserts that $TEST_log contains <count> lines matching <regexp>.
329*9e965d6fSRomain Jobredeaux# Prints the contents of $TEST_log and the specified (optional)
330*9e965d6fSRomain Jobredeaux# error message otherwise, and returns non-zero.
331*9e965d6fSRomain Jobredeauxfunction expect_log_n() {
332*9e965d6fSRomain Jobredeaux    local pattern=$1
333*9e965d6fSRomain Jobredeaux    local expectednum=${2:-1}
334*9e965d6fSRomain Jobredeaux    local message=${3:-Expected regexp "$pattern" not found exactly $expectednum times}
335*9e965d6fSRomain Jobredeaux    local count=$(grep -sc -- "$pattern" $TEST_log)
336*9e965d6fSRomain Jobredeaux    (( count == expectednum )) && return 0
337*9e965d6fSRomain Jobredeaux    fail "$message"
338*9e965d6fSRomain Jobredeaux    return 1
339*9e965d6fSRomain Jobredeaux}
340*9e965d6fSRomain Jobredeaux
341*9e965d6fSRomain Jobredeaux# Usage: expect_not_log <regexp> [error-message]
342*9e965d6fSRomain Jobredeaux# Asserts that $TEST_log does not match regexp.  Prints the contents
343*9e965d6fSRomain Jobredeaux# of $TEST_log and the specified (optional) error message otherwise, and
344*9e965d6fSRomain Jobredeaux# returns non-zero.
345*9e965d6fSRomain Jobredeauxfunction expect_not_log() {
346*9e965d6fSRomain Jobredeaux    local pattern=$1
347*9e965d6fSRomain Jobredeaux    local message=${2:-Unexpected regexp "$pattern" found}
348*9e965d6fSRomain Jobredeaux    grep -sq -- "$pattern" $TEST_log || return 0
349*9e965d6fSRomain Jobredeaux
350*9e965d6fSRomain Jobredeaux    fail "$message"
351*9e965d6fSRomain Jobredeaux    return 1
352*9e965d6fSRomain Jobredeaux}
353*9e965d6fSRomain Jobredeaux
354*9e965d6fSRomain Jobredeaux# Usage: expect_query_targets <arguments>
355*9e965d6fSRomain Jobredeaux# Checks that log file contains exactly the targets in the argument list.
356*9e965d6fSRomain Jobredeauxfunction expect_query_targets() {
357*9e965d6fSRomain Jobredeaux  for arg in "$@"; do
358*9e965d6fSRomain Jobredeaux    expect_log_once "^$arg$"
359*9e965d6fSRomain Jobredeaux  done
360*9e965d6fSRomain Jobredeaux
361*9e965d6fSRomain Jobredeaux# Checks that the number of lines started with '//' equals to the number of
362*9e965d6fSRomain Jobredeaux# arguments provided.
363*9e965d6fSRomain Jobredeaux  expect_log_n "^//[^ ]*$" $#
364*9e965d6fSRomain Jobredeaux}
365*9e965d6fSRomain Jobredeaux
366*9e965d6fSRomain Jobredeaux# Usage: expect_log_with_timeout <regexp> <timeout> [error-message]
367*9e965d6fSRomain Jobredeaux# Waits for the given regexp in the $TEST_log for up to timeout seconds.
368*9e965d6fSRomain Jobredeaux# Prints the contents of $TEST_log and the specified (optional)
369*9e965d6fSRomain Jobredeaux# error message otherwise, and returns non-zero.
370*9e965d6fSRomain Jobredeauxfunction expect_log_with_timeout() {
371*9e965d6fSRomain Jobredeaux    local pattern=$1
372*9e965d6fSRomain Jobredeaux    local timeout=$2
373*9e965d6fSRomain Jobredeaux    local message=${3:-Regexp "$pattern" not found in "$timeout" seconds}
374*9e965d6fSRomain Jobredeaux    local count=0
375*9e965d6fSRomain Jobredeaux    while (( count < timeout )); do
376*9e965d6fSRomain Jobredeaux      grep -sq -- "$pattern" "$TEST_log" && return 0
377*9e965d6fSRomain Jobredeaux      let count=count+1
378*9e965d6fSRomain Jobredeaux      sleep 1
379*9e965d6fSRomain Jobredeaux    done
380*9e965d6fSRomain Jobredeaux
381*9e965d6fSRomain Jobredeaux    grep -sq -- "$pattern" "$TEST_log" && return 0
382*9e965d6fSRomain Jobredeaux    fail "$message"
383*9e965d6fSRomain Jobredeaux    return 1
384*9e965d6fSRomain Jobredeaux}
385*9e965d6fSRomain Jobredeaux
386*9e965d6fSRomain Jobredeaux# Usage: expect_cmd_with_timeout <expected> <cmd> [timeout]
387*9e965d6fSRomain Jobredeaux# Repeats the command once a second for up to timeout seconds (10s by default),
388*9e965d6fSRomain Jobredeaux# until the output matches the expected value. Fails and returns 1 if
389*9e965d6fSRomain Jobredeaux# the command does not return the expected value in the end.
390*9e965d6fSRomain Jobredeauxfunction expect_cmd_with_timeout() {
391*9e965d6fSRomain Jobredeaux    local expected="$1"
392*9e965d6fSRomain Jobredeaux    local cmd="$2"
393*9e965d6fSRomain Jobredeaux    local timeout=${3:-10}
394*9e965d6fSRomain Jobredeaux    local count=0
395*9e965d6fSRomain Jobredeaux    while (( count < timeout )); do
396*9e965d6fSRomain Jobredeaux      local actual="$($cmd)"
397*9e965d6fSRomain Jobredeaux      [[ "$expected" == "$actual" ]] && return 0
398*9e965d6fSRomain Jobredeaux      (( ++count ))
399*9e965d6fSRomain Jobredeaux      sleep 1
400*9e965d6fSRomain Jobredeaux    done
401*9e965d6fSRomain Jobredeaux
402*9e965d6fSRomain Jobredeaux    [[ "$expected" == "$actual" ]] && return 0
403*9e965d6fSRomain Jobredeaux    fail "Expected '${expected}' within ${timeout}s, was '${actual}'"
404*9e965d6fSRomain Jobredeaux    return 1
405*9e965d6fSRomain Jobredeaux}
406*9e965d6fSRomain Jobredeaux
407*9e965d6fSRomain Jobredeaux# Usage: assert_one_of <expected_list>... <actual>
408*9e965d6fSRomain Jobredeaux# Asserts that actual is one of the items in expected_list
409*9e965d6fSRomain Jobredeaux#
410*9e965d6fSRomain Jobredeaux# Example:
411*9e965d6fSRomain Jobredeaux#     local expected=( "foo", "bar", "baz" )
412*9e965d6fSRomain Jobredeaux#     assert_one_of $expected $actual
413*9e965d6fSRomain Jobredeauxfunction assert_one_of() {
414*9e965d6fSRomain Jobredeaux    local args=("$@")
415*9e965d6fSRomain Jobredeaux    local last_arg_index=$((${#args[@]} - 1))
416*9e965d6fSRomain Jobredeaux    local actual=${args[last_arg_index]}
417*9e965d6fSRomain Jobredeaux    unset args[last_arg_index]
418*9e965d6fSRomain Jobredeaux    for expected_item in "${args[@]}"; do
419*9e965d6fSRomain Jobredeaux      [[ "$expected_item" == "$actual" ]] && return 0
420*9e965d6fSRomain Jobredeaux    done;
421*9e965d6fSRomain Jobredeaux
422*9e965d6fSRomain Jobredeaux    fail "Expected one of '${args[*]}', was '$actual'"
423*9e965d6fSRomain Jobredeaux    return 1
424*9e965d6fSRomain Jobredeaux}
425*9e965d6fSRomain Jobredeaux
426*9e965d6fSRomain Jobredeaux# Usage: assert_not_one_of <expected_list>... <actual>
427*9e965d6fSRomain Jobredeaux# Asserts that actual is not one of the items in expected_list
428*9e965d6fSRomain Jobredeaux#
429*9e965d6fSRomain Jobredeaux# Example:
430*9e965d6fSRomain Jobredeaux#     local unexpected=( "foo", "bar", "baz" )
431*9e965d6fSRomain Jobredeaux#     assert_not_one_of $unexpected $actual
432*9e965d6fSRomain Jobredeauxfunction assert_not_one_of() {
433*9e965d6fSRomain Jobredeaux    local args=("$@")
434*9e965d6fSRomain Jobredeaux    local last_arg_index=$((${#args[@]} - 1))
435*9e965d6fSRomain Jobredeaux    local actual=${args[last_arg_index]}
436*9e965d6fSRomain Jobredeaux    unset args[last_arg_index]
437*9e965d6fSRomain Jobredeaux    for expected_item in "${args[@]}"; do
438*9e965d6fSRomain Jobredeaux      if [[ "$expected_item" == "$actual" ]]; then
439*9e965d6fSRomain Jobredeaux        fail "'${args[*]}' contains '$actual'"
440*9e965d6fSRomain Jobredeaux        return 1
441*9e965d6fSRomain Jobredeaux      fi
442*9e965d6fSRomain Jobredeaux    done;
443*9e965d6fSRomain Jobredeaux
444*9e965d6fSRomain Jobredeaux    return 0
445*9e965d6fSRomain Jobredeaux}
446*9e965d6fSRomain Jobredeaux
447*9e965d6fSRomain Jobredeaux# Usage: assert_equals <expected> <actual>
448*9e965d6fSRomain Jobredeaux# Asserts [[ expected == actual ]].
449*9e965d6fSRomain Jobredeauxfunction assert_equals() {
450*9e965d6fSRomain Jobredeaux    local expected=$1 actual=$2
451*9e965d6fSRomain Jobredeaux    [[ "$expected" == "$actual" ]] && return 0
452*9e965d6fSRomain Jobredeaux
453*9e965d6fSRomain Jobredeaux    fail "Expected '$expected', was '$actual'"
454*9e965d6fSRomain Jobredeaux    return 1
455*9e965d6fSRomain Jobredeaux}
456*9e965d6fSRomain Jobredeaux
457*9e965d6fSRomain Jobredeaux# Usage: assert_not_equals <unexpected> <actual>
458*9e965d6fSRomain Jobredeaux# Asserts [[ unexpected != actual ]].
459*9e965d6fSRomain Jobredeauxfunction assert_not_equals() {
460*9e965d6fSRomain Jobredeaux    local unexpected=$1 actual=$2
461*9e965d6fSRomain Jobredeaux    [[ "$unexpected" != "$actual" ]] && return 0;
462*9e965d6fSRomain Jobredeaux
463*9e965d6fSRomain Jobredeaux    fail "Expected not '${unexpected}', was '${actual}'"
464*9e965d6fSRomain Jobredeaux    return 1
465*9e965d6fSRomain Jobredeaux}
466*9e965d6fSRomain Jobredeaux
467*9e965d6fSRomain Jobredeaux# Usage: assert_contains <regexp> <file> [error-message]
468*9e965d6fSRomain Jobredeaux# Asserts that file matches regexp.  Prints the contents of
469*9e965d6fSRomain Jobredeaux# file and the specified (optional) error message otherwise, and
470*9e965d6fSRomain Jobredeaux# returns non-zero.
471*9e965d6fSRomain Jobredeauxfunction assert_contains() {
472*9e965d6fSRomain Jobredeaux    local pattern=$1
473*9e965d6fSRomain Jobredeaux    local file=$2
474*9e965d6fSRomain Jobredeaux    local message=${3:-Expected regexp "$pattern" not found in "$file"}
475*9e965d6fSRomain Jobredeaux    grep -sq -- "$pattern" "$file" && return 0
476*9e965d6fSRomain Jobredeaux
477*9e965d6fSRomain Jobredeaux    cat "$file" >&2
478*9e965d6fSRomain Jobredeaux    fail "$message"
479*9e965d6fSRomain Jobredeaux    return 1
480*9e965d6fSRomain Jobredeaux}
481*9e965d6fSRomain Jobredeaux
482*9e965d6fSRomain Jobredeaux# Usage: assert_not_contains <regexp> <file> [error-message]
483*9e965d6fSRomain Jobredeaux# Asserts that file does not match regexp.  Prints the contents of
484*9e965d6fSRomain Jobredeaux# file and the specified (optional) error message otherwise, and
485*9e965d6fSRomain Jobredeaux# returns non-zero.
486*9e965d6fSRomain Jobredeauxfunction assert_not_contains() {
487*9e965d6fSRomain Jobredeaux    local pattern=$1
488*9e965d6fSRomain Jobredeaux    local file=$2
489*9e965d6fSRomain Jobredeaux    local message=${3:-Expected regexp "$pattern" found in "$file"}
490*9e965d6fSRomain Jobredeaux
491*9e965d6fSRomain Jobredeaux    if [[ -f "$file" ]]; then
492*9e965d6fSRomain Jobredeaux      grep -sq -- "$pattern" "$file" || return 0
493*9e965d6fSRomain Jobredeaux    else
494*9e965d6fSRomain Jobredeaux      fail "$file is not a file: $message"
495*9e965d6fSRomain Jobredeaux      return 1
496*9e965d6fSRomain Jobredeaux    fi
497*9e965d6fSRomain Jobredeaux
498*9e965d6fSRomain Jobredeaux    cat "$file" >&2
499*9e965d6fSRomain Jobredeaux    fail "$message"
500*9e965d6fSRomain Jobredeaux    return 1
501*9e965d6fSRomain Jobredeaux}
502*9e965d6fSRomain Jobredeaux
503*9e965d6fSRomain Jobredeauxfunction assert_contains_n() {
504*9e965d6fSRomain Jobredeaux    local pattern=$1
505*9e965d6fSRomain Jobredeaux    local expectednum=${2:-1}
506*9e965d6fSRomain Jobredeaux    local file=$3
507*9e965d6fSRomain Jobredeaux    local message=${4:-Expected regexp "$pattern" not found exactly $expectednum times}
508*9e965d6fSRomain Jobredeaux    local count
509*9e965d6fSRomain Jobredeaux    if [[ -f "$file" ]]; then
510*9e965d6fSRomain Jobredeaux      count=$(grep -sc -- "$pattern" "$file")
511*9e965d6fSRomain Jobredeaux    else
512*9e965d6fSRomain Jobredeaux      fail "$file is not a file: $message"
513*9e965d6fSRomain Jobredeaux      return 1
514*9e965d6fSRomain Jobredeaux    fi
515*9e965d6fSRomain Jobredeaux    (( count == expectednum )) && return 0
516*9e965d6fSRomain Jobredeaux
517*9e965d6fSRomain Jobredeaux    cat "$file" >&2
518*9e965d6fSRomain Jobredeaux    fail "$message"
519*9e965d6fSRomain Jobredeaux    return 1
520*9e965d6fSRomain Jobredeaux}
521*9e965d6fSRomain Jobredeaux
522*9e965d6fSRomain Jobredeaux# Updates the global variables TESTS if
523*9e965d6fSRomain Jobredeaux# sharding is enabled, i.e. ($TEST_TOTAL_SHARDS > 0).
524*9e965d6fSRomain Jobredeauxfunction __update_shards() {
525*9e965d6fSRomain Jobredeaux    [[ -z "${TEST_TOTAL_SHARDS-}" ]] && return 0
526*9e965d6fSRomain Jobredeaux
527*9e965d6fSRomain Jobredeaux    (( TEST_TOTAL_SHARDS > 0 )) ||
528*9e965d6fSRomain Jobredeaux      { echo "Invalid total shards ${TEST_TOTAL_SHARDS}" >&2; exit 1; }
529*9e965d6fSRomain Jobredeaux
530*9e965d6fSRomain Jobredeaux    (( TEST_SHARD_INDEX < 0 || TEST_SHARD_INDEX >= TEST_TOTAL_SHARDS )) &&
531*9e965d6fSRomain Jobredeaux      { echo "Invalid shard ${TEST_SHARD_INDEX}" >&2; exit 1; }
532*9e965d6fSRomain Jobredeaux
533*9e965d6fSRomain Jobredeaux    IFS=$'\n' read -rd $'\0' -a TESTS < <(
534*9e965d6fSRomain Jobredeaux        for test in "${TESTS[@]}"; do echo "$test"; done |
535*9e965d6fSRomain Jobredeaux            awk "NR % ${TEST_TOTAL_SHARDS} == ${TEST_SHARD_INDEX}" &&
536*9e965d6fSRomain Jobredeaux            echo -en '\0')
537*9e965d6fSRomain Jobredeaux
538*9e965d6fSRomain Jobredeaux    [[ -z "${TEST_SHARD_STATUS_FILE-}" ]] || touch "$TEST_SHARD_STATUS_FILE"
539*9e965d6fSRomain Jobredeaux}
540*9e965d6fSRomain Jobredeaux
541*9e965d6fSRomain Jobredeaux# Usage: __test_terminated <signal-number>
542*9e965d6fSRomain Jobredeaux# Handler that is called when the test terminated unexpectedly
543*9e965d6fSRomain Jobredeauxfunction __test_terminated() {
544*9e965d6fSRomain Jobredeaux    __show_log >&2
545*9e965d6fSRomain Jobredeaux    echo "$TEST_name FAILED: terminated by signal $1." >&2
546*9e965d6fSRomain Jobredeaux    TEST_passed="false"
547*9e965d6fSRomain Jobredeaux    __show_stack
548*9e965d6fSRomain Jobredeaux    timeout
549*9e965d6fSRomain Jobredeaux    exit 1
550*9e965d6fSRomain Jobredeaux}
551*9e965d6fSRomain Jobredeaux
552*9e965d6fSRomain Jobredeaux# Usage: __test_terminated_err
553*9e965d6fSRomain Jobredeaux# Handler that is called when the test terminated unexpectedly due to "errexit".
554*9e965d6fSRomain Jobredeauxfunction __test_terminated_err() {
555*9e965d6fSRomain Jobredeaux    # When a subshell exits due to signal ERR, its parent shell also exits,
556*9e965d6fSRomain Jobredeaux    # thus the signal handler is called recursively and we print out the
557*9e965d6fSRomain Jobredeaux    # error message and stack trace multiple times. We're only interested
558*9e965d6fSRomain Jobredeaux    # in the first one though, as it contains the most information, so ignore
559*9e965d6fSRomain Jobredeaux    # all following.
560*9e965d6fSRomain Jobredeaux    if [[ -f $TEST_TMPDIR/__err_handled ]]; then
561*9e965d6fSRomain Jobredeaux      exit 1
562*9e965d6fSRomain Jobredeaux    fi
563*9e965d6fSRomain Jobredeaux    __show_log >&2
564*9e965d6fSRomain Jobredeaux    if [[ ! -z "$TEST_name" ]]; then
565*9e965d6fSRomain Jobredeaux      echo -n "$TEST_name " >&2
566*9e965d6fSRomain Jobredeaux    fi
567*9e965d6fSRomain Jobredeaux    echo "FAILED: terminated because this command returned a non-zero status:" >&2
568*9e965d6fSRomain Jobredeaux    touch $TEST_TMPDIR/__err_handled
569*9e965d6fSRomain Jobredeaux    TEST_passed="false"
570*9e965d6fSRomain Jobredeaux    __show_stack
571*9e965d6fSRomain Jobredeaux    # If $TEST_name is still empty, the test suite failed before we even started
572*9e965d6fSRomain Jobredeaux    # to run tests, so we shouldn't call tear_down.
573*9e965d6fSRomain Jobredeaux    if [[ -n "$TEST_name" ]]; then
574*9e965d6fSRomain Jobredeaux      __run_tear_down_after_failure
575*9e965d6fSRomain Jobredeaux    fi
576*9e965d6fSRomain Jobredeaux    exit 1
577*9e965d6fSRomain Jobredeaux}
578*9e965d6fSRomain Jobredeaux
579*9e965d6fSRomain Jobredeaux# Usage: __trap_with_arg <handler> <signals ...>
580*9e965d6fSRomain Jobredeaux# Helper to install a trap handler for several signals preserving the signal
581*9e965d6fSRomain Jobredeaux# number, so that the signal number is available to the trap handler.
582*9e965d6fSRomain Jobredeauxfunction __trap_with_arg() {
583*9e965d6fSRomain Jobredeaux    func="$1" ; shift
584*9e965d6fSRomain Jobredeaux    for sig ; do
585*9e965d6fSRomain Jobredeaux        trap "$func $sig" "$sig"
586*9e965d6fSRomain Jobredeaux    done
587*9e965d6fSRomain Jobredeaux}
588*9e965d6fSRomain Jobredeaux
589*9e965d6fSRomain Jobredeaux# Usage: <node> <block>
590*9e965d6fSRomain Jobredeaux# Adds the block to the given node in the report file. Quotes in the in
591*9e965d6fSRomain Jobredeaux# arguments need to be escaped.
592*9e965d6fSRomain Jobredeauxfunction __log_to_test_report() {
593*9e965d6fSRomain Jobredeaux    local node="$1"
594*9e965d6fSRomain Jobredeaux    local block="$2"
595*9e965d6fSRomain Jobredeaux    if [[ ! -e "$XML_OUTPUT_FILE" ]]; then
596*9e965d6fSRomain Jobredeaux        local xml_header='<?xml version="1.0" encoding="UTF-8"?>'
597*9e965d6fSRomain Jobredeaux        echo "${xml_header}<testsuites></testsuites>" > "$XML_OUTPUT_FILE"
598*9e965d6fSRomain Jobredeaux    fi
599*9e965d6fSRomain Jobredeaux
600*9e965d6fSRomain Jobredeaux    # replace match on node with block and match
601*9e965d6fSRomain Jobredeaux    # replacement expression only needs escaping for quotes
602*9e965d6fSRomain Jobredeaux    perl -e "\
603*9e965d6fSRomain Jobredeaux\$input = @ARGV[0]; \
604*9e965d6fSRomain Jobredeaux\$/=undef; \
605*9e965d6fSRomain Jobredeauxopen FILE, '+<$XML_OUTPUT_FILE'; \
606*9e965d6fSRomain Jobredeaux\$content = <FILE>; \
607*9e965d6fSRomain Jobredeauxif (\$content =~ /($node.*)\$/) { \
608*9e965d6fSRomain Jobredeaux  seek FILE, 0, 0; \
609*9e965d6fSRomain Jobredeaux  print FILE \$\` . \$input . \$1; \
610*9e965d6fSRomain Jobredeaux}; \
611*9e965d6fSRomain Jobredeauxclose FILE" "$block"
612*9e965d6fSRomain Jobredeaux}
613*9e965d6fSRomain Jobredeaux
614*9e965d6fSRomain Jobredeaux# Usage: <total> <passed>
615*9e965d6fSRomain Jobredeaux# Adds the test summaries to the xml nodes.
616*9e965d6fSRomain Jobredeauxfunction __finish_test_report() {
617*9e965d6fSRomain Jobredeaux    local suite_name="$1"
618*9e965d6fSRomain Jobredeaux    local total="$2"
619*9e965d6fSRomain Jobredeaux    local passed="$3"
620*9e965d6fSRomain Jobredeaux    local failed=$((total - passed))
621*9e965d6fSRomain Jobredeaux
622*9e965d6fSRomain Jobredeaux    # Update the xml output with the suite name and total number of
623*9e965d6fSRomain Jobredeaux    # passed/failed tests.
624*9e965d6fSRomain Jobredeaux    cat "$XML_OUTPUT_FILE" | \
625*9e965d6fSRomain Jobredeaux      sed \
626*9e965d6fSRomain Jobredeaux        "s/<testsuites>/<testsuites tests=\"$total\" failures=\"0\" errors=\"$failed\">/" | \
627*9e965d6fSRomain Jobredeaux      sed \
628*9e965d6fSRomain Jobredeaux        "s/<testsuite>/<testsuite name=\"${suite_name}\" tests=\"$total\" failures=\"0\" errors=\"$failed\">/" \
629*9e965d6fSRomain Jobredeaux        > "${XML_OUTPUT_FILE}.bak"
630*9e965d6fSRomain Jobredeaux
631*9e965d6fSRomain Jobredeaux    rm -f "$XML_OUTPUT_FILE"
632*9e965d6fSRomain Jobredeaux    mv "${XML_OUTPUT_FILE}.bak" "$XML_OUTPUT_FILE"
633*9e965d6fSRomain Jobredeaux}
634*9e965d6fSRomain Jobredeaux
635*9e965d6fSRomain Jobredeaux# Multi-platform timestamp function
636*9e965d6fSRomain JobredeauxUNAME=$(uname -s | tr 'A-Z' 'a-z')
637*9e965d6fSRomain Jobredeauxif [[ "$UNAME" == "linux" ]] || [[ "$UNAME" =~ msys_nt* ]]; then
638*9e965d6fSRomain Jobredeaux    function timestamp() {
639*9e965d6fSRomain Jobredeaux      echo $(($(date +%s%N)/1000000))
640*9e965d6fSRomain Jobredeaux    }
641*9e965d6fSRomain Jobredeauxelse
642*9e965d6fSRomain Jobredeaux    function timestamp() {
643*9e965d6fSRomain Jobredeaux      # macOS and BSDs do not have %N, so Python is the best we can do.
644*9e965d6fSRomain Jobredeaux      # LC_ALL=C works around python 3.8 and 3.9 crash on macOS when the
645*9e965d6fSRomain Jobredeaux      # filesystem encoding is unspecified (e.g. when LANG=en_US).
646*9e965d6fSRomain Jobredeaux      local PYTHON=python
647*9e965d6fSRomain Jobredeaux      command -v python3 &> /dev/null && PYTHON=python3
648*9e965d6fSRomain Jobredeaux      LC_ALL=C "${PYTHON}" -c 'import time; print(int(round(time.time() * 1000)))'
649*9e965d6fSRomain Jobredeaux    }
650*9e965d6fSRomain Jobredeauxfi
651*9e965d6fSRomain Jobredeaux
652*9e965d6fSRomain Jobredeauxfunction get_run_time() {
653*9e965d6fSRomain Jobredeaux  local ts_start=$1
654*9e965d6fSRomain Jobredeaux  local ts_end=$2
655*9e965d6fSRomain Jobredeaux  run_time_ms=$((ts_end - ts_start))
656*9e965d6fSRomain Jobredeaux  echo $((run_time_ms / 1000)).${run_time_ms: -3}
657*9e965d6fSRomain Jobredeaux}
658*9e965d6fSRomain Jobredeaux
659*9e965d6fSRomain Jobredeaux# Usage: run_tests <suite-comment>
660*9e965d6fSRomain Jobredeaux# Must be called from the end of the user's test suite.
661*9e965d6fSRomain Jobredeaux# Calls exit with zero on success, non-zero otherwise.
662*9e965d6fSRomain Jobredeauxfunction run_suite() {
663*9e965d6fSRomain Jobredeaux  local message="$1"
664*9e965d6fSRomain Jobredeaux  # The name of the suite should be the script being run, which
665*9e965d6fSRomain Jobredeaux  # will be the filename with the ".sh" extension removed.
666*9e965d6fSRomain Jobredeaux  local suite_name="$(basename "$0")"
667*9e965d6fSRomain Jobredeaux
668*9e965d6fSRomain Jobredeaux  echo >&2
669*9e965d6fSRomain Jobredeaux  echo "$message" >&2
670*9e965d6fSRomain Jobredeaux  echo >&2
671*9e965d6fSRomain Jobredeaux
672*9e965d6fSRomain Jobredeaux  __log_to_test_report "<\/testsuites>" "<testsuite></testsuite>"
673*9e965d6fSRomain Jobredeaux
674*9e965d6fSRomain Jobredeaux  local total=0
675*9e965d6fSRomain Jobredeaux  local passed=0
676*9e965d6fSRomain Jobredeaux
677*9e965d6fSRomain Jobredeaux  atexit "cleanup"
678*9e965d6fSRomain Jobredeaux
679*9e965d6fSRomain Jobredeaux  # If the user didn't specify an explicit list of tests (e.g. a
680*9e965d6fSRomain Jobredeaux  # working set), use them all.
681*9e965d6fSRomain Jobredeaux  if (( ${#TESTS[@]} == 0 )); then
682*9e965d6fSRomain Jobredeaux    # Even if there aren't any tests, this needs to succeed.
683*9e965d6fSRomain Jobredeaux    local all_tests=()
684*9e965d6fSRomain Jobredeaux    IFS=$'\n' read -d $'\0' -ra all_tests < <(
685*9e965d6fSRomain Jobredeaux        declare -F | awk '{print $3}' | grep ^test_ || true; echo -en '\0')
686*9e965d6fSRomain Jobredeaux
687*9e965d6fSRomain Jobredeaux    if (( "${#_TEST_FILTERS[@]}" == 0 )); then
688*9e965d6fSRomain Jobredeaux      # Use ${array[@]+"${array[@]}"} idiom to avoid errors when running with
689*9e965d6fSRomain Jobredeaux      # Bash version <= 4.4 with `nounset` when `all_tests` is empty (
690*9e965d6fSRomain Jobredeaux      # https://github.com/bminor/bash/blob/a0c0a00fc419b7bc08202a79134fcd5bc0427071/CHANGES#L62-L63).
691*9e965d6fSRomain Jobredeaux      TESTS=("${all_tests[@]+${all_tests[@]}}")
692*9e965d6fSRomain Jobredeaux    else
693*9e965d6fSRomain Jobredeaux      for t in "${all_tests[@]+${all_tests[@]}}"; do
694*9e965d6fSRomain Jobredeaux        local matches=0
695*9e965d6fSRomain Jobredeaux        for f in "${_TEST_FILTERS[@]}"; do
696*9e965d6fSRomain Jobredeaux          # We purposely want to glob match.
697*9e965d6fSRomain Jobredeaux          # shellcheck disable=SC2053
698*9e965d6fSRomain Jobredeaux          [[ "$t" = $f ]] && matches=1 && break
699*9e965d6fSRomain Jobredeaux        done
700*9e965d6fSRomain Jobredeaux        if (( matches )); then
701*9e965d6fSRomain Jobredeaux          TESTS+=("$t")
702*9e965d6fSRomain Jobredeaux        fi
703*9e965d6fSRomain Jobredeaux      done
704*9e965d6fSRomain Jobredeaux    fi
705*9e965d6fSRomain Jobredeaux
706*9e965d6fSRomain Jobredeaux  elif [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then
707*9e965d6fSRomain Jobredeaux    if grep -q "TESTS=" "$TEST_script" ; then
708*9e965d6fSRomain Jobredeaux      echo "TESTS variable overridden in sh_test. Please remove before submitting" \
709*9e965d6fSRomain Jobredeaux        >> "$TEST_WARNINGS_OUTPUT_FILE"
710*9e965d6fSRomain Jobredeaux    fi
711*9e965d6fSRomain Jobredeaux  fi
712*9e965d6fSRomain Jobredeaux
713*9e965d6fSRomain Jobredeaux  # Reset TESTS in the common case where it contains a single empty string.
714*9e965d6fSRomain Jobredeaux  if [[ -z "${TESTS[*]-}" ]]; then
715*9e965d6fSRomain Jobredeaux    TESTS=()
716*9e965d6fSRomain Jobredeaux  fi
717*9e965d6fSRomain Jobredeaux  local original_tests_size=${#TESTS[@]}
718*9e965d6fSRomain Jobredeaux
719*9e965d6fSRomain Jobredeaux  __update_shards
720*9e965d6fSRomain Jobredeaux
721*9e965d6fSRomain Jobredeaux  if [[ "${#TESTS[@]}" -ne 0 ]]; then
722*9e965d6fSRomain Jobredeaux    for TEST_name in "${TESTS[@]}"; do
723*9e965d6fSRomain Jobredeaux      >"$TEST_log" # Reset the log.
724*9e965d6fSRomain Jobredeaux      TEST_passed="true"
725*9e965d6fSRomain Jobredeaux
726*9e965d6fSRomain Jobredeaux      (( ++total ))
727*9e965d6fSRomain Jobredeaux      if [[ "$TEST_verbose" == "true" ]]; then
728*9e965d6fSRomain Jobredeaux          date >&2
729*9e965d6fSRomain Jobredeaux          __pad "$TEST_name" '*' >&2
730*9e965d6fSRomain Jobredeaux      fi
731*9e965d6fSRomain Jobredeaux
732*9e965d6fSRomain Jobredeaux      local run_time="0.0"
733*9e965d6fSRomain Jobredeaux      rm -f "${TEST_TMPDIR}"/{__ts_start,__ts_end}
734*9e965d6fSRomain Jobredeaux
735*9e965d6fSRomain Jobredeaux      if [[ "$(type -t "$TEST_name")" == function ]]; then
736*9e965d6fSRomain Jobredeaux        # Save exit handlers eventually set.
737*9e965d6fSRomain Jobredeaux        local SAVED_ATEXIT="$ATEXIT";
738*9e965d6fSRomain Jobredeaux        ATEXIT=
739*9e965d6fSRomain Jobredeaux
740*9e965d6fSRomain Jobredeaux        # Run test in a subshell.
741*9e965d6fSRomain Jobredeaux        rm -f "${TEST_TMPDIR}"/__err_handled
742*9e965d6fSRomain Jobredeaux        __trap_with_arg __test_terminated INT KILL PIPE TERM ABRT FPE ILL QUIT SEGV
743*9e965d6fSRomain Jobredeaux
744*9e965d6fSRomain Jobredeaux        # Remember -o pipefail value and disable it for the subshell result
745*9e965d6fSRomain Jobredeaux        # collection.
746*9e965d6fSRomain Jobredeaux        if [[ "${SHELLOPTS}" =~ (^|:)pipefail(:|$) ]]; then
747*9e965d6fSRomain Jobredeaux          local __opt_switch=-o
748*9e965d6fSRomain Jobredeaux        else
749*9e965d6fSRomain Jobredeaux          local __opt_switch=+o
750*9e965d6fSRomain Jobredeaux        fi
751*9e965d6fSRomain Jobredeaux        set +o pipefail
752*9e965d6fSRomain Jobredeaux        (
753*9e965d6fSRomain Jobredeaux          set "${__opt_switch}" pipefail
754*9e965d6fSRomain Jobredeaux          # if errexit is enabled, make sure we run cleanup and collect the log.
755*9e965d6fSRomain Jobredeaux          if [[ "$-" = *e* ]]; then
756*9e965d6fSRomain Jobredeaux            set -E
757*9e965d6fSRomain Jobredeaux            trap __test_terminated_err ERR
758*9e965d6fSRomain Jobredeaux          fi
759*9e965d6fSRomain Jobredeaux          timestamp >"${TEST_TMPDIR}"/__ts_start
760*9e965d6fSRomain Jobredeaux          testenv_set_up
761*9e965d6fSRomain Jobredeaux          set_up
762*9e965d6fSRomain Jobredeaux          eval "$TEST_name"
763*9e965d6fSRomain Jobredeaux          __in_tear_down=1
764*9e965d6fSRomain Jobredeaux          tear_down
765*9e965d6fSRomain Jobredeaux          testenv_tear_down
766*9e965d6fSRomain Jobredeaux          timestamp >"${TEST_TMPDIR}"/__ts_end
767*9e965d6fSRomain Jobredeaux          test "$TEST_passed" == "true"
768*9e965d6fSRomain Jobredeaux        ) 2>&1 | tee "${TEST_TMPDIR}"/__log
769*9e965d6fSRomain Jobredeaux        # Note that tee will prevent the control flow continuing if the test
770*9e965d6fSRomain Jobredeaux        # spawned any processes which are still running and have not closed
771*9e965d6fSRomain Jobredeaux        # their stdout.
772*9e965d6fSRomain Jobredeaux
773*9e965d6fSRomain Jobredeaux        test_subshell_status=${PIPESTATUS[0]}
774*9e965d6fSRomain Jobredeaux        set "${__opt_switch}" pipefail
775*9e965d6fSRomain Jobredeaux        if (( test_subshell_status != 0 )); then
776*9e965d6fSRomain Jobredeaux          TEST_passed="false"
777*9e965d6fSRomain Jobredeaux          # Ensure that an end time is recorded in case the test subshell
778*9e965d6fSRomain Jobredeaux          # terminated prematurely.
779*9e965d6fSRomain Jobredeaux          [[ -f "$TEST_TMPDIR"/__ts_end ]] || timestamp >"$TEST_TMPDIR"/__ts_end
780*9e965d6fSRomain Jobredeaux        fi
781*9e965d6fSRomain Jobredeaux
782*9e965d6fSRomain Jobredeaux        # Calculate run time for the testcase.
783*9e965d6fSRomain Jobredeaux        local ts_start
784*9e965d6fSRomain Jobredeaux        ts_start=$(<"${TEST_TMPDIR}"/__ts_start)
785*9e965d6fSRomain Jobredeaux        local ts_end
786*9e965d6fSRomain Jobredeaux        ts_end=$(<"${TEST_TMPDIR}"/__ts_end)
787*9e965d6fSRomain Jobredeaux        run_time=$(get_run_time $ts_start $ts_end)
788*9e965d6fSRomain Jobredeaux
789*9e965d6fSRomain Jobredeaux        # Eventually restore exit handlers.
790*9e965d6fSRomain Jobredeaux        if [[ -n "$SAVED_ATEXIT" ]]; then
791*9e965d6fSRomain Jobredeaux          ATEXIT="$SAVED_ATEXIT"
792*9e965d6fSRomain Jobredeaux          trap "$ATEXIT" EXIT
793*9e965d6fSRomain Jobredeaux        fi
794*9e965d6fSRomain Jobredeaux      else # Bad test explicitly specified in $TESTS.
795*9e965d6fSRomain Jobredeaux        fail "Not a function: '$TEST_name'"
796*9e965d6fSRomain Jobredeaux      fi
797*9e965d6fSRomain Jobredeaux
798*9e965d6fSRomain Jobredeaux      local testcase_tag=""
799*9e965d6fSRomain Jobredeaux
800*9e965d6fSRomain Jobredeaux      local red='\033[0;31m'
801*9e965d6fSRomain Jobredeaux      local green='\033[0;32m'
802*9e965d6fSRomain Jobredeaux      local no_color='\033[0m'
803*9e965d6fSRomain Jobredeaux
804*9e965d6fSRomain Jobredeaux      if [[ "$TEST_verbose" == "true" ]]; then
805*9e965d6fSRomain Jobredeaux          echo >&2
806*9e965d6fSRomain Jobredeaux      fi
807*9e965d6fSRomain Jobredeaux
808*9e965d6fSRomain Jobredeaux      if [[ "$TEST_passed" == "true" ]]; then
809*9e965d6fSRomain Jobredeaux        if [[ "$TEST_verbose" == "true" ]]; then
810*9e965d6fSRomain Jobredeaux          echo -e "${green}PASSED${no_color}: ${TEST_name}" >&2
811*9e965d6fSRomain Jobredeaux        fi
812*9e965d6fSRomain Jobredeaux        (( ++passed ))
813*9e965d6fSRomain Jobredeaux        testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"></testcase>"
814*9e965d6fSRomain Jobredeaux      else
815*9e965d6fSRomain Jobredeaux        echo -e "${red}FAILED${no_color}: ${TEST_name}" >&2
816*9e965d6fSRomain Jobredeaux        # end marker in CDATA cannot be escaped, we need to split the CDATA sections
817*9e965d6fSRomain Jobredeaux        log=$(sed 's/]]>/]]>]]&gt;<![CDATA[/g' "${TEST_TMPDIR}"/__log)
818*9e965d6fSRomain Jobredeaux        fail_msg=$(cat "${TEST_TMPDIR}"/__fail 2> /dev/null || echo "No failure message")
819*9e965d6fSRomain Jobredeaux        # Replacing '&' with '&amp;', '<' with '&lt;', '>' with '&gt;', and '"' with '&quot;'
820*9e965d6fSRomain Jobredeaux        escaped_fail_msg=$(echo "$fail_msg" | sed 's/&/\&amp;/g' | sed 's/</\&lt;/g' | sed 's/>/\&gt;/g' | sed 's/"/\&quot;/g')
821*9e965d6fSRomain Jobredeaux        testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"><error message=\"${escaped_fail_msg}\"><![CDATA[${log}]]></error></testcase>"
822*9e965d6fSRomain Jobredeaux      fi
823*9e965d6fSRomain Jobredeaux
824*9e965d6fSRomain Jobredeaux      if [[ "$TEST_verbose" == "true" ]]; then
825*9e965d6fSRomain Jobredeaux          echo >&2
826*9e965d6fSRomain Jobredeaux      fi
827*9e965d6fSRomain Jobredeaux      __log_to_test_report "<\/testsuite>" "$testcase_tag"
828*9e965d6fSRomain Jobredeaux    done
829*9e965d6fSRomain Jobredeaux  fi
830*9e965d6fSRomain Jobredeaux
831*9e965d6fSRomain Jobredeaux  __finish_test_report "$suite_name" $total $passed
832*9e965d6fSRomain Jobredeaux  __pad "${passed} / ${total} tests passed." '*' >&2
833*9e965d6fSRomain Jobredeaux  if (( original_tests_size == 0 )); then
834*9e965d6fSRomain Jobredeaux    __pad "No tests found." '*'
835*9e965d6fSRomain Jobredeaux    exit 1
836*9e965d6fSRomain Jobredeaux  elif (( total != passed )); then
837*9e965d6fSRomain Jobredeaux    __pad "There were errors." '*' >&2
838*9e965d6fSRomain Jobredeaux    exit 1
839*9e965d6fSRomain Jobredeaux  elif (( total == 0 )); then
840*9e965d6fSRomain Jobredeaux    __pad "No tests executed due to sharding. Check your test's shard_count." '*'
841*9e965d6fSRomain Jobredeaux    __pad "Succeeding anyway." '*'
842*9e965d6fSRomain Jobredeaux  fi
843*9e965d6fSRomain Jobredeaux
844*9e965d6fSRomain Jobredeaux  exit 0
845*9e965d6fSRomain Jobredeaux}
846