Skip to content

Commit bf7d3b0

Browse files
committed
stub, test_helper: support concurrent, out-of-order and multiple-times stub command execution
1 parent dd2ad3e commit bf7d3b0

2 files changed

Lines changed: 234 additions & 42 deletions

File tree

test/stubs/stub

Lines changed: 200 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash
2+
export PS4='+($$:${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
23
set -e
34

45
status=0
@@ -13,33 +14,149 @@ _STUB_RESULT="${PROGRAM}_STUB_RESULT"
1314
_STUB_END="${PROGRAM}_STUB_END"
1415
_STUB_LOG="${PROGRAM}_STUB_LOG"
1516

17+
18+
STUB_LOCKFILE="${TMPDIR}/${program}-stub.lock"
19+
20+
release_lock() {
21+
rm -f "$STUB_LOCKFILE"
22+
trap - EXIT
23+
}
24+
25+
acquire_lock() {
26+
local start=$SECONDS
27+
local acquire_timeout=10
28+
local acquired=
29+
while (( SECONDS <= start + $acquire_timeout )); do
30+
31+
( set -o noclobber; echo -n >"$STUB_LOCKFILE" ) 2>/dev/null && acquired=1
32+
33+
if [[ -n $acquired ]]; then
34+
trap release_lock EXIT
35+
break
36+
else
37+
# POSIX sleep(1) doesn't provide subsecond precision, but many others do
38+
sleep 0.1 2>/dev/null || sleep 1
39+
fi
40+
done
41+
if [[ -z $acquired ]]; then
42+
echo "$0: error: could not acquire stub lock \`$STUB_LOCKFILE' in ${acquire_timeout} seconds" >&2
43+
exit 2
44+
fi
45+
}
46+
47+
acquire_lock
48+
1649
[ -n "${!_STUB_LOG}" ] || eval "${_STUB_LOG}"="${TMPDIR}/${program}-stub-log"
50+
1751
if test -z "${!_STUB_END}"; then echo "$program" "$@" >>"${!_STUB_LOG}"; fi
1852

1953
[ -e "${!_STUB_PLAN}" ] || exit 1
2054
[ -n "${!_STUB_RUN}" ] || eval "${_STUB_RUN}"="${TMPDIR}/${program}-stub-run"
2155

2256

2357
# Initialize or load the stub run information.
24-
eval "${_STUB_INDEX}"=1
25-
eval "${_STUB_RESULT}"=0
26-
if test -e "${!_STUB_RUN}"; then source "${!_STUB_RUN}"; fi
27-
58+
read_runfile() {
59+
if test -e "${!_STUB_RUN}"; then source "${!_STUB_RUN}"; fi
60+
}
61+
write_runfile() {
62+
{
63+
local i
64+
echo "${_STUB_INDEX}=${!_STUB_INDEX}"
65+
echo "${_STUB_RESULT}=${!_STUB_RESULT}"
66+
echo "STUB_RUNCOUNTS=()"
67+
for i in ${!STUB_RUNCOUNTS[@]}; do
68+
echo "STUB_RUNCOUNTS[$i]=${STUB_RUNCOUNTS[$i]}"
69+
done
70+
} > "${!_STUB_RUN}"
71+
}
72+
update_runfile_index() {
73+
( eval "${_STUB_INDEX}=$((${!_STUB_INDEX} + 1))"
74+
write_runfile
75+
)
76+
}
77+
update_runfile_result() {
78+
(
79+
# Another stubs may have run while we were running payload
80+
# So we need to merge possible state changes
81+
local our_result="${!_STUB_RESULT}"
82+
local -a our_runcounts
83+
array_copy STUB_RUNCOUNTS our_runcounts
84+
85+
read_runfile
86+
87+
# merge our match_result and their match_result, with failure taking precedence
88+
local new_result=$(( $our_result | ${!_STUB_RESULT} ))
89+
eval "${_STUB_RESULT}=\$new_result"
90+
91+
# 3-way merge STUB_RUNCOUNTS (their changes),
92+
# our_runcounts (our changes) and initial_runcounts (base)
93+
local i
94+
for i in $(printf '%s\n' ${!STUB_RUNCOUNTS[@]} ${!our_runcounts[@]} | sort -u); do
95+
STUB_RUNCOUNTS[$i]=$((STUB_RUNCOUNTS[i] + our_runcounts[i] - initial_runcounts[i]))
96+
done
97+
98+
write_runfile
99+
)
100+
}
28101

