Skip to content

Commit

Permalink
Merge pull request #663 from qec-pconner/addOutputProcessor
Browse files Browse the repository at this point in the history
Adding `bats_pipe` Helper Function (originally suggested adding `--output-processor` Flag to `run` Helper).
  • Loading branch information
martin-schulze-vireso committed Jul 13, 2023
2 parents f4552f1 + 5752471 commit 60abfaf
Show file tree
Hide file tree
Showing 4 changed files with 1,603 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog][kac] and this project adheres to
* add tests for `--formatter cat` (#710)
* test coverage in CI (#718)
* Support for [rush](https://github.com/shenwei356/rush) as alternative to GNU parallel (#729)
* add `bats_pipe` helper function for `run` that executes `\|` as pipes (#663)

### Documentation

Expand Down
121 changes: 121 additions & 0 deletions lib/bats-core/test_functions.bash
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,127 @@ bats_separate_lines() { # <output-array> <input-var>
fi
}

bats_pipe() { # [-N] [--] command0 [ \| command1 [ \| command2 [...]]]
# This will run each command given, piping them appropriately.
# Meant to be used in combination with `run` helper to allow piped commands
# to be used.
# Note that `\|` must be used, not `|`.
# By default, the exit code of this command will be the last failure in the
# chain of piped commands (similar to `set -o pipefail`).
# Supplying -N (e.g. -0) will instead always use the exit code of the command
# at that position in the chain.
# --returned-status=N could be used as an alternative to -N. This also allows
# for negative values (which count from the end in reverse order).

local pipestatus_position=

# parse options starting with -
while [[ $# -gt 0 ]] && [[ $1 == -* ]]; do
case "$1" in
-[0-9]*)
pipestatus_position="${1#-}"
;;
--returned-status*)
if [ "$1" = "--returned-status" ]; then
pipestatus_position="$2"
shift
elif [[ "$1" =~ ^--returned-status= ]]; then
pipestatus_position="${1#--returned-status=}"
else
printf "Usage error: unknown flag '%s'" "$1" >&2
return 1
fi
;;
--)
shift # eat the -- before breaking away
break
;;
*)
printf "Usage error: unknown flag '%s'" "$1" >&2
return 1
;;
esac
shift
done

# parse and validate arguments, escape as necessary
local -a commands_and_args=("$@")
local -a escaped_args=()
local -i pipe_count=0
local -i previous_pipe_index=-1
local -i index=0
for (( index = 0; index < $#; index++ )); do
local current_command_or_arg="${commands_and_args[$index]}"
local escaped_arg="$current_command_or_arg"
if [[ "$current_command_or_arg" != '|' ]]; then
# escape args to protect them when eval'd (e.g. if they contain whitespace).
printf -v escaped_arg "%q" "$current_command_or_arg"
elif [ "$current_command_or_arg" = "|" ]; then
if [ "$index" -eq 0 ]; then
printf "Usage error: Cannot have leading \`\\|\`.\n" >&2
return 1
fi
if (( (previous_pipe_index + 1) >= index )); then
printf "Usage error: Cannot have consecutive \`\\|\`. Found at argument position '%s'.\n" "$index" >&2
return 1
fi
(( ++pipe_count ))
previous_pipe_index="$index"
fi
escaped_args+=("$escaped_arg")
done

if (( (previous_pipe_index > 0) && (previous_pipe_index == ($# - 1)) )); then
printf "Usage error: Cannot have trailing \`\\|\`.\n" >&2
return 1
fi

if (( pipe_count == 0 )); then
# Don't allow for no pipes. This might be a typo in the test,
# e.g. `run bats_pipe command0 | command1`
# instead of `run bats_pipe command0 \| command1`
# Unfortunately, we can't catch `run bats_pipe command0 \| command1 | command2`.
# But this check is better than just allowing no pipes.
printf "Usage error: No \`\\|\`s found. Is this an error?\n" >&2
return 1
fi

# there will be pipe_count + 1 entries in PIPE_STATUS (pipe_count number of \|'s between each entry).
# valid indices are [-(pipe_count + 1), pipe_count]
if [ -n "$pipestatus_position" ] && (( (pipestatus_position > pipe_count) || (-pipestatus_position > (pipe_count + 1)) )); then
printf "Usage error: Too large of -N argument (or --returned-status) given. Argument value: '%s'.\n" "$pipestatus_position" >&2
return 1
fi

# run commands and return appropriate pipe status
local -a __bats_pipe_eval_pipe_status=()
eval "${escaped_args[@]}" '; __bats_pipe_eval_pipe_status=(${PIPESTATUS[@]})'

local result_status=
if [ -z "$pipestatus_position" ]; then
# if we are performing default "last failure" behavior,
# iterate backwards through pipe_status to find the last error.
result_status=0
for index in "${!__bats_pipe_eval_pipe_status[@]}"; do
# OSX bash doesn't support negative indexing.
local backward_iter_index="$((${#__bats_pipe_eval_pipe_status[@]} - index - 1))"
local status_at_backward_iter_index="${__bats_pipe_eval_pipe_status[$backward_iter_index]}"
if (( status_at_backward_iter_index != 0 )); then
result_status="$status_at_backward_iter_index"
break;
fi
done
elif (( pipestatus_position >= 0 )); then
result_status="${__bats_pipe_eval_pipe_status[$pipestatus_position]}"
else
# Must use positive values for some bash's (like OSX).
local backward_iter_index="$((${#__bats_pipe_eval_pipe_status[@]} + pipestatus_position))"
result_status="${__bats_pipe_eval_pipe_status[$backward_iter_index]}"
fi

return "$result_status"
}

run() { # [!|-N] [--keep-empty-lines] [--separate-stderr] [--] <command to run...>
# This has to be restored on exit from this function to avoid leaking our trap INT into surrounding code.
# Non zero exits won't restore under the assumption that they will fail the test before it can be aborted,
Expand Down
71 changes: 71 additions & 0 deletions man/bats.7.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,77 @@ All additional parameters to run should come before the command.
If you want to run a command that starts with `-`, prefix it with `--` to
prevent `run` from parsing it as an option.

THE BATS_PIPE HELPER
--------------

Usage: bats_pipe [OPTIONS] [--] <command0...> [ \| <command1...> [ \| <command2...> [...] ] ]
Options:
-<N> return the exit code from the <N>th command in the chain
of piped commands, instead of default behavior of "the last
non-zero status".

The bats_pipe helper command is meant to handle piping between commands. Its
main purpose is to aide the `run` helper command (which cannot handle pipes,
due to bash parsing priority). `run command0 | command1` will parse `|` before
`run`, which is commonly not intended by test authors.

Running `run bats_pipe command0 \| command1` will have the piped commands run
within the context of the `run` command, and thus have the output and status
variables properly contained within the normal `output` and `status` variables.

Note that this requires the usage of `\|`, not `|`. This is to avoid bash
parsing out `|` first, instead sending `\|` to the bats_pipe command for it to
parse and set up intended piping. Running bats_pipe with no instances of `\|`
will always fail; this is intended to catch typos (accidentally using `|`) by
the test author.

The bats_pipe command will also properly propagate exit status from the piped
commands. The default behavior mimics `set -o pipefail`, returning the status
of the last (rightmost) command that exits with a non-zero status. This ensures
that usage of pipes do not mask the exit statuses of earlier commands.

@test "invoking foo piped to bar" {
run bats_pipe foo \| bar
# asserting foo or bar would return 17 (from foo if bar returns 0).
[ "$status" -eq 17 ]
[ "$output" = "bar output." ]
}

Alternatively, if the test always cares about the status of a specific command,
the -<N> option can be given (e.g. -0) to always return the status of the
command of interest.

@test "invoking foo piped to bar always return foo status" {
run bats_pipe -0 foo \| bar
# status of bar is ignored, status is always from foo.
[ "$status" -eq 2 ]
[ "$output" = "bar output." ]
}

Similarly, --returned-status N (or --returned-status=N) can be used for similar
functionality. This option supports negative values, which always return the
of the command starting from the end and in reverse order.

@test "invoking foo piped to bar always return foo status" {
run bats_pipe --returned-status -2 foo \| bar
# status of bar is ignored, status is always from foo.
[ "$status" -eq 2 ]
[ "$output" = "bar output." ]
}

Piping of command output is especially helpful when the output needs to be
modified in some way (e.g. the command outputs binary data into stdout, which
cannot be stored as-is in an environment variable).

@test "invoking foo that returns binary data" {
run bats_pipe foo \| hexdump -v -e "1/1 \"0x%02X \""
[ "$status" -eq 17 ]
[[ "$output" =~ 0xDE\ 0xAD ]]
}

Any number of pipes can be used in conjunction to chain output between some set
of running commands.

THE LOAD COMMAND
----------------

Expand Down

0 comments on commit 60abfaf

Please sign in to comment.