1#!/bin/zsh -f
2# The line above is just for convenience.  Normally tests will be run using
3# a specified version of zsh.  With dynamic loading, any required libraries
4# must already have been installed in that case.
5#
6# Takes one argument: the name of the test file.  Currently only one such
7# file will be processed each time ztst.zsh is run.  This is slower, but
8# much safer in terms of preserving the correct status.
9# To avoid namespace pollution, all functions and parameters used
10# only by the script begin with ZTST_.
11#
12# Options (without arguments) may precede the test file argument; these
13# are interpreted as shell options to set.  -x is probably the most useful.
14
15# Produce verbose messages if non-zero.
16# If 1, produce reports of tests executed; if 2, also report on progress.
17# Defined in such a way that any value from the environment is used.
18: ${ZTST_verbose:=0}
19
20# We require all options to be reset, not just emulation options.
21# Unfortunately, due to the crud which may be in /etc/zshenv this might
22# still not be good enough.  Maybe we should trick it somehow.
23emulate -R zsh
24
25# Ensure the locale does not screw up sorting.  Don't supply a locale
26# unless there's one set, to minimise problems.
27[[ -n $LC_ALL ]] && LC_ALL=C
28[[ -n $LC_COLLATE ]] && LC_COLLATE=C
29[[ -n $LC_NUMERIC ]] && LC_NUMERIC=C
30[[ -n $LC_MESSAGES ]] && LC_MESSAGES=C
31[[ -n $LANG ]] && LANG=C
32
33# Don't propagate variables that are set by default in the shell.
34typeset +x WORDCHARS
35
36# Set the module load path to correspond to this build of zsh.
37# This Modules directory should have been created by "make check".
38[[ -d Modules/zsh ]] && module_path=( $PWD/Modules )
39# Allow this to be passed down.
40export MODULE_PATH
41
42# We need to be able to save and restore the options used in the test.
43# We use the $options variable of the parameter module for this.
44zmodload -i zsh/parameter
45
46# Note that both the following are regular arrays, since we only use them
47# in whole array assignments to/from $options.
48# Options set in test code (i.e. by default all standard options)
49ZTST_testopts=(${(kv)options})
50
51setopt extendedglob nonomatch
52while [[ $1 = [-+]* ]]; do
53  set $1
54  shift
55done
56# Options set in main script
57ZTST_mainopts=(${(kv)options})
58
59# We run in the current directory, so remember it.
60ZTST_testdir=$PWD
61ZTST_testname=$1
62
63integer ZTST_testfailed
64
65# This is POSIX nonsense.  Because of the vague feeling someone, somewhere
66# may one day need to examine the arguments of "tail" using a standard
67# option parser, every Unix user in the world is expected to switch
68# to using "tail -n NUM" instead of "tail -NUM".  Older versions of
69# tail don't support this.
70tail() {
71  emulate -L zsh
72
73  if [[ -z $TAIL_SUPPORTS_MINUS_N ]]; then
74    local test
75    test=$(echo "foo\nbar" | command tail -n 1 2>/dev/null)
76    if [[ $test = bar ]]; then
77      TAIL_SUPPORTS_MINUS_N=1
78    else
79      TAIL_SUPPORTS_MINUS_N=0
80    fi
81  fi
82
83  integer argi=${argv[(i)-<->]}
84
85  if [[ $argi -le $# && $TAIL_SUPPORTS_MINUS_N = 1 ]]; then
86    argv[$argi]=(-n ${argv[$argi][2,-1]})
87  fi
88
89  command tail "$argv[@]"
90}
91
92# The source directory is not necessarily the current directory,
93# but if $0 doesn't contain a `/' assume it is.
94if [[ $0 = */* ]]; then
95  ZTST_srcdir=${0%/*}
96else
97  ZTST_srcdir=$PWD
98fi
99[[ $ZTST_srcdir = /* ]] || ZTST_srcdir="$ZTST_testdir/$ZTST_srcdir"
100
101# Set the function autoload paths to correspond to this build of zsh.
102fpath=( $ZTST_srcdir/../Functions/*~*/CVS(/)
103        $ZTST_srcdir/../Completion
104        $ZTST_srcdir/../Completion/*/*~*/CVS(/) )
105
106: ${TMPPREFIX:=/tmp/zsh}
107# Temporary files for redirection inside tests.
108ZTST_in=${TMPPREFIX}.ztst.in.$$
109# hold the expected output
110ZTST_out=${TMPPREFIX}.ztst.out.$$
111ZTST_err=${TMPPREFIX}.ztst.err.$$
112# hold the actual output from the test
113ZTST_tout=${TMPPREFIX}.ztst.tout.$$
114ZTST_terr=${TMPPREFIX}.ztst.terr.$$
115
116ZTST_cleanup() {
117  cd $ZTST_testdir
118  rm -rf $ZTST_testdir/dummy.tmp $ZTST_testdir/*.tmp(N) \
119    ${TMPPREFIX}.ztst*$$(N)
120}
121
122# This cleanup always gets performed, even if we abort.  Later,
123# we should try and arrange that any test-specific cleanup
124# always gets called as well.
125##trap 'print cleaning up...
126##ZTST_cleanup' INT QUIT TERM
127# Make sure it's clean now.
128rm -rf dummy.tmp *.tmp
129
130# Report failure.  Note that all output regarding the tests goes to stdout.
131# That saves an unpleasant mixture of stdout and stderr to sort out.
132ZTST_testfailed() {
133  print -r "Test $ZTST_testname failed: $1"
134  if [[ -n $ZTST_message ]]; then
135    print -r "Was testing: $ZTST_message"
136  fi
137  print -r "$ZTST_testname: test failed."
138  if [[ -n $ZTST_failmsg ]]; then
139    print -r "The following may (or may not) help identifying the cause:
140$ZTST_failmsg"
141  fi
142  ZTST_testfailed=1
143  return 1
144}
145
146# Print messages if $ZTST_verbose is non-empty
147ZTST_verbose() {
148  local lev=$1
149  shift
150  if [[ -n $ZTST_verbose && $ZTST_verbose -ge $lev ]]; then
151    print -r -u $ZTST_fd -- $*
152  fi
153}
154ZTST_hashmark() {
155  if [[ ZTST_verbose -le 0 && -t $ZTST_fd ]]; then
156    print -n -u$ZTST_fd -- ${(pl:SECONDS::\#::\#\r:)}
157  fi
158  (( SECONDS > COLUMNS+1 && (SECONDS -= COLUMNS) ))
159}
160
161if [[ ! -r $ZTST_testname ]]; then
162  ZTST_testfailed "can't read test file."
163  exit 1
164fi
165
166exec {ZTST_fd}>&1
167exec {ZTST_input}<$ZTST_testname
168
169# The current line read from the test file.
170ZTST_curline=''
171# The current section being run
172ZTST_cursect=''
173
174# Get a new input line.  Don't mangle spaces; set IFS locally to empty.
175# We shall skip comments at this level.
176ZTST_getline() {
177  local IFS=
178  while true; do
179    read -u $ZTST_input -r ZTST_curline || return 1
180    [[ $ZTST_curline == \#* ]] || return 0
181  done
182}
183
184# Get the name of the section.  It may already have been read into
185# $curline, or we may have to skip some initial comments to find it.
186# If argument present, it's OK to skip the reset of the current section,
187# so no error if we find garbage.
188ZTST_getsect() {
189  local match mbegin mend
190
191  while [[ $ZTST_curline != '%'(#b)([[:alnum:]]##)* ]]; do
192    ZTST_getline || return 1
193    [[ $ZTST_curline = [[:blank:]]# ]] && continue
194    if [[ $# -eq 0 && $ZTST_curline != '%'[[:alnum:]]##* ]]; then
195      ZTST_testfailed "bad line found before or after section:
196$ZTST_curline"
197      exit 1
198    fi
199  done
200  # have the next line ready waiting
201  ZTST_getline
202  ZTST_cursect=${match[1]}
203  ZTST_verbose 2 "ZTST_getsect: read section name: $ZTST_cursect"
204  return 0
205}
206
207# Read in an indented code chunk for execution
208ZTST_getchunk() {
209  # Code chunks are always separated by blank lines or the
210  # end of a section, so if we already have a piece of code,
211  # we keep it.  Currently that shouldn't actually happen.
212  ZTST_code=''
213  # First find the chunk.
214  while [[ $ZTST_curline = [[:blank:]]# ]]; do
215    ZTST_getline || break
216  done
217  while [[ $ZTST_curline = [[:blank:]]##[^[:blank:]]* ]]; do
218    ZTST_code="${ZTST_code:+${ZTST_code}
219}${ZTST_curline}"
220    ZTST_getline || break
221  done
222  ZTST_verbose 2 "ZTST_getchunk: read code chunk:
223$ZTST_code"
224  [[ -n $ZTST_code ]]
225}
226
227# Read in a piece for redirection.
228ZTST_getredir() {
229  local char=${ZTST_curline[1]} fn
230  ZTST_redir=${ZTST_curline[2,-1]}
231  while ZTST_getline; do
232    [[ $ZTST_curline[1] = $char ]] || break
233    ZTST_redir="${ZTST_redir}
234${ZTST_curline[2,-1]}"
235  done
236  ZTST_verbose 2 "ZTST_getredir: read redir for '$char':
237$ZTST_redir"
238
239  case $char in
240    ('<') fn=$ZTST_in
241    ;;
242    ('>') fn=$ZTST_out
243    ;;
244    ('?') fn=$ZTST_err
245    ;;
246    (*)  ZTST_testfailed "bad redir operator: $char"
247    return 1
248    ;;
249  esac
250  if [[ $ZTST_flags = *q* && $char = '<' ]]; then
251    # delay substituting output until variables are set
252    print -r -- "${(e)ZTST_redir}" >>$fn
253  else
254    print -r -- "$ZTST_redir" >>$fn
255  fi
256
257  return 0
258}
259
260# Execute an indented chunk.  Redirections will already have
261# been set up, but we need to handle the options.
262ZTST_execchunk() {
263  options=($ZTST_testopts)
264  eval "$ZTST_code"
265  ZTST_status=$?
266  # careful... ksh_arrays may be in effect.
267  ZTST_testopts=(${(kv)options[*]})
268  options=(${ZTST_mainopts[*]})
269  ZTST_verbose 2 "ZTST_execchunk: status $ZTST_status"
270  return $ZTST_status
271}
272
273# Functions for preparation and cleaning.
274# When cleaning up (non-zero string argument), we ignore status.
275ZTST_prepclean() {
276  # Execute indented code chunks.
277  while ZTST_getchunk; do
278    ZTST_execchunk >/dev/null || [[ -n $1 ]] || {
279      [[ -n "$ZTST_unimplemented" ]] ||
280      ZTST_testfailed "non-zero status from preparation code:
281$ZTST_code" && return 0
282    }
283  done
284}
285
286# diff wrapper
287ZTST_diff() {
288  emulate -L zsh
289  setopt extendedglob
290
291  local diff_out
292  integer diff_pat diff_ret
293
294  case $1 in
295    (p)
296    diff_pat=1
297    ;;
298
299    (d)
300    ;;
301
302    (*)
303    print "Bad ZTST_diff code: d for diff, p for pattern match"
304    ;;
305  esac
306  shift
307      
308  if (( diff_pat )); then
309    local -a diff_lines1 diff_lines2
310    integer failed i
311
312    diff_lines1=("${(f)$(<$argv[-2])}")
313    diff_lines2=("${(f)$(<$argv[-1])}")
314    if (( ${#diff_lines1} != ${#diff_lines2} )); then
315      failed=1
316    else
317      for (( i = 1; i <= ${#diff_lines1}; i++ )); do
318	if [[ ${diff_lines2[i]} != ${~diff_lines1[i]} ]]; then
319	  failed=1
320	  break
321	fi
322      done
323    fi
324    if (( failed )); then
325      print -rl "Pattern match failed:" \<${^diff_lines1} \>${^diff_lines2}
326      diff_ret=1
327    fi
328  else
329    diff_out=$(diff "$@")
330    diff_ret="$?"
331    if [[ "$diff_ret" != "0" ]]; then
332      print -r "$diff_out"
333    fi
334  fi
335
336  return "$diff_ret"
337}
338    
339ZTST_test() {
340  local last match mbegin mend found substlines
341  local diff_out diff_err
342
343  while true; do
344    rm -f $ZTST_in $ZTST_out $ZTST_err
345    touch $ZTST_in $ZTST_out $ZTST_err
346    ZTST_message=''
347    ZTST_failmsg=''
348    found=0
349    diff_out=d
350    diff_err=d
351
352    ZTST_verbose 2 "ZTST_test: looking for new test"
353
354    while true; do
355      ZTST_verbose 2 "ZTST_test: examining line:
356$ZTST_curline"
357      case $ZTST_curline in
358	(%*) if [[ $found = 0 ]]; then
359	      break 2
360	    else
361	      last=1
362	      break
363	    fi
364	    ;;
365	([[:space:]]#)
366	    if [[ $found = 0 ]]; then
367	      ZTST_getline || break 2
368	      continue
369	    else
370	      break
371	    fi
372	    ;;
373	([[:space:]]##[^[:space:]]*) ZTST_getchunk
374	  if [[ $ZTST_curline == (#b)([-0-9]##)([[:alpha:]]#)(:*)# ]]; then
375	    ZTST_xstatus=$match[1]
376	    ZTST_flags=$match[2]
377	    ZTST_message=${match[3]:+${match[3][2,-1]}}
378	  else
379	    ZTST_testfailed "expecting test status at:
380$ZTST_curline"
381	    return 1
382	  fi
383	  ZTST_getline
384	  found=1
385	  ;;
386	('<'*) ZTST_getredir || return 1
387	  found=1
388	  ;;
389	('*>'*)
390	  ZTST_curline=${ZTST_curline[2,-1]}
391	  diff_out=p
392	  ;&
393	('>'*)
394	  ZTST_getredir || return 1
395	  found=1
396	  ;;
397	('*?'*)
398	  ZTST_curline=${ZTST_curline[2,-1]}
399	  diff_err=p
400	  ;&
401	('?'*)
402	  ZTST_getredir || return 1
403	  found=1
404	  ;;
405	('F:'*) ZTST_failmsg="${ZTST_failmsg:+${ZTST_failmsg}
406}  ${ZTST_curline[3,-1]}"
407	  ZTST_getline
408	  found=1
409          ;;
410	(*) ZTST_testfailed "bad line in test block:
411$ZTST_curline"
412	  return 1
413          ;;
414      esac
415    done
416
417    # If we found some code to execute...
418    if [[ -n $ZTST_code ]]; then
419      ZTST_hashmark
420      ZTST_verbose 1 "Running test: $ZTST_message"
421      ZTST_verbose 2 "ZTST_test: expecting status: $ZTST_xstatus"
422      ZTST_verbose 2 "Input: $ZTST_in, output: $ZTST_out, error: $ZTST_terr"
423
424      ZTST_execchunk <$ZTST_in >$ZTST_tout 2>$ZTST_terr
425
426      # First check we got the right status, if specified.
427      if [[ $ZTST_xstatus != - && $ZTST_xstatus != $ZTST_status ]]; then
428	ZTST_testfailed "bad status $ZTST_status, expected $ZTST_xstatus from:
429$ZTST_code${$(<$ZTST_terr):+
430Error output:
431$(<$ZTST_terr)}"
432	return 1
433      fi
434
435      ZTST_verbose 2 "ZTST_test: test produced standard output:
436$(<$ZTST_tout)
437ZTST_test: and standard error:
438$(<$ZTST_terr)"
439
440      # Now check output and error.
441      if [[ $ZTST_flags = *q* && -s $ZTST_out ]]; then
442	substlines="$(<$ZTST_out)"
443	rm -rf $ZTST_out
444	print -r -- "${(e)substlines}" >$ZTST_out
445      fi
446      if [[ $ZTST_flags != *d* ]] && ! ZTST_diff $diff_out -c $ZTST_out $ZTST_tout; then
447	ZTST_testfailed "output differs from expected as shown above for:
448$ZTST_code${$(<$ZTST_terr):+
449Error output:
450$(<$ZTST_terr)}"
451	return 1
452      fi
453      if [[ $ZTST_flags = *q* && -s $ZTST_err ]]; then
454	substlines="$(<$ZTST_err)"
455	rm -rf $ZTST_err
456	print -r -- "${(e)substlines}" >$ZTST_err
457      fi
458      if [[ $ZTST_flags != *D* ]] && ! ZTST_diff $diff_err -c $ZTST_err $ZTST_terr; then
459	ZTST_testfailed "error output differs from expected as shown above for:
460$ZTST_code"
461	return 1
462      fi
463    fi
464    ZTST_verbose 1 "Test successful."
465    [[ -n $last ]] && break
466  done
467
468  ZTST_verbose 2 "ZTST_test: all tests successful"
469
470  # reset message to keep ZTST_testfailed output correct
471  ZTST_message=''
472}
473
474
475# Remember which sections we've done.
476typeset -A ZTST_sects
477ZTST_sects=(prep 0 test 0 clean 0)
478
479print "$ZTST_testname: starting."
480
481# Now go through all the different sections until the end.
482# prep section may set ZTST_unimplemented, in this case the actual
483# tests will be skipped
484ZTST_skipok=
485ZTST_unimplemented=
486while [[ -z "$ZTST_unimplemented" ]] && ZTST_getsect $ZTST_skipok; do
487  case $ZTST_cursect in
488    (prep) if (( ${ZTST_sects[prep]} + ${ZTST_sects[test]} + \
489	        ${ZTST_sects[clean]} )); then
490	    ZTST_testfailed "\`prep' section must come first"
491            exit 1
492	  fi
493	  ZTST_prepclean
494	  ZTST_sects[prep]=1
495	  ;;
496    (test)
497	  if (( ${ZTST_sects[test]} + ${ZTST_sects[clean]} )); then
498	    ZTST_testfailed "bad placement of \`test' section"
499	    exit 1
500	  fi
501	  # careful here: we can't execute ZTST_test before || or &&
502	  # because that affects the behaviour of traps in the tests.
503	  ZTST_test
504	  (( $? )) && ZTST_skipok=1
505	  ZTST_sects[test]=1
506	  ;;
507    (clean)
508	   if (( ${ZTST_sects[test]} == 0 || ${ZTST_sects[clean]} )); then
509	     ZTST_testfailed "bad use of \`clean' section"
510	   else
511	     ZTST_prepclean 1
512	     ZTST_sects[clean]=1
513	   fi
514	   ZTST_skipok=
515	   ;;
516    *) ZTST_testfailed "bad section name: $ZTST_cursect"
517       ;;
518  esac
519done
520
521if [[ -n "$ZTST_unimplemented" ]]; then
522  print "$ZTST_testname: skipped ($ZTST_unimplemented)"
523  ZTST_testfailed=2
524elif (( ! $ZTST_testfailed )); then
525  print "$ZTST_testname: all tests successful."
526fi
527ZTST_cleanup
528exit $(( ZTST_testfailed ))
529