-
Notifications
You must be signed in to change notification settings - Fork 410
/
tracing.bash
386 lines (348 loc) · 13 KB
/
tracing.bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#!/usr/bin/env bash
# shellcheck source=lib/bats-core/common.bash
source "$BATS_ROOT/lib/bats-core/common.bash"
bats_capture_stack_trace() {
local test_file
local funcname
local i
BATS_DEBUG_LAST_STACK_TRACE=()
for ((i = 2; i != ${#FUNCNAME[@]}; ++i)); do
# Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby
# calling an exported function erases the test file's BASH_SOURCE entry.
test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}"
funcname="${FUNCNAME[$i]}"
BATS_DEBUG_LAST_STACK_TRACE+=("${BASH_LINENO[$((i-1))]} $funcname $test_file")
case "$funcname" in
"$BATS_TEST_NAME" | setup | teardown | setup_file | teardown_file | setup_suite | teardown_suite)
break
;;
esac
if [[ "${BASH_SOURCE[$i + 1]:-}" == *"bats-exec-file" ]] && [[ "$funcname" == 'source' ]]; then
break
fi
done
}
bats_get_failure_stack_trace() {
local stack_trace_var
# See bats_debug_trap for details.
if [[ -n "${BATS_DEBUG_LAST_STACK_TRACE_IS_VALID:-}" ]]; then
stack_trace_var=BATS_DEBUG_LAST_STACK_TRACE
else
stack_trace_var=BATS_DEBUG_LASTLAST_STACK_TRACE
fi
# shellcheck disable=SC2016
eval "$(printf \
'%s=(${%s[@]+"${%s[@]}"})' \
"${1}" \
"${stack_trace_var}" \
"${stack_trace_var}")"
}
bats_print_stack_trace() {
local frame
local index=1
local count="${#@}"
local filename
local lineno
for frame in "$@"; do
bats_frame_filename "$frame" 'filename'
bats_trim_filename "$filename" 'filename'
bats_frame_lineno "$frame" 'lineno'
printf '%s' "${BATS_STACK_TRACE_PREFIX-# }"
if [[ $index -eq 1 ]]; then
printf '('
else
printf ' '
fi
local fn
bats_frame_function "$frame" 'fn'
if [[ "$fn" != "$BATS_TEST_NAME" ]] &&
# don't print "from function `source'"",
# when failing in free code during `source $test_file` from bats-exec-file
! [[ "$fn" == 'source' && $index -eq $count ]]; then
local quoted_fn
bats_quote_code quoted_fn "$fn"
printf "from function %s " "$quoted_fn"
fi
if [[ $index -eq $count ]]; then
printf 'in test file %s, line %d)\n' "$filename" "$lineno"
else
printf 'in file %s, line %d,\n' "$filename" "$lineno"
fi
((++index))
done
}
bats_print_failed_command() {
local stack_trace=("${@}")
if [[ ${#stack_trace[@]} -eq 0 ]]; then
return
fi
local frame="${stack_trace[${#stack_trace[@]} - 1]}"
local filename
local lineno
local failed_line
local failed_command
bats_frame_filename "$frame" 'filename'
bats_frame_lineno "$frame" 'lineno'
bats_extract_line "$filename" "$lineno" 'failed_line'
bats_strip_string "$failed_line" 'failed_command'
local quoted_failed_command
bats_quote_code quoted_failed_command "$failed_command"
printf '# %s ' "${quoted_failed_command}"
if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then
printf 'failed%s\n' "$BATS_ERROR_SUFFIX"
else
printf 'failed with status %d%s\n' "$BATS_ERROR_STATUS" "$BATS_ERROR_SUFFIX"
fi
}
bats_frame_lineno() {
printf -v "$2" '%s' "${1%% *}"
}
bats_frame_function() {
local __bff_function="${1#* }"
printf -v "$2" '%s' "${__bff_function%% *}"
}
bats_frame_filename() {
local __bff_filename="${1#* }"
__bff_filename="${__bff_filename#* }"
if [[ "$__bff_filename" == "$BATS_TEST_SOURCE" ]]; then
__bff_filename="$BATS_TEST_FILENAME"
fi
printf -v "$2" '%s' "$__bff_filename"
}
bats_extract_line() {
local __bats_extract_line_line
local __bats_extract_line_index=0
while IFS= read -r __bats_extract_line_line; do
if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then
printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}"
break
fi
done <"$1"
}
bats_strip_string() {
[[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]
printf -v "$2" '%s' "${BASH_REMATCH[1]}"
}
bats_trim_filename() {
printf -v "$2" '%s' "${1#"$BATS_CWD"/}"
}
# normalize a windows path from e.g. C:/directory to /c/directory
# The path must point to an existing/accessable directory, not a file!
bats_normalize_windows_dir_path() { # <output-var> <path>
local output_var="$1" path="$2"
if [[ "$output_var" != NORMALIZED_INPUT ]]; then
local NORMALIZED_INPUT
fi
if [[ $path == ?:* ]]; then
NORMALIZED_INPUT="$(cd "$path" || exit 1; pwd)"
else
NORMALIZED_INPUT="$path"
fi
printf -v "$output_var" "%s" "$NORMALIZED_INPUT"
}
bats_emit_trace() {
if [[ $BATS_TRACE_LEVEL -gt 0 ]]; then
local line=${BASH_LINENO[1]}
# shellcheck disable=SC2016
if [[ $BASH_COMMAND != '"$BATS_TEST_NAME" >> "$BATS_OUT" 2>&1 4>&1' && $BASH_COMMAND != "bats_test_begin "* ]] && # don't emit these internal calls
[[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || $line != "$BATS_LAST_BASH_LINENO" ]] &&
# avoid printing a function twice (at call site and at definition site)
[[ $BASH_COMMAND != "$BATS_LAST_BASH_COMMAND" || ${BASH_LINENO[2]} != "$BATS_LAST_BASH_LINENO" || ${BASH_SOURCE[3]} != "$BATS_LAST_BASH_SOURCE" ]]; then
local file="${BASH_SOURCE[2]}" # index 2: skip over bats_emit_trace and bats_debug_trap
if [[ $file == "${BATS_TEST_SOURCE}" ]]; then
file="$BATS_TEST_FILENAME"
fi
local padding='$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$'
if (( BATS_LAST_STACK_DEPTH != ${#BASH_LINENO[@]} )); then
printf '%s [%s:%d]\n' "${padding::${#BASH_LINENO[@]}-4}" "${file##*/}" "$line" >&4
fi
printf '%s %s\n' "${padding::${#BASH_LINENO[@]}-4}" "$BASH_COMMAND" >&4
BATS_LAST_BASH_COMMAND="$BASH_COMMAND"
BATS_LAST_BASH_LINENO="$line"
BATS_LAST_BASH_SOURCE="${BASH_SOURCE[2]}"
BATS_LAST_STACK_DEPTH="${#BASH_LINENO[@]}"
fi
fi
}
# bats_debug_trap tracks the last line of code executed within a test. This is
# necessary because $BASH_LINENO is often incorrect inside of ERR and EXIT
# trap handlers.
#
# Below are tables describing different command failure scenarios and the
# reliability of $BASH_LINENO within different the executed DEBUG, ERR, and EXIT
# trap handlers. Naturally, the behaviors change between versions of Bash.
#
# Table rows should be read left to right. For example, on bash version
# 4.0.44(2)-release, if a test executes `false` (or any other failing external
# command), bash will do the following in order:
# 1. Call the DEBUG trap handler (bats_debug_trap) with $BASH_LINENO referring
# to the source line containing the `false` command, then
# 2. Call the DEBUG trap handler again, but with an incorrect $BASH_LINENO, then
# 3. Call the ERR trap handler, but with a (possibly-different) incorrect
# $BASH_LINENO, then
# 4. Call the DEBUG trap handler again, but with $BASH_LINENO set to 1, then
# 5. Call the EXIT trap handler, with $BASH_LINENO set to 1.
#
# bash version 4.4.20(1)-release
# command | first DEBUG | second DEBUG | ERR | third DEBUG | EXIT
# -------------+-------------+--------------+---------+-------------+--------
# false | OK | OK | OK | BAD[1] | BAD[1]
# [[ 1 = 2 ]] | OK | BAD[2] | BAD[2] | BAD[1] | BAD[1]
# (( 1 = 2 )) | OK | BAD[2] | BAD[2] | BAD[1] | BAD[1]
# ! true | OK | --- | BAD[4] | --- | BAD[1]
# $var_dne | OK | --- | --- | BAD[1] | BAD[1]
# source /dne | OK | --- | --- | BAD[1] | BAD[1]
#
# bash version 4.0.44(2)-release
# command | first DEBUG | second DEBUG | ERR | third DEBUG | EXIT
# -------------+-------------+--------------+---------+-------------+--------
# false | OK | BAD[3] | BAD[3] | BAD[1] | BAD[1]
# [[ 1 = 2 ]] | OK | --- | BAD[3] | --- | BAD[1]
# (( 1 = 2 )) | OK | --- | BAD[3] | --- | BAD[1]
# ! true | OK | --- | BAD[3] | --- | BAD[1]
# $var_dne | OK | --- | --- | BAD[1] | BAD[1]
# source /dne | OK | --- | --- | BAD[1] | BAD[1]
#
# [1] The reported line number is always 1.
# [2] The reported source location is that of the beginning of the function
# calling the command.
# [3] The reported line is that of the last command executed in the DEBUG trap
# handler.
# [4] The reported source location is that of the call to the function calling
# the command.
bats_debug_trap() {
# on windows we sometimes get a mix of paths (when install via nmp install -g)
# which have C:/... or /c/... comparing them is going to be problematic.
# We need to normalize them to a common format!
local NORMALIZED_INPUT
bats_normalize_windows_dir_path NORMALIZED_INPUT "${1%/*}"
local file_excluded='' path
for path in "${BATS_DEBUG_EXCLUDE_PATHS[@]}"; do
if [[ "$NORMALIZED_INPUT" == "$path"* ]]; then
file_excluded=1
break
fi
done
# don't update the trace within library functions or we get backtraces from inside traps
# also don't record new stack traces while handling interruptions, to avoid overriding the interrupted command
if [[ -z "$file_excluded" && "${BATS_INTERRUPTED-NOTSET}" == NOTSET ]]; then
BATS_DEBUG_LASTLAST_STACK_TRACE=(
${BATS_DEBUG_LAST_STACK_TRACE[@]+"${BATS_DEBUG_LAST_STACK_TRACE[@]}"}
)
BATS_DEBUG_LAST_LINENO=(${BASH_LINENO[@]+"${BASH_LINENO[@]}"})
BATS_DEBUG_LAST_SOURCE=(${BASH_SOURCE[@]+"${BASH_SOURCE[@]}"})
bats_capture_stack_trace
bats_emit_trace
fi
}
# For some versions of Bash, the `ERR` trap may not always fire for every
# command failure, but the `EXIT` trap will. Also, some command failures may not
# set `$?` properly. See #72 and #81 for details.
#
# For this reason, we call `bats_check_status_from_trap` at the very beginning
# of `bats_teardown_trap` and check the value of `$BATS_TEST_COMPLETED` before
# taking other actions. We also adjust the exit status value if needed.
#
# See `bats_exit_trap` for an additional EXIT error handling case when `$?`
# isn't set properly during `teardown()` errors.
bats_check_status_from_trap() {
local status="$?"
if [[ -z "${BATS_TEST_COMPLETED:-}" ]]; then
BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}"
if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then
BATS_ERROR_STATUS=1
fi
trap - DEBUG
fi
}
bats_add_debug_exclude_path() { # <path>
if [[ -z "$1" ]]; then # don't exclude everything
printf "bats_add_debug_exclude_path: Exclude path must not be empty!\n" >&2
return 1
fi
if [[ "$OSTYPE" == cygwin || "$OSTYPE" == msys ]]; then
local normalized_dir
bats_normalize_windows_dir_path normalized_dir "$1"
BATS_DEBUG_EXCLUDE_PATHS+=("$normalized_dir")
else
BATS_DEBUG_EXCLUDE_PATHS+=("$1")
fi
}
bats_setup_tracing() {
# Variables for capturing accurate stack traces. See bats_debug_trap for
# details.
#
# BATS_DEBUG_LAST_LINENO, BATS_DEBUG_LAST_SOURCE, and
# BATS_DEBUG_LAST_STACK_TRACE hold data from the most recent call to
# bats_debug_trap.
#
# BATS_DEBUG_LASTLAST_STACK_TRACE holds data from two bats_debug_trap calls
# ago.
#
# BATS_DEBUG_LAST_STACK_TRACE_IS_VALID indicates that
# BATS_DEBUG_LAST_STACK_TRACE contains the stack trace of the test's error. If
# unset, BATS_DEBUG_LAST_STACK_TRACE is unreliable and
# BATS_DEBUG_LASTLAST_STACK_TRACE should be used instead.
BATS_DEBUG_LASTLAST_STACK_TRACE=()
BATS_DEBUG_LAST_LINENO=()
BATS_DEBUG_LAST_SOURCE=()
BATS_DEBUG_LAST_STACK_TRACE=()
BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=
BATS_ERROR_SUFFIX=
BATS_DEBUG_EXCLUDE_PATHS=()
# exclude some paths by default
bats_add_debug_exclude_path "$BATS_ROOT/lib/"
bats_add_debug_exclude_path "$BATS_ROOT/libexec/"
exec 4<&1 # used for tracing
if [[ "${BATS_TRACE_LEVEL:-0}" -gt 0 ]]; then
# avoid undefined variable errors
BATS_LAST_BASH_COMMAND=
BATS_LAST_BASH_LINENO=
BATS_LAST_BASH_SOURCE=
BATS_LAST_STACK_DEPTH=
# try to exclude helper libraries if found, this is only relevant for tracing
while read -r path; do
bats_add_debug_exclude_path "$path"
done < <(find "$PWD" -type d -name bats-assert -o -name bats-support)
fi
local exclude_paths path
# exclude user defined libraries
IFS=':' read -r exclude_paths <<< "${BATS_DEBUG_EXCLUDE_PATHS:-}"
for path in "${exclude_paths[@]}"; do
if [[ -n "$path" ]]; then
bats_add_debug_exclude_path "$path"
fi
done
# turn on traps after setting excludes to avoid tracing the exclude setup
trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG
trap 'bats_error_trap' ERR
}
bats_error_trap() {
bats_check_status_from_trap
# If necessary, undo the most recent stack trace captured by bats_debug_trap.
# See bats_debug_trap for details.
if [[ "${BASH_LINENO[*]}" = "${BATS_DEBUG_LAST_LINENO[*]:-}"
&& "${BASH_SOURCE[*]}" = "${BATS_DEBUG_LAST_SOURCE[*]:-}"
&& -z "$BATS_DEBUG_LAST_STACK_TRACE_IS_VALID" ]]; then
BATS_DEBUG_LAST_STACK_TRACE=(
${BATS_DEBUG_LASTLAST_STACK_TRACE[@]+"${BATS_DEBUG_LASTLAST_STACK_TRACE[@]}"}
)
fi
BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=1
}
bats_interrupt_trap() {
# mark the interruption, to handle during exit
BATS_INTERRUPTED=true
BATS_ERROR_STATUS=130
# debug trap fires before interrupt trap but gets wrong linenumber (line 1)
# -> use last stack trace
exit $BATS_ERROR_STATUS
}
# this is used inside run()
bats_interrupt_trap_in_run() {
# mark the interruption, to handle during exit
BATS_INTERRUPTED=true
BATS_ERROR_STATUS=130
BATS_DEBUG_LAST_STACK_TRACE_IS_VALID=true
exit $BATS_ERROR_STATUS
}