Skip to content

Commit

Permalink
Minor release 0.10.0
Browse files Browse the repository at this point in the history
Signed-off-by: Torsten Long <[email protected]>
  • Loading branch information
razziel89 committed Nov 22, 2024
1 parent 8e97ee9 commit 6b55ccc
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 64 deletions.
9 changes: 2 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,8 @@ format:
# Run tests under all possible combinations of some shell options.
.PHONY: test
test: build
for opts in {-e,+e}{-u,+u}{-o,+o}; do \
TEST_OPTS="$$(sed 's/+/ +/g;s/-/ -/g;s/o/o pipefail/g' <<< "$${opts}")" && \
export TEST_OPTS && \
echo >&2 "Testing $${BASH_VERSION} with shell options $${TEST_OPTS}" && \
bats --jobs "$$(nproc --all)" --print-output-on-failure ./tests/*.bats \
|| exit 1; \
done
echo >&2 "Running tests for version $${BASH_VERSION}."
bats --jobs "$$(nproc --all)" --print-output-on-failure ./tests/*.bats

DOWNLOAD_URL_PREFIX := https://mirror.kumi.systems/gnu/bash/

Expand Down
46 changes: 38 additions & 8 deletions bin/mock_exe.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ env_var_check() {
if ! [[ -d ${!dir-} ]]; then
echo >&2 "Variable ${dir} not defined or no directory."
_kill_parent
return 1
fi
done
for var in "${vars[@]}"; do
if [[ -z ${!var-} ]]; then
echo >&2 "Variable ${var} not defined."
_kill_parent
return 1
fi
done
}
Expand Down Expand Up @@ -338,18 +340,46 @@ should_forward() {
local cmd_spec="$1"
local rc_env_var
rc_env_var="MOCK_RC_${cmd_spec}"
[[ -n ${!rc_env_var-} && ${!rc_env_var} == forward ]]
[[ -n ${!rc_env_var-} ]] \
&& [[ ${!rc_env_var} == forward || ${!rc_env_var} == "forward:"* ]]
}

# Forward the arguments to the first executable in PATH that is not controlled
# by shellmock, that is the first executable not in __SHELLMOCK_MOCKBIN. We can
# also forward to functions that we stored, but those functions cannot access
# shell variables of the surrounding shell.
# shell variables of the surrounding shell. If requested, we will first call an
# exported shell function that may modify the arguments that we forward.
forward() {
local cmd=$1
shift
local cmd_spec=$1
local cmd=$2
shift 2
local args=("$@")

# Update arguments if requested.
local rc_env_var
rc_env_var="MOCK_RC_${cmd_spec}"
if [[ ${!rc_env_var} == "forward:"* ]]; then
local forward_fn="${!rc_env_var##forward:}"
local updated_args=()
if ! {
mapfile -t -d $'\0' updated_args < <(
# The function will be invoked indirectly by the forward function.
# shellcheck disable=SC2317
update_args() {
local arg && for arg in "$@"; do printf -- '%s\0' "${arg}"; done
}
"${forward_fn}" "${cmd}" "${args[@]}"
) && wait $!
}; then
echo >&2 "SHELLMOCK: failed to call function ${forward_fn@Q} that" \
"shall modify arguments forwarded to ${cmd@Q}."
_kill_parent
return 1
fi
cmd=${updated_args[0]}
args=("${updated_args[@]:1}")
fi

local exe
local path="${__SHELLMOCK_FUNCSTORE}:${PATH}"
# Extend PATH by shellmock's funcstore because we may want to forward to a
Expand All @@ -359,6 +389,7 @@ forward() {
then
echo >&2 "SHELLMOCK: failed to find executable to forward to: ${cmd}"
_kill_parent
return 1
fi
echo >&2 "SHELLMOCK: forwarding call: ${exe@Q} ${*@Q}"
exec "${exe}" "${args[@]}"
Expand Down Expand Up @@ -386,11 +417,11 @@ main() {
# it cannot be found, either exit with an error or kill the parent process.
local cmd_spec
cmd_spec="$(find_matching_argspec "${outdir}" "${cmd}" "${cmd_b32}" "$@")"
run_hook "${cmd_spec}" "$@"
if should_forward "${cmd_spec}"; then
forward "${cmd}" "$@"
forward "${cmd_spec}" "${cmd}" "$@"
else
provide_output "${cmd_spec}"
run_hook "${cmd_spec}" "$@"
return_with_code "${cmd_spec}"
fi
}
Expand All @@ -399,7 +430,6 @@ main() {
# which simplifies testing.
if [[ -z ${BASH_SOURCE[0]-} ]] || [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
set -euo pipefail
shopt -s inherit_errexit
main "$@"
else
:
fi
89 changes: 77 additions & 12 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ It is implemented as a shell function with the following sub-commands:
- `delete` / `unmock`:
Remove all mocks for an executable.
- `is-mock`:
Determine whether an executable has been mocked by shellmock.
Determine whether an executable has been mocked by Shellmock.
- `help`:
Provide a help text.

Expand Down Expand Up @@ -91,6 +91,7 @@ You can jump to the respective section via the following links.
- [calls](#calls)
- [Example](#example)
- [delete](#delete)
- [is-mock](#is-mock)

### new

Expand Down Expand Up @@ -123,7 +124,7 @@ from that point forward, assuming no code changes `PATH`.
<!-- shellmock-helptext-start -->

Syntax:
`shellmock config <name> [<exit_code>|forward] [hook:<hook-function>] [1:<argspec> [...]]`
`shellmock config <name> [<exit_code>|forward[:<arg_adjustment_function>]] [hook:<hook_function>] [1:<argspec> [...]]`

The `config` command defines expectations for calls to your mocked executable.
You need to define expectations before you can make assertions on your mock.
Expand All @@ -137,7 +138,8 @@ The `config` command takes at least two arguments:

1. the `name` of the mock you wish you define expectations for, and
2. the mock's `exit_code` for invocations matching the expectations configured
with this call or the literal string `forward`.
with this call or the literal string `forward` optionally followed by the
name of a function that may modify the forwarded arguments.
See [below](#forwarding-calls) for details on forwarding calls.

Next, you may optionally specify the name of a `bash` function that the mock
Expand Down Expand Up @@ -170,7 +172,7 @@ shellmock config git 0 1:branch <<< "* main"
```

**Note:** The example shows one possible way to define the output of the mock.
The example uses a _here string_ to define the input to shellmock.
The example uses a _here string_ to define the input to `shellmock`.
There are different ways to write to standard input, which even depend on the
used shell.
Here strings are known to work for `bash` and `zsh`, for example.
Expand Down Expand Up @@ -447,8 +449,8 @@ requests should still be issued.
Or you may want to mock all calls to `git push` while other commands should
still be executed.

You can forward specific calls to an executable by specifying the literal string
`forward` as the second argument to the `config` command.
You can forward specific calls to an executable by specifying only the literal
string `forward` as the second argument to the `config` command.
Calls matching argspecs provided this way will be forwarded to the actual
executable.

Expand Down Expand Up @@ -477,6 +479,69 @@ shellmock config git 0 1:push
shellmock config git forward
```

While forwarding, it might be desirable to modify some arguments.
You can do so by providing the name of a function that may modify the arguments
that will be forwarded.
That function receives all the arguments that the mock has been called with.
It is expected to pass all arguments that shall be forwarded via the function
`update_args`, either individually or in bulk.
Note that the first argument received is the name of the executable to forward
to.
That means it is possible to forward to a different executable.

**Example**:

```bash
# Initialising mock for curl.
shellmock new curl
# Forwarding all GET requests, i.e. calls that have the literal string GET as
# argument anywhere. However, we do not want to forward the `--silent` flag.
# Thus, we first define a function that modifies the arguments.
remove_silent_flag() {
# Keep all arguments apart from the one we want to skip.
for arg in "$@"; do
if [[ ${arg} != --silent ]]; then
# Report each argument that shall be forwarded individually.
update_args "${arg}"
fi
done
}
# Then, we use it to modify the arguments that will be forwarded.
shellmock config curl forward:remove_silent_flag any:GET
# This GET request will be forwarded but we will not forward the --silent flag.
curl -X GET --silent --output shellmock.bash \
https://github.com/boschresearch/shellmock/releases/latest/download/shellmock.bash
```

**Example**:

```bash
# Initialising mock for git.
shellmock new git
# Mocking all push commands, i.e. calls that have the literal string push as
# first argument.
shellmock config git 0 1:push
# Forwarding all other calls. Specific configurations have to go first. When
# forwarding, we want to make sure all forwarded calls are executed in a
# specific temporary directory, which we can do via git's `-C` flag.
# Thus, we first define an environment variable containing the desired path.
export NEW_GIT_WORKDIR=$(mktemp -d)
# Then, we define a function that modifies the arguments.
modify_git_workdir() {
# Report the executable to forward to first. We keep the same one.
update_args "$1"
# Discard the first argument.
shift
# Report the arguments modifying the workdir first.
update_args "-C" "${NEW_GIT_WORKDIR}"
# Then, report all the original arguments in bulk.
update_args "${@}"
}
# Then, we use it to modify the arguments that will be forwarded.
shellmock config git forward:modify_git_workdir
# This call will be forwarded but it will use the directory of our choice.
```

### assert

<!-- shellmock-helptext-start -->
Expand Down Expand Up @@ -603,10 +668,10 @@ However, its name is stored in a shell variable.
To be able to detect such cases, the values of all shell variables would have to
be known, which is not possible without executing the script.

To support examples like the one above, `shellmock` allows for specifying
commands that are used indirectly by adding specific directives as comments.
Lines containing directives generally look like `# shellmock:
uses-command=cmd1,cmd2` and may be followed by a comment.
To support examples like the one above, Shellmock allows for specifying commands
that are used indirectly by adding specific directives as comments.
Lines containing directives generally look like
`# shellmock: uses-command=cmd1,cmd2` and may be followed by a comment.
The above example can thus be updated to report all used executables.

**Example**:
Expand Down Expand Up @@ -720,8 +785,8 @@ Use `shellmock global-config getval killparent` to retrieve the current setting.
#### ensure-assertions

When creating and configuring a mock using Shellmock, you have to make sure to
assert that your configured mock has been used as expected via the `shellmock
assert` command.
assert that your configured mock has been used as expected via the
`shellmock assert` command.
Otherwise, you might not detect unexpected calls to your mock, or even the fact
that your mock has not even been used!
By default, Shellmock will fail a test that creates a mock without also running
Expand Down
13 changes: 13 additions & 0 deletions lib/main.bash
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@
# following a specific naming scheme. We avoid complex parsing of arguments with
# a tool such as getopt or getopts.
shellmock() {
# Ensure that only those shell options are set that shellmock needs.
local - # Restrict all changes to shell options to this function.
# Options available via "set". Options available via "shopt" cannot easily be
# scoped to a function without using a RETURN trap, but we are already uisng
# one for another purpose.
local opt opts=() flags=()
IFS=: read -r -a opts <<< "${SHELLOPTS}"
for opt in "${opts[@]}"; do flags+=(+o "${opt}"); done
set "${flags[@]}"
# Set the ones we expect and need.
set -euo pipefail

# Main code follows.
# Handle the user requesting a help text.
local arg
for arg in "$@"; do
Expand Down
26 changes: 25 additions & 1 deletion lib/mock_management.bash
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,37 @@ __shellmock__config() {
local rc="$2"
shift 2

# Sanity check the provided return code.
if
! [[ ${rc} =~ ^[0-9][0-9]*$ || ${rc} == "forward" || ${rc} == "forward:"* ]]
then
echo >&2 "Incorrect format for second argument to 'shellmock config'." \
"It must be numeric, 'forward', or 'forward:<function_name>'"
return 1
fi
# If a forwarding function has been set, check whether it is a function and
# export it.
if [[ ${rc} == "forward:"* ]]; then
local forward_fn
forward_fn="${rc##forward:}"
if [[ $(type -t "${forward_fn}") != function ]]; then
echo >&2 "Requested forwarding function ${forward_fn@Q} does not exist."
return 1
fi
if [[ ${forward_fn} == update_args ]]; then
echo >&2 "Forwarding function must not be called 'update_args'."
return 1
fi
export -f "${forward_fn?}"
fi

# If a hook has been set, check whether it is a function and export it.
local hook
if [[ ${1-} == "hook:"* ]]; then
hook="${1##hook:}"
shift
if [[ $(type -t "${hook}") != function ]]; then
echo >&2 "Requested hook function '${hook}' does not exist."
echo >&2 "Requested hook function ${hook@Q} does not exist."
return 1
fi
export -f "${hook?}"
Expand Down
Loading

0 comments on commit 6b55ccc

Please sign in to comment.