Skip to content

Commit 0391bc9

Browse files
authored
Merge pull request #159 from mbland/bats-function
lib/bats: Extract `lib/bats/helper-function`
2 parents 8b82085 + 629d5cd commit 0391bc9

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)