mirror of
https://github.com/zunit-zsh/zunit.git
synced 2026-09-10 06:36:15 -04:00
664 lines
15 KiB
Bash
Executable file
664 lines
15 KiB
Bash
Executable file
#!/usr/bin/env zsh
|
|
|
|
################################
|
|
# Internal assertion functions #
|
|
################################
|
|
|
|
###
|
|
# Assert that two integers are equal
|
|
###
|
|
function _zunit_assert_equals() {
|
|
local value=$1 comparison=$2
|
|
|
|
[[ $value -eq $comparison ]] && return 0
|
|
|
|
echo "'$value' is not equal to '$comparison'"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that two integers are not equal
|
|
###
|
|
function _zunit_assert_not_equal_to() {
|
|
local value=$1 comparison=$2
|
|
|
|
[[ $value -ne $comparison ]] && return 0
|
|
|
|
echo "'$value' is equal to '$comparison'"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that two string are the same
|
|
###
|
|
function _zunit_assert_same_as() {
|
|
local value=$1 comparison=$2
|
|
|
|
[[ $value = $comparison ]] && return 0
|
|
|
|
echo "'$value' is not the same as '$comparison'"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that two string are different
|
|
###
|
|
function _zunit_assert_different_to() {
|
|
local value=$1 comparison=$2
|
|
|
|
[[ $value != $comparison ]] && return 0
|
|
|
|
echo "'$value' is the same as '$comparison'"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that a value is empty
|
|
###
|
|
function _zunit_assert_is_empty() {
|
|
local value=$1
|
|
|
|
[[ -z ${value[@]} ]] && return 0
|
|
|
|
echo "'${value[@]}' is not empty"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that a value is not empty
|
|
###
|
|
function _zunit_assert_is_not_empty() {
|
|
local value=$1
|
|
|
|
[[ -n ${value[@]} ]] && return 0
|
|
|
|
echo "value is empty"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that the value matches a regex pattern
|
|
###
|
|
function _zunit_assert_matches() {
|
|
local value=$1 pattern=$2
|
|
|
|
[[ $value =~ $pattern ]] && return 0
|
|
|
|
echo "'$value' does not match /$pattern/"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that the value does not match a regex pattern
|
|
###
|
|
function _zunit_assert_does_not_match() {
|
|
local value=$1 pattern=$2
|
|
|
|
[[ ! $value =~ $pattern ]] && return 0
|
|
|
|
echo "'$value' matches /$pattern/"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that a value is found in an array
|
|
###
|
|
function _zunit_assert_in() {
|
|
local i found=0 value=$1 array=(${(@)@:2})
|
|
|
|
for i in ${(@f)array}; do
|
|
[[ $i = $value ]] && found=1
|
|
done
|
|
|
|
[[ $found -eq 1 ]] && return 0
|
|
|
|
echo "'$value' is not in (${(@z)array})"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert that a value is not found in an array
|
|
###
|
|
function _zunit_assert_not_in() {
|
|
local i found=0 value=$1 array=(${(@)@:2})
|
|
|
|
for i in ${(@f)array}; do
|
|
[[ $i = $value ]] && found=1
|
|
done
|
|
|
|
[[ $found -eq 0 ]] && return 0
|
|
|
|
echo "'$value' is in (${(@z)array})"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert the a path exists
|
|
###
|
|
function _zunit_assert_exists() {
|
|
local path=$1
|
|
|
|
# If filepath is relative, prepend the test directory
|
|
if [[ "${path:0:1}" != "/" ]]; then
|
|
filepath="$testdir/${path}"
|
|
fi
|
|
|
|
[[ -e $filepath ]] && return 0
|
|
|
|
echo "'$path' does not exist"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert the a path exists and is a file
|
|
###
|
|
function _zunit_assert_is_file() {
|
|
local path=$1
|
|
|
|
# If filepath is relative, prepend the test directory
|
|
if [[ "${path:0:1}" != "/" ]]; then
|
|
filepath="$testdir/${path}"
|
|
fi
|
|
|
|
[[ -f $filepath ]] && return 0
|
|
|
|
echo "'$path' does not exist or is not a file"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert the a path exists and is a directory
|
|
###
|
|
function _zunit_assert_is_dir() {
|
|
local path=$1
|
|
|
|
# If filepath is relative, prepend the test directory
|
|
if [[ "${path:0:1}" != "/" ]]; then
|
|
filepath="$testdir/${path}"
|
|
fi
|
|
|
|
[[ -d $filepath ]] && return 0
|
|
|
|
echo "'$path' does not exist or is not a directory"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert the a path exists and is a symbolic link
|
|
###
|
|
function _zunit_assert_is_link() {
|
|
local path=$1
|
|
|
|
# If filepath is relative, prepend the test directory
|
|
if [[ "${path:0:1}" != "/" ]]; then
|
|
filepath="$testdir/${path}"
|
|
fi
|
|
|
|
[[ -h $filepath ]] && return 0
|
|
|
|
echo "'$path' does not exist or is not a symbolic link"
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Assert the a path exists and is readable
|
|
###
|
|
function _zunit_assert_is_readable() {
|
|
local path=$1
|
|
|
|
# If filepath is relative, prepend the test directory
|
|
if [[ "${path:0:1}" != "/" ]]; then
|
|
filepath="$testdir/${path}"
|
|
fi
|
|
|
|
[[ -r $filepath ]] && return 0
|
|
|
|
echo "'$path' does not exist or is not readable"
|
|
exit 1
|
|
}
|
|
|
|
################################
|
|
# Helpers for use within tests #
|
|
################################
|
|
|
|
###
|
|
# Find a file, and load it into the environment
|
|
###
|
|
function load() {
|
|
local name="$1"
|
|
local filename
|
|
|
|
# If filepath is absolute, then use it as is
|
|
if [[ "${name:0:1}" = "/" ]]; then
|
|
filename="${name}"
|
|
# If it's relative, prepend the test directory
|
|
else
|
|
filename="$testdir/${name}"
|
|
fi
|
|
|
|
# Check if the file exists
|
|
if [[ -f "$filename" ]]; then
|
|
# Source the file and exit if it's found
|
|
source "$filename"
|
|
return 0
|
|
fi
|
|
|
|
# Perform the check again, adding the .zsh extension
|
|
if [[ -f "$filename.zsh" ]]; then
|
|
# Source the file and exit if it's found
|
|
source "$filename.zsh"
|
|
return 0
|
|
fi
|
|
|
|
# Output an error message to the user
|
|
echo "File $filename does not exist" >&2
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Run an external command and capture its output and exit status
|
|
###
|
|
function run() {
|
|
# Stop the shell from exiting on error temporarily
|
|
unsetopt ERR_EXIT
|
|
|
|
# Preserve current $IFS
|
|
local oldIFS=$IFS cmd name
|
|
|
|
# Grab the first argument
|
|
cmd=(${@[@]})
|
|
name="${cmd[1]}"
|
|
|
|
# If the command is not an existing command or file,
|
|
# then prepend the test directory to the path
|
|
type $name > /dev/null
|
|
if [[ $? -ne 0 && ! -f $name && -f "$testdir/${name}" ]]; then
|
|
cmd[1]="$testdir/${name}"
|
|
fi
|
|
|
|
# Store lines of output in an array
|
|
IFS=$'\n' lines=($("${cmd[@]}" 2>&1))
|
|
|
|
# Get the process exit state
|
|
state="$?"
|
|
|
|
# Store the full output in a variable
|
|
output=${lines[@]}
|
|
|
|
# Restore $IFS
|
|
IFS=$oldIFS
|
|
|
|
# Restore the exit on error state
|
|
setopt ERR_EXIT
|
|
}
|
|
|
|
###
|
|
# Redirect the assertion shorthand to the correct function
|
|
###
|
|
function assert() {
|
|
local value=$1 assertion=$2 comparisons=${(@)@:3}
|
|
|
|
if [[ -z $assertion ]]; then
|
|
assertion=$value
|
|
value=""
|
|
fi
|
|
|
|
if (( ! $+functions[_zunit_assert_${assertion}] )); then
|
|
echo "$(color red "Assertion $assertion does not exist")"
|
|
return 127
|
|
fi
|
|
|
|
"_zunit_assert_${assertion}" $value $comparisons
|
|
|
|
return $?
|
|
}
|
|
|
|
######################
|
|
# Main zunit process #
|
|
######################
|
|
|
|
###
|
|
# Output usage information and exit
|
|
###
|
|
function _zunit_usage() {
|
|
echo "\033[0;33mUsage:\033[0;m"
|
|
echo " zunit [options] <tests...>"
|
|
echo
|
|
echo "\033[0;33mOptions:\033[0;m"
|
|
echo " -h, --help Output help text and exit"
|
|
echo " -v, --version Output version information and exit"
|
|
echo " -f, --fail-fast Stop the test runner immediately after the first failure"
|
|
}
|
|
|
|
###
|
|
# Execute a test and store the result
|
|
###
|
|
_zunit_execute_test() {
|
|
local name="$1" body="$2"
|
|
|
|
if [[ $body && $name ]]; then
|
|
# Update the progress indicator
|
|
revolver update "${name}"
|
|
sleep 0.1
|
|
|
|
# Make sure we don't already have a function defined
|
|
(( $+functions[__zunit_tmp_test_function] )) && \
|
|
unfunction __zunit_tmp_test_function
|
|
|
|
func="function __zunit_tmp_test_function() {
|
|
setopt ERR_EXIT
|
|
integer state
|
|
local output
|
|
typeset -a lines
|
|
|
|
if (( $+functions[__zunit_test_setup] )); then
|
|
__zunit_test_setup
|
|
fi
|
|
|
|
${body}
|
|
|
|
if (( $+functions[__zunit_test_teardown] )); then
|
|
__zunit_test_teardown
|
|
fi
|
|
}"
|
|
|
|
# Quietly eval the body into a variable as a first test
|
|
output=$(eval "$(echo "$func")" 2>&1)
|
|
|
|
# Check the status of the eval, and output any errors
|
|
if [[ $? -ne 0 ]]; then
|
|
echo "$(color red '✘' ${name})"
|
|
echo " $(color red underline "Failed to parse test body")"
|
|
echo " $(color red $output)"
|
|
|
|
[[ -n $fail_fast ]] && revolver stop && exit 1
|
|
|
|
return 1
|
|
fi
|
|
|
|
# Run the eval again, this time within the current context so that
|
|
# the function is registered in the current scope
|
|
eval "$(echo "$func")" 2>/dev/null
|
|
|
|
# Any errors should have been caught above, but if the function
|
|
# does not exist, we can't go any further
|
|
if (( ! $+functions[__zunit_tmp_test_function] )); then
|
|
echo "$(color red '✘' ${name})"
|
|
echo " $(color red underline "Failed to parse test body")"
|
|
|
|
[[ -n $fail_fast ]] && revolver stop && exit 1
|
|
|
|
return 1
|
|
fi
|
|
|
|
# Execute the test body, and capture its output
|
|
output="$(__zunit_tmp_test_function 2>&1)"
|
|
|
|
# Output the result to the user
|
|
if [[ $? -eq 0 ]]; then
|
|
echo "$(color green '✔') ${name}"
|
|
return 0
|
|
else
|
|
echo "$(color red '✘' ${name})"
|
|
if [[ -n $output ]]; then
|
|
echo " $(color red "${output}")"
|
|
fi
|
|
|
|
[[ -n $fail_fast ]] && revolver stop && exit 1
|
|
|
|
return 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
###
|
|
# Encode test name into a value which can be used as a hash key
|
|
###
|
|
function _zunit_encode_test_name() {
|
|
echo "$1" | tr A-Z a-z \
|
|
| tr _ ' ' \
|
|
| tr - ' ' \
|
|
| tr -s ' ' \
|
|
| sed 's/\- /-/' \
|
|
| sed 's/ \-/-/' \
|
|
| tr ' ' "-"
|
|
}
|
|
|
|
###
|
|
# Run all tests within a file
|
|
###
|
|
function _zunit_run_testfile() {
|
|
local testbody testname pattern \
|
|
setup teardown \
|
|
testfile="$1" testdir="$(dirname "$testfile")"
|
|
|
|
typeset -A tests
|
|
|
|
# Update status message
|
|
revolver update "Loading tests from $testfile"
|
|
|
|
# A regex pattern to match test declarations
|
|
pattern='^ *@test *([^ ].*) *\{ *(.*)$'
|
|
|
|
# Loop through each of the lines in the file
|
|
IFS=$'\n' lines=($(cat $testfile))
|
|
for line in $lines[@]; do
|
|
# Match current line against pattern
|
|
if [[ "$line" =~ $pattern ]]; then
|
|
# Get test name from matches
|
|
testname=${line[(( ${line[(i)[\']]}+1 )),(( ${line[(I)[\']]}-1 ))]}
|
|
elif [[ "$line" =~ '^@setup([ ])?\{$' ]]; then
|
|
setup=''
|
|
parsing_setup=true
|
|
elif [[ "$line" =~ '^@teardown([ ])?\{$' ]]; then
|
|
teardown=''
|
|
parsing_teardown=true
|
|
elif [[ "$line" = '}' ]]; then
|
|
testname=''
|
|
parsing_setup=''
|
|
parsing_teardown=''
|
|
else
|
|
if [[ -n $testname ]]; then
|
|
tests[$testname]+="$line\n"
|
|
continue
|
|
fi
|
|
|
|
if [[ -n $parsing_setup ]]; then
|
|
setup+="$line\n"
|
|
continue
|
|
fi
|
|
|
|
if [[ -n $parsing_teardown ]]; then
|
|
teardown+="$line\n"
|
|
continue
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [[ -n $setup ]]; then
|
|
setupfunc="function __zunit_test_setup() {
|
|
${setup}
|
|
}"
|
|
|
|
# Quietly eval the body into a variable as a first test
|
|
output=$(eval "$(echo "$setupfunc")" 2>&1)
|
|
|
|
# Check the status of the eval, and output any errors
|
|
if [[ $? -ne 0 ]]; then
|
|
echo "$(color red '✘' ${testfile})"
|
|
echo " $(color red underline "Failed to parse setup method")"
|
|
echo " $(color red $output)"
|
|
|
|
[[ -n $fail_fast ]] && revolver stop && exit 1
|
|
|
|
return 1
|
|
fi
|
|
|
|
# Run the eval again, this time within the current context so that
|
|
# the function is registered in the current scope
|
|
eval "$(echo "$setupfunc")" 2>/dev/null
|
|
|
|
# Any errors should have been caught above, but if the function
|
|
# does not exist, we can't go any further
|
|
if (( ! $+functions[__zunit_test_setup] )); then
|
|
echo "$(color red '✘' ${testfile})"
|
|
echo " $(color red underline "Failed to parse setup method")"
|
|
|
|
[[ -n $fail_fast ]] && revolver stop && exit 1
|
|
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if [[ -n $teardown ]]; then
|
|
teardownfunc="function __zunit_test_teardown() {
|
|
${teardown}
|
|
}"
|
|
|
|
# Quietly eval the body into a variable as a first test
|
|
output=$(eval "$(echo "$teardownfunc")" 2>&1)
|
|
|
|
# Check the status of the eval, and output any errors
|
|
if [[ $? -ne 0 ]]; then
|
|
echo "$(color red '✘' ${testfile})"
|
|
echo " $(color red underline "Failed to parse teardown method")"
|
|
echo " $(color red $output)"
|
|
|
|
[[ -n $fail_fast ]] && revolver stop && exit 1
|
|
|
|
return 1
|
|
fi
|
|
|
|
# Run the eval again, this time within the current context so that
|
|
# the function is registered in the current scope
|
|
eval "$(echo "$teardownfunc")" 2>/dev/null
|
|
|
|
# Any errors should have been caught above, but if the function
|
|
# does not exist, we can't go any further
|
|
if (( ! $+functions[__zunit_test_teardown] )); then
|
|
echo "$(color red '✘' ${testfile})"
|
|
echo " $(color red underline "Failed to parse teardown method")"
|
|
|
|
[[ -n $fail_fast ]] && revolver stop && exit 1
|
|
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Loop through each of the tests and execute it
|
|
for name body ("${(@kv)tests}") _zunit_execute_test "$name" "$body"
|
|
|
|
(( $+functions[__zunit_test_setup] )) && unfunction __zunit_test_setup
|
|
(( $+functions[__zunit_test_teardown] )) && unfunction __zunit_test_teardown
|
|
(( $+functions[__zunit_tmp_test_function] )) && unfunction __zunit_tmp_test_function
|
|
}
|
|
|
|
###
|
|
# Parse a list of arguments
|
|
###
|
|
function _zunit_parse_argument() {
|
|
local argument="$1"
|
|
|
|
# If the argument begins with an underscore, then it
|
|
# should not be run, so we skip it
|
|
if [[ "${argument:0:1}" = "_" || "$(basename $argument | cut -c 1)" = "_" ]]; then
|
|
return
|
|
fi
|
|
|
|
# If the argument is a directory
|
|
if [[ -d $argument ]]; then
|
|
# Loop through each of the files in the directory
|
|
for file in $(find $argument -depth 1); do
|
|
# Run it through the parser again
|
|
_zunit_parse_argument $file
|
|
done
|
|
|
|
return
|
|
fi
|
|
|
|
# If it is a valid file
|
|
if [[ -f $argument ]]; then
|
|
# Grab the first line of the file
|
|
line=$(cat $argument | head -n 1)
|
|
|
|
# Check for the zunit shebang
|
|
if [[ $line = "#!/usr/bin/env zunit" ]]; then
|
|
# Add it to the array
|
|
testfiles[(( ${#testfiles} + 1 ))]=($argument)
|
|
return
|
|
fi
|
|
|
|
# The test file does not contain the zunit shebang, therefore
|
|
# we can't trust that running it will not be harmful, and throw
|
|
# a fatal error
|
|
echo $(color red "File '$argument' is not a valid zunit test file") >&2
|
|
echo "Test files must contain the following shebang on the first line" >&2
|
|
echo " #!/usr/bin/env zunit" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The file could not be found, so we throw a fatal error
|
|
echo $(color red "Test file '$argument' could not be found") >&2
|
|
exit 1
|
|
}
|
|
|
|
###
|
|
# Run tests
|
|
###
|
|
function _zunit_run() {
|
|
local arguments=("$@") testfiles=()
|
|
|
|
# Start the progress indicator
|
|
revolver start 'Loading tests'
|
|
|
|
# If no arguments are passed, use the current directory
|
|
if [[ ${#arguments} -eq 0 ]]; then
|
|
arguments=("tests")
|
|
fi
|
|
|
|
# Loop through each of the passed arguments
|
|
local argument
|
|
for argument in $arguments; do
|
|
# Parse the argument, so that we end up with a list of valid files
|
|
_zunit_parse_argument $argument
|
|
done
|
|
|
|
# Loop through each of the test files and run them
|
|
local line
|
|
for testfile in $testfiles; do
|
|
_zunit_run_testfile $testfile
|
|
done
|
|
|
|
revolver stop
|
|
}
|
|
|
|
###
|
|
# The main zunit process
|
|
###
|
|
function _zunit() {
|
|
local help version ctx="$1"
|
|
|
|
zparseopts -D \
|
|
h=help -help=help \
|
|
v=version -version=version \
|
|
f=fail_fast -fail-fast=fail_fast
|
|
|
|
# If the help option is passed,
|
|
# output usage information and exit
|
|
if [[ $help ]]; then
|
|
_zunit_usage
|
|
exit 0
|
|
fi
|
|
|
|
# If the version option is passed,
|
|
# output version information and exit
|
|
if [[ $version ]]; then
|
|
echo '0.2.0'
|
|
exit 0
|
|
fi
|
|
|
|
_zunit_run "$@"
|
|
}
|
|
|
|
_zunit "$@"
|