2014-05-09 00:46:18 -04:00
|
|
|
#!/bin/bash
|
|
|
|
# Specification language
|
|
|
|
#
|
2017-04-19 01:52:19 -04:00
|
|
|
# Copyright (C) 2014, 2017 Mike Gerwitz
|
2014-05-09 00:46:18 -04:00
|
|
|
#
|
|
|
|
# This file is part of shspec.
|
|
|
|
#
|
|
|
|
# shspec is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
# Upon examining this code, you will likely notice that it is not
|
|
|
|
# re-entrant. This is okay, because the intended use is to invoke a new
|
|
|
|
# process *per specification*, not to run multiple specifications
|
|
|
|
# concurrently within the same process.
|
|
|
|
#
|
|
|
|
# You've been warned.
|
|
|
|
##
|
|
|
|
|
|
|
|
[ -z $__INC_SPEC ] || return
|
|
|
|
__INC_SPEC=1
|
|
|
|
|
2014-06-10 22:00:18 -04:00
|
|
|
source specstack.sh
|
|
|
|
source expect.sh
|
2014-05-09 00:46:18 -04:00
|
|
|
|
2014-05-12 23:51:08 -04:00
|
|
|
# number of internal arguments before remainder clause
|
2014-06-10 01:35:27 -04:00
|
|
|
declare -ir __SHIFTN=4
|
2014-05-12 23:51:08 -04:00
|
|
|
|
2014-05-09 00:46:18 -04:00
|
|
|
|
|
|
|
##
|
|
|
|
# Attempts to make tempoary path in /dev/shm, falling back to default
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:__mktemp-shm()
|
2014-05-09 00:46:18 -04:00
|
|
|
{
|
|
|
|
local -r shm=/dev/shm
|
|
|
|
[ -d $shm -a -r $shm ] && mktemp -p$shm || mktemp
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-19 01:52:19 -04:00
|
|
|
# std{out,err} file
|
2017-05-10 01:10:14 -04:00
|
|
|
readonly __spec_outpath="$(shspec:__mktemp-shm)"
|
|
|
|
readonly __spec_errpath="$(shspec:__mktemp-shm)"
|
2014-05-09 00:46:18 -04:00
|
|
|
|
2014-06-10 01:35:27 -04:00
|
|
|
# env dump file
|
2017-05-10 01:10:14 -04:00
|
|
|
readonly __spec_envpath="$(shspec:__mktemp-shm)"
|
2014-06-10 01:35:27 -04:00
|
|
|
|
2017-04-19 01:52:19 -04:00
|
|
|
# most recent expect result exit code
|
2014-05-09 00:46:18 -04:00
|
|
|
declare -i __spec_rexit=0
|
|
|
|
|
|
|
|
# most recent caller for expectations
|
|
|
|
declare __spec_caller=
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Begin a new specification definition
|
|
|
|
#
|
|
|
|
# A specification is any shell script using the specification commands
|
|
|
|
# defined herein.
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:begin-spec()
|
2014-05-09 00:46:18 -04:00
|
|
|
{
|
|
|
|
: placeholder
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Mark the end of a specification
|
|
|
|
#
|
|
|
|
# If the specification was improperly nested, this will output a list of
|
|
|
|
# nesting errors and return a non-zero error. Otherwise, nothing is done.
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:end-spec()
|
2014-05-09 00:46:18 -04:00
|
|
|
{
|
|
|
|
# if the stack is empty then everything is in order
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_empty && return 0
|
2014-05-09 00:46:18 -04:00
|
|
|
|
|
|
|
# otherwise, output an error message for each item in the stack
|
2017-05-10 01:10:14 -04:00
|
|
|
until shspec:stack:_empty; do
|
|
|
|
shspec:stack:_read type line file _ < <(shspec:stack:_head)
|
|
|
|
shspec:stack:_pop
|
2014-05-09 00:46:18 -04:00
|
|
|
echo "error: unterminated \`$type' at $file:$line"
|
|
|
|
done
|
|
|
|
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Begin describing a SUT
|
|
|
|
#
|
|
|
|
# All arguments are used as the description of the SUT (but remember that,
|
|
|
|
# even though the DSL makes it look like plain english, shell quoting and
|
|
|
|
# escaping rules still apply!).
|
|
|
|
describe()
|
|
|
|
{
|
|
|
|
local -r desc="$*"
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_push "describe" $(caller) "$desc"
|
2014-05-09 00:46:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Declare a fact
|
|
|
|
#
|
|
|
|
# Like `describe', the entire argument list is used as the fact description.
|
|
|
|
it()
|
|
|
|
{
|
|
|
|
local -r desc="$*"
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_push "it" $(caller) "$desc"
|
2014-05-09 00:46:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# End the nearest declaration
|
|
|
|
#
|
|
|
|
# Note that some declarations (such as `expect') are implicitly closed and
|
|
|
|
# should not use this command.
|
|
|
|
end()
|
|
|
|
{
|
2017-05-10 01:10:14 -04:00
|
|
|
local -r head="$(shspec:stack:_head-type)"
|
2014-05-09 00:46:18 -04:00
|
|
|
local -r cleanhead="$head"
|
|
|
|
|
|
|
|
# some statements are implicitly terminated; explicitly doing so is
|
|
|
|
# indicitive of a syntax issue
|
|
|
|
[ "${head:0:1}" != : ] \
|
2017-05-10 01:10:14 -04:00
|
|
|
|| shspec:bail \
|
2014-06-13 17:10:02 -04:00
|
|
|
"unexpected \`end': still processing \`$cleanhead'" $(caller)
|
2014-05-09 00:46:18 -04:00
|
|
|
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_pop >/dev/null || shspec:bail "unmatched \`end'"
|
2014-05-09 00:46:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Declare the premise of an expectation
|
|
|
|
#
|
|
|
|
# All arguments are interpreted to be the command line to execute for the
|
|
|
|
# test. The actual expectations that assert upon this declaration are
|
|
|
|
# defined by `to'.
|
|
|
|
#
|
|
|
|
# That is, this declares "given this command, I can expect that..."
|
|
|
|
expect()
|
|
|
|
{
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_assert-within it expect $(caller)
|
2017-04-19 01:52:19 -04:00
|
|
|
( "$@" >"$__spec_outpath" 2>"$__spec_errpath" )
|
2014-05-09 00:46:18 -04:00
|
|
|
__spec_rexit=$?
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_push :expect $(caller) "$@"
|
2014-05-09 00:46:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Declare expectations
|
|
|
|
#
|
|
|
|
# This declares an expectation on the immediately preceding expectation
|
|
|
|
# premise.
|
|
|
|
to()
|
|
|
|
{
|
|
|
|
__spec_caller=${__spec_caller:-$(caller)}
|
|
|
|
|
2014-06-13 17:10:02 -04:00
|
|
|
[ $# -gt 0 ] || \
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:bail "missing assertion string for \`to'" $__spec_caller
|
2014-05-09 00:46:18 -04:00
|
|
|
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_assert-follow :expect to $(caller)
|
|
|
|
shspec:stack:_pop
|
2014-05-09 00:46:18 -04:00
|
|
|
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:__handle-to "$__spec_rexit" $__SHIFTN \
|
2014-06-10 01:35:27 -04:00
|
|
|
"$__spec_errpath" "$__spec_envpath" "$@" \
|
2014-05-15 00:42:47 -04:00
|
|
|
|| fail "$*"
|
2014-05-12 23:51:08 -04:00
|
|
|
|
|
|
|
__spec_caller=
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Perform expectation assertion by invoking expectation handler
|
|
|
|
#
|
2014-05-15 00:42:47 -04:00
|
|
|
# Will throw an error if the handler cannot be found. Arguments are expected
|
|
|
|
# to be of the following form:
|
|
|
|
#
|
|
|
|
# <exit code> <shiftn> <...N> <expect type> <...remainder clause>
|
|
|
|
#
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:__handle-to()
|
2014-05-12 23:51:08 -04:00
|
|
|
{
|
2014-05-15 00:42:47 -04:00
|
|
|
local -ri rexit="$1"
|
|
|
|
local -ri shiftn="$2"
|
|
|
|
local -r errpath="$( [ $shiftn -gt 2 ] && echo "$3" )"
|
2014-06-10 01:35:27 -04:00
|
|
|
local -r envpath="$( [ $shiftn -gt 3 ] && echo "$4" )"
|
2014-05-15 00:42:47 -04:00
|
|
|
shift "$shiftn"
|
|
|
|
|
2014-05-12 23:51:08 -04:00
|
|
|
local -r type="$1"
|
|
|
|
shift
|
|
|
|
|
2017-05-10 01:10:14 -04:00
|
|
|
local -r assert="shspec:expect:$type"
|
2014-05-09 00:46:18 -04:00
|
|
|
type "$assert" &>/dev/null \
|
2017-05-10 01:10:14 -04:00
|
|
|
|| shspec:bail "unknown expectation: \`$type'" $__spec_caller
|
2014-05-09 00:46:18 -04:00
|
|
|
|
2014-05-10 00:34:01 -04:00
|
|
|
# first argument is exit code, second is the number of arguments to shift
|
2014-05-12 21:35:47 -04:00
|
|
|
# to place $1 at the remainder clause, third is the path to the stderr
|
|
|
|
# output file, and all remaining arguments are said remainder clause; the
|
|
|
|
# shift argument allows the implementation to vary without breaking BC so
|
|
|
|
# long as the meaning of the shifted arguments do not change
|
2014-06-10 01:35:27 -04:00
|
|
|
$assert $rexit $__SHIFTN "$errpath" "$envpath" "$@" \
|
2017-04-19 01:52:19 -04:00
|
|
|
< "$__spec_outpath"
|
2014-05-12 23:51:08 -04:00
|
|
|
}
|
2014-05-09 01:59:21 -04:00
|
|
|
|
2014-05-12 23:51:08 -04:00
|
|
|
|
|
|
|
##
|
2014-05-15 00:42:47 -04:00
|
|
|
# Alias for _handle-to
|
2014-05-12 23:51:08 -04:00
|
|
|
#
|
2014-05-15 00:42:47 -04:00
|
|
|
# Shows intent to proxy a call and allows proxy implementation to vary.
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:proxy-to() { shspec:__handle-to "$@"; }
|
2014-05-09 00:46:18 -04:00
|
|
|
|
|
|
|
|
2014-05-09 21:48:09 -04:00
|
|
|
##
|
|
|
|
# Declares additional expectations for the preceding premise
|
|
|
|
and()
|
|
|
|
{
|
|
|
|
__spec_caller="$(caller)"
|
|
|
|
|
|
|
|
# the most recently popped value should be an expect premise, implying
|
|
|
|
# that an expectation declaration implicitly popped it
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:stack:_unpop
|
|
|
|
shspec:stack:_assert-within :expect and $(caller) \
|
2014-05-09 21:48:09 -04:00
|
|
|
"follow an expectation as part of"
|
|
|
|
|
|
|
|
"$@"
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-05-09 00:46:18 -04:00
|
|
|
##
|
|
|
|
# Outputs failure details and exits
|
|
|
|
#
|
|
|
|
# TODO: This is a temporary implementation; we would like to list all
|
|
|
|
# failures, and the actual display shall be handled by a reporter, not by
|
|
|
|
# us. This function will ultimately, therefore, simply collect data for
|
|
|
|
# later processing.
|
|
|
|
fail()
|
|
|
|
{
|
2014-05-12 23:45:38 -04:00
|
|
|
echo "expected to $*" >&2
|
2014-05-09 00:46:18 -04:00
|
|
|
|
|
|
|
echo ' stdout:'
|
2017-04-19 01:59:12 -04:00
|
|
|
sed 's/^/ /g' < <( xxd < "$__spec_outpath" )
|
2014-05-09 00:46:18 -04:00
|
|
|
echo
|
|
|
|
echo ' stderr:'
|
2017-04-19 01:59:12 -04:00
|
|
|
sed 's/^/ /g' < <( xxd < "$__spec_errpath" )
|
2014-05-09 00:46:18 -04:00
|
|
|
echo
|
|
|
|
echo " exit code: $__spec_rexit"
|
|
|
|
|
2014-05-11 21:47:56 -04:00
|
|
|
echo
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:bail "expectation failure"
|
2014-05-09 00:46:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
##
|
|
|
|
# Something went wrong; immediately abort processing with an error
|
|
|
|
#
|
|
|
|
# This should only be used in the case of a specification parsing error or
|
|
|
|
# other fatal errors; it should not be used for failing expectations.
|
2014-05-09 02:14:11 -04:00
|
|
|
#
|
|
|
|
# If no file and line number are provided, this will default to the current
|
|
|
|
# spec caller, if any.
|
2017-05-10 01:10:14 -04:00
|
|
|
shspec:bail()
|
2014-05-09 00:46:18 -04:00
|
|
|
{
|
2014-05-09 02:14:11 -04:00
|
|
|
local -r msg="$1"
|
2014-05-09 00:46:18 -04:00
|
|
|
local line="$2"
|
|
|
|
local file="$3"
|
|
|
|
|
2014-05-09 02:14:11 -04:00
|
|
|
# default to current caller if no line/file was provided
|
|
|
|
if [ -z "$file" -a -n "$__spec_caller" ]; then
|
|
|
|
read line file <<< "$__spec_caller"
|
|
|
|
fi
|
|
|
|
|
2017-04-19 01:59:12 -04:00
|
|
|
echo -n "error: $msg" >&2
|
2014-05-09 02:14:11 -04:00
|
|
|
[ -n "$file" ] && echo -n " at $file:$line" >&2
|
2014-05-09 00:46:18 -04:00
|
|
|
|
2017-04-19 01:59:12 -04:00
|
|
|
echo >&2
|
2014-05-09 00:46:18 -04:00
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
|