mirror of
git://git.code.sf.net/p/zsh/code
synced 2024-06-08 00:06:04 +02:00
c6dbd862d0
Use ed to get minimal user interaction without needing advanced terminal handling. Improve test output of failures of pattern differences.
567 lines
15 KiB
Bash
Executable File
567 lines
15 KiB
Bash
Executable File
#!/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.
|
|
# Defined in such a way that any value from the environment is used.
|
|
: ${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
|
|
|
|
# Ensure the locale does not screw up sorting. Don't supply a locale
|
|
# unless there's one set, to minimise problems.
|
|
[[ -n $LC_ALL ]] && LC_ALL=C
|
|
[[ -n $LC_COLLATE ]] && LC_COLLATE=C
|
|
[[ -n $LC_NUMERIC ]] && LC_NUMERIC=C
|
|
[[ -n $LC_MESSAGES ]] && LC_MESSAGES=C
|
|
[[ -n $LANG ]] && LANG=C
|
|
|
|
# Don't propagate variables that are set by default in the shell.
|
|
typeset +x WORDCHARS
|
|
|
|
# Set the module load path to correspond to this build of zsh.
|
|
# This Modules directory should have been created by "make check".
|
|
[[ -d Modules/zsh ]] && module_path=( $PWD/Modules )
|
|
# Allow this to be passed down.
|
|
export MODULE_PATH
|
|
|
|
# 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 zsh/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
|
|
|
|
integer ZTST_testfailed
|
|
|
|
# This is POSIX nonsense. Because of the vague feeling someone, somewhere
|
|
# may one day need to examine the arguments of "tail" using a standard
|
|
# option parser, every Unix user in the world is expected to switch
|
|
# to using "tail -n NUM" instead of "tail -NUM". Older versions of
|
|
# tail don't support this.
|
|
tail() {
|
|
emulate -L zsh
|
|
|
|
if [[ -z $TAIL_SUPPORTS_MINUS_N ]]; then
|
|
local test
|
|
test=$(echo "foo\nbar" | command tail -n 1 2>/dev/null)
|
|
if [[ $test = bar ]]; then
|
|
TAIL_SUPPORTS_MINUS_N=1
|
|
else
|
|
TAIL_SUPPORTS_MINUS_N=0
|
|
fi
|
|
fi
|
|
|
|
integer argi=${argv[(i)-<->]}
|
|
|
|
if [[ $argi -le $# && $TAIL_SUPPORTS_MINUS_N = 1 ]]; then
|
|
argv[$argi]=(-n ${argv[$argi][2,-1]})
|
|
fi
|
|
|
|
command tail "$argv[@]"
|
|
}
|
|
|
|
# The source directory is not necessarily the current directory,
|
|
# but if $0 doesn't contain a `/' assume it is.
|
|
if [[ $0 = */* ]]; then
|
|
ZTST_srcdir=${0%/*}
|
|
else
|
|
ZTST_srcdir=$PWD
|
|
fi
|
|
[[ $ZTST_srcdir = /* ]] || ZTST_srcdir="$ZTST_testdir/$ZTST_srcdir"
|
|
|
|
# Set the function autoload paths to correspond to this build of zsh.
|
|
fpath=( $ZTST_srcdir/../Functions/*~*/CVS(/)
|
|
$ZTST_srcdir/../Completion
|
|
$ZTST_srcdir/../Completion/*/*~*/CVS(/) )
|
|
|
|
: ${TMPPREFIX:=/tmp/zsh}
|
|
ZTST_tmp=${TMPPREFIX}.ztst.$$
|
|
if ! rm -f $ZTST_tmp || ! mkdir -p $ZTST_tmp || ! chmod go-w $ZTST_tmp; then
|
|
print "Can't create $ZTST_tmp for exclusive use." >&2
|
|
exit 1
|
|
fi
|
|
# Temporary files for redirection inside tests.
|
|
ZTST_in=${ZTST_tmp}/ztst.in
|
|
# hold the expected output
|
|
ZTST_out=${ZTST_tmp}/ztst.out
|
|
ZTST_err=${ZTST_tmp}/ztst.err
|
|
# hold the actual output from the test
|
|
ZTST_tout=${ZTST_tmp}/ztst.tout
|
|
ZTST_terr=${ZTST_tmp}/ztst.terr
|
|
|
|
ZTST_cleanup() {
|
|
cd $ZTST_testdir
|
|
rm -rf $ZTST_testdir/dummy.tmp $ZTST_testdir/*.tmp(N) ${ZTST_tmp}
|
|
}
|
|
|
|
# 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 -r "Test $ZTST_testname failed: $1"
|
|
if [[ -n $ZTST_message ]]; then
|
|
print -r "Was testing: $ZTST_message"
|
|
fi
|
|
print -r "$ZTST_testname: test failed."
|
|
if [[ -n $ZTST_failmsg ]]; then
|
|
print -r "The following may (or may not) help identifying the cause:
|
|
$ZTST_failmsg"
|
|
fi
|
|
ZTST_testfailed=1
|
|
return 1
|
|
}
|
|
|
|
# Print messages if $ZTST_verbose is non-empty
|
|
ZTST_verbose() {
|
|
local lev=$1
|
|
shift
|
|
if [[ -n $ZTST_verbose && $ZTST_verbose -ge $lev ]]; then
|
|
print -r -u $ZTST_fd -- $*
|
|
fi
|
|
}
|
|
ZTST_hashmark() {
|
|
if [[ ZTST_verbose -le 0 && -t $ZTST_fd ]]; then
|
|
print -n -u$ZTST_fd -- ${(pl:SECONDS::\#::\#\r:)}
|
|
fi
|
|
(( SECONDS > COLUMNS+1 && (SECONDS -= COLUMNS) ))
|
|
}
|
|
|
|
if [[ ! -r $ZTST_testname ]]; then
|
|
ZTST_testfailed "can't read test file."
|
|
exit 1
|
|
fi
|
|
|
|
exec {ZTST_fd}>&1
|
|
exec {ZTST_input}<$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 -u $ZTST_input -r ZTST_curline || 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.
|
|
# If argument present, it's OK to skip the reset of the current section,
|
|
# so no error if we find garbage.
|
|
ZTST_getsect() {
|
|
local match mbegin mend
|
|
|
|
while [[ $ZTST_curline != '%'(#b)([[:alnum:]]##)* ]]; do
|
|
ZTST_getline || return 1
|
|
[[ $ZTST_curline = [[:blank:]]# ]] && continue
|
|
if [[ $# -eq 0 && $ZTST_curline != '%'[[:alnum:]]##* ]]; then
|
|
ZTST_testfailed "bad line found before or after section:
|
|
$ZTST_curline"
|
|
exit 1
|
|
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]} fn
|
|
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"
|
|
|
|
case $char in
|
|
('<') fn=$ZTST_in
|
|
;;
|
|
('>') fn=$ZTST_out
|
|
;;
|
|
('?') fn=$ZTST_err
|
|
;;
|
|
(*) ZTST_testfailed "bad redir operator: $char"
|
|
return 1
|
|
;;
|
|
esac
|
|
if [[ $ZTST_flags = *q* && $char = '<' ]]; then
|
|
# delay substituting output until variables are set
|
|
print -r -- "${(e)ZTST_redir}" >>$fn
|
|
else
|
|
print -r -- "$ZTST_redir" >>$fn
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Execute an indented chunk. Redirections will already have
|
|
# been set up, but we need to handle the options.
|
|
ZTST_execchunk() {
|
|
setopt localloops # don't let continue & break propagate out
|
|
options=($ZTST_testopts)
|
|
() {
|
|
unsetopt localloops
|
|
eval "$ZTST_code"
|
|
}
|
|
ZTST_status=$?
|
|
# careful... ksh_arrays may be in effect.
|
|
ZTST_testopts=(${(kv)options[*]})
|
|
options=(${ZTST_mainopts[*]})
|
|
ZTST_verbose 2 "ZTST_execchunk: status $ZTST_status"
|
|
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 ]] || {
|
|
[[ -n "$ZTST_unimplemented" ]] ||
|
|
ZTST_testfailed "non-zero status from preparation code:
|
|
$ZTST_code" && return 0
|
|
}
|
|
done
|
|
}
|
|
|
|
# diff wrapper
|
|
ZTST_diff() {
|
|
emulate -L zsh
|
|
setopt extendedglob
|
|
|
|
local diff_out
|
|
integer diff_pat diff_ret
|
|
|
|
case $1 in
|
|
(p)
|
|
diff_pat=1
|
|
;;
|
|
|
|
(d)
|
|
;;
|
|
|
|
(*)
|
|
print "Bad ZTST_diff code: d for diff, p for pattern match"
|
|
;;
|
|
esac
|
|
shift
|
|
|
|
if (( diff_pat )); then
|
|
local -a diff_lines1 diff_lines2
|
|
integer failed i l n
|
|
local p
|
|
|
|
diff_lines1=("${(f)$(<$argv[-2])}")
|
|
diff_lines2=("${(f)$(<$argv[-1])}")
|
|
if (( ${#diff_lines1} != ${#diff_lines2} )); then
|
|
failed=1
|
|
else
|
|
for (( i = 1; i <= ${#diff_lines1}; i++ )); do
|
|
if [[ ${diff_lines2[i]} != ${~diff_lines1[i]} ]]; then
|
|
failed=1
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
if (( failed )); then
|
|
print -r "Pattern match failed, line $i:"
|
|
n=${#diff_lines1}
|
|
(( ${#diff_lines2} > $n )) && n=${#diff_lines2}
|
|
for (( l = 1; l <= n; ++l )); do
|
|
if (( l == i )); then
|
|
p="-"
|
|
else
|
|
p=" "
|
|
fi
|
|
print -r -- "$p<${diff_lines1[l]}"
|
|
done
|
|
for (( l = 1; l <= n; ++l )); do
|
|
if (( l == i )); then
|
|
p="+"
|
|
else
|
|
p=" "
|
|
fi
|
|
print -r -- "$p>${diff_lines2[l]}"
|
|
done
|
|
diff_ret=1
|
|
fi
|
|
else
|
|
diff_out=$(diff -a "$@")
|
|
diff_ret="$?"
|
|
if [[ "$diff_ret" != "0" ]]; then
|
|
print -r -- "$diff_out"
|
|
fi
|
|
fi
|
|
|
|
return "$diff_ret"
|
|
}
|
|
|
|
ZTST_test() {
|
|
local last match mbegin mend found substlines
|
|
local diff_out diff_err
|
|
local ZTST_skip
|
|
|
|
while true; do
|
|
rm -f $ZTST_in $ZTST_out $ZTST_err
|
|
touch $ZTST_in $ZTST_out $ZTST_err
|
|
ZTST_message=''
|
|
ZTST_failmsg=''
|
|
found=0
|
|
diff_out=d
|
|
diff_err=d
|
|
|
|
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
|
|
if [[ $ZTST_curline == (#b)([-0-9]##)([[:alpha:]]#)(:*)# ]]; then
|
|
ZTST_xstatus=$match[1]
|
|
ZTST_flags=$match[2]
|
|
ZTST_message=${match[3]:+${match[3][2,-1]}}
|
|
else
|
|
ZTST_testfailed "expecting test status at:
|
|
$ZTST_curline"
|
|
return 1
|
|
fi
|
|
ZTST_getline
|
|
found=1
|
|
;;
|
|
('<'*) ZTST_getredir || return 1
|
|
found=1
|
|
;;
|
|
('*>'*)
|
|
ZTST_curline=${ZTST_curline[2,-1]}
|
|
diff_out=p
|
|
;&
|
|
('>'*)
|
|
ZTST_getredir || return 1
|
|
found=1
|
|
;;
|
|
('*?'*)
|
|
ZTST_curline=${ZTST_curline[2,-1]}
|
|
diff_err=p
|
|
;&
|
|
('?'*)
|
|
ZTST_getredir || return 1
|
|
found=1
|
|
;;
|
|
('F:'*) ZTST_failmsg="${ZTST_failmsg:+${ZTST_failmsg}
|
|
} ${ZTST_curline[3,-1]}"
|
|
ZTST_getline
|
|
found=1
|
|
;;
|
|
(*) ZTST_testfailed "bad line in test block:
|
|
$ZTST_curline"
|
|
return 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# If we found some code to execute...
|
|
if [[ -n $ZTST_code ]]; then
|
|
ZTST_hashmark
|
|
ZTST_verbose 1 "Running test: $ZTST_message"
|
|
ZTST_verbose 2 "ZTST_test: expecting status: $ZTST_xstatus"
|
|
ZTST_verbose 2 "Input: $ZTST_in, output: $ZTST_out, error: $ZTST_terr"
|
|
|
|
ZTST_execchunk <$ZTST_in >$ZTST_tout 2>$ZTST_terr
|
|
|
|
if [[ -n $ZTST_skip ]]; then
|
|
ZTST_verbose 0 "Test case skipped: $ZTST_skip"
|
|
ZTST_skip=
|
|
if [[ -n $last ]]; then
|
|
break
|
|
else
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
# 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${$(<$ZTST_terr):+
|
|
Error output:
|
|
$(<$ZTST_terr)}"
|
|
return 1
|
|
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 [[ $ZTST_flags = *q* && -s $ZTST_out ]]; then
|
|
substlines="$(<$ZTST_out)"
|
|
rm -rf $ZTST_out
|
|
print -r -- "${(e)substlines}" >$ZTST_out
|
|
fi
|
|
if [[ $ZTST_flags != *d* ]] && ! ZTST_diff $diff_out -u $ZTST_out $ZTST_tout; then
|
|
ZTST_testfailed "output differs from expected as shown above for:
|
|
$ZTST_code${$(<$ZTST_terr):+
|
|
Error output:
|
|
$(<$ZTST_terr)}"
|
|
return 1
|
|
fi
|
|
if [[ $ZTST_flags = *q* && -s $ZTST_err ]]; then
|
|
substlines="$(<$ZTST_err)"
|
|
rm -rf $ZTST_err
|
|
print -r -- "${(e)substlines}" >$ZTST_err
|
|
fi
|
|
if [[ $ZTST_flags != *D* ]] && ! ZTST_diff $diff_err -u $ZTST_err $ZTST_terr; then
|
|
ZTST_testfailed "error output differs from expected as shown above for:
|
|
$ZTST_code"
|
|
return 1
|
|
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)
|
|
|
|
print "$ZTST_testname: starting."
|
|
|
|
# Now go through all the different sections until the end.
|
|
# prep section may set ZTST_unimplemented, in this case the actual
|
|
# tests will be skipped
|
|
ZTST_skipok=
|
|
ZTST_unimplemented=
|
|
while [[ -z "$ZTST_unimplemented" ]] && ZTST_getsect $ZTST_skipok; do
|
|
case $ZTST_cursect in
|
|
(prep) if (( ${ZTST_sects[prep]} + ${ZTST_sects[test]} + \
|
|
${ZTST_sects[clean]} )); then
|
|
ZTST_testfailed "\`prep' section must come first"
|
|
exit 1
|
|
fi
|
|
ZTST_prepclean
|
|
ZTST_sects[prep]=1
|
|
;;
|
|
(test)
|
|
if (( ${ZTST_sects[test]} + ${ZTST_sects[clean]} )); then
|
|
ZTST_testfailed "bad placement of \`test' section"
|
|
exit 1
|
|
fi
|
|
# careful here: we can't execute ZTST_test before || or &&
|
|
# because that affects the behaviour of traps in the tests.
|
|
ZTST_test
|
|
(( $? )) && ZTST_skipok=1
|
|
ZTST_sects[test]=1
|
|
;;
|
|
(clean)
|
|
if (( ${ZTST_sects[test]} == 0 || ${ZTST_sects[clean]} )); then
|
|
ZTST_testfailed "bad use of \`clean' section"
|
|
else
|
|
ZTST_prepclean 1
|
|
ZTST_sects[clean]=1
|
|
fi
|
|
ZTST_skipok=
|
|
;;
|
|
*) ZTST_testfailed "bad section name: $ZTST_cursect"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -n "$ZTST_unimplemented" ]]; then
|
|
print "$ZTST_testname: skipped ($ZTST_unimplemented)"
|
|
ZTST_testfailed=2
|
|
elif (( ! $ZTST_testfailed )); then
|
|
print "$ZTST_testname: all tests successful."
|
|
fi
|
|
ZTST_cleanup
|
|
exit $(( ZTST_testfailed ))
|