29-
# Loop over each line in the plan.
30-
index=0
31-
while IFS= read -r line; do
32-
index=$(($index + 1))
102+
array_copy() {
103+
#`declare -p' is supposed to produce "declare -a src=([index]="value" <etc>)"
104+
local data="$(declare -p ${1:?})"
105+
local dest="${2:?}"
106+
# Bash 5 dumps empty arrays as "declare -a arr"
107+
if [[ $data != *=* ]]; then
108+
data="()";
109+
else
110+
data="${data#*=}"
111+
fi
112+
113+
# Bash 3 and MacPorts version of Bash 5 dump arrays in single quotes "declare -a arr='()'"
114+
# but arr='(<value>)' createss "([0]='<value>')" rather than duplicate the array
115+
if [[ ${data:0:1} == "'" && ${data:${#data}-1:1} == "'" ]]; then
116+
data="${data:1:${#data}-2}"
117+
fi
118+
eval "$dest=$data"
119+
}
33120

34-
if [ -z "${!_STUB_END}" ] && [ $index -eq "${!_STUB_INDEX}" ]; then
35-
# We found the plan line we're interested in.
36-
# Start off by assuming success.
37-
result=0
121+
eval "${_STUB_INDEX}"=1
122+
eval "${_STUB_RESULT}"=0
123+
declare -a STUB_RUNCOUNTS
124+
read_runfile
125+
declare -a initial_runcounts
126+
array_copy STUB_RUNCOUNTS initial_runcounts
38127

128+
# !_STUB_END is set externally to trigger verification mode for `unstub'
129+
# Execution mode
130+
if [ -z "${!_STUB_END}" ]; then
131+
132+
# Loop over each line in the plan.
133+
regular_command_index=0
134+
no_order_command_index=0
135+
match_result=1
136+
while IFS= read -r line; do
137+
line_flags="${line%% *}"
138+
line="${line#${line_flags} }"
139+
line_flag_no_order="$(if [[ $line_flags == N ]]; then echo 1; fi)"
140+
line_flag_multiple="$(if [[ $line_flags == M ]]; then echo 1; fi)"
141+
line_flag_regular="$(if [[ $line_flags == - ]]; then echo 1; fi)"
142+
unset line_flags
143+
144+
# Go through the plan until a match is found.
145+
# For regular commands, only check the next command by index.
146+
# Also keep track of no-order commands for the purpose of run count tracking
147+
if [[ -n $line_flag_regular ]]; then
148+
regular_command_index=$(($regular_command_index + 1))
149+
if [[ $regular_command_index -ne ${!_STUB_INDEX} ]]; then
150+
continue;
151+
fi
152+
else
153+
no_order_command_index=$(($no_order_command_index + 1))
154+
fi
155+
39156
# Split the line into an array of arguments to
40157
# match and a command to run to produce output.
41158
command=" $line"
42-
if [ "$command" != "${command/ : }" ]; then
159+
if [[ $command == *" : "* ]]; then
43160
patterns="${command%% : *}"
44161
command="${command#* : }"
45162
fi
@@ -54,36 +171,93 @@ while IFS= read -r line; do
54171

55172
# Match the expected argument patterns to actual
56173
# arguments.
174+
match_result=0
57175
for (( i=0; i<${#patterns[@]}; i++ )); do
58176
pattern="${patterns[$i]}"
59177
argument="${arguments[$i]}"
60178

61179
case "$argument" in
62180
$pattern ) ;;
63-
* ) result=1 ;;
181+
* ) match_result=1 ;;
64182
esac
65183
done
66184

67185
# If the arguments matched, evaluate the command
68186
# in a subshell. Otherwise, log the failure.
69-
if [ $result -eq 0 ] ; then
70-
set +e
71-
( eval "$command" )
72-
status="$?"
73-
set -e
74-
else
75-
eval "${_STUB_RESULT}"=1
187+
if [ $match_result -eq 0 ] ; then
188+
189+
# If this is a regular command, push the regular command index for the next stub invocation
190+
if [[ -n $line_flag_regular ]]; then
191+
update_runfile_index
192+
else
193+
STUB_RUNCOUNTS[$no_order_command_index]=$((STUB_RUNCOUNTS[no_order_command_index]+1))
194+
fi
195+
196+
# Release the lock while running the payload to allow another `stub'
197+
# of the same program to run concurrently (e.g. in a pipeline).
198+
release_lock
199+
200+
( eval "$command" ) && status="$?" || status="$?"
201+
202+
break
76203
fi
204+
205+
done < "${!_STUB_PLAN}"
206+
207+
#If we never matched anything, we failed.
208+
if [[ $match_result -eq 1 ]]; then
209+
eval "${_STUB_RESULT}"=1
77210
fi
78-
done < "${!_STUB_PLAN}"
79211

212+
# Write out the match_result information.
213+
acquire_lock
214+
update_runfile_result
215+
release_lock
216+
217+
exit "$status"
218+
219+
fi
80220

221+
# Verification mode (`unstub')
81222
if [ -n "${!_STUB_END}" ]; then
82-
# If the number of lines in the plan is larger than
83-
# the requested index, we failed.
84-
if [ $index -ge "${!_STUB_INDEX}" ]; then
223+
224+
# `unstub' is supposed to run after any stubs are finished
225+
release_lock
226+
227+
# If the number of regular commands in the plan is larger than
228+
# the final regular_command_index, we failed.
229+
if [ "$(grep -Ee '^-' "${!_STUB_PLAN}" | wc -l )" -ge "${!_STUB_INDEX}" ]; then
85230
eval "${_STUB_RESULT}"=1
86231
fi
232+
233+
# If no-order commands weren't executed exactly once
234+
# and multiple-times commands at least once, we failed.
235+
no_order_command_index=0
236+
while IFS= read -r line; do
237+
line_flags="${line%% *}"
238+
line="${line#${line_flags} }"
239+
line_flag_no_order="$(if [[ $line_flags == N ]]; then echo 1; fi)"
240+
line_flag_multiple="$(if [[ $line_flags == M ]]; then echo 1; fi)"
241+
line_flag_regular="$(if [[ $line_flags == - ]]; then echo 1; fi)"
242+
unset line_flags
243+
244+
if [[ -z $line_flag_regular ]]; then
245+
continue
246+
fi
247+
248+
no_order_command_index=$((no_order_command_index + 1))
249+
250+
if [[ ( -n $line_flag_no_order && \
251+
(( STUB_RUNCOUNTS[no_order_command_index] != 1 )) ) \
252+
|| \
253+
( -n $line_flag_multiple && \
254+
(( STUB_RUNCOUNTS[no_order_command_index] < 1 )) ) ]]
255+
then
256+
eval "${_STUB_RESULT}"=1
257+
fi
258+
259+
done < "${!_STUB_PLAN}"
260+
87261
if [ "${!_STUB_RESULT}" -ne 0 ]; then
88262
{
89263
echo "plan:"
@@ -97,21 +271,7 @@ if [ -n "${!_STUB_END}" ]; then
97271
rm -f "${!_STUB_RUN}"
98272
rm -f "${!_STUB_LOG}"
99273

100-
# Return the result.
274+
# Return the run result.
101275
exit "${!_STUB_RESULT}"
102276

103-
else
104-
# If the requested index is larger than the number
105-
# of lines in the plan file, we failed.
106-
if [ "${!_STUB_INDEX}" -gt $index ]; then
107-
eval "${_STUB_RESULT}"=1
108-
fi
109-
110-
# Write out the run information.
111-
{ echo "${_STUB_INDEX}=$((${!_STUB_INDEX} + 1))"
112-
echo "${_STUB_RESULT}=${!_STUB_RESULT}"
113-
} > "${!_STUB_RUN}"
114-
115-
exit "$status"
116-
117277
fi

test/test_helper.bash

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export TMP="$BATS_TEST_DIRNAME/tmp"
2+
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
23

3-
PATH=/usr/bin:/usr/sbin:/bin/:/sbin
4+
PATH=/usr/bin:/usr/sbin:/bin:/sbin
45
PATH="$BATS_TEST_DIRNAME/../bin:$PATH"
56
PATH="$TMP/bin:$PATH"
67
export PATH
@@ -10,6 +11,35 @@ teardown() {
1011
}
1112

1213
stub() {
14+
local FLAG_NO_ORDER=
15+
local FLAG_MULTIPLE=
16+
while (($#)); do
17+
case "$1" in
18+
-N|--no-order)
19+
FLAG_NO_ORDER=1
20+
shift
21+
;;
22+
-M|--multiple)
23+
FLAG_MULTIPLE=1
24+
shift
25+
;;
26+
-*)
27+
echo "stub: unrecognized switch: $arg" >$2
28+
return 1
29+
;;
30+
*)
31+
break
32+
;;
33+
esac
34+
done
35+
36+
local FLAGS=-
37+
if [[ -n $FLAG_MULTIPLE ]]; then
38+
FLAGS=M
39+
elif [[ -n $FLAG_NO_ORDER ]]; then
40+
FLAGS=N
41+
fi
42+
1343
local program="$1"
1444
local prefix="$(echo "$program" | tr a-z- A-Z_)"
1545
shift
@@ -23,7 +53,9 @@ stub() {
2353
ln -sf "${BATS_TEST_DIRNAME}/stubs/stub" "${TMP}/bin/${program}"
2454

2555
touch "${TMP}/${program}-stub-plan"
26-
for arg in "$@"; do printf "%s\n" "$arg" >> "${TMP}/${program}-stub-plan"; done
56+
for arg in "$@"; do
57+
echo "$FLAGS" "$arg" >> "${TMP}/${program}-stub-plan"
58+
done
2759
}
2860

2961
unstub() {

0 commit comments

Comments
 (0)