From 7c4cc63ad92dc6b5b53eeeb2b233b82a4cfd68f2 Mon Sep 17 00:00:00 2001 From: vladislav doster <10052309+vladdoster@users.noreply.github.com> Date: Fri, 15 Dec 2023 23:11:09 -0600 Subject: [PATCH] feat: format log messages (#11) * feat: add vim folds and log formatting Signed-off-by: Vladislav Doster --- build.zsh | 1 - src/assertions.zsh | 647 ++++++++++++------------- src/commands/init.zsh | 177 ++++--- src/commands/run.zsh | 1067 ++++++++++++++++++++--------------------- src/events.zsh | 207 ++++---- src/helpers.zsh | 541 ++++++++++----------- src/zunit.zsh | 163 +++---- 7 files changed, 1352 insertions(+), 1451 deletions(-) diff --git a/build.zsh b/build.zsh index 39af127..1be8762 100755 --- a/build.zsh +++ b/build.zsh @@ -10,7 +10,6 @@ setopt extendedglob warncreateglobal typesetsilent noshortloops local ZUNIT_BIN="${${0:h}:A}/zunit" - # Clear the file to start with cat /dev/null > "$ZUNIT_BIN" diff --git a/src/assertions.zsh b/src/assertions.zsh index 0a05270..bdd020b 100644 --- a/src/assertions.zsh +++ b/src/assertions.zsh @@ -1,437 +1,382 @@ +# vim: ft=zsh sw=4 ts=4 et foldmarker=[[[,]]] foldmethod=marker + ################################ # Internal assertion functions # ################################ -### -# Assert one string is a substring of another -### -function _zunit_assert_is_substring_of() { - local value=$1 comparison=$2 - - [[ "$comparison" = *"$value"* ]] && return 0 - - echo "'$value' is not a substring of '$comparison'" - exit 1 -} - -### -# Assert one string is not a substring of another -### -function _zunit_assert_is_not_substring_of() { - local value=$1 comparison=$2 - - [[ "$comparison" != *"$value"* ]] && return 0 - - echo "'$value' is a substring of '$comparison'" - exit 1 -} - -### +# FUNCTION: _zunit_assert_contains [[[ # Assert one string contains another -### function _zunit_assert_contains() { - local value=$1 comparison=$2 + local value=$1 comparison=$2 + + [[ "$value" = *"$comparison"* ]] && return 0 - [[ "$value" = *"$comparison"* ]] && return 0 + echo "'$value' does not contain '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_different_to [[[ +# Assert that two string are different +function _zunit_assert_different_to() { + local value=$1 comparison=$2 - echo "'$value' does not contain '$comparison'" - exit 1 -} + [[ $value != $comparison ]] && return 0 -### + echo "'$value' is the same as '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_does_not_contain [[[ # Assert one string does not contain another -### function _zunit_assert_does_not_contain() { - local value=$1 comparison=$2 + local value=$1 comparison=$2 - [[ "$value" != *"$comparison"* ]] && return 0 + [[ "$value" != *"$comparison"* ]] && return 0 + + echo "'$value' contains '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_does_not_match [[[ +# Assert that the value does not match a regex pattern +function _zunit_assert_does_not_match() { + local value=$1 pattern=$2 - echo "'$value' contains '$comparison'" - exit 1 -} + [[ ! $value =~ $pattern ]] && return 0 -### + echo "'$value' matches /$pattern/" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_equals [[[ # 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 an integer is positive -### -function _zunit_assert_is_positive() { - local value=$1 comparison=$2 - - [[ $value -gt 0 ]] && return 0 - - echo "'$value' is not positive" - exit 1 -} - -### -# Assert that an integer is negative -### -function _zunit_assert_is_negative() { - local value=$1 comparison=$2 + local value=$1 comparison=$2 - [[ $value -lt 0 ]] && return 0 + [[ $value -eq $comparison ]] && return 0 - echo "'$value' is not negative" - exit 1 -} - -### -# Assert that an integer is greater than the comparison -### -function _zunit_assert_is_greater_than() { - local value=$1 comparison=$2 - - [[ $value -gt $comparison ]] && return 0 - - echo "'$value' is not greater than '$comparison'" - exit 1 -} + echo "'$value' is not equal to '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_exists [[[ +# Assert the a path exists +function _zunit_assert_exists() { + local pathname=$1 filepath -### -# Assert that an integer is less than the comparison -### -function _zunit_assert_is_less_than() { - local value=$1 comparison=$2 + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi - [[ $value -lt $comparison ]] && return 0 + [[ -e "$filepath" ]] && return 0 - echo "'$value' is not less than '$comparison'" - exit 1 -} + echo "'$pathname' does not exist" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_in [[[ +# Assert that a value is found in an array +function _zunit_assert_in() { + local i found=0 value=$1 + local -a array + array=(${(@)@:2}) -### -# Assert that two string are the same -### -function _zunit_assert_same_as() { - local value=$1 comparison=$2 + for i in ${(@f)array}; do + [[ $i = $value ]] && found=1 + done - [[ $value = $comparison ]] && return 0 - echo "'$value' is not the same as '$comparison'" - exit 1 -} + [[ $found -eq 1 ]] && return 0 -### -# Assert that two string are different -### -function _zunit_assert_different_to() { - local value=$1 comparison=$2 + echo "'$value' is not in (${(@f)array})" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_dir [[[ +# Assert the a path exists and is a directory +function _zunit_assert_is_dir() { + local pathname=$1 filepath - [[ $value != $comparison ]] && return 0 + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/$pathname" + else + filepath="$pathname" + fi - echo "'$value' is the same as '$comparison'" - exit 1 -} + [[ -d "$filepath" ]] && return 0 -### + echo "'$pathname' does not exist or is not a directory" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_empty [[[ # Assert that a value is empty -### function _zunit_assert_is_empty() { - local value=$1 - - [[ -z ${value[@]} ]] && return 0 + local value=$1 - echo "'${value[@]}' is not empty" - exit 1 -} + [[ -z ${value[@]} ]] && return 0 -### -# Assert that a value is not empty -### -function _zunit_assert_is_not_empty() { - local value=$1 + echo "'${value[@]}' is not empty" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_executable [[[ +# Assert the a path exists and is executable +function _zunit_assert_is_executable() { + local pathname=$1 filepath - [[ -n ${value[@]} ]] && return 0 + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi - echo "value is empty" - exit 1 -} + [[ -x "$filepath" ]] && return 0 -### -# Assert that the value matches a regex pattern -### -function _zunit_assert_matches() { - local value=$1 pattern=$2 + echo "'$pathname' does not exist or is not executable" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_file [[[ +# Assert the a path exists and is a file +function _zunit_assert_is_file() { + local pathname=$1 filepath - [[ $value =~ $pattern ]] && return 0 + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi - echo "'$value' does not match /$pattern/" - exit 1 -} + [[ -f "$filepath" ]] && return 0 -### -# 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 -} + echo "'$pathname' does not exist or is not a file" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_greater_than [[[ +# Assert that an integer is greater than the comparison +function _zunit_assert_is_greater_than() { + local value=$1 comparison=$2 -### -# Assert that a value is found in an array -### -function _zunit_assert_in() { - local i found=0 value=$1 - local -a array - array=(${(@)@:2}) + [[ $value -gt $comparison ]] && return 0 - for i in ${(@f)array}; do - [[ $i = $value ]] && found=1 - done + echo "'$value' is not greater than '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_key_in [[[ +# 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 + [[ $found -eq 1 ]] && return 0 - echo "'$value' is not in (${(@f)array})" - exit 1 -} + echo "'$value' is not a key in (${(@kv)hash})" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_less_than [[[ +# Assert that an integer is less than the comparison +function _zunit_assert_is_less_than() { + local value=$1 comparison=$2 -### -# 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}) + [[ $value -lt $comparison ]] && return 0 - for i in ${(@f)array}; do - [[ $i = $value ]] && found=1 - done + echo "'$value' is not less than '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_link [[[ +# Assert the a path exists and is a symbolic link +function _zunit_assert_is_link() { + local pathname=$1 filepath - [[ $found -eq 0 ]] && return 0 + # If filepath is relative, prepend the test directory + if [[ "${pathname:0:1}" != "/" ]]; then + filepath="$testdir/${pathname}" + else + filepath="$pathname" + fi - echo "'$value' is in (${(@f)array})" - exit 1 -} + [[ -h "$filepath" ]] && return 0 -### -# 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}) + echo "'$pathname' does not exist or is not a symbolic link" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_negative [[[ +# Assert that an integer is negative +function _zunit_assert_is_negative() { + local value=$1 comparison=$2 - for k v in ${(@kv)hash}; do - [[ $k = $value ]] && found=1 - done + [[ $value -lt 0 ]] && return 0 - [[ $found -eq 1 ]] && return 0 + echo "'$value' is not negative" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_not_empty [[[ +# Assert that a value is not empty +function _zunit_assert_is_not_empty() { + local value=$1 - echo "'$value' is not a key in (${(@kv)hash})" - exit 1 -} + [[ -n ${value[@]} ]] && return 0 -### + echo "value is empty" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_not_key_in [[[ # 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 -} + local i found=0 value=$1 + local -A hash + hash=(${(@)@:2}) -### -# 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 + [[ $k = $value ]] && found=1 + done - for k v in ${(@kv)hash}; do - [[ $v = $value ]] && found=1 - done + [[ $found -eq 0 ]] && return 0 - [[ $found -eq 1 ]] && return 0 + echo "'$value' is a key in (${(@kv)hash})" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_not_substring_of [[[ +# Assert one string is not a substring of another +function _zunit_assert_is_not_substring_of() { + local value=$1 comparison=$2 - echo "'$value' is not a value in (${(@kv)hash})" - exit 1 -} + [[ "$comparison" != *"$value"* ]] && return 0 -### + echo "'$value' is a substring of '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_not_value_in [[[ # 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 + local i found=0 value=$1 + local -A hash + hash=(${(@)@:2}) - [[ -e "$filepath" ]] && return 0 + for k v in ${(@kv)hash}; do + [[ $v = $value ]] && found=1 + done - echo "'$pathname' does not exist" - exit 1 -} + [[ $found -eq 0 ]] && return 0 -### -# 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 + echo "'$value' is a value in (${(@kv)hash})" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_positive [[[ +# Assert that an integer is positive +function _zunit_assert_is_positive() { + local value=$1 comparison=$2 - [[ -f "$filepath" ]] && return 0 + [[ $value -gt 0 ]] && return 0 - echo "'$pathname' does not exist or is not a file" - exit 1 -} + echo "'$value' is not positive" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_readable [[[ +# Assert the a path exists and is readable +function _zunit_assert_is_readable() { + local pathname=$1 filepath -### -# 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 - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/$pathname" - else - filepath="$pathname" - fi + [[ -r "$filepath" ]] && return 0 - [[ -d "$filepath" ]] && return 0 + echo "'$pathname' does not exist or is not readable" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_substring_of [[[ +# Assert one string is a substring of another +function _zunit_assert_is_substring_of() { + local value=$1 comparison=$2 - echo "'$pathname' does not exist or is not a directory" - exit 1 -} + [[ "$comparison" = *"$value"* ]] && return 0 -### -# Assert the a path exists and is a symbolic link -### -function _zunit_assert_is_link() { - local pathname=$1 filepath + echo "'$value' is not a substring of '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_value_in [[[ +# 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}) - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi + for k v in ${(@kv)hash}; do + [[ $v = $value ]] && found=1 + done - [[ -h "$filepath" ]] && return 0 + [[ $found -eq 1 ]] && return 0 - echo "'$pathname' does not exist or is not a symbolic link" - exit 1 -} + echo "'$value' is not a value in (${(@kv)hash})" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_is_writable [[[ +# Assert the a path exists and is writable +function _zunit_assert_is_writable() { + local pathname=$1 filepath -### -# 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 - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi + [[ -w "$filepath" ]] && return 0 - [[ -r "$filepath" ]] && return 0 + echo "'$pathname' does not exist or is not writable" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_matches [[[ +# Assert that the value matches a regex pattern +function _zunit_assert_matches() { + local value=$1 pattern=$2 - echo "'$pathname' does not exist or is not readable" - exit 1 -} + [[ $value =~ $pattern ]] && return 0 -### -# Assert the a path exists and is writable -### -function _zunit_assert_is_writable() { - local pathname=$1 filepath + echo "'$value' does not match /$pattern/" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_not_equal_to [[[ +# Assert that two integers are not equal +function _zunit_assert_not_equal_to() { + local value=$1 comparison=$2 - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi + [[ $value -ne $comparison ]] && return 0 - [[ -w "$filepath" ]] && return 0 + echo "'$value' is equal to '$comparison'" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_not_in [[[ +# 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}) - echo "'$pathname' does not exist or is not writable" - exit 1 -} + for i in ${(@f)array}; do + [[ $i = $value ]] && found=1 + done -### -# Assert the a path exists and is executable -### -function _zunit_assert_is_executable() { - local pathname=$1 filepath + [[ $found -eq 0 ]] && return 0 - # If filepath is relative, prepend the test directory - if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" - else - filepath="$pathname" - fi + echo "'$value' is in (${(@f)array})" + exit 1 +} # ]]] +# FUNCTION: _zunit_assert_same_as [[[ +# Assert that two string are the same +function _zunit_assert_same_as() { + local value=$1 comparison=$2 - [[ -x "$filepath" ]] && return 0 + [[ $value = $comparison ]] && return 0 - echo "'$pathname' does not exist or is not executable" - exit 1 -} + echo "'$value' is not the same as '$comparison'" + exit 1 +} # ]]] diff --git a/src/commands/init.zsh b/src/commands/init.zsh index f54b900..be00af8 100644 --- a/src/commands/init.zsh +++ b/src/commands/init.zsh @@ -1,32 +1,31 @@ +# vim: ft=zsh sw=4 ts=4 et foldmarker=[[[,]]] foldmethod=marker + ############################ # The 'zunit init' command # ############################ -### +# FUNCTION: _zunit_init_usage [[[ # 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" -} - -### + 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" +} # ]]] +# FUNCTION: _zunit_parse_yaml [[[ # 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}" '{ + 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]}} @@ -34,86 +33,86 @@ function _zunit_parse_yaml() { vn=""; for (i=0; i .bin/zunit - - curl -L https://raw.githubusercontent.com/molovo/revolver/master/revolver > .bin/revolver - - curl -L https://raw.githubusercontent.com/molovo/color/master/color.zsh > .bin/color -before_script: - - chmod u+x .bin/{color,revolver,zunit} - - export PATH=\"\$PWD/.bin:\$PATH\" -script: zunit" + zsh + install: + - mkdir .bin + - curl -L https://github.com/zunit-zsh/zunit/releases/download/v$(_zunit_version)/zunit > .bin/zunit + - curl -L https://raw.githubusercontent.com/molovo/revolver/master/revolver > .bin/revolver + - curl -L https://raw.githubusercontent.com/molovo/color/master/color.zsh > .bin/color + before_script: + - chmod u+x .bin/{color,revolver,zunit} + - export PATH=\"\$PWD/.bin:\$PATH\" + script: zunit" - # Check that a config file doesn't already exist so that - # we don't overwrite it - if [[ -f "$PWD/.zunit.yml" ]]; then - echo $(color yellow "ZUnit config file already exists at $PWD/.zunit.yml. Skipping...") - else - # Write the contents to the config file - echo "Writing ZUnit config file to $PWD/.zunit.yml" - echo "$yaml" > "$PWD/.zunit.yml" - fi - - # Check that the tests directory doesn't already exist so that - # we don't overwrite it - if [[ -d "$PWD/tests" ]]; then - echo $(color yellow "Test directory already exists at $PWD/tests. Skipping...") - else - echo "Creating test directory at $PWD/tests" - # Create the directory structure for tests - mkdir -p tests/_{output,support} - touch tests/_{output,support}/.gitkeep - - # Save the bootstrap script and example test - echo "$bootstrap" > "$PWD/tests/_support/bootstrap" - echo "$example" > "$PWD/tests/example.zunit" - fi + # Check that a config file doesn't already exist so that + # we don't overwrite it + if [[ -f "$PWD/.zunit.yml" ]]; then + echo $(color yellow "ZUnit config file already exists at $PWD/.zunit.yml. Skipping...") + else + # Write the contents to the config file + echo "Writing ZUnit config file to $PWD/.zunit.yml" + echo "$yaml" > "$PWD/.zunit.yml" + fi - # If travis config has been requested - if [[ -n $with_travis ]]; then - # Check that a travis config doesn't already exist so that + # Check that the tests directory doesn't already exist so that # we don't overwrite it - if [[ -f "$PWD/.travis.yml" ]]; then - echo $(color yellow "Travis config already exists at $PWD/.travis.yml. Skipping...") + if [[ -d "$PWD/tests" ]]; then + echo $(color yellow "Test directory already exists at $PWD/tests. Skipping...") else - echo "Writing Travis CI config to $PWD/.travis.yml" - # Write the contents to the config file - echo "$travis_yml" > "$PWD/.travis.yml" + echo "Creating test directory at $PWD/tests" + # Create the directory structure for tests + mkdir -p tests/_{output,support} + touch tests/_{output,support}/.gitkeep + + # Save the bootstrap script and example test + echo "$bootstrap" > "$PWD/tests/_support/bootstrap" + echo "$example" > "$PWD/tests/example.zunit" + fi + + # If travis config has been requested + if [[ -n $with_travis ]]; then + # Check that a travis config doesn't already exist so that + # we don't overwrite it + if [[ -f "$PWD/.travis.yml" ]]; then + echo $(color yellow "Travis config already exists at $PWD/.travis.yml. Skipping...") + else + echo "Writing Travis CI config to $PWD/.travis.yml" + # Write the contents to the config file + echo "$travis_yml" > "$PWD/.travis.yml" + fi fi - fi -} +} # ]]] diff --git a/src/commands/run.zsh b/src/commands/run.zsh index ef3118f..b652854 100644 --- a/src/commands/run.zsh +++ b/src/commands/run.zsh @@ -1,583 +1,572 @@ +# vim: ft=zsh sw=4 ts=4 et foldmarker=[[[,]]] foldmethod=marker + ########################### # 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 " -f, --fail-fast Stop the test runner immediately after the first failure" - echo " -r --revolver Run tests with revolver spinner" - echo " -t, --tap Output results in a TAP compatible format" - echo " -v, --version Output version information and exit" - echo " --allow-risky Supress warnings generated for risky tests" - echo " --output-html Print results to a HTML page" - echo " --output-text Print results to a text log, in TAP compatible format" - echo " --time-limit Set a time limit of n seconds for each test" - echo " --verbose Prints full output from each test" -} - -### +# FUNCTION: _zunit_encode_test_name [[[ +# 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 ' ' "-" +} # ]]] +# FUNCTION: _zunit_execute_test [[[ +# 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 + # Make sure we don't already have a function defined + (( $+functions[__zunit_tmp_test_function] )) && unfunction __zunit_tmp_test_function + # Create a wrapper function with our test body inside it + func="function __zunit_tmp_test_function() { + # Exit on errors. We do this so that execution stops immediately, + # and the error will be reported back to the test runner + setopt ERR_EXIT + + # Add an exit handler which calls the teardown function if it is + # defined and the test exits early + if (( \$+functions[__zunit_test_teardown] )); then + zshexit() { + __zunit_test_teardown 2>&1 + } + fi + + # Create some local variables to store test state in + integer _zunit_assertion_count=0 + integer state + local output + typeset -a lines + + # If a setup function is defined, run it now + if (( \$+functions[__zunit_test_setup] )); then + __zunit_test_setup 2>&1 + fi + + # The test body is printed here, so when we eval the wrapper + # function it will be read as part of the body of this function + ${body} + + # If a teardown function is defined, run it now + if (( \$+functions[__zunit_test_teardown] )); then + __zunit_test_teardown 2>&1 + fi + + # Remove the error handler + zshexit() {} + + # Check the assertion count, and if it is 0, return + # the warning exit code + [[ \$_zunit_assertion_count -gt 0 ]] || return 248 + }" + + # Increment the test count + total=$(( total + 1 )) + + # 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 + _zunit_error "Failed to parse ${name} 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 ${name} test body" + return 126 + fi + + # Check if a time limit has been specified. We only do this if + # the ZSH version is at least 5.1.0, since older versions of ZSH + # are unable to handle asynchronous processes in the way we need + autoload is-at-least + if is-at-least 5.1.0 && [[ -n ${time_limit:#0} ]]; then + # Create another wrapper function around the test + __zunit_async_test_wrapper() { + local pid + # Get the current timestamp, and the time limit in ms, and use + # those to work out the kill time for the sub process + integer time_limit_ms=$(( time_limit * 1000 )) + integer time=$(( EPOCHREALTIME * 1000 )) + integer kill_time=$(( $time + $time_limit_ms )) + # 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 + echo "${name} test exceeded time limit. Terminated after ${time_limit} seconds" + 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 $output + return + elif [[ -z $allow_risky && $state -eq 248 ]]; then + # If --verbose is specified, print test output to screen + [[ -n $verbose && -n $output ]] && echo $output + _zunit_warn 'No assertions were run, ${name} test considered risky' + return + elif [[ -n $allow_risky && $state -eq 248 ]] || [[ $state -eq 0 ]]; then + # If --verbose is specified, print test output to screen + [[ -n $verbose && -n $output ]] && echo $output + _zunit_success + return + else + _zunit_failure $output + return 1 + fi + fi +} # ]]] +# FUNCTION: _zunit_human_time [[[ # Format a ms timestamp in a human-readable format -### function _zunit_human_time() { - 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 )) - local ms=$(( $1 % 1000 )) - (( $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}ms" - (( $seconds <= 5 )) && print -n "${1}ms" -} - -### + 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 )) + local ms=$(( $1 % 1000 )) + (( $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}ms" + (( $seconds <= 5 )) && print -n "${1}ms" +} # ]]] +# FUNCTION: _zunit_output_results [[[ # 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 - # Make sure we don't already have a function defined - (( $+functions[__zunit_tmp_test_function] )) && unfunction __zunit_tmp_test_function - # Create a wrapper function with our test body inside it - func="function __zunit_tmp_test_function() { - # Exit on errors. We do this so that execution stops immediately, - # and the error will be reported back to the test runner - setopt ERR_EXIT - - # Add an exit handler which calls the teardown function if it is - # defined and the test exits early - if (( \$+functions[__zunit_test_teardown] )); then - zshexit() { - __zunit_test_teardown 2>&1 - } - fi - - # Create some local variables to store test state in - integer _zunit_assertion_count=0 - integer state - local output - typeset -a lines - - # If a setup function is defined, run it now - if (( \$+functions[__zunit_test_setup] )); then - __zunit_test_setup 2>&1 - fi - - # The test body is printed here, so when we eval the wrapper - # function it will be read as part of the body of this function - ${body} - - # If a teardown function is defined, run it now - if (( \$+functions[__zunit_test_teardown] )); then - __zunit_test_teardown 2>&1 - fi - - # Remove the error handler - zshexit() {} - - # Check the assertion count, and if it is 0, return - # the warning exit code - [[ \$_zunit_assertion_count -gt 0 ]] || return 248 - }" - - # Increment the test count - total=$(( total + 1 )) - - # 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 - _zunit_error "Failed to parse ${name} test body" $output - return 126 + integer elapsed=$(( end_time - start_time )) + print ' ' + print -PR "= "$'\e[1;4m'"ZUnit Results"$'\e[0m'" =====" + print -Pr "%F{green}Passed%f: ${#passed}/${total}" + print -Pr "%F{red}Errors%f: ${#errors} $( (( $#errors )) && print "(${(j:, :)errors})" )" + print -Pr "%F{red}Failed%f: ${#failed} $( (( $#failed )) && print "(${(j:, :)failed})" )" + print -Pr "%F{yellow}Warnings%f: ${#warnings} $( (( $#warnings )) && print "(${(j:, :)warnings})" )" + print -Pr "%F{white}Skipped%f: ${#skipped} $( (( $#skipped )) && print "(${(j:, :)skipped})" )" + print ' ' + print -Pr "$total tests ran in $(_zunit_human_time $elapsed)" + print -Pr "%B======================%b" + print ' ' + + [[ -n $output_text ]] && echo "TAP report written at $PWD/$logfile_text" + [[ -n $output_html ]] && echo "HTML report written at $PWD/$logfile_html" +} # ]]] +# FUNCTION: _zunit_parse_argument [[[ +# Parse a list of arguments +function _zunit_parse_argument() { + local -a bits; bits=("${(s/@/)1}") + local argument="$bits[1]" test_name="$bits[2]" + + # 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 - # 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 + # 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 + # Skip further processing for files without a .zunit extension + if [[ -f $file && $file != *.zunit ]]; then + continue + fi + # Run it through the parser again + _zunit_parse_argument $file + done - # 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 ${name} test body" - return 126 + return fi - # Check if a time limit has been specified. We only do this if - # the ZSH version is at least 5.1.0, since older versions of ZSH - # are unable to handle asynchronous processes in the way we need - autoload is-at-least - if is-at-least 5.1.0 && [[ -n ${time_limit:#0} ]]; then - # Create another wrapper function around the test - __zunit_async_test_wrapper() { - local pid - # Get the current timestamp, and the time limit in ms, and use - # those to work out the kill time for the sub process - integer time_limit_ms=$(( time_limit * 1000 )) - integer time=$(( EPOCHREALTIME * 1000 )) - integer kill_time=$(( $time + $time_limit_ms )) - # 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 - echo "${name} test exceeded time limit. Terminated after ${time_limit} seconds" - 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)" + # 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${test_name+"@$test_name"}") + 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 - # Output the result to the user - state=$? - if [[ $state -eq 48 ]]; then - _zunit_skip $output - return - elif [[ $state -eq 78 ]]; then - _zunit_error $output - return - elif [[ -z $allow_risky && $state -eq 248 ]]; then - # If --verbose is specified, print test output to screen - [[ -n $verbose && -n $output ]] && echo $output - _zunit_warn 'No assertions were run, ${name} test considered risky' - return - elif [[ -n $allow_risky && $state -eq 248 ]] || [[ $state -eq 0 ]]; then - # If --verbose is specified, print test output to screen - [[ -n $verbose && -n $output ]] && echo $output - _zunit_success - return - else - _zunit_failure $output - return 1 + # 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 +} # ]]] +# FUNCTION: _zunit_run [[[ +# Run tests +function _zunit_run() { + local -a arguments testfiles + local fail_fast tap allow_risky verbose revolver + local output_text logfile_text output_html logfile_html + + # Load the datetime module, and record the start time + 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 \ + r=revolver -revolver=revolver \ + t=tap -tap=tap \ + -allow-risky=allow_risky \ + -output-html=output_html \ + -output-text=output_text \ + -time-limit:=time_limit \ + -verbose=verbose + + # TAP output is enabled + if [[ -n $tap ]] || [[ "$zunit_config_tap" = "true" ]]; then + # Set the $tap variable, so we can check it later + tap=1 + + # Print the TAP header + echo '--- TAP version 13' 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 - local -a bits; bits=("${(s/@/)1}") - local testfile="${bits[1]}" test_to_run="${bits[2]}" testdir="$(dirname "$testfile")" - local -a lines tests test_names - tests=() - test_names=() - - # Update status message - echo "--- 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 ))]}" - - # If a test name has been passed to the CLI, don't parse this test - # unless it matches the name passed - if [[ -n $test_to_run && $testname != $test_to_run ]]; then - testname='' - continue - fi - - # Store the test name and body in the arrays so we have somewhere to - # store the test body - 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 - # We've hit a closing brace as the only character on a line, - # therefore we are at the end of either a test or a setup or teardown - # function. We'll just clear all three here rather than work out which. - testname='' - parsing_setup='' - parsing_teardown='' - else - # A test name is set, so we are parsing a test. Add the - # current line to the function body. - if [[ -n $testname ]]; then - tests[${#test_names}]+="$line"$'\n' - continue - fi - - # Add the current line to the body of the setup function - if [[ -n $parsing_setup ]]; then - setup+="$line"$'\n' - continue - fi - - # Add the current line to the body of the teardown function - if [[ -n $parsing_teardown ]]; then - teardown+="$line"$'\n' - continue - fi + # TAP output is disabled + if [[ -z $tap ]]; then + # Print version information + echo + print -Pr "%F{green}==>%f Launching ZUnit $(_zunit_version)" + print -Pr "%F{blue}==>%f ZSH: $(zsh --version)" + echo fi - done - # A setup function has been defined - if [[ -n $setup ]]; then - # Print the body into a function declaration - setupfunc="function __zunit_test_setup() { - ${setup} - }" + # Text output has been requested + if [[ -n $output_text || -n $output_html ]]; then + # Make sure we have a config file, otherwise we can't determine + # which directory to write logs to + if [[ $missing_config -eq 1 ]]; then + echo $(color red '.zunit.yml could not be found. Run `zulu init`') + exit 1 + fi + + # If the output directory still isn't defined, it must not + # be defined in the config file + 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 + # Set the log filepath + logfile_text="$zunit_config_directories_output/output.txt" - # Quietly eval the body into a variable as a first test - output=$(eval "$(echo "$setupfunc")" 2>&1) + # Print the header to the logfile + echo 'TAP version 13' > $logfile_text + fi - # Check the status of the eval, and output any errors - if [[ $? -ne 0 ]]; then - _zunit_error "Failed to parse setup method" $output + if [[ -n $output_html ]]; then + # Set the log filepath + logfile_html="$zunit_config_directories_output/output.html" - return 126 + # Print the header to the logfile + _zunit_html_header > $logfile_html 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 + if [[ -n $zunit_config_directories_support ]]; then + # Check that the support directory exists + local support="$zunit_config_directories_support" + if [[ ! -d $support ]]; then + echo $(color red "Support directory at $support is missing") + exit 1 + fi + + # Look for a bootstrap script in the support directory, + # and run it if it is available + if [[ -f "$support/bootstrap" ]]; then + source "$support/bootstrap" + print -Pr "%F{blue}==>%f Sourced bootstrap script $support/bootstrap" + fi + fi + # Check if fail_fast is specified in the config or as an option + if [[ -z $fail_fast ]] && [[ "$zunit_config_fail_fast" = "true" ]]; then + fail_fast=1 + fi + # Check if allow_risky is specified in the config or as an option + if [[ -z $allow_risky ]] && [[ "$zunit_config_allow_risky" = "true" ]]; then + allow_risky=1 + fi + # Check if verbose is specified in the config or as an option + if [[ -z $verbose ]] && [[ "$zunit_config_verbose" = "true" ]]; then + verbose=1 + fi + # Check if verbose is specified in the config or as an option + if [[ -z $revolver ]] && [[ "$zunit_config_revolver" = "true" ]]; then + # Check for the 'revolver' dependency + $(type revolver >/dev/null 2>&1) + if [[ $? -ne 0 ]]; then + # 'revolver' could not be found, so print an error message + print -P "%F{red}[ERROR]%f: %F{white}Missing required dependency%f: %F{cyan}Revolver%f - %F{cyan}https://github.com/molovo/revolver%f" >&2 + exit 1 + else + revolver=1 + fi + fi - # 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" + # Check if time_limit is specified in the config or as an option + if [[ -n $time_limit ]]; then + shift time_limit + elif [[ -n $zunit_config_time_limit ]]; then + time_limit=$zunit_config_time_limit + fi - return 126 + arguments=("$@") + testfiles=() + + # Start the progress indicator + # If no arguments are passed, try to work out where the tests are + if [[ ${#arguments} -eq 0 ]]; then + # Check for a path defined in .zunit.yml + if [[ -n $zunit_config_directories_tests ]]; then + arguments=("$zunit_config_directories_tests") + # Fall back to the directory 'tests' by default + else + arguments=("tests") + fi 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 -i total + local -a errors failed passed skipped warnings + for testfile in ${(o)testfiles}; do + _zunit_run_testfile $testfile + done - # A teardown function has been defined - if [[ -n $teardown ]]; then - # Print the body into a function declaration - teardownfunc="function __zunit_test_teardown() { - ${teardown} - }" + end_time=$((EPOCHREALTIME*1000)) - # Quietly eval the body into a variable as a first test - output=$(eval "$(echo "$teardownfunc")" 2>&1) + # Print report footers + [[ -n $tap ]] && echo "1..$total" + [[ -n $output_text ]] && echo "1..$total" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_footer >> $logfile_html - # Check the status of the eval, and output any errors - if [[ $? -ne 0 ]]; then - _zunit_error "Failed to parse teardown method" $output + # Output results to screen and kill the progress indicator + _zunit_output_results - return 126 - fi + # If the total of ($passed + $skipped) is not equal to the + # total, then there must have been failures, errors or warnings, + # in which case this assertion will return the correct exit code + # for the test run as a whole + [[ $(( $#passed + $#skipped )) -eq $total ]] +} # ]]] +# FUNCTION: _zunit_run_testfile [[[ +# Run all tests within a file +function _zunit_run_testfile() { + local testbody testname pattern \ + setup teardown + local -a bits; bits=("${(s/@/)1}") + local testfile="${bits[1]}" test_to_run="${bits[2]}" testdir="$(dirname "$testfile")" + local -a lines tests test_names + tests=() + test_names=() + + # Update status message + print -Pr "%F{blue}==>%f Loading tests in %B${testfile}%b" + + # 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 ))]}" + + # If a test name has been passed to the CLI, don't parse this test + # unless it matches the name passed + if [[ -n $test_to_run && $testname != $test_to_run ]]; then + testname='' + continue + fi + + # Store the test name and body in the arrays so we have somewhere to + # store the test body + 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 + # We've hit a closing brace as the only character on a line, + # therefore we are at the end of either a test or a setup or teardown + # function. We'll just clear all three here rather than work out which. + testname='' + parsing_setup='' + parsing_teardown='' + else + # A test name is set, so we are parsing a test. Add the + # current line to the function body. + if [[ -n $testname ]]; then + tests[${#test_names}]+="$line"$'\n' + continue + fi + + # Add the current line to the body of the setup function + if [[ -n $parsing_setup ]]; then + setup+="$line"$'\n' + continue + fi + + # Add the current line to the body of the teardown function + if [[ -n $parsing_teardown ]]; then + teardown+="$line"$'\n' + continue + fi + fi + done - # 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 + # A setup function has been defined + if [[ -n $setup ]]; then + # Print the body into a function declaration + setupfunc="function __zunit_test_setup() { + ${setup} + }" - # 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" + # Quietly eval the body into a variable as a first test + output=$(eval "$(echo "$setupfunc")" 2>&1) - 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 - - # Remove the temporary functions - (( $+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 -a bits; bits=("${(s/@/)1}") - local argument="$bits[1]" test_name="$bits[2]" - - # 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 - # Skip further processing for files without a .zunit extension - if [[ -f $file && $file != *.zunit ]]; then - continue - fi - # Run it through the parser again - _zunit_parse_argument $file - done + # Check the status of the eval, and output any errors + if [[ $? -ne 0 ]]; then + _zunit_error "Failed to parse setup method" $output - return - fi + return 126 + fi - # If it is a valid file - if [[ -f $argument ]]; then - # Grab the first line of the file - line=$(cat $argument | head -n 1) + # 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 - # Check for the zunit shebang - if [[ $line =~ "#! ?/usr/bin/env zunit" ]]; then - # Add it to the array - testfiles[(( ${#testfiles} + 1 ))]=("$argument${test_name+"@$test_name"}") - return + # 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 - # 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 + # A teardown function has been defined + if [[ -n $teardown ]]; then + # Print the body into a function declaration + teardownfunc="function __zunit_test_teardown() { + ${teardown} + }" - # 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 -} + # Quietly eval the body into a variable as a first test + output=$(eval "$(echo "$teardownfunc")" 2>&1) -### -# Run tests -### -function _zunit_run() { - local -a arguments testfiles - local fail_fast tap allow_risky verbose revolver - local output_text logfile_text output_html logfile_html - - # Load the datetime module, and record the start time - 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 \ - r=revolver -revolver=revolver \ - t=tap -tap=tap \ - -allow-risky=allow_risky \ - -output-html=output_html \ - -output-text=output_text \ - -time-limit:=time_limit \ - -verbose=verbose - - # TAP output is enabled - if [[ -n $tap ]] || [[ "$zunit_config_tap" = "true" ]]; then - # Set the $tap variable, so we can check it later - tap=1 - - # Print the TAP header - echo '--- TAP version 13' - fi - - # TAP output is disabled - if [[ -z $tap ]]; then - # Print version information - echo $(color yellow 'Launching ZUnit') - echo "ZUnit: $(_zunit_version)" - echo "ZSH: $(zsh --version)" - echo - fi - - # Text output has been requested - if [[ -n $output_text || -n $output_html ]]; then - # Make sure we have a config file, otherwise we can't determine - # which directory to write logs to - if [[ $missing_config -eq 1 ]]; then - echo $(color red '.zunit.yml could not be found. Run `zulu init`') - exit 1 - fi + # Check the status of the eval, and output any errors + if [[ $? -ne 0 ]]; then + _zunit_error "Failed to parse teardown method" $output - # If the output directory still isn't defined, it must not - # be defined in the config file - 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 - # Set the log filepath - logfile_text="$zunit_config_directories_output/output.txt" - - # Print the header to the logfile - echo 'TAP version 13' > $logfile_text - fi - - if [[ -n $output_html ]]; then - # Set the log filepath - logfile_html="$zunit_config_directories_output/output.html" - - # Print the header to the logfile - _zunit_html_header > $logfile_html - fi - - if [[ -n $zunit_config_directories_support ]]; then - # Check that the support directory exists - local support="$zunit_config_directories_support" - if [[ ! -d $support ]]; then - echo $(color red "Support directory at $support is missing") - exit 1 - fi + return 126 + fi - # Look for a bootstrap script in the support directory, - # and run it if it is available - if [[ -f "$support/bootstrap" ]]; then - source "$support/bootstrap" - echo "$(color green '[SUCCESS]') Sourced bootstrap script $support/bootstrap" - fi - fi - # Check if fail_fast is specified in the config or as an option - if [[ -z $fail_fast ]] && [[ "$zunit_config_fail_fast" = "true" ]]; then - fail_fast=1 - fi - # Check if allow_risky is specified in the config or as an option - if [[ -z $allow_risky ]] && [[ "$zunit_config_allow_risky" = "true" ]]; then - allow_risky=1 - fi - # Check if verbose is specified in the config or as an option - if [[ -z $verbose ]] && [[ "$zunit_config_verbose" = "true" ]]; then - verbose=1 - fi - # Check if verbose is specified in the config or as an option - if [[ -z $revolver ]] && [[ "$zunit_config_revolver" = "true" ]]; then - # Check for the 'revolver' dependency - $(type revolver >/dev/null 2>&1) - if [[ $? -ne 0 ]]; then - # 'revolver' could not be found, so print an error message - print -P "%F{red}[ERROR]%f: %F{white}Missing required dependency%f: %F{cyan}Revolver%f - %F{cyan}https://github.com/molovo/revolver%f" >&2 - exit 1 - else - revolver=1 - fi - fi - - # Check if time_limit is specified in the config or as an option - if [[ -n $time_limit ]]; then - shift time_limit - elif [[ -n $zunit_config_time_limit ]]; then - time_limit=$zunit_config_time_limit - fi - - arguments=("$@") - testfiles=() - - # Start the progress indicator - # If no arguments are passed, try to work out where the tests are - if [[ ${#arguments} -eq 0 ]]; then - # Check for a path defined in .zunit.yml - if [[ -n $zunit_config_directories_tests ]]; then - arguments=("$zunit_config_directories_tests") - # Fall back to the directory 'tests' by default - else - arguments=("tests") + # 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 - 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 ${(o)testfiles}; do - _zunit_run_testfile $testfile - done - - end_time=$((EPOCHREALTIME*1000)) - - # Print report footers - [[ -n $tap ]] && echo "1..$total" - [[ -n $output_text ]] && echo "1..$total" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_footer >> $logfile_html - - # Output results to screen and kill the progress indicator - _zunit_output_results - - # If the total of ($passed + $skipped) is not equal to the - # total, then there must have been failures, errors or warnings, - # in which case this assertion will return the correct exit code - # for the test run as a whole - [[ $(( $passed + $skipped )) -eq $total ]] -} + + # 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 + + # Remove the temporary functions + (( $+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 +} # ]]] +# FUNCTION: _zunit_run_usage [[[ +# 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 " -f, --fail-fast Stop the test runner immediately after the first failure" + echo " -r --revolver Run tests with revolver spinner" + echo " -t, --tap Output results in a TAP compatible format" + echo " -v, --version Output version information and exit" + echo " --allow-risky Supress warnings generated for risky tests" + echo " --output-html Print results to a HTML page" + echo " --output-text Print results to a text log, in TAP compatible format" + echo " --time-limit Set a time limit of n seconds for each test" + echo " --verbose Prints full output from each test" +} # ]]] diff --git a/src/events.zsh b/src/events.zsh index 82eb0f1..b4c1124 100644 --- a/src/events.zsh +++ b/src/events.zsh @@ -2,135 +2,124 @@ # Functions for handling internal events # ########################################## -### -# Shutdown testing early. Called if --fail-fast is specified -# or if a fatal error occurred during testing -### -function _zunit_fail_shutdown() { - # Print a message to screen - echo $(color red bold 'Execution halted after failure') - - # Record the time at which testing ended - end_time=$((EPOCHREALTIME*1000)) - - # If we're not printing TAP output, then print the - # results table to screen - [[ -z $tap ]] && _zunit_output_results - - # If a HTML report has been requested, then print - # the end of the HTML report - if [[ -n $output_html ]]; then - name='Execution halted after failure' - _zunit_html_error >> $logfile_html - _zunit_html_footer >> $logfile_html - fi - - # Return a error exit code - exit 1 -} - -### -# Output a success message -### -function _zunit_success() { - # Write to reports - [[ -n $output_text ]] && _zunit_tap_success "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_success "$@" >> $logfile_html +# FUNCTION: _zunit_error [[[ +# Output a error message +function _zunit_error() { + local message="$1" output="${(@)@:2}" - passed=$(( passed + 1 )) + errors+=("${name}") - if [[ -n $tap ]]; then - _zunit_tap_success "$@" - return - fi + # Write to reports + [[ -n $output_text ]] && _zunit_tap_error "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_error "$@" >> $logfile_html - echo "[$(color green bold 'PASS')] $(color cyan \#${passed}) ${name}" -} + if [[ -n $tap ]]; then + _zunit_tap_error "$@" + else + echo "$(color red bold 'ERROR' ${name})" + echo " $(color red underline ${message})" + echo " $(color red ${output})" + fi -### + [[ -n $fail_fast ]] && _zunit_fail_shutdown +} # ]]] +# FUNCTION: _zunit_fail_shutdown [[[ +# Shutdown testing early. Called if --fail-fast is specified +# or if a fatal error occurred during testing +function _zunit_fail_shutdown() { + # Print a message to screen + echo $(color red bold 'Execution halted after failure') + + # Record the time at which testing ended + end_time=$((EPOCHREALTIME*1000)) + + # If we're not printing TAP output, then print the + # results table to screen + [[ -z $tap ]] && _zunit_output_results + + # If a HTML report has been requested, then print + # the end of the HTML report + if [[ -n $output_html ]]; then + name='Execution halted after failure' + _zunit_html_error >> $logfile_html + _zunit_html_footer >> $logfile_html + fi + + # Return a error exit code + exit 1 +} # ]]] +# FUNCTION: _zunit_failure [[[ # Output a failure message -### function _zunit_failure() { - local message="$1" output="${(@)@:2}" + local message="$1" output="${(@)@:2}" - failed=$(( failed + 1 )) + failed+=("${name}") - # Write to reports - [[ -n $output_text ]] && _zunit_tap_failure "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_failure "$@" >> $logfile_html + # Write to reports + [[ -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 bold 'FAIL')] ${name}" - echo " $(color red underline ${message})" - echo " $(color red ${output})" - fi + if [[ -n $tap ]]; then + _zunit_tap_failure "$@" + else + echo "[$(color red bold 'FAIL')] ${name}" + echo " $(color red underline ${message})" + echo " $(color red ${output})" + fi - [[ -n $fail_fast ]] && _zunit_fail_shutdown -} + [[ -n $fail_fast ]] && _zunit_fail_shutdown +} # ]]] +# FUNCTION: _zunit_skip [[[ +# Output a skipped test message +function _zunit_skip() { + local message="$@" -### -# Output a error message -### -function _zunit_error() { - local message="$1" output="${(@)@:2}" + skipped+=("${name}") + # Write to reports + [[ -n $output_text ]] && _zunit_tap_skip "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_skip "$@" >> $logfile_html - errors=$(( errors + 1 )) + if [[ -n $tap ]]; then + _zunit_tap_skip "$@" + return + fi - # Write to reports - [[ -n $output_text ]] && _zunit_tap_error "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_error "$@" >> $logfile_html + print -P "[$(color magenta bold 'SKIPPED')] ${name} (\e[38;5;238m${message}\e[0m)" +} # ]]] +# FUNCTION: _zunit_success [[[ +# Output a success message +function _zunit_success() { + # Write to reports + [[ -n $output_text ]] && _zunit_tap_success "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_success "$@" >> $logfile_html - if [[ -n $tap ]]; then - _zunit_tap_error "$@" - else - echo "$(color red bold 'ERROR' ${name})" - echo " $(color red underline ${message})" - echo " $(color red ${output})" - fi + passed+=("${name}") - [[ -n $fail_fast ]] && _zunit_fail_shutdown -} + if [[ -n $tap ]]; then + _zunit_tap_success "$@" + return + fi -### + print -Pr "[$(color green bold 'PASS')] $(color cyan \#${#passed}) %B${name}%b" +} # ]]] +# FUNCTION: _zunit_warn [[[ # Output a warning message -### function _zunit_warn() { - local message="$@" + local message="$@" - warnings=$(( warnings + 1 )) - - # Write to reports - [[ -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 bold 'WARN')] ${name}" - echo " $(color yellow underline ${message})" -} - -### -# Output a skipped test message -### -function _zunit_skip() { - local message="$@" + warnings=$(( warnings + 1 )) - skipped=$(( skipped + 1 )) + # Write to reports + [[ -n $output_text ]] && _zunit_tap_warn "$@" >> $logfile_text + [[ -n $output_html ]] && _zunit_html_warn "$@" >> $logfile_html - # Write to reports - [[ -n $output_text ]] && _zunit_tap_skip "$@" >> $logfile_text - [[ -n $output_html ]] && _zunit_html_skip "$@" >> $logfile_html + if [[ -n $tap ]]; then + _zunit_tap_warn "$@" + return + fi - if [[ -n $tap ]]; then - _zunit_tap_skip "$@" - return - fi + echo "[$(color yellow bold 'WARN')] ${name}" + echo " $(color yellow underline ${message})" +} # ]]] - echo "[$(color magenta bold 'SKIPPED')] ${name}" - echo " \033[0;38;5;242m# ${message}\033[0;m" -} +# vim: ft=zsh sw=4 ts=4 et foldmarker=[[[,]]] foldmethod=marker diff --git a/src/helpers.zsh b/src/helpers.zsh index 1900618..83bf03b 100644 --- a/src/helpers.zsh +++ b/src/helpers.zsh @@ -2,298 +2,281 @@ # Helpers for use within tests # ################################ -### +# FUNCTION: assert [[[ +# Redirect the assertion shorthand to the correct function +function assert() { + local value=$1 assertion=$2 + local -a comparisons + + # Preserve current $IFS + local oldIFS=$IFS + IFS=$'\n' + + # Store all comparison values in an array + comparisons=(${(@)@:3}) + + # If no assertion is passed, then use the first value, as it + # could be that the value is simply empty + if [[ -z $assertion ]]; then + assertion=$value + value="" + fi + + # Check that the requested assertion method exists + if (( ! $+functions[_zunit_assert_${assertion}] )); then + echo "$(color red "Assertion $assertion does not exist")" + exit 127 + fi + + # Increment the assertion count + _zunit_assertion_count=$(( _zunit_assertion_count + 1 )) + + # Run the assertion + "_zunit_assert_${assertion}" "$value" ${(@f)comparisons[@]} + + local state=$? + + # If the assertion failed, then return that exit code to the + # test, which will stop its execution and mark it as failed + if [[ $state -ne 0 ]]; then + exit $state + fi + + # Reset $IFS + IFS=$oldIFS +} # ]]] +# FUNCTION: color [[[ # Colorise and style a string -### function color() { - local color=$1 style=$2 b=0 - - shift - - case $style in - bold|b) b=1; shift ;; - italic|i) b=2; shift ;; - underline|u) b=4; shift ;; - inverse|in) b=7; shift ;; - strikethrough|s) b=9; shift ;; - esac - - case $color in - black|b) echo "\033[${b};30m${@}\033[0;m" ;; - red|r) echo "\033[${b};31m${@}\033[0;m" ;; - green|g) echo "\033[${b};32m${@}\033[0;m" ;; - yellow|y) echo "\033[${b};33m${@}\033[0;m" ;; - blue|bl) echo "\033[${b};34m${@}\033[0;m" ;; - magenta|m) echo "\033[${b};35m${@}\033[0;m" ;; - cyan|c) echo "\033[${b};36m${@}\033[0;m" ;; - white|w) echo "\033[${b};37m${@}\033[0;m" ;; - *) echo "\033[${b};38;5;$(( ${color} ))m${@}\033[0;m" ;; - esac -} - -### -# 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 - - # We couldn't find the file, so output an error message to the user - # and fail the test - echo "File $filename does not exist" >&2 - exit 1 -} - -### -# Run an external command and capture its output and exit status -### -function run() { - # Within tests, the shell is set to exit immediately when errors - # occur. Since we want to capture the exit code of the command - # we're running, we stop the shell from exiting on error temporarily - unsetopt ERR_EXIT - - # Preserve current $IFS - local oldIFS=$IFS name - local -a cmd - - # Store each word of the command in an array, and grab the first - # argument which is the command name - 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 - - local -a dont_quote - dont_quote=( - # Allow redirections and running multiple commands - "[[:digit:]]#(>|>>)(&|)[[:digit:]]#" - "[[:digit:]]#(<|<<)(&|)[[:digit:]]#" - "<<<" ";" "\\|" "\\|\\|" "&" "&&" - - # Skip quoting of var=... -like strings and also of a single - # `)' as it might follow var=( ... array assignment - "([0-9]#|[a-zA-Z_][a-zA-Z0-9_]#)=*" "\\)" - ) - - # The new line is important, it makes the error messages include the line - # number, i.e. e.g.: - # run:1: command not found: a-non-existent-command - # It would be skipped otherwise, i.e. "run: command ..." would be printed - IFS=$'\n' eval "output=\$( function run { - ${cmd[@]/(#m)*/${${${${${(M)MATCH:#(${(j:|:)~dont_quote})}:+$MATCH}}:-\"${MATCH//(#b)([\"\`\\])/\\${match[1]}}\"}}} 2>&1 }; run )"; - - # Get the process exit state - state="$?" - - # Store individual lines of output in an array - IFS=$'\n' - lines=("${(@f)output}") - - # Restore $IFS - IFS=$oldIFS - - # Print the command output if --verbose is specified - if [[ -n $verbose && -n $output ]]; then - echo $output - fi - - # Restore the exit on error state - setopt ERR_EXIT -} - -### + local color=$1 style=$2 b=0 + + shift + + case $style in + bold|b) b=1; shift ;; + italic|i) b=2; shift ;; + underline|u) b=4; shift ;; + inverse|in) b=7; shift ;; + strikethrough|s) b=9; shift ;; + esac + + case $color in + black|b) echo "\033[${b};30m${@}\033[0;m" ;; + red|r) echo "\033[${b};31m${@}\033[0;m" ;; + green|g) echo "\033[${b};32m${@}\033[0;m" ;; + yellow|y) echo "\033[${b};33m${@}\033[0;m" ;; + blue|bl) echo "\033[${b};34m${@}\033[0;m" ;; + magenta|m) echo "\033[${b};35m${@}\033[0;m" ;; + cyan|c) echo "\033[${b};36m${@}\033[0;m" ;; + white|w) echo "\033[${b};37m${@}\033[0;m" ;; + *) echo "\033[${b};38;5;$(( ${color} ))m${@}\033[0;m" ;; + esac +} # ]]] +# FUNCTION: error [[[ +# Mark the current test as skipped +function error() { + # Exit code 78 will end the test, and report an error. The error message + # is echoed to stdout first, so that it can be picked up by the error handler + echo "$@" + exit 78 +} # ]]] +# FUNCTION: evl [[[ # Eval given code and capture its output and exit status -### function evl() { - # Separation of the main zunit option scope - setopt localoptions - - # Within tests, the shell is set to exit immediately when errors - # occur. Since we want to capture the exit code of the command - # we're running, we stop the shell from exiting on error temporarily - unsetopt ERR_EXIT - - # Preserve current $IFS - local ___oldIFS=$IFS ___name - local -a ___cmd - - # Store each word of the command in an array, and grab the first - # argument which is the command ___name - ___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 - - local -a ___dont_quote - ___dont_quote=( - # Allow redirections and running multiple commands - "[[:digit:]]#(>|>>)(&|)[[:digit:]]#" - "[[:digit:]]#(<|<<)(&|)[[:digit:]]#" - "<<<" ";" "\\|" "\\|\\|" "&" "&&" - - # Skip quoting of var=... -like strings and also of a single - # `)' as it might follow var=( ... array assignment - "([0-9]#|[a-zA-Z_][a-zA-Z0-9_]#)=*" "\\)" - ) - - # Prepare the output file - local ___OUTFILE=$(mktemp) - - # The new line is important, it makes the error messages include the line - # number, i.e. e.g.: - # eval:1: command not found: a-non-existent-command - # It would be skipped otherwise, i.e. "eval: command ..." would be printed. - # This is to maintain consistency with the messages printed from run(). - IFS=$'\n' builtin eval "function __eval { + # Separation of the main zunit option scope + setopt localoptions + + # Within tests, the shell is set to exit immediately when errors + # occur. Since we want to capture the exit code of the command + # we're running, we stop the shell from exiting on error temporarily + unsetopt ERR_EXIT + + # Preserve current $IFS + local ___oldIFS=$IFS ___name + local -a ___cmd + + # Store each word of the command in an array, and grab the first + # argument which is the command ___name + ___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 + + local -a ___dont_quote + ___dont_quote=( + # Allow redirections and running multiple commands + "[[:digit:]]#(>|>>)(&|)[[:digit:]]#" + "[[:digit:]]#(<|<<)(&|)[[:digit:]]#" + "<<<" ";" "\\|" "\\|\\|" "&" "&&" + + # Skip quoting of var=... -like strings and also of a single + # `)' as it might follow var=( ... array assignment + "([0-9]#|[a-zA-Z_][a-zA-Z0-9_]#)=*" "\\)" + ) + + # Prepare the output file + local ___OUTFILE=$(mktemp) + + # The new line is important, it makes the error messages include the line + # number, i.e. e.g.: + # eval:1: command not found: a-non-existent-command + # It would be skipped otherwise, i.e. "eval: command ..." would be printed. + # This is to maintain consistency with the messages printed from run(). + IFS=$'\n' builtin eval "function __eval { ${___cmd[@]/(#m)*/${${${${${(M)MATCH:#(${(j:|:)~___dont_quote})}:+$MATCH}}:-\"${MATCH//(#b)([\"\`\\])/\\${match[1]}}\"}}} \ }; __eval >!$___OUTFILE 2>&1"; - # Get the process exit state - state="$?" - - # $(<...) trims all trailing \n-s, therefore cat is being used - output="$(cat "$___OUTFILE")" - - # Cleanup - unset -f __eval; - command rm -f "$___OUTFILE" - - # Store individual lines of output in an array - IFS=$'\n' - lines=("${(@f)output}") - - # Restore $IFS - IFS=$___oldIFS - - # Print the command output if --verbose is specified - if [[ -n $verbose && -n $output ]]; then - echo $output - fi -} - -### -# Redirect the assertion shorthand to the correct function -### -function assert() { - local value=$1 assertion=$2 - local -a comparisons - - # Preserve current $IFS - local oldIFS=$IFS - IFS=$'\n' + # Get the process exit state + state="$?" - # Store all comparison values in an array - comparisons=(${(@)@:3}) + # $(<...) trims all trailing \n-s, therefore cat is being used + output="$(cat "$___OUTFILE")" - # If no assertion is passed, then use the first value, as it - # could be that the value is simply empty - if [[ -z $assertion ]]; then - assertion=$value - value="" - fi + # Cleanup + unset -f __eval; + command rm -f "$___OUTFILE" - # Check that the requested assertion method exists - if (( ! $+functions[_zunit_assert_${assertion}] )); then - echo "$(color red "Assertion $assertion does not exist")" - exit 127 - fi + # Store individual lines of output in an array + IFS=$'\n' + lines=("${(@f)output}") - # Increment the assertion count - _zunit_assertion_count=$(( _zunit_assertion_count + 1 )) + # Restore $IFS + IFS=$___oldIFS - # Run the assertion - "_zunit_assert_${assertion}" "$value" ${(@f)comparisons[@]} - - local state=$? - - # If the assertion failed, then return that exit code to the - # test, which will stop its execution and mark it as failed - if [[ $state -ne 0 ]]; then - exit $state - fi - - # Reset $IFS - IFS=$oldIFS -} - -### -# Mark the current test as passed -### -function pass() { - # Exit code 0 will end the test, and mark is as passed. The reason for - # skipping is echoed to stdout first, so that it can be picked up by the - # error handler - exit 0 -} - -### + # Print the command output if --verbose is specified + if [[ -n $verbose && -n $output ]]; then + echo $output + fi +} # ]]] +# FUNCTION: fail [[[ # Mark the current test as failed -### function fail() { - # Any non-zero exit code without special meaning will mark the test as failed. - # The failure message is echoed to stdout first, so that it can be picked up - # by the error handler - echo "$@" - exit 1 -} - -### -# Mark the current test as skipped -### -function error() { - # Exit code 78 will end the test, and report an error. The error message - # is echoed to stdout first, so that it can be picked up by the error handler - echo "$@" - exit 78 -} - -### + # Any non-zero exit code without special meaning will mark the test as failed. + # The failure message is echoed to stdout first, so that it can be picked up + # by the error handler + echo "$@" + exit 1 +} # ]]] +# FUNCTION: load [[[ +# 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 + + # We couldn't find the file, so output an error message to the user + # and fail the test + echo "File $filename does not exist" >&2 + exit 1 +} # ]]] +# FUNCTION: pass [[[ +# Mark the current test as passed +function pass() { + # Exit code 0 will end the test, and mark is as passed. The reason for + # skipping is echoed to stdout first, so that it can be picked up by the + # error handler + exit 0 +} # ]]] +# FUNCTION: run [[[ +# Run an external command and capture its output and exit status +function run() { + # Within tests, the shell is set to exit immediately when errors + # occur. Since we want to capture the exit code of the command + # we're running, we stop the shell from exiting on error temporarily + unsetopt ERR_EXIT + + # Preserve current $IFS + local oldIFS=$IFS name + local -a cmd + + # Store each word of the command in an array, and grab the first + # argument which is the command name + 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 + + local -a dont_quote + dont_quote=( + # Allow redirections and running multiple commands + "[[:digit:]]#(>|>>)(&|)[[:digit:]]#" + "[[:digit:]]#(<|<<)(&|)[[:digit:]]#" + "<<<" ";" "\\|" "\\|\\|" "&" "&&" + + # Skip quoting of var=... -like strings and also of a single + # `)' as it might follow var=( ... array assignment + "([0-9]#|[a-zA-Z_][a-zA-Z0-9_]#)=*" "\\)" + ) + + # The new line is important, it makes the error messages include the line + # number, i.e. e.g.: + # run:1: command not found: a-non-existent-command + # It would be skipped otherwise, i.e. "run: command ..." would be printed + IFS=$'\n' eval "output=\$( function run { + ${cmd[@]/(#m)*/${${${${${(M)MATCH:#(${(j:|:)~dont_quote})}:+$MATCH}}:-\"${MATCH//(#b)([\"\`\\])/\\${match[1]}}\"}}} 2>&1 }; run )"; + + # Get the process exit state + state="$?" + + # Store individual lines of output in an array + IFS=$'\n' + lines=("${(@f)output}") + + # Restore $IFS + IFS=$oldIFS + + # Print the command output if --verbose is specified + if [[ -n $verbose && -n $output ]]; then + echo $output + fi + + # Restore the exit on error state + setopt ERR_EXIT +} # ]]] +# FUNCTION: skip [[[ # 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 -} - -# vim:ft=zsh:et:sts=2:sw=2 + # 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 +} # ]]] + +# vim: ft=zsh sw=4 ts=4 et foldmarker=[[[,]]] foldmethod=marker diff --git a/src/zunit.zsh b/src/zunit.zsh index f0d1093..2965e7b 100755 --- a/src/zunit.zsh +++ b/src/zunit.zsh @@ -6,98 +6,95 @@ setopt extendedglob typesetsilent # Main zunit process # ###################### -### +# FUNCTION: _zunit_usage [[[ # 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 " -f, --fail-fast Stop the test runner immediately after the first failure" - echo " -r, --revolver Run tests with revolver spinner" - echo " -t, --tap Output results in a TAP compatible format" - echo " -v, --version Output version information and exit" - echo " --allow-risky Supress warnings generated for risky tests" - echo " --output-html Print results to a HTML page" - echo " --output-text Print results to a text log, in TAP compatible format" - echo " --time-limit Set a time limit in seconds for each test" - echo " --verbose Prints full output from each test" -} - -### + 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 " -f, --fail-fast Stop the test runner immediately after the first failure" + echo " -r, --revolver Run tests with revolver spinner" + echo " -t, --tap Output results in a TAP compatible format" + echo " -v, --version Output version information and exit" + echo " --allow-risky Supress warnings generated for risky tests" + echo " --output-html Print results to a HTML page" + echo " --output-text Print results to a text log, in TAP compatible format" + echo " --time-limit Set a time limit in seconds for each test" + echo " --verbose Prints full output from each test" +} # ]]] +# FUNCTION: _zunit_version [[[ # Output the version number -### function _zunit_version() { - echo '0.8.2' -} - -### + echo '0.9.0' +} # ]]] +# FUNCTION: _zunit [[[ # 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 - # The config file was parsed successfully, so we Perform the parse - # again, but this time on the main thread 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 - # The config file failed to parse, so we report this to the user and exit - print -P "%F{red}[ERROR]%f: %F{white}Unable to parse configuration file%f" >&2 - exit 1 + 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 + # The config file was parsed successfully, so we Perform the parse + # again, but this time on the main thread 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 + # The config file failed to parse, so we report this to the user and exit + print -P "%F{red}[ERROR]%f: %F{white}Unable to parse configuration file%f" >&2 + exit 1 + fi fi - fi - zparseopts -D -E \ - h=help -help=help \ - v=version -version=version + 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 + # If the version option is passed, + # output version information and exit + if [[ -n $version ]]; then + _zunit_version && exit 0 + fi - # Check which command has been passed, and run it. If the command - # is not recognised, then we'll assume it's a test file and pass - # it to `zunit run`, since that will catch it if it's not a valid file - 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 -} + # Check which command has been passed, and run it. If the command + # is not recognised, then we'll assume it's a test file and pass + # it to `zunit run`, since that will catch it if it's not a valid file + 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 "$@" + +# vim: ft=zsh sw=4 ts=4 et foldmarker=[[[,]]] foldmethod=marker