From 4f256c7d58e87f96618ba8a1d2f090e2431e5c01 Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 22 Feb 2017 23:56:14 +0000 Subject: [PATCH] Separate source files, and add Makefile to compile --- .gitignore | 4 + .travis.yml | 1 + Makefile | 12 + src/assertions.zsh | 341 ++++++++++ src/commands/init.zsh | 101 +++ src/commands/run.zsh | 481 ++++++++++++++ src/events.zsh | 127 ++++ src/helpers.zsh | 122 ++++ src/reports/html.zsh | 72 +++ src/reports/tap.zsh | 66 ++ src/zunit.zsh | 115 ++++ zunit | 1412 ----------------------------------------- 12 files changed, 1442 insertions(+), 1412 deletions(-) create mode 100644 Makefile create mode 100644 src/assertions.zsh create mode 100644 src/commands/init.zsh create mode 100644 src/commands/run.zsh create mode 100644 src/events.zsh create mode 100644 src/helpers.zsh create mode 100644 src/reports/html.zsh create mode 100644 src/reports/tap.zsh create mode 100755 src/zunit.zsh delete mode 100755 zunit diff --git a/.gitignore b/.gitignore index baf411e..aee474a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ +# Ignore test output, but keep the directory /tests/_output/* !/tests/_output/.gitkeep + +# Ignore the compiled program +/zunit diff --git a/.travis.yml b/.travis.yml index 89e9563..49c8e98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ before_script: - chmod u+x .bin/{color,revolver,zvm} - export PATH="$HOME/.zvm/bin:$PWD/.bin:$PATH" - zvm use ${ZVM_VERSION} +- make script: ./zunit notifications: email: false diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1ba9624 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +target: + @cat /dev/null > zunit + @echo "#!/usr/bin/env zsh\n" >> zunit + @cat src/reports/*.zsh >> zunit + @cat $(filter-out src/zunit.zsh,$(wildcard src/*.zsh)) >> zunit + @cat src/commands/*.zsh >> zunit + @cat src/zunit.zsh >> zunit + @chmod u+x zunit + @echo "\033[0;32m✔\033[0;m ZUnit built successfully" + +test: + @./zunit diff --git a/src/assertions.zsh b/src/assertions.zsh new file mode 100644 index 0000000..7dcd9a7 --- /dev/null +++ b/src/assertions.zsh @@ -0,0 +1,341 @@ +################################ +# 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 + local -a array + array=(${(@)@:2}) + + for i in ${(@f)array}; do + [[ $i = $value ]] && found=1 + done + + + [[ $found -eq 1 ]] && return 0 + + echo "'$value' is not in (${(@f)array})" + exit 1 +} + +### +# Assert that a value is not found in an array +### +function _zunit_assert_not_in() { + local i found=0 value=$1 + local -a array + array=(${(@)@:2}) + + for i in ${(@f)array}; do + [[ $i = $value ]] && found=1 + done + + [[ $found -eq 0 ]] && return 0 + + echo "'$value' is in (${(@f)array})" + exit 1 +} + +### +# Assert that a value is a key in a hash +### +function _zunit_assert_is_key_in() { + local i found=0 value=$1 + local -A hash + hash=(${(@)@:2}) + + for k v in ${(@kv)hash}; do + [[ $k = $value ]] && found=1 + done + + [[ $found -eq 1 ]] && return 0 + + echo "'$value' is not a key in (${(@kv)hash})" + exit 1 +} + +### +# Assert that a value is not a key in a hash +### +function _zunit_assert_is_not_key_in() { + local i found=0 value=$1 + local -A hash + hash=(${(@)@:2}) + + for k v in ${(@kv)hash}; do + [[ $k = $value ]] && found=1 + done + + [[ $found -eq 0 ]] && return 0 + + echo "'$value' is a key in (${(@kv)hash})" + exit 1 +} + +### +# Assert that a value is a value in a hash +### +function _zunit_assert_is_value_in() { + local i found=0 value=$1 + local -A hash + hash=(${(@)@:2}) + + for k v in ${(@kv)hash}; do + [[ $v = $value ]] && found=1 + done + + [[ $found -eq 1 ]] && return 0 + + echo "'$value' is not a value in (${(@kv)hash})" + exit 1 +} + +### +# Assert that a value is not a value in a hash +### +function _zunit_assert_is_not_value_in() { + local i found=0 value=$1 + local -A hash + hash=(${(@)@:2}) + + for k v in ${(@kv)hash}; do + [[ $v = $value ]] && found=1 + done + + [[ $found -eq 0 ]] && return 0 + + echo "'$value' is a value in (${(@kv)hash})" + exit 1 +} + +### +# Assert the a path exists +### +function _zunit_assert_exists() { + local pathname=$1 filepath + + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi + + [[ -e "$filepath" ]] && return 0 + + echo "'$pathname' does not exist" + exit 1 +} + +### +# Assert the a path exists and is a file +### +function _zunit_assert_is_file() { + local pathname=$1 filepath + + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi + + [[ -f "$filepath" ]] && return 0 + + echo "'$pathname' 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 pathname=$1 filepath + + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/$pathname" + else + filepath="$pathname" + fi + + [[ -d "$filepath" ]] && return 0 + + echo "'$pathname' 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 pathname=$1 filepath + + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi + + [[ -h "$filepath" ]] && return 0 + + echo "'$pathname' 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 pathname=$1 filepath + + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi + + [[ -r "$filepath" ]] && return 0 + + echo "'$pathname' does not exist or is not readable" + exit 1 +} + +### +# Assert the a path exists and is writable +### +function _zunit_assert_is_writable() { + local pathname=$1 filepath + + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi + + [[ -w "$filepath" ]] && return 0 + + echo "'$pathname' does not exist or is not writable" + exit 1 +} + +### +# Assert the a path exists and is executable +### +function _zunit_assert_is_executable() { + local pathname=$1 filepath + + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi + + [[ -x "$filepath" ]] && return 0 + + echo "'$pathname' does not exist or is not executable" + exit 1 +} diff --git a/src/commands/init.zsh b/src/commands/init.zsh new file mode 100644 index 0000000..f3ef7ca --- /dev/null +++ b/src/commands/init.zsh @@ -0,0 +1,101 @@ +############################ +# The 'zunit init' command # +############################ + +### +# Output usage information and exit +### +function _zunit_init_usage() { + echo "$(color yellow 'Usage:')" + echo " zunit init [options]" + echo + echo "$(color yellow 'Options:')" + echo " -h, --help Output help text and exit" + echo " -v, --version Output version information and exit" + echo " -t, --travis Generate .travis.yml in project" +} + +### +# Parse a YAML config file +# Based on https://gist.github.com/pkuczynski/8665367 +### +function _zunit_parse_yaml() { + local s w fs prefix=$2 + s='[[:space:]]*' + w='[a-zA-Z0-9_]*' + fs="$(echo @|tr @ '\034')" + sed -ne "s|^\(${s}\)\(${w}\)${s}:${s}\"\(.*\)\"${s}\$|\1${fs}\2${fs}\3|p" \ + -e "s|^\(${s}\)\(${w}\)${s}[:-]${s}\(.*\)${s}\$|\1${fs}\2${fs}\3|p" "$1" | + awk -F"${fs}" '{ + indent = length($1)/2; + vname[indent] = $2; + for (i in vname) {if (i > indent) {delete vname[i]}} + if (length($3) > 0) { + vn=""; for (i=0; i&2 + exit 1 + else + echo "$yaml" > "$PWD/.zunit.yml" + fi + + if [[ -d "$PWD/tests" ]]; then + echo $(color red "Directory already exists at $PWD/tests") >&2 + exit 1 + else + mkdir -p tests/_{output,support} + touch tests/_{output,support}/.gitkeep + + echo "$bootstrap" > "$PWD/tests/_support/bootstrap" + echo "$example" > "$PWD/tests/example.zunit" + fi + + #statements + if [[ -n $with_travis ]]; then + if [[ -f "$PWD/.travis.yml" ]]; then + echo $(color red "Travis config already exists at $PWD/.travis.yml") >&2 + exit 1 + else + echo "$travis_yml" > "$PWD/.travis.yml" + fi + fi +} diff --git a/src/commands/run.zsh b/src/commands/run.zsh new file mode 100644 index 0000000..c018ed2 --- /dev/null +++ b/src/commands/run.zsh @@ -0,0 +1,481 @@ +########################### +# The 'zunit run' command # +########################### + +### +# Output usage information and exit +### +function _zunit_run_usage() { + echo "$(color yellow 'Usage:')" + echo " zunit run [options] [tests...]" + echo + echo "$(color yellow 'Options:')" + 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" + echo " -t, --tap Output results in a TAP compatible format" + echo " --output-text Print results to a text log, in TAP compatible format" + echo " --output-html Print results to a HTML page" + echo " --allow-risky Supress warnings generated for risky tests" +} + +### +# Format a ms timestamp in a human-readable format +### +function _zunit_human_time() { + local ms=$1 + local tmp=$(( $1 / 1000 )) + local days=$(( tmp / 60 / 60 / 24 )) + local hours=$(( tmp / 60 / 60 % 24 )) + local minutes=$(( tmp / 60 % 60 )) + local seconds=$(( tmp % 60 )) + (( $days > 0 )) && print -n "${days}d " + (( $hours > 0 )) && print -n "${hours}h " + (( $minutes > 0 )) && print -n "${minutes}m " + (( $seconds > 5 )) && print -n "${seconds}s " + (( $seconds < 30 )) && (( $seconds > 5 )) && print -n "$(( ms - $((seconds*1000)) ))ms" + (( $tmp <= 5 )) && print -n "${1}ms" +} + +### +# Output test results +### +function _zunit_output_results() { + integer elapsed=$(( end_time - start_time )) + echo + echo "$total tests run in $(_zunit_human_time $elapsed)" + echo + echo "$(color yellow underline 'Results') " + echo "$(color green '✔') Passed $passed " + echo "$(color red '✘') Failed $failed " + echo "$(color red '‼') Errors $errors " + echo "$(color magenta '•') Skipped $skipped " + echo "$(color yellow '‼') Warnings $warnings " + echo + + [[ -n $output_text ]] && echo "TAP report written at $PWD/$logfile_text" + [[ -n $output_html ]] && echo "HTML report written at $PWD/$logfile_html" +} + +### +# Execute a test and store the result +### +function _zunit_execute_test() { + local name="$1" body="$2" + + if [[ -n $body ]] && [[ -n $name ]]; then + # Update the progress indicator + [[ -z $tap ]] && revolver update "${name}" + + # 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 + integer _zunit_assertion_count=0 + local output + typeset -a lines + + if (( $+functions[__zunit_test_setup] )); then + __zunit_test_setup >/dev/null 2>&1 + fi + + ${body} + + if (( $+functions[__zunit_test_teardown] )); then + __zunit_test_teardown >/dev/null 2>&1 + fi + + [[ \$_zunit_assertion_count -gt 0 ]] || return 248 + }" + + # Quietly eval the body into a variable as a first test + output=$(eval "$(echo "$func")" 2>&1) + + total=$(( total + 1 )) + + # Check the status of the eval, and output any errors + if [[ $? -ne 0 ]]; then + _zunit_error 'Failed to parse test body' $output + + return 126 + 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 + _zunit_error 'Failed to parse test body' + + return 126 + fi + + autoload is-at-least + + # Check if a time limit has been specified + if is-at-least 5.1.0 && [[ -n $zunit_config_time_limit ]]; then + # Create a wrapper function around the test + __zunit_async_test_wrapper() { + local pid + + # Get the current timestamp, and the time limit, and use those to + # work out the kill time for the sub process + integer time_limit=$(( ${zunit_config_time_limit:-30} * 1000 )) + integer time=$(( EPOCHREALTIME * 1000 )) + integer kill_time=$(( $time + $time_limit )) + + # Launch the test function asynchronously and store its PID + __zunit_tmp_test_function & + pid=$! + + # While the child process is still running + while kill -0 $pid >/dev/null 2>&1; do + # Check that the kill time has not yet been reached + time=$(( EPOCHREALTIME * 1000 )) + if [[ $time -gt $kill_time ]]; then + # The kill time has been reached, kill the child process, + # and exit the wrapper function + kill -9 $pid >/dev/null 2>&1 + exit 78 + fi + done + + # Use wait to get the exit code from the background process, + # and return that so that the test result can be deduced + wait $pid + return $? + } + + # Launch the async wrapper, and capture the output in a variable + output="$(__zunit_async_test_wrapper 2>&1)" + else + # Launch the test, and capture the output in a variable + output="$(__zunit_tmp_test_function 2>&1)" + fi + + # Output the result to the user + state=$? + if [[ $state -eq 48 ]]; then + _zunit_skip $output + + return + elif [[ $state -eq 78 ]]; then + _zunit_error "Test took too long to run. Terminated after ${zunit_config_time_limit:-30} seconds" $output + + return + elif [[ -z $allow_risky && $state -eq 248 ]]; then + _zunit_warn 'No assertions were run, test is risky' + + return + elif [[ -n $allow_risky && $state -eq 248 ]] || [[ $state -eq 0 ]]; then + _zunit_success + + return + else + _zunit_failure $output + + 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")" + local -a lines tests test_names + tests=() + test_names=() + + # Update status message + [[ -z $tap ]] && revolver update "Loading tests from $testfile" + + # A regex pattern to match test declarations + pattern='^ *@test *([^ ].*) *\{ *(.*)$' + + # Loop through each of the lines in the file + local oldIFS=$IFS + IFS=$'\n' lines=($(cat $testfile)) + IFS=$oldIFS + 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 ))]}" + test_names=($test_names $testname) + tests[${#test_names}]='' + 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[${#test_names}]+="$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 + _zunit_error "Failed to parse setup method" $output + + return 126 + 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 + _zunit_error "Failed to parse setup method" + + return 126 + 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 + _zunit_error "Failed to parse teardown method" $output + + return 126 + 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 + _zunit_error "Failed to parse teardown method" + + return 126 + fi + fi + + # Loop through each of the tests and execute it + integer i=1 + local name body + for name in "${test_names[@]}"; do + body="${tests[$i]}" + _zunit_execute_test "$name" "$body" + i=$(( i + 1 )) + done + + (( $+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 -mindepth 1 -maxdepth 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 126 + fi + + # The file could not be found, so we throw a fatal error + echo $(color red "Test file or directory '$argument' could not be found") >&2 + exit 126 +} + +### +# Run tests +### +function _zunit_run() { + local -a arguments testfiles + local fail_fast tap allow_risky + local output_text logfile_text output_html logfile_html + + zmodload zsh/datetime + local start_time=$((EPOCHREALTIME*1000)) end_time + + zparseopts -D -E \ + h=help -help=help \ + v=version -version=version \ + f=fail_fast -fail-fast=fail_fast \ + t=tap -tap=tap \ + -output-text=output_text \ + -output-html=output_html \ + -allow-risky=allow_risky + + if [[ -n $tap ]] || [[ "$zunit_config_tap" = "true" ]]; then + tap=1 + echo 'TAP version 13' + fi + + if [[ -z $tap ]]; then + # Print version information + echo $(color yellow 'Launching ZUnit') + echo "ZUnit: $(_zunit_version)" + echo "ZSH: $(zsh --version)" + echo + fi + + if [[ -n $output_text ]]; then + if [[ $missing_config -eq 1 ]]; then + echo $(color red '.zunit.yml could not be found. Run `zulu init`') + exit 1 + fi + + if [[ -z $zunit_config_directories_output ]]; then + echo $(color red 'Output directory must be specified in .zunit.yml') + exit 1 + fi + fi + + if [[ -n $output_text ]]; then + logfile_text="$zunit_config_directories_output/output.txt" + echo 'TAP version 13' > $logfile_text + fi + + if [[ -n $output_html ]]; then + logfile_html="$zunit_config_directories_output/output.html" + _zunit_html_header > $logfile_html + fi + + if [[ -n $zunit_config_directories_support ]]; then + local support="$zunit_config_directories_support" + if [[ ! -d $support ]]; then + echo $(color red "Support directory at $support is missing") + exit 1 + fi + + if [[ -f "$support/bootstrap" ]]; then + source "$support/bootstrap" + echo "$(color green '✔') Sourced bootstrap script $support/bootstrap" + fi + fi + + arguments=("$@") + testfiles=() + + # Start the progress indicator + [[ -z $tap ]] && revolver start 'Loading tests' + + # If no arguments are passed, use the current directory + if [[ ${#arguments} -eq 0 ]]; then + if [[ -n $zunit_config_directories_tests ]]; then + arguments=("$zunit_config_directories_tests") + else + arguments=("tests") + fi + 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 + local total=0 passed=0 failed=0 errors=0 warnings=0 skipped=0 + for testfile in $testfiles; do + _zunit_run_testfile $testfile + done + + end_time=$((EPOCHREALTIME*1000)) + + [[ -n $tap ]] && echo "1..$total" + [[ -n $output_text ]] && echo "1..$total" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_footer >> $logfile_html + + [[ -z $tap ]] && _zunit_output_results && revolver stop + + [[ $(( $passed + $skipped )) -eq $total ]] +} diff --git a/src/events.zsh b/src/events.zsh new file mode 100644 index 0000000..c3f3fd6 --- /dev/null +++ b/src/events.zsh @@ -0,0 +1,127 @@ +########################################## +# Functions for handling internal events # +########################################## + +### +# Shutdown testing early +# Usually called if --fail-fast is specified +### +function _zunit_fail_shutdown() { + # Kill the revolver process + [[ -z $tap ]] && revolver stop + + echo $(color red bold 'Execution halted after failure') + end_time=$((EPOCHREALTIME*1000)) + + [[ -z $tap ]] && _zunit_output_results + + # Print end of HTML report + if [[ -n $output_html ]]; then + name='Execution halted after failure' + _zunit_html_error >> $logfile_html + _zunit_html_footer >> $logfile_html + fi + + exit 1 +} + +### +# Output a success message +### +function _zunit_success() { + [[ -n $output_text ]] && _zunit_tap_success "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_success "$@" >> $logfile_html + + passed=$(( passed + 1 )) + + if [[ -n $tap ]]; then + _zunit_tap_success "$@" + return + fi + + echo "$(color green '✔') ${name}" +} + +### +# Output a failure message +### +function _zunit_failure() { + local message="$1" output="${(@)@:2}" + + failed=$(( failed + 1 )) + + [[ -n $output_text ]] && _zunit_tap_failure "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_failure "$@" >> $logfile_html + + if [[ -n $tap ]]; then + _zunit_tap_failure "$@" + else + echo "$(color red '✘' ${name})" + echo " $(color red underline ${message})" + echo " $(color red ${output})" + fi + + [[ -n $fail_fast ]] && _zunit_fail_shutdown +} + +### +# Output a error message +### +function _zunit_error() { + local message="$1" output="${(@)@:2}" + + errors=$(( errors + 1 )) + + [[ -n $output_text ]] && _zunit_tap_error "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_error "$@" >> $logfile_html + + if [[ -n $tap ]]; then + _zunit_tap_error "$@" + else + echo "$(color red '‼' ${name})" + echo " $(color red underline ${message})" + echo " $(color red ${output})" + fi + + [[ -n $fail_fast ]] && _zunit_fail_shutdown +} + +### +# Output a warning message +### +function _zunit_warn() { + local message="$@" + + warnings=$(( warnings + 1 )) + + [[ -n $output_text ]] && _zunit_tap_warn "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_warn "$@" >> $logfile_html + + if [[ -n $tap ]]; then + _zunit_tap_warn "$@" + return + fi + + echo "$(color yellow '‼') ${name}" + echo " $(color yellow underline ${message})" +} + +### +# Output a skipped test message +### +function _zunit_skip() { + local message="$@" + + skipped=$(( skipped + 1 )) + + [[ -n $output_text ]] && _zunit_tap_skip "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_skip "$@" >> $logfile_html + + if [[ -n $tap ]]; then + _zunit_tap_skip "$@" + return + fi + + echo "$(color magenta '•') Skipped: ${name}" + echo " # ${message}" +} diff --git a/src/helpers.zsh b/src/helpers.zsh new file mode 100644 index 0000000..759ccff --- /dev/null +++ b/src/helpers.zsh @@ -0,0 +1,122 @@ +################################ +# 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 name + local -a cmd lines + + # 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 full output in a variable + output=$("${cmd[@]}" 2>&1) + + # Get the process exit state + state="$?" + + # Store individual lines of output in an array + IFS=$'\n' + lines=($output) + + # 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 + local -a comparisons + + IFS=$'\n' + + comparisons=(${(@)@:3}) + + if [[ -z $assertion ]]; then + assertion=$value + value="" + fi + + if (( ! $+functions[_zunit_assert_${assertion}] )); then + echo "$(color red "Assertion $assertion does not exist")" + exit 127 + fi + + _zunit_assertion_count=$(( _zunit_assertion_count + 1 )) + + "_zunit_assert_${assertion}" $value ${(@f)comparisons[@]} + + local state=$? + + if [[ $state -ne 0 ]]; then + exit $state + fi + + IFS=$oldIFS +} + +### +# Mark the current test as skipped +### +function skip() { + # Exit code 48 will skip the test, so all we have to do + # to mark the test as skipped is exit. + # The reason for skipping is echoed to stdout first, so that + # it can be picked up by the error handler + echo "$@" + exit 48 +} diff --git a/src/reports/html.zsh b/src/reports/html.zsh new file mode 100644 index 0000000..d34b07e --- /dev/null +++ b/src/reports/html.zsh @@ -0,0 +1,72 @@ +######################################### +# Functions for handling HTML reporting # +######################################### + +function _zunit_html_header() { + echo ' ZUnit Test Results
    ' +} + +function _zunit_html_footer() { + integer elapsed=$(( end_time - start_time )) + output="
" + echo $output +} + +### +# Output a HTML success message +### +_zunit_html_success() { + echo "
  • +
    $name
    +
  • " +} + +### +# Output a HTML failure message +### +_zunit_html_failure() { + echo "
  • +
    $name
    +
    $message
    +
  • " +} + +### +# Output a HTML error message +### +_zunit_html_error() { + echo "
  • +
    $name
    +
    $message
    +
  • " +} + +### +# Output a HTML warning message +### +_zunit_html_warn() { + echo "
  • +
    $name
    + $message +
  • " +} + +### +# Output a HTML skipped test message +### +_zunit_html_skip() { + echo "
  • +
    $name
    + $message +
  • " +} + +### +# Output a HTML fatal error message +### +_zunit_html_fatal_error() { + message="$@" + echo "
  • +
    $message
    +
  • " +} diff --git a/src/reports/tap.zsh b/src/reports/tap.zsh new file mode 100644 index 0000000..896842d --- /dev/null +++ b/src/reports/tap.zsh @@ -0,0 +1,66 @@ +######################################## +# Functions for handling TAP reporting # +######################################## + +### +# Output a TAP compatible success message +### +function _zunit_tap_success() { + echo "ok ${total} - ${name}" +} + +### +# Output a TAP compatible failure message +### +function _zunit_tap_failure() { + local message="$@" + + echo "not ok ${total} - Failure: ${name}" + echo " ---" + echo " message: ${message}" + echo " severity: fail" + echo " ..." + + [[ -n $fail_fast ]] && echo "Bail out!" +} + +### +# Output a TAP compatible error message +### +function _zunit_tap_error() { + local message="$@" + + echo "not ok ${total} - Error: ${name}" + echo " ---" + echo " message: ${message}" + echo " severity: fail" + echo " ..." + + [[ -n $fail_fast ]] && echo "Bail out!" +} + +### +# Output a TAP compatible warning message +### +function _zunit_tap_warn() { + local message="$@" + + echo "ok ${total} - Warning: ${name}" + echo " ---" + echo " message: ${message}" + echo " severity: comment" + echo " ..." +} + +### +# Output a TAP compatible skipped test message +### +function _zunit_tap_skip() { + local message="$@" + + echo "ok ${total} - # SKIP ${name}" + echo " ---" + echo " message: ${message}" + echo " severity: comment" + echo " ..." +} diff --git a/src/zunit.zsh b/src/zunit.zsh new file mode 100755 index 0000000..2a66c76 --- /dev/null +++ b/src/zunit.zsh @@ -0,0 +1,115 @@ +#!/usr/bin/env zsh + +###################### +# Main zunit process # +###################### + +### +# Output usage information and exit +### +function _zunit_usage() { + echo "$(color yellow 'Usage:')" + echo " zunit [options] [command] [tests...]" + echo + echo "$(color yellow 'Commands:')" + echo " init Bootstrap zunit in a new project" + echo " run [tests...] Run tests" + echo + echo "$(color yellow 'Options:')" + 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" + echo " -t, --tap Output results in a TAP compatible format" + echo " --output-text Print results to a text log, in TAP compatible format" + echo " --output-html Print results to a HTML page" + echo " --allow-risky Supress warnings generated for risky tests" +} + +### +# Output the version number +### +function _zunit_version() { + echo '0.6.0' +} + +### +# The main zunit process +### +function _zunit() { + local help version ctx="$1" missing_dependencies=0 missing_config=1 + + if [[ -f .zunit.yml ]]; then + # Try and parse the config file within a subprocess, + # to avoid killing the main thread + $(eval $(_zunit_parse_yaml .zunit.yml 'zunit_config_') >/dev/null 2>&1) + if [[ $? -eq 0 ]]; then + # Perform the parse again, in context, so that the + # config vars are loaded into the enviroment + eval $(_zunit_parse_yaml .zunit.yml 'zunit_config_') >/dev/null 2>&1 + missing_config=0 + else + echo "\033[0;31mFailed to parse config file\033[0;m" >&2 + exit 1 + fi + fi + + $(type color >/dev/null 2>&1) + if [[ $? -ne 0 ]]; then + missing_dependencies=$(( missing_dependencies + 1 )) + echo "\033[0;31mMissing required dependency: Color - https://github.com/molovo/color\033[0;m" >&2 + fi + + $(type revolver >/dev/null 2>&1) + if [[ $? -ne 0 ]]; then + missing_dependencies=$(( missing_dependencies + 1 )) + echo "\033[0;31mMissing required dependency: Revolver - https://github.com/molovo/revolver\033[0;m" >&2 + fi + + [[ $missing_dependencies -gt 0 ]] && exit 1 + + zparseopts -D -E \ + h=help -help=help \ + v=version -version=version + + # If the version option is passed, + # output version information and exit + if [[ -n $version ]]; then + _zunit_version + exit 0 + fi + + case "$ctx" in + init ) + # If the help option is passed, + # output usage information and exit + if [[ -n $help ]]; then + _zunit_init_usage + exit 0 + fi + + _zunit_init "${(@)@:2}" + ;; + run ) + # If the help option is passed, + # output usage information and exit + if [[ -n $help ]]; then + _zunit_run_usage + exit 0 + fi + + _zunit_run "${(@)@:2}" + ;; + * ) + # If the help option is passed, + # output usage information and exit + if [[ -n $help ]]; then + _zunit_usage + exit 0 + fi + + _zunit_run "$@" + ;; + esac +} + +_zunit "$@" diff --git a/zunit b/zunit deleted file mode 100755 index 1cac956..0000000 --- a/zunit +++ /dev/null @@ -1,1412 +0,0 @@ -#!/usr/bin/env zsh - -################################ -# Internal assertion functions # -################################ - -function _zunit_html_header() { - echo ' ZUnit Test Results
      ' -} - -function _zunit_html_footer() { - integer elapsed=$(( end_time - start_time )) - output="
    " - echo $output -} - -### -# 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 - local -a array - array=(${(@)@:2}) - - for i in ${(@f)array}; do - [[ $i = $value ]] && found=1 - done - - - [[ $found -eq 1 ]] && return 0 - - echo "'$value' is not in (${(@f)array})" - exit 1 -} - -### -# Assert that a value is not found in an array -### -function _zunit_assert_not_in() { - local i found=0 value=$1 - local -a array - array=(${(@)@:2}) - - for i in ${(@f)array}; do - [[ $i = $value ]] && found=1 - done - - [[ $found -eq 0 ]] && return 0 - - echo "'$value' is in (${(@f)array})" - exit 1 -} - -### -# Assert that a value is a key in a hash -### -function _zunit_assert_is_key_in() { - local i found=0 value=$1 - local -A hash - hash=(${(@)@:2}) - - for k v in ${(@kv)hash}; do - [[ $k = $value ]] && found=1 - done - - [[ $found -eq 1 ]] && return 0 - - echo "'$value' is not a key in (${(@kv)hash})" - exit 1 -} - -### -# Assert that a value is not a key in a hash -### -function _zunit_assert_is_not_key_in() { - local i found=0 value=$1 - local -A hash - hash=(${(@)@:2}) - - for k v in ${(@kv)hash}; do - [[ $k = $value ]] && found=1 - done - - [[ $found -eq 0 ]] && return 0 - - echo "'$value' is a key in (${(@kv)hash})" - exit 1 -} - -### -# Assert that a value is a value in a hash -### -function _zunit_assert_is_value_in() { - local i found=0 value=$1 - local -A hash - hash=(${(@)@:2}) - - for k v in ${(@kv)hash}; do - [[ $v = $value ]] && found=1 - done - - [[ $found -eq 1 ]] && return 0 - - echo "'$value' is not a value in (${(@kv)hash})" - exit 1 -} - -### -# Assert that a value is not a value in a hash -### -function _zunit_assert_is_not_value_in() { - local i found=0 value=$1 - local -A hash - hash=(${(@)@:2}) - - for k v in ${(@kv)hash}; do - [[ $v = $value ]] && found=1 - done - - [[ $found -eq 0 ]] && return 0 - - echo "'$value' is a value in (${(@kv)hash})" - exit 1 -} - -### -# Assert the a path exists -### -function _zunit_assert_exists() { - local pathname=$1 filepath - - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi - - [[ -e "$filepath" ]] && return 0 - - echo "'$pathname' does not exist" - exit 1 -} - -### -# Assert the a path exists and is a file -### -function _zunit_assert_is_file() { - local pathname=$1 filepath - - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi - - [[ -f "$filepath" ]] && return 0 - - echo "'$pathname' 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 pathname=$1 filepath - - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/$pathname" - else - filepath="$pathname" - fi - - [[ -d "$filepath" ]] && return 0 - - echo "'$pathname' 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 pathname=$1 filepath - - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi - - [[ -h "$filepath" ]] && return 0 - - echo "'$pathname' 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 pathname=$1 filepath - - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi - - [[ -r "$filepath" ]] && return 0 - - echo "'$pathname' does not exist or is not readable" - exit 1 -} - -### -# Assert the a path exists and is writable -### -function _zunit_assert_is_writable() { - local pathname=$1 filepath - - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi - - [[ -w "$filepath" ]] && return 0 - - echo "'$pathname' does not exist or is not writable" - exit 1 -} - -### -# Assert the a path exists and is executable -### -function _zunit_assert_is_executable() { - local pathname=$1 filepath - - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi - - [[ -x "$filepath" ]] && return 0 - - echo "'$pathname' does not exist or is not executable" - 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 name - local -a cmd lines - - # 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 full output in a variable - output=$("${cmd[@]}" 2>&1) - - # Get the process exit state - state="$?" - - # Store individual lines of output in an array - IFS=$'\n' - lines=($output) - - # 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 - local -a comparisons - - IFS=$'\n' - - comparisons=(${(@)@:3}) - - if [[ -z $assertion ]]; then - assertion=$value - value="" - fi - - if (( ! $+functions[_zunit_assert_${assertion}] )); then - echo "$(color red "Assertion $assertion does not exist")" - exit 127 - fi - - _zunit_assertion_count=$(( _zunit_assertion_count + 1 )) - - "_zunit_assert_${assertion}" $value ${(@f)comparisons[@]} - - local state=$? - - if [[ $state -ne 0 ]]; then - exit $state - fi - - IFS=$oldIFS -} - -### -# Mark the current test as skipped -### -function skip() { - # Exit code 48 will skip the test, so all we have to do - # to mark the test as skipped is exit. - # The reason for skipping is echoed to stdout first, so that - # it can be picked up by the error handler - echo "$@" - exit 48 -} - -###################### -# Main zunit process # -###################### - -### -# Output usage information and exit -### -function _zunit_usage() { - echo "$(color yellow 'Usage:')" - echo " zunit [options] [command] [tests...]" - echo - echo "$(color yellow 'Commands:')" - echo " init Bootstrap zunit in a new project" - echo " run [tests...] Run tests" - echo - echo "$(color yellow 'Options:')" - 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" - echo " -t, --tap Output results in a TAP compatible format" - echo " --output-text Print results to a text log, in TAP compatible format" - echo " --output-html Print results to a HTML page" - echo " --allow-risky Supress warnings generated for risky tests" -} - -### -# Output usage information and exit -### -function _zunit_run_usage() { - echo "$(color yellow 'Usage:')" - echo " zunit run [options] [tests...]" - echo - echo "$(color yellow 'Options:')" - 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" - echo " -t, --tap Output results in a TAP compatible format" - echo " --output-text Print results to a text log, in TAP compatible format" - echo " --output-html Print results to a HTML page" - echo " --allow-risky Supress warnings generated for risky tests" -} - -### -# Output usage information and exit -### -function _zunit_init_usage() { - echo "$(color yellow 'Usage:')" - echo " zunit init [options]" - echo - echo "$(color yellow 'Options:')" - echo " -h, --help Output help text and exit" - echo " -v, --version Output version information and exit" - echo " -t, --travis Generate .travis.yml in project" -} - -### -# Shutdown testing early -# Usually called if --fail-fast is specified -### -function _zunit_fail_shutdown() { - # Kill the revolver process - [[ -z $tap ]] && revolver stop - - echo $(color red bold 'Execution halted after failure') - end_time=$((EPOCHREALTIME*1000)) - - [[ -z $tap ]] && _zunit_output_results - - # Print end of HTML report - if [[ -n $output_html ]]; then - name='Execution halted after failure' - _zunit_html_error >> $logfile_html - _zunit_html_footer >> $logfile_html - fi - - exit 1 -} - -### -# Output a HTML fatal error message -### -_zunit_html_fatal_error() { - message="$@" - echo "
  • -
    $message
    -
  • " -} - -### -# Output a TAP compatible success message -### -function _zunit_tap_success() { - echo "ok ${total} - ${name}" -} - -### -# Output a HTML success message -### -_zunit_html_success() { - echo "
  • -
    $name
    -
  • " -} - -### -# Output a success message -### -function _zunit_success() { - [[ -n $output_text ]] && _zunit_tap_success "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_success "$@" >> $logfile_html - - passed=$(( passed + 1 )) - - if [[ -n $tap ]]; then - _zunit_tap_success "$@" - return - fi - - echo "$(color green '✔') ${name}" -} - -### -# Output a TAP compatible failure message -### -function _zunit_tap_failure() { - local message="$@" - - echo "not ok ${total} - Failure: ${name}" - echo " ---" - echo " message: ${message}" - echo " severity: fail" - echo " ..." - - [[ -n $fail_fast ]] && echo "Bail out!" -} - -### -# Output a HTML failure message -### -_zunit_html_failure() { - echo "
  • -
    $name
    -
    $message
    -
  • " -} - -### -# Output a failure message -### -function _zunit_failure() { - local message="$1" output="${(@)@:2}" - - failed=$(( failed + 1 )) - - [[ -n $output_text ]] && _zunit_tap_failure "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_failure "$@" >> $logfile_html - - if [[ -n $tap ]]; then - _zunit_tap_failure "$@" - else - echo "$(color red '✘' ${name})" - echo " $(color red underline ${message})" - echo " $(color red ${output})" - fi - - [[ -n $fail_fast ]] && _zunit_fail_shutdown -} - -### -# Output a TAP compatible error message -### -function _zunit_tap_error() { - local message="$@" - - echo "not ok ${total} - Error: ${name}" - echo " ---" - echo " message: ${message}" - echo " severity: fail" - echo " ..." - - [[ -n $fail_fast ]] && echo "Bail out!" -} - -### -# Output a HTML error message -### -_zunit_html_error() { - echo "
  • -
    $name
    -
    $message
    -
  • " -} - -### -# Output a error message -### -function _zunit_error() { - local message="$1" output="${(@)@:2}" - - errors=$(( errors + 1 )) - - [[ -n $output_text ]] && _zunit_tap_error "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_error "$@" >> $logfile_html - - if [[ -n $tap ]]; then - _zunit_tap_error "$@" - else - echo "$(color red '‼' ${name})" - echo " $(color red underline ${message})" - echo " $(color red ${output})" - fi - - [[ -n $fail_fast ]] && _zunit_fail_shutdown -} - -### -# Output a TAP compatible warning message -### -function _zunit_tap_warn() { - local message="$@" - - echo "ok ${total} - Warning: ${name}" - echo " ---" - echo " message: ${message}" - echo " severity: comment" - echo " ..." -} - -### -# Output a HTML warning message -### -_zunit_html_warn() { - echo "
  • -
    $name
    - $message -
  • " -} - -### -# Output a warning message -### -function _zunit_warn() { - local message="$@" - - warnings=$(( warnings + 1 )) - - [[ -n $output_text ]] && _zunit_tap_warn "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_warn "$@" >> $logfile_html - - if [[ -n $tap ]]; then - _zunit_tap_warn "$@" - return - fi - - echo "$(color yellow '‼') ${name}" - echo " $(color yellow underline ${message})" -} - -### -# Output a TAP compatible skipped test message -### -function _zunit_tap_skip() { - local message="$@" - - echo "ok ${total} - # SKIP ${name}" - echo " ---" - echo " message: ${message}" - echo " severity: comment" - echo " ..." -} - -### -# Output a HTML skipped test message -### -_zunit_html_skip() { - echo "
  • -
    $name
    - $message -
  • " -} - -### -# Output a skipped test message -### -function _zunit_skip() { - local message="$@" - - skipped=$(( skipped + 1 )) - - [[ -n $output_text ]] && _zunit_tap_skip "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_skip "$@" >> $logfile_html - - if [[ -n $tap ]]; then - _zunit_tap_skip "$@" - return - fi - - echo "$(color magenta '•') Skipped: ${name}" - echo " # ${message}" -} - -### -# Format a ms timestamp in a human-readable format -### -function _zunit_human_time() { - local ms=$1 - local tmp=$(( $1 / 1000 )) - local days=$(( tmp / 60 / 60 / 24 )) - local hours=$(( tmp / 60 / 60 % 24 )) - local minutes=$(( tmp / 60 % 60 )) - local seconds=$(( tmp % 60 )) - (( $days > 0 )) && print -n "${days}d " - (( $hours > 0 )) && print -n "${hours}h " - (( $minutes > 0 )) && print -n "${minutes}m " - (( $seconds > 5 )) && print -n "${seconds}s " - (( $seconds < 30 )) && (( $seconds > 5 )) && print -n "$(( ms - $((seconds*1000)) ))ms" - (( $tmp <= 5 )) && print -n "${1}ms" -} - -### -# Output test results -### -function _zunit_output_results() { - integer elapsed=$(( end_time - start_time )) - echo - echo "$total tests run in $(_zunit_human_time $elapsed)" - echo - echo "$(color yellow underline 'Results') " - echo "$(color green '✔') Passed $passed " - echo "$(color red '✘') Failed $failed " - echo "$(color red '‼') Errors $errors " - echo "$(color magenta '•') Skipped $skipped " - echo "$(color yellow '‼') Warnings $warnings " - echo - - [[ -n $output_text ]] && echo "TAP report written at $PWD/$logfile_text" - [[ -n $output_html ]] && echo "HTML report written at $PWD/$logfile_html" -} - -### -# Execute a test and store the result -### -function _zunit_execute_test() { - local name="$1" body="$2" - - if [[ -n $body ]] && [[ -n $name ]]; then - # Update the progress indicator - [[ -z $tap ]] && revolver update "${name}" - - # 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 - integer _zunit_assertion_count=0 - local output - typeset -a lines - - if (( $+functions[__zunit_test_setup] )); then - __zunit_test_setup >/dev/null 2>&1 - fi - - ${body} - - if (( $+functions[__zunit_test_teardown] )); then - __zunit_test_teardown >/dev/null 2>&1 - fi - - [[ \$_zunit_assertion_count -gt 0 ]] || return 248 - }" - - # Quietly eval the body into a variable as a first test - output=$(eval "$(echo "$func")" 2>&1) - - total=$(( total + 1 )) - - # Check the status of the eval, and output any errors - if [[ $? -ne 0 ]]; then - _zunit_error 'Failed to parse test body' $output - - return 126 - 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 - _zunit_error 'Failed to parse test body' - - return 126 - fi - - autoload is-at-least - - # Check if a time limit has been specified - if is-at-least 5.1.0 && [[ -n $zunit_config_time_limit ]]; then - # Create a wrapper function around the test - __zunit_async_test_wrapper() { - local pid - - # Get the current timestamp, and the time limit, and use those to - # work out the kill time for the sub process - integer time_limit=$(( ${zunit_config_time_limit:-30} * 1000 )) - integer time=$(( EPOCHREALTIME * 1000 )) - integer kill_time=$(( $time + $time_limit )) - - # Launch the test function asynchronously and store its PID - __zunit_tmp_test_function & - pid=$! - - # While the child process is still running - while kill -0 $pid >/dev/null 2>&1; do - # Check that the kill time has not yet been reached - time=$(( EPOCHREALTIME * 1000 )) - if [[ $time -gt $kill_time ]]; then - # The kill time has been reached, kill the child process, - # and exit the wrapper function - kill -9 $pid >/dev/null 2>&1 - exit 78 - fi - done - - # Use wait to get the exit code from the background process, - # and return that so that the test result can be deduced - wait $pid - return $? - } - - # Launch the async wrapper, and capture the output in a variable - output="$(__zunit_async_test_wrapper 2>&1)" - else - # Launch the test, and capture the output in a variable - output="$(__zunit_tmp_test_function 2>&1)" - fi - - # Output the result to the user - state=$? - if [[ $state -eq 48 ]]; then - _zunit_skip $output - - return - elif [[ $state -eq 78 ]]; then - _zunit_error "Test took too long to run. Terminated after ${zunit_config_time_limit:-30} seconds" $output - - return - elif [[ -z $allow_risky && $state -eq 248 ]]; then - _zunit_warn 'No assertions were run, test is risky' - - return - elif [[ -n $allow_risky && $state -eq 248 ]] || [[ $state -eq 0 ]]; then - _zunit_success - - return - else - _zunit_failure $output - - 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")" - local -a lines tests test_names - tests=() - test_names=() - - # Update status message - [[ -z $tap ]] && revolver update "Loading tests from $testfile" - - # A regex pattern to match test declarations - pattern='^ *@test *([^ ].*) *\{ *(.*)$' - - # Loop through each of the lines in the file - local oldIFS=$IFS - IFS=$'\n' lines=($(cat $testfile)) - IFS=$oldIFS - 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 ))]}" - test_names=($test_names $testname) - tests[${#test_names}]='' - 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[${#test_names}]+="$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 - _zunit_error "Failed to parse setup method" $output - - return 126 - 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 - _zunit_error "Failed to parse setup method" - - return 126 - 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 - _zunit_error "Failed to parse teardown method" $output - - return 126 - 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 - _zunit_error "Failed to parse teardown method" - - return 126 - fi - fi - - # Loop through each of the tests and execute it - integer i=1 - local name body - for name in "${test_names[@]}"; do - body="${tests[$i]}" - _zunit_execute_test "$name" "$body" - i=$(( i + 1 )) - done - - (( $+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 -mindepth 1 -maxdepth 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 126 - fi - - # The file could not be found, so we throw a fatal error - echo $(color red "Test file or directory '$argument' could not be found") >&2 - exit 126 -} - -### -# Parse a YAML config file -# Based on https://gist.github.com/pkuczynski/8665367 -### -function _zunit_parse_yaml() { - local s w fs prefix=$2 - s='[[:space:]]*' - w='[a-zA-Z0-9_]*' - fs="$(echo @|tr @ '\034')" - sed -ne "s|^\(${s}\)\(${w}\)${s}:${s}\"\(.*\)\"${s}\$|\1${fs}\2${fs}\3|p" \ - -e "s|^\(${s}\)\(${w}\)${s}[:-]${s}\(.*\)${s}\$|\1${fs}\2${fs}\3|p" "$1" | - awk -F"${fs}" '{ - indent = length($1)/2; - vname[indent] = $2; - for (i in vname) {if (i > indent) {delete vname[i]}} - if (length($3) > 0) { - vn=""; for (i=0; i&2 - exit 1 - else - echo "$yaml" > "$PWD/.zunit.yml" - fi - - if [[ -d "$PWD/tests" ]]; then - echo $(color red "Directory already exists at $PWD/tests") >&2 - exit 1 - else - mkdir -p tests/_{output,support} - touch tests/_{output,support}/.gitkeep - - echo "$bootstrap" > "$PWD/tests/_support/bootstrap" - echo "$example" > "$PWD/tests/example.zunit" - fi - - #statements - if [[ -n $with_travis ]]; then - if [[ -f "$PWD/.travis.yml" ]]; then - echo $(color red "Travis config already exists at $PWD/.travis.yml") >&2 - exit 1 - else - echo "$travis_yml" > "$PWD/.travis.yml" - fi - fi -} - -### -# Run tests -### -function _zunit_run() { - local -a arguments testfiles - local fail_fast tap allow_risky - local output_text logfile_text output_html logfile_html - - zmodload zsh/datetime - local start_time=$((EPOCHREALTIME*1000)) end_time - - zparseopts -D -E \ - h=help -help=help \ - v=version -version=version \ - f=fail_fast -fail-fast=fail_fast \ - t=tap -tap=tap \ - -output-text=output_text \ - -output-html=output_html \ - -allow-risky=allow_risky - - if [[ -n $tap ]] || [[ "$zunit_config_tap" = "true" ]]; then - tap=1 - echo 'TAP version 13' - fi - - if [[ -z $tap ]]; then - # Print version information - echo $(color yellow 'Launching ZUnit') - echo "ZUnit: $(_zunit_version)" - echo "ZSH: $(zsh --version)" - echo - fi - - if [[ -n $output_text ]]; then - if [[ $missing_config -eq 1 ]]; then - echo $(color red '.zunit.yml could not be found. Run `zulu init`') - exit 1 - fi - - if [[ -z $zunit_config_directories_output ]]; then - echo $(color red 'Output directory must be specified in .zunit.yml') - exit 1 - fi - fi - - if [[ -n $output_text ]]; then - logfile_text="$zunit_config_directories_output/output.txt" - echo 'TAP version 13' > $logfile_text - fi - - if [[ -n $output_html ]]; then - logfile_html="$zunit_config_directories_output/output.html" - _zunit_html_header > $logfile_html - fi - - if [[ -n $zunit_config_directories_support ]]; then - local support="$zunit_config_directories_support" - if [[ ! -d $support ]]; then - echo $(color red "Support directory at $support is missing") - exit 1 - fi - - if [[ -f "$support/bootstrap" ]]; then - source "$support/bootstrap" - echo "$(color green '✔') Sourced bootstrap script $support/bootstrap" - fi - fi - - arguments=("$@") - testfiles=() - - # Start the progress indicator - [[ -z $tap ]] && revolver start 'Loading tests' - - # If no arguments are passed, use the current directory - if [[ ${#arguments} -eq 0 ]]; then - if [[ -n $zunit_config_directories_tests ]]; then - arguments=("$zunit_config_directories_tests") - else - arguments=("tests") - fi - 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 - local total=0 passed=0 failed=0 errors=0 warnings=0 skipped=0 - for testfile in $testfiles; do - _zunit_run_testfile $testfile - done - - end_time=$((EPOCHREALTIME*1000)) - - [[ -n $tap ]] && echo "1..$total" - [[ -n $output_text ]] && echo "1..$total" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_footer >> $logfile_html - - [[ -z $tap ]] && _zunit_output_results && revolver stop - - [[ $(( $passed + $skipped )) -eq $total ]] -} - -### -# Output the version number -### -function _zunit_version() { - echo '0.6.0' -} - -### -# The main zunit process -### -function _zunit() { - local help version ctx="$1" missing_dependencies=0 missing_config=1 - - if [[ -f .zunit.yml ]]; then - # Try and parse the config file within a subprocess, - # to avoid killing the main thread - $(eval $(_zunit_parse_yaml .zunit.yml 'zunit_config_') >/dev/null 2>&1) - if [[ $? -eq 0 ]]; then - # Perform the parse again, in context, so that the - # config vars are loaded into the enviroment - eval $(_zunit_parse_yaml .zunit.yml 'zunit_config_') >/dev/null 2>&1 - missing_config=0 - else - echo "\033[0;31mFailed to parse config file\033[0;m" >&2 - exit 1 - fi - fi - - $(type color >/dev/null 2>&1) - if [[ $? -ne 0 ]]; then - missing_dependencies=$(( missing_dependencies + 1 )) - echo "\033[0;31mMissing required dependency: Color - https://github.com/molovo/color\033[0;m" >&2 - fi - - $(type revolver >/dev/null 2>&1) - if [[ $? -ne 0 ]]; then - missing_dependencies=$(( missing_dependencies + 1 )) - echo "\033[0;31mMissing required dependency: Revolver - https://github.com/molovo/revolver\033[0;m" >&2 - fi - - [[ $missing_dependencies -gt 0 ]] && exit 1 - - zparseopts -D -E \ - h=help -help=help \ - v=version -version=version - - # If the version option is passed, - # output version information and exit - if [[ -n $version ]]; then - _zunit_version - exit 0 - fi - - case "$ctx" in - init ) - # If the help option is passed, - # output usage information and exit - if [[ -n $help ]]; then - _zunit_init_usage - exit 0 - fi - - _zunit_init "${(@)@:2}" - ;; - run ) - # If the help option is passed, - # output usage information and exit - if [[ -n $help ]]; then - _zunit_run_usage - exit 0 - fi - - _zunit_run "${(@)@:2}" - ;; - * ) - # If the help option is passed, - # output usage information and exit - if [[ -n $help ]]; then - _zunit_usage - exit 0 - fi - - _zunit_run "$@" - ;; - esac -} - -_zunit "$@"