Skip to content

Commit f0b3f98

Browse files
committed
Minor release 0.7.0
1 parent e94bc7c commit f0b3f98

File tree

6 files changed

+363
-28
lines changed

6 files changed

+363
-28
lines changed

bin/mock_exe.sh

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ set -euo pipefail
3030

3131
# Check whether required environment variables are set.
3232
env_var_check() {
33-
if ! [[ -d ${__SHELLMOCK_OUTPUT-} ]]; then
34-
echo "Vairable __SHELLMOCK_OUTPUT not defined or no directory."
35-
exit 1
36-
fi
33+
local var
34+
for var in __SHELLMOCK_MOCKBIN __SHELLMOCK_FUNCSTORE __SHELLMOCK_OUTPUT; do
35+
if ! [[ -d ${!var-} ]]; then
36+
echo >&2 "Vairable ${var} not defined or no directory."
37+
exit 1
38+
fi
39+
done
3740
}
3841

3942
get_and_ensure_outdir() {
@@ -137,7 +140,7 @@ _match_spec() {
137140
errecho "Internal error, incorrect spec ${spec}"
138141
return 1
139142
fi
140-
done < <(base64 --decode <<< "${full_spec}") && wait $!
143+
done < <(base64 --decode <<< "${full_spec}") && wait $! || return 1
141144
}
142145

143146
# Check whether the given process is a bats process. A bats process is a bash
@@ -196,8 +199,10 @@ find_matching_argspec() {
196199
fi
197200
done < <(
198201
env | sed 's/=.*$//' \
199-
| grep -x "MOCK_ARGSPEC_BASE64_${cmd_b32}_[0-9][0-9]*" | sort -u
200-
) && wait $!
202+
| {
203+
grep -x "MOCK_ARGSPEC_BASE64_${cmd_b32}_[0-9][0-9]*" || :
204+
} | sort -u
205+
) && wait $! || return 1
201206

202207
errecho "SHELLMOCK: unexpected call '${cmd} $*'"
203208
_kill_parent "${PPID}"
@@ -248,7 +253,40 @@ return_with_code() {
248253
return 0
249254
}
250255

256+
# Check whether this mock sould actually call the external executable instead of
257+
# providing mock output and exit code. If it should forward, the value of the
258+
# checked env var for this cmd_spec should be "forward".
259+
should_forward() {
260+
local cmd_spec="$1"
261+
local rc_env_var
262+
rc_env_var="MOCK_RC_${cmd_spec}"
263+
[[ -n ${!rc_env_var-} && ${!rc_env_var} == forward ]]
264+
}
265+
266+
# Forward the arguments to the first executable in PATH that is not controlled
267+
# by shellmock, that is the first executable not in __SHELLMOCK_MOCKBIN. We can
268+
# also forward to functions that we stored, but those functions cannot access
269+
# shell variables of the surrounding shell.
270+
forward() {
271+
local cmd=$1
272+
shift
273+
local args=("$@")
274+
275+
while read -r -d: path; do
276+
if
277+
[[ ${path} != "${__SHELLMOCK_MOCKBIN}" ]] \
278+
&& PATH="${path}" command -v "${cmd}" &> /dev/null
279+
then
280+
local exe="${path}/${cmd}"
281+
echo >&2 "SHELLMOCK: forwarding call: ${exe} $*"
282+
exec "${exe}" "${args[@]}"
283+
fi
284+
done <<< "${__SHELLMOCK_FUNCSTORE}:${PATH}"
285+
}
286+
251287
main() {
288+
# Make sure that shell aliases never interfere with this mock.
289+
unalias -a
252290
env_var_check
253291
# Determine our name. This assumes that the first value in argv is the name of
254292
# the command. This is almost always so.
@@ -265,9 +303,13 @@ main() {
265303
# it cannot be found, either exit with an error or kill the parent process.
266304
local cmd_spec
267305
cmd_spec="$(find_matching_argspec "${outdir}" "${cmd}" "${cmd_b32}" "$@")"
268-
provide_output "${cmd_spec}"
269-
run_hook "${cmd_spec}"
270-
return_with_code "${cmd_spec}"
306+
if should_forward "${cmd_spec}"; then
307+
forward "${cmd}" "$@"
308+
else
309+
provide_output "${cmd_spec}"
310+
run_hook "${cmd_spec}"
311+
return_with_code "${cmd_spec}"
312+
fi
271313
}
272314

273315
# Run if executed directly. If sourced from a bash shell, don't do anything,

docs/usage.md

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ sure to replace `${PATH_TO_SHELLMOCK_LIBRARY}` appropriately.
4545
You can access all functionality of Shellmock via the `shellmock` command.
4646
It is implemented as a shell function with the following sub-commands:
4747

48-
- `new`:
48+
- `new` / `mock`:
4949
Create a new mock for an executable.
5050
- `config`:
5151
Configure a previously-created mock by defining expectations.
@@ -55,6 +55,8 @@ It is implemented as a shell function with the following sub-commands:
5555
Configure global behaviour of Shellmock itself.
5656
- `calls`:
5757
Log past calls to mocks and suggest mock configs to reproduce.
58+
- `delete` / `unmock`:
59+
Remove all mocks for an executable.
5860
- `help`:
5961
Provide a help text.
6062

@@ -80,6 +82,7 @@ You can jump to the respective section via the following links.
8082
- [ensure-assertions](#ensure-assertions)
8183
- [calls](#calls)
8284
- [Example](#example)
85+
- [delete](#delete)
8386

8487
### new
8588

@@ -91,6 +94,7 @@ Syntax:
9194
The `new` command creates a new mock executable called `name`.
9295
It is created in a directory in your `PATH` that is controlled by Shellmock.
9396
You need to create a mock before you can configure it or make assertions on it.
97+
The `mock` command is an alias for the `new` command.
9498

9599
<!-- shellmock-helptext-end -->
96100

@@ -111,7 +115,7 @@ from that point forward, assuming no code changes `PATH`.
111115
<!-- shellmock-helptext-start -->
112116

113117
Syntax:
114-
`shellmock config <name> <exit_code> [hook:<hook-function>] [1:<argspec> [...]]`
118+
`shellmock config <name> [<exit_code>|forward] [hook:<hook-function>] [1:<argspec> [...]]`
115119

116120
The `config` command defines expectations for calls to your mocked executable.
117121
You need to define expectations before you can make assertions on your mock.
@@ -125,7 +129,8 @@ The `config` command takes at least two arguments:
125129

126130
1. the `name` of the mock you wish you define expectations for, and
127131
2. the mock's `exit_code` for invocations matching the expectations configured
128-
with this call.
132+
with this call or the literal string `forward`.
133+
See [below](#forwarding-calls) for details on forwarding calls.
129134

130135
Next, you may optionally specify the name of a `bash` function that the mock
131136
will execute immediately before exiting.
@@ -425,6 +430,44 @@ EOF
425430
shellmock config git 0 1:tag 2:--list <<< $'first\nsecond\n'
426431
```
427432

433+
### Forwarding Calls
434+
435+
It can be desirable to mock only some calls to an executable.
436+
For example, you may want to mock only `POST` request sent via `curl` but `GET`
437+
requests should still be issued.
438+
Or you may want to mock all calls to `git push` while other commands should
439+
still be executed.
440+
441+
You can forward specific calls to an executable by specifying the literal string
442+
`forward` as the second argument to the `config` command.
443+
Calls matching argspecs provided this way will be forwarded to the actual
444+
executable.
445+
446+
**Example**:
447+
448+
```bash
449+
# Initialising mock for curl.
450+
shellmock new curl
451+
# Mocking all POST requests, i.e. calls that have the literal string POST as
452+
# argument anywhere.
453+
shellmock config curl 0 any:POST <<< "my mock output"
454+
# Forwarding all GET requests, i.e. calls that have the literal string GET as
455+
# argument anywhere.
456+
shellmock config curl forward any:GET
457+
```
458+
459+
**Example**:
460+
461+
```bash
462+
# Initialising mock for git.
463+
shellmock new git
464+
# Mocking all push commands, i.e. calls that have the literal string push as
465+
# first argument.
466+
shellmock config git 0 1:push
467+
# Forwarding all other calls. Specific configurations have to go first.
468+
shellmock config git forward
469+
```
470+
428471
### assert
429472

430473
<!-- shellmock-helptext-start -->
@@ -707,3 +750,29 @@ the output would be as follows instead:
707750
]
708751
```
709752
<!-- prettier-ignore-end -->
753+
754+
### delete
755+
756+
<!-- shellmock-helptext-start -->
757+
758+
Syntax:
759+
`shellmock delete <name>`
760+
761+
The `delete` command completely removes all mocks for `name`.
762+
Mock executables are removed from your `PATH` and environment variables used to
763+
configure the mock are removed.
764+
The `unmock` command is an alias for the `delete` command.
765+
766+
<!-- shellmock-helptext-end -->
767+
768+
The `delete` command takes exactly one argument:
769+
the name of the executable whose mocks shall be removed.
770+
For example:
771+
772+
```bash
773+
shellmock delete git
774+
```
775+
776+
This will remove the mock executable for `git`.
777+
It will also undo all mock configurations issued via `shellmock config git`.
778+
After unmocking, new mocks can be created for the very same executable.

lib/mock_management.bash

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,59 @@ __shellmock__new() {
2626

2727
if [[ $(type -t "${cmd}") == function ]]; then
2828
# We are mocking a function, unset it or it will take precedence over our
29-
# injected executable.
29+
# injected executable. However, store the original function so that we could
30+
# restore it.
31+
__shellmock_internal_funcstore "${cmd}" > "${__SHELLMOCK_FUNCSTORE}/${cmd}"
32+
chmod +x "${__SHELLMOCK_FUNCSTORE}/${cmd}"
3033
unset -f "${cmd}"
3134
fi
3235

3336
__shellmock_write_mock_exe > "${__SHELLMOCK_MOCKBIN}/${cmd}"
3437
chmod +x "${__SHELLMOCK_MOCKBIN}/${cmd}"
3538
}
3639

40+
# An alias for the "new" command.
41+
__shellmock__mock() {
42+
__shellmock__new "$@"
43+
}
44+
45+
__shellmock__unmock() {
46+
__shellmock_internal_pathcheck
47+
__shellmock_internal_trapcheck
48+
49+
local cmd="$1"
50+
local cmd_b32
51+
cmd_b32=$(base32 -w0 <<< "${cmd}" | tr "=" "_")
52+
53+
# Restore the function if we are mocking one.
54+
local store="${__SHELLMOCK_FUNCSTORE}/${cmd}"
55+
if [[ -f ${store} ]]; then
56+
# shellcheck disable=SC1090
57+
source "${store}"
58+
rm "${store}"
59+
fi
60+
61+
# In any case, remove the mock and unset all env vars defined for it. Mocks
62+
# are identified by their argspecs or return codes. Thus, we only remove those
63+
# env vars.
64+
local env_var
65+
while read -r env_var; do
66+
unset "${env_var}"
67+
done < <(
68+
env | sed 's/=.*$//' \
69+
| { grep -xE "^MOCK_(RC|ARGSPEC_BASE64)_${cmd_b32}_[0-9][0-9]*" || :; }
70+
) && wait $! || return 1
71+
72+
if [[ -f "${__SHELLMOCK_MOCKBIN}/${cmd}" ]]; then
73+
rm "${__SHELLMOCK_MOCKBIN}/${cmd}"
74+
fi
75+
}
76+
77+
# An alias for the "unmock" command.
78+
__shellmock__delete() {
79+
__shellmock__unmock "$@"
80+
}
81+
3782
__shellmock_assert_no_duplicate_argspecs() {
3883
local args=("$@")
3984

@@ -209,7 +254,7 @@ __shellmock__assert() {
209254
fi
210255
done < <(
211256
find "${__SHELLMOCK_OUTPUT}/${cmd_b32}" -mindepth 2 -type f -name stderr
212-
) && wait $!
257+
) && wait $! || return 1
213258
if [[ ${has_err} -ne 0 ]]; then
214259
echo >&2 "SHELLMOCK: got at least one unexpected call for mock ${cmd}."
215260
return 1
@@ -226,7 +271,7 @@ __shellmock__assert() {
226271
find "${__SHELLMOCK_OUTPUT}/${cmd_b32}" -mindepth 2 -type f \
227272
-name argspec -print0 | xargs -r -0 cat | sort -u
228273
fi
229-
) && wait $!
274+
) && wait $! || return 1
230275

231276
declare -a expected_argspecs
232277
mapfile -t expected_argspecs < <(
@@ -235,7 +280,7 @@ __shellmock__assert() {
235280
env | sed 's/=.*$//' \
236281
| { grep -x "MOCK_ARGSPEC_BASE64_${cmd_b32}_[0-9][0-9]*" || :; } \
237282
| sort -u
238-
) && wait $!
283+
) && wait $! || return 1
239284

240285
local has_err=0
241286
for argspec in "${expected_argspecs[@]}"; do
@@ -317,7 +362,7 @@ __shellmock__calls() {
317362
readarray -d $'\n' -t call_ids < <(
318363
find "${__SHELLMOCK_OUTPUT}/${cmd_b32}" -mindepth 1 -maxdepth 1 -type d \
319364
| sort -n
320-
) && wait $!
365+
) && wait $! || return 1
321366

322367
for call_idx in "${!call_ids[@]}"; do
323368
local call_id="${call_ids[${call_idx}]}"

lib/shellmock.bash

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616
# License for the specific language governing permissions and limitations under
1717
# the License.
1818

19+
__shellmock_mktemp() {
20+
local has_bats=$1
21+
local what=$2
22+
local dir
23+
dir=$(mktemp -d -p "${BATS_TEST_TMPDIR-${TMPDIR-/tmp}}")
24+
if [[ ${has_bats} -eq 0 ]]; then
25+
echo >&2 "Keeping ${what} in: ${dir}"
26+
fi
27+
echo "${dir}"
28+
}
29+
1930
# Initialise shellmock, which includes setting up temporary directories either
2031
# as subdirectories of bats' temporary ones when run via bats, or global
2132
# temporary directories when run without bats. This function also modifies PATH
@@ -31,25 +42,27 @@ __shellmock_internal_init() {
3142
if [[ -z ${BATS_TEST_TMPDIR} ]]; then
3243
has_bats=0
3344
fi
45+
46+
if [[ ${has_bats} -eq 0 ]]; then
47+
echo >&2 "Running outside of bats, temporary directories will be kept."
48+
fi
49+
3450
# Modify PATH to permit injecting executables.
3551
declare -gx __SHELLMOCK_MOCKBIN
36-
__SHELLMOCK_MOCKBIN="$(mktemp -d -p "${BATS_TEST_TMPDIR-${TMPDIR-/tmp}}")"
52+
__SHELLMOCK_MOCKBIN="$(__shellmock_mktemp "${has_bats}" "mocks")"
3753
export PATH="${__SHELLMOCK_MOCKBIN}:${PATH}"
3854

3955
declare -gx __SHELLMOCK_OUTPUT
40-
__SHELLMOCK_OUTPUT="$(mktemp -d -p "${BATS_TEST_TMPDIR-${TMPDIR-/tmp}}")"
56+
__SHELLMOCK_OUTPUT="$(__shellmock_mktemp "${has_bats}" "mock call data")"
57+
58+
declare -gx __SHELLMOCK_FUNCSTORE
59+
__SHELLMOCK_FUNCSTORE="$(__shellmock_mktemp "${has_bats}" "mocked functions")"
4160

4261
declare -gx __SHELLMOCK_EXPECTATIONS_DIR
4362
__SHELLMOCK_EXPECTATIONS_DIR="$(
44-
mktemp -d -p "${BATS_TEST_TMPDIR-${TMPDIR-/tmp}}"
63+
__shellmock_mktemp "${has_bats}" "call records"
4564
)"
4665

47-
if [[ ${has_bats} -eq 0 ]]; then
48-
echo >&2 "Running outside of bats, temporary directories will be kept."
49-
echo >&2 "Keeping mocks in: ${__SHELLMOCK_MOCKBIN}"
50-
echo >&2 "Keeping mock call data in: ${__SHELLMOCK_OUTPUT}"
51-
fi
52-
5366
declare -gx __SHELLMOCK_PATH
5467
# Remember the value of "${PATH}" when shellmock was loaded, including the
5568
# prepended mockbin dir.

0 commit comments

Comments
 (0)