abra zsh config 2.0

This commit is contained in:
Andrey Anurin
2018-08-12 15:26:21 +03:00
parent 201abd09c4
commit 6b114440e2
1195 changed files with 68948 additions and 10539 deletions

View File

@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
[*.{yml,json}]
indent_style = space
indent_size = 2

View File

@@ -0,0 +1 @@
gitdir: ../../../../.git/modules/modules/prompt/external/async

View File

@@ -0,0 +1,54 @@
language: sh
addons:
apt:
packages:
- build-essential
env:
global:
- ZSH_DIST=$HOME/.zshdist
matrix:
# Use _ZSH_VERSION since if ZSH_VERSION is present, travis cacher thinks it
# is running in zsh and tries to use zsh specific functions.
- _ZSH_VERSION=5.5.1
- _ZSH_VERSION=5.4.2
- _ZSH_VERSION=5.3.1
- _ZSH_VERSION=5.3
- _ZSH_VERSION=5.2
- _ZSH_VERSION=5.1.1
- _ZSH_VERSION=5.0.8
- _ZSH_VERSION=5.0.2
cache:
directories:
- $ZSH_DIST
before_script:
- >
setup_zsh() {
dest="$ZSH_DIST/$1"
if [[ ! -d $dest/bin ]]; then
tmp="$(mktemp --directory --tmpdir="${TMPDIR:/tmp}" zshbuild.XXXXXX)"
(
cd "$tmp" &&
curl -L http://downloads.sourceforge.net/zsh/zsh-${1}.tar.gz | tar zx &&
cd zsh-$1 &&
./configure --prefix="$dest" &&
make &&
mkdir -p "$dest" &&
make install ||
echo "Failed to build zsh-${1}!"
)
fi
export PATH="$dest/bin:$PATH"
}
- setup_zsh $_ZSH_VERSION
- zsh --version
script:
- zsh test.zsh -v
allow_failures:
- env: _ZSH_VERSION=5.0.2
- env: _ZSH_VERSION=5.0.8

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Mathias Fredriksson <mafredri@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,163 @@
# zsh-async
```
Because your terminal should be able to perform tasks asynchronously without external tools!
```
## Intro (TL;DR)
With `zsh-async` you can run multiple asynchronous jobs, enforce unique jobs (multiple instances of the same job will not run), flush all currently running jobs and create multiple workers (each with their own jobs). For each worker you can register a callback-function through which you will be notified about the job results (job name, return code, output and execution time).
## Overview
`zsh-async` is a small library for running asynchronous tasks in zsh without requiring any external tools. It utilizes `zsh/zpty` to launch a pseudo-terminal in which all commands get executed without blocking any other processes. Checking for completed tasks can be done manually, by polling, or better yet, automatically whenever a process has finished executing by notifying through a `SIGWINCH` kill-signal.
This library bridges the gap between spawning child processes and disowning them. Child processes launched by normal means clutter the terminal with output about their state, and disowned processes become separate entities, no longer under control of the parent. Now you can have both!
## Usage
The async worker is a separate environment (think web worker). You send it a job (command + parameters) to execute and it returns the result of that execution through a callback function. If you find that you need to stop/start a worker to update global state (variables) you should consider refactoring so that state is passed during the `async_job` call (e.g. `async_job my_worker my_function $state1 $state2`).
### Installation
#### Manual
You can either source the `async.zsh` script directly or insert under your `$fpath` as async and autoload it through `autoload -Uz async && async`.
#### Integration
##### zplug
```
zplug "mafredri/zsh-async", from:"github", use:"async.zsh"
```
### Functions
The `zsh-async` library has a bunch of functions that need to be used to perform async actions:
#### `async_init`
Initializes the async library (not required if using async from `$fpath` with autoload.)
#### `async_start_worker <worker_name> [-u] [-n] [-p <pid>]`
Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a process when tasks are complete.
* `-u` unique. Only unique job names can run, e.g. the command `git status` will have `git` as the unique job name identifier
* `-n` notify through `SIGWINCH` signal. Needs to be caught with a `trap '' WINCH` in the process defined by `-p`
**NOTE:** When `zsh-async` is used in an interactive shell with ZLE enabled this option is not needed. Signaling through `SIGWINCH` has been replaced by a ZLE watcher that is triggered on output from the `zpty` instance (still requires a callback function through `async_register_callback` though). Technically zsh versions prior to `5.2` do not return the file descriptor for zpty instances, however, `zsh-async` attempts to deduce it anyway.
* `-p` pid to notify (defaults to current pid)
#### `async_stop_worker <worker_name_1> [<worker_name_2>]`
Simply stops a worker and all active jobs will be terminated immediately.
#### `async_job <worker_name> <my_function> [<function_params>]`
Start a new asynchronous job on specified worker, assumes the worker is running.
#### `async_process_results <worker_name> <callback_function>`
Get results from finished jobs and pass it to the to callback function. This is the only way to reliably return the job name, return code, output and execution time and with minimal effort.
The `callback_function` is called with the following parameters:
* `$1` job name, e.g. the function passed to async_job
* `$2` return code
* Returns `-1` if return code is missing, this should never happen, if it does, you have likely run into a bug. Please open a new [issue](https://github.com/mafredri/zsh-async/issues/new) with a detailed description of what you were doing.
* `$3` resulting (stdout) output from job execution
* `$4` execution time, floating point e.g. 0.0076138973 seconds
* `$5` resulting (stderr) error output from job execution
* `$6` has next result in buffer (0 = buffer empty, 1 = yes)
* This means another async job has completed and is pending in the buffer, it's very likely that your callback function will be called a second time (or more) in this execution. It's generally a good idea to e.g. delay prompt updates (`zle reset-prompt`) until the buffer is empty to prevent strange states in ZLE.
#### `async_register_callback <worker_name> <callback_function>`
Register a callback for completed jobs. As soon as a job is finished, `async_process_results` will be called with the specified callback function. This requires that a worker is initialized with the -n (notify) option.
#### `async_unregister_callback <worker_name>`
Unregister the callback for a specific worker.
#### `async_flush_jobs <worker_name>`
Flush all current jobs running on a worker. This will terminate any and all running processes under the worker by sending a `SIGTERM` to the entire process group, use with caution.
## Example code
```zsh
#!/usr/bin/env zsh
source ./async.zsh
async_init
# Initialize a new worker (with notify option)
async_start_worker my_worker -n
# Create a callback function to process results
COMPLETED=0
completed_callback() {
COMPLETED=$(( COMPLETED + 1 ))
print $@
}
# Register callback function for the workers completed jobs
async_register_callback my_worker completed_callback
# Give the worker some tasks to perform
async_job my_worker print hello
async_job my_worker sleep 0.3
# Wait for the two tasks to be completed
while (( COMPLETED < 2 )); do
print "Waiting..."
sleep 0.1
done
print "Completed $COMPLETED tasks!"
# Output:
# Waiting...
# print 0 hello 0.001583099365234375
# Waiting...
# Waiting...
# sleep 0 0.30631208419799805
# Completed 2 tasks!
```
## Testing
Tests are located in `*_test.zsh` and can be run by executing the test runner: `./test.zsh`.
Example:
```console
$ ./test.zsh
ok ./async_test.zsh 2.334s
```
The test suite can also run specific tasks that match a pattern, for example:
```console
$ ./test.zsh -v -run zle
=== RUN test_zle_watcher
--- PASS: test_zle_watcher (0.07s)
PASS
ok ./async_test.zsh 0.070s
```
## Limitations
* A NULL-character (`$'\0'`) is used by `async_job` to signify the end of the command, it is recommended not to pass them as arguments, although they should work when passing multiple arguments to `async_job` (because of quoting).
* Tell me? :)
## Tips
If you do not wish to use the `notify` feature, you can couple `zsh-async` with `zsh/sched` or the zsh `periodic` function for scheduling the worker results to be processed.
## Why did I make this?
I found a great theme for zsh, [Pure](https://github.com/sindresorhus/pure) by Sindre Sorhus. After using it for a while I noticed some graphical glitches due to the terminal being updated by a disowned process. Thus, I became inspired to get my hands dirty and find a solution. I tried many things, coprocesses (seemed too limited by themselves), different combinations of trapping kill-signals, etc. I also had problems with the zsh process ending up in a deadlock due to some zsh bug. After working out the kinks, I ended up with this and thought, hey, why not make it a library.

View File

@@ -0,0 +1,2 @@
0=${(%):-%N}
source ${0:A:h}/async.zsh

View File

@@ -0,0 +1,499 @@
#!/usr/bin/env zsh
#
# zsh-async
#
# version: 1.6.2
# author: Mathias Fredriksson
# url: https://github.com/mafredri/zsh-async
#
typeset -g ASYNC_VERSION=1.6.2
# Produce debug output from zsh-async when set to 1.
typeset -g ASYNC_DEBUG=${ASYNC_DEBUG:-0}
# Wrapper for jobs executed by the async worker, gives output in parseable format with execution time
_async_job() {
# Disable xtrace as it would mangle the output.
setopt localoptions noxtrace
# Store start time for job.
float -F duration=$EPOCHREALTIME
# Run the command and capture both stdout (`eval`) and stderr (`cat`) in
# separate subshells. When the command is complete, we grab write lock
# (mutex token) and output everything except stderr inside the command
# block, after the command block has completed, the stdin for `cat` is
# closed, causing stderr to be appended with a $'\0' at the end to mark the
# end of output from this job.
local stdout stderr ret tok
{
stdout=$(eval "$@")
ret=$?
duration=$(( EPOCHREALTIME - duration )) # Calculate duration.
# Grab mutex lock, stalls until token is available.
read -r -k 1 -p tok || exit 1
# Return output (<job_name> <return_code> <stdout> <duration> <stderr>).
print -r -n - ${(q)1} $ret ${(q)stdout} $duration
} 2> >(stderr=$(cat) && print -r -n - " "${(q)stderr}$'\0')
# Unlock mutex by inserting a token.
print -n -p $tok
}
# The background worker manages all tasks and runs them without interfering with other processes
_async_worker() {
# Reset all options to defaults inside async worker.
emulate -R zsh
# Make sure monitor is unset to avoid printing the
# pids of child processes.
unsetopt monitor
# Redirect stderr to `/dev/null` in case unforseen errors produced by the
# worker. For example: `fork failed: resource temporarily unavailable`.
# Some older versions of zsh might also print malloc errors (know to happen
# on at least zsh 5.0.2 and 5.0.8) likely due to kill signals.
exec 2>/dev/null
# When a zpty is deleted (using -d) all the zpty instances created before
# the one being deleted receive a SIGHUP, unless we catch it, the async
# worker would simply exit (stop working) even though visible in the list
# of zpty's (zpty -L).
TRAPHUP() {
return 0 # Return 0, indicating signal was handled.
}
local -A storage
local unique=0
local notify_parent=0
local parent_pid=0
local coproc_pid=0
local processing=0
local -a zsh_hooks zsh_hook_functions
zsh_hooks=(chpwd periodic precmd preexec zshexit zshaddhistory)
zsh_hook_functions=(${^zsh_hooks}_functions)
unfunction $zsh_hooks &>/dev/null # Deactivate all zsh hooks inside the worker.
unset $zsh_hook_functions # And hooks with registered functions.
unset zsh_hooks zsh_hook_functions # Cleanup.
child_exit() {
local -a pids
pids=(${${(v)jobstates##*:*:}%\=*})
# If coproc (cat) is the only child running, we close it to avoid
# leaving it running indefinitely and cluttering the process tree.
if (( ! processing )) && [[ $#pids = 1 ]] && [[ $coproc_pid = $pids[1] ]]; then
coproc :
coproc_pid=0
fi
# On older version of zsh (pre 5.2) we notify the parent through a
# SIGWINCH signal because `zpty` did not return a file descriptor (fd)
# prior to that.
if (( notify_parent )); then
# We use SIGWINCH for compatibility with older versions of zsh
# (pre 5.1.1) where other signals (INFO, ALRM, USR1, etc.) could
# cause a deadlock in the shell under certain circumstances.
kill -WINCH $parent_pid
fi
}
# Register a SIGCHLD trap to handle the completion of child processes.
trap child_exit CHLD
# Process option parameters passed to worker
while getopts "np:u" opt; do
case $opt in
n) notify_parent=1;;
p) parent_pid=$OPTARG;;
u) unique=1;;
esac
done
killjobs() {
local tok
local -a pids
pids=(${${(v)jobstates##*:*:}%\=*})
# No need to send SIGHUP if no jobs are running.
(( $#pids == 0 )) && continue
(( $#pids == 1 )) && [[ $coproc_pid = $pids[1] ]] && continue
# Grab lock to prevent half-written output in case a child
# process is in the middle of writing to stdin during kill.
(( coproc_pid )) && read -r -k 1 -p tok
kill -HUP -$$ # Send to entire process group.
coproc : # Quit coproc.
coproc_pid=0 # Reset pid.
}
local request
local -a cmd
while :; do
# Wait for jobs sent by async_job.
read -r -d $'\0' request || {
# Since we handle SIGHUP above (and thus do not know when `zpty -d`)
# occurs, a failure to read probably indicates that stdin has
# closed. This is why we propagate the signal to all children and
# exit manually.
kill -HUP -$$ # Send SIGHUP to all jobs.
exit 0
}
# Check for non-job commands sent to worker
case $request in
_unset_trap) notify_parent=0; continue;;
_killjobs) killjobs; continue;;
esac
# Parse the request using shell parsing (z) to allow commands
# to be parsed from single strings and multi-args alike.
cmd=("${(z)request}")
# Name of the job (first argument).
local job=$cmd[1]
# If worker should perform unique jobs
if (( unique )); then
# Check if a previous job is still running, if yes, let it finnish
for pid in ${${(v)jobstates##*:*:}%\=*}; do
if [[ ${storage[$job]} == $pid ]]; then
continue 2
fi
done
fi
# Guard against closing coproc from trap before command has started.
processing=1
# Because we close the coproc after the last job has completed, we must
# recreate it when there are no other jobs running.
if (( ! coproc_pid )); then
# Use coproc as a mutex for synchronized output between children.
coproc cat
coproc_pid="$!"
# Insert token into coproc
print -n -p "t"
fi
# Run job in background, completed jobs are printed to stdout.
_async_job $cmd &
# Store pid because zsh job manager is extremely unflexible (show jobname as non-unique '$job')...
storage[$job]="$!"
processing=0 # Disable guard.
done
}
#
# Get results from finnished jobs and pass it to the to callback function. This is the only way to reliably return the
# job name, return code, output and execution time and with minimal effort.
#
# usage:
# async_process_results <worker_name> <callback_function>
#
# callback_function is called with the following parameters:
# $1 = job name, e.g. the function passed to async_job
# $2 = return code
# $3 = resulting stdout from execution
# $4 = execution time, floating point e.g. 2.05 seconds
# $5 = resulting stderr from execution
# $6 = has next result in buffer (0 = buffer empty, 1 = yes)
#
async_process_results() {
setopt localoptions unset noshwordsplit noksharrays noposixidentifiers noposixstrings
local worker=$1
local callback=$2
local caller=$3
local -a items
local null=$'\0' data
integer -l len pos num_processed has_next
typeset -gA ASYNC_PROCESS_BUFFER
# Read output from zpty and parse it if available.
while zpty -r -t $worker data 2>/dev/null; do
ASYNC_PROCESS_BUFFER[$worker]+=$data
len=${#ASYNC_PROCESS_BUFFER[$worker]}
pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
# Keep going until we find a NULL-character.
if (( ! len )) || (( pos > len )); then
continue
fi
while (( pos <= len )); do
# Take the content from the beginning, until the NULL-character and
# perform shell parsing (z) and unquoting (Q) as an array (@).
items=("${(@Q)${(z)ASYNC_PROCESS_BUFFER[$worker][1,$pos-1]}}")
# Remove the extracted items from the buffer.
ASYNC_PROCESS_BUFFER[$worker]=${ASYNC_PROCESS_BUFFER[$worker][$pos+1,$len]}
len=${#ASYNC_PROCESS_BUFFER[$worker]}
if (( len > 1 )); then
pos=${ASYNC_PROCESS_BUFFER[$worker][(i)$null]} # Get index of NULL-character (delimiter).
fi
has_next=$(( len != 0 ))
if (( $#items == 5 )); then
items+=($has_next)
$callback "${(@)items}" # Send all parsed items to the callback.
else
# In case of corrupt data, invoke callback with *async* as job
# name, non-zero exit status and an error message on stderr.
$callback "async" 1 "" 0 "$0:$LINENO: error: bad format, got ${#items} items (${(q)items})" $has_next
fi
(( num_processed++ ))
done
done
(( num_processed )) && return 0
# Avoid printing exit value when `setopt printexitvalue` is active.`
[[ $caller = trap || $caller = watcher ]] && return 0
# No results were processed
return 1
}
# Watch worker for output
_async_zle_watcher() {
setopt localoptions noshwordsplit
typeset -gA ASYNC_PTYS ASYNC_CALLBACKS
local worker=$ASYNC_PTYS[$1]
local callback=$ASYNC_CALLBACKS[$worker]
if [[ -n $callback ]]; then
async_process_results $worker $callback watcher
fi
}
#
# Start a new asynchronous job on specified worker, assumes the worker is running.
#
# usage:
# async_job <worker_name> <my_function> [<function_params>]
#
async_job() {
setopt localoptions noshwordsplit noksharrays noposixidentifiers noposixstrings
local worker=$1; shift
local -a cmd
cmd=("$@")
if (( $#cmd > 1 )); then
cmd=(${(q)cmd}) # Quote special characters in multi argument commands.
fi
# Quote the cmd in case RC_EXPAND_PARAM is set.
zpty -w $worker "$cmd"$'\0'
}
# This function traps notification signals and calls all registered callbacks
_async_notify_trap() {
setopt localoptions noshwordsplit
local k
for k in ${(k)ASYNC_CALLBACKS}; do
async_process_results $k ${ASYNC_CALLBACKS[$k]} trap
done
}
#
# Register a callback for completed jobs. As soon as a job is finnished, async_process_results will be called with the
# specified callback function. This requires that a worker is initialized with the -n (notify) option.
#
# usage:
# async_register_callback <worker_name> <callback_function>
#
async_register_callback() {
setopt localoptions noshwordsplit nolocaltraps
typeset -gA ASYNC_CALLBACKS
local worker=$1; shift
ASYNC_CALLBACKS[$worker]="$*"
# Enable trap when the ZLE watcher is unavailable, allows
# workers to notify (via -n) when a job is done.
if [[ ! -o interactive ]] || [[ ! -o zle ]]; then
trap '_async_notify_trap' WINCH
fi
}
#
# Unregister the callback for a specific worker.
#
# usage:
# async_unregister_callback <worker_name>
#
async_unregister_callback() {
typeset -gA ASYNC_CALLBACKS
unset "ASYNC_CALLBACKS[$1]"
}
#
# Flush all current jobs running on a worker. This will terminate any and all running processes under the worker, use
# with caution.
#
# usage:
# async_flush_jobs <worker_name>
#
async_flush_jobs() {
setopt localoptions noshwordsplit
local worker=$1; shift
# Check if the worker exists
zpty -t $worker &>/dev/null || return 1
# Send kill command to worker
async_job $worker "_killjobs"
# Clear the zpty buffer.
local junk
if zpty -r -t $worker junk '*'; then
(( ASYNC_DEBUG )) && print -n "async_flush_jobs $worker: ${(V)junk}"
while zpty -r -t $worker junk '*'; do
(( ASYNC_DEBUG )) && print -n "${(V)junk}"
done
(( ASYNC_DEBUG )) && print
fi
# Finally, clear the process buffer in case of partially parsed responses.
typeset -gA ASYNC_PROCESS_BUFFER
unset "ASYNC_PROCESS_BUFFER[$worker]"
}
#
# Start a new async worker with optional parameters, a worker can be told to only run unique tasks and to notify a
# process when tasks are complete.
#
# usage:
# async_start_worker <worker_name> [-u] [-n] [-p <pid>]
#
# opts:
# -u unique (only unique job names can run)
# -n notify through SIGWINCH signal
# -p pid to notify (defaults to current pid)
#
async_start_worker() {
setopt localoptions noshwordsplit
local worker=$1; shift
zpty -t $worker &>/dev/null && return
typeset -gA ASYNC_PTYS
typeset -h REPLY
typeset has_xtrace=0
# Make sure async worker is started without xtrace
# (the trace output interferes with the worker).
[[ -o xtrace ]] && {
has_xtrace=1
unsetopt xtrace
}
if (( ! ASYNC_ZPTY_RETURNS_FD )) && [[ -o interactive ]] && [[ -o zle ]]; then
# When zpty doesn't return a file descriptor (on older versions of zsh)
# we try to guess it anyway.
integer -l zptyfd
exec {zptyfd}>&1 # Open a new file descriptor (above 10).
exec {zptyfd}>&- # Close it so it's free to be used by zpty.
fi
zpty -b $worker _async_worker -p $$ $@ || {
async_stop_worker $worker
return 1
}
# Re-enable it if it was enabled, for debugging.
(( has_xtrace )) && setopt xtrace
if [[ $ZSH_VERSION < 5.0.8 ]]; then
# For ZSH versions older than 5.0.8 we delay a bit to give
# time for the worker to start before issuing commands,
# otherwise it will not be ready to receive them.
sleep 0.001
fi
if [[ -o interactive ]] && [[ -o zle ]]; then
if (( ! ASYNC_ZPTY_RETURNS_FD )); then
REPLY=$zptyfd # Use the guessed value for the file desciptor.
fi
ASYNC_PTYS[$REPLY]=$worker # Map the file desciptor to the worker.
zle -F $REPLY _async_zle_watcher # Register the ZLE handler.
# Disable trap in favor of ZLE handler when notify is enabled (-n).
async_job $worker _unset_trap
fi
}
#
# Stop one or multiple workers that are running, all unfetched and incomplete work will be lost.
#
# usage:
# async_stop_worker <worker_name_1> [<worker_name_2>]
#
async_stop_worker() {
setopt localoptions noshwordsplit
local ret=0 worker k v
for worker in $@; do
# Find and unregister the zle handler for the worker
for k v in ${(@kv)ASYNC_PTYS}; do
if [[ $v == $worker ]]; then
zle -F $k
unset "ASYNC_PTYS[$k]"
fi
done
async_unregister_callback $worker
zpty -d $worker 2>/dev/null || ret=$?
# Clear any partial buffers.
typeset -gA ASYNC_PROCESS_BUFFER
unset "ASYNC_PROCESS_BUFFER[$worker]"
done
return $ret
}
#
# Initialize the required modules for zsh-async. To be called before using the zsh-async library.
#
# usage:
# async_init
#
async_init() {
(( ASYNC_INIT_DONE )) && return
typeset -g ASYNC_INIT_DONE=1
zmodload zsh/zpty
zmodload zsh/datetime
# Check if zsh/zpty returns a file descriptor or not,
# shell must also be interactive with zle enabled.
typeset -g ASYNC_ZPTY_RETURNS_FD=0
[[ -o interactive ]] && [[ -o zle ]] && {
typeset -h REPLY
zpty _async_test :
(( REPLY )) && ASYNC_ZPTY_RETURNS_FD=1
zpty -d _async_test
}
}
async() {
async_init
}
async "$@"

View File

@@ -0,0 +1,574 @@
#!/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 trailing null, parse, unquote and interpret as array.
line=$line[1,$#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)}"
[[ $output = *'print_four 0 4'* ]] || {
t_error "want discarded output 'print_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})"
}
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
}

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env zsh
#
# zsh-async test runner.
# Checks for test files named *_test.zsh or *_test.sh and runs all functions
# named test_*.
#
emulate -R zsh
zmodload zsh/datetime
zmodload zsh/parameter
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
show_help() {
print "usage: ./test.zsh [-v] [-x] [-run pattern] [search pattern]"
}
parse_opts() {
local -a verbose debug trace help run
local out
zparseopts -E -D \
v=verbose verbose=verbose -verbose=verbose \
d=debug debug=debug -debug=debug \
x=trace trace=trace -trace=trace \
h=help -help=help \
\?=help \
run:=run -run:=run
(( $? )) || (( $+help[1] )) && show_help && exit 0
if (( $#@ > 1 )); then
print -- "unknown arguments: $@"
show_help
exit 1
fi
[[ -n $1 ]] && TEST_GLOB=$1
TEST_VERBOSE=$+verbose[1]
TEST_TRACE=$+trace[1]
ZTEST_DEBUG=$+debug[1]
(( $+run[2] )) && TEST_RUN=$run[2]
}
t_runner_init() {
emulate -L zsh
zmodload zsh/parameter
# _t_runner is the main loop that waits for tests,
# used to abort test execution by exec.
_t_runner() {
local -a _test_defer_funcs
integer _test_errors=0
while read -r; do
eval "$REPLY"
done
}
_t_log() {
local trace=$1; shift
local -a lines indent
lines=("${(@f)@}")
indent=($'\t\t'${^lines[2,$#lines]})
print -u7 -lr - $'\t'"$trace: $lines[1]" ${(F)indent}
}
# t_log is for printing log output, visible in verbose (-v) mode.
t_log() {
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() {
_t_log $funcfiletrace[1] "$*"
() { return 100 }
t_done
}
# t_error logs the error and fails the test without aborting.
t_error() {
(( _test_errors++ ))
_t_log $funcfiletrace[1] "$*"
}
# t_fatal fails the test and halts execution immediately.
t_fatal() {
_t_log $funcfiletrace[1] "$*"
() { return 101 }
t_done
}
# t_defer takes a function (and optionally, arguments)
# to be executed after the test has completed.
t_defer() {
_test_defer_funcs+=("$*")
}
# t_done completes the test execution, called automatically after a test.
# Can also be called manually when the test is done.
t_done() {
local ret=$? w=${1:-1}
(( _test_errors )) && ret=101
(( w )) && wait # Wait for test children to exit.
for d in $_test_defer_funcs; do
eval "$d"
done
print -n -u8 $ret # Send exit code to ztest.
exec _t_runner # Replace shell, wait for new test.
}
source $1 # Load the test module.
# Send available test functions to main process.
print -u7 ${(R)${(okM)functions:#test_*}:#test_main}
# Run test_main.
if [[ -n $functions[test_main] ]]; then
test_main
fi
exec _t_runner # Wait for commands.
}
# run_test_module runs all the tests from a test module (asynchronously).
run_test_module() {
local module=$1
local -a tests
float start module_time
# Create fd's for communication with test runner.
integer run_pid cmdoutfd cmdinfd outfd infd doneoutfd doneinfd
coproc cat; exec {cmdoutfd}>&p; exec {cmdinfd}<&p
coproc cat; exec {outfd}>&p; exec {infd}<&p
coproc cat; exec {doneoutfd}>&p; exec {doneinfd}<&p
# No need to keep coproc (&p) open since we
# have redirected the outputs and inputs.
coproc exit
# Launch a new interactive zsh test runner. We don't capture stdout
typeset -a run_args
(( TEST_TRACE )) && run_args+=('-x')
zsh -s $run_args <&$cmdinfd 7>&$outfd 8>&$doneoutfd &
run_pid=$!
# Initialize by sending function body from t_runner_init
# and immediately execute it as an anonymous function.
syswrite -o $cmdoutfd "() { ${functions[t_runner_init]} } $module"$'\n'
sysread -i $infd
tests=(${(@)=REPLY})
[[ -n $TEST_RUN ]] && tests=(${(M)tests:#*$TEST_RUN*})
integer mod_exit=0
float mod_start mod_time
mod_start=$EPOCHREALTIME # Store the module start time.
# Run all tests.
local test_out
float test_start test_time
integer text_exit
for test in $tests; do
(( TEST_VERBOSE )) && print "=== RUN $test"
test_start=$EPOCHREALTIME # Store the test start time.
# Start the test.
syswrite -o $cmdoutfd "$test; t_done"$'\n'
test_out=
test_exit=-1
while (( test_exit == -1 )); do
# Block until there is data to be read.
zselect -r $doneinfd -r $infd
if [[ $reply[2] = $doneinfd ]]; then
sysread -i $doneinfd
test_exit=$REPLY # Store reply from sysread
# Store the test execution time.
test_time=$(( EPOCHREALTIME - test_start ))
fi
# Read all output from the test output channel.
while sysread -i $infd -t 0; do
test_out+=$REPLY
unset REPLY
done
done
case $test_exit in
(0|1) state=PASS;;
(100) state=SKIP;;
(101|102) state=FAIL; mod_exit=1;;
*) state="????";;
esac
if [[ $state = FAIL ]] || (( TEST_VERBOSE )); then
printf -- "--- $state: $test (%.2fs)\n" $test_time
print -n $test_out
fi
done
# Store module execution time.
mod_time=$(( EPOCHREALTIME - mod_start ))
# Perform cleanup.
kill -HUP $run_pid
exec {outfd}>&-
exec {infd}<&-
exec {cmdinfd}>&-
exec {cmdoutfd}<&-
exec {doneinfd}<&-
exec {doneoutfd}>&-
if (( mod_exit )); then
print "FAIL"
(( TEST_VERBOSE )) && print "exit code $mod_exit"
printf "FAIL\t$module\t%.3fs\n" $mod_time
else
(( TEST_VERBOSE )) && print "PASS"
printf "ok\t$module\t%.3fs\n" $mod_time
fi
return $mod_exit
}
cleanup() {
trap - HUP
kill -HUP $$ 2>/dev/null
kill -HUP -$$ 2>/dev/null
}
trap cleanup EXIT INT HUP QUIT TERM USR1
# Parse command arguments.
parse_opts $@
(( ZTEST_DEBUG )) && setopt xtrace
# Execute tests modules.
failed=0
for tf in ${~TEST_GLOB}/*_test.(zsh|sh); do
run_test_module $tf &
wait $!
(( $? )) && failed=1
done
exit $failed