blob: c88ba2ceb854936df00a40a04df713148e6cf530 [file] [log] [blame]
#!/bin/bash
#
# Copyright 2015 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Common utility file for Bazel shell tests
#
# unittest.bash: a unit test framework in Bash.
#
# A typical test suite looks like so:
#
# ------------------------------------------------------------------------
# #!/bin/bash
#
# source path/to/unittest.bash || exit 1
#
# # Test that foo works.
# function test_foo() {
# foo >$TEST_log || fail "foo failed";
# expect_log "blah" "Expected to see 'blah' in output of 'foo'."
# }
#
# # Test that bar works.
# function test_bar() {
# bar 2>$TEST_log || fail "bar failed";
# expect_not_log "ERROR" "Unexpected error from 'bar'."
# ...
# assert_equals $x $y
# }
#
# run_suite "Test suite for blah"
# ------------------------------------------------------------------------
#
# Each test function is considered to pass iff fail() is not called
# while it is active. fail() may be called directly, or indirectly
# via other assertions such as expect_log(). run_suite must be called
# at the very end.
#
# A test suite may redefine functions "set_up" and/or "tear_down";
# these functions are executed before and after each test function,
# respectively. Similarly, "cleanup" and "timeout" may be redefined,
# and these function are called upon exit (of any kind) or a timeout.
#
# The user can pass --test_filter to blaze test to select specific tests
# to run with Bash globs. A union of tests matching any of the provided globs
# will be run. Additionally the user may define TESTS=(test_foo test_bar ...) to
# specify a subset of test functions to execute, for example, a working set
# during debugging. By default, all functions called test_* will be executed.
#
# This file provides utilities for assertions over the output of a
# command. The output of the command under test is directed to the
# file $TEST_log, and then the expect_log* assertions can be used to
# test for the presence of certain regular expressions in that file.
#
# The test framework is responsible for restoring the original working
# directory before each test.
#
# The order in which test functions are run is not defined, so it is
# important that tests clean up after themselves.
#
# Each test will be run in a new subshell.
#
# Functions named __* are not intended for use by clients.
#
# This framework implements the "test sharding protocol".
#
[[ -n "$BASH_VERSION" ]] ||
{ echo "unittest.bash only works with bash!" >&2; exit 1; }
export BAZEL_SHELL_TEST=1
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# Load the environment support utilities.
source "${DIR}/unittest_utils.sh" || { echo "unittest_utils.sh not found" >&2; exit 1; }
#### Global variables:
TEST_name="" # The name of the current test.
TEST_log=$TEST_TMPDIR/log # The log file over which the
# expect_log* assertions work. Must
# be absolute to be robust against
# tests invoking 'cd'!
TEST_passed="true" # The result of the current test;
# failed assertions cause this to
# become false.
# These variables may be overridden by the test suite:
TESTS=() # A subset or "working set" of test
# functions that should be run. By
# default, all tests called test_* are
# run.
_TEST_FILTERS=() # List of globs to use to filter the tests.
# If non-empty, all tests matching at least one
# of the globs are run and test list provided in
# the arguments is ignored if present.
__in_tear_down=0 # Indicates whether we are in `tear_down` phase
# of test. Used to avoid re-entering `tear_down`
# on failures within it.
if (( $# > 0 )); then
(
IFS=':'
echo "WARNING: Passing test names in arguments (--test_arg) is deprecated, please use --test_filter='$*' instead." >&2
)
# Legacy behavior is to ignore missing regexp, but with errexit
# the following line fails without || true.
# TODO(dmarting): maybe we should revisit the way of selecting
# test with that framework (use Bazel's environment variable instead).
TESTS=($(for i in "$@"; do echo $i; done | grep ^test_ || true))
if (( ${#TESTS[@]} == 0 )); then
echo "WARNING: Arguments do not specify tests!" >&2
fi
fi
# TESTBRIDGE_TEST_ONLY contains the value of --test_filter, if any. We want to
# preferentially use that instead of $@ to determine which tests to run.
if [[ ${TESTBRIDGE_TEST_ONLY:-} != "" ]]; then
if (( ${#TESTS[@]} != 0 )); then
echo "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg" >&2
TESTS=()
fi
# Split TESTBRIDGE_TEST_ONLY on colon and store it in `_TEST_FILTERS` array.
IFS=':' read -r -a _TEST_FILTERS <<< "$TESTBRIDGE_TEST_ONLY"
fi
TEST_verbose="true" # Whether or not to be verbose. A
# command; "true" or "false" are
# acceptable. The default is: true.
TEST_script="$0" # Full path to test script
# Check if the script path is absolute, if not prefix the PWD.
if [[ ! "$TEST_script" = /* ]]; then
TEST_script="${PWD}/$0"
fi
#### Internal functions
function __show_log() {
echo "-- Test log: -----------------------------------------------------------"
[[ -e $TEST_log ]] && cat "$TEST_log" || echo "(Log file did not exist.)"
echo "------------------------------------------------------------------------"
}
# Usage: __pad <title> <pad-char>
# Print $title padded to 80 columns with $pad_char.
function __pad() {
local title=$1
local pad=$2
# Ignore the subshell error -- `head` closes the fd before reading to the
# end, therefore the subshell will get SIGPIPE while stuck in `write`.
{
echo -n "${pad}${pad} ${title} "
printf "%80s" " " | tr ' ' "$pad"
} | head -c 80 || true
echo
}
#### Exported functions
# Usage: init_test ...
# Deprecated. Has no effect.
function init_test() {
:
}
# Usage: set_up
# Called before every test function. May be redefined by the test suite.
function set_up() {
:
}
# Usage: tear_down
# Called after every test function. May be redefined by the test suite.
function tear_down() {
:
}
# Usage: cleanup
# Called upon eventual exit of the test suite. May be redefined by
# the test suite.
function cleanup() {
:
}
# Usage: timeout
# Called upon early exit from a test due to timeout.
function timeout() {
:
}
# Usage: testenv_set_up
# Called prior to set_up. For use by testenv.sh.
function testenv_set_up() {
:
}
# Usage: testenv_tear_down
# Called after tear_down. For use by testenv.sh.
function testenv_tear_down() {
:
}
# Usage: fail <message> [<message> ...]
# Print failure message with context information, and mark the test as
# a failure. The context includes a stacktrace including the longest sequence
# of calls outside this module. (We exclude the top and bottom portions of
# the stack because they just add noise.) Also prints the contents of
# $TEST_log.
function fail() {
__show_log >&2
echo "${TEST_name} FAILED: $*." >&2
# Keep the original error message if we fail in `tear_down` after a failure.
[[ "${TEST_passed}" == "true" ]] && echo "$@" >"$TEST_TMPDIR"/__fail
TEST_passed="false"
__show_stack
# Cleanup as we are leaving the subshell now
__run_tear_down_after_failure
exit 1
}
function __run_tear_down_after_failure() {
# Skip `tear_down` after a failure in `tear_down` to prevent infinite
# recursion.
(( __in_tear_down )) && return
__in_tear_down=1
echo -e "\nTear down:\n" >&2
tear_down
testenv_tear_down
}
# Usage: warn <message>
# Print a test warning with context information.
# The context includes a stacktrace including the longest sequence
# of calls outside this module. (We exclude the top and bottom portions of
# the stack because they just add noise.)
function warn() {
__show_log >&2
echo "${TEST_name} WARNING: $1." >&2
__show_stack
if [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then
echo "${TEST_name} WARNING: $1." >> "$TEST_WARNINGS_OUTPUT_FILE"
fi
}
# Usage: show_stack
# Prints the portion of the stack that does not belong to this module,
# i.e. the user's code that called a failing assertion. Stack may not
# be available if Bash is reading commands from stdin; an error is
# printed in that case.
__show_stack() {
local i=0
local trace_found=0
# Skip over active calls within this module:
while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} == "${BASH_SOURCE[0]}" ]]; do
(( ++i ))
done
# Show all calls until the next one within this module (typically run_suite):
while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} != "${BASH_SOURCE[0]}" ]]; do
# Read online docs for BASH_LINENO to understand the strange offset.
# Undefined can occur in the BASH_SOURCE stack apparently when one exits from a subshell
echo "${BASH_SOURCE[i]:-"Unknown"}:${BASH_LINENO[i - 1]:-"Unknown"}: in call to ${FUNCNAME[i]:-"Unknown"}" >&2
(( ++i ))
trace_found=1
done
(( trace_found )) || echo "[Stack trace not available]" >&2
}
# Usage: expect_log <regexp> [error-message]
# Asserts that $TEST_log matches regexp. Prints the contents of
# $TEST_log and the specified (optional) error message otherwise, and
# returns non-zero.
function expect_log() {
local pattern=$1
local message=${2:-Expected regexp "$pattern" not found}
grep -sq -- "$pattern" $TEST_log && return 0
fail "$message"
return 1
}
# Usage: expect_log_warn <regexp> [error-message]
# Warns if $TEST_log does not match regexp. Prints the contents of
# $TEST_log and the specified (optional) error message on mismatch.
function expect_log_warn() {
local pattern=$1
local message=${2:-Expected regexp "$pattern" not found}
grep -sq -- "$pattern" $TEST_log && return 0
warn "$message"
return 1
}
# Usage: expect_log_once <regexp> [error-message]
# Asserts that $TEST_log contains one line matching <regexp>.
# Prints the contents of $TEST_log and the specified (optional)
# error message otherwise, and returns non-zero.
function expect_log_once() {
local pattern=$1
local message=${2:-Expected regexp "$pattern" not found exactly once}
expect_log_n "$pattern" 1 "$message"
}
# Usage: expect_log_n <regexp> <count> [error-message]
# Asserts that $TEST_log contains <count> lines matching <regexp>.
# Prints the contents of $TEST_log and the specified (optional)
# error message otherwise, and returns non-zero.
function expect_log_n() {
local pattern=$1
local expectednum=${2:-1}
local message=${3:-Expected regexp "$pattern" not found exactly $expectednum times}
local count=$(grep -sc -- "$pattern" $TEST_log)
(( count == expectednum )) && return 0
fail "$message"
return 1
}
# Usage: expect_not_log <regexp> [error-message]
# Asserts that $TEST_log does not match regexp. Prints the contents
# of $TEST_log and the specified (optional) error message otherwise, and
# returns non-zero.
function expect_not_log() {
local pattern=$1
local message=${2:-Unexpected regexp "$pattern" found}
grep -sq -- "$pattern" $TEST_log || return 0
fail "$message"
return 1
}
# Usage: expect_query_targets <arguments>
# Checks that log file contains exactly the targets in the argument list.
function expect_query_targets() {
for arg in "$@"; do
expect_log_once "^$arg$"
done
# Checks that the number of lines started with '//' equals to the number of
# arguments provided.
expect_log_n "^//[^ ]*$" $#
}
# Usage: expect_log_with_timeout <regexp> <timeout> [error-message]
# Waits for the given regexp in the $TEST_log for up to timeout seconds.
# Prints the contents of $TEST_log and the specified (optional)
# error message otherwise, and returns non-zero.
function expect_log_with_timeout() {
local pattern=$1
local timeout=$2
local message=${3:-Regexp "$pattern" not found in "$timeout" seconds}
local count=0
while (( count < timeout )); do
grep -sq -- "$pattern" "$TEST_log" && return 0
let count=count+1
sleep 1
done
grep -sq -- "$pattern" "$TEST_log" && return 0
fail "$message"
return 1
}
# Usage: expect_cmd_with_timeout <expected> <cmd> [timeout]
# Repeats the command once a second for up to timeout seconds (10s by default),
# until the output matches the expected value. Fails and returns 1 if
# the command does not return the expected value in the end.
function expect_cmd_with_timeout() {
local expected="$1"
local cmd="$2"
local timeout=${3:-10}
local count=0
while (( count < timeout )); do
local actual="$($cmd)"
[[ "$expected" == "$actual" ]] && return 0
(( ++count ))
sleep 1
done
[[ "$expected" == "$actual" ]] && return 0
fail "Expected '${expected}' within ${timeout}s, was '${actual}'"
return 1
}
# Usage: assert_one_of <expected_list>... <actual>
# Asserts that actual is one of the items in expected_list
#
# Example:
# local expected=( "foo", "bar", "baz" )
# assert_one_of $expected $actual
function assert_one_of() {
local args=("$@")
local last_arg_index=$((${#args[@]} - 1))
local actual=${args[last_arg_index]}
unset args[last_arg_index]
for expected_item in "${args[@]}"; do
[[ "$expected_item" == "$actual" ]] && return 0
done;
fail "Expected one of '${args[*]}', was '$actual'"
return 1
}
# Usage: assert_not_one_of <expected_list>... <actual>
# Asserts that actual is not one of the items in expected_list
#
# Example:
# local unexpected=( "foo", "bar", "baz" )
# assert_not_one_of $unexpected $actual
function assert_not_one_of() {
local args=("$@")
local last_arg_index=$((${#args[@]} - 1))
local actual=${args[last_arg_index]}
unset args[last_arg_index]
for expected_item in "${args[@]}"; do
if [[ "$expected_item" == "$actual" ]]; then
fail "'${args[*]}' contains '$actual'"
return 1
fi
done;
return 0
}
# Usage: assert_equals <expected> <actual>
# Asserts [[ expected == actual ]].
function assert_equals() {
local expected=$1 actual=$2
[[ "$expected" == "$actual" ]] && return 0
fail "Expected '$expected', was '$actual'"
return 1
}
# Usage: assert_not_equals <unexpected> <actual>
# Asserts [[ unexpected != actual ]].
function assert_not_equals() {
local unexpected=$1 actual=$2
[[ "$unexpected" != "$actual" ]] && return 0;
fail "Expected not '${unexpected}', was '${actual}'"
return 1
}
# Usage: assert_contains <regexp> <file> [error-message]
# Asserts that file matches regexp. Prints the contents of
# file and the specified (optional) error message otherwise, and
# returns non-zero.
function assert_contains() {
local pattern=$1
local file=$2
local message=${3:-Expected regexp "$pattern" not found in "$file"}
grep -sq -- "$pattern" "$file" && return 0
cat "$file" >&2
fail "$message"
return 1
}
# Usage: assert_not_contains <regexp> <file> [error-message]
# Asserts that file does not match regexp. Prints the contents of
# file and the specified (optional) error message otherwise, and
# returns non-zero.
function assert_not_contains() {
local pattern=$1
local file=$2
local message=${3:-Expected regexp "$pattern" found in "$file"}
if [[ -f "$file" ]]; then
grep -sq -- "$pattern" "$file" || return 0
else
fail "$file is not a file: $message"
return 1
fi
cat "$file" >&2
fail "$message"
return 1
}
function assert_contains_n() {
local pattern=$1
local expectednum=${2:-1}
local file=$3
local message=${4:-Expected regexp "$pattern" not found exactly $expectednum times}
local count
if [[ -f "$file" ]]; then
count=$(grep -sc -- "$pattern" "$file")
else
fail "$file is not a file: $message"
return 1
fi
(( count == expectednum )) && return 0
cat "$file" >&2
fail "$message"
return 1
}
# Updates the global variables TESTS if
# sharding is enabled, i.e. ($TEST_TOTAL_SHARDS > 0).
function __update_shards() {
[[ -z "${TEST_TOTAL_SHARDS-}" ]] && return 0
(( TEST_TOTAL_SHARDS > 0 )) ||
{ echo "Invalid total shards ${TEST_TOTAL_SHARDS}" >&2; exit 1; }
(( TEST_SHARD_INDEX < 0 || TEST_SHARD_INDEX >= TEST_TOTAL_SHARDS )) &&
{ echo "Invalid shard ${TEST_SHARD_INDEX}" >&2; exit 1; }
IFS=$'\n' read -rd $'\0' -a TESTS < <(
for test in "${TESTS[@]}"; do echo "$test"; done |
awk "NR % ${TEST_TOTAL_SHARDS} == ${TEST_SHARD_INDEX}" &&
echo -en '\0')
[[ -z "${TEST_SHARD_STATUS_FILE-}" ]] || touch "$TEST_SHARD_STATUS_FILE"
}
# Usage: __test_terminated <signal-number>
# Handler that is called when the test terminated unexpectedly
function __test_terminated() {
__show_log >&2
echo "$TEST_name FAILED: terminated by signal $1." >&2
TEST_passed="false"
__show_stack
timeout
exit 1
}
# Usage: __test_terminated_err
# Handler that is called when the test terminated unexpectedly due to "errexit".
function __test_terminated_err() {
# When a subshell exits due to signal ERR, its parent shell also exits,
# thus the signal handler is called recursively and we print out the
# error message and stack trace multiple times. We're only interested
# in the first one though, as it contains the most information, so ignore
# all following.
if [[ -f $TEST_TMPDIR/__err_handled ]]; then
exit 1
fi
__show_log >&2
if [[ ! -z "$TEST_name" ]]; then
echo -n "$TEST_name " >&2
fi
echo "FAILED: terminated because this command returned a non-zero status:" >&2
touch $TEST_TMPDIR/__err_handled
TEST_passed="false"
__show_stack
# If $TEST_name is still empty, the test suite failed before we even started
# to run tests, so we shouldn't call tear_down.
if [[ -n "$TEST_name" ]]; then
__run_tear_down_after_failure
fi
exit 1
}
# Usage: __trap_with_arg <handler> <signals ...>
# Helper to install a trap handler for several signals preserving the signal
# number, so that the signal number is available to the trap handler.
function __trap_with_arg() {
func="$1" ; shift
for sig ; do
trap "$func $sig" "$sig"
done
}
# Usage: <node> <block>
# Adds the block to the given node in the report file. Quotes in the in
# arguments need to be escaped.
function __log_to_test_report() {
local node="$1"
local block="$2"
if [[ ! -e "$XML_OUTPUT_FILE" ]]; then
local xml_header='<?xml version="1.0" encoding="UTF-8"?>'
echo "${xml_header}<testsuites></testsuites>" > "$XML_OUTPUT_FILE"
fi
# replace match on node with block and match
# replacement expression only needs escaping for quotes
perl -e "\
\$input = @ARGV[0]; \
\$/=undef; \
open FILE, '+<$XML_OUTPUT_FILE'; \
\$content = <FILE>; \
if (\$content =~ /($node.*)\$/) { \
seek FILE, 0, 0; \
print FILE \$\` . \$input . \$1; \
}; \
close FILE" "$block"
}
# Usage: <total> <passed>
# Adds the test summaries to the xml nodes.
function __finish_test_report() {
local suite_name="$1"
local total="$2"
local passed="$3"
local failed=$((total - passed))
# Update the xml output with the suite name and total number of
# passed/failed tests.
cat "$XML_OUTPUT_FILE" | \
sed \
"s/<testsuites>/<testsuites tests=\"$total\" failures=\"0\" errors=\"$failed\">/" | \
sed \
"s/<testsuite>/<testsuite name=\"${suite_name}\" tests=\"$total\" failures=\"0\" errors=\"$failed\">/" \
> "${XML_OUTPUT_FILE}.bak"
rm -f "$XML_OUTPUT_FILE"
mv "${XML_OUTPUT_FILE}.bak" "$XML_OUTPUT_FILE"
}
# Multi-platform timestamp function
UNAME=$(uname -s | tr 'A-Z' 'a-z')
if [[ "$UNAME" == "linux" ]] || [[ "$UNAME" =~ msys_nt* ]]; then
function timestamp() {
echo $(($(date +%s%N)/1000000))
}
else
function timestamp() {
# macOS and BSDs do not have %N, so Python is the best we can do.
# LC_ALL=C works around python 3.8 and 3.9 crash on macOS when the
# filesystem encoding is unspecified (e.g. when LANG=en_US).
local PYTHON=python
command -v python3 &> /dev/null && PYTHON=python3
LC_ALL=C "${PYTHON}" -c 'import time; print(int(round(time.time() * 1000)))'
}
fi
function get_run_time() {
local ts_start=$1
local ts_end=$2
run_time_ms=$((ts_end - ts_start))
echo $((run_time_ms / 1000)).${run_time_ms: -3}
}
# Usage: run_tests <suite-comment>
# Must be called from the end of the user's test suite.
# Calls exit with zero on success, non-zero otherwise.
function run_suite() {
local message="$1"
# The name of the suite should be the script being run, which
# will be the filename with the ".sh" extension removed.
local suite_name="$(basename "$0")"
echo >&2
echo "$message" >&2
echo >&2
__log_to_test_report "<\/testsuites>" "<testsuite></testsuite>"
local total=0
local passed=0
atexit "cleanup"
# If the user didn't specify an explicit list of tests (e.g. a
# working set), use them all.
if (( ${#TESTS[@]} == 0 )); then
# Even if there aren't any tests, this needs to succeed.
local all_tests=()
IFS=$'\n' read -d $'\0' -ra all_tests < <(
declare -F | awk '{print $3}' | grep ^test_ || true; echo -en '\0')
if (( "${#_TEST_FILTERS[@]}" == 0 )); then
# Use ${array[@]+"${array[@]}"} idiom to avoid errors when running with
# Bash version <= 4.4 with `nounset` when `all_tests` is empty (
# https://github.com/bminor/bash/blob/a0c0a00fc419b7bc08202a79134fcd5bc0427071/CHANGES#L62-L63).
TESTS=("${all_tests[@]+${all_tests[@]}}")
else
for t in "${all_tests[@]+${all_tests[@]}}"; do
local matches=0
for f in "${_TEST_FILTERS[@]}"; do
# We purposely want to glob match.
# shellcheck disable=SC2053
[[ "$t" = $f ]] && matches=1 && break
done
if (( matches )); then
TESTS+=("$t")
fi
done
fi
elif [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then
if grep -q "TESTS=" "$TEST_script" ; then
echo "TESTS variable overridden in sh_test. Please remove before submitting" \
>> "$TEST_WARNINGS_OUTPUT_FILE"
fi
fi
# Reset TESTS in the common case where it contains a single empty string.
if [[ -z "${TESTS[*]-}" ]]; then
TESTS=()
fi
local original_tests_size=${#TESTS[@]}
__update_shards
if [[ "${#TESTS[@]}" -ne 0 ]]; then
for TEST_name in "${TESTS[@]}"; do
>"$TEST_log" # Reset the log.
TEST_passed="true"
(( ++total ))
if [[ "$TEST_verbose" == "true" ]]; then
date >&2
__pad "$TEST_name" '*' >&2
fi
local run_time="0.0"
rm -f "${TEST_TMPDIR}"/{__ts_start,__ts_end}
if [[ "$(type -t "$TEST_name")" == function ]]; then
# Save exit handlers eventually set.
local SAVED_ATEXIT="$ATEXIT";
ATEXIT=
# Run test in a subshell.
rm -f "${TEST_TMPDIR}"/__err_handled
__trap_with_arg __test_terminated INT KILL PIPE TERM ABRT FPE ILL QUIT SEGV
# Remember -o pipefail value and disable it for the subshell result
# collection.
if [[ "${SHELLOPTS}" =~ (^|:)pipefail(:|$) ]]; then
local __opt_switch=-o
else
local __opt_switch=+o
fi
set +o pipefail
(
set "${__opt_switch}" pipefail
# if errexit is enabled, make sure we run cleanup and collect the log.
if [[ "$-" = *e* ]]; then
set -E
trap __test_terminated_err ERR
fi
timestamp >"${TEST_TMPDIR}"/__ts_start
testenv_set_up
set_up
eval "$TEST_name"
__in_tear_down=1
tear_down
testenv_tear_down
timestamp >"${TEST_TMPDIR}"/__ts_end
test "$TEST_passed" == "true"
) 2>&1 | tee "${TEST_TMPDIR}"/__log
# Note that tee will prevent the control flow continuing if the test
# spawned any processes which are still running and have not closed
# their stdout.
test_subshell_status=${PIPESTATUS[0]}
set "${__opt_switch}" pipefail
if (( test_subshell_status != 0 )); then
TEST_passed="false"
# Ensure that an end time is recorded in case the test subshell
# terminated prematurely.
[[ -f "$TEST_TMPDIR"/__ts_end ]] || timestamp >"$TEST_TMPDIR"/__ts_end
fi
# Calculate run time for the testcase.
local ts_start
ts_start=$(<"${TEST_TMPDIR}"/__ts_start)
local ts_end
ts_end=$(<"${TEST_TMPDIR}"/__ts_end)
run_time=$(get_run_time $ts_start $ts_end)
# Eventually restore exit handlers.
if [[ -n "$SAVED_ATEXIT" ]]; then
ATEXIT="$SAVED_ATEXIT"
trap "$ATEXIT" EXIT
fi
else # Bad test explicitly specified in $TESTS.
fail "Not a function: '$TEST_name'"
fi
local testcase_tag=""
local red='\033[0;31m'
local green='\033[0;32m'
local no_color='\033[0m'
if [[ "$TEST_verbose" == "true" ]]; then
echo >&2
fi
if [[ "$TEST_passed" == "true" ]]; then
if [[ "$TEST_verbose" == "true" ]]; then
echo -e "${green}PASSED${no_color}: ${TEST_name}" >&2
fi
(( ++passed ))
testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"></testcase>"
else
echo -e "${red}FAILED${no_color}: ${TEST_name}" >&2
# end marker in CDATA cannot be escaped, we need to split the CDATA sections
log=$(sed 's/]]>/]]>]]&gt;<![CDATA[/g' "${TEST_TMPDIR}"/__log)
fail_msg=$(cat "${TEST_TMPDIR}"/__fail 2> /dev/null || echo "No failure message")
# Replacing '&' with '&amp;', '<' with '&lt;', '>' with '&gt;', and '"' with '&quot;'
escaped_fail_msg=$(echo "$fail_msg" | sed 's/&/\&amp;/g' | sed 's/</\&lt;/g' | sed 's/>/\&gt;/g' | sed 's/"/\&quot;/g')
testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"><error message=\"${escaped_fail_msg}\"><![CDATA[${log}]]></error></testcase>"
fi
if [[ "$TEST_verbose" == "true" ]]; then
echo >&2
fi
__log_to_test_report "<\/testsuite>" "$testcase_tag"
done
fi
__finish_test_report "$suite_name" $total $passed
__pad "${passed} / ${total} tests passed." '*' >&2
if (( original_tests_size == 0 )); then
__pad "No tests found." '*'
exit 1
elif (( total != passed )); then
__pad "There were errors." '*' >&2
exit 1
elif (( total == 0 )); then
__pad "No tests executed due to sharding. Check your test's shard_count." '*'
__pad "Succeeding anyway." '*'
fi
exit 0
}