11#! /usr/bin/env bash
2+ export PS4=' +($$:${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
23set -e
34
45status=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+
1751if 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')
81222if [ -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-
117277fi
0 commit comments