| #!/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} | |
| } |