#!/usr/bin/env bash set -eET flags=('--dummy-flag') num_jobs=${BATS_NUMBER_OF_PARALLEL_JOBS:-1} extended_syntax='' BATS_TRACE_LEVEL="${BATS_TRACE_LEVEL:-0}" declare -r BATS_RETRY_RETURN_CODE=126 export BATS_TEST_RETRIES=0 # no retries by default while [[ "$#" -ne 0 ]]; do case "$1" in -j) shift num_jobs="$1" ;; -T) flags+=('-T') ;; -x) flags+=('-x') extended_syntax=1 ;; --no-parallelize-within-files) # use singular to allow for users to override in file BATS_NO_PARALLELIZE_WITHIN_FILE=1 ;; --dummy-flag) ;; --trace) flags+=('--trace') ;; --print-output-on-failure) flags+=(--print-output-on-failure) ;; --show-output-of-passing-tests) flags+=(--show-output-of-passing-tests) ;; --verbose-run) flags+=(--verbose-run) ;; --gather-test-outputs-in) shift flags+=(--gather-test-outputs-in "$1") ;; *) break ;; esac shift done filename="$1" TESTS_FILE="$2" if [[ ! -f "$filename" ]]; then printf 'Testfile "%s" not found\n' "$filename" >&2 exit 1 fi export BATS_TEST_FILENAME="$filename" # shellcheck source=lib/bats-core/preprocessing.bash # shellcheck disable=SC2153 source "$BATS_ROOT/$BATS_LIBDIR/bats-core/preprocessing.bash" bats_run_setup_file() { # shellcheck source=lib/bats-core/tracing.bash # shellcheck disable=SC2153 source "$BATS_ROOT/$BATS_LIBDIR/bats-core/tracing.bash" # shellcheck source=lib/bats-core/test_functions.bash # shellcheck disable=SC2153 source "$BATS_ROOT/$BATS_LIBDIR/bats-core/test_functions.bash" _bats_test_functions_setup -1 # invalid TEST_NUMBER, as this is not a test exec 3<&1 # these are defined only to avoid errors when referencing undefined variables down the line # shellcheck disable=2034 BATS_TEST_NAME= # used in tracing.bash # shellcheck disable=2034 BATS_TEST_COMPLETED= # used in tracing.bash BATS_SOURCE_FILE_COMPLETED= BATS_SETUP_FILE_COMPLETED= BATS_TEARDOWN_FILE_COMPLETED= # shellcheck disable=2034 BATS_ERROR_STATUS= # used in tracing.bash touch "$BATS_OUT" bats_setup_tracing trap 'bats_file_teardown_trap' EXIT local status=0 # get the setup_file/teardown_file functions for this file (if it has them) # shellcheck disable=SC1090 source "$BATS_TEST_SOURCE" BATS_SOURCE_FILE_COMPLETED=1 bats_set_stacktrace_limit setup_file >>"$BATS_OUT" 2>&1 BATS_SETUP_FILE_COMPLETED=1 } bats_run_teardown_file() { local bats_teardown_file_status=0 # avoid running the therdown trap due to errors in teardown_file trap 'bats_file_exit_trap' EXIT bats_set_stacktrace_limit # rely on bats_error_trap to catch failures teardown_file >>"$BATS_OUT" 2>&1 || bats_teardown_file_status=$? if ((bats_teardown_file_status == 0)); then BATS_TEARDOWN_FILE_COMPLETED=1 elif [[ -n "${BATS_SETUP_FILE_COMPLETED:-}" ]]; then BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1 BATS_ERROR_STATUS=$bats_teardown_file_status return $BATS_ERROR_STATUS fi } # shellcheck disable=SC2317 bats_file_teardown_trap() { bats_run_teardown_file bats_file_exit_trap in-teardown_trap } # shellcheck source=lib/bats-core/common.bash source "$BATS_ROOT/$BATS_LIBDIR/bats-core/common.bash" # shellcheck disable=SC2317 bats_file_exit_trap() { local -r last_return_code=$? if [[ ${1:-} != in-teardown_trap ]]; then BATS_ERROR_STATUS=$last_return_code fi trap - ERR EXIT local failure_reason local -i failure_test_index=$((BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE + 1)) if [[ -n "${BATS_TEST_SKIPPED-}" ]]; then export BATS_TEST_SKIPPED # indicate to exec-test that it should skip # print skip message for each test (use this to avoid reimplementing filtering) bats_run_tests 1<&3 # restore original stdout (this is running in setup_file's redirection to BATS_OUT) bats_exec_file_status=0 # this should not lead to errors elif [[ -z "$BATS_SETUP_FILE_COMPLETED" || -z "$BATS_TEARDOWN_FILE_COMPLETED" ]]; then if [[ -z "$BATS_SETUP_FILE_COMPLETED" ]]; then failure_reason='setup_file' elif [[ -z "$BATS_TEARDOWN_FILE_COMPLETED" ]]; then failure_reason='teardown_file' failure_test_index=$((BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE + ${#tests_to_run[@]} + 1)) elif [[ -z "$BATS_SOURCE_FILE_COMPLETED" ]]; then failure_reason='source' else failure_reason='unknown internal' fi printf "not ok %d %s\n" "$failure_test_index" "$failure_reason failed" >&3 local stack_trace bats_get_failure_stack_trace stack_trace bats_print_stack_trace "${stack_trace[@]}" >&3 bats_print_failed_command "${stack_trace[@]}" >&3 bats_prefix_lines_for_tap_output <"$BATS_OUT" | bats_replace_filename >&3 rm -rf "$BATS_OUT" bats_exec_file_status=1 fi # setup_file not executed but defined in this test file? -> might be defined in the wrong file if [[ -z "${BATS_SETUP_SUITE_COMPLETED-}" ]] && declare -F setup_suite >/dev/null; then bats_generate_warning 3 --no-stacktrace "$BATS_TEST_FILENAME" fi exit "$bats_exec_file_status" } function setup_file() { return 0 } function teardown_file() { return 0 } bats_forward_output_of_parallel_test() { local test_number_in_suite=$1 local status=0 wait "$(cat "$output_folder/$test_number_in_suite/pid")" || status=1 cat "$output_folder/$test_number_in_suite/stdout" cat "$output_folder/$test_number_in_suite/stderr" >&2 return $status } bats_is_next_parallel_test_finished() { local PID # get the pid of the next potentially finished test PID=$(cat "$output_folder/$((test_number_in_suite_of_last_finished_test + 1))/pid") # try to send a signal to this process # if it fails, the process exited, # if it succeeds, the process is still running if kill -0 "$PID" 2>/dev/null; then return 1 fi } # prints output from all tests in the order they were started # $1 == "blocking": wait for a test to finish before printing # != "blocking": abort printing, when a test has not finished bats_forward_output_for_parallel_tests() { local status=0 # was the next test already started? while ((test_number_in_suite_of_last_finished_test + 1 <= test_number_in_suite)); do # if we are okay with waiting or if the test has already been finished if [[ "$1" == "blocking" ]] || bats_is_next_parallel_test_finished; then ((++test_number_in_suite_of_last_finished_test)) bats_forward_output_of_parallel_test "$test_number_in_suite_of_last_finished_test" || status=$? else # non-blocking and the process has not finished -> abort the printing break fi done return $status } bats_run_test_with_retries() { # local status=0 local should_try_again=1 try_number for ((try_number = 1; should_try_again; ++try_number)); do if "$BATS_LIBEXEC/bats-exec-test" "$@" "$try_number"; then should_try_again=0 else status=$? if ((status == BATS_RETRY_RETURN_CODE)); then should_try_again=1 status=0 # this is not the last try -> reset status else should_try_again=0 bats_exec_file_status=$status fi fi done return $status } bats_run_tests_in_parallel() { local output_folder="$BATS_RUN_TMPDIR/parallel_output" local status=0 mkdir -p "$output_folder" # shellcheck source=lib/bats-core/semaphore.bash source "$BATS_ROOT/$BATS_LIBDIR/bats-core/semaphore.bash" bats_semaphore_setup # the test_number_in_file is not yet incremented -> one before the next test to run local test_number_in_suite_of_last_finished_test="$BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE" # stores which test was printed last local test_number_in_file=0 test_number_in_suite=$BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE for test_name in "${tests_to_run[@]}"; do # Only handle non-empty lines if [[ $test_name ]]; then ((++test_number_in_suite)) ((++test_number_in_file)) mkdir -p "$output_folder/$test_number_in_suite" bats_semaphore_run "$output_folder/$test_number_in_suite" \ bats_run_test_with_retries "${flags[@]}" "$filename" "$test_name" "$test_number_in_suite" "$test_number_in_file" \ >"$output_folder/$test_number_in_suite/pid" fi # print results early to get interactive feedback bats_forward_output_for_parallel_tests non-blocking || status=1 # ignore if we did not finish yet done bats_forward_output_for_parallel_tests blocking || status=1 return $status } bats_read_tests_list_file() { local line_number=0 tests_to_run=() # the global test number must be visible to traps -> not local local test_number_in_suite='' while read -r test_line; do # check if the line begins with filename # filename might contain some hard to parse characters, # use simple string operations to work around that issue if [[ "$filename" == "${test_line::${#filename}}" ]]; then # get the rest of the line without the separator \t test_name=${test_line:$((1 + ${#filename}))} tests_to_run+=("$test_name") # save the first test's number for later iteration # this assumes that tests for a file are stored consecutive in the file! if [[ -z "$test_number_in_suite" ]]; then test_number_in_suite=$line_number fi fi ((++line_number)) done <"$TESTS_FILE" BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE="$test_number_in_suite" declare -ri BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE # mark readonly (cannot merge assignment, because value would be lost) } bats_run_tests() { bats_exec_file_status=0 # TODO: this does not seem to be used anymore? if [[ "${BATS_RUN_TESTS_SKIPPED-}" ]]; then # shellcheck disable=SC2317 bats_test_begin() { printf "ok %d %s # skip %s\n" "$test_number_in_suite" "$1" "$BATS_RUN_TESTS_SKIPPED_REASON" >&3 return 1 } local test_number_in_suite=0 for test_name in "${tests_to_run[@]}"; do ((++test_number_in_suite)) eval "$test_name" || true done exit 0 fi if [[ "$num_jobs" -lt 1 ]]; then printf 'Invalid number of jobs: %s\n' "$num_jobs" >&2 exit 1 fi if [[ "$num_jobs" != 1 && "${BATS_NO_PARALLELIZE_WITHIN_FILE-False}" == False ]]; then export BATS_SEMAPHORE_NUMBER_OF_SLOTS="$num_jobs" bats_run_tests_in_parallel "$BATS_RUN_TMPDIR/parallel_output" || bats_exec_file_status=1 else local test_number_in_suite=$BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE \ test_number_in_file=0 for test_name in "${tests_to_run[@]}"; do #echo "bats-exec-file: execute test ${test_name}" >&2 if [[ "${BATS_INTERRUPTED-NOTSET}" != NOTSET ]]; then bats_exec_file_status=130 # bash's code for SIGINT exits break fi # Only handle non-empty lines if [[ -n ${test_name-} ]]; then ((++test_number_in_suite)) ((++test_number_in_file)) bats_run_test_with_retries "${flags[@]}" "$filename" "$test_name" \ "$test_number_in_suite" "$test_number_in_file" || bats_exec_file_status=$? fi done fi } bats_create_file_tempdirs() { local bats_files_tmpdir="${BATS_RUN_TMPDIR}/file" if ! mkdir -p "$bats_files_tmpdir"; then printf 'Failed to create %s\n' "$bats_files_tmpdir" >&2 exit 1 fi BATS_FILE_TMPDIR="$bats_files_tmpdir/${BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE?}" if ! mkdir "$BATS_FILE_TMPDIR"; then printf 'Failed to create BATS_FILE_TMPDIR=%s\n' "$BATS_FILE_TMPDIR" >&2 exit 1 fi ln -s "$BATS_TEST_FILENAME" "$BATS_FILE_TMPDIR-$(basename "$BATS_TEST_FILENAME").source_file" export BATS_FILE_TMPDIR } trap 'BATS_INTERRUPTED=true' INT BATS_FILE_FIRST_TEST_NUMBER_IN_SUITE=0 # predeclare as Bash 3.2 does not support declare -g bats_read_tests_list_file # don't run potentially expensive setup/teardown_file # when there are no tests to run if [[ ${#tests_to_run[@]} -eq 0 ]]; then exit 0 fi if [[ -n "$extended_syntax" ]]; then printf "suite %s\n" "$filename" fi # requires the test list to be read but not empty bats_create_file_tempdirs bats_preprocess_source "$filename" trap bats_interrupt_trap INT bats_run_setup_file # during tests, we don't want to get backtraces from this level # just wait for the test to be interrupted and display their trace trap 'BATS_INTERRUPTED=true' INT bats_run_tests trap bats_interrupt_trap INT bats_run_teardown_file exit $bats_exec_file_status