blob: dad7a02367da5ef93d7fbf84543609b96e9bf92d [file] [log] [blame]
#!/bin/bash
# Copyright 2019 The Fuchsia Authors. All rights reserved.
#/ Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# The Bash Test Framework
#
# OVERVIEW
#
# The Bash Test Framework makes it easy to write tests that exercise Bash
# scripts.
#
# RUNNING TESTS
#
# The bash_test_framework.sh library script does not depend on Fuchsia's
# environment, but the "fx self-test" command is a convenient way to
# launch a test from the //tools/devshell/tests directory. For example:
#
# $ fx self-test <host_test_script>
#
# The "self-test" command automatically loads the
# bash_test_framework.sh library and then runs the host test script.
#
# HOW THE FRAMEWORK EXECUTES YOUR TESTS
#
# By default, the framework finds all functions beginning with "TEST_..." and
# runs them in the order they were declared. (The default subset and/or order
# of test function to run can be set manually by declaring the list of test
# functions in the bash arrayh variable "BT_TEST_FUNCTIONS".)
#
# For each "TEST_..." function, the framework creates a pseudo-sandbox
# environment and relaunches the test script to run the test function, then
# tears down the sandbox, and creates a new sandbox to run the next test
# function. More specifically, for each "TEST_..." function, the framework:
#
# 1. Creates a temporary directory and copies the test script and the bash
# framework to that directory.
# 2. Creates the directories listed in the array BT_MKDIR_DEPS within the
# temp directory. The framework creates subdirectories within the paths.
# 3. Copies the files listed in BT_FILE_DEPS to the same relative locations in
# the temp directory, creating any additional intermediate directories if
# required.
# 4. Creates symbolic links for files and directories listed in
# BT_LINKED_DEPS to the same relative locations in
# the temp directory, creating any additional intermediate directories if
# required.
# 5. Copies the mock.sh mock executable script to the tool subpaths lised in
# BT_MOCKED_TOOLS.
# 6. Calls BT_INIT_TEMP_DIR() (if present).
# 7. Launches the sandboxed subprocess (a new sub-shell without user-specific
# settings)
# 8. Then calls BT_SET_UP() (if present), the current "TEST_..." function,
# and BT_TEAR_DOWN() (if present).
#
# CONVENIENCE FUNCTIONS
#
# The framework provides conveninece functions in the style of other testing
# frameworks and summarizes test success/failure upon completion. Functions prefixed
# with BT_EXPECT_... increment a counter for each failed test but continue executing
# the remaining tests in the test function. If one or more BT_EXPECT_... tests fail,
# The entire test function fails. Functions prefixed with BT_ASSERT_... abort the
# test function from that point, and increment the failed test counter, also failing
# the entire test function.
#
# Utility functions start with "btf::", such as "btf::error", "btf::failed", or
# "btf::stderr".
#
# EXAMPLES:
#
# Imagine a script to test is at "tools/prefs.sh":
#---------------------------------------------------------------------------------------------------
# #!/bin/bash
#
# # Some other set of functions the script depends on.
# source lib/file_utils.sh
#
# create_prefs_file() {
# bin/formatter $2 > "${FUCHSIA_DIR}/out/default/$1"
# }
#---------------------------------------------------------------------------------------------------
#
# A test for this script, such as "tools/devshell/test/prefs_test", may look something like:
#---------------------------------------------------------------------------------------------------
# #!/bin/bash
#
# BT_FILE_DEPS=("lib/file_utils.sh")
# BT_MOCKED_TOOLS=("bin/formatter")
# BT_MKDIR_DEPS=("out/default")
#
# BT_INIT_TEMP_DIR() {
# cat > bin/formatter.mock_side_effects <<EOF
# echo $1
# EOF
# }
#
# BT_SET_UP() {
# if [[ "${FUCHSIA_DIR}" == "" ]]; then
# FUCHSIA_DIR="${BT_TEMP_DIR}"
# source "${FUCHSIA_DIR}/tools/prefs.sh"
# fi
# }
#
# TEST_create_prefs_file {
# BT_ASSERT_FUNCTION_EXISTS create_prefs_file
# local -r content="config=DEBUG"
# create_prefs_file .config "${content}"
# BT_EXPECT_FILE_CONTAINS "${FUCHSIA_DIR}/out/default/.config" "${content}"
# }
#
# BT_RUN_TESTS "$@"
#---------------------------------------------------------------------------------------------------
#
# ANNOTATED EXAMPLE OF THE SAME TEST SCRIPT:
#---------------------------------------------------------------------------------------------------
# #!/bin/bash
#
# # The variable and function declarations prefixed by "BT_", shown below,
# # are optional:
#
# #######################################
# # The root directory for your project source is normally inferred from the location
# # of the bash_test_framework.sh script, but can be manually overridden by setting:
# #######################################
# # BT_DEPS_ROOT="/Your/Root/Dir".
#
# #######################################
# # Files (or entire subdirectories, recursively), to be copied to the isolated
# # test directory. (Intermediate directories will be created automatically.)
# #######################################
# BT_FILE_DEPS=("lib/file_utils.sh")
#
# #######################################
# # Executables (binaries and/or scripts, including sourced scripts) to generated
# # as a mock version of the executable. See "mock.sh". (Intermediate directories
# # will be created automatically.)
# #######################################
# BT_MOCKED_TOOLS=("bin/formatter")
#
# #######################################
# # Any additional directories not already created as a result of one of the prior
# # declarations.
# #######################################
# BT_MKDIR_DEPS=("out/default")
#
# #######################################
# # If declared, the BT_INIT_TEMP_DIR function is called by the framework,
# # after staging all files, directories, and mock executables, and before
# # relaunching the test script in its clean environment.
# #
# # Any additional initialization steps that require access to the original
# # project source directory should be performed here, if needed.
# #
# # BT_TEMP_DIR will be set to the temporary root directory created to execute
# # a single test.
# # BT_DEPS_ROOT will be set to the root directory of the original project
# # source directory path.
# #
# # The current working directory will be set to the root of the new temporary
# # directory (BT_TEMP_DIR).
# #
# # No variables (exported or otherwise) set from this script will propagate
# # to test functions.
# #######################################
# BT_INIT_TEMP_DIR() {
# cat > bin/formatter.mock_side_effects <<EOF
# echo $1
# EOF
# }
#
# #######################################
# # If declared, BT_SET_UP is called within the isolated test environment, just
# # before invoking one of the test functions.
# #
# # Any initialization steps that do not require access to the original project
# # source directory should be performed here, if needed.
# #
# # BT_TEST_ARGS is a bash array variable that may contain test-specific command
# # line options passed to the test script after the argument '--', for example:
# #
# # fx self-test <host_test_script> [--framework_options] -- [--test_options]
# #
# # BT_TEMP_DIR will be set to the temporary root directory created to execute
# # a single test.
# # BT_TEST_ARGS - array of command line arguments passed to the test script
# # after the argument '--' (can be included at the end of 'fx self-test')
# #
# # The current working directory will be set to the directory containing the
# # BT_SET_UP bash function (from within the temporary root directory).
# #######################################
# BT_SET_UP() {
# if [[ "${FUCHSIA_DIR}" == "" ]]; then
# FUCHSIA_DIR="${BT_TEMP_DIR}"
# source "${FUCHSIA_DIR}/tools/prefs.sh"
# fi
# }
#
# #######################################
# # If declared, BT_TEAR_DOWN is called within the isolated test environment, just
# # after invoking one of the test functions.
# #
# # BT_TEMP_DIR will be set to the temporary root directory created to execute
# # a single test. This directory and all subdirectories will be deleted,
# # automatically, after the test executes. BT_TEAR_DOWN should be used only
# # if there are other resources to tear down, such as to kill a background
# # process created by BT_SET_UP or one of the TEST_... functions.
# #
# # The current working directory will be set to the directory containing the
# # BT_SET_UP bash function (from within the temporary root directory).
# #
# # Important: BT_TEAR_DOWN is only called after the test function returns or
# # exits, *and* closes the stdout stream. If the test starts a background task
# # witout redirecting stdout, and the test fails before completing or killing
# # that background task, stdout will remain open and the test will hang. To
# # avoid this, always redirect background task stdout. Among the typical
# # alternatives are redirecting stdout to stderr ("some_program >&2"), a
# # variable, a file, or /dev/null.
# #######################################
# # BT_TEAR_DOWN() {
# # }
#
# TEST_create_prefs_file {
# BT_ASSERT_FUNCTION_EXISTS create_prefs_file
# local -r content="config=DEBUG"
# create_prefs_file .config "${content}"
# BT_EXPECT_FILE_CONTAINS "${FUCHSIA_DIR}/out/default/.config" "${content}"
# }
#
# #######################################
# # The last line of the test function should be the call to the
# # bash_test_framework.sh declared function BT_RUN_TESTS, as follows:
# #######################################
# BT_RUN_TESTS "$@"
#---------------------------------------------------------------------------------------------------
declare -r -i MAX_ERROR_STATUS=255 # 0-255 is the range of values available for exit codes
# PRIVATE FUNCTIONS AND VARIABLES
#
# Framework-private functions begin with "btf::_" and private global variables
# begin with the prefix "_btf_" (or "_BTF_" for constants). The private
# functions and variables should not be called / used by test scripts.
# Constants
readonly _BTF_HOST_SCRIPT_NAME="$(basename $0)"
readonly _BTF_HOST_SCRIPT_DIR="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"
readonly _BTF_HOST_SCRIPT="${_BTF_HOST_SCRIPT_DIR}/${_BTF_HOST_SCRIPT_NAME}"
readonly _BTF_ASSERT_ERROR_COUNT_MESSAGE_PREFIX="Current error count is"
readonly _BTF_END_OF_TEST_MARKER=$'\nEOT'
# Default root assumes the bash_test_framework.sh script is a specific
# directory depth below the root of the project source tree. For example,
# under $FUCHSIA_DIR, the framework script is in "tools/devshell/tests".
# If necessary, override this assumption in the host test script by
# explicitly setting the variable BT_DEPS_ROOT to the root directory path.
readonly _BTF_FRAMEWORK_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
readonly _BTF_DEFAULT_ROOT_DIR="$(cd "${_BTF_FRAMEWORK_SCRIPT_DIR}/../../../.." >/dev/null 2>&1 && pwd)"
readonly _BTF_TEMP_DIR_MARKER=".btf"
readonly _BTF_FUNCTION_NAME_PREFIX="TEST_"
if [[ -t 2 ]]; then
# stderr messages use ANSI terminal escape sequences only if output is tty
readonly ESC=$'\033'
readonly _ANSI_BRIGHT_RED="${ESC}[1;31m"
readonly _ANSI_BRIGHT_GREEN="${ESC}[1;32m"
readonly _ANSI_CLEAR="${ESC}[0m"
else
readonly _ANSI_BRIGHT_RED=
readonly _ANSI_BRIGHT_GREEN=
readonly _ANSI_CLEAR=
fi
if [[ -t 1 ]]; then
readonly _BTF_FAIL="${_ANSI_BRIGHT_RED}FAIL:${_ANSI_CLEAR}"
else
# if stdout is not a tty, ensure failure messages don't use ANSI escape sequences
readonly _BTF_FAIL="FAIL:"
fi
# For BT_EXPECT_{TRUE/FALSE}, when using 'eval' to execute a command, the ${FUNCNAME[@]}
# array should be interpreted with an additional offset, as if the command was
# actually executed from the context of the BT_ function that called eval.
declare _BTF_EVAL_OFFSET=0
# Ensure array types
declare -a BT_MKDIR_DEPS
declare -a BT_FILE_DEPS
declare -a BT_LINKED_DEPS
declare -a BT_MOCKED_TOOLS
declare -a BT_TEST_FUNCTIONS
#######################################
# Returns true (return status 0) if a function with the given name is declared.
# Arguments:
# $1 - the function name to look for
# Returns:
# 0 if it exists, non-zero otherwise
#######################################
btf::function_exists() {
local function_name="$1"
declare -f "${function_name}" >/dev/null
}
# Define no-op versions of the following functions.
# These may be redefined in the host test script.
btf::function_exists BT_INIT_TEMP_DIR || BT_INIT_TEMP_DIR() { :; }
btf::function_exists BT_SET_UP || BT_SET_UP() { :; }
btf::function_exists BT_TEAR_DOWN || BT_TEAR_DOWN() { :; }
#######################################
# Prints a message to stderr.
# Arguments:
# - optional flag "-n" to print the message without adding a newline
# - message or format string
# - remaining arg(s) - values to match the printf format string placeholders
# Outputs:
# Writes the formatted message to stderr
#######################################
btf::stderr() {
local newline=true
if [[ "$1" == "-n" ]]; then
newline=false
shift
fi
local format_string="$1"
shift
printf >&2 "${format_string}" "$@"
if ! [[ "${format_string}" =~ % ]]; then
printf >&2 " %s" "$@"
fi
if ${newline}; then
printf >&2 "\n"
fi
}
#######################################
# Internal function called from public functions like "error", "failed", or
# "success"
# Arguments:
# - optional flags that begin with "-"
# - ANSI terminal escape sequence for the desired output style, if any
# - A message prefix (may be an empty string, but the argument is still present)
# - message or format string
# - remaining arg(s) - values to match the printf format string placeholders
# Globals:
# _ANSI_CLEAR
# Outputs:
# Writes the formatted message to stderr
#######################################
btf::_print_with_style() {
local flags=()
while [[ "$1" == -* ]]; do
flags+=("$1")
shift
done
local ansi_style="$1"
shift
local prefix="$1"
shift
local format_string="$1"
shift
btf::stderr "${flags[@]}" "${ansi_style}${prefix}${format_string}${_ANSI_CLEAR}" "$@"
}
#######################################
# Prints "ERROR: " followed by a message. If printing to a terminal, error
# message style is applied (typically red)
# Arguments:
# - optional flags that begin with "-"
# - message or format string
# - remaining arg(s) - values to match the printf format string placeholders
# Globals:
# _ANSI_BRIGHT_RED
# _BTF_DEFAULT_ROOT_DIR
# Outputs:
# Writes the formatted message to stderr
#######################################
btf::error() {
local flags=()
while [[ "$1" == -* ]]; do
flags+=("$1")
shift
done
local offset=0
while [[ "${FUNCNAME[$((offset + 1))]}" == "btf::"* ]]; do
: $((offset++))
done
local source_file="${BASH_SOURCE[$((1 + offset))]#$_BTF_DEFAULT_ROOT_DIR/}"
local source_line=${BASH_LINENO[$((0 + offset))]}
btf::_print_with_style "${flags[@]}" "${_ANSI_BRIGHT_RED}" \
"ERROR: ${source_file}:${source_line}: " "$@"
}
#######################################
# Invokes btf::error with the given message/format string and other arguments,
# then exits with a status 1.
# Arguments:
# $1 - The status code to exit with
# All remaining arguments are passed directly to btf::error. See btf::error
# for additional detail.
# Outputs:
# Writes the formatted message to stderr, possibly with ANSI terminal escapes.
#######################################
btf::abort() {
local -i status=$1
shift
if ((${status} == 0)); then
btf::stderr "$@"
else
btf::error "$@"
fi
exit ${status}
}
#######################################
# Prints "FAILED: " followed by a message. If printing to a terminal, error
# message style is applied (typically red).
# Arguments:
# - optional flags that begin with "-"
# - message or format string
# - remaining arg(s) - values to match the printf format string placeholders
# Globals:
# _ANSI_BRIGHT_RED
# Outputs:
# Writes the formatted message to stderr
#######################################
btf::failed() {
local flags=()
while [[ "$1" == -* ]]; do
flags+=("$1")
shift
done
btf::_print_with_style "${flags[@]}" "${_ANSI_BRIGHT_RED}" "FAILED: " "$@"
}
#######################################
# Prints a success message. If printing to a terminal, success message style
# is applied (typically green)
# Arguments:
# - optional flags that begin with "-"
# - message or format string
# - remaining arg(s) - values to match the printf format string placeholders
# Globals:
# _ANSI_BRIGHT_GREEN
# Outputs:
# Writes the formatted message to stderr
#######################################
btf::success() {
local flags=()
while [[ "$1" == -* ]]; do
flags+=("$1")
shift
done
btf::_print_with_style "${flags[@]}" "${_ANSI_BRIGHT_GREEN}" "" "$@"
}
#######################################
# Writes a specific marker to stdout and calls exit with the given status code.
# When a test is invoked, stdout will be captured to a variable. The marker will
# be stripped from the stdout string before printing. If not found, the test
# did not complete a controlled test exit, and the user will be informed of
# the problem. For example, if a test calls a bash function, and that function
# calls "exit 0", the test will end. If there were no test failures to that
# point, the test will appear to have completed successfully. The marker ensures
# the test runs either to a failed "ASSERT" or to the end of the test function.
# Arguments:
# $1 - The status code to exit with.
# Returns:
# The current test error count.
#######################################
btf::_end_of_test() {
local -i status=$1
printf "%s" "${_BTF_END_OF_TEST_MARKER}"
exit ${status}
}
#######################################
# Simply prints standard "existing" message to stdout and exits the script.
# Returns:
# The current test error count.
#######################################
btf::_assert_failed() {
if [[ ${_btf_test_pid} == ${BASHPID} ]]; then
echo -n "Aborting test due to failed ASSERT"
else
echo -n "Exiting subshell"
fi
echo ". ${_BTF_ASSERT_ERROR_COUNT_MESSAGE_PREFIX} ${_btf_test_error_count}."
# mark a controlled end of test, and return the error count from the subshell to main process
btf::_end_of_test ${_btf_test_error_count}
}
#######################################
# Prints the failure message (using the custom message if provided,
# or the default message from the BT_EXPECT_... script), increments the
# error count, and returns the given status.
# Arguments:
# $1 - The status code to return.
# $2 - A default message for stdout, used if there are no more parameters.
# $3 - (optional) A printf-style format string
# Remaining args - to supply format parameters in the format string
# Returns:
# The given status parameter value.
#######################################
btf::_fail() {
local -i status=$1
shift
local default_message="$1"
shift
local format_string="$1"
shift
if [[ "${format_string}" == "" ]]; then
format_string="${default_message}"
fi
local func_offset=0
local source_offset=0
if [[ "${FUNCNAME[$((3 + _BTF_EVAL_OFFSET))]}" != "btf::_run_isolated_test" ]]; then
: $((func_offset++))
fi
source_offset=$((func_offset + _BTF_EVAL_OFFSET))
local called_function="${FUNCNAME[$((1 + func_offset))]}"
local test_file_loc="${BASH_SOURCE[$((2 + source_offset))]#$BT_TEMP_DIR/}:${BASH_LINENO[$((1 + source_offset))]}"
printf "${_BTF_FAIL} ${test_file_loc}: (${called_function}) ${format_string}\n" "$@"
: $((_btf_test_error_count++))
return ${status}
}
#######################################
# Outputs the absolute path to the given file, also resolving symbolic links.
# (Note: This attempts to use the 'realpath' binary, and falls back to
# look for a valid parent, "cd" to it and use pwd -P.
# otherwise)
# Arguments:
# $1 - a relative file path. The full path does not have to exist, but
# all left-most path components that do exist are evaluated, resolving
# path symbols (such as ".", "..", "~") and replacing symbolic links
# Outputs:
# Writes the absolute path to stdout
# Returns:
# Exits the script with non-zero status if assertions fail
#######################################
btf::realpath() {
local path="$1"
[[ "${path}" != "" ]] ||
btf::abort 1 "btf::realpath: input path cannot be blank"
local rp
local -i status
if hash realpath >/dev/null 2>&1; then
rp="$(realpath -m "$path" 2>/dev/null)"
status=$?
fi
if [[ -z "${rp}" || ${status} -ne 0 ]]; then
local parent child
parent="$(dirname "$path")"
child="$(basename "$path")"
while [[ -n "$parent" && ! -d "$parent" ]]; do
child="$(basename "$parent")/${child}"
parent="$(dirname "$parent")"
done
if [[ -z "$parent" ]]; then
btf::abort 1 "btf::realpath: path has no valid parent and 'realpath' is not available. Cannot resolve: '${path}'"
fi
rp="$(cd "$parent" && pwd -P)/$child"
fi
[[ -n "${rp}" ]] ||
btf::abort 1 "btf::realpath: result for '${path}' was unexpectedly blank, status=${status}"
printf "%s" "${rp}"
return ${status}
}
#######################################
# Evaluates the arguments (interprets the arguments as a command string, using the
# 'eval' built-in), generates a non-fatal failure if the return status is not zero (0).
# Arguments:
# $@ - All arguments converted to a command string
# Returns:
# The status code returned from the executed command
#######################################
BT_EXPECT() {
local -i status
: $((_BTF_EVAL_OFFSET++))
# Do not run in a subshell. The command may set variables in the current shell.
eval "$@"
status=$?
: $((_BTF_EVAL_OFFSET--))
if (($status == 0)); then
return 0
fi
btf::_fail ${status} "Exit code: ${status}; expected 0 status from: $*"
}
#######################################
# Evaluates the arguments (interprets the arguments as a command string, using the
# 'eval' built-in), generates a fatal failure if the return status is not zero (0).
# Arguments:
# $@ - All arguments converted to a command string
# Returns:
# 0 if the command returned 0, exits the test script with 1 otherwise
#######################################
BT_ASSERT() {
BT_EXPECT "$@" || btf::_assert_failed
}
#######################################
# Produce a test failure with a given message
# Arguments:
# remaining arg(s) - custom error message (optional)
# Returns:
# 0
#######################################
BT_FAIL() {
# return 1 (*not* the given 0 status)
btf::_fail 1 "$@"
}
#######################################
# Evaluates the arguments (interprets the arguments as a command string, using the
# 'eval' built-in), generates a non-fatal failure if the return status is zero (0).
# Arguments:
# $@ - All arguments converted to a command string
# Returns:
# 0 if the command returned a non-zero status, 1 otherwise
#######################################
BT_EXPECT_FAIL() {
local -i status
: $((_BTF_EVAL_OFFSET++))
# Do not run in a subshell. The command may set variables in the current shell.
# (Even though the command is expected to return an error status code, it may still
# be expected to complete some changes and/or cause side effects. And for consistency
# with BT_EXPECT(), both scripts should be run without a subshell.)
eval "$@"
status=$?
: $((_BTF_EVAL_OFFSET--))
if (($status != 0)); then
return 0
fi
btf::_fail 1 "Exit code: 0; expected non-zero status from: $*"
}
#######################################
# Evaluates the arguments (interprets the arguments as a command string, using the
# 'eval' built-in), generates a fatal failure if the return status is zero (0).
# Arguments:
# $@ - All arguments converted to a command string
# Returns:
# 0 if the command returned a non-zero status, exits the test script with 1 otherwise
#######################################
BT_ASSERT_FAIL() {
BT_EXPECT_FAIL "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the first and second arguments
# are not equal.
# Arguments:
# $1 - left argument
# $2 - right argument
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the arguments are equal, or 1 otherwise
#######################################
BT_EXPECT_EQ() {
local lhs="$1"
shift
local rhs="$1"
shift
if [[ "${lhs}" == "${rhs}" ]]; then
return 0
fi
btf::_fail 1 "'${lhs}' != '${rhs}'" "$@"
}
#######################################
# Generates a fatal failure if the first and second arguments
# are not equal.
# Arguments:
# $1 - left argument
# $2 - right argument
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the arguments are equal, exits the test script with 1 otherwise
#######################################
BT_ASSERT_EQ() {
BT_EXPECT_EQ "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the first argument,
# assumed to be a return result, is non-zero.
# Arguments:
# $1 - return result
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the given status is 0, or returns the given status otherwise
#######################################
BT_EXPECT_GOOD_STATUS() {
local -i status="$1"
shift
if [[ ${status} == 0 ]]; then
return 0
fi
# return the given non-zero status (instead of 1, this time)
btf::_fail ${status} "Returned status '${status}' is not a success" "$@"
}
#######################################
# Generates a fatal failure if the first argument,
# assumed to be a return result, is non-zero.
# Arguments:
# $1 - return result
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the given status is 0, exits the test script with 1 otherwise
#######################################
BT_ASSERT_GOOD_STATUS() {
BT_EXPECT_GOOD_STATUS "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the first argument,
# assumed to be a return result, is zero (0), the success status.
# Arguments:
# $1 - return result
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the given status is not 0, 1 otherwise
#######################################
BT_EXPECT_BAD_STATUS() {
local -i status="$1"
shift
if [[ ${status} != 0 ]]; then
return 0
fi
# return 1 (*not* the given 0 status)
btf::_fail 1 "Expected an error status, but 0 is not an error" "$@"
}
#######################################
# Generates a fatal failure if the first argument,
# assumed to be a return result, is zero (0), the success status.
# Arguments:
# $1 - return result
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the given status is not 0, exits the test script with 1 otherwise
#######################################
BT_ASSERT_BAD_STATUS() {
BT_EXPECT_BAD_STATUS "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the first argument
# value is not an empty string.
# Arguments:
# $1 - value to check
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the string is empty, non-zero otherwise
#######################################
BT_EXPECT_EMPTY() {
local string="$1"
shift
if [[ "${string}" == "" ]]; then
return 0
fi
btf::_fail 1 "String '${string}' is not empty" "$@"
}
#######################################
# Generates a fatal failure if the first argument
# value is not an empty string.
# Arguments:
# $1 - value to check
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the string is empty, exits the test script with 1 otherwise
#######################################
BT_ASSERT_EMPTY() {
BT_EXPECT_EMPTY "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the first argument
# value does not exist or is the empty string.
# Arguments:
# $1 - value to check
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the string is not empty, non-zero otherwise
#######################################
BT_EXPECT_NOT_EMPTY() {
local string="$1"
shift
if [[ "${string}" != "" ]]; then
return 0
fi
btf::_fail 1 "String is empty" "$@"
}
#######################################
# Generates a fatal failure if the first argument
# value does not exist or is the empty string.
# Arguments:
# $1 - value to check
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the string is not empty, exits the test script with 1 otherwise
#######################################
BT_ASSERT_NOT_EMPTY() {
BT_EXPECT_NOT_EMPTY "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the file does not exist
# Arguments:
# $1 - filename
# $2 - expected file content
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the file exists, non-zero otherwise
#######################################
BT_EXPECT_FILE_EXISTS() {
local filename="$1"
shift
if [[ -e "${filename}" ]]; then
return 0
fi
btf::_fail 1 "File '${filename}' not found" "$@"
}
#######################################
# Generates a fatal failure if the file does not exist
# Arguments:
# $1 - filename
# $2 - expected file content
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the file exists, exits the test script with 1 otherwise
#######################################
BT_ASSERT_FILE_EXISTS() {
BT_EXPECT_FILE_EXISTS "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the file exists
# Arguments:
# $1 - filename
# $2 - expected file content
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the file does not exist, non-zero otherwise
#######################################
BT_EXPECT_FILE_DOES_NOT_EXIST() {
local filename="$1"
shift
if [[ ! -e "${filename}" ]]; then
return 0
fi
btf::_fail 1 "Existing file '${filename}' should not exist" "$@"
}
#######################################
# Generates a fatal failure if the file exists
# Arguments:
# $1 - filename
# $2 - expected file content
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the file does not exist, exits the test script with 1 otherwise
#######################################
BT_ASSERT_FILE_DOES_NOT_EXIST() {
BT_EXPECT_FILE_DOES_NOT_EXIST "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the file content
# does not match the given string
# Arguments:
# $1 - filename
# $2 - expected file content
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, non-zero otherwise
#######################################
BT_EXPECT_FILE_CONTAINS() {
local filename="$1"
shift
local expected_content="$1"
shift
if [[ -e "${filename}" ]]; then
if [[ "$(cat "${filename}")" == "${expected_content}" ]]; then
return 0
fi
btf::_fail 1 "File '${filename}' content does not match expected content:" "$@"
echo "expected: '${expected_content}'"
echo " actual: '$(cat "${filename}")'"
else
btf::_fail 1 "File '${filename}' not found" "$@"
fi
return 1
}
#######################################
# Generates a fatal failure if the file content
# does not match the given string
# Arguments:
# $1 - filename
# $2 - expected file content
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, exits the test script with 1 otherwise
#######################################
BT_ASSERT_FILE_CONTAINS() {
BT_EXPECT_FILE_CONTAINS "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the file
# does not contain the substring.
# Arguments:
# $1 - filename
# $2 - substring to look for in the file
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, non-zero otherwise
#######################################
BT_EXPECT_FILE_CONTAINS_SUBSTRING() {
local filename="$1"
shift
local substring="$1"
shift
if [[ -e "${filename}" ]]; then
if grep -q "${substring}" "${filename}"; then
return 0
fi
btf::_fail 1 "Substring '${substring}' not found in file '${filename}'" "$@"
echo "actual file content: '$(cat "${filename}")'"
else
btf::_fail 1 "File '${filename}' not found" "$@"
fi
return 1
}
#######################################
# Generates a non-fatal failure if the file
# does containS the substring.
# Arguments:
# $1 - filename
# $2 - substring to look for in the file
# remaining arg(s) - custom error message (optional)
# Returns:
# 1 if the substring was found, 0 otherwise
#######################################
BT_EXPECT_FILE_DOES_NOT_CONTAIN_SUBSTRING() {
local filename="$1"
shift
local substring="$1"
shift
if [[ -e "${filename}" ]]; then
if grep -q "${substring}" "${filename}"; then
btf::_fail 1 "Substring '${substring}' found in file '${filename}'" "$@"
echo "actual file content: '$(cat "${filename}")'"
fi
return 0
else
btf::_fail 1 "File '${filename}' not found" "$@"
fi
return 1
}
#######################################
# Generates a fatal failure if the file
# does not contain the substring.
# Arguments:
# $1 - filename
# $2 - substring to look for in test string
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, exits the test script with 1 otherwise
#######################################
BT_ASSERT_FILE_CONTAINS_SUBSTRING() {
BT_EXPECT_FILE_CONTAINS_SUBSTRING "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if no files in the directory
# contain the substring.
# Arguments:
# $1 - directory name
# $2 - substring to look for, in any file in the directory
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, non-zero otherwise
#######################################
BT_EXPECT_DIRECTORY_CONTAINS_SUBSTRING() {
local directory="$1"
shift
local substring="$1"
shift
if [[ -d "${directory}" ]]; then
if grep -Rq "${substring}" "${directory}"; then
return 0
fi
btf::_fail 1 "Substring '${substring}' not found in directory '${directory}'" "$@"
else
if [[ -e "${directory}" ]]; then
btf::_fail 1 "File '${directory}' is not a directory" "$@"
else
btf::_fail 1 "Directory '${directory}' not found" "$@"
fi
fi
return 1
}
#######################################
# Generates a fatal failure if no files in the directory
# contain the substring.
# Arguments:
# $1 - directory name
# $2 - substring to look for, in any file in the directory
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, exits the test script with 1 otherwise
#######################################
BT_ASSERT_DIRECTORY_CONTAINS_SUBSTRING() {
BT_EXPECT_DIRECTORY_CONTAINS_SUBSTRING "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the string
# does not contain the substring.
# Arguments:
# $1 - the full test string
# $2 - substring to look for in test string
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, non-zero otherwise
#######################################
BT_EXPECT_STRING_CONTAINS_SUBSTRING() {
local string="$1"
shift
local substring="$1"
shift
if [[ "${string#*$substring}" != "${string}" ]]; then
return 0
fi
btf::_fail 1 "Substring '${substring}' not found in string '${string}'" "$@"
return 1
}
#######################################
# Generates a fatal failure if the string
# does not contain the substring.
# Arguments:
# $1 - the full test string
# $2 - substring to look for in test string
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the substring was found, exits the test script with 1 otherwise
#######################################
BT_ASSERT_STRING_CONTAINS_SUBSTRING() {
BT_EXPECT_STRING_CONTAINS_SUBSTRING "$@" || btf::_assert_failed
}
#######################################
# Generates a non-fatal failure if the function
# identified by the first argument does not exist.
# BT_ASSERT_FUNCTION_EXISTS - Generates a fatal failure
# if the function identified by the first argument does not exist.
# Arguments:
# $1 - function name
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the function exists, non-zero otherwise
#######################################
BT_EXPECT_FUNCTION_EXISTS() {
local function="$1"
shift
if btf::function_exists "${function}"; then
return 0
fi
btf::_fail 1 "Function '${function}' not found" "$@"
}
#######################################
# Generates a fatal failure if the function
# identified by the first argument does not exist.
# Arguments:
# $1 - function name
# remaining arg(s) - custom error message (optional)
# Returns:
# 0 if the function exists, exits the test script with 1 otherwise
#######################################
BT_ASSERT_FUNCTION_EXISTS() {
BT_EXPECT_FUNCTION_EXISTS "$@" || btf::_assert_failed
}
#######################################
# Returns true (status 0) if the given root directory is a
# temporary bash_test_framework test execution directory.
# Arguments:
# $1 - directory path to check
# Globals:
# _BTF_TEMP_DIR_MARKER - name of the marker file to look for in the directory
# Returns:
# 0 if the given directory path contains the marker file for the test execution dir,
# non-zero otherwise
#######################################
btf::is_bt_temp_dir() {
local root_dir="$(btf::realpath "$1")"
shift
# As an additional safety measure, check the path name string length
# in addition to checking for the marker file.
local -r temp_dir_string_length=${#root_dir}
((${temp_dir_string_length} > 10)) &&
[[ -f "${root_dir}/${_BTF_TEMP_DIR_MARKER}" ]]
}
#######################################
# Creates a temporary directory for isolated test scripts and files (copies and/or
# mock files), and adds a marker file to identify the directory as a bash_test_framework
# test execution directory.
# Outputs:
# Writes the generated temporary directory name to stdout
# Globals:
# _BTF_TEMP_DIR_MARKER - name of the marker file to add to the specified directory
# Returns:
# 1 if the directory could not be created, 0 otherwise
#######################################
btf::_make_temp_dir() {
local temp_dir
temp_dir="$(mktemp -d -t "tmp.fx-self-test.XXXXXX")" || btf::abort $? "Unable to create temporary test directory."
touch "${temp_dir}/${_BTF_TEMP_DIR_MARKER}"
printf "${temp_dir}"
}
#######################################
# Makes a mock executable (via hard link) from the bash test framework's mock.sh,
# creating intermediate directories if needed.
# Globals:
# BT_TEMP_DIR - the root of the temporary test directory
# Returns:
# 1 if the mock executable could not be created, 0 otherwise
#######################################
btf::make_mock() {
local mockpath="$1"
local mockpath_realpath="$(btf::realpath "${mockpath}")"
local -r bt_temp_dir_realpath="$(btf::realpath "${BT_TEMP_DIR}")"
if [[ "${mockpath_realpath}" != "${bt_temp_dir_realpath}"/* ]]; then
btf::error "mocked executable path '${mockpath_realpath}',
is outside the BT_TEMP_DIR root directory '${bt_temp_dir_realpath}'."
return 1
fi
mkdir -p $(dirname "${mockpath_realpath}") || return $?
ln -f "${BT_TEMP_DIR}/${_BTF_FRAMEWORK_SCRIPT_SUBDIR}/mock.sh" "${mockpath_realpath}"
}
btf::_simplify_mock_extension() {
local extension="$1"
local simplify=${extension//_/}
simplify=${simplify//-/}
simplify=${simplify/mock/}
simplify=${simplify%s}
printf "%s" "${simplify}"
}
btf::_sanity_check_mocks() {
local linked_mock_script="${BT_TEMP_DIR}/${_BTF_FRAMEWORK_SCRIPT_SUBDIR}/mock.sh"
local mocks=($(find "${BT_TEMP_DIR}" -samefile "${linked_mock_script}"))
local simplify
local suggestion
local valid_extensions="mock_stdout|mock_stderr|mock_status|mock_side_effects|mock_state"
for mock in "${mocks[@]}"; do
for file in $(ls "${mock}".* 2>/dev/null); do
extension=${file##*.}
if ! [[ "${extension}" =~ ${valid_extensions} ]]; then
local suggestion=". Valid mock extensions are: ${valid_extensions//|/, }"
extension_simplified="$(btf::_simplify_mock_extension ${extension})"
local valid_extensions_array=(${valid_extensions//|/ })
for valid_extension in "${valid_extensions_array[@]}"; do
valid_simplified="$(btf::_simplify_mock_extension ${valid_extension})"
if [[ "${extension_simplified}" == "${valid_simplified}" ]]; then
suggestion=". Perhaps you meant to use the extension '${valid_extension}'"
fi
done
echo "Unexpected file extension for mock executable '${file#$BT_TEMP_DIR}'${suggestion}."
return 1
fi
done
done
return 0
}
#######################################
# Copies the testing framework, the test script, and its dependencies from the
# original source to the test execution temporary directory, and calls the
# BT_INIT_TEMP_DIR function from the new test execution directory, if given.
# Arguments:
# $1 - relative path from the root to the host (test) script
# Globals:
# _BTF_TEMP_DIR_MARKER (in)
# _BTF_HOST_SCRIPT_NAME (in)
# _BTF_FRAMEWORK_SCRIPT_SUBDIR (in)
# BT_TEMP_DIR (propagated to subshell for optional BT_INIT_TEMP_DIR function)
# BT_DEPS_DIR (original directory from which files will be copied to the temp dir)
# Outputs:
# Error messages to stderr
# Returns:
# 1 on any failure, 0 if everything completed successfully
#######################################
btf::_init_temp_dir() {
local host_script_subdir="$1"
shift
# If the _BTF_TEMP_DIR_MARKER exists in the current working directory, the test
# is running from a temp directory.
if [[ -f "${BT_DEPS_ROOT}/${_BTF_TEMP_DIR_MARKER}" ]]; then
btf::error "Attempted ${FUNCNAME[0]} from an existing temp directory."
return 1
fi
local -r bt_deps_root_realpath="$(btf::realpath "${BT_DEPS_ROOT}")"
local -r bt_temp_dir_realpath="$(btf::realpath "${BT_TEMP_DIR}")"
# Copy the host script and the test framework.
mkdir -p "${BT_TEMP_DIR}/${host_script_subdir}" || return $?
cp "${BT_DEPS_ROOT}/${host_script_subdir}/${_BTF_HOST_SCRIPT_NAME}" \
"${BT_TEMP_DIR}/${host_script_subdir}/${_BTF_HOST_SCRIPT_NAME}" || return $?
mkdir -p "${BT_TEMP_DIR}/${_BTF_FRAMEWORK_SCRIPT_SUBDIR}" || return $?
cp -r "${BT_DEPS_ROOT}/${_BTF_FRAMEWORK_SCRIPT_SUBDIR}/"* \
"${BT_TEMP_DIR}/${_BTF_FRAMEWORK_SCRIPT_SUBDIR}/" || return $?
# mock.sh will be hard linked to MOCKED_FILES. Prevent writing, but make it executable.
chmod a-w,u+x "${BT_TEMP_DIR}/${_BTF_FRAMEWORK_SCRIPT_SUBDIR}/mock.sh" || return $?
# Copy original files and/or directories (recursively) declared by the test,
# creating intermediate directories as needed.
for filepath in "${BT_FILE_DEPS[@]}"; do
local filepath_realpath="$(btf::realpath "${BT_DEPS_ROOT}/${filepath}")"
if [[ "${filepath_realpath}" != "${bt_deps_root_realpath}"/* ]]; then
btf::error "BT_FILE_DEPS element '${filepath}' expands to '${filepath_realpath}',
which is outside the root directory '${bt_deps_root_realpath}'."
return 1
fi
mkdir -p $(dirname "${BT_TEMP_DIR}/${filepath}") || return $?
cp -r "${BT_DEPS_ROOT}/${filepath}" "${BT_TEMP_DIR}/${filepath}" || return $?
done
# Link original files/directories declared by the test,
# creating intermediate directories as needed.
for filepath in "${BT_LINKED_DEPS[@]}"; do
local filepath_realpath="$(btf::realpath "${BT_DEPS_ROOT}/${filepath}")"
if [[ "${filepath_realpath}" != "${bt_deps_root_realpath}"/* ]]; then
btf::error "BT_LINKED_DEPS element '${filepath}' expands to '${filepath_realpath}',
which is outside the root directory '${bt_deps_root_realpath}'."
return 1
fi
mkdir -p $(dirname "${BT_TEMP_DIR}/${filepath}") || return $?
ln -s "${BT_DEPS_ROOT}/${filepath}" "${BT_TEMP_DIR}/${filepath}" || return $?
done
# Make mock executables as hardlinks to the BT_TEMP_DIR copy of the framework's mock.sh.
for mockpath in "${BT_MOCKED_TOOLS[@]}"; do
btf::make_mock "${BT_TEMP_DIR}/${mockpath}" || return $?
done
# Create additional directories declared by the test.
for dirpath in "${BT_MKDIR_DEPS[@]}"; do
local dirpath_realpath="$(btf::realpath "${BT_TEMP_DIR}/${dirpath}")"
if [[ "${dirpath_realpath}" != "${bt_temp_dir_realpath}"/* ]]; then
btf::error "BT_MKDIR_DEPS element '${dirpath}' expands to '${dirpath_realpath}',
which is outside the root directory '${bt_temp_dir_realpath}'."
return 1
fi
mkdir -p "${BT_TEMP_DIR}/${dirpath}" || return $?
done
# Execute the BT_INIT_TEMP_DIR function
(
cd "${BT_TEMP_DIR}"
BT_INIT_TEMP_DIR
) || return $?
}
#######################################
# Called after btf::_init_temp_dir, this function restarts the
# test script in a subshell, with a clean environment and with the
# current directory set to the BT_TEMP_ROOT directory. The function returns
# the status returned from the subshell.
# Arguments:
# $1 - relative path from the root to the host (test) script
# $2 - the name of the host (test) script
# $3 - the name of the test function to run
# Globals:
# USER (in)
# HOME (in)
# BT_TEMP_DIR (test execution dir path, propagated to subshell)
# _BTF_FRAMEWORK_SCRIPT_SUBDIR (in)
# _BTF_SUBSHELL_TEST_FUNCTION (propagated to subshell)
# Returns:
# 0 if the script was successfully executed and the test succeeded;
# MAX_ERROR_STATUS if there was a test execution error;
# otherwise, the count of test failures.
#######################################
btf::_launch_isolated_test_script() {
local -i test_counter=$1
shift
local host_script_subdir="$1"
shift
local host_script_name="$1"
shift
local test_function_name="$1"
shift
local test_args=()
if (($# > 0)); then
test_args=(-- "$@")
fi
# propagate certain bash flags if present
shell_flags=()
if [[ $- == *x* ]]; then
shell_flags+=(-x)
fi
local host_script_dir="${BT_TEMP_DIR}/${host_script_subdir}"
local host_script_path="${host_script_dir}/${host_script_name}"
# Start a clean environment, cd to the BT_TEMP_DIR subdirectory containing
# the test script, load the bash_test_framework.sh, then re-start the test
# script for the specific test.
local launch_script="$(
cat <<EOF
cd '${host_script_dir}'
source '${BT_TEMP_DIR}/${_BTF_FRAMEWORK_SCRIPT_SUBDIR}/bash_test_framework.sh' \
|| exit 1
source '${host_script_path}' $(((${#test_args[@]} > 0)) && printf "'%s' " "${test_args[@]}") \
|| exit \$?
EOF
)"
/usr/bin/env -i \
USER="${USER}" \
HOME="${HOME}" \
BT_TEMP_DIR="${BT_TEMP_DIR}" \
_BTF_FRAMEWORK_SCRIPT_SUBDIR="${_BTF_FRAMEWORK_SCRIPT_SUBDIR}" \
_BTF_SUBSHELL_TEST_FUNCTION="${test_function_name}" \
_BTF_SUBSHELL_TEST_NUMBER="${test_counter}" \
bash "${shell_flags[@]}" \
-c "${launch_script}" "${host_script_path}" ||
return $?
}
#######################################
# Called after test execution, this function validates the given directory path
# is the temporary directory created for a test, then deletes the directory and all
# content, recursively.
# Arguments:
# $1 - path to the BT_TEMP_DIR to clean up
# Globals:
# _BTF_TEMP_DIR_MARKER (in)
# Returns:
# 0 if successful, otherwise, exits the script with a non-zero status.
#######################################
btf::_clean_up_temp_dir() {
# Clean up. rm -rf is dangerous - make sure at least the temp dir string
# is of reasonable length.
if btf::is_bt_temp_dir "${BT_TEMP_DIR}"; then
rm -rf "${BT_TEMP_DIR}"
else
btf::error "Invalid BT_TEMP_DIR dir path - aborting cleanup."
btf::stderr "Given directory path was '${BT_TEMP_DIR}'."
exit 1
fi
}
#######################################
# Sets scoped local variables initialized by caller to values
# indicated by command line options, if provided.
# Arguments:
# Command line arguments, if any
# Returns:
# 0 if successful, otherwise, exits the script with a non-zero status.
#######################################
btf::_get_options() {
while (($# > 0)); do
local opt="$1"
shift
case "${opt}" in
--test)
(($# > 0)) || btf::abort 1 "Test option '--test TEST_name' is missing the test name"
_BTF_TEST_NAME_FILTER="$1"
shift
;;
--help)
btf::stderr "
Test options include:
--test <TEST_name>
Run only the test matching the given name.
-- args to add to BT_TEST_ARGS
Example:
fx self-test my_script_test --test TEST_some_function -- --test specific --options here
Tests found in ${_BTF_HOST_SCRIPT}:
$(btf::_get_test_functions)
"
exit 0
;;
--)
break
;;
*)
btf::abort 1 "Invalid test option: $opt. Try '--help' instead."
;;
esac
done
# save any arguments after "--"
BT_TEST_ARGS=("$@")
}
#######################################
# Called from a clean environment in a subshell, run the
# test function identified by name.
# Arguments:
# $1 - Name of the test function to execute.
# Globals:
# _ANSI_... constants (in)
# _btf_test_error_count (in/out)
# Returns:
# Failure count on test failures, MAX_ERROR_STATUS if there was a
# test execution error (such as during test set up), or 0 if passed
# (no errors or test failures)
#######################################
btf::_run_isolated_test() {
local test_function_name="$1"
# Safety checks
btf::is_bt_temp_dir "${BT_TEMP_DIR}" ||
btf::abort ${MAX_ERROR_STATUS} "BT_TEMP_DIR is not a valid temp dir path: ${BT_TEMP_DIR}"
[[ "$(pwd)" == "${_BTF_HOST_SCRIPT_DIR}" ]] ||
btf::abort ${MAX_ERROR_STATUS} "Current directory '$(pwd)' should be '${_BTF_HOST_SCRIPT_DIR}'"
[[ "${_BTF_HOST_SCRIPT_DIR}" == "${BT_TEMP_DIR}"* ]] ||
btf::abort ${MAX_ERROR_STATUS} "Test script dir '${_BTF_HOST_SCRIPT_DIR}' not in BT_TEMP_DIR='${BT_TEMP_DIR}'"
BT_SET_UP ||
btf::abort ${MAX_ERROR_STATUS} "BT_SET_UP function returned error status $?"
if [[ $_BTF_SUBSHELL_TEST_NUMBER == 1 ]]; then
local error_message=
error_message="$(btf::_sanity_check_mocks)" ||
btf::abort ${MAX_ERROR_STATUS} "${error_message} (error status $?)"
fi
# Call the test function.
# Run the test in a subshell so any ASSERT will only exit the subshell
local -i status
local stdout
stdout=$(
local -i status=0
local -i _btf_test_pid=${BASHPID}
local -i _btf_test_error_count=0 # incremented by failed BT_EXPECT_... function calls
${test_function_name}
status=$?
# The test function is not required to return an error status, but if they do,
# it should only be because a test failed.
if [[ $status != 0 && $_btf_test_error_count == 0 ]]; then
btf::abort ${MAX_ERROR_STATUS} \
"Unexpected error status ${status} without incrementing _btf_test_error_count"
fi
# mark a controlled end of test, and return the error count from the subshell to main process
btf::_end_of_test ${_btf_test_error_count}
)
status=$?
local test_output="${stdout%$_BTF_END_OF_TEST_MARKER}"
if ((${#test_output} > 0)); then
printf "\n%s" "${test_output}"
fi
if [[ "${test_output}" == "${stdout}" ]]; then
echo # start error message on a new line
btf::error "$(
cat <<EOF
Test exited prematurely, with status ${status}.
If the test calls a function that invokes 'exit', use a subshell; for example:
BT_EXPECT "( function_that_may_exit )"
EOF
)"
fi
if [[ ${status} == 0 ]]; then
echo "[${_ANSI_BRIGHT_GREEN}PASSED${_ANSI_CLEAR}]"
elif [[ ${status} == ${MAX_ERROR_STATUS} ]]; then
echo "[${_ANSI_BRIGHT_RED}ERROR${_ANSI_CLEAR}]"
else
echo "[${_ANSI_BRIGHT_RED}FAILED${_ANSI_CLEAR}]"
fi
BT_TEAR_DOWN ||
btf::abort ${MAX_ERROR_STATUS} "BT_TEAR_DOWN function returned error status $?"
return ${status}
}
btf::_get_test_functions() {
local bash_functions_declaration_order_sedprog="
s/^ *\(${_BTF_FUNCTION_NAME_PREFIX}[-a-zA-Z0-9_]*\) *().*\$/\1/p;
s/^ *function *\(${_BTF_FUNCTION_NAME_PREFIX}[-a-zA-Z0-9_]*\).*\$/\1/p;
"
sed -ne "${bash_functions_declaration_order_sedprog}" "${_BTF_HOST_SCRIPT}"
}
#######################################
# Creates a temporary directory, copies scripts and resources
# to that directory, restarts the shell with a clean environment
# and sets the current directory to the root of the temporary
# directory, re-starts the test script, and executes the test
# script's functions prefixed with TEST_.
# Arguments:
# Command line options (see btf::_get_options)
# Globals:
# BT_DEPS_ROOT (in/out) directory path from which BT_FILE_DEPS,
# BT_MKDIR_DEPS, and BT_MOCKED_TOOLS are relative paths. For Fuchsia
# tests this is normally FUCHSIA_DIR, and can be derived by default
# from the test script location.
# _BTF_... - readonly values defined at the top of this script
# Returns:
# error count on error (exits), 0 if passed (no errors or test failures)
#######################################
btf::_run_tests_in_isolation() {
local -i test_counter=0
local -i total_error_count=0
local -i test_failure_count=0
if [[ "${BT_DEPS_ROOT}" == "" ]]; then
BT_DEPS_ROOT="${_BTF_DEFAULT_ROOT_DIR}"
fi
readonly BT_DEPS_ROOT
local -r host_script_subdir="${_BTF_HOST_SCRIPT_DIR#$BT_DEPS_ROOT/}"
local -r _BTF_FRAMEWORK_SCRIPT_SUBDIR="${_BTF_FRAMEWORK_SCRIPT_DIR#$BT_DEPS_ROOT/}"
if [[ ${#BT_TEST_FUNCTIONS} == 0 ]]; then
BT_TEST_FUNCTIONS=(
$(btf::_get_test_functions)
) || return $?
fi
local has_filter=false
local found_filtered_test=false
if [[ "${_BTF_TEST_NAME_FILTER}" != "" ]]; then
has_filter=true
fi
for next_test in "${BT_TEST_FUNCTIONS[@]}"; do
if $has_filter; then
if [[ "${next_test}" != "${_BTF_TEST_NAME_FILTER}" ]]; then
continue
else
found_filtered_test=true
fi
fi
: $((test_counter++))
BT_TEMP_DIR=$(btf::_make_temp_dir) ||
return $?
export BT_TEMP_DIR
btf::_init_temp_dir "${host_script_subdir}" ||
return $?
# Launch the test in a subshell with clean environment
echo -n "[${test_counter}] ${next_test}() "
local test_error_count=0
btf::_launch_isolated_test_script \
"${test_counter}" \
"${host_script_subdir}" \
"${_BTF_HOST_SCRIPT_NAME}" "${next_test}" "${BT_TEST_ARGS[@]}"
test_error_count=$?
if [[ test_error_count == ${MAX_ERROR_STATUS} ]]; then
btf::abort ${test_error_count} "Fatal test execution error"
fi
# Keep temporary directory for debugging.
if [[ ${test_error_count} == 0 ]]; then
btf::_clean_up_temp_dir || return $?
else
echo "Preserving the temp directory: ${BT_TEMP_DIR}"
fi
if ((${test_error_count} > 0)); then
: $((test_failure_count++))
: $((total_error_count += test_error_count))
fi
done
if $has_filter && ! $found_filtered_test; then
btf::failed "Test function '${_BTF_TEST_NAME_FILTER}' was not found"
return 1
fi
if [[ ${test_failure_count} == 0 ]]; then
if ((${test_counter} == 1)); then
btf::success "1 test passed."
else
btf::success "All ${test_counter} tests passed."
fi
return 0
fi
local error_count_str
if ((${total_error_count} > 1)); then
error_count_str="(${total_error_count} errors)"
else
error_count_str="(1 error)"
fi
btf::failed "${test_failure_count} of ${test_counter} tests failed ${error_count_str}."
# Do not return from the function.
# Exit the script, with error count (0 if PASSED), so the
# host script is not responsible for propagating the error.
exit ${total_error_count}
}
#######################################
# On first invocation from host test script, _BTF_SUBSHELL_TEST_FUNCTION is not
# set, so the script calls btf::_run_tests_in_isolation to cycle through all
# declared TEST_... functions. If _BTF_SUBSHELL_TEST_FUNCTION is set, the test
# script has been re-entered, via a subshell with a clean environment, and the
# _BTF_SUBSHELL_TEST_FUNCTION contains the name of the next test function to
# execute.
# Arguments:
# Command line options (forwarded to btf::_run_tests_in_isolation)
# Globals:
# _BTF_SUBSHELL_TEST_FUNCTION (in), set only if script is re-entered
# _btf_... collected function variables (scoped to function)
# _BTF_... - readonly values defined at the top of this script
# Returns:
# Failure count on test failures, MAX_ERROR_STATUS if there was a
# test execution error (such as during test set up), or 0 if passed
# (no errors or test failures)
#######################################
BT_RUN_TESTS() {
# Get command line options
local _BTF_TEST_NAME_FILTER=
local BT_TEST_ARGS=()
btf::_get_options "$@" || return $?
local -i status=0
if [[ "${_BTF_SUBSHELL_TEST_FUNCTION}" != "" ]]; then
btf::_run_isolated_test "${_BTF_SUBSHELL_TEST_FUNCTION}" ||
status=$?
else
btf::_run_tests_in_isolation "$@" ||
status=$?
fi
# "exit" the script (do not "return"), with the error count, (error count is
# 0 if test(s) PASSED), so the host script authors do not have to worry about
# adding logic to propagating the error.
exit ${status}
}