11#! /usr/bin/env bash
2+ export PS4=' +($$:${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
23set -e
34
45status=0
56program=" ${0##*/ } "
67PROGRAM=" $( 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
120282fi
0 commit comments