Skip to content

Commit 57d863c

Browse files
committed
Add batslib_is_caller()
1 parent d0a1318 commit 57d863c

File tree

5 files changed

+232
-0
lines changed

5 files changed

+232
-0
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55

66

7+
## [Unreleased]
8+
9+
### Added
10+
11+
- Restricting invocation to specific locations with
12+
`batslib_is_caller()`
13+
14+
715
## [0.2.0] - 2016-03-22
816

917
### Added
@@ -34,4 +42,5 @@ This project adheres to [Semantic Versioning](http://semver.org/).
3442
`batslib_get_max_single_line_key_width()`
3543

3644

45+
[Unreleased]: https://github.com/ztombol/bats-support/compare/v0.2.0...HEAD
3746
[0.2.0]: https://github.com/ztombol/bats-support/compare/v0.1.0...v0.2.0

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ test helper libraries written for [Bats][bats].
1919
Features:
2020
- [error reporting](#error-reporting)
2121
- [output formatting](#output-formatting)
22+
- [language tools](#language-and-execution)
2223

2324
See the [shared documentation][bats-docs] to learn how to install and
2425
load this library.
@@ -121,6 +122,66 @@ actual (3 lines):
121122
--
122123
```
123124

125+
## Language and Execution
126+
127+
### Restricting invocation to specific locations
128+
129+
Sometimes a helper may work properly only when called from a certain
130+
location. Because it depends on variables to be set or some other side
131+
effect.
132+
133+
A good example is cleaning up temporary files only if the test has
134+
succeeded. The outcome of a test is only available in `teardown`. Thus,
135+
to avoid programming mistakes, it makes sense to restrict such a
136+
clean-up helper to that function.
137+
138+
`batslib_is_caller` checks the call stack and returns `0` if the caller
139+
was invoked from a given function, and `1` otherwise. This function
140+
becomes really useful with the `--indirect` option, which allows calls
141+
through intermediate functions, e.g. the calling function may be called
142+
from a function that was called from the given function.
143+
144+
Staying with the example above, the following code snippet implements a
145+
helper that is restricted to `teardown` or any function called
146+
indirectly from it.
147+
148+
```shell
149+
clean_up() {
150+
# Check caller.
151+
if batslib_is_caller --indirect 'teardown'; then
152+
echo "Must be called from \`teardown'" \
153+
| batslib_decorate 'ERROR: clean_up' \
154+
| fail
155+
return $?
156+
fi
157+
158+
# Body goes here...
159+
}
160+
```
161+
162+
In some cases a helper may be called from multiple locations. For
163+
example, a logging function that uses the test name, description or
164+
number, information only available in `setup`, `@test` or `teardown`, to
165+
distinguish entries. The following snippet implements this restriction.
166+
167+
```shell
168+
log_test() {
169+
# Check caller.
170+
if ! ( batslib_is_caller --indirect 'setup' \
171+
|| batslib_is_caller --indirect "$BATS_TEST_NAME" \
172+
|| batslib_is_caller --indirect 'teardown' )
173+
then
174+
echo "Must be called from \`setup', \`@test' or \`teardown'" \
175+
| batslib_decorate 'ERROR: log_test' \
176+
| fail
177+
return $?
178+
fi
179+
180+
# Body goes here...
181+
}
182+
```
183+
184+
124185
<!-- REFERENCES -->
125186

126187
[bats]: https://github.com/sstephenson/bats

load.bash

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
source "$(dirname "${BASH_SOURCE[0]}")/src/output.bash"
22
source "$(dirname "${BASH_SOURCE[0]}")/src/error.bash"
3+
source "$(dirname "${BASH_SOURCE[0]}")/src/lang.bash"

src/lang.bash

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#
2+
# bats-util - Various auxiliary functions for Bats
3+
#
4+
# Written in 2016 by Zoltan Tombol <zoltan dot tombol at gmail dot com>
5+
#
6+
# To the extent possible under law, the author(s) have dedicated all
7+
# copyright and related and neighboring rights to this software to the
8+
# public domain worldwide. This software is distributed without any
9+
# warranty.
10+
#
11+
# You should have received a copy of the CC0 Public Domain Dedication
12+
# along with this software. If not, see
13+
# <http://creativecommons.org/publicdomain/zero/1.0/>.
14+
#
15+
16+
#
17+
# lang.bash
18+
# ---------
19+
#
20+
# Bash language and execution related functions. Used by public helper
21+
# functions.
22+
#
23+
24+
# Check whether the calling function was called from a given function.
25+
#
26+
# By default, direct invocation is checked. The function succeeds if the
27+
# calling function was called directly from the given function. In other
28+
# words, if the given function is the next element on the call stack.
29+
#
30+
# When `--indirect' is specified, indirect invocation is checked. The
31+
# function succeeds if the calling function was called from the given
32+
# function with any number of intermediate calls. In other words, if the
33+
# given function can be found somewhere on the call stack.
34+
#
35+
# Direct invocation is a form of indirect invocation with zero
36+
# intermediate calls.
37+
#
38+
# Globals:
39+
# FUNCNAME
40+
# Options:
41+
# -i, --indirect - check indirect invocation
42+
# Arguments:
43+
# $1 - calling function's name
44+
# Returns:
45+
# 0 - current function was called from the given function
46+
# 1 - otherwise
47+
batslib_is_caller() {
48+
local -i is_mode_direct=1
49+
50+
# Handle options.
51+
while (( $# > 0 )); do
52+
case "$1" in
53+
-i|--indirect) is_mode_direct=0; shift ;;
54+
--) shift; break ;;
55+
*) break ;;
56+
esac
57+
done
58+
59+
# Arguments.
60+
local -r func="$1"
61+
62+
# Check call stack.
63+
if (( is_mode_direct )); then
64+
[[ $func == "${FUNCNAME[2]}" ]] && return 0
65+
else
66+
local -i depth
67+
for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do
68+
[[ $func == "${FUNCNAME[$depth]}" ]] && return 0
69+
done
70+
fi
71+
72+
return 1
73+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bats
2+
3+
load 'test_helper'
4+
5+
6+
# Test functions
7+
test_func_lvl_2() {
8+
test_func_lvl_1 "$@"
9+
}
10+
11+
test_func_lvl_1() {
12+
test_func_lvl_0 "$@"
13+
}
14+
15+
test_func_lvl_0() {
16+
batslib_is_caller "$@"
17+
}
18+
19+
20+
#
21+
# Direct invocation
22+
#
23+
24+
# Interface
25+
@test 'batslib_is_caller() <function>: returns 0 if the current function was called directly from <function>' {
26+
run test_func_lvl_1 test_func_lvl_1
27+
[ "$status" -eq 0 ]
28+
[ "${#lines[@]}" -eq 0 ]
29+
}
30+
31+
@test 'batslib_is_caller() <function>: returns 1 if the current function was not called directly from <function>' {
32+
run test_func_lvl_0 test_func_lvl_1
33+
[ "$status" -eq 1 ]
34+
[ "${#lines[@]}" -eq 0 ]
35+
}
36+
37+
# Correctness
38+
@test 'batslib_is_caller() <function>: the current function does not appear on the call stack' {
39+
run test_func_lvl_0 test_func_lvl_0
40+
[ "$status" -eq 1 ]
41+
[ "${#lines[@]}" -eq 0 ]
42+
}
43+
44+
45+
#
46+
# Indirect invocation
47+
#
48+
49+
# Options
50+
test_i_indirect() {
51+
run test_func_lvl_2 "$@"
52+
[ "$status" -eq 0 ]
53+
[ "${#lines[@]}" -eq 0 ]
54+
}
55+
56+
@test 'batslib_is_caller() -i <function>: enables indirect checking' {
57+
test_i_indirect -i test_func_lvl_2
58+
}
59+
60+
@test 'batslib_is_caller() --indirect <function>: enables indirect checking' {
61+
test_i_indirect --indirect test_func_lvl_2
62+
}
63+
64+
# Interface
65+
@test 'batslib_is_caller() --indirect <function>: returns 0 if the current function was called indirectly from <function>' {
66+
run test_func_lvl_2 --indirect test_func_lvl_2
67+
[ "$status" -eq 0 ]
68+
[ "${#lines[@]}" -eq 0 ]
69+
}
70+
71+
@test 'batslib_is_caller() --indirect <function>: returns 1 if the current function was not called indirectly from <function>' {
72+
run test_func_lvl_1 --indirect test_func_lvl_2
73+
[ "$status" -eq 1 ]
74+
[ "${#lines[@]}" -eq 0 ]
75+
}
76+
77+
# Correctness
78+
@test 'batslib_is_caller() --indirect <function>: direct invocation is a special case of indirect invocation with zero intermediate calls' {
79+
run test_func_lvl_1 --indirect test_func_lvl_1
80+
[ "$status" -eq 0 ]
81+
[ "${#lines[@]}" -eq 0 ]
82+
}
83+
84+
@test 'batslib_is_caller() --indirect <function>: the current function does not appear on the call stack' {
85+
run test_func_lvl_0 --indirect test_func_lvl_0
86+
[ "$status" -eq 1 ]
87+
[ "${#lines[@]}" -eq 0 ]
88+
}

0 commit comments

Comments
 (0)