Skip to content

Commit

Permalink
Add batslib_is_caller()
Browse files Browse the repository at this point in the history
  • Loading branch information
ztombol committed Nov 21, 2016
1 parent d0a1318 commit 57d863c
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 0 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).


## [Unreleased]

### Added

- Restricting invocation to specific locations with
`batslib_is_caller()`


## [0.2.0] - 2016-03-22

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


[Unreleased]: https://github.com/ztombol/bats-support/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/ztombol/bats-support/compare/v0.1.0...v0.2.0
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test helper libraries written for [Bats][bats].
Features:
- [error reporting](#error-reporting)
- [output formatting](#output-formatting)
- [language tools](#language-and-execution)

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

## Language and Execution

### Restricting invocation to specific locations

Sometimes a helper may work properly only when called from a certain
location. Because it depends on variables to be set or some other side
effect.

A good example is cleaning up temporary files only if the test has
succeeded. The outcome of a test is only available in `teardown`. Thus,
to avoid programming mistakes, it makes sense to restrict such a
clean-up helper to that function.

`batslib_is_caller` checks the call stack and returns `0` if the caller
was invoked from a given function, and `1` otherwise. This function
becomes really useful with the `--indirect` option, which allows calls
through intermediate functions, e.g. the calling function may be called
from a function that was called from the given function.

Staying with the example above, the following code snippet implements a
helper that is restricted to `teardown` or any function called
indirectly from it.

```shell
clean_up() {
# Check caller.
if batslib_is_caller --indirect 'teardown'; then
echo "Must be called from \`teardown'" \
| batslib_decorate 'ERROR: clean_up' \
| fail
return $?
fi

# Body goes here...
}
```

In some cases a helper may be called from multiple locations. For
example, a logging function that uses the test name, description or
number, information only available in `setup`, `@test` or `teardown`, to
distinguish entries. The following snippet implements this restriction.

```shell
log_test() {
# Check caller.
if ! ( batslib_is_caller --indirect 'setup' \
|| batslib_is_caller --indirect "$BATS_TEST_NAME" \
|| batslib_is_caller --indirect 'teardown' )
then
echo "Must be called from \`setup', \`@test' or \`teardown'" \
| batslib_decorate 'ERROR: log_test' \
| fail
return $?
fi

# Body goes here...
}
```


<!-- REFERENCES -->

[bats]: https://github.com/sstephenson/bats
Expand Down
1 change: 1 addition & 0 deletions load.bash
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
source "$(dirname "${BASH_SOURCE[0]}")/src/output.bash"
source "$(dirname "${BASH_SOURCE[0]}")/src/error.bash"
source "$(dirname "${BASH_SOURCE[0]}")/src/lang.bash"
73 changes: 73 additions & 0 deletions src/lang.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#
# bats-util - Various auxiliary functions for Bats
#
# Written in 2016 by Zoltan Tombol <zoltan dot tombol at gmail dot com>
#
# To the extent possible under law, the author(s) have dedicated all
# copyright and related and neighboring rights to this software to the
# public domain worldwide. This software is distributed without any
# warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication
# along with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
#

#
# lang.bash
# ---------
#
# Bash language and execution related functions. Used by public helper
# functions.
#

# Check whether the calling function was called from a given function.
#
# By default, direct invocation is checked. The function succeeds if the
# calling function was called directly from the given function. In other
# words, if the given function is the next element on the call stack.
#
# When `--indirect' is specified, indirect invocation is checked. The
# function succeeds if the calling function was called from the given
# function with any number of intermediate calls. In other words, if the
# given function can be found somewhere on the call stack.
#
# Direct invocation is a form of indirect invocation with zero
# intermediate calls.
#
# Globals:
# FUNCNAME
# Options:
# -i, --indirect - check indirect invocation
# Arguments:
# $1 - calling function's name
# Returns:
# 0 - current function was called from the given function
# 1 - otherwise
batslib_is_caller() {
local -i is_mode_direct=1

# Handle options.
while (( $# > 0 )); do
case "$1" in
-i|--indirect) is_mode_direct=0; shift ;;
--) shift; break ;;
*) break ;;
esac
done

# Arguments.
local -r func="$1"

# Check call stack.
if (( is_mode_direct )); then
[[ $func == "${FUNCNAME[2]}" ]] && return 0
else
local -i depth
for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do
[[ $func == "${FUNCNAME[$depth]}" ]] && return 0
done
fi

return 1
}
88 changes: 88 additions & 0 deletions test/52-lang-10-batslib_is_caller.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bats

load 'test_helper'


# Test functions
test_func_lvl_2() {
test_func_lvl_1 "$@"
}

test_func_lvl_1() {
test_func_lvl_0 "$@"
}

test_func_lvl_0() {
batslib_is_caller "$@"
}


#
# Direct invocation
#

# Interface
@test 'batslib_is_caller() <function>: returns 0 if the current function was called directly from <function>' {
run test_func_lvl_1 test_func_lvl_1
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 0 ]
}

@test 'batslib_is_caller() <function>: returns 1 if the current function was not called directly from <function>' {
run test_func_lvl_0 test_func_lvl_1
[ "$status" -eq 1 ]
[ "${#lines[@]}" -eq 0 ]
}

# Correctness
@test 'batslib_is_caller() <function>: the current function does not appear on the call stack' {
run test_func_lvl_0 test_func_lvl_0
[ "$status" -eq 1 ]
[ "${#lines[@]}" -eq 0 ]
}


#
# Indirect invocation
#

# Options
test_i_indirect() {
run test_func_lvl_2 "$@"
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 0 ]
}

@test 'batslib_is_caller() -i <function>: enables indirect checking' {
test_i_indirect -i test_func_lvl_2
}

@test 'batslib_is_caller() --indirect <function>: enables indirect checking' {
test_i_indirect --indirect test_func_lvl_2
}

# Interface
@test 'batslib_is_caller() --indirect <function>: returns 0 if the current function was called indirectly from <function>' {
run test_func_lvl_2 --indirect test_func_lvl_2
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 0 ]
}

@test 'batslib_is_caller() --indirect <function>: returns 1 if the current function was not called indirectly from <function>' {
run test_func_lvl_1 --indirect test_func_lvl_2
[ "$status" -eq 1 ]
[ "${#lines[@]}" -eq 0 ]
}

# Correctness
@test 'batslib_is_caller() --indirect <function>: direct invocation is a special case of indirect invocation with zero intermediate calls' {
run test_func_lvl_1 --indirect test_func_lvl_1
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 0 ]
}

@test 'batslib_is_caller() --indirect <function>: the current function does not appear on the call stack' {
run test_func_lvl_0 --indirect test_func_lvl_0
[ "$status" -eq 1 ]
[ "${#lines[@]}" -eq 0 ]
}

0 comments on commit 57d863c

Please sign in to comment.