Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
588 lines (540 sloc) 17.1 KB
#! /bin/bash
#
# Assertions for Bats tests
#
# These functions provide detailed output for assertion failures, which is
# especially helpful when running as part of a continuous integration suite.
# Compare the output from the typical `[ "$output" == 'bar' ]` statement:
#
# ✗ actual output matches expected
# (in test file test.bats, line 7)
# `[ "$output" == 'bar' ]' failed
#
# with that from `assert_output 'bar'`, which shows the `$output` that failed:
#
# ✗ actual output matches expected
# (in test file test.bats, line 7)
# `assert_output 'bar'' failed
# output not equal to expected value:
# expected: 'bar'
# actual: 'foo'
#
# These assertions borrow inspiration from rbenv/test/test_helper.bash.
#
# Usage:
# -----
# The recommended way to make these assertions available is to create an
# 'environment.bash' file in the top-level test directory containing the
# following line:
#
# . "path/to/bats/assertions"
#
# Then have each Bats test file load the environment file. This environment file
# can contain any other custom helper functions or assertions to fit your
# project.
#
# If none of the assertions suit your needs (including their negations provided
# by `fail_if`), you can use the `fail` function to provide a custom error
# message.
#
# Defining new assertions:
# -----------------------
# Alternatively, write your own assertion function with the following as the
# first line:
#
# set "$DISABLE_BATS_SHELL_OPTIONS"
#
# and then make sure every return path ends with a direct call to the following
# (not delegated to a helper function, and followed by a `return` statement if
# not explicitly passed an error status and not at the end of the function):
#
# restore_bats_shell_options "$return_status"
#
# These two steps ensure that your assertion will pinpoint the line in the test
# case at which it was called, and that it may be reused to compose new
# assertions. For the deep technical details, see the function comment for
# `restore_bats_shell_options` from `lib/bats/helper-function`.
#
# Assertions should generally follow the pattern:
#
# assert_some_condition() {
# set "$DISABLE_BATS_SHELL_OPTIONS"
# if [[ "$actual" != "$expected" ]]; then
# printf "Something's wrong:\n expected: '%s'\n actual: '%s'\n" \
# "$expected" "$actual" >&2
# restore_bats_shell_options '1'
# else
# restore_bats_shell_options
# fi
# }
#
# Assertions that wrap a single existing assertion should follow the pattern:
#
# assert_with_more_context() {
# set "$DISABLE_BATS_SHELL_OPTIONS"
# # ...set up context...
# assert_using_an_existing_assertion "$with_more_context"
# restore_bats_shell_options "$?"
# }
#
# For assertions that check multiple error conditions before exiting:
#
# assert_some_stuff() {
# set "$DISABLE_BATS_SHELL_OPTIONS"
# local num_errors=0
#
# # ...check conditions, print errors, increment num_errors...
#
# if [[ "$num_errors" -ne '0' ]]; then
# restore_bats_shell_options '1'
# else
# restore_bats_shell_options
# fi
# }
#
# Also, if your assertion contains a `for` or `while` loop, you may wish to
# delegate to a helper function for efficiency; see the header comments from
# `lib/bats/helper-function` for an explanation:
#
# assert_something_involving_a_file() {
# set "$DISABLE_BATS_SHELL_OPTIONS"
# __assert_something_involving_a_file "$@"
# restore_bats_shell_options "$?"
# }
#
# __assert_something_involving_a_file() {
# local filename="$1"
# local line
# while IFS= read -r line; do
# line="${line%$'\r'}"
# assert_something_on_a_file_line "$filename" "$line"
# done <"$filename"
# }
. "${BASH_SOURCE%/*}/helper-function"
# Unconditionally returns a failing status
#
# Will print an optional failure reason, the Bats 'run' command exit status, and
# the output from the 'run' command, all to standard error.
#
# Globals:
# bats_fail_no_output: If nonempty, will not print `status` and `output`
#
# Arguments:
# $1: (optional) Reason to include in the failure output
fail() {
set "$DISABLE_BATS_SHELL_OPTIONS"
local reason="$1"
if [[ -n "$reason" ]]; then
printf '%b\n' "$reason" >&2
fi
if [[ -z "$bats_fail_no_output" ]]; then
printf 'STATUS: %d\nOUTPUT:\n%b\n' "$status" "$output" >&2
fi
restore_bats_shell_options '1'
}
# Negates the expected outcome of an assertion from this file.
#
# The first argument should be the name of an assertion from this file _without_
# the `assert_` prefix. For example:
#
# fail_if equal 'foo' 'bar' "Some values we don't expect to be equal"
#
# This is essentially the same as the following, but `fail_if` provides more
# context for the failure:
#
# ! assert_equal 'foo' 'bar' "Some values we don't expect to be equal"
#
# Arguments:
# assertion: The name of the assertion to negate minus the `_assert_` prefix
# ...: The arguments to the assertion being negated
fail_if() {
set "$DISABLE_BATS_SHELL_OPTIONS"
local assertion="assert_${1}"
shift
local label
local value
local operation='equal'
local constraints=()
local constraint
local i
local bats_fail_no_output=''
if [[ "$assertion" =~ _match ]]; then
operation='match'
fi
case "$assertion" in
assert_equal|assert_matches)
bats_fail_no_output='true'
label="${3:-value}"
constraints=("$1")
value="$2"
;;
assert_output*|assert_status)
bats_fail_no_output='true'
label="${assertion#assert_}"
label="${label%_*}"
constraints=("$@")
value="$output"
;;
assert_line_*)
label="line $1"
constraints=("$2")
value="${lines[$1]}"
;;
assert_lines_*)
label="lines"
constraints=("${@}")
;;
assert_file_*)
label="'$1'"
constraints=("${@:2}")
;;
*)
printf "Unknown assertion: '%s'\n" "$assertion" >&2
restore_bats_shell_options '1'
return
esac
if ! "$assertion" "$@" &>/dev/null; then
restore_bats_shell_options
return
fi
for ((i=0; i != ${#constraints[@]}; ++i)); do
constraint+=$'\n'" '${constraints[$i]}'"
done
if [[ "$operation" == 'match' && -n "$value" ]]; then
value=$'\nValue:\n'" '$value'"
else
value=
fi
fail "Expected $label not to $operation:$constraint$value"
restore_bats_shell_options "$?"
}
# Compares two values for equality
#
# Arguments:
# expected: The expected value
# actual: The actual value to evaluate
# label: (Optional) A label explaining the value being evaluated
assert_equal() {
set "$DISABLE_BATS_SHELL_OPTIONS"
local expected="$1"
local actual="$2"
local label="${3:-Actual value}"
if [[ "$expected" != "$actual" ]]; then
printf '%s not equal to expected value:\n %s\n %s\n' \
"$label" "expected: '$expected'" "actual: '$actual'" >&2
restore_bats_shell_options '1'
else
restore_bats_shell_options
fi
}
# Validates whether a value matches a regular expression
#
# Arguments:
# pattern: The regular expression to match against the value
# value: The value to match
# label: (Optional) A label explaining the value being matched
assert_matches() {
set "$DISABLE_BATS_SHELL_OPTIONS"
local pattern="$1"
local value="$2"
local label="${3:-Value}"
if [[ ! "$value" =~ $pattern ]]; then
printf '%s does not match expected pattern:\n %s\n %s\n' \
"$label" "pattern: '$pattern'" "value: '$value'" >&2
restore_bats_shell_options '1'
else
restore_bats_shell_options
fi
}
# Validates that the Bats `output` value is equal to the expected value
#
# Will join multiple arguments using a newline character to check a multiline
# value for equality. This is suggested only for short `output` values, however.
# For longer values, use `assert_lines_equal` or `assert_lines_match`, possibly
# in combination with `split_bats_output_into_lines` from `lib/bats/helpers`.
#
# Arguments:
# ...: Lines comprising the expected value for `output`
assert_output() {
set "$DISABLE_BATS_SHELL_OPTIONS"
local expected
local origIFS="$IFS"
local IFS=$'\n'
printf -v 'expected' -- '%s' "$*"
origIFS="$IFS"
assert_equal "$expected" "$output" 'output'
restore_bats_shell_options "$?"
}
# Validates that the Bats $output value matches a regular expression
#
# Arguments:
# $1: The regular expression to match against $output
assert_output_matches() {
set "$DISABLE_BATS_SHELL_OPTIONS"
local pattern="$1"
if [[ "$#" -ne 1 ]]; then
printf 'ERROR: %s takes exactly one argument\n' "${FUNCNAME[0]}" >&2
restore_bats_shell_options '1'
else
assert_matches "$pattern" "$output" 'output'
restore_bats_shell_options "$?"
fi
}
# Validates that the Bats $status value is equal to the expected value
#
# Arguments:
# $1: The expected value for $status
assert_status() {
set "$DISABLE_BATS_SHELL_OPTIONS"
assert_equal "$1" "$status" "exit status"
restore_bats_shell_options "$?"
}
# Validates that `run` returned success and `output` equals the expected value
#
# Arguments:
# ...: (Optional) Lines comprising the expected value for `output`
assert_success() {
set "$DISABLE_BATS_SHELL_OPTIONS"
if [[ "$status" -ne '0' ]]; then
printf 'expected success, but command failed\n' >&2
fail
elif [[ "$#" -ne 0 ]]; then
assert_output "$@"
fi
restore_bats_shell_options "$?"
}
# Validates that `run` returned an error and `output` equals the expected value
#
# Arguments:
# ...: (Optional) Lines comprising the expected value for `output`
assert_failure() {
set "$DISABLE_BATS_SHELL_OPTIONS"
if [[ "$status" -eq '0' ]]; then
printf 'expected failure, but command succeeded\n' >&2
fail
elif [[ "$#" -ne 0 ]]; then
assert_output "$@"
fi
restore_bats_shell_options "$?"
}
# Validates that a specific line from $line equals the expected value
#
# Arguments:
# $1: The index into $line identifying the line to evaluate
# $2: The expected value for ${line[$1]}
assert_line_equals() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__assert_line 'assert_equal' "$@"
restore_bats_shell_options "$?"
}
# Validates that a specific line from $line matches the expected value
#
# Arguments:
# $1: The index into $line identifying the line to match
# $2: The regular expression to match against ${line[$1]}
assert_line_matches() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__assert_line 'assert_matches' "$@"
restore_bats_shell_options "$?"
}
# Validates that each output line equals each corresponding argument
#
# Also ensures there are no more and no fewer lines of output than expected. If
# `output` should contain blank lines, call `split_bats_output_into_lines` from
# `lib/bats/helpers` before this.
#
# If you expect zero lines, then don't supply any arguments.
#
# Arguments:
# $@: Values to compare to each element of `${lines[@]}` for equality
assert_lines_equal() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__assert_lines 'assert_equal' "$@"
restore_bats_shell_options "$?"
}
# Validates that each output line matches each corresponding argument
#
# Also ensures there are no more and no fewer lines of output than expected. If
# `output` should contain blank lines, call `split_bats_output_into_lines` from
# `lib/bats/helpers` before this.
#
# Arguments:
# $@: Values to compare to each element of `${lines[@]}` for equality
assert_lines_match() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__assert_lines 'assert_matches' "$@"
restore_bats_shell_options "$?"
}
# Validates that a file contains exactly the specified output
#
# NOTE: If the file doesn't end with a newline, the last line will not be
# present. To check that a file is completely empty, supply only the `file_path`
# argument.
#
# Arguments:
# file_path: Path to file to evaluate
# ...: Exact lines expected to appear in the file
assert_file_equals() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__assert_file 'assert_lines_equal' "$@"
restore_bats_shell_options "$?"
}
# Validates that a file matches a single regular expression
#
# Arguments:
# file_path: Path to the file to examine
# pattern: Regular expression used to validate the contents of the file
assert_file_matches() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__assert_file 'assert_matches' "$@"
restore_bats_shell_options "$?"
}
# Validates that every line in a file matches a corresponding regular expression
#
# Arguments:
# file_path: Path to the file to examine
# ...: Regular expressions used to validate each line of the file
assert_file_lines_match() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__assert_file 'assert_lines_match' "$@"
restore_bats_shell_options "$?"
}
# Sets the `output` and `lines` variables to the contents of a file.
#
# This differs from `run cat $file` or similar in that it automatically strips
# `\r` characters from files produced on Windows systems and preserves empty
# lines.
#
# Normally you should use one of the `assert_file_*` assertions, which rely on
# this function; but if you wish to examine specific output lines without the
# regard to the rest (such as the first or last lines), or search for several
# regular expressions in no particular order, this function may help.
#
# NOTE: If the file doesn't end with a newline, the last line will not be
# present. If the file is completely empty, `lines` will contain zero elements.
#
# Arguments:
# file_path: Path to file from which `output` and `lines` will be filled
set_bats_output_and_lines_from_file() {
set "$DISABLE_BATS_SHELL_OPTIONS"
__set_bats_output_and_lines_from_file "$@"
restore_bats_shell_options "$?"
}
# --------------------------------
# IMPLEMENTATION - HERE BE DRAGONS
#
# None of the functions below this line are part of the public interface.
# --------------------------------
# Common implementation for assertions that evaluate a single `$lines` element
#
# Arguments:
# assertion: The assertion to execute
# lineno: The index into $lines identifying the line to evaluate
# constraint: The assertion constraint used to evaluate ${lines[$lineno]}
__assert_line() {
local assertion="$1"
local lineno="$2"
local constraint="$3"
# Implement negative indices for Bash 3.x.
if [[ "${lineno:0:1}" == '-' ]]; then
lineno="$((${#lines[@]} - ${lineno:1}))"
fi
if ! "$assertion" "$constraint" "${lines[$lineno]}" "line $lineno"; then
if [[ -z "$__bats_assert_line_suppress_output" ]]; then
printf 'OUTPUT:\n%s\n' "$output" >&2
fi
return '1'
fi
}
# Common implementation for assertions that evaluate every element of `$lines`
#
# Arguments:
# assertion: The assertion to execute
# ...: Assertion constraints for each corresponding element of $lines
__assert_lines() {
local assertion="$1"
shift
local expected=("$@")
local num_lines="${#expected[@]}"
local lines_diff="$((${#lines[@]} - num_lines))"
local __bats_assert_line_suppress_output='true'
local num_errors=0
local i
for ((i=0; i != ${#expected[@]}; ++i)); do
if ! __assert_line "$assertion" "$i" "${expected[$i]}"; then
((++num_errors))
fi
done
if [[ "$lines_diff" -gt '0' ]]; then
if [[ "$lines_diff" -eq '1' ]]; then
printf 'There is one more line of output than expected:\n' >&2
else
printf 'There are %d more lines of output than expected:\n' \
"$lines_diff" >&2
fi
printf '%s\n' "${lines[@]:$num_lines}" >&2
((++num_errors))
elif [[ "$lines_diff" -lt '0' ]]; then
lines_diff="$((-lines_diff))"
if [[ "$lines_diff" -eq '1' ]]; then
printf 'There is one fewer line of output than expected.\n' >&2
else
printf 'There are %d fewer lines of output than expected.\n' \
"$lines_diff" >&2
fi
((++num_errors))
fi
if [[ "$num_errors" -ne '0' ]]; then
printf 'OUTPUT:\n%s\n' "$output" >&2
return '1'
fi
}
# Common implementation for assertions that evaluate a file's contents
#
# Arguments:
# assertion: The assertion to execute
# file_path: Path to file to evaluate
# ...: Assertion constraints for the contents of `file_path`
__assert_file() {
local assertion="$1"
local file_path="$2"
shift 2
local constraints=("$@")
if ! set_bats_output_and_lines_from_file "$file_path"; then
return '1'
fi
if [[ "$assertion" == 'assert_matches' ]]; then
if [[ "$#" -ne '1' ]]; then
printf 'ERROR: %s takes exactly two arguments\n' "${FUNCNAME[1]}" >&2
return '1'
fi
constraints=("$1" "$output" "The content of '$file_path'")
fi
"$assertion" "${constraints[@]}"
}
# Implementation for `set_bats_output_and_lines_from_file`
#
# Arguments:
# file_path: Path to file from which `output` and `lines` will be filled
__set_bats_output_and_lines_from_file() {
local file_path="$1"
if [[ ! -f "$file_path" ]]; then
printf "'%s' doesn't exist or isn't a regular file.\n" "$file_path" >&2
return 1
elif [[ ! -r "$file_path" ]]; then
printf "You don't have permission to access '%s'.\n" "$file_path" >&2
return 1
fi
lines=()
output=''
# This loop preserves leading and trailing blank lines. We need to chomp the
# last newline off of `output` though, to make it consistent with the
# conventional `output` format.
while IFS= read -r line; do
line="${line%$'\r'}"
lines+=("$line")
output+="$line"$'\n'
done <"$file_path"
output="${output%$'\n'}"
restore_bats_shell_options
}