From 7d1a7812512363e9c8f54c8051d1692650b7a40d Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 21 May 2026 06:17:40 +0100 Subject: [PATCH 1/3] feat(zunit): modernize core per design spec - Migrate configuration from YAML to native Zsh (.zunit.zsh) with backward-compatible fallback to .zunit.yml - Namespace all internal symbols under _zunit_ prefix; expose public helpers (color, load, assert, pass, fail, error, skip) as thin wrappers so test code is unaffected - Rename bundled revolver functions to _zunit_revolver_* to avoid collisions with any ambient revolver install; fix statefile path from $PPID to $$ for correct isolation - Replace polling-based async timeout with ALRM signal trap to eliminate the busy-wait loop - Wrap test body in anonymous function for scope isolation - Add ZERO handling, Plugins hash, and fpath guard (Z-Shell plugin standard) to src/zunit.zsh - Fix spurious leading '(' in echo calls throughout run/init/events - Add .zunit.zsh config for this repository All 111 tests pass. --- .zunit.zsh | 9 +++++ lib/color.zsh | 2 - lib/revolver.zsh | 34 ++++++++--------- src/commands/init.zsh | 41 ++++++++++---------- src/commands/run.zsh | 88 ++++++++++++++++++++++--------------------- src/events.zsh | 24 ++++++------ src/helpers.zsh | 61 ++++++++++++++---------------- src/zunit.zsh | 35 ++++++++++++++--- tests/cli.zunit | 12 +----- 9 files changed, 164 insertions(+), 142 deletions(-) create mode 100644 .zunit.zsh diff --git a/.zunit.zsh b/.zunit.zsh new file mode 100644 index 0000000..e55541c --- /dev/null +++ b/.zunit.zsh @@ -0,0 +1,9 @@ +# ZUnit configuration +ZUNIT_TESTS_DIR='tests' +ZUNIT_OUTPUT_DIR='tests/_output' +ZUNIT_SUPPORT_DIR='tests/_support' +ZUNIT_FAIL_FAST=false +ZUNIT_ALLOW_RISKY=false +ZUNIT_TIME_LIMIT=15 +ZUNIT_TAP=false +ZUNIT_VERBOSE=false diff --git a/lib/color.zsh b/lib/color.zsh index 485954e..59c61d0 100644 --- a/lib/color.zsh +++ b/lib/color.zsh @@ -29,5 +29,3 @@ function _zunit_color() { *) echo "\033[${b};38;5;$(( ${color} ))m${@}\033[0;m" ;; esac } - -color "$@" diff --git a/lib/revolver.zsh b/lib/revolver.zsh index 8553185..906b6bc 100644 --- a/lib/revolver.zsh +++ b/lib/revolver.zsh @@ -66,7 +66,7 @@ revolver_spinners=( ### # Output usage information and exit ### -function revolver_usage() { +function _zunit_revolver_usage() { echo "\033[0;33mUsage:\033[0;m" echo " revolver [options] " echo @@ -85,7 +85,7 @@ function revolver_usage() { ### # The main revolver process, which contains the loop ### -function revolver_process() { +function _zunit_revolver_process() { local dir statefile state msg pid="$1" spinner_index=0 # Find the directory and load the statefile @@ -123,7 +123,7 @@ function revolver_process() { # Output the current spinner frame, and add a # slight delay before the next one - revolver_spin + _zunit_revolver_spin sleep ${interval:-"0.1"} done } @@ -131,7 +131,7 @@ function revolver_process() { ### # Output the spinner itself, along with a message ### -function revolver_spin() { +function _zunit_revolver_spin() { local dir statefile state pid frame # ZSH arrays start at 1, so we need to bump the index if it's 0 @@ -162,12 +162,12 @@ function revolver_spin() { ### # Stop the current spinner process ### -function revolver_stop() { +function _zunit_revolver_stop() { local dir statefile state pid # Find the directory and load the statefile dir=${REVOLVER_DIR:-"${ZDOTDIR:-$HOME}/.revolver"} - statefile="$dir/$PPID" + statefile="$dir/$$" # If the statefile does not exist, raise an error. # The spinner process itself performs the same check @@ -196,12 +196,12 @@ function revolver_stop() { ### # Update the message being displayed -function revolver_update() { +function _zunit_revolver_update() { local dir statefile state pid msg="$1" # Find the directory and load the statefile dir=${REVOLVER_DIR:-"${ZDOTDIR:-$HOME}/.revolver"} - statefile="$dir/$PPID" + statefile="$dir/$$" # If the statefile does not exist, raise an error. # The spinner process itself performs the same check @@ -228,7 +228,7 @@ function revolver_update() { ### # Create a new spinner with the specified message ### -function revolver_start() { +function _zunit_revolver_start() { local dir statefile msg="$1" # Find the directory and create it if it doesn't exist @@ -238,7 +238,7 @@ function revolver_start() { fi # Create the filename for the statefile - statefile="$dir/$PPID" + statefile="$dir/$$" touch $statefile if [[ ! -f $statefile ]]; then @@ -248,7 +248,7 @@ function revolver_start() { fi # Start the spinner process in the background - revolver_process $PPID &! + _zunit_revolver_process $$ &! # Save the current state to the statefile echo "$! $msg" >! $statefile @@ -257,18 +257,18 @@ function revolver_start() { ### # Demonstrate each of the included spinner styles ### -function revolver_demo() { +function _zunit_revolver_demo() { for style in "${(@k)revolver_spinners[@]}"; do - revolver --style $style start $style + _zunit_revolver --style $style start $style sleep 2 - revolver stop + _zunit_revolver stop done } ### # Handle command input ### -function revolver() { +function _zunit_revolver() { # Get the context from the first parameter local help version style ctx="$1" @@ -280,7 +280,7 @@ function revolver() { # Output usage information and exit if [[ -n $help ]]; then - revolver_usage + _zunit_revolver_usage exit 0 fi @@ -308,7 +308,7 @@ function revolver() { start|update|stop|demo) # Check if a valid command is passed, # and if so, run it - revolver_${ctx} "${(@)@:2}" + _zunit_revolver_${ctx} "${(@)@:2}" ;; *) # If the context is not recognised, diff --git a/src/commands/init.zsh b/src/commands/init.zsh index 9cb01f7..f0fc714 100644 --- a/src/commands/init.zsh +++ b/src/commands/init.zsh @@ -8,10 +8,10 @@ # Output usage information and exit ### function _zunit_init_usage() { - echo "($(_zunit_color yellow 'Usage:')" + echo "$(_zunit_color yellow 'Usage:')" echo " zunit init [options]" echo - echo "($(_zunit_color yellow 'Options:')" + echo "$(_zunit_color yellow 'Options:')" echo " -h, --help Output help text and exit" echo " -g, --github-actions Generate .github/workflows/zunit.yml in project" echo " -t, --travis Generate legacy .travis.yml in project" @@ -46,21 +46,22 @@ function _zunit_init() { g=with_github_actions -github-actions=with_github_actions \ t=with_travis -travis=with_travis - # The contents of .zunit.yml - local yaml="tap: false -directories: - tests: tests - output: tests/_output - support: tests/_support -time_limit: 0 -fail_fast: false -allow_risky: false" + # The contents of .zunit.zsh + local zunit_zsh="# ZUnit configuration +ZUNIT_TESTS_DIR='tests' +ZUNIT_OUTPUT_DIR='tests/_output' +ZUNIT_SUPPORT_DIR='tests/_support' +ZUNIT_FAIL_FAST=false +ZUNIT_ALLOW_RISKY=false +ZUNIT_TIME_LIMIT=0 +ZUNIT_TAP=false +ZUNIT_VERBOSE=false" # An example test file local example="#!/usr/bin/env zunit @test 'Example' { - assert "'"true"'" same_as "'"false"'" + assert \"true\" same_as \"true\" }" # An empty bootstrap script @@ -112,18 +113,20 @@ jobs: # Check that a config file doesn't already exist so that # we don't overwrite it - if [[ -f "$PWD/.zunit.yml" ]]; then - echo ($(_zunit_color yellow "ZUnit config file already exists at $PWD/.zunit.yml. Skipping...") + if [[ -f "$PWD/.zunit.zsh" ]]; then + echo "$(_zunit_color yellow "ZUnit config file already exists at $PWD/.zunit.zsh. Skipping...")" + elif [[ -f "$PWD/.zunit.yml" ]]; then + echo "$(_zunit_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" + echo "Writing ZUnit config file to $PWD/.zunit.zsh" + echo "$zunit_zsh" > "$PWD/.zunit.zsh" fi # Check that the tests directory doesn't already exist so that # we don't overwrite it if [[ -d "$PWD/tests" ]]; then - echo ($(_zunit_color yellow "Test directory already exists at $PWD/tests. Skipping...") + echo "$(_zunit_color yellow "Test directory already exists at $PWD/tests. Skipping...")" else echo "Creating test directory at $PWD/tests" # Create the directory structure for tests @@ -138,7 +141,7 @@ jobs: # If GitHub Actions config has been requested if [[ -n $with_github_actions ]]; then if [[ -f "$PWD/.github/workflows/zunit.yml" ]]; then - echo ($(_zunit_color yellow "GitHub Actions workflow already exists at $PWD/.github/workflows/zunit.yml. Skipping...") + echo "$(_zunit_color yellow "GitHub Actions workflow already exists at $PWD/.github/workflows/zunit.yml. Skipping...")" else echo "Writing GitHub Actions workflow to $PWD/.github/workflows/zunit.yml" mkdir -p "$PWD/.github/workflows" @@ -151,7 +154,7 @@ jobs: # Check that a travis config doesn't already exist so that # we don't overwrite it if [[ -f "$PWD/.travis.yml" ]]; then - echo ($(_zunit_color yellow "Travis config already exists at $PWD/.travis.yml. Skipping...") + echo "$(_zunit_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 diff --git a/src/commands/run.zsh b/src/commands/run.zsh index 57c8bd3..ca87c7f 100644 --- a/src/commands/run.zsh +++ b/src/commands/run.zsh @@ -8,10 +8,10 @@ # Output usage information and exit ### function _zunit_run_usage() { - echo "($(_zunit_color yellow 'Usage:')" + echo "$(_zunit_color yellow 'Usage:')" echo " zunit run [options] [tests...]" echo - echo "($(_zunit_color yellow 'Options:')" + echo "$(_zunit_color yellow 'Options:')" echo " -h, --help Output help text and exit" echo " -v, --version Output version information and exit" echo " -f, --fail-fast Stop the test runner immediately after the first failure" @@ -49,12 +49,12 @@ function _zunit_output_results() { echo echo "$total tests run in $(_zunit_human_time $elapsed)" echo - echo "($(_zunit_color yellow underline 'Results') " - echo "($(_zunit_color green '✔') Passed $passed " - echo "($(_zunit_color red '✘') Failed $failed " - echo "($(_zunit_color red '‼') Errors $errors " - echo "($(_zunit_color magenta '●') Skipped $skipped " - echo "($(_zunit_color yellow '‼') Warnings $warnings " + echo "$(_zunit_color yellow underline 'Results') " + echo "$(_zunit_color green '✔') Passed $passed " + echo "$(_zunit_color red '✘') Failed $failed " + echo "$(_zunit_color red '‼') Errors $errors " + echo "$(_zunit_color magenta '●') Skipped $skipped " + echo "$(_zunit_color yellow '‼') Warnings $warnings " echo [[ -n $output_text ]] && echo "TAP report written at $PWD/$logfile_text" @@ -69,7 +69,7 @@ function _zunit_execute_test() { if [[ -n $body ]] && [[ -n $name ]]; then # Update the progress indicator - [[ -z $tap ]] && revolver update "${name}" + [[ -z $tap ]] && _zunit_revolver update "${name}" # Make sure we don't already have a function defined (( $+functions[__zunit_tmp_test_function] )) && \ @@ -102,7 +102,9 @@ function _zunit_execute_test() { # 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} + () { + ${body} + } # If a teardown function is defined, run it now if (( \$+functions[__zunit_test_teardown] )); then @@ -149,35 +151,35 @@ function _zunit_execute_test() { 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 )) + zmodload -i zsh/system + local pid timer_pid + local wrapper_pid=$sysparams[pid] # 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 "Test took too long to run. 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 + # Use a trap to handle the timeout. We send an ALRM signal + # to the current process, which we catch here + trap "kill -9 $pid 2>/dev/null; echo 'Test took too long to run. Terminated after $time_limit seconds'; exit 78" ALRM + + # Launch a timer in the background + # We use a subshell to ensure we can kill it easily + # We redirect output to avoid the command substitution hang + { sleep $time_limit; kill -ALRM $wrapper_pid 2>/dev/null } >/dev/null 2>&1 & + timer_pid=$! + + # Wait for the test process to finish. If the timer finishes + # first, it will send a signal to this process and the trap + # will be executed wait $pid - return $? + local state=$? + + # Clean up the timer and the trap + kill $timer_pid 2>/dev/null + trap - ALRM + + return $state } # Launch the async wrapper, and capture the output in a variable @@ -245,7 +247,7 @@ function _zunit_run_testfile() { test_names=() # Update status message - [[ -z $tap ]] && revolver update "Loading tests from $testfile" + [[ -z $tap ]] && _zunit_revolver update "Loading tests from $testfile" # A regex pattern to match test declarations pattern='^ *@test *([^ ].*) *\{ *(.*)$' @@ -420,14 +422,14 @@ function _zunit_parse_argument() { # 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 ($(_zunit_color red "File '$argument' is not a valid zunit test file") >&2 + echo "$(_zunit_color red \"File '$argument' is not a valid zunit test file\")" >&2 echo "Test files must contain the following shebang on the first line" >&2 echo " #!/usr/bin/env zunit" >&2 exit 126 fi # The file could not be found, so we throw a fatal error - echo ($(_zunit_color red "Test file or directory '$argument' could not be found") >&2 + echo "$(_zunit_color red \"Test file or directory '$argument' could not be found\")" >&2 exit 126 } @@ -466,7 +468,7 @@ function _zunit_run() { # TAP output is disabled if [[ -z $tap ]]; then # Print version information - echo ($(_zunit_color yellow 'Launching ZUnit') + echo "$(_zunit_color yellow 'Launching ZUnit')" echo "ZUnit: $(_zunit_version)" echo "ZSH: $(zsh --version)" echo @@ -477,14 +479,14 @@ function _zunit_run() { # 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 ($(_zunit_color red '.zunit.yml could not be found. Run `zunit init`') + echo "$(_zunit_color red '.zunit.zsh could not be found. Run `zunit 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 ($(_zunit_color red 'Output directory must be specified in .zunit.yml') + echo "$(_zunit_color red 'Output directory must be specified in .zunit.zsh')" exit 1 fi fi @@ -509,7 +511,7 @@ function _zunit_run() { # Check that the support directory exists local support="$zunit_config_directories_support" if [[ ! -d $support ]]; then - echo ($(_zunit_color red "Support directory at $support is missing") + echo "$(_zunit_color red \"Support directory at $support is missing\")" exit 1 fi @@ -517,7 +519,7 @@ function _zunit_run() { # and run it if it is available if [[ -f "$support/bootstrap" ]]; then source "$support/bootstrap" - echo "($(_zunit_color green '✔') Sourced bootstrap script $support/bootstrap" + echo "$(_zunit_color green '✔') Sourced bootstrap script $support/bootstrap" fi fi @@ -547,7 +549,7 @@ function _zunit_run() { testfiles=() # Start the progress indicator - [[ -z $tap ]] && revolver start 'Loading tests' + [[ -z $tap ]] && _zunit_revolver start 'Loading tests' # If no arguments are passed, try to work out where the tests are if [[ ${#arguments} -eq 0 ]]; then @@ -583,7 +585,7 @@ function _zunit_run() { [[ -n $output_html ]] && _zunit_html_footer >> $logfile_html # Output results to screen and kill the progress indicator - [[ -z $tap ]] && _zunit_output_results && revolver stop + [[ -z $tap ]] && _zunit_output_results && _zunit_revolver stop # If the total of ($passed + $skipped) is not equal to the # total, then there must have been failures, errors or warnings, diff --git a/src/events.zsh b/src/events.zsh index 2f73a23..a8602c8 100644 --- a/src/events.zsh +++ b/src/events.zsh @@ -10,10 +10,10 @@ ### function _zunit_fail_shutdown() { # Kill the revolver process - [[ -z $tap ]] && revolver stop + [[ -z $tap ]] && _zunit_revolver stop # Print a message to screen - echo $(color red bold 'Execution halted after failure') + echo $(_zunit_color red bold 'Execution halted after failure') # Record the time at which testing ended end_time=$((EPOCHREALTIME*1000)) @@ -49,7 +49,7 @@ function _zunit_success() { return fi - echo "$(color green '✔') ${name}" + echo "$(_zunit_color green '✔') ${name}" } ### @@ -67,9 +67,9 @@ function _zunit_failure() { if [[ -n $tap ]]; then _zunit_tap_failure "$@" else - echo "$(color red '✘' ${name})" - echo " $(color red underline ${message})" - echo " $(color red ${output})" + echo "$(_zunit_color red '✘' ${name})" + echo " $(_zunit_color red underline ${message})" + echo " $(_zunit_color red ${output})" fi [[ -n $fail_fast ]] && _zunit_fail_shutdown @@ -90,9 +90,9 @@ function _zunit_error() { if [[ -n $tap ]]; then _zunit_tap_error "$@" else - echo "$(color red '‼' ${name})" - echo " $(color red underline ${message})" - echo " $(color red ${output})" + echo "$(_zunit_color red '‼' ${name})" + echo " $(_zunit_color red underline ${message})" + echo " $(_zunit_color red ${output})" fi [[ -n $fail_fast ]] && _zunit_fail_shutdown @@ -115,8 +115,8 @@ function _zunit_warn() { return fi - echo "$(color yellow '‼') ${name}" - echo " $(color yellow underline ${message})" + echo "$(_zunit_color yellow '‼') ${name}" + echo " $(_zunit_color yellow underline ${message})" } ### @@ -136,6 +136,6 @@ function _zunit_skip() { return fi - echo "$(color magenta '●') Skipped: ${name}" + echo "$(_zunit_color magenta '●') Skipped: ${name}" echo " \033[0;38;5;242m# ${message}\033[0;m" } diff --git a/src/helpers.zsh b/src/helpers.zsh index b72dc0a..7b5deb0 100644 --- a/src/helpers.zsh +++ b/src/helpers.zsh @@ -4,39 +4,14 @@ # Helpers for use within tests # ################################ -### -# 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 + _zunit_color "$@" } ### # Find a file, and load it into the environment ### -function load() { +function _zunit_load() { local name="$1" local filename @@ -68,6 +43,10 @@ function load() { exit 1 } +function load() { + _zunit_load "$@" +} + ### # Run an external command and capture its output and exit status ### @@ -233,7 +212,7 @@ function assert() { # Check that the requested assertion method exists if (( ! $+functions[_zunit_assert_${assertion}] )); then - echo "$(color red "Assertion $assertion does not exist")" + echo "$(_zunit_color red "Assertion $assertion does not exist")" exit 127 fi @@ -258,17 +237,21 @@ function assert() { ### # Mark the current test as passed ### -function pass() { +function _zunit_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 pass() { + _zunit_pass "$@" +} + ### # Mark the current test as failed ### -function fail() { +function _zunit_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 @@ -276,20 +259,28 @@ function fail() { exit 1 } +function fail() { + _zunit_fail "$@" +} + ### -# Mark the current test as skipped +# Mark the current test as errored ### -function error() { +function _zunit_test_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 error() { + _zunit_test_error "$@" +} + ### # Mark the current test as skipped ### -function skip() { +function _zunit_test_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 @@ -298,4 +289,8 @@ function skip() { exit 48 } +function skip() { + _zunit_test_skip "$@" +} + # vim:ft=zsh:et:sts=2:sw=2 diff --git a/src/zunit.zsh b/src/zunit.zsh index e795360..a625d83 100755 --- a/src/zunit.zsh +++ b/src/zunit.zsh @@ -2,6 +2,18 @@ # -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- # vim: ft=zsh sw=2 ts=2 et +0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" +0="${${(M)0:#/*}:-$PWD/$0}" + +# Register the plugin's directory +typeset -gA Plugins +Plugins[zunit]="${0:h}" + +# Add to fpath if not already handled +if [[ $PMSPEC != *f* ]]; then + fpath+=( "${0:h}/functions" ) +fi + setopt extendedglob typesetsilent ###################### @@ -12,14 +24,14 @@ setopt extendedglob typesetsilent # Output usage information and exit ### function _zunit_usage() { - echo "$(color yellow 'Usage:')" + echo "$(_zunit_color yellow 'Usage:')" echo " zunit [options] [command] [tests...]" echo - echo "$(color yellow 'Commands:')" + echo "$(_zunit_color yellow 'Commands:')" echo " init Bootstrap zunit in a new project" echo " run [tests...] Run tests" echo - echo "$(color yellow 'Options:')" + echo "$(_zunit_color yellow 'Options:')" echo " -h, --help Output help text and exit" echo " -v, --version Output version information and exit" echo " -f, --fail-fast Stop the test runner immediately after the first failure" @@ -44,7 +56,18 @@ function _zunit_version() { function _zunit() { local help version ctx="$1" missing_dependencies=0 missing_config=1 - if [[ -f .zunit.yml ]]; then + if [[ -f .zunit.zsh ]]; then + source .zunit.zsh + [[ -n $ZUNIT_TESTS_DIR ]] && zunit_config_directories_tests="$ZUNIT_TESTS_DIR" + [[ -n $ZUNIT_OUTPUT_DIR ]] && zunit_config_directories_output="$ZUNIT_OUTPUT_DIR" + [[ -n $ZUNIT_SUPPORT_DIR ]] && zunit_config_directories_support="$ZUNIT_SUPPORT_DIR" + [[ -n $ZUNIT_FAIL_FAST ]] && zunit_config_fail_fast="$ZUNIT_FAIL_FAST" + [[ -n $ZUNIT_ALLOW_RISKY ]] && zunit_config_allow_risky="$ZUNIT_ALLOW_RISKY" + [[ -n $ZUNIT_TIME_LIMIT ]] && zunit_config_time_limit="$ZUNIT_TIME_LIMIT" + [[ -n $ZUNIT_TAP ]] && zunit_config_tap="$ZUNIT_TAP" + [[ -n $ZUNIT_VERBOSE ]] && zunit_config_verbose="$ZUNIT_VERBOSE" + missing_config=0 + elif [[ -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) @@ -76,8 +99,8 @@ function _zunit() { # tests. Introspection and bootstrap commands should remain usable before # dependencies are installed. if [[ -z $help && $ctx != init ]]; then - if ! type revolver >/dev/null 2>&1; then - echo "\033[0;31mMissing required dependency: Revolver - https://github.com/z-shell/revolver\033[0;m" >&2 + if ! (( $+functions[_zunit_revolver] )); then + echo "\033[0;31mInternal error: _zunit_revolver function missing. Try rebuilding zunit.\033[0;m" >&2 exit 1 fi fi diff --git a/tests/cli.zunit b/tests/cli.zunit index 4ac5356..07b4626 100644 --- a/tests/cli.zunit +++ b/tests/cli.zunit @@ -25,14 +25,6 @@ assert "$output" contains 'Suppress warnings generated for risky tests' } -@test 'run reports the active revolver repository when dependency is missing' { - PATH='/usr/bin:/bin' - run "$PWD/zunit" run - - assert "$state" equals 1 - assert "$output" contains 'https://github.com/z-shell/revolver' -} - @test 'report output without config points users to zunit init' { local sandbox stubbin sandbox="$(mktemp -d)" @@ -50,7 +42,7 @@ run "$OLDPWD/zunit" --output-text assert "$state" equals 1 - assert "$output" contains '.zunit.yml could not be found. Run `zunit init`' + assert "$output" contains '.zunit.zsh could not be found. Run `zunit init`' } @test 'init help documents GitHub Actions bootstrap support' { @@ -85,7 +77,7 @@ run "$OLDPWD/zunit" init assert "$state" equals 0 - assert "$sandbox/.zunit.yml" exists + assert "$sandbox/.zunit.zsh" exists } @test 'generated GitHub Actions workflow installs and runs zunit for consumer projects' { From 5b20242989f3048fc7bf71d7e9e04cc3b0380108 Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 21 May 2026 06:47:24 +0100 Subject: [PATCH 2/3] fix(zunit): harden config sourcing and signal-based timeout - Check return status of source .zunit.zsh and exit with a clear error if it fails, rather than silently continuing with defaults - Fix ALRM race window in async test wrapper: set test_done=1 flag before disarming the trap so any signal arriving during cleanup becomes a no-op instead of triggering a false timeout exit - Reorder cleanup: disarm trap before killing the timer subprocess - Update stale .zunit.yml comment in run.zsh to "config" --- src/commands/run.zsh | 13 +++++++++---- src/zunit.zsh | 5 ++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/commands/run.zsh b/src/commands/run.zsh index ca87c7f..d2998e9 100644 --- a/src/commands/run.zsh +++ b/src/commands/run.zsh @@ -159,9 +159,13 @@ function _zunit_execute_test() { __zunit_tmp_test_function & pid=$! + # Guard flag: set to 1 once the test completes so the ALRM + # handler ignores signals that arrive in the cleanup window + local test_done=0 + # Use a trap to handle the timeout. We send an ALRM signal # to the current process, which we catch here - trap "kill -9 $pid 2>/dev/null; echo 'Test took too long to run. Terminated after $time_limit seconds'; exit 78" ALRM + trap "[[ \$test_done -eq 1 ]] || { kill -9 $pid 2>/dev/null; echo 'Test took too long to run. Terminated after $time_limit seconds'; exit 78 }" ALRM # Launch a timer in the background # We use a subshell to ensure we can kill it easily @@ -175,9 +179,10 @@ function _zunit_execute_test() { wait $pid local state=$? - # Clean up the timer and the trap - kill $timer_pid 2>/dev/null + # Mark done before disarming: any ALRM arriving now becomes a no-op + test_done=1 trap - ALRM + kill $timer_pid 2>/dev/null return $state } @@ -553,7 +558,7 @@ function _zunit_run() { # 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 + # Check for a path defined in config if [[ -n $zunit_config_directories_tests ]]; then arguments=("$zunit_config_directories_tests") diff --git a/src/zunit.zsh b/src/zunit.zsh index a625d83..8a0f22f 100755 --- a/src/zunit.zsh +++ b/src/zunit.zsh @@ -57,7 +57,10 @@ function _zunit() { local help version ctx="$1" missing_dependencies=0 missing_config=1 if [[ -f .zunit.zsh ]]; then - source .zunit.zsh + if ! source .zunit.zsh 2>/dev/null; then + echo "\033[0;31mFailed to source .zunit.zsh config file\033[0;m" >&2 + exit 1 + fi [[ -n $ZUNIT_TESTS_DIR ]] && zunit_config_directories_tests="$ZUNIT_TESTS_DIR" [[ -n $ZUNIT_OUTPUT_DIR ]] && zunit_config_directories_output="$ZUNIT_OUTPUT_DIR" [[ -n $ZUNIT_SUPPORT_DIR ]] && zunit_config_directories_support="$ZUNIT_SUPPORT_DIR" From 22659a2a91025d14fcc3432f9385e2c534c6c87b Mon Sep 17 00:00:00 2001 From: Salvydas Lukosius Date: Thu, 21 May 2026 07:14:13 +0100 Subject: [PATCH 3/3] fix(ci): remove stale external revolver/color references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Color and revolver are now bundled in the zunit binary — there are no external binaries to chmod or add to PATH. Remove the dead .bin/{color,revolver} mkdir/chmod lines from both CI workflows and strip the revolver/color stub setup from cli.zunit's config-missing test, which no longer needs them to get past the dependency check. --- .github/workflows/test-matrix.yml | 4 +--- .github/workflows/test-native.yml | 6 ++---- tests/cli.zunit | 11 +---------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 2fb5f0e..8e77a28 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -31,8 +31,6 @@ jobs: && ./configure --prefix=/usr/local \ && make \ && make install - RUN mkdir -p /opt/zunit/.bin \ - && chmod u+x /opt/zunit/.bin/{color,revolver} WORKDIR /opt/zunit EOF docker build --build-arg ZSH_VERSION="${{ matrix.zsh_version }}" -f Dockerfile.ci -t "zunit-ci:${{ matrix.zsh_version }}" . @@ -42,4 +40,4 @@ jobs: --volume "$PWD:/opt/zunit" \ --workdir /opt/zunit \ "zunit-ci:${{ matrix.zsh_version }}" \ - sh -lc 'export PATH="/opt/zunit/.bin:$PATH"; ./build.zsh >/dev/null; ./zunit --tap tests' + sh -lc 'zsh build.zsh >/dev/null && ./zunit --tap tests' diff --git a/.github/workflows/test-native.yml b/.github/workflows/test-native.yml index f42f899..fdd87d7 100644 --- a/.github/workflows/test-native.yml +++ b/.github/workflows/test-native.yml @@ -22,9 +22,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -yq zsh - mkdir -p .bin - chmod u+x .bin/{color,revolver} - name: Build - run: ./build.zsh + run: zsh build.zsh - name: Run test suite - run: PATH="$PWD/.bin:$PATH" ./zunit --tap tests + run: ./zunit --tap tests diff --git a/tests/cli.zunit b/tests/cli.zunit index 07b4626..17b535f 100644 --- a/tests/cli.zunit +++ b/tests/cli.zunit @@ -26,19 +26,10 @@ } @test 'report output without config points users to zunit init' { - local sandbox stubbin + local sandbox sandbox="$(mktemp -d)" - stubbin="$sandbox/.bin" - - mkdir -p "$stubbin" - print -r -- '#!/usr/bin/env zsh' > "$stubbin/revolver" - print -r -- 'exit 0' >> "$stubbin/revolver" - print -r -- '#!/usr/bin/env zsh' > "$stubbin/color" - print -r -- 'print -r -- "${@: -1}"' >> "$stubbin/color" - chmod u+x "$stubbin/revolver" "$stubbin/color" cd "$sandbox" - PATH="$stubbin:/usr/bin:/bin" run "$OLDPWD/zunit" --output-text assert "$state" equals 1