Skip to content

Commit 38f3333

Browse files
authored
Merge pull request #520 from native-api/prevent-deletion-of-contents-in-generic-prefix-env-var
CI: tests: use Bats 1.10; support concurrent, out-of-order and multiple-times execution of stub commands; fix race condition between concurrent stubs
2 parents b83e2c3 + 10c8ace commit 38f3333

4 files changed

Lines changed: 263 additions & 68 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ jobs:
1414
runs-on: ${{ matrix.os }}
1515
steps:
1616
- uses: actions/checkout@v2
17-
- run: git clone https://github.com/bats-core/bats-core.git --depth=1 -b v1.2.0 bats
17+
- run: git clone https://github.com/bats-core/bats-core.git --depth=1 -b v1.10.0 bats
1818
- run: bats/bin/bats --tap test

test/conda.bats

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ unstub_pyenv() {
2626
stub_pyenv "${PYENV_VERSION}"
2727
stub pyenv-prefix " : echo '${PYENV_ROOT}/versions/${PYENV_VERSION}'"
2828
stub pyenv-virtualenv-prefix " : echo '${PYENV_ROOT}/versions/${PYENV_VERSION}'"
29-
stub pyenv-exec "conda * : echo PYENV_VERSION=\${PYENV_VERSION} \"\$@\""
29+
stub -N pyenv-exec "conda list * : true"
30+
stub -N pyenv-exec "conda create * : echo PYENV_VERSION=\${PYENV_VERSION} \"\$@\""
3031
stub pyenv-exec "python -s -m ensurepip : true"
3132

3233
run pyenv-virtualenv venv
@@ -49,11 +50,11 @@ OUT
4950
stub_pyenv "${PYENV_VERSION}"
5051
stub pyenv-prefix " : echo '${PYENV_ROOT}/versions/${PYENV_VERSION}'"
5152
stub pyenv-virtualenv-prefix " : echo '${PYENV_ROOT}/versions/${PYENV_VERSION}'"
52-
stub pyenv-exec "conda * : echo PYENV_VERSION=\${PYENV_VERSION} \"\$@\""
53+
stub pyenv-exec "conda create * : echo PYENV_VERSION=\${PYENV_VERSION} \"\$@\""
5354
stub pyenv-exec "python -s -m ensurepip : true"
5455

5556
run pyenv-virtualenv -p python3.5 venv
56-
57+
5758
assert_success
5859
assert_output <<OUT
5960
PYENV_VERSION=miniconda3-3.16.0 conda create --name venv --yes python=3.5
@@ -72,7 +73,7 @@ OUT
7273
stub_pyenv "${PYENV_VERSION}"
7374
stub pyenv-prefix " : echo '${PYENV_ROOT}/versions/${PYENV_VERSION}'"
7475
stub pyenv-virtualenv-prefix " : echo '${PYENV_ROOT}/versions/${PYENV_VERSION}'"
75-
stub pyenv-exec "conda * : echo PYENV_VERSION=\${PYENV_VERSION} \"\$@\""
76+
stub pyenv-exec "conda create * : echo PYENV_VERSION=\${PYENV_VERSION} \"\$@\""
7677
stub pyenv-exec "python -s -m ensurepip : true"
7778

7879
run pyenv-virtualenv --python=python3.5 venv

test/stubs/stub

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

45
status=0
56
program="${0##*/}"
67
PROGRAM="$(echo "$program" | tr a-z- A-Z_)"
78
[ -n "$TMPDIR" ] || TMPDIR="/tmp"
89

9-
_STUB_PLAN="${PROGRAM}_STUB_PLAN"
10-
_STUB_RUN="${PROGRAM}_STUB_RUN"
11-
_STUB_INDEX="${PROGRAM}_STUB_INDEX"
12-
_STUB_RESULT="${PROGRAM}_STUB_RESULT"
13-
_STUB_END="${PROGRAM}_STUB_END"
14-
_STUB_LOG="${PROGRAM}_STUB_LOG"
10+
STUB_PLAN="${PROGRAM}_STUB_PLAN"
11+
STUB_PLAN="${!STUB_PLAN}"
1512

16-
[ -n "${!_STUB_LOG}" ] || eval "${_STUB_LOG}"="${TMPDIR}/${program}-stub-log"
17-
if test -z "${!_STUB_END}"; then echo "$program" "$@" >>"${!_STUB_LOG}"; fi
13+
STUB_RUN="${PROGRAM}_STUB_RUN"
14+
STUB_RUN="${!STUB_RUN:-${TMPDIR}/${program}-stub-run}"
15+
STUB_INDEX=
16+
STUB_RESULT=
1817

19-
[ -e "${!_STUB_PLAN}" ] || exit 1
20-
[ -n "${!_STUB_RUN}" ] || eval "${_STUB_RUN}"="${TMPDIR}/${program}-stub-run"
18+
STUB_END="${PROGRAM}_STUB_END"
19+
STUB_END="${!STUB_END}"
2120

21+
STUB_LOG="${PROGRAM}_STUB_LOG"
22+
STUB_LOG="${!STUB_LOG:-${TMPDIR}/${program}-stub-log}"
2223

23-
# 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
2724

25+
STUB_LOCKFILE="${TMPDIR}/${program}-stub.lock"
26+
27+
release_lock() {
28+
rm -f "$STUB_LOCKFILE"
29+
trap - EXIT
30+
}
2831

29-
# Loop over each line in the plan.
30-
index=0
31-
while IFS= read -r line; do
32-
index=$(($index + 1))
32+
acquire_lock() {
33+
local start=$SECONDS
34+
local acquire_timeout=10
35+
local acquired=
36+
while (( SECONDS <= start + $acquire_timeout )); do
37+
38+
( set -o noclobber; echo -n >"$STUB_LOCKFILE" ) 2>/dev/null && acquired=1
3339

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
40+
if [[ -n $acquired ]]; then
41+
trap release_lock EXIT
42+
break
43+
else
44+
# POSIX sleep(1) doesn't provide subsecond precision, but many others do
45+
sleep 0.1 2>/dev/null || sleep 1
46+
fi
47+
done
48+
if [[ -z $acquired ]]; then
49+
echo "$0: error: could not acquire stub lock \`$STUB_LOCKFILE' in ${acquire_timeout} seconds" >&2
50+
exit 2
51+
fi
52+
}
3853

54+
acquire_lock
55+
56+
if [[ -z $STUB_END ]]; then echo "$program" "$@" >>"$STUB_LOG"; fi
57+
58+
[[ -e $STUB_PLAN ]] || exit 1
59+
60+
# Initialize or load the stub run information.
61+
read_runfile() {
62+
if [[ -e $STUB_RUN ]]; then source "$STUB_RUN"; fi
63+
}
64+
write_runfile() {
65+
{
66+
local i
67+
echo "STUB_INDEX=$STUB_INDEX"
68+
echo "STUB_RESULT=$STUB_RESULT"
69+
echo "STUB_RUNCOUNTS=()"
70+
for i in ${!STUB_RUNCOUNTS[@]}; do
71+
echo "STUB_RUNCOUNTS[$i]=${STUB_RUNCOUNTS[$i]}"
72+
done
73+
} > "$STUB_RUN"
74+
}
75+
update_runfile_index() {
76+
( STUB_INDEX=$((STUB_INDEX + 1))
77+
write_runfile
78+
)
79+
}
80+
update_runfile_result() {
81+
(
82+
# Another stubs may have run while we were running payload
83+
# So we need to merge possible state changes
84+
local our_result="$STUB_RESULT"
85+
local -a our_runcounts
86+
array_copy STUB_RUNCOUNTS our_runcounts
87+
88+
read_runfile
89+
90+
# merge our match_result and their match_result, with failure taking precedence
91+
STUB_RESULT=$(( our_result | STUB_RESULT ))
92+
93+
# 3-way merge STUB_RUNCOUNTS (their changes),
94+
# our_runcounts (our changes) and initial_runcounts (base)
95+
local i
96+
for i in $(printf '%s\n' ${!STUB_RUNCOUNTS[@]} ${!our_runcounts[@]} | sort -u); do
97+
STUB_RUNCOUNTS[$i]=$((STUB_RUNCOUNTS[i] + our_runcounts[i] - initial_runcounts[i]))
98+
done
99+
100+
write_runfile
101+
)
102+
}
103+
104+
array_copy() {
105+
#`declare -p' is supposed to produce "declare -a src=([index]="value" <etc>)"
106+
local data="$(declare -p ${1:?})"
107+
local dest="${2:?}"
108+
# Bash 5 dumps empty arrays as "declare -a arr"
109+
if [[ $data != *=* ]]; then
110+
data="()";
111+
else
112+
data="${data#*=}"
113+
fi
114+
115+
# Bash 3 and MacPorts version of Bash 5 dump arrays in single quotes "declare -a arr='()'"
116+
# but arr='(<value>)' createss "([0]='<value>')" rather than duplicate the array
117+
if [[ ${data:0:1} == "'" && ${data:${#data}-1:1} == "'" ]]; then
118+
data="${data:1:${#data}-2}"
119+
fi
120+
eval "$dest=$data"
121+
}
122+
123+
STUB_INDEX=1
124+
STUB_RESULT=0
125+
declare -a STUB_RUNCOUNTS
126+
read_runfile
127+
declare -a initial_runcounts
128+
array_copy STUB_RUNCOUNTS initial_runcounts
129+
130+
# ${PROGRAM}_STUB_END envvar is set externally to trigger verification mode for `unstub'
131+
# Execution mode
132+
if [[ -z $STUB_END ]]; then
133+
134+
# Loop over each line in the plan.
135+
regular_command_index=0
136+
no_order_command_index=0
137+
match_result=1
138+
while IFS= read -r line; do
139+
line_flags="${line%% *}"
140+
line="${line#${line_flags} }"
141+
line_flag_no_order="$(if [[ $line_flags == N ]]; then echo 1; fi)"
142+
line_flag_multiple="$(if [[ $line_flags == M ]]; then echo 1; fi)"
143+
line_flag_regular="$(if [[ $line_flags == - ]]; then echo 1; fi)"
144+
unset line_flags
145+
146+
# Go through the plan until a match is found.
147+
# For regular commands, only check the next command by index.
148+
# Also keep track of no-order commands for the purpose of run count tracking
149+
if [[ -n $line_flag_regular ]]; then
150+
regular_command_index=$(($regular_command_index + 1))
151+
if [[ $regular_command_index -ne $STUB_INDEX ]]; then
152+
continue;
153+
fi
154+
else
155+
no_order_command_index=$(($no_order_command_index + 1))
156+
fi
157+
39158
# Split the line into an array of arguments to
40159
# match and a command to run to produce output.
41160
command=" $line"
42-
if [ "$command" != "${command/ : }" ]; then
161+
if [[ $command == *" : "* ]]; then
43162
patterns="${command%% : *}"
44163
command="${command#* : }"
45164
fi
@@ -54,67 +173,110 @@ while IFS= read -r line; do
54173

55174
# Match the expected argument patterns to actual
56175
# arguments.
176+
match_result=0
57177
for (( i=0; i<${#patterns[@]}; i++ )); do
58178
pattern="${patterns[$i]}"
59179
argument="${arguments[$i]}"
60180

61181
case "$argument" in
62182
$pattern ) ;;
63-
* ) result=1 ;;
183+
* ) match_result=1 ;;
64184
esac
65185
done
66186

67187
# If the arguments matched, evaluate the command
68188
# 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
189+
if [ $match_result -eq 0 ] ; then
190+
191+
# If this is a regular command, push the regular command index for the next stub invocation
192+
if [[ -n $line_flag_regular ]]; then
193+
update_runfile_index
194+
else
195+
STUB_RUNCOUNTS[$no_order_command_index]=$((STUB_RUNCOUNTS[no_order_command_index]+1))
196+
fi
197+
198+
# Release the lock while running the payload to allow another `stub'
199+
# of the same program to run concurrently (e.g. in a pipeline).
200+
release_lock
201+
202+
( eval "$command" ) && status="$?" || status="$?"
203+
204+
break
76205
fi
206+
207+
done < "$STUB_PLAN"
208+
209+
#If we never matched anything, we failed.
210+
if [[ $match_result -eq 1 ]]; then
211+
STUB_RESULT=1
212+
213+
#This also means that we never released the lock
214+
# before running the payload
215+
else
216+
acquire_lock
77217
fi
78-
done < "${!_STUB_PLAN}"
218+
# Write out the match_result information.
219+
update_runfile_result
220+
release_lock
221+
222+
exit "$status"
223+
224+
fi
225+
226+
# Verification mode (`unstub')
227+
if [[ -n $STUB_END ]]; then
79228

229+
# `unstub' is supposed to run after any stubs are finished
230+
release_lock
80231

81-
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
85-
eval "${_STUB_RESULT}"=1
232+
# If the number of regular commands in the plan is larger than
233+
# the final regular_command_index, we failed.
234+
if [[ $(grep -Ee '^-' "$STUB_PLAN" | wc -l ) -ge $STUB_INDEX ]]; then
235+
STUB_RESULT=1
86236
fi
87-
if [ "${!_STUB_RESULT}" -ne 0 ]; then
237+
238+
# If no-order commands weren't executed exactly once
239+
# and multiple-times commands at least once, we failed.
240+
no_order_command_index=0
241+
while IFS= read -r line; do
242+
line_flags="${line%% *}"
243+
line="${line#${line_flags} }"
244+
line_flag_no_order="$(if [[ $line_flags == N ]]; then echo 1; fi)"
245+
line_flag_multiple="$(if [[ $line_flags == M ]]; then echo 1; fi)"
246+
line_flag_regular="$(if [[ $line_flags == - ]]; then echo 1; fi)"
247+
unset line_flags
248+
249+
if [[ -z $line_flag_regular ]]; then
250+
continue
251+
fi
252+
253+
no_order_command_index=$((no_order_command_index + 1))
254+
255+
if [[ ( -n $line_flag_no_order && \
256+
(( STUB_RUNCOUNTS[no_order_command_index] != 1 )) ) \
257+
|| \
258+
( -n $line_flag_multiple && \
259+
(( STUB_RUNCOUNTS[no_order_command_index] < 1 )) ) ]]
260+
then
261+
STUB_RESULT=1
262+
fi
263+
264+
done < "$STUB_PLAN"
265+
266+
if [[ $STUB_RESULT -ne 0 ]]; then
88267
{
89-
echo "index: $index; stub index: ${!_STUB_INDEX}"
90268
echo "plan:"
91-
cat "${!_STUB_PLAN}" || true
92-
echo "run:"
93-
cat "${!_STUB_RUN}" || true
269+
cat "$STUB_PLAN" || true
94270
echo "log:"
95-
cat "${!_STUB_LOG}" || true
271+
cat "$STUB_LOG" || true
96272
} >&2
97273
fi
98274

99275
# Clean up the run file.
100-
rm -f "${!_STUB_RUN}"
101-
rm -f "${!_STUB_LOG}"
102-
103-
# Return the result.
104-
exit "${!_STUB_RESULT}"
276+
rm -f "$STUB_RUN"
277+
rm -f "$STUB_LOG"
105278

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

120282
fi

0 commit comments

Comments
 (0)