Files
dotfiles/.zprezto/modules/prompt/external/async/async_test.zsh
2018-11-18 23:37:19 +04:00

606 lines
15 KiB
Bash

#!/usr/bin/env zsh
test__async_job_print_hi() {
coproc cat
print -n -p t # Insert token into coproc.
local line
local -a out
line=$(_async_job print hi)
# Remove leading/trailing null, parse, unquote and interpret as array.
line=$line[2,$#line-1]
out=("${(@Q)${(z)line}}")
coproc exit
[[ $out[1] = print ]] || t_error "command name should be print, got" $out[1]
[[ $out[2] = 0 ]] || t_error "want exit code 0, got" $out[2]
[[ $out[3] = hi ]] || t_error "want output: hi, got" $out[3]
}
test__async_job_stderr() {
coproc cat
print -n -p t # Insert token into coproc.
local line
local -a out
line=$(_async_job print 'hi 1>&2')
# Remove trailing null, parse, unquote and interpret as array.
line=$line[1,$#line-1]
out=("${(@Q)${(z)line}}")
coproc exit
[[ $out[2] = 0 ]] || t_error "want status 0, got" $out[2]
[[ -z $out[3] ]] || t_error "want empty output, got" $out[3]
[[ $out[5] = hi ]] || t_error "want stderr: hi, got" $out[5]
}
test__async_job_wait_for_token() {
float start duration
coproc cat
_async_job print hi >/dev/null &
job=$!
start=$EPOCHREALTIME
{
sleep 0.1
print -n -p t
} &
wait $job
coproc exit
duration=$(( EPOCHREALTIME - start ))
# Fail if the execution time was faster than 0.1 seconds.
(( duration >= 0.1 )) || t_error "execution was too fast, want >= 0.1, got" $duration
}
test__async_job_multiple_commands() {
coproc cat
print -n -p t
local line
local -a out
line="$(_async_job print '-n hi; for i in "1 2" 3 4; do print -n $i; done')"
# Remove trailing null, parse, unquote and interpret as array.
line=$line[1,$#line-1]
out=("${(@Q)${(z)line}}")
coproc exit
# $out[1] here will be the entire string passed to _async_job()
# ('print -n hi...') since proper command parsing is done by
# the async worker.
[[ $out[3] = "hi1 234" ]] || t_error "want output hi1 234, got " $out[3]
}
test_async_start_stop_worker() {
local out
async_start_worker test
out=$(zpty -L)
[[ $out =~ "test _async_worker" ]] || t_error "want zpty worker running, got ${(Vq-)out}"
async_stop_worker test || t_error "stop worker: want exit code 0, got $?"
out=$(zpty -L)
[[ -z $out ]] || t_error "want no zpty worker running, got ${(Vq-)out}"
async_stop_worker nonexistent && t_error "stop non-existent worker: want exit code 1, got $?"
}
test_async_job_print_matches_input_exactly() {
local -a result
cb() { result=("$@") }
async_start_worker test
t_defer async_stop_worker test
want='
Hello world!
Much *formatting*,
many space\t...\n\n
Such "quote", v '$'\'quote\'''
'
async_job test print -r - "$want"
while ! async_process_results test cb; do :; done
[[ $result[3] = $want ]] || t_error "output, want ${(Vqqqq)want}, got ${(Vqqqq)result[3]}"
}
test_async_process_results() {
local -a r
cb() { r+=("$@") }
async_start_worker test
t_defer async_stop_worker test
async_process_results test cb # No results.
ret=$?
(( ret == 1 )) || t_error "want exit code 1, got $ret"
async_job test print -n hi
while ! async_process_results test cb; do :; done
(( $#r == 6 )) || t_error "want one result, got $(( $#r % 6 ))"
}
test_async_process_results_stress() {
# NOTE: This stress test does not always pass properly on older versions of
# zsh, sometimes writing to zpty can hang and other times reading can hang,
# etc.
local -a r
cb() { r+=("$@") }
async_start_worker test
t_defer async_stop_worker test
integer iter=40 timeout=5
for i in {1..$iter}; do
async_job test "print -n $i"
# TODO: Figure out how we can remove sleep & process here.
# If we do not sleep here, we end up losing some of the commands sent to
# async_job (~90 get sent). This could possibly be due to the zpty
# buffer being full (see below).
sleep 0.00001
# Without processing resuls we occasionally run into 'print -n 39'
# failing due to the command name and exit status missing. Sample output
# from processing for 39 (stdout, time, stderr):
# $'39 0.0056798458 '
# This is again, probably due to the zpty buffer being full, we only
# need to ensure that not too many commands are run before we process.
(( iter % 6 == 0 )) && async_process_results test cb
done
float start=$EPOCHSECONDS
while (( $#r / 6 < iter )); do
async_process_results test cb
(( EPOCHSECONDS - start > timeout )) && {
t_log "timed out after ${timeout}s"
t_fatal "wanted $iter results, got $(( $#r / 6 ))"
}
done
local -a stdouts
while (( $#r > 0 )); do
[[ $r[1] = print ]] || t_error "want 'print', got ${(Vq-)r[1]}"
[[ $r[2] = 0 ]] || t_error "want exit 0, got $r[2]"
stdouts+=($r[3])
[[ -z $r[5] ]] || t_error "want no stderr, got ${(Vq-)r[5]}"
shift 6 r
done
local got want
# Check that we received all numbers.
got=(${(on)stdouts})
want=({1..$iter})
[[ $want = $got ]] || t_error "want stdout: ${(Vq-)want}, got ${(Vq-)got}"
# Test with longer running commands (sleep, then print).
iter=40
for i in {1..$iter}; do
async_job test "sleep 1 && print -n $i"
sleep 0.00001
(( iter % 6 == 0 )) && async_process_results test cb
done
start=$EPOCHSECONDS
while (( $#r / 6 < iter )); do
async_process_results test cb
(( EPOCHSECONDS - start > timeout )) && {
t_log "timed out after ${timeout}s"
t_fatal "wanted $iter results, got $(( $#r / 6 ))"
}
done
stdouts=()
while (( $#r > 0 )); do
[[ $r[1] = sleep ]] || t_error "want 'sleep', got ${(Vq-)r[1]}"
[[ $r[2] = 0 ]] || t_error "want exit 0, got $r[2]"
stdouts+=($r[3])
[[ -z $r[5] ]] || t_error "want no stderr, got ${(Vq-)r[5]}"
shift 6 r
done
# Check that we received all numbers.
got=(${(on)stdouts})
want=({1..$iter})
[[ $want = $got ]] || t_error "want stdout: ${(Vq-)want}, got ${(Vq-)got}"
}
test_async_job_multiple_commands_in_multiline_string() {
local -a result
cb() { result=("$@") }
async_start_worker test
# Test multi-line (single string) command.
async_job test 'print "hi\n 123 "'$'\nprint -n bye'
while ! async_process_results test cb; do :; done
async_stop_worker test
[[ $result[1] = print ]] || t_error "want command name: print, got" $result[1]
local want=$'hi\n 123 \nbye'
[[ $result[3] = $want ]] || t_error "want output: ${(Vq-)want}, got ${(Vq-)result[3]}"
}
test_async_job_git_status() {
local -a result
cb() { result=("$@") }
async_start_worker test
async_job test git status --porcelain
while ! async_process_results test cb; do :; done
async_stop_worker test
[[ $result[1] = git ]] || t_error "want command name: git, got" $result[1]
[[ $result[2] = 0 ]] || t_error "want exit code: 0, got" $result[2]
want=$(git status --porcelain)
got=$result[3]
[[ $got = $want ]] || t_error "want ${(Vq-)want}, got ${(Vq-)got}"
}
test_async_job_multiple_arguments_and_spaces() {
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
[[ $result[1] = print ]] || t_error "want command name: print, got" $result[1]
[[ $result[2] = 0 ]] || t_error "want exit code: 0, got" $result[2]
[[ $result[3] = "hello world" ]] || {
t_error "want output: \"hello world\", got" ${(Vq-)result[3]}
}
}
test_async_job_unique_worker() {
local -a result
cb() {
# Add to result so we can detect if it was called multiple times.
result+=("$@")
}
helper() {
sleep 0.1; print $1
}
# Start a unique (job) worker.
async_start_worker test -u
# 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
# If both jobs were running but only one was complete,
# async_process_results() could've returned true for
# the first job, wait a little extra to make sure the
# other didn't run.
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]}
}
test_async_job_error_and_nonzero_exit() {
local -a r
cb() { r+=("$@") }
error() {
print "Errors!"
12345
54321
print "Done!"
exit 99
}
async_start_worker test
async_job test error
while ! async_process_results test cb; do :; done
[[ $r[1] = error ]] || t_error "want 'error', got ${(Vq-)r[1]}"
[[ $r[2] = 99 ]] || t_error "want exit code 99, got $r[2]"
want=$'Errors!\nDone!'
[[ $r[3] = $want ]] || t_error "want ${(Vq-)want}, got ${(Vq-)r[3]}"
want=$'.*command not found: 12345\n.*command not found: 54321'
[[ $r[5] =~ $want ]] || t_error "want ${(Vq-)want}, got ${(Vq-)r[5]}"
}
test_async_worker_notify_sigwinch() {
local -a result
cb() { result=("$@") }
ASYNC_USE_ZLE_HANDLER=0
async_start_worker test -n
async_register_callback test cb
async_job test 'sleep 0.1; print hi'
while (( ! $#result )); do sleep 0.01; done
async_stop_worker test
[[ $result[3] = hi ]] || t_error "expected output: hi, got" $result[3]
}
test_async_job_keeps_nulls() {
local -a r
cb() { r=("$@") }
null_echo() {
print Hello$'\0' with$'\0' nulls!
print "Did we catch them all?"$'\0'
print $'\0'"What about the errors?"$'\0' 1>&2
}
async_start_worker test
async_job test null_echo
while ! async_process_results test cb; do :; done
async_stop_worker test
local want
want=$'Hello\0 with\0 nulls!\nDid we catch them all?\0'
[[ $r[3] = $want ]] || t_error stdout: want ${(Vq-)want}, got ${(Vq-)r[3]}
want=$'\0What about the errors?\0'
[[ $r[5] = $want ]] || t_error stderr: want ${(Vq-)want}, got ${(Vq-)r[5]}
}
test_async_flush_jobs() {
local -a r
cb() { r=+("$@") }
print_four() { print -n 4 }
print_123_delayed_exit() {
print -n 1
{ sleep 0.25 && print -n 2 } &!
{ sleep 0.3 && print -n 3 } &!
}
async_start_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
# immediately (and thus print 1) because the child processes are still
# running.
async_job test print_123_delayed_exit
# Check that the job is waiting for the child processes.
sleep 0.05
async_process_results test cb
(( $#r == 0 )) || t_error "want no output, got ${(Vq-)r}"
# Start a job that prints four, it will produce
# output but we will not process it.
async_job test print_four
sleep 0.2
# 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)}"
# NOTE(mafredri): First 'p' in print_four is lost when null-prefixing
# _async_job output.
[[ $output = *'rint_four 0 4'* ]] || {
t_error "want discarded output 'rint_four 0 4' when ASYNC_DEBUG=1, got ${(Vq-)output}"
}
# Check that the killed job did not produce output.
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() {
local -a result
cb() { result+=("$@") }
async_start_worker test1
t_defer async_stop_worker test1
# Start and stop a worker, will send SIGHUP to previous worker
# (probably has to do with some shell inheritance).
async_start_worker test2
async_stop_worker test2
async_job test1 print hi
integer start=$EPOCHREALTIME
while (( EPOCHREALTIME - start < 2.0 )); do
async_process_results test1 cb && break
done
(( $#result == 6 )) || t_error "wanted a result, got (${(@Vq)result})"
}
test_async_worker_update_pwd() {
local -a result
local eval_out
cb() {
if [[ $1 == '[async/eval]' ]]; then
eval_out="$3"
else
result+=("$3")
fi
}
async_start_worker test1
t_defer async_stop_worker test1
async_job test1 'print $PWD'
async_worker_eval test1 'print -n foo; cd ..; print -n bar; print -n -u2 baz'
async_job test1 'print $PWD'
start=$EPOCHREALTIME
while (( EPOCHREALTIME - start < 2.0 && $#result < 3 )); do
async_process_results test1 cb
done
(( $#result == 2 )) || t_error "wanted 2 results, got ${#result}"
[[ $eval_out = foobarbaz ]] || t_error "wanted async_worker_eval to output foobarbaz, got ${(q)eval_out}"
[[ -n $result[2] ]] || t_error "wanted second pwd to be non-empty"
[[ $result[1] != $result[2] ]] || t_error "wanted worker to change pwd, was ${(q)result[1]}, got ${(q)result[2]}"
}
setopt_helper() {
setopt localoptions $1
# Make sure to test with multiple options
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
# 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[2] = 0 ]] || t_fatal "$1 want exit code: 0, got" $result[2]
[[ $result[3] = "hello world" ]] || {
t_fatal "$1 want output: \"hello world\", got" ${(Vq-)result[3]}
}
}
test_all_options() {
local -a opts exclude
if [[ $ZSH_VERSION == 5.0.? ]]; then
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
{ sleep 15 && t_fatal "timed out" } &
local tpid=$!
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.
}
test_async_job_with_rc_expand_param() {
setopt localoptions rcexpandparam
# Make sure to test with multiple options
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
[[ $result[1] = print ]] || t_error "want command name: print, got" $result[1]
[[ $result[2] = 0 ]] || t_error "want exit code: 0, got" $result[2]
[[ $result[3] = "hello world" ]] || {
t_error "want output: \"hello world\", got" ${(Vq-)result[3]}
}
}
zpty_init() {
zmodload zsh/zpty
export PS1="<PROMPT>"
zpty zsh 'zsh -f +Z'
zpty -r zsh zpty_init1 "*<PROMPT>*" || {
t_log "initial prompt missing"
return 1
}
zpty -w zsh "{ $@ }"
zpty -r -m zsh zpty_init2 "*<PROMPT>*" || {
t_log "prompt missing"
return 1
}
}
zpty_run() {
zpty -w zsh "$*"
zpty -r -m zsh zpty_run "*<PROMPT>*" || {
t_log "prompt missing after ${(Vq-)*}"
return 1
}
}
zpty_deinit() {
zpty -d zsh
}
test_zle_watcher() {
zpty_init '
emulate -R zsh
setopt zle
stty 38400 columns 80 rows 24 tabs -icanon -iexten
TERM=vt100
. "'$PWD'/async.zsh"
async_init
print_result_cb() { print ${(Vq-)@} }
async_start_worker test
async_register_callback test print_result_cb
' || {
zpty_deinit
t_fatal "failed to init zpty"
}
t_defer zpty_deinit # Deinit after test completion.
zpty -w zsh "zle -F"
zpty -r -m zsh result "*_async_zle_watcher*" || {
t_fatal "want _async_zle_watcher to be registered as zle watcher, got output ${(Vq-)result}"
}
zpty_run async_job test 'print hello world' || t_fatal "could not send async_job command"
zpty -r -m zsh result "*print 0 'hello world'*" || {
t_fatal "want \"print 0 'hello world'\", got output ${(Vq-)result}"
}
}
test_main() {
# Load zsh-async before running each test.
zmodload zsh/datetime
. ./async.zsh
async_init
}