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}