Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Implement t_timeout #48

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions async.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,14 @@ _async_worker() {
while :; do
# Wait for jobs sent by async_job.
read -r -d $'\0' request || {
local ret=$?
# Unknown error occurred while reading from stdin, the zpty
# worker is likely in a broken state, so we shut down.
terminate_jobs

# Stdin is broken and in case this was an unintended
# crash, we try to report it as a last hurrah.
print -r -n $'\0'"'[async]'" $(( 127 + 3 )) "''" 0 "'$0:$LINENO: zpty fd died, exiting'"$'\0'
print -r -n $'\0'"'[async]'" $(( 127 + 3 )) "''" 0 "'$0:$LINENO: zpty fd died ($ret), exiting'"$'\0'

# We use `return` to abort here because using `exit` may
# result in an infinite loop that never exits and, as a
Expand Down Expand Up @@ -341,7 +342,7 @@ async_process_results() {

# Watch worker for output
_async_zle_watcher() {
setopt localoptions noshwordsplit
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
local worker=$ASYNC_PTYS[$1]
local callback=$ASYNC_CALLBACKS[$worker]
Expand All @@ -368,7 +369,7 @@ _async_zle_watcher() {
}

_async_send_job() {
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
setopt localoptions noshwordsplit

local caller=$1
local worker=$2
Expand Down Expand Up @@ -435,7 +436,7 @@ async_worker_eval() {

# This function traps notification signals and calls all registered callbacks
_async_notify_trap() {
setopt localoptions noshwordsplit
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings

local k
for k in ${(k)ASYNC_CALLBACKS}; do
Expand All @@ -451,7 +452,7 @@ _async_notify_trap() {
# async_register_callback <worker_name> <callback_function>
#
async_register_callback() {
setopt localoptions noshwordsplit nolocaltraps
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings nolocaltraps

typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
local worker=$1; shift
Expand Down Expand Up @@ -493,7 +494,7 @@ async_unregister_callback() {
# async_flush_jobs <worker_name>
#
async_flush_jobs() {
setopt localoptions noshwordsplit
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings

local worker=$1; shift

Expand Down Expand Up @@ -531,7 +532,7 @@ async_flush_jobs() {
# -p pid to notify (defaults to current pid)
#
async_start_worker() {
setopt localoptions noshwordsplit noclobber
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings noclobber

local worker=$1; shift
local -a args
Expand Down Expand Up @@ -613,7 +614,7 @@ async_start_worker() {
# async_stop_worker <worker_name_1> [<worker_name_2>]
#
async_stop_worker() {
setopt localoptions noshwordsplit
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings

local ret=0 worker k v
for worker in $@; do
Expand Down
64 changes: 38 additions & 26 deletions async_test.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,18 @@ test_async_job_unique_worker() {
helper() {
sleep 0.1; print $1
}
t_timeout 2

# Start a unique (job) worker.
async_start_worker test -u
t_defer async_stop_worker test

# Launch two jobs with the same name, the first one should be
# allowed to complete whereas the second one is never run.
async_job test helper one
async_job test helper two

while ! async_process_results test cb; do :; done
while ! async_process_results test cb; do sleep 0.05; done

# If both jobs were running but only one was complete,
# async_process_results() could've returned true for
Expand All @@ -275,8 +277,6 @@ test_async_job_unique_worker() {
sleep 0.1
async_process_results test cb

async_stop_worker test

# Ensure that cb was only called once with correc output.
[[ ${#result} = 6 ]] || t_error "result: want 6 elements, got" ${#result}
[[ $result[3] = one ]] || t_error "output: want 'one', got" ${(Vq-)result[3]}
Expand All @@ -298,6 +298,8 @@ test_async_job_error_and_nonzero_exit() {

while ! async_process_results test cb; do :; done

async_stop_worker test

[[ $r[1] = error ]] || t_error "want 'error', got ${(Vq-)r[1]}"
[[ $r[2] = 99 ]] || t_error "want exit code 99, got $r[2]"

Expand Down Expand Up @@ -365,6 +367,7 @@ test_async_flush_jobs() {
}

async_start_worker test
t_defer async_stop_worker test

# Start a job that prints 1 and starts two disowned child processes that
# print 2 and 3, respectively, after a timeout. The job will not exit
Expand All @@ -384,8 +387,10 @@ test_async_flush_jobs() {

# Flush jobs, this kills running jobs and discards unprocessed results.
# TODO: Confirm that they no longer exist in the process tree.
local output
output="${(Q)$(ASYNC_DEBUG=1 async_flush_jobs test)}"
local output line
ASYNC_DEBUG=1 async_flush_jobs test | while read -r line; do output+="$line"; done
output="${(Q)output}"

# NOTE(mafredri): First 'p' in print_four is lost when null-prefixing
# _async_job output.
[[ $output = *'rint_four 0 4'* ]] || {
Expand All @@ -396,8 +401,6 @@ test_async_flush_jobs() {
sleep 0.1
async_process_results test cb
(( $#r == 0 )) || t_error "want no output, got ${(Vq-)r}"

async_stop_worker test
}

test_async_worker_survives_termination_of_other_worker() {
Expand Down Expand Up @@ -488,15 +491,17 @@ setopt_helper() {
local -a result
cb() { result=("$@") }

async_start_worker test
async_job test print "hello world"
while ! async_process_results test cb; do :; done
async_stop_worker test
async_start_worker ${1}_worker
#sleep 0.001 # Fails sporadically on GitHub Actions without a sleep here.
async_job ${1}_worker print "hello world"
while ! async_process_results ${1}_worker cb; do sleep 0.001; done
#sleep 0.001 # Fails sporadically on GitHub Actions without a sleep here.
async_stop_worker ${1}_worker

# At this point, ksh arrays will only mess with the test.
setopt noksharrays

[[ $result[1] = print ]] || t_fatal "$1 want command name: print, got" $result[1]
[[ $result[1] = print ]] || t_fatal "$1 want command name: print, got" $result[1] "(${(Vq-)result})"
[[ $result[2] = 0 ]] || t_fatal "$1 want exit code: 0, got" $result[2]

[[ $result[3] = "hello world" ]] || {
Expand All @@ -511,29 +516,36 @@ test_all_options() {
t_skip "Test is not reliable on zsh 5.0.X"
fi

# Make sure worker is stopped, even if tests fail.
t_defer async_stop_worker test
t_timeout 30

{ sleep 15 && t_fatal "timed out" } &
local tpid=$!
# Make sure worker is stopped, even if tests fail.
#t_defer async_stop_worker test

local -a opts exclude
opts=(${(k)options})

# These options can't be tested.
exclude=(
zle interactive restricted shinstdin stdin onecmd singlecommand
warnnestedvar errreturn
)

for opt in ${opts:|exclude}; do
if [[ $options[$opt] = on ]]; then
setopt_helper no$opt
else
setopt_helper $opt
fi
done 2>/dev/null # Remove redirect to see output.

kill $tpid # Stop timeout.
#setopt nopromptsubst

local -a testopts=(${opts:|exclude})
for ((i = 1; i <= $#testopts; i++)); do
t_log "Testing with ${testopts[i]} included..."
for opt in $testopts[1,$i]; do
if [[ $options[$opt] = on ]]; then
setopt_helper no$opt
else
if [[ $opt = xtrace ]] || [[ $opt = printexitvalue ]]; then
setopt_helper $opt 2>/dev/null
else
setopt_helper $opt
fi
fi
done
done
}

test_async_job_with_rc_expand_param() {
Expand Down
51 changes: 38 additions & 13 deletions test.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ zmodload zsh/zutil
zmodload zsh/system
zmodload zsh/zselect

TEST_GLOB=.
TEST_RUN=
TEST_VERBOSE=0
TEST_TRACE=1
TEST_CODE_SKIP=100
TEST_CODE_ERROR=101
TEST_CODE_TIMEOUT=102
export ZTEST_DEBUG=0
export TEST_GLOB=.
export TEST_RUN=
export TEST_VERBOSE=0
export TEST_TRACE=0
export TEST_CODE_SKIP=100
export TEST_CODE_ERROR=101
export TEST_CODE_TIMEOUT=102

show_help() {
print "usage: ./test.zsh [-v] [-x] [-run pattern] [search pattern]"
Expand Down Expand Up @@ -60,7 +61,18 @@ t_runner_init() {
# used to abort test execution by exec.
_t_runner() {
local -a _test_defer_funcs
integer _test_errors=0
local _test_timeout_trace _test_timeout=0
integer _test_errors=0 _test_timeout_pid=0

TRAPALRM() {
if ((_test_timeout)); then
_test_timeout_pid=0
_t_log $_test_timeout_trace "timed out after ${_test_timeout}s"
() { return $TEST_CODE_TIMEOUT }
t_done
fi
return 0
}
while read -r; do
eval "$REPLY"
done
Expand All @@ -76,31 +88,43 @@ t_runner_init() {

# t_log is for printing log output, visible in verbose (-v) mode.
t_log() {
setopt localoptions noposixidentifiers
local line=$funcfiletrace[1]
[[ ${line%:[0-9]*} = "" ]] && line=ztest:$functrace[1] # Not from a file.
_t_log $line "$*"
}

# t_skip is for skipping a test.
t_skip() {
setopt localoptions noposixidentifiers
_t_log $funcfiletrace[1] "$*"
() { return 100 }
() { return $TEST_CODE_SKIP }
t_done
}

# t_error logs the error and fails the test without aborting.
t_error() {
setopt localoptions noposixidentifiers
(( _test_errors++ ))
_t_log $funcfiletrace[1] "$*"
}

# t_fatal fails the test and halts execution immediately.
t_fatal() {
setopt localoptions noposixidentifiers
_t_log $funcfiletrace[1] "$*"
() { return 101 }
() { return $TEST_CODE_ERROR }
t_done
}

t_timeout() {
setopt localoptions noposixidentifiers
_test_timeout_trace=$funcfiletrace[1]
_test_timeout=$1
{ sleep $_test_timeout && kill -ALRM $$ } &
_test_timeout_pid=$!
}

# t_defer takes a function (and optionally, arguments)
# to be executed after the test has completed.
t_defer() {
Expand All @@ -111,7 +135,8 @@ t_runner_init() {
# Can also be called manually when the test is done.
t_done() {
local ret=$? w=${1:-1}
(( _test_errors )) && ret=101
(( ret < 100 && _test_errors )) && ret=101
(( _test_timeout_pid )) && kill $_test_timeout_pid

(( w )) && wait # Wait for test children to exit.
for d in $_test_defer_funcs; do
Expand Down Expand Up @@ -204,8 +229,8 @@ run_test_module() {

case $test_exit in
(0|1) state=PASS;;
(100) state=SKIP;;
(101|102) state=FAIL; mod_exit=1;;
($TEST_CODE_SKIP) state=SKIP;;
($TEST_CODE_ERROR|$TEST_CODE_TIMEOUT) state=FAIL; mod_exit=1;;
*) state="????";;
esac

Expand Down