Skip to content

Commit

Permalink
Merge pull request #11 from buildkite-plugins/flamefire_improvements
Browse files Browse the repository at this point in the history
Flamefire improvements + bugfix
  • Loading branch information
pzeballos authored Dec 30, 2022
2 parents 39d98e4 + 08f2155 commit f952747
Show file tree
Hide file tree
Showing 6 changed files with 560 additions and 79 deletions.
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,88 @@ If you want to verify that your stub was called with the correct arguments, you
}
```

### Accepting any (or no) arguments

Sometimes the argument is too complicated to determine in advance or it would make the stubbing really long and convoluted. In those cases you can use `\*` to ensure that an argument is given.

```bash
@test "send_message" {

stub grep \
'\* \* : echo OK' \
'\* \* : echo OK'

# matches because there are exactly 2 arguments
grep "$complicated_pattern" /home/user/file
# this does not because there are 3 arguments :(
grep -ri "$complicated_pattern" /home/user/file

}
```

If you do not care about the amount of arguments, not having any colons whatsoever means accepting any amount of arguments:

```bash
@test "send_message" {

stub grep \
'exit 0' \
'exit 1' \
'exit 2'

# Will match the first stub line and exit with code 0
grep "$complicated_pattern" /home/user/file

# Matches the second one and exits witch code 1
grep -E -i "$some_pattern" "$some_file"

# No arguments also match, the third one exits with code 2 :)
grep
```
If you want to ensure no arguments whatsoever, you add a single colon at the very beginning:
```bash
@test "send_message" {
# Note that a single colon at the start is interpreted as "no arguments"
stub cat ': echo "OK"'
! cat foo # `cat` stub fails as an argument was passed

# But don't forget the space!
stub cat':echo "OK"'
# Will accept any arguments and execute `:echo "OK"` -> Fails
!cat foo # command `:echo` not found

# If your command contains ' : ' just start with double-colon
stub cat '::echo "Hello : World"'
# Prints "Hello : World"
cat foo bar
```
### Incremental Stubbing
In some case it might be preferable to define the invocation plan incrementally to mirror the actual behavior of the program under test.
This can be done by invocing `stub` multiple times with the same command.
In case you want to to start with a new plan call `unstub` first.
```bash
# Function to test
function install() {
apt-get update
pt-add-repository -y myrepo
apt-get update
}
@test "test installation" {
stub apt-get "update : "
stub apt-add-repository "-y myrepo : "
stub apt-get "update : " # Appends to existing plan
run install
unstub apt-get # Verifies plan and removes all remaining files
stub apt-get "upgrade" # Start with a new plan
}
```
## Troubleshooting
It can be difficult to figure out why your mock has failed. You can enable debugging setting an environment variable called after the command being stubbed (all in underscore-separeted, uppercase) with the `STUB_DEBUG` suffix. The value of the variable needs to be a device or file descriptor where to redirect the debugging output. Recommended value is `3`, which should make the output compatible with tap's expectation but you can also use `/dev/tty`.
Expand Down
148 changes: 80 additions & 68 deletions binstub
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#!/usr/bin/env bash
set -e

# If stdin comes from a pipe, save its content for later
if ! [ -t 0 ]; then
input="$(cat)"
fi

status=0
# Assume failure of stubbed command
status=1
program="${0##*/}"
PROGRAM="$(echo "$program" | tr a-z- A-Z_)"
# shellcheck disable=SC2018,SC2019 # anything not A-Z0-9 will be _
# the "\n" is necessary to avoid adding a trailing _ to the name
PROGRAM="$(echo "${program}" | tr a-z A-Z | tr -C "A-Z0-9\n" '_')"

if [[ "$PROGRAM" =~ ^[^A-Z_]+(.*)$ ]]; then
# remove leading non A-Z_ characters to make a valid variable name
PROGRAM="${BASH_REMATCH[1]}"
fi

_STUB_PLAN="${PROGRAM}_STUB_PLAN"
_STUB_RUN="${PROGRAM}_STUB_RUN"
Expand All @@ -19,7 +22,7 @@ _STUB_DEBUG="${PROGRAM}_STUB_DEBUG"

debug() {
if [ -n "${!_STUB_DEBUG}" ] ; then
echo "bats-mock($program): $*" >&${!_STUB_DEBUG}
echo "bats-mock($program): $*" >&"${!_STUB_DEBUG}"
fi
}

Expand All @@ -29,87 +32,89 @@ debug() {
# Initialize or load the stub run information.
eval "${_STUB_INDEX}"=1
eval "${_STUB_RESULT}"=0
# shellcheck source=stub.bash
[ ! -e "${!_STUB_RUN}" ] || source "${!_STUB_RUN}"

if [ -z "${!_STUB_END}" ] && [ -n "${!_STUB_DEBUG}" ]; then
debug "got $program $*" >&${!_STUB_DEBUG}
if [ -z "${!_STUB_END}" ]; then
debug "got $program $*"
fi

# Loop over each line in the plan.
index=0
match_found=0
while IFS= read -r line; do
index=$((index + 1))

# if [ -n "${!_STUB_DEBUG}" ]; then
# echo "bats-mock: [idx $index, want ${!_STUB_INDEX}] $line" >&${!_STUB_DEBUG}
# fi
# debug "bats-mock: [idx $index, want ${!_STUB_INDEX}] $line"

if [ -z "${!_STUB_END}" ] && [ $index -eq "${!_STUB_INDEX}" ]; then
# We found the plan line we're interested in.
# Start off by assuming success.
result=0

# Split the line into an array of arguments to
# match and a command to run to produce output.
command=" $line"
if [ "$command" != "${command/ : }" ]; then
patterns="${command%% : *}"
command="${command#* : }"
fi
match_found=1

arguments=("$@")
parsed_patterns=()

# Parse patterns into tokens using eval to respect quoted
# strings. This is less than ideal, but the pattern input
# is also already eval'd elsewhere. At least this handles
# things like newlines (which xargs doesn't)
eval "parsed_patterns=(${patterns})"

debug "patterns [${#parsed_patterns[@]}] = $(printf "'%q' " "${parsed_patterns[@]}")"
debug "arguments [${#arguments[@]}] = $(printf "'%q' " "${arguments[@]}")"

# Match the expected argument patterns to actual
# arguments.
for (( i=0; i<${#parsed_patterns[@]}; i++ )); do
pattern="${parsed_patterns[$i]}"
argument="${arguments[$i]}"
# Split the line into an array of arguments to match and a command to run.
# If the line does not contain ' : ' and does not start with a colon
# then the call is assumed to match any arguments and execute the command.
# Special case: Lines starting with double colon are also handled that way
# and get the double colons removed
command=" $line"
if [[ "$line" == ::* ]]; then
command="${line#::}"
elif [ "$command" != "${command/ : }" ]; then
patterns="${command%% : *}"
command="${command#* : }"

if [[ "$pattern" != "$argument" ]] && [[ "$pattern" != "*" ]] ; then
debug "$(printf "match failed at idx %d, expected '%q', got '%q'" $i "$pattern" "$argument")"
result=1
break
parsed_patterns=()

# Parse patterns into tokens using eval to respect quoted
# strings. This is less than ideal, but the pattern input
# is also already eval'd elsewhere. At least this handles
# things like newlines (which xargs doesn't)
origFlags="$-"
set -f
eval "parsed_patterns=(${patterns})"
set "-$origFlags"

debug "patterns [${#parsed_patterns[@]}] = $(printf "'%q' " "${parsed_patterns[@]}")"

# Match the expected argument patterns to actual
# arguments.
for (( i=0; i<${#parsed_patterns[@]}; i++ )); do
pattern="${parsed_patterns[$i]}"
argument="${arguments[$i]}"

if [ "$pattern" = '*' ]; then
continue
fi

case "$argument" in
# uncomment this line for partial pattern matching
# will break existing * matching and generate a shellcheck warning
# $pattern ) ;;
"$pattern" ) ;;
* ) debug "$(printf "match failed at idx %d, expected '%q', got '%q'" $i "$pattern" "$argument")"
match_found=2
break ;;
esac
done

# Check if there are unmatched arguments
if [[ ${#arguments[@]} -gt ${#parsed_patterns[@]} ]] ; then
idx="${#parsed_patterns[@]}"
argument="${arguments[$idx]}"
debug "$(printf "unexpected argument '%q' at idx %d" "$argument" "$idx")"
match_found=3
fi
done

# Check if there are unmatched arguments
if [[ ${#arguments[@]} -gt ${#parsed_patterns[@]} ]] ; then
idx="${#parsed_patterns[@]}"
argument="${arguments[$idx]}"
debug "$(printf "unexpected argument '%q' at idx %d" "$argument" "$idx")"
result=2
break
fi

# If the arguments matched, evaluate the command
# in a subshell. Otherwise, log the failure.
if [ $result -eq 0 ] ; then
debug "running $command"
debug "command input is $input"
set +e
( eval "$command" <<< "$input" )
status="$?"
debug "command result was $status"
set -e
else
eval "${_STUB_RESULT}"=1
fi
break
fi
done < "${!_STUB_PLAN}"


if [ -n "${!_STUB_END}" ]; then
echo "${_STUB_DEBUG}"
debug "unstubbing"

if [ ! -f "${!_STUB_RUN}" ] && [ -n "${!_STUB_DEBUG}" ] ; then
Expand All @@ -118,7 +123,7 @@ if [ -n "${!_STUB_END}" ]; then
fi

# Clean up the run file.
rm -f "${!_STUB_RUN}"
"$BATS_MOCK_REAL_rm" -f "${!_STUB_RUN}"

# If the number of lines in the plan is larger than
# the requested index, we failed.
Expand All @@ -129,9 +134,16 @@ if [ -n "${!_STUB_END}" ]; then
# Return the result.
exit "${!_STUB_RESULT}"
else
# If the requested index is larger than the number
# of lines in the plan file, we failed.
if [ "${!_STUB_INDEX}" -gt $index ]; then
# If the arguments matched, evaluate the command
# in a subshell. Otherwise, log the failure.
if [ $match_found -eq 1 ] ; then
debug "running $command"
set +e
( eval "$command" )
status="$?"
debug "command result was $status"
set -e
else
debug "no plan row found"
eval "${_STUB_RESULT}"=1
fi
Expand Down
53 changes: 44 additions & 9 deletions stub.bash
Original file line number Diff line number Diff line change
@@ -1,36 +1,71 @@
BATS_MOCK_TMPDIR="${BATS_TMPDIR}"
BATS_MOCK_BINDIR="${BATS_MOCK_TMPDIR}/bin"

BATS_MOCK_REAL_mkdir=$(which mkdir)
export BATS_MOCK_REAL_mkdir
BATS_MOCK_REAL_ln=$(which ln)
export BATS_MOCK_REAL_ln
BATS_MOCK_REAL_touch=$(which touch)
export BATS_MOCK_REAL_touch
BATS_MOCK_REAL_rm=$(which rm)
export BATS_MOCK_REAL_rm

PATH="$BATS_MOCK_BINDIR:$PATH"

stub() {
local program="$1"
local prefix="$(echo "$program" | tr a-z- A-Z_)"
local prefix
# shellcheck disable=SC2018,SC2019 # anything not A-Z0-9 will be _
# the "\n" is necessary to avoid adding a trailing _ to the name
prefix="$(echo "$program" | tr a-z A-Z | tr -C "A-Z0-9\n" '_')"
shift


if [[ "$prefix" =~ ^[^A-Z_]+(.*)$ ]]; then
# remove leading non A-Z_ characters to make a valid variable name
prefix="${BASH_REMATCH[1]}"
fi

export "${prefix}_STUB_PLAN"="${BATS_MOCK_TMPDIR}/${program}-stub-plan"
export "${prefix}_STUB_RUN"="${BATS_MOCK_TMPDIR}/${program}-stub-run"
export "${prefix}_STUB_END"=

mkdir -p "${BATS_MOCK_BINDIR}"
ln -sf "${BASH_SOURCE[0]%stub.bash}binstub" "${BATS_MOCK_BINDIR}/${program}"
"$BATS_MOCK_REAL_mkdir" -p "${BATS_MOCK_BINDIR}"
"$BATS_MOCK_REAL_ln" -sf "${BASH_SOURCE[0]%stub.bash}binstub" "${BATS_MOCK_BINDIR}/${program}"

rm -f "${BATS_MOCK_TMPDIR}/${program}-stub-plan" "${BATS_MOCK_TMPDIR}/${program}-stub-run"
touch "${BATS_MOCK_TMPDIR}/${program}-stub-plan"
"$BATS_MOCK_REAL_touch" "${BATS_MOCK_TMPDIR}/${program}-stub-plan"
for arg in "$@"; do printf "%s\n" "$arg" >> "${BATS_MOCK_TMPDIR}/${program}-stub-plan"; done
}

unstub() {
local allow_missing=0
if [ "$1" == "--allow-missing" ]; then
allow_missing=1
shift
fi
local program="$1"
local prefix="$(echo "$program" | tr a-z- A-Z_)"
local path="${BATS_MOCK_BINDIR}/${program}"
local prefix
# shellcheck disable=SC2018,SC2019 # anything not A-Z0-9 will be _
# the "\n" is necessary to avoid adding a trailing _ to the name
prefix="$(echo "${program}" | tr a-z A-Z | tr -C "A-Z0-9\n" '_')"

if [[ "$prefix" =~ ^[^A-Z_]+(.*)$ ]]; then
# remove leading non A-Z_ characters to make a valid variable name
prefix="${BASH_REMATCH[1]}"
fi

export "${prefix}_STUB_END"=1

local STATUS=0
"$path" || STATUS="$?"
if [ -f "$path" ]; then
"$path" || STATUS="$?"
elif [ $allow_missing -eq 0 ]; then
echo "$program is not stubbed" >&2
STATUS=1
fi

rm -f "$path"
rm -f "${BATS_MOCK_TMPDIR}/${program}-stub-plan" "${BATS_MOCK_TMPDIR}/${program}-stub-run"
"$BATS_MOCK_REAL_rm" -f "$path"
"$BATS_MOCK_REAL_rm" -f "${BATS_MOCK_TMPDIR}/${program}-stub-plan" "${BATS_MOCK_TMPDIR}/${program}-stub-run"
return "$STATUS"
}
Loading

0 comments on commit f952747

Please sign in to comment.