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/]]>/]]>]]><![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 '&', '<' with '<', '>' with '>', and '"' with '"' 820*9e965d6fSRomain Jobredeaux escaped_fail_msg=$(echo "$fail_msg" | sed 's/&/\&/g' | sed 's/</\</g' | sed 's/>/\>/g' | sed 's/"/\"/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