#!/usr/bin/env bash export PS4='+($$:${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' set -e status=0 program="${0##*/}" PROGRAM="$(echo "$program" | tr a-z- A-Z_)" [ -n "$TMPDIR" ] || TMPDIR="/tmp" STUB_PLAN="${PROGRAM}_STUB_PLAN" STUB_PLAN="${!STUB_PLAN}" STUB_RUN="${PROGRAM}_STUB_RUN" STUB_RUN="${!STUB_RUN:-${TMPDIR}/${program}-stub-run}" STUB_INDEX= STUB_RESULT= STUB_END="${PROGRAM}_STUB_END" STUB_END="${!STUB_END}" STUB_LOG="${PROGRAM}_STUB_LOG" STUB_LOG="${!STUB_LOG:-${TMPDIR}/${program}-stub-log}" STUB_LOCKFILE="${TMPDIR}/${program}-stub.lock" release_lock() { rm -f "$STUB_LOCKFILE" trap - EXIT } acquire_lock() { local start=$SECONDS local acquire_timeout=10 local acquired= while (( SECONDS <= start + $acquire_timeout )); do ( set -o noclobber; echo -n >"$STUB_LOCKFILE" ) 2>/dev/null && acquired=1 if [[ -n $acquired ]]; then trap release_lock EXIT break else # POSIX sleep(1) doesn't provide subsecond precision, but many others do sleep 0.1 2>/dev/null || sleep 1 fi done if [[ -z $acquired ]]; then echo "$0: error: could not acquire stub lock \`$STUB_LOCKFILE' in ${acquire_timeout} seconds" >&2 exit 2 fi } acquire_lock if [[ -z $STUB_END ]]; then echo "$program" "$@" >>"$STUB_LOG"; fi [[ -e $STUB_PLAN ]] || exit 1 # Initialize or load the stub run information. read_runfile() { if [[ -e $STUB_RUN ]]; then source "$STUB_RUN"; fi } write_runfile() { { local i echo "STUB_INDEX=$STUB_INDEX" echo "STUB_RESULT=$STUB_RESULT" echo "STUB_RUNCOUNTS=()" for i in ${!STUB_RUNCOUNTS[@]}; do echo "STUB_RUNCOUNTS[$i]=${STUB_RUNCOUNTS[$i]}" done } > "$STUB_RUN" } update_runfile_index() { ( STUB_INDEX=$((STUB_INDEX + 1)) write_runfile ) } update_runfile_result() { ( # Another stubs may have run while we were running payload # So we need to merge possible state changes local our_result="$STUB_RESULT" local -a our_runcounts array_copy STUB_RUNCOUNTS our_runcounts read_runfile # merge our match_result and their match_result, with failure taking precedence STUB_RESULT=$(( our_result | STUB_RESULT )) # 3-way merge STUB_RUNCOUNTS (their changes), # our_runcounts (our changes) and initial_runcounts (base) local i for i in $(printf '%s\n' ${!STUB_RUNCOUNTS[@]} ${!our_runcounts[@]} | sort -u); do STUB_RUNCOUNTS[$i]=$((STUB_RUNCOUNTS[i] + our_runcounts[i] - initial_runcounts[i])) done write_runfile ) } array_copy() { #`declare -p' is supposed to produce "declare -a src=([index]="value" )" local data="$(declare -p ${1:?})" local dest="${2:?}" # Bash 5 dumps empty arrays as "declare -a arr" if [[ $data != *=* ]]; then data="()"; else data="${data#*=}" fi # Bash 3 and MacPorts version of Bash 5 dump arrays in single quotes "declare -a arr='()'" # but arr='()' createss "([0]='')" rather than duplicate the array if [[ ${data:0:1} == "'" && ${data:${#data}-1:1} == "'" ]]; then data="${data:1:${#data}-2}" fi eval "$dest=$data" } STUB_INDEX=1 STUB_RESULT=0 declare -a STUB_RUNCOUNTS read_runfile declare -a initial_runcounts array_copy STUB_RUNCOUNTS initial_runcounts # ${PROGRAM}_STUB_END envvar is set externally to trigger verification mode for `unstub' # Execution mode if [[ -z $STUB_END ]]; then # Loop over each line in the plan. regular_command_index=0 no_order_command_index=0 match_result=1 while IFS= read -r line; do line_flags="${line%% *}" line="${line#${line_flags} }" line_flag_no_order="$(if [[ $line_flags == N ]]; then echo 1; fi)" line_flag_multiple="$(if [[ $line_flags == M ]]; then echo 1; fi)" line_flag_regular="$(if [[ $line_flags == - ]]; then echo 1; fi)" unset line_flags # Go through the plan until a match is found. # For regular commands, only check the next command by index. # Also keep track of no-order commands for the purpose of run count tracking if [[ -n $line_flag_regular ]]; then regular_command_index=$(($regular_command_index + 1)) if [[ $regular_command_index -ne $STUB_INDEX ]]; then continue; fi else no_order_command_index=$(($no_order_command_index + 1)) fi # Split the line into an array of arguments to # match and a command to run to produce output. command=" $line" if [[ $command == *" : "* ]]; then patterns="${command%% : *}" command="${command#* : }" fi # Naively split patterns by whitespace for now. # In the future, use a sed script to split while # respecting quoting. set -f patterns=($patterns) set +f arguments=("$@") # Match the expected argument patterns to actual # arguments. match_result=0 for (( i=0; i<${#patterns[@]}; i++ )); do pattern="${patterns[$i]}" argument="${arguments[$i]}" case "$argument" in $pattern ) ;; * ) match_result=1 ;; esac done # If the arguments matched, evaluate the command # in a subshell. Otherwise, log the failure. if [ $match_result -eq 0 ] ; then # If this is a regular command, push the regular command index for the next stub invocation if [[ -n $line_flag_regular ]]; then update_runfile_index else STUB_RUNCOUNTS[$no_order_command_index]=$((STUB_RUNCOUNTS[no_order_command_index]+1)) fi # Release the lock while running the payload to allow another `stub' # of the same program to run concurrently (e.g. in a pipeline). release_lock ( eval "$command" ) && status="$?" || status="$?" break fi done < "$STUB_PLAN" #If we never matched anything, we failed. if [[ $match_result -eq 1 ]]; then STUB_RESULT=1 #This also means that we never released the lock # before running the payload else acquire_lock fi # Write out the match_result information. update_runfile_result release_lock exit "$status" fi # Verification mode (`unstub') if [[ -n $STUB_END ]]; then # `unstub' is supposed to run after any stubs are finished release_lock # If the number of regular commands in the plan is larger than # the final regular_command_index, we failed. if [[ $(grep -Ee '^-' "$STUB_PLAN" | wc -l ) -ge $STUB_INDEX ]]; then STUB_RESULT=1 fi # If no-order commands weren't executed exactly once # and multiple-times commands at least once, we failed. no_order_command_index=0 while IFS= read -r line; do line_flags="${line%% *}" line="${line#${line_flags} }" line_flag_no_order="$(if [[ $line_flags == N ]]; then echo 1; fi)" line_flag_multiple="$(if [[ $line_flags == M ]]; then echo 1; fi)" line_flag_regular="$(if [[ $line_flags == - ]]; then echo 1; fi)" unset line_flags if [[ -z $line_flag_regular ]]; then continue fi no_order_command_index=$((no_order_command_index + 1)) if [[ ( -n $line_flag_no_order && \ (( STUB_RUNCOUNTS[no_order_command_index] != 1 )) ) \ || \ ( -n $line_flag_multiple && \ (( STUB_RUNCOUNTS[no_order_command_index] < 1 )) ) ]] then STUB_RESULT=1 fi done < "$STUB_PLAN" if [[ $STUB_RESULT -ne 0 ]]; then { echo "plan:" cat "$STUB_PLAN" || true echo "log:" cat "$STUB_LOG" || true } >&2 fi # Clean up the run file. rm -f "$STUB_RUN" rm -f "$STUB_LOG" # Return the run result. exit "$STUB_RESULT" fi