From f67eeadbbe114782ffebf9d49c77e60178b6d17d Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 19:30:22 +0000 Subject: [PATCH 1/9] Fix file-based assertion methods so they work with absolute paths --- zunit | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/zunit b/zunit index 2a21f18..880aab1 100755 --- a/zunit +++ b/zunit @@ -155,9 +155,11 @@ function _zunit_assert_exists() { # If filepath is relative, prepend the test directory if [[ "${pathname:0:1}" != "/" ]]; then filepath="$testdir/${pathname}" + else + filepath="$pathname" fi - [[ -e $filepath ]] && return 0 + [[ -e "$filepath" ]] && return 0 echo "'$pathname' does not exist" exit 1 @@ -172,9 +174,11 @@ function _zunit_assert_is_file() { # If filepath is relative, prepend the test directory if [[ "${pathname:0:1}" != "/" ]]; then filepath="$testdir/${pathname}" + else + filepath="$pathname" fi - [[ -f $filepath ]] && return 0 + [[ -f "$filepath" ]] && return 0 echo "'$pathname' does not exist or is not a file" exit 1 @@ -188,10 +192,12 @@ function _zunit_assert_is_dir() { # If filepath is relative, prepend the test directory if [[ "${pathname:0:1}" != "/" ]]; then - filepath="$testdir/${pathname}" + filepath="$testdir/$pathname" + else + filepath="$pathname" fi - [[ -d $filepath ]] && return 0 + [[ -d "$filepath" ]] && return 0 echo "'$pathname' does not exist or is not a directory" exit 1 @@ -206,9 +212,11 @@ function _zunit_assert_is_link() { # If filepath is relative, prepend the test directory if [[ "${pathname:0:1}" != "/" ]]; then filepath="$testdir/${pathname}" + else + filepath="$pathname" fi - [[ -h $filepath ]] && return 0 + [[ -h "$filepath" ]] && return 0 echo "'$pathname' does not exist or is not a symbolic link" exit 1 @@ -223,9 +231,11 @@ function _zunit_assert_is_readable() { # If filepath is relative, prepend the test directory if [[ "${pathname:0:1}" != "/" ]]; then filepath="$testdir/${pathname}" + else + filepath="$pathname" fi - [[ -r $filepath ]] && return 0 + [[ -r "$filepath" ]] && return 0 echo "'$pathname' does not exist or is not readable" exit 1 From c678b61d380edfe67a3fcc2d31c0cb6097715e06 Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 19:31:04 +0000 Subject: [PATCH 2/9] Stop run helper from passing modified $IFS to sub process --- zunit | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/zunit b/zunit index 880aab1..680ceff 100755 --- a/zunit +++ b/zunit @@ -301,14 +301,15 @@ function run() { cmd[1]="$testdir/${name}" fi - # Store lines of output in an array - IFS=$'\n' lines=($("${cmd[@]}" 2>&1)) + # Store full output in a variable + output=$("${cmd[@]}" 2>&1) # Get the process exit state state="$?" - # Store the full output in a variable - output=${lines[@]} + # Store individual lines of output in an array + IFS=$'\n' + lines=($output) # Restore $IFS IFS=$oldIFS From 9357b32211b95c76891a0f62b5691fcb99d5634e Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 19:36:43 +0000 Subject: [PATCH 3/9] Bump version --- zunit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zunit b/zunit index 680ceff..aa2eea6 100755 --- a/zunit +++ b/zunit @@ -1196,7 +1196,7 @@ function _zunit() { # If the version option is passed, # output version information and exit if [[ -n $version ]]; then - echo '0.4.3' + echo '0.5.0' exit 0 fi From a0963ecbac3931873eae3c862325b23d33871b7e Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 21:10:13 +0000 Subject: [PATCH 4/9] Re-enable trusty builds for travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index bbe59ac..e7e2b8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ addons: apt: packages: zsh +dist: trusty before_script: - mkdir .bin - curl -L https://raw.githubusercontent.com/molovo/revolver/master/revolver > .bin/revolver From a47b4b081d12199abb848466c52b46dacd661207 Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 21:26:05 +0000 Subject: [PATCH 5/9] Fix `in` and `not_in` assertion methods in ZSH<5.3 --- zunit | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/zunit b/zunit index aa2eea6..e0caf9a 100755 --- a/zunit +++ b/zunit @@ -122,6 +122,7 @@ function _zunit_assert_in() { [[ $i = $value ]] && found=1 done + [[ $found -eq 1 ]] && return 0 echo "'$value' is not in (${(@f)array})" @@ -324,6 +325,9 @@ function run() { function assert() { local value=$1 assertion=$2 local -a comparisons + + IFS=$'\n' + comparisons=(${(@)@:3}) if [[ -z $assertion ]]; then @@ -345,6 +349,8 @@ function assert() { if [[ $state -ne 0 ]]; then exit $state fi + + IFS=$oldIFS } ### From 7ded6a27f0fd37b592a822ec18cfb56b8fc9ce55 Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 20:41:31 +0000 Subject: [PATCH 6/9] Allow a per-process time limit to be specified If the key `time_limit` is specified in `.zunit.yml`, then the test is executed in a child process. The child process is killed if it is still running after `$time_limit` seconds have passed, and the test is marked as an error in the results. Fix #39 --- zunit | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/zunit b/zunit index e0caf9a..72ab16b 100755 --- a/zunit +++ b/zunit @@ -762,14 +762,56 @@ function _zunit_execute_test() { return 126 fi - # Execute the test body, and capture its output - output="$(__zunit_tmp_test_function 2>&1)" + # Check if a time limit has been specified + if [[ -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' From 2a35a70c2452a8c75e0ac14a0aea7e5f8d26f6a5 Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 21:27:09 +0000 Subject: [PATCH 7/9] Enable time limit in .zunit.yml --- .zunit.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.zunit.yml b/.zunit.yml index 8c58cda..e761e4f 100644 --- a/.zunit.yml +++ b/.zunit.yml @@ -3,3 +3,4 @@ directories: tests: tests output: tests/_output support: tests/_support +time_limit: 15 From 83c508250c8c9bb1b8e42591e36aa612f759c413 Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 23:08:46 +0000 Subject: [PATCH 8/9] Prevent time limits from being enforced in ZSH version less than 5.1.0 --- zunit | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zunit b/zunit index 72ab16b..d5fbabf 100755 --- a/zunit +++ b/zunit @@ -762,8 +762,10 @@ function _zunit_execute_test() { return 126 fi + autoload is-at-least + # Check if a time limit has been specified - if [[ -n $zunit_config_time_limit ]]; then + 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 From cdb9ee114c82b1d9ed7ce792c373eae2cbd79926 Mon Sep 17 00:00:00 2001 From: James Dinsdale Date: Wed, 15 Feb 2017 23:30:52 +0000 Subject: [PATCH 9/9] Documentation updates for 0.5.0 --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c1a19f..aeb7376 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,23 @@ directories: support: tests/_support ``` -To set up ZUnit for a new project, just run `zunit init` in the project's root directory. This will create the `.zunit.yml` config file and relevant directories, including an example test. +### Bootstrap script + +ZUnit will look in the support directory (`tests/_support` by default) for a file named `bootstrap`. If found, this is sourced prior to any tests being run. This bootstrap script can be used to install software, set environment variables and source programs required for your tests to run. + +### Test time limits + +ZUnit can enforce a time limit for tests, and will terminate them with an error if they run for longer than this. Just add the `time_limit` key to your `.zunit.yml`. + +```yaml +time_limit: 5 # Will terminate tests after they have run for 5 seconds +``` + +> **NOTE:** Due to the way child processes are handled in earlier versions of ZSH, the `time_limit` setting is **ignored** for ZSH versions below **5.1.0**. This is necessary because in versions below 5.1.0, the exit state is never returned from the asynchronous process, which would cause tests to hang indefinitely. + +### Setting up a new project + +To set up ZUnit for a new project, just run `zunit init` in the project's root directory. This will create the `.zunit.yml` config file and relevant directories, including a bootstrap script and example test. ### [Travis CI](https://travis-ci.org) config