#!/usr/bin/env bash set -e count_only_flag='' filter='' num_jobs=${BATS_NUMBER_OF_PARALLEL_JOBS:-1} parallel_binary_name=${BATS_PARALLEL_BINARY_NAME:-"parallel"} bats_no_parallelize_across_files=${BATS_NO_PARALLELIZE_ACROSS_FILES-} bats_no_parallelize_within_files= filter_status='' gather_tests_flags=('--dummy-flag') flags=('--dummy-flag') # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions setup_suite_file='' BATS_TRACE_LEVEL="${BATS_TRACE_LEVEL:-0}" BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS= abort() { printf 'Error: %s\n' "$1" >&2 exit 1 } # shellcheck source=lib/bats-core/common.bash disable=SC2153 source "$BATS_ROOT/$BATS_LIBDIR/bats-core/common.bash" while [[ "$#" -ne 0 ]]; do case "$1" in -c) count_only_flag=1 ;; -f) shift filter="$1" ;; -j) shift num_jobs="$1" flags+=('-j' "$num_jobs") ;; --parallel-binary-name) shift parallel_binary_name="$1" ;; -T) flags+=('-T') ;; -x) flags+=('-x') ;; --no-parallelize-across-files) bats_no_parallelize_across_files=1 ;; --no-parallelize-within-files) bats_no_parallelize_within_files=1 flags+=("--no-parallelize-within-files") ;; --filter-status) shift filter_status="$1" ;; --filter-tags) shift gather_tests_flags+=("--filter-tags" "$1") ;; --dummy-flag) ;; --trace) flags+=('--trace') ((++BATS_TRACE_LEVEL)) # avoid returning 0 ;; --print-output-on-failure) flags+=(--print-output-on-failure) ;; --show-output-of-passing-tests) flags+=(--show-output-of-passing-tests) BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS=1 ;; --verbose-run) flags+=(--verbose-run) ;; --gather-test-outputs-in) shift flags+=(--gather-test-outputs-in "$1") ;; --setup-suite-file) shift setup_suite_file="$1" ;; *) break ;; esac shift done if [[ "$num_jobs" != 1 ]]; then if ! type -p "${parallel_binary_name}" >/dev/null && "${parallel_binary_name}" --version &>/dev/null && [[ -z "$bats_no_parallelize_across_files" ]]; then abort "Cannot execute \"${num_jobs}\" jobs without GNU parallel" fi # shellcheck source=lib/bats-core/semaphore.bash source "${BATS_ROOT}/$BATS_LIBDIR/bats-core/semaphore.bash" bats_semaphore_setup fi # create a file that contains all (filtered) tests to run from all files TESTS_LIST_FILE="${BATS_RUN_TMPDIR}/test_list_file.txt" TEST_ROOT=${1-} TEST_ROOT=${TEST_ROOT%/*} export BATS_RUN_LOGS_DIRECTORY="$TEST_ROOT/.bats/run-logs" if [[ ! -d "$BATS_RUN_LOGS_DIRECTORY" ]]; then if [[ -n "$filter_status" ]]; then printf "Error: --filter-status needs '%s/' to save failed tests. Please create this folder, add it to .gitignore and try again.\n" "$BATS_RUN_LOGS_DIRECTORY" exit 1 else BATS_RUN_LOGS_DIRECTORY= fi # discard via sink instead of having a conditional later export BATS_RUNLOG_FILE='/dev/null' else # use UTC (-u) to avoid problems with TZ changes BATS_RUNLOG_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC') export BATS_RUNLOG_FILE="$BATS_RUN_LOGS_DIRECTORY/${BATS_RUNLOG_DATE}.log" BATS_RUNLOG_DATE if [[ ! -w "$BATS_RUN_LOGS_DIRECTORY" ]]; then printf "WARNING: Cannot write in %s. This run will not write a log!\n" "$BATS_RUN_LOGS_DIRECTORY" >&2 BATS_RUNLOG_FILE='/dev/null' # disable runlog file fi fi # create file even if no tests are found so the `read` below won't fail touch "$TESTS_LIST_FILE" gather_tests_output=$(TESTS_LIST_FILE="$TESTS_LIST_FILE" filter="$filter" filter_status="$filter_status" bats-gather-tests "${gather_tests_flags[@]}" -- "$@") test_count=$(grep -c '' "$TESTS_LIST_FILE" || true) # don't fail here when there are no tests if [[ $gather_tests_output == focus_mode ]]; then focus_mode=1 else focus_mode= fi if [[ -n "$count_only_flag" ]]; then printf '%d\n' "${test_count}" # we don't want to pollute the runlog files, as no tests would have been run [[ $BATS_RUNLOG_FILE != /dev/null ]] && rm "$BATS_RUNLOG_FILE" exit 0 fi if [[ -n "$bats_no_parallelize_across_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then abort "The flag --no-parallelize-across-files requires at least --jobs 2" fi if [[ -n "$bats_no_parallelize_within_files" ]] && [[ ! "$num_jobs" -gt 1 ]]; then abort "The flag --no-parallelize-across-files requires at least --jobs 2" fi # only abort on the lowest levels trap 'BATS_INTERRUPTED=true' INT if [[ -n "$focus_mode" ]]; then printf "WARNING: This test run only contains tests tagged \`bats:focus\`!\n" fi bats_exec_suite_status=0 printf '1..%d\n' "${test_count}" # No point on continuing if there's no tests. if [[ "${test_count}" == 0 ]]; then exit fi export BATS_SUITE_TMPDIR="${BATS_RUN_TMPDIR}/suite" if ! mkdir "$BATS_SUITE_TMPDIR"; then printf '%s\n' "Failed to create BATS_SUITE_TMPDIR" >&2 exit 1 fi # Deduplicate filenames (without reordering) to avoid running duplicate tests n by n times. # (see https://github.com/bats-core/bats-core/issues/329) # If a file was specified multiple times, we already got it repeatedly in our TESTS_LIST_FILE. # Thus, it suffices to bats-exec-file it once to run all repeated tests on it. IFS=$'\n' read -d '' -r -a BATS_UNIQUE_TEST_FILENAMES < <(printf "%s\n" "$@" | nl | sort -k 2 | uniq -f 1 | sort -n | cut -f 2-) || true # shellcheck source=lib/bats-core/tracing.bash source "$BATS_ROOT/$BATS_LIBDIR/bats-core/tracing.bash" bats_setup_tracing trap bats_suite_exit_trap EXIT exec 3<&1 # shellcheck disable=SC2317 bats_suite_exit_trap() { local print_bats_out="${BATS_SHOW_OUTPUT_OF_SUCCEEDING_TESTS}" if [[ -z "${BATS_SETUP_SUITE_COMPLETED}" || -z "${BATS_TEARDOWN_SUITE_COMPLETED}" ]]; then if [[ -z "${BATS_SETUP_SUITE_COMPLETED}" ]]; then printf "not ok 1 setup_suite\n" elif [[ -z "${BATS_TEARDOWN_SUITE_COMPLETED}" ]]; then printf "not ok %d teardown_suite\n" $((test_count + 1)) fi local stack_trace bats_get_failure_stack_trace stack_trace bats_print_stack_trace "${stack_trace[@]}" bats_print_failed_command "${stack_trace[@]}" print_bats_out=1 bats_exec_suite_status=1 fi if [[ -n "$print_bats_out" ]]; then bats_prefix_lines_for_tap_output <"$BATS_OUT" fi if [[ ${BATS_INTERRUPTED-NOTSET} != NOTSET ]]; then printf "\n# Received SIGINT, aborting ...\n\n" fi if [[ -d "$BATS_RUN_LOGS_DIRECTORY" && -n "${BATS_INTERRUPTED:-}" ]]; then # aborting a test run with CTRL+C does not save the runlog file [[ "$BATS_RUNLOG_FILE" != /dev/null ]] && rm "$BATS_RUNLOG_FILE" fi exit "$bats_exec_suite_status" } >&3 bats_run_teardown_suite() { local bats_teardown_suite_status=0 # avoid being called twice, in case this is not called through bats_teardown_suite_trap # but from the end of file trap bats_suite_exit_trap EXIT bats_set_stacktrace_limit BATS_TEARDOWN_SUITE_COMPLETED= teardown_suite >>"$BATS_OUT" 2>&1 || bats_teardown_suite_status=$? if ((bats_teardown_suite_status == 0)); then BATS_TEARDOWN_SUITE_COMPLETED=1 elif [[ -n "${BATS_SETUP_SUITE_COMPLETED:-}" ]]; then BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1 BATS_ERROR_STATUS=$bats_teardown_suite_status return $BATS_ERROR_STATUS fi } # shellcheck disable=SC2317 bats_teardown_suite_trap() { bats_run_teardown_suite bats_suite_exit_trap } teardown_suite() { : } trap bats_teardown_suite_trap EXIT BATS_OUT="$BATS_RUN_TMPDIR/suite.out" if [[ -n "$setup_suite_file" ]]; then setup_suite() { printf "%s does not define \`setup_suite()\`\n" "$setup_suite_file" >&2 return 1 } # shellcheck disable=SC2034 # will be used in the sourced file below BATS_TEST_FILENAME="$setup_suite_file" # shellcheck source=lib/bats-core/test_functions.bash source "$BATS_ROOT/$BATS_LIBDIR/bats-core/test_functions.bash" _bats_test_functions_setup -1 # invalid TEST_NUMBER, as this is not a test # shellcheck disable=SC1090 source "$setup_suite_file" bats_set_stacktrace_limit set -eET export BATS_SETUP_SUITE_COMPLETED= setup_suite >>"$BATS_OUT" 2>&1 BATS_SETUP_SUITE_COMPLETED=1 set +ET else # prevent exit trap from printing an error because of incomplete setup_suite, # when there was none to execute BATS_SETUP_SUITE_COMPLETED=1 fi if [[ "$num_jobs" -gt 1 ]] && [[ -z "$bats_no_parallelize_across_files" ]]; then # run files in parallel to get the maximum pool of parallel tasks # shellcheck disable=SC2086,SC2068 # we need to handle the quoting of ${flags[@]} ourselves, # because parallel can only quote it as one "${parallel_binary_name}" --keep-order --jobs "$num_jobs" -- "bats-exec-file" "$(printf "%q " "${flags[@]}")" "{}" "$TESTS_LIST_FILE" <<< "$(printf "%s\n" "${BATS_UNIQUE_TEST_FILENAMES[@]}")" 2>&1 || bats_exec_suite_status=1 else for filename in "${BATS_UNIQUE_TEST_FILENAMES[@]}"; do if [[ "${BATS_INTERRUPTED-NOTSET}" != NOTSET ]]; then bats_exec_suite_status=130 # bash's code for SIGINT exits break fi bats-exec-file "${flags[@]}" "$filename" "${TESTS_LIST_FILE}" || bats_exec_suite_status=1 done fi set -eET bats_run_teardown_suite if [[ "$focus_mode" == 1 && $bats_exec_suite_status -eq 0 ]]; then if [[ ${BATS_NO_FAIL_FOCUS_RUN-} == 1 ]]; then printf "WARNING: This test run only contains tests tagged \`bats:focus\`!\n" else printf "Marking test run as failed due to \`bats:focus\` tag. (Set \`BATS_NO_FAIL_FOCUS_RUN=1\` to disable.)\n" >&2 bats_exec_suite_status=1 fi fi exit "$bats_exec_suite_status" # the actual exit code will be set by the exit trap using bats_exec_suite_status