diff options
Diffstat (limited to 'Test/ztst.zsh')
-rwxr-xr-x | Test/ztst.zsh | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/Test/ztst.zsh b/Test/ztst.zsh new file mode 100755 index 000000000..d3d03e883 --- /dev/null +++ b/Test/ztst.zsh @@ -0,0 +1,316 @@ +#!/usr/local/bin/zsh -f +# The line above is just for convenience. Normally tests will be run using +# a specified version of zsh. With dynamic loading, any required libraries +# must already have been installed in that case. +# +# Takes one argument: the name of the test file. Currently only one such +# file will be processed each time ztst.zsh is run. This is slower, but +# much safer in terms of preserving the correct status. +# To avoid namespace pollution, all functions and parameters used +# only by the script begin with ZTST_. +# +# Options (without arguments) may precede the test file argument; these +# are interpreted as shell options to set. -x is probably the most useful. + +# Produce verbose messages if non-zero. +# If 1, produce reports of tests executed; if 2, also report on progress. +ZTST_verbose=0 + +# We require all options to be reset, not just emulation options. +# Unfortunately, due to the crud which may be in /etc/zshenv this might +# still not be good enough. Maybe we should trick it somehow. +emulate -R zsh + +# We need to be able to save and restore the options used in the test. +# We use the $options variable of the parameter module for this. +zmodload -i parameter + +# Note that both the following are regular arrays, since we only use them +# in whole array assignments to/from $options. +# Options set in test code (i.e. by default all standard options) +ZTST_testopts=(${(kv)options}) + +setopt extendedglob nonomatch +while [[ $1 = [-+]* ]]; do + set $1 + shift +done +# Options set in main script +ZTST_mainopts=(${(kv)options}) + +# We run in the current directory, so remember it. +ZTST_testdir=$PWD +ZTST_testname=$1 + +# Temporary files for redirection inside tests. +ZTST_in=${TMPPREFIX-:/tmp/zsh}.ztst.in.$$ +# hold the expected output +ZTST_out=${TMPPREFIX-:/tmp/zsh}.ztst.out.$$ +ZTST_err=${TMPPREFIX-:/tmp/zsh}.ztst.err.$$ +# hold the actual output from the test +ZTST_tout=${TMPPREFIX-:/tmp/zsh}.ztst.tout.$$ +ZTST_terr=${TMPPREFIX-:/tmp/zsh}.ztst.terr.$$ + +ZTST_cleanup() { + rm -rf $ZTST_testdir/dummy.tmp $ZTST_testdir/*.tmp \ + $ZTST_in $ZTST_out $ZTST_err $ZTST_tout $ZTST_terr +} + +# This cleanup always gets performed, even if we abort. Later, +# we should try and arrange that any test-specific cleanup +# always gets called as well. +trap - 'print cleaning up... +ZTST_cleanup' INT QUIT TERM +# Make sure it's clean now. +rm -rf dummy.tmp *.tmp + +# Report failure. Note that all output regarding the tests goes to stdout. +# That saves an unpleasant mixture of stdout and stderr to sort out. +ZTST_testfailed() { + print "Test $ZTST_testname failed: $1" + if [[ -n $ZTST_message ]]; then + print "Was testing: $ZTST_message" + fi + ZTST_cleanup + exit 1 +} + +# Print messages if $ZTST_verbose is non-empty +ZTST_verbose() { + local lev=$1 + shift + [[ -n $ZTST_verbose && $ZTST_verbose -ge $lev ]] && print $* >&8 +} + +[[ ! -r $ZTST_testname ]] && ZTST_testfailed "can't read test file." + +[[ -n $ZTST_verbose && $ZTST_verbose -ge 0 ]] && exec 8>&1 +exec 9<$ZTST_testname + +# The current line read from the test file. +ZTST_curline='' +# The current section being run +ZTST_cursect='' + +# Get a new input line. Don't mangle spaces; set IFS locally to empty. +# We shall skip comments at this level. +ZTST_getline() { + local IFS= + while true; do + read ZTST_curline <&9 || return 1 + [[ $ZTST_curline == \#* ]] || return 0 + done +} + +# Get the name of the section. It may already have been read into +# $curline, or we may have to skip some initial comments to find it. +ZTST_getsect() { + local match mbegin mend + + while [[ $ZTST_curline != '%'(#b)([[:alnum:]]##)* ]]; do + ZTST_getline || return 1 + [[ $ZTST_curline = [[:blank:]]# ]] && continue + if [[ $ZTST_curline != '%'[[:alnum:]]##* ]]; then + ZTST_testfailed "bad line found before or after section: +$ZTST_curline" + fi + done + # have the next line ready waiting + ZTST_getline + ZTST_cursect=${match[1]} + ZTST_verbose 2 "ZTST_getsect: read section name: $ZTST_cursect" + return 0 +} + +# Read in an indented code chunk for execution +ZTST_getchunk() { + # Code chunks are always separated by blank lines or the + # end of a section, so if we already have a piece of code, + # we keep it. Currently that shouldn't actually happen. + ZTST_code='' + # First find the chunk. + while [[ $ZTST_curline = [[:blank:]]# ]]; do + ZTST_getline || break + done + while [[ $ZTST_curline = [[:blank:]]##[^[:blank:]]* ]]; do + ZTST_code="${ZTST_code:+${ZTST_code} +}${ZTST_curline}" + ZTST_getline || break + done + ZTST_verbose 2 "ZTST_getchunk: read code chunk: +$ZTST_code" + [[ -n $ZTST_code ]] +} + +# Read in a piece for redirection. +ZTST_getredir() { + local char=${ZTST_curline[1]} + ZTST_redir=${ZTST_curline[2,-1]} + while ZTST_getline; do + [[ $ZTST_curline[1] = $char ]] || break + ZTST_redir="${ZTST_redir} +${ZTST_curline[2,-1]}" + done + ZTST_verbose 2 "ZTST_getredir: read redir for '$char': +$ZTST_redir" +} + +# Execute an indented chunk. Redirections will already have +# been set up, but we need to handle the options. +ZTST_execchunk() { + options=($ZTST_testopts) + eval "$ZTST_code" + ZTST_status=$? + ZTST_verbose 2 "ZTST_execchunk: status $ZTST_status" + ZTST_testopts=(${(kv)options}) + options=($ZTST_mainopts) + return $ZTST_status +} + +# Functions for preparation and cleaning. +# When cleaning up (non-zero string argument), we ignore status. +ZTST_prepclean() { + # Execute indented code chunks. + while ZTST_getchunk; do + ZTST_execchunk >/dev/null || [[ -n $1 ]] || + ZTST_testfailed "non-zero status from preparation code: +$ZTST_code" + done +} + +ZTST_test() { + local last match mbegin mend found + + while true; do + rm -f $ZTST_in $ZTST_out $ZTST_err + touch $ZTST_in $ZTST_out $ZTST_err + ZTST_message='' + found=0 + + ZTST_verbose 2 "ZTST_test: looking for new test" + + while true; do + ZTST_verbose 2 "ZTST_test: examining line: +$ZTST_curline" + case $ZTST_curline in + %*) if [[ $found = 0 ]]; then + break 2 + else + last=1 + break + fi + ;; + [[:space:]]#) + if [[ $found = 0 ]]; then + ZTST_getline || break 2 + continue + else + break + fi + ;; + [[:space:]]##[^[:space:]]*) ZTST_getchunk + [[ $ZTST_curline != [-0-9]* ]] && + ZTST_testfailed "expecting test status at: +$ZTST_curline" + ZTST_xstatus=$ZTST_curline + if [[ $ZTST_curline == (#b)([^:]##):(*) ]]; then + ZTST_xstatus=$match[1] + ZTST_message=$match[2] + fi + ZTST_getline + found=1 + ;; + '<'*) ZTST_getredir + print -r "${(e)ZTST_redir}" >>$ZTST_in + found=1 + ;; + '>'*) ZTST_getredir + print -r "${(e)ZTST_redir}" >>$ZTST_out + found=1 + ;; + '?'*) ZTST_getredir + print -r "${(e)ZTST_redir}" >>$ZTST_err + found=1 + ;; + *) ZTST_testfailed "bad line in test block: +$ZTST_curline" + ;; + esac + done + + # If we found some code to execute... + if [[ -n $ZTST_code ]]; then + ZTST_verbose 1 "Running test: +$ZTST_message" + ZTST_verbose 2 "ZTST_test: expecting status: $ZTST_xstatus" + + ZTST_execchunk <$ZTST_in >$ZTST_tout 2>$ZTST_terr + + # First check we got the right status, if specified. + if [[ $ZTST_xstatus != - && $ZTST_xstatus != $ZTST_status ]]; then + ZTST_testfailed "bad status $ZTST_status, expected $ZTST_xstatus from: +$ZTST_code" + fi + + ZTST_verbose 2 "ZTST_test: test produced standard output: +$(<$ZTST_tout) +ZTST_test: and standard error: +$(<$ZTST_terr)" + + # Now check output and error. + if ! diff -c $ZTST_out $ZTST_tout; then + ZTST_testfailed "output differs from expected as shown above for: +$ZTST_code" + fi + if ! diff -c $ZTST_err $ZTST_terr; then + ZTST_testfailed "error output differs from expected as shown above for: +$ZTST_code" + fi + fi + ZTST_verbose 1 "Test successful." + [[ -n $last ]] && break + done + + ZTST_verbose 2 "ZTST_test: all tests successful" + + # reset message to keep ZTST_testfailed output correct + ZTST_message='' +} + + +# Remember which sections we've done. +typeset -A ZTST_sects +ZTST_sects=(prep 0 test 0 clean 0) + +# Now go through all the different sections until the end. +while ZTST_getsect; do + case $ZTST_cursect in + prep) if (( ${ZTST_sects[prep]} + ${ZTST_sects[test]} + \ + ${ZTST_sects[clean]} )); then + ZTST_testfailed "\`prep' section must come first" + fi + ZTST_prepclean + ZTST_sects[prep]=1 + ;; + test) + if (( ${ZTST_sects[test]} + ${ZTST_sects[clean]} )); then + ZTST_testfailed "bad placement of \`test' section" + fi + ZTST_test + ZTST_sects[test]=1 + ;; + clean) + if (( ${ZTST_sects[test]} == 0 || ${ZTST_sects[clean]} )); then + ZTST_testfailed "bad use of \`clean' section" + fi + ZTST_prepclean 1 + ZTST_sects[clean]=1 + ;; + *) ZTST_testfailed "bad section name: $ZTST_cursect" + ;; + esac +done + +print "$ZTST_testname: all tests successful." +ZTST_cleanup +exit 0 |