Skip to content

Commit 629d5cd

Browse files
committed
lib/bats: Extract lib/bats/helper-function
First part of #156. Though it's still a work in progress, the speedup in the tests with this new behavior added to the functions from `lib/testing/environment` is substantial enough that I'm eager to get it merged sooner than later. The other changes will be more extensive, so this will reduce the amount of noise in the pull requests as well. Also, I finally grokked Bats enough to understand why processing `BATS_PREVIOUS_STACK_TRACE` is unnecessary. I've removed that processing everywhere, and added a thorough comment to `restore_bats_shell_options`.
1 parent 8b82085 commit 629d5cd

File tree

5 files changed

+196
-11
lines changed

5 files changed

+196
-11
lines changed

lib/bats/assertion-test-helpers

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,5 @@ __return_from_expect_assertion() {
311311
unset 'BATS_CURRENT_STACK_TRACE[0]'
312312
set -eET
313313
fi
314-
if [[ "${BATS_PREVIOUS_STACK_TRACE[0]}" =~ $pattern ]]; then
315-
unset 'BATS_PREVIOUS_STACK_TRACE[0]'
316-
set -eET
317-
fi
318314
return "${1:-0}"
319315
}

lib/bats/assertions

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,10 +465,6 @@ return_from_bats_assertion() {
465465
unset 'BATS_CURRENT_STACK_TRACE[0]'
466466
set -eET
467467
fi
468-
if [[ "${BATS_PREVIOUS_STACK_TRACE[0]}" =~ $target_stack_item_pattern ]]; then
469-
unset 'BATS_PREVIOUS_STACK_TRACE[0]'
470-
set -eET
471-
fi
472468
return "$result"
473469
}
474470

lib/bats/helper-function

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#! /usr/bin/env bash
2+
#
3+
# Utilities for writing helper functions for Bats tests
4+
5+
# The first line of every Bats helper function must call `set` with this
6+
# argument. See the function comments for `restore_bats_shell_options`.
7+
export DISABLE_BATS_SHELL_OPTIONS='+eET'
8+
9+
# Restore shell options disabled with `set "$DISABLE_BATS_SHELL_OPTIONS"`.
10+
#
11+
# Ensures a Bats helper function failure points to its call, not its internals.
12+
# This is critical for writing Bats assertion functions.
13+
#
14+
# You must ensure that `set "$DISABLE_BATS_SHELL_OPTIONS"` is in effect prior to
15+
# calling this function, and that your Bats helper function calls this function
16+
# directly through every return path (i.e. you can't delegate the call to
17+
# another helper function). See the comments at the top of `lib/bats/assertions`
18+
# for usage instructions and patterns.
19+
#
20+
# Notice that each public assertion starts with:
21+
#
22+
# set "$DISABLE_BATS_SHELL_OPTIONS"`
23+
#
24+
# and that `DISABLE_BATS_SHELL_OPTIONS` is set to `+eET`. Bats uses `set -e`,
25+
# `set -E`, and `set -T` (in `tests/bats/libexec/bats-exec-test`) to make sure
26+
# failing commands are pinpointed and their stack traces are collected. This is
27+
# almost always the desired behavior.
28+
#
29+
# When it comes to test assertions, however, we want the stack trace to point to
30+
# the assertion call itself, not the line within its implementation at which a
31+
# condition triggered the failure. Otherwise, it produces a bit of mental strain
32+
# when reviewing test failures to identify the location of the failing assertion
33+
# in the test case itself.
34+
#
35+
# Starting an assertion with `set "$DISABLE_BATS_SHELL_OPTIONS"` (i.e. `set
36+
# +eET`) disables the `set -e`, `set -E`, and `set -T` shell options, which
37+
# prevents the functions and commands it calls from updating the Bats stack
38+
# traces. However, by itself, this still leaves the function, file, and line
39+
# number where the assertion was defined in the Bats stack traces. It's also
40+
# important to reinstate `set -eET` upon returning, but we want to make it easy
41+
# to write new assertions composed from existing assertions by reinstating these
42+
# options only when returning from the outermost assertion.
43+
#
44+
# This function solves both aspects of the problem by removing the immediate
45+
# caller from the Bats stack traces and reinstating `set -eET` if it is the
46+
# outermost assertion function, which will be the only one pushed onto the Bats
47+
# stacks prior to calling `set "$DISABLE_BATS_SHELL_OPTIONS"`.
48+
#
49+
# Arguments:
50+
# result: Return value of the calling assertion; defaults to 0
51+
restore_bats_shell_options() {
52+
local result="${1:-0}"
53+
local target_stack_item_pattern=" ${FUNCNAME[1]} ${BASH_SOURCE[1]}$"
54+
55+
# After removing our caller from BATS_CURRENT_STACK_TRACE and restoring the
56+
# Bats shell options, the `return` call at the end of the function will fire
57+
# `bats_debug_trap`, which sets BATS_CURRENT_STACK_TRACE to
58+
# BATS_PREVIOUS_STACK_TRACE.
59+
#
60+
# If `result` is nonzero, `bats_error_trap` will fire and set
61+
# BATS_ERROR_STACK_TRACE to BATS_PREVIOUS_STACK_TRACE and fail the test.
62+
if [[ "${BATS_CURRENT_STACK_TRACE[0]}" =~ $target_stack_item_pattern ]]; then
63+
unset 'BATS_CURRENT_STACK_TRACE[0]'
64+
set -eET
65+
fi
66+
return "$result"
67+
}
68+
export -f restore_bats_shell_options

lib/testing/environment

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# `$_GO_CORE_DIR/lib/bats/helpers`. See the comments for those functions for
1313
# further details.
1414

15+
. "$_GO_CORE_DIR/lib/bats/helper-function"
1516
. "$_GO_CORE_DIR/lib/bats/helpers"
1617

1718
# Avoid having to fold our test strings. Tests that verify folding behavior will
@@ -40,7 +41,9 @@ readonly TEST_GO_PLUGINS_DIR="$TEST_GO_SCRIPTS_DIR/plugins"
4041
# Call this from `teardown` if your tests create any files or directories in
4142
# `TEST_GO_ROOTDIR`.
4243
@go.remove_test_go_rootdir() {
44+
set "$DISABLE_BATS_SHELL_OPTIONS"
4345
remove_bats_test_dirs
46+
restore_bats_shell_options "$?"
4447
}
4548

4649
# Creates an executable `./go` script as `TEST_GO_SCRIPT`
@@ -51,6 +54,7 @@ readonly TEST_GO_PLUGINS_DIR="$TEST_GO_SCRIPTS_DIR/plugins"
5154
# Arguments:
5255
# ...: Lines comprising the `./go` script
5356
@go.create_test_go_script() {
57+
set "$DISABLE_BATS_SHELL_OPTIONS"
5458
create_bats_test_script 'go' \
5559
". '$_GO_CORE_DIR/go-core.bash' '$TEST_GO_SCRIPTS_RELATIVE_DIR'" \
5660
"$@"
@@ -60,6 +64,7 @@ readonly TEST_GO_PLUGINS_DIR="$TEST_GO_SCRIPTS_DIR/plugins"
6064
if [[ ! -d "$TEST_GO_SCRIPTS_DIR" ]]; then
6165
mkdir "$TEST_GO_SCRIPTS_DIR"
6266
fi
67+
restore_bats_shell_options "$?"
6368
}
6469

6570
# Sets `_GO_CMD` to make for neater test output when running `TEST_GO_SCRIPT`
@@ -83,7 +88,9 @@ test-go() {
8388
# script_name: Name of the command script
8489
# ...: Lines comprising the `./go` command script
8590
@go.create_test_command_script() {
91+
set "$DISABLE_BATS_SHELL_OPTIONS"
8692
create_bats_test_script "$TEST_GO_SCRIPTS_RELATIVE_DIR/$1" "${@:2}"
93+
restore_bats_shell_options "$?"
8794
}
8895

8996
# Creates empty "parent" and "child" command scripts in `TEST_GO_SCRIPTS_DIR`
@@ -98,6 +105,7 @@ test-go() {
98105
# parent_name: Name of the "parent" command script
99106
# ...: Names of "child" command scripts
100107
@go.create_parent_and_subcommands() {
108+
set "$DISABLE_BATS_SHELL_OPTIONS"
101109
local parent="$1"
102110
shift
103111
local subcommand
@@ -107,6 +115,7 @@ test-go() {
107115
for subcommand in "$@"; do
108116
@go.create_test_command_script "$parent.d/$subcommand"
109117
done
118+
restore_bats_shell_options "$?"
110119
}
111120

112121
# Assigns the results of `@go.compgen` to a caller-defined test data array
@@ -118,9 +127,8 @@ test-go() {
118127
# result: Name of the caller-declared output array
119128
# ...: Arguments passed directly to `@go.compgen`
120129
@go.test_compgen() {
121-
. "$_GO_CORE_DIR/lib/bats/assertions"
122-
set "$BATS_ASSERTION_DISABLE_SHELL_OPTIONS"
130+
set "$DISABLE_BATS_SHELL_OPTIONS"
123131
. "$_GO_USE_MODULES" 'complete' 'strings'
124132
@go.split $'\n' "$(@go.compgen "${@:2}")" "$1"
125-
return_from_bats_assertion
133+
restore_bats_shell_options "$?"
126134
}

tests/bats-helper-function.bats

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#! /usr/bin/env bats
2+
3+
load environment
4+
5+
TEST_SCRIPT="$BATS_TMPDIR/test-script"
6+
NUM_ERRORS=
7+
8+
setup() {
9+
NUM_ERRORS='0'
10+
}
11+
12+
teardown() {
13+
rm "$TEST_SCRIPT"
14+
}
15+
16+
create_test_script() {
17+
local test_case_name="$1"
18+
local test_case_impl=("${@:2}")
19+
local script=('#! /usr/bin/env bats'
20+
''
21+
". '$_GO_CORE_DIR/lib/bats/helper-function'"
22+
''
23+
'foo() {'
24+
' set "$DISABLE_BATS_SHELL_OPTIONS"'
25+
' restore_bats_shell_options "$1"'
26+
'}'
27+
''
28+
'bar() {'
29+
' set "$DISABLE_BATS_SHELL_OPTIONS"'
30+
' foo "$1"'
31+
' restore_bats_shell_options "$?"'
32+
'}'
33+
''
34+
"@test \"$test_case_name\" {"
35+
"${test_case_impl[@]}"
36+
'}')
37+
printf '%s\n' "${script[@]}" >"$TEST_SCRIPT"
38+
chmod 755 "$TEST_SCRIPT"
39+
}
40+
41+
check_status() {
42+
local expected="$1"
43+
if [[ "$status" -ne "$expected" ]]; then
44+
printf 'Expected status %d, got: %d\n' "$expected" "$status" >&2
45+
((++NUM_ERRORS))
46+
fi
47+
}
48+
49+
check_line() {
50+
local lineno="$1"
51+
local expected="$2"
52+
if [[ "${lines[$lineno]}" != "$expected" ]]; then
53+
printf "line %d doesn't match expected:\n" "$lineno" >&2
54+
printf ' expected: "%s"\n' "$expected" >&2
55+
printf ' actual: "%s"\n' "${lines[$lineno]}" >&2
56+
((++NUM_ERRORS))
57+
fi
58+
}
59+
60+
check_line_matches() {
61+
local lineno="$1"
62+
local pattern="$2"
63+
if [[ ! "${lines[$lineno]}" =~ $pattern ]]; then
64+
printf "line %d doesn't match pattern:\n" "$lineno" >&2
65+
printf ' pattern: "%s"\n' "$pattern" >&2
66+
printf ' value: "%s"\n' "${lines[$lineno]}" >&2
67+
((++NUM_ERRORS))
68+
fi
69+
}
70+
71+
finish() {
72+
if [[ "$NUM_ERRORS" -ne '0' ]]; then
73+
printf 'Total errors: %d\n' "$NUM_ERRORS" >&2
74+
printf 'OUTPUT:\n%s\n' "$output" >&2
75+
return 1
76+
fi
77+
}
78+
79+
@test "$SUITE: helper function passes" {
80+
create_test_script 'foo passes' 'foo'
81+
run "$_GO_BATS_PATH" "$TEST_SCRIPT"
82+
check_status '0'
83+
check_line '0' '1..1'
84+
check_line '1' 'ok 1 foo passes'
85+
finish
86+
}
87+
88+
@test "$SUITE: helper function fails" {
89+
create_test_script 'foo fails' 'foo 1'
90+
run "$_GO_BATS_PATH" "$TEST_SCRIPT"
91+
check_status '1'
92+
check_line 0 '1..1'
93+
check_line 1 'not ok 1 foo fails'
94+
check_line_matches 2 "# \(in test file $TEST_SCRIPT, line [1-9][0-9]*\)" \
95+
check_line 3 "# \`foo 1' failed"
96+
finish
97+
}
98+
99+
@test "$SUITE: nested helper function passes" {
100+
create_test_script 'bar passes' 'bar'
101+
run "$_GO_BATS_PATH" "$TEST_SCRIPT"
102+
check_status '0'
103+
check_line '0' '1..1'
104+
check_line '1' 'ok 1 bar passes'
105+
finish
106+
}
107+
108+
@test "$SUITE: nested helper function fails" {
109+
create_test_script 'bar fails' 'bar 1'
110+
run "$_GO_BATS_PATH" "$TEST_SCRIPT"
111+
check_status '1'
112+
check_line 0 '1..1'
113+
check_line 1 'not ok 1 bar fails'
114+
check_line_matches 2 "# \(in test file $TEST_SCRIPT, line [1-9][0-9]*\)" \
115+
check_line 3 "# \`bar 1' failed"
116+
finish
117+
}

0 commit comments

Comments
 (0)