1#!/usr/bin/env bash 2 3bats_capture_stack_trace() { 4 local test_file 5 local funcname 6 local i 7 8 # The last entry in the stack trace is not useful when en error occured: 9 # It is either duplicated (kinda correct) or has wrong line number (Bash < 4.4) 10 # Therefore we capture the stacktrace but use it only after the next debug 11 # trap fired. 12 # Expansion is required for empty arrays which otherwise error 13 BATS_CURRENT_STACK_TRACE=("${BATS_STACK_TRACE[@]+"${BATS_STACK_TRACE[@]}"}") 14 15 BATS_STACK_TRACE=() 16 17 for ((i = 2; i != ${#FUNCNAME[@]}; ++i)); do 18 # Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby 19 # calling an exported function erases the test file's BASH_SOURCE entry. 20 test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}" 21 funcname="${FUNCNAME[$i]}" 22 BATS_STACK_TRACE+=("${BASH_LINENO[$((i - 1))]} $funcname $test_file") 23 case "$funcname" in 24 "$BATS_TEST_NAME" | setup | teardown | setup_file | teardown_file) 25 break 26 ;; 27 esac 28 if [[ "${BASH_SOURCE[$i + 1]:-}" == *"bats-exec-file" ]] && [[ "$funcname" == 'source' ]]; then 29 break 30 fi 31 done 32} 33 34bats_print_stack_trace() { 35 local frame 36 local index=1 37 local count="${#@}" 38 local filename 39 local lineno 40 41 for frame in "$@"; do 42 bats_frame_filename "$frame" 'filename' 43 bats_trim_filename "$filename" 'filename' 44 bats_frame_lineno "$frame" 'lineno' 45 46 if [[ $index -eq 1 ]]; then 47 printf '# (' 48 else 49 printf '# ' 50 fi 51 52 local fn 53 bats_frame_function "$frame" 'fn' 54 if [[ "$fn" != "$BATS_TEST_NAME" ]] && 55 # don't print "from function `source'"", 56 # when failing in free code during `source $test_file` from bats-exec-file 57 ! [[ "$fn" == 'source' && $index -eq $count ]]; then 58 printf "from function \`%s' " "$fn" 59 fi 60 61 if [[ $index -eq $count ]]; then 62 printf 'in test file %s, line %d)\n' "$filename" "$lineno" 63 else 64 printf 'in file %s, line %d,\n' "$filename" "$lineno" 65 fi 66 67 ((++index)) 68 done 69} 70 71bats_print_failed_command() { 72 if [[ ${#BATS_STACK_TRACE[@]} -eq 0 ]]; then 73 return 74 fi 75 local frame="${BATS_STACK_TRACE[${#BATS_STACK_TRACE[@]} - 1]}" 76 local filename 77 local lineno 78 local failed_line 79 local failed_command 80 81 bats_frame_filename "$frame" 'filename' 82 bats_frame_lineno "$frame" 'lineno' 83 bats_extract_line "$filename" "$lineno" 'failed_line' 84 bats_strip_string "$failed_line" 'failed_command' 85 printf '%s' "# \`${failed_command}' " 86 87 if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then 88 printf 'failed%s\n' "$BATS_ERROR_SUFFIX" 89 else 90 printf 'failed with status %d%s\n' "$BATS_ERROR_STATUS" "$BATS_ERROR_SUFFIX" 91 fi 92} 93 94bats_frame_lineno() { 95 printf -v "$2" '%s' "${1%% *}" 96} 97 98bats_frame_function() { 99 local __bff_function="${1#* }" 100 printf -v "$2" '%s' "${__bff_function%% *}" 101} 102 103bats_frame_filename() { 104 local __bff_filename="${1#* }" 105 __bff_filename="${__bff_filename#* }" 106 107 if [[ "$__bff_filename" == "$BATS_TEST_SOURCE" ]]; then 108 __bff_filename="$BATS_TEST_FILENAME" 109 fi 110 printf -v "$2" '%s' "$__bff_filename" 111} 112 113bats_extract_line() { 114 local __bats_extract_line_line 115 local __bats_extract_line_index=0 116 117 while IFS= read -r __bats_extract_line_line; do 118 if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then 119 printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}" 120 break 121 fi 122 done <"$1" 123} 124 125bats_strip_string() { 126 [[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]] 127 printf -v "$2" '%s' "${BASH_REMATCH[1]}" 128} 129 130bats_trim_filename() { 131 printf -v "$2" '%s' "${1#$BATS_CWD/}" 132} 133 134# normalize a windows path from e.g. C:/directory to /c/directory 135# The path must point to an existing/accessable directory, not a file! 136bats_normalize_windows_dir_path() { # <output-var> <path> 137 local output_var="$1" 138 local path="$2" 139 if [[ $path == ?:* ]]; then 140 NORMALIZED_INPUT="$(cd "$path" || exit 1; pwd)" 141 else 142 NORMALIZED_INPUT="$path" 143 fi 144 printf -v "$output_var" "%s" "$NORMALIZED_INPUT" 145} 146 147bats_emit_trace() { 148 if [[ $BATS_TRACE_LEVEL -gt 0 ]]; then 149 local line=${BASH_LINENO[1]} 150 # shellcheck disable=SC2016 151 if [[ $BASH_COMMAND != '"$BATS_TEST_NAME" >> "$BATS_OUT" 2>&1 4>&1' && $BASH_COMMAND != "bats_test_begin "* ]] && # don't emit these internal calls 152 [[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || $line != "$BATS_LAST_BASH_LINENO" ]] && 153 # avoid printing a function twice (at call site and at definiton site) 154 [[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || ${BASH_LINENO[2]} != "$BATS_LAST_BASH_LINENO" || ${BASH_SOURCE[3]} != "$BATS_LAST_BASH_SOURCE" ]]; then 155 local file="${BASH_SOURCE[2]}" # index 2: skip over bats_emit_trace and bats_debug_trap 156 if [[ $file == "${BATS_TEST_SOURCE}" ]]; then 157 file="$BATS_TEST_FILENAME" 158 fi 159 local padding='$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' 160 if (( BATS_LAST_STACK_DEPTH != ${#BASH_LINENO[@]} )); then 161 printf '%s [%s:%d]\n' "${padding::${#BASH_LINENO[@]}-4}" "${file##*/}" "$line" >&4 162 fi 163 printf '%s %s\n' "${padding::${#BASH_LINENO[@]}-4}" "$BASH_COMMAND" >&4 164 BATS_LAST_BASH_COMMAND="$BASH_COMMAND" 165 BATS_LAST_BASH_LINENO="$line" 166 BATS_LAST_BASH_SOURCE="${BASH_SOURCE[2]}" 167 BATS_LAST_STACK_DEPTH="${#BASH_LINENO[@]}" 168 fi 169 fi 170} 171 172bats_debug_trap() { 173 # on windows we sometimes get a mix of paths (when install via nmp install -g) 174 # which have C:/... or /c/... comparing them is going to be problematic. 175 # We need to normalize them to a common format! 176 local NORMALIZED_INPUT 177 bats_normalize_windows_dir_path NORMALIZED_INPUT "${1%/*}" 178 local file_excluded='' path 179 for path in "${_BATS_DEBUG_EXCLUDE_PATHS[@]}"; do 180 if [[ "$NORMALIZED_INPUT" == "$path"* ]]; then 181 file_excluded=1 182 break 183 fi 184 done 185 186 # don't update the trace within library functions or we get backtraces from inside traps 187 # also don't record new stack traces while handling interruptions, to avoid overriding the interrupted command 188 if [[ -z "$file_excluded" && "${BATS_INTERRUPTED-NOTSET}" == NOTSET ]]; then 189 bats_capture_stack_trace 190 bats_emit_trace 191 fi 192} 193 194# For some versions of Bash, the `ERR` trap may not always fire for every 195# command failure, but the `EXIT` trap will. Also, some command failures may not 196# set `$?` properly. See #72 and #81 for details. 197# 198# For this reason, we call `bats_error_trap` at the very beginning of 199# `bats_teardown_trap` (the `DEBUG` trap for the call will fix the stack trace) 200# and check the value of `$BATS_TEST_COMPLETED` before taking other actions. 201# We also adjust the exit status value if needed. 202# 203# See `bats_exit_trap` for an additional EXIT error handling case when `$?` 204# isn't set properly during `teardown()` errors. 205bats_error_trap() { 206 local status="$?" 207 if [[ -z "$BATS_TEST_COMPLETED" ]]; then 208 BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}" 209 if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then 210 BATS_ERROR_STATUS=1 211 fi 212 BATS_STACK_TRACE=("${BATS_CURRENT_STACK_TRACE[@]}") 213 trap - DEBUG 214 fi 215} 216 217bats_add_debug_exclude_path() { # <path> 218 if [[ -z "$1" ]]; then # don't exclude everything 219 printf "bats_add_debug_exclude_path: Exclude path must not be empty!\n" >&2 220 return 1 221 fi 222 if [[ "$OSTYPE" == cygwin || "$OSTYPE" == msys ]]; then 223 bats_normalize_windows_dir_path normalized_dir "$1" 224 _BATS_DEBUG_EXCLUDE_PATHS+=("$normalized_dir") 225 else 226 _BATS_DEBUG_EXCLUDE_PATHS+=("$1") 227 fi 228} 229 230bats_setup_tracing() { 231 _BATS_DEBUG_EXCLUDE_PATHS=() 232 # exclude some paths by default 233 bats_add_debug_exclude_path "$BATS_ROOT/lib/" 234 bats_add_debug_exclude_path "$BATS_ROOT/libexec/" 235 236 237 exec 4<&1 # used for tracing 238 if [[ "${BATS_TRACE_LEVEL:-0}" -gt 0 ]]; then 239 # avoid undefined variable errors 240 BATS_LAST_BASH_COMMAND= 241 BATS_LAST_BASH_LINENO= 242 BATS_LAST_BASH_SOURCE= 243 BATS_LAST_STACK_DEPTH= 244 # try to exclude helper libraries if found, this is only relevant for tracing 245 while read -r path; do 246 bats_add_debug_exclude_path "$path" 247 done < <(find "$PWD" -type d -name bats-assert -o -name bats-support) 248 fi 249 250 # exclude user defined libraries 251 IFS=':' read -r exclude_paths <<< "${BATS_DEBUG_EXCLUDE_PATHS:-}" 252 for path in "${exclude_paths[@]}"; do 253 if [[ -n "$path" ]]; then 254 bats_add_debug_exclude_path "$path" 255 fi 256 done 257 258 # turn on traps after setting excludedes to avoid tracing the exclude setup 259 trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG 260 trap 'bats_error_trap' ERR 261}