regress.sh   [plain text]


########################################################################
#                                                                      #
#               This software is part of the ast package               #
#                     Copyright (c) 1994-2011 AT&T                     #
#                      and is licensed under the                       #
#                  Common Public License, Version 1.0                  #
#                               by AT&T                                #
#                                                                      #
#                A copy of the License is available at                 #
#            http://www.opensource.org/licenses/cpl1.0.txt             #
#         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         #
#                                                                      #
#              Information and Software Systems Research               #
#                            AT&T Research                             #
#                           Florham Park NJ                            #
#                                                                      #
#                 Glenn Fowler <gsf@research.att.com>                  #
#                                                                      #
########################################################################
: regress - run regression tests in command.tst

command=regress
case $(getopts '[-][123:xyz]' opt --xyz 2>/dev/null; echo 0$opt) in
0123)	USAGE=$'
[-?
@(#)$Id: regress (AT&T Research) 2010-06-12 $
]
'$USAGE_LICENSE$'
[+NAME?regress - run regression tests]
[+DESCRIPTION?\bregress\b runs the tests in \aunit\a, or
    \aunit\a\b.tst\b if \aunit\a does not exist. If \acommand\a is omitted
    then it is assumed to be the base name of \aunit\a. All testing is done
    in the temporary directory \aunit\a\b.tmp\b.]
[+?Default test output lists the \anumber\a and \adescription\a for
    each active \bTEST\b group and the \anumber\a:\aline\a for each
    individual \bEXEC\b test. Each test that fails results in a diagnostic
    that contains the word \bFAILED\b; no other diagnostics contain this
    word.]
[b:ignore-space?Ignore space differences when comparing expected
    output.]
[i:pipe-input?Repeat each test with the standard input redirected through a
    pipe.]
[k:keep?Enable \bcore\b dumps, exit after the first test that fails,
    and do not remove the temporary directory \aunit\a\b.tmp\b.]
[l:local-fs?Force \aunit\a\b.tmp\b to be in a local filesystem.]
[o:pipe-output?Repeat each test with the standard output redirected through
    a pipe.]
[p:pipe-io?Repeat each test with the standard input and standard output
    redirected through pipes.]
[q:quiet?Output information on \bFAILED\b tests only.]
[r!:regular?Run each test with the standard input and standard output
    redirected through regular files.]
[t:test?Run only tests matching \apattern\a. Tests are numbered and
    consist of at least two digits (0 filled if necessary.) Tests matching
    \b+(0)\b are always run.]:[pattern]
[x:trace?Enable debug tracing.]
[v:verbose?List differences between actual (<) and expected (>) output,
    errors and exit codes. Also disable long output line truncation.]

unit [ command [ arg ... ] ]

[+INPUT FILES?The regression test file \aunit\a\b.tst\b is a \bksh\b(1)
    script that is executed in an environment with the following functions
    defined:]
    {
        [+BODY { ... }?Defines the test body; used for complex tests.]
        [+CD \adirectory\a?Create and change to working directory for
            one test.]
        [+CLEANUP \astatus\a?Called at exit time to remove the
            temporary directory \aunit\a\b.tmp\b, list the tests totals via
            \bTALLY\b, and exit with status \astatus\a.]
        [+COMMAND \aarg\a ...?Runs the current command under test with
            \aarg\a ... appended to the default args.]
        [+CONTINUE?The background job must be running.]
        [+COPY \afrom to\a?Copy file \afrom\a to \ato\a. \afrom\a may
            be a regular file or \bINPUT\b, \bOUTPUT\b or \bERROR\b. Post
            test comparisons are still done for \afrom\a.]
        [+DIAGNOSTICS [ \b1\b | \b0\b | \apattern\a ]]?No argument or an
	    argument of \b1\b declares that diagnostics are to expected for
	    the remainder of the current \bTEST\b; \b0\b reverts to the default
            state that diagnostics are not expected; otherwise the argument
	    is a \bksh\b(1) pattern that must match the non-empty contents
	    of the standard error.]
        [+DO \astatement\a?Defines additional statements to be executed
            for the current test. \astatement\a may be a { ... } group.]
        [+EMPTY \bINPUT|OUTPUT|ERROR|SAME?The corresponding file is
            expected to be empty.]
        [+ERROR [ \b-n\b ]] \afile\a | - \adata\a ...?The standard
            error is expected to match either the contents of \afile\a or
            the line \adata\a. \bERROR -n\b does not append a newline to
            \adata\a.]
        [+EXEC [ \aarg\a ... ]]?Runs the command under test with
            optional arguments. \bINPUT\b, \bOUTPUT\b, \bERROR\b, \bEXIT\b
            and \bSAME\b calls following this \bEXEC\b up until the next
            \bEXEC\b or the end of the script provide details for the
            expected results. If no arguments are specified then the
            arguments from the previious \bEXEC\b in the current \bTEST\b
            group are used, or no arguments if this is the first \bEXEC\b
            in the group.]
        [+EXIT \astatus\a?The command exit status is expected to match
            the pattern \astatus\a.]
        [+EXITED?The background job must have exited.]
        [+EXPORT [-]] \aname\a=\avalue\a ...?Export environment
            variables for one test.]
        [+FATAL \amessage\a ...?\amessage\a is printed on the standard
            error and \bregress\b exits with status \b1\b.]
        [+FIFO \bINPUT|OUTPUT|ERROR\b [ \b-n\b ]] \afile\a | - \adata\a ...?
	    The \bIO\B file is a fifo.]
        [+IF \acommand\a [\anote\a]]?If the \bsh\b(1) \acommand\a exits
            0 then tests until the next \bELIF\b, \bELSE\b or \bFI\b are
            enabled. Otherwise those tests are skipped. \bIF\b ... \bFI\b
            may be nested, but must not cross \bTEST\b boundaries. \anote\a
            is listed on the standard error if the correspoding test block
            is enabled; \bIF\b, \bELIF\b, \bELSE\b may nave a \anote\a
            operand.]
        [+IGNORE \afile\a ...?\afile\a is ignored for subsequent result
            comparisons. \afile\a may be \bOUTPUT\b or \bERROR\b.]
        [+IGNORESPACE?Ignore space differences when comparing expected
            output.]
        [+INCLUDE \afile\a ...?One or more \afile\a operands are read
            via the \bksh\b(1) \b.\b(1) command. \bVIEW\b is used to locate
            the files.]
        [+INFO \adescription\a?\adescription\a is printed on the
            standard error.]
        [+INITIALIZE?Called by \bregress\b to initialize a each
            \bTEST\b group.]
        [+INPUT [ \b-n\b ]] \afile\a | - \adata\a ...?The standard
            input is set to either the contents of \afile\a or the line
            \adata\a. \bINPUT -n\b does not append a newline to \adata\a.]
        [+INTRO?Called by \bregress\b to introduce all \bTEST\b
            groups.]
        [+IO [ \bFIFO\b | \bPIPE\b ]] \bINPUT|OUTPUT|ERROR\b [ \b-n\b ]] \afile\a | - \adata\a ...?
            Internal support for the \bINPUT\b, \bOUTPUT\b and \bERROR\b
            functions.]
        [+JOB \aop\a [ ... ]]?Like \bEXEC\b except the command is run
            as a background job for the duration of the group or until it
            is killed via \bKILL\b.]
        [+KEEP \apattern\a ...?The temporary directory is cleared for
            each test. Files matching \apattern\a are retained between
            tests.]
        [+KILL [ \asignal\a ]]?Kill the background job with \asignal\a
        [ \bSIGKILL\b ]].]
        [+MOVE \afrom to\a?Rename file \afrom\a to \ato\a. \afrom\a may
            be a regular file or \bINPUT\b, \bOUTPUT\b or \bERROR\b. Post
            test comparisons are ignored for \afrom\a.]
        [+NOTE \acomment\a?\acomment\a is added to the current test
            trace output.]
        [+OUTPUT [ \b-n\b ]] \afile\a | - \adata\a ...?The standard
            output is expected to match either the contents of \afile\a or
            the line \adata\a. \bOUTPUT -n\b does not append a newline to
            \adata\a.]
        [+PIPE \bINPUT|OUTPUT|ERROR\b [ \b-n\b ]] \afile\a | - \adata\a ...?
	    The \bIO\B file is a pipe.]
        [+PROG \acommand\a [ \aarg\a ... ]]?\acommand\a is run with
            optional arguments.]
        [+REMOVE \afile\a ...?\afile\a ... are removed after the
            current test is done.]
        [+RUN?Called by \bregress\b to run the current test.]
        [+SAME \anew old\a?\anew\a is expected to be the same as
            \aold\a after the current test completes.]
        [+SET [\bno\b]]\aname\a[=\avalue\a]]?Set the command line
            option --\aname\a. The setting is in effect for all tests until
            the next explicit \bSET\b.]
        [+TALLY?Called by \bregress\b display the \bTEST\b results.]
        [+TEST \anumber\a [ \adescription\a ... ]]?Define a new test
            group labelled \anumber\a with optional \adescripion\a.]
        [+TITLE [+]] \atext\a?Set the \bTEST\b output title to
            \atext\a. If \b+\b is specified then \atext\a is appended to
            the default title. The default title is the test file base
            name, and, if different from the test file base name, the test
            unit base name.]
        [+TWD [ \adir\a ... ]]?Set the temporary test dir to \adir\a.
            The default is \aunit\a\b.tmp\b, where \aunit\a is the test
            input file sans directory and suffix. If \adir\a matches \b/*\b
            then it is the directory name; if \adir\a is non-null then the
            prefix \b${TMPDIR:-/tmp}\b is added; otherwise if \adir\a is
            omitted then
            \b${TMPDIR:-/tmp}/tst-\b\aunit\a-$$-$RANDOM.\b\aunit\a is
            used.]
        [+UMASK [ \amask\a ]]?Run subsequent tests with \bumask\b(1)
            \amask\a. If \amask\a is omitted then the original \bumask\b is
            used.]
        [+UNIT \acommand\a [ \aarg\a ... ]]?Define the command and
            optional default arguments to be tested. \bUNIT\b explicitly
            overrides the default command name derived from the test script
            file name. A \acommand\a operand with optional arguments
            overrides the \bUNIT\b \acommand\a and arguments, with the
            exception that if the \bUNIT\b \acommand\a is \b-\b or \b+\b
            the \bUNIT\b arguments are appended to the operand or default
            unit command and arguments.]
        [+VIEW \avar\a [ \afile\a ]]?\avar\a is set to the full
            pathname of \avar\a [ \afile\a ]] in the current \b$VPATH\b
            view if defined.]
    }
[+SEE ALSO?\bnmake\b(1), \bksh\b(1)]
'
	;;
*)	USAGE='ko:[[no]name[=value]]t:[test]v unit [path [arg ...]]'
	;;
esac

function FATAL # message
{
	print -r -u2 "$command: $*"
	GROUP=FINI
	exit 1
}

function EMPTY
{
	typeset i
	typeset -n ARRAY=$1
	for i in ${!ARRAY[@]}
	do	unset ARRAY[$i]
	done
}

function INITIALIZE # void
{
	typeset i j
	cd "$TWD"
	case $KEEP in
	"")	RM *
		;;
	*)	for i in *
		do	case $i in
			!($KEEP))	j="$j $i" ;;
			esac
		done
		case $j in
		?*)	RM $j ;;
		esac
		;;
	esac
	: >INPUT >OUTPUT.ex >ERROR.ex
	BODY=""
	COPY=""
	DIAGNOSTICS=""
	DONE=""
	ERROR=""
	EXIT=0
	IGNORE=""
	INIT=""
	INPUT=""
	MOVE=""
	OUTPUT=""
	EMPTY FILE
	EMPTY SAME
	EMPTY TYPE
}

function INTRO
{
	typeset base command

	if	[[ ! $TEST_quiet ]]
	then	base=${REGRESS##*/}
		base=${base%.tst}
		command=${COMMAND##*/}
		command=${command%' '*}
		set -- $TITLE
		TITLE=
		case $1 in
		''|+)	if	[[ $command == $base ]]
			then	TITLE=$COMMAND
			else	TITLE="$COMMAND, $base"
			fi
			if	(( $# ))
			then	shift
			fi
			;;
		esac
		while	(( $# ))
		do	if	[[ $TITLE ]]
			then	TITLE="$TITLE, $1"
			else	TITLE="$1"
			fi
			shift
		done
		print -u2 "TEST	$TITLE"
	fi
}

function TALLY # extra message text
{
	typeset msg
	case $GROUP in
	INIT)	;;
	*)	msg="TEST	$TITLE, $TESTS test"
		case $TESTS in
		1)	;;
		*)	msg=${msg}s ;;
		esac
		msg="$msg, $ERRORS error"
		case $ERRORS in
		1)	;;
		*)	msg=${msg}s ;;
		esac
		if	(( $# ))
		then	msg="$msg, $*"
		fi
		print -u2 "$msg"
		GROUP=INIT
		TESTS=0
		ERRORS=0
		;;
	esac
}

function TITLE # text
{
	TITLE=$@
}

function UNWIND
{
	while	(( COND > 1 ))
	do	print -r -u2 "$command: line $LINE: no matching FI for IF on line ${COND_LINE[COND]}"
		(( COND-- ))
	done
	if	(( COND > 0 ))
	then	(( COND = 0 ))
		FATAL "line $LINE: no matching FI for IF on line ${COND_LINE[COND+1]}"
	fi
	if	[[ $JOBPID ]]
	then	if	[[ $JOBPID != 0 ]]
		then	kill -KILL $JOBPID 2>/dev/null
			wait
		fi
		JOBPID=
	fi
	JOBSTATUS=
	JOBOP=
	wait
}

function CLEANUP # status
{
	typeset note

	if	[[ $GROUP != INIT ]]
	then	if	[[ ! $TEST_keep ]]
		then	cd $SOURCE
			if	[[ $TEST_local ]]
			then	RM ${TEST_local}
			fi
			RM "$TWD"
		fi
		if	(( $1 )) && [[ $GROUP != FINI ]]
		then	note=terminated
		fi
	fi
	TALLY $note
	[[ $TEST_keep ]] || UNWIND
	exit $1
}

function RUN # [ op ]
{
	typeset i r=1
	[[ $UMASK != $UMASK_ORIG ]] && umask $UMASK_ORIG
#print -u2 AHA#$LINENO $0 GROUP=$GROUP ITEM=$ITEM FLUSHED=$FLUSHED JOBOP=$JOBOP
	case $GROUP in
	INIT)	RM "$TWD"
		if	[[ $TEST_local ]]
		then	TEST_local=${TMPDIR:-/tmp}/rt-$$/${TWD##*/}
			mkdir -p "$TEST_local" && ln -s "$TEST_local" "$TWD" || FATAL "$TWD": cannot create directory
			TEST_local=${TEST_local%/*}
		else	mkdir "$TWD" || FATAL "$TWD": cannot create directory
		fi
		cd "$TWD"
		TWD=$PWD
		: > rmu
		if	rm -u rmu >/dev/null 2>&1
		then	TEST_rmu=-u
		else	rm rmu
		fi
		if	[[ $UNIT ]]
		then	set -- "${ARGV[@]}"
			case $1 in
			""|[-+]*)
				UNIT $UNIT "${ARGV[@]}"
				;;
			*)	UNIT "${ARGV[@]}"
				;;
			esac
		fi
		INTRO
		;;
	FINI)	;;
	$TEST_select)
		if	[[ $ITEM == $FLUSHED ]]
		then	return 0
		fi
		FLUSHED=$ITEM
		if	(( COND_SKIP[COND] ))
		then	return 1
		fi
		((COUNT++))
		if	(( $ITEM <= $LASTITEM ))
		then	LABEL=$TEST#$COUNT
		else	LASTITEM=$ITEM
			LABEL=$TEST:$ITEM
		fi
		TEST_file=""
		exec >/dev/null
		for i in $INPUT
		do	case " $OUTPUT " in
			*" $i "*)
				if	[[ -f $i.sav ]]
				then	cp $i.sav $i
					COMPARE="$COMPARE $i"
				elif	[[ -f $i ]]
				then	cp $i $i.sav
					COMPARE="$COMPARE $i"
				fi
				;;
			esac
		done
		for i in $OUTPUT
		do	case " $COMPARE " in
			*" $i "*)
				;;
			*)	COMPARE="$COMPARE $i"
				;;
			esac
		done
		for i in $INIT
		do	$i $TEST INIT
		done
#print -u2 AHA#$LINENO $0 GROUP=$GROUP ITEM=$ITEM JOBOP=$JOBOP JOBPID=$JOBPID JOBSTATUS=$JOBSTATUS
		if	[[ $JOBPID != 0 && ( $JOBPID || $JOBSTATUS ) ]]
		then	if	[[ ! $TEST_quiet ]]
			then	print -nu2 "$LABEL"
			fi
			RESULTS
		elif	[[ $BODY ]]
		then	SHOW=$NOTE
			if	[[ ! $TEST_quiet ]]
			then	print -r -u2 "	$SHOW"
			fi
			for i in $BODY
			do	$i $TEST BODY
			done
		else	SHOW=
			if	[[ ${TYPE[INPUT]} == PIPE ]]
			then	if	[[ ${TYPE[OUTPUT]} == PIPE ]]
				then	if	[[ ! $TEST_quiet ]]
					then	print -nu2 "$LABEL"
					fi
					cat <$TWD/INPUT | COMMAND "${ARGS[@]}" | cat >$TWD/OUTPUT
					RESULTS 'pipe input'
				else	if	[[ ! $TEST_quiet ]]
					then	print -nu2 "$LABEL"
					fi
					cat <$TWD/INPUT | COMMAND "${ARGS[@]}" >$TWD/OUTPUT
					RESULTS 'pipe io'
				fi
			elif	[[ ${TYPE[OUTPUT]} == PIPE ]]
			then	if	[[ ! $TEST_quiet ]]
				then	print -nu2 "$LABEL"
				fi
				COMMAND "${ARGS[@]}" <$TWD/INPUT | cat >$TWD/OUTPUT
				RESULTS 'pipe output'
			else	if	[[ $TEST_regular ]]
				then	if	[[ ! $TEST_quiet ]]
					then	print -nu2 "$LABEL"
					fi
					if	[[ ${TYPE[INPUT]} == FIFO ]]
					then	COMMAND "${ARGS[@]}" >$TWD/OUTPUT
					else	COMMAND "${ARGS[@]}" <$TWD/INPUT >$TWD/OUTPUT
					fi
					RESULTS
				fi
				if	[[ $TEST_pipe_input ]]
				then	if	[[ ! $TEST_quiet ]]
					then	print -nu2 "$LABEL"
					fi
					(trap '' PIPE; cat <$TWD/INPUT 2>/dev/null; exit 0) | COMMAND "${ARGS[@]}" >$TWD/OUTPUT
					STATUS=$?
					RESULTS 'pipe input'
				fi
				if	[[ $TEST_pipe_output ]]
				then	if	[[ ! $TEST_quiet ]]
					then	print -nu2 "$LABEL"
					fi
					COMMAND "${ARGS[@]}" <$TWD/INPUT | cat >$TWD/OUTPUT
					STATUS=$?
					RESULTS 'pipe output'
				fi
				if	[[ $TEST_pipe_io ]]
				then	if	[[ ! $TEST_quiet ]]
					then	print -nu2 "$LABEL"
					fi
					(trap '' PIPE; cat <$TWD/INPUT 2>/dev/null; exit 0) | COMMAND "${ARGS[@]}" | cat >$TWD/OUTPUT
					STATUS=$?
					RESULTS 'pipe io'
				fi
			fi
			set -- $COPY
			COPY=""
			while	:
			do	case $# in
				0|1)	break ;;
				*)	cp $1 $2 ;;
				esac
				shift 2
			done
			set -- $MOVE
			MOVE=""
			while	(( $# > 1 ))
			do	mv $1 $2
				shift 2
			done
		fi
		for i in $DONE
		do	$i $TEST DONE $STATUS
		done
		COMPARE=""
		r=0
		;;
	esac
	if	[[ $COMMAND_ORIG ]]
	then	COMMAND=$COMMAND_ORIG
		COMMAND_ORIG=
		ARGS=(${ARGS_ORIG[@]})
	fi
	return $r
}

function DO # cmd ...
{
	[[ $GROUP == $TEST_select ]] || return 1
	(( COND_SKIP[COND] )) && return 1
	[[ $UMASK != $UMASK_ORIG ]] && umask $UMASK
	return 0
}

function UNIT # cmd arg ...
{
	typeset cmd=$1
	case $cmd in
	[-+])	shift
		if	(( UNIT_READONLY ))
		then	COMMAND="$COMMAND $*"
		else	#BUG# ARGV=("${ARGV[@]}" "$@")
			set -- "${ARGV[@]}" "$@"
			ARGV=("$@")
		fi
		return
		;;
	esac
	(( UNIT_READONLY )) && return
	if	[[ $UNIT ]] && (( $# <= 1 ))
	then	set -- "${ARGV[@]}"
		case $1 in
		"")	set -- "$cmd" ;;
		[-+]*)	set -- "$cmd" "${ARGV[@]}" ;;
		*)	cmd=$1 ;;
		esac
	fi
	UNIT=
	COMMAND=$cmd
	shift
	typeset cmd=$(whence $COMMAND)
	if	[[ ! $cmd ]]
	then	FATAL $COMMAND: not found
	elif	[[ ! $cmd ]]
	then	FATAL $cmd: not found
	fi
	case $# in
	0)	;;
	*)	COMMAND="$COMMAND $*" ;;
	esac
}

function TWD # [ dir ]
{
	case $1 in
	'')	TWD=${TWD##*/}; TWD=${TMPDIR:-/tmp}/tst-${TWD%.*}-$$-$RANDOM ;;
	/*)	TWD=$1 ;;
	*)	TWD=${TMPDIR:-/tmp}/$1 ;;
	esac
}

function TEST # number description arg ...
{
	RUN
	LINE=$TESTLINE
	UNWIND
	COUNT=0
	LASTITEM=0
	case $1 in
	-)		((LAST++)); TEST=$LAST ;;
	+([0123456789]))	LAST=$1 TEST=$1 ;;
	*)		LAST=0${1/[!0123456789]/} TEST=$1 ;;
	esac
	NOTE=
	if	[[ ! $TEST_quiet && $TEST == $TEST_select ]] && (( ! COND_SKIP[COND] ))
	then	print -r -u2 "$TEST	$2"
	fi
	unset ARGS
	unset EXPORT
	EXPORTS=0
	TEST_file=""
	if	[[ $TEST != ${GROUP}* ]]
	then	GROUP=${TEST%%+([abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ])}
		if	[[ $GROUP == $TEST_select ]] && (( ! COND_SKIP[COND] ))
		then	INITIALIZE
		fi
	fi
	((SUBTESTS=0))
	[[ $TEST == $TEST_select ]] && (( ! COND_SKIP[COND] ))
}

function EXEC # arg ...
{
	if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
	then	return
	fi
	if	((SUBTESTS++))
	then	RUN
	fi
	case $# in
	0)	set -- "${ARGS[@]}" ;;
	esac
	ITEM=$LINE
	NOTE="$(print -r -f '%q ' -- $COMMAND_ORIG "$@")${JOBPID:+&}"
	ARGS=("$@")
}

function JOB # arg ...
{
	JOBPID=0
	EXEC "$@"
}

function CONTINUE
{
	RUN || return
	JOBOP=CONTINUE
	ITEM=$LINE
	NOTE="$(print -r -f '%q ' -- $JOBOP)"
#print -u2 AHA#$LINENO JOBOP=$JOBOP ITEM=$ITEM NOTE=$NOTE
}

function EXITED
{
	RUN || return
	JOBOP=EXITED
	ITEM=$LINE
	NOTE="$(print -r -f '%q ' -- $JOBOP)"
#print -u2 AHA#$LINENO JOBOP=$JOBOP ITEM=$ITEM NOTE=$NOTE
}

function KILL # [ signal ]
{
	RUN || return
	JOBOP=$2
	[[ $JOBOP ]] || JOBOP=KILL
	ITEM=$LINE
	NOTE="$(print -r -f '%q ' -- $JOBOP)"
}

function CD
{
	RUN
	if	[[ $GROUP == $TEST_select ]] && (( ! COND_SKIP[COND] ))
	then	mkdir -p "$@" && cd "$@" || FATAL cannot initialize working directory "$@"
	fi
}

function EXPORT
{
	typeset x n v
	if	[[ $GROUP == INIT ]]
	then	for x
		do	n=${x%%=*}
			v=${x#*=}
			ENVIRON[ENVIRONS++]=$n="'$v'"
		done
	else	RUN
		if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
		then	return
		fi
		for x
		do	n=${x%%=*}
			v=${x#*=}
			EXPORT[EXPORTS++]=$n="'$v'"
		done
	fi
}

function FLUSH
{
	if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
	then	return
	fi
	if	((SUBTESTS++))
	then	RUN
	fi
}

function PROG # cmd arg ...
{
	typeset command args
	if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
	then	return
	fi
	ITEM=$LINE
	NOTE="$(print -r -f '%q ' -- "$@")"
	COMMAND_ORIG=$COMMAND
	COMMAND=$1
	shift
	ARGS_ORIG=(${ARGS[@]})
	ARGS=("$@")
}

function NOTE # description
{
	NOTE=$*
}

function IO # [ PIPE ] INPUT|OUTPUT|ERROR [-f*|-n] file|- data ...
{
	typeset op i v f file type x
	if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
	then	return
	fi
	[[ $UMASK != $UMASK_ORIG ]] && umask $UMASK_ORIG
	while	:
	do	case $1 in
		FIFO|PIPE)	type=$1; shift ;;
		*)		break ;;
		esac
	done
	op=$1
	shift
	[[ $type ]] && TYPE[$op]=$type
	file=$TWD/$op
	while	:
	do	case $1 in
		-x)	x=1; shift ;;
		-f*|-n)	f=$1; shift ;;
		*)	break ;;
		esac
	done
	case $# in
	0)	;;
	*)	case $1 in
		-)	;;
		*)	file=$1
			eval i='$'$op
			case " $i " in
			*" $file "*)
				;;
			*)	eval $op='"$'$op' $file"'
				;;
			esac
			;;
		esac
		shift
		;;
	esac
	case " $IGNORE " in
	*" $file "*)
		for i in $IGNORE
		do	case $i in
			$file)	;;
			*)	v="$v $i" ;;
			esac
		done
		IGNORE=$v
		;;
	esac
	FILE[$op]=$file
	case $op in
	OUTPUT|ERROR)
		file=$file.ex
		if	[[ $file != /* ]]
		then	file=$TWD/$file
		fi
		;;
	esac
	#unset SAME[$op]
	SAME[$op]=
	if	[[ $file == /* ]]
	then	RM $file.sav
	else	RM $TWD/$file.sav
	fi
	if	[[ $file == */* ]]
	then	mkdir -p ${file%/*}
	fi
	if	[[ $file != */ ]]
	then	if	[[ $type == FIFO ]]
		then	rm -f $file
			mkfifo $file
		fi
		if	[[ ${TYPE[$op]} != FIFO ]]
		then	if	[[ $JOBOP ]]
			then	case $#:$f in
				0:)	;;
				*:-f)	printf -- "$@" ;;
				*:-f*)	printf -- "${f#-f}""$@" ;;
				*)	print $f -r -- "$@" ;;
				esac >> $file
			else	case $#:$f in
				0:)	;;
				*:-f)	printf -- "$@" ;;
				*:-f*)	printf -- "${f#-f}""$@" ;;
				*)	print $f -r -- "$@" ;;
				esac > $file
			fi
		elif	[[ $#:$f != 0: ]]
		then	case $#:$f in
			*:-f)	printf -- "$@" ;;
			*:-f*)	printf -- "${f#-f}""$@" ;;
			*)	print $f -r -- "$@" ;;
			esac >> $file &
		fi
		if	[[ $x ]]
		then	chmod +x $file
		fi
	fi
}

function INPUT # file|- data ...
{
	IO $0 "$@"
}

function COPY # from to
{
	if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
	then	return
	fi
	COPY="$COPY $@"
}

function MOVE # from to
{
	typeset f
	if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
	then	return
	fi
	for f
	do	case $f in
		INPUT|OUTPUT|ERROR)
			f=$TWD/$f
			;;
		/*)	;;
		*)	f=$PWD/$f
			;;
		esac
		MOVE="$MOVE $f"
	done
}

function SAME # new old
{
	typeset i file v
	if	[[ $GROUP != $TEST_select ]] || (( COND_SKIP[COND] ))
	then	return
	fi
	case $# in
	2)	case $1 in
		INPUT)	cat $2 > $1; return ;;
		esac
		SAME[$1]=$2
		file=$1
		COMPARE="$COMPARE $1"
		;;
	3)	SAME[$2]=$3
		file=$2
		eval i='$'$1
		case " $i " in
		*" $2 "*)
			;;
		*)	eval $1='"$'$1' $2"'
			;;
		esac
		COMPARE="$COMPARE $2"
		;;
	esac
	case " $IGNORE " in
	*" $file "*)
		for i in $IGNORE
		do	case $i in
			$file)	;;
			*)	v="$v $i" ;;
			esac
		done
		IGNORE=$v
		;;
	esac
}

function OUTPUT # file|- data ...
{
	IO $0 "$@"
}

function ERROR # file|- data ...
{
	IO $0 "$@"
}

function RM # rm(1) args
{
	if	[[ ! $TEST_rmu ]]
	then	chmod -R u+rwx "$@" >/dev/null 2>&1
	fi
	rm $TEST_rmu $TEST_rmflags "$@"
}

function REMOVE # file ...
{
	typeset i
	for i
	do	RM $i $i.sav
	done
}

function IGNORE # file ...
{
	typeset i
	for i
	do	case $i in
		INPUT|OUTPUT|ERROR)
			i=$TWD/$i
			;;
		esac
		case " $IGNORE " in
		*" $i "*)
			;;
		*)	IGNORE="$IGNORE $i"
			;;
		esac
	done
}

function KEEP # pattern ...
{
	typeset i
	for i
	do	case $KEEP in
		"")	KEEP="$i" ;;
		*)	KEEP="$KEEP|$i" ;;
		esac
	done
}

function DIAGNOSTICS # [ 1 | 0 ]
{
	case $#:$1 in
	0:|1:1)	DIAGNOSTICS=1
		EXIT='*'
		;;
	1:|1:0)	DIAGNOSTICS=""
		EXIT=0
		;;
	*)	DIAGNOSTICS=$1
		EXIT='*'
		;;
	esac
}

function IGNORESPACE
{
	: ${IGNORESPACE=-b}
}

function EXIT # status
{
	EXIT=$1
}

function INFO # info description
{
	typeset -R15 info=$1
	if	[[ ! $1 ]]
	then	info=no
	fi
	shift
	if	[[ ! $TEST_quiet ]]
	then	print -r -u2 "$info " "$@"
	fi
}

function COMMAND # arg ...
{
	typeset input
	((TESTS++))
	case " ${ENVIRON[*]} ${EXPORT[*]}" in
	*' 'LC_ALL=*)
		;;
	*' 'LC_+([A-Z])=*)
		EXPORT[EXPORTS++]="LC_ALL="
		;;
	esac
	if	[[ $TEST_keep ]]
	then	(
		PS4=''
		set -x
		print -r -- "${ENVIRON[@]}" "${EXPORT[@]}" "PATH=$PATH" $COMMAND "$@"
		) 2>&1 >/dev/null |
		sed -e 's,^print -r -- ,,' -e 's,$, "$@",' >$TWD/COMMAND
		chmod +x $TWD/COMMAND
	fi
	if	[[ $UMASK != $UMASK_ORIG ]]
	then	: >$TWD/ERROR
		umask $UMASK
	fi
	if	[[ ${TYPE[INPUT]} == FIFO && ${FILE[INPUT]} == */INPUT ]]
	then	input="< ${FILE[INPUT]}"
	fi
	if	[[ $TEST_trace ]]
	then	set +x
		eval print -u2 "$PS4" "${ENVIRON[@]}" "${EXPORT[@]}" PATH='$PATH' '$'COMMAND '"$@"' '$input' '"2>$TWD/ERROR"' '"${JOBPID:+&}"'
	fi
	eval "${ENVIRON[@]}" "${EXPORT[@]}" PATH='$PATH' '$'COMMAND '"$@"' $input "2>$TWD/ERROR" "${JOBPID:+&}"
	STATUS=$?
	[[ $TEST_trace ]] && set -x
	if	[[ $JOBPID ]]
	then	JOBPID=$!
	fi
	[[ $UMASK != $UMASK_ORIG ]] && umask $UMASK_ORIG
	return $STATUS
}

function RESULTS # pipe*
{
	typeset i j k s failed ignore io
	if	[[ $1 ]]
	then	io="$1 "
	fi
	[[ $JOBOP || $JOBPID || $JOBSTATUS ]] && sleep 1
	for i in $COMPARE $TWD/OUTPUT $TWD/ERROR
	do	case " $IGNORE $ignore $MOVE " in
		*" $i "*)	continue ;;
		esac
		ignore="$ignore $i"
		j=${SAME[${i##*/}]}
		if	[[ ! $j ]]
		then	if	[[ $i == /* ]]
			then	k=$i
			else	k=$TWD/$i
			fi
			for s in ex sav err
			do	[[ -f $k.$s ]] && break
			done
			j=$k.$s
		fi
		if	[[ "$DIAGNOSTICS" && $i == */ERROR ]]
		then	if	[[ $STATUS == 0 && ! -s $TWD/ERROR || $DIAGNOSTICS != 1 && $(<$i) != $DIAGNOSTICS ]]
			then	failed=$failed${failed:+,}DIAGNOSTICS
				if	[[ $TEST_verbose && $DIAGNOSTICS != 1 ]]
				then	print -u2 "	===" "diagnostic pattern '$DIAGNOSTICS' did not match" ${i#$TWD/} "==="
					cat $i >&2
				fi
			fi
			continue
		fi
		diff $IGNORESPACE $i $j >$i.diff 2>&1
		if	[[ -s $i.diff ]]
		then	failed=$failed${failed:+,}${i#$TWD/}
			if	[[ $TEST_verbose ]]
			then	print -u2 "	===" diff $IGNORESPACE ${i#$TWD/} "<actual >expected ==="
				cat $i.diff >&2
			fi
		fi
	done
	if	[[ $JOBOP ]]
	then	if	[[ $JOBPID ]] && ! kill -0 $JOBPID 2>/dev/null
		then	wait $JOBPID
			JOBSTATUS=$?
			JOBPID=
		fi
#print -u2 AHA#$LINENO JOBOP=$JOBOP JOBPID=$JOBPID JOBSTATUS=$JOBSTATUS
		case $JOBOP in
		CONTINUE)
			if	[[ ! $JOBPID ]]
			then	failed=$failed${failed:+,}EXITED
			fi
			;;
		EXITED) if	[[ $JOBPID ]]
			then	failed=$failed${failed:+,}RUNNING
			fi
			;;
		*)	if	[[ ! $JOBPID ]]
			then	failed=$failed${failed:+,}EXITED
			fi
			if	! kill -$JOBOP $JOBPID 2>/dev/null
			then	failed=$failed${failed:+,}KILL-$JOBOP
			fi
			;;
		esac
		JOBOP=
	fi
	if	[[ ! $failed && $STATUS != $EXIT ]]
	then	failed="exit code $EXIT expected -- got $STATUS"
	fi
	if	[[ $failed ]]
	then	((ERRORS++))
		if	[[ ! $TEST_quiet ]]
		then	SHOW="FAILED ${io}[ $failed ] $NOTE"
			print -r -u2 "	$SHOW"
		fi
		if	[[ $TEST_keep ]]
		then	GROUP=FINI
			exit
		fi
	elif	[[ ! $TEST_quiet ]]
	then	SHOW=$NOTE
		print -r -u2 "	$SHOW"
	fi
}

function SET # [no]name[=value]
{
	typeset i r
	if	[[ $TEST ]]
	then	RUN
	fi
	for i
	do	if	[[ $i == - ]]
		then	r=1
		elif	[[ $i == + ]]
		then	r=
		else	if	[[ $i == no?* ]]
			then	i=${i#no}
				v=
			elif	[[ $i == *=* ]]
			then	v=${i#*=}
				if	[[ $v == 0 ]]
				then	v=
				fi
				i=${i%%=*}
			else	v=1
			fi
			i=${i//-/_}
			if	[[ $r ]]
			then	READONLY[$i]=1
			elif	[[ ${READONLY[$i]} ]]
			then	continue
			fi
			eval TEST_$i=$v
		fi
	done
}

function VIEW # var [ file ]
{
	nameref var=$1
	typeset i bwd file pwd view root offset
	if	[[ $var ]]
	then	return 0
	fi
	case $# in
	1)	file=$1 ;;
	*)	file=$2 ;;
	esac
	pwd=${TWD%/*}
	bwd=${PMP%/*}
	if	[[ -r $file ]]
	then	if	[[ ! -d $file ]]
		then	var=$PWD/$file
			return 0
		fi
		for i in $file/*
		do	if	[[ -r $i ]]
			then	var=$PWD/$file
				return 0
			fi
			break
		done
	fi
	for view in ${VIEWS[@]}
	do	case $view in
		/*)	;;
		*)	view=$pwd/$view ;;
		esac
		case $offset in
		'')	case $pwd in
			$view/*)	offset=${pwd#$view} ;;
			*)		offset=${bwd#$view} ;;
			esac
			;;
		esac
		if	[[ -r $view$offset/$file ]]
		then	if	[[ ! -d $view$offset/$file ]]
			then	var=$view$offset/$file
				return 0
			fi
			for i in $view$offset/$file/*
			do	if	[[ -f $i ]]
				then	var=$view$offset/$file
					return 0
				fi
				break
			done
		fi
	done
	var=
	return 1
}

function INCLUDE # file ...
{
	typeset f v
	for f
	do	if	VIEW v $f || [[ $PREFIX && $f != /* ]] && VIEW v $PREFIX$f
		then	. $v
		else	FATAL $f: not found
		fi
	done
}

function UMASK # [ mask ]
{
	if	(( $# ))
	then	UMASK=$1
	else	UMASK=$UMASK_ORIG
	fi
}

function PIPE # INPUT|OUTPUT|ERROR file|- data ...
{
	IO $0 "$@"
}

function FIFO # INPUT|OUTPUT|ERROR file|- data ...
{
	IO $0 "$@"
}

function IF # command(s) [note]
{
	[[ $GROUP == $TEST_select ]] || return
	RUN
	(( COND++ ))
	COND_LINE[COND]=$LINE
	if	(( COND > 1 && COND_SKIP[COND-1] ))
	then	(( COND_KEPT[COND] = 1 ))
		(( COND_SKIP[COND] = 1 ))
	elif	eval "{ $1 ;} >/dev/null 2>&1"
	then	(( COND_KEPT[COND] = 1 ))
		(( COND_SKIP[COND] = 0 ))
		[[ $2 && ! $TEST_quiet ]] && print -u2 "NOTE	$2"
	else	(( COND_KEPT[COND] = 0 ))
		(( COND_SKIP[COND] = 1 ))
	fi
}

function ELIF # command(s) [note]
{
	[[ $GROUP == $TEST_select ]] || return
	RUN
	if	(( COND <= 0 ))
	then	FATAL line $LINE: no matching IF for ELIF
	fi
	if	(( COND_KEPT[COND] ))
	then	(( COND_SKIP[COND] = 0 ))
	elif	eval "$* > /dev/null 2>&1"
	then	(( COND_KEPT[COND] = 1 ))
		(( COND_SKIP[COND] = 0 ))
		[[ $2 && ! $TEST_quiet ]] && print -u2 "NOTE	$2"
	else	(( COND_SKIP[COND] = 1 ))
	fi
}

function ELSE # [note]
{
	[[ $GROUP == $TEST_select ]] || return
	RUN
	if	(( COND <= 0 ))
	then	FATAL line $LINE: no matching IF for ELSE
	fi
	if	(( COND_KEPT[COND] ))
	then	(( COND_SKIP[COND] = 1 ))
	else	(( COND_KEPT[COND] = 1 ))
		(( COND_SKIP[COND] = 0 ))
		[[ $1 && ! $TEST_quiet ]] && print -u2 "NOTE	$1"
	fi
}

function FI
{
	[[ $GROUP == $TEST_select ]] || return
	RUN
	if	(( COND <= 0 ))
	then	FATAL line $LINE: no matching IF for FI on line $LINE
	fi
	(( ! COND_KEPT[COND] )) && [[ $1 && ! $TEST_quiet ]] && print -u2 "NOTE	$1"
	(( COND-- ))
}

# main

integer ERRORS=0 ENVIRONS=0 EXPORTS=0 TESTS=0 SUBTESTS=0 LINE=0 TESTLINE=0
integer ITEM=0 LASTITEM=0 COND=0 UNIT_READONLY=0 COUNT
typeset ARGS COMMAND COPY DIAGNOSTICS ERROR EXEC FLUSHED=0 GROUP=INIT
typeset IGNORE INPUT KEEP OUTPUT TEST SOURCE MOVE NOTE UMASK UMASK_ORIG
typeset ARGS_ORIG COMMAND_ORIG TITLE UNIT ARGV PREFIX OFFSET IGNORESPACE
typeset COMPARE MAIN JOBPID='' JOBSTATUS=''
typeset TEST_file TEST_keep TEST_pipe_input TEST_pipe_io TEST_pipe_output TEST_local
typeset TEST_quiet TEST_regular=1 TEST_rmflags='-rf --' TEST_rmu TEST_select

typeset -A SAME VIEWS FILE TYPE READONLY
typeset -a COND_LINE COND_SKIP COND_KEPT ENVIRON EXPORT
typeset -Z LAST=00

unset FIGNORE

while	getopts -a $command "$USAGE" OPT
do	case $OPT in
	b)	(( $OPTARG )) && IGNORESPACE=-b
		;;
	i)	SET - pipe-input=$OPTARG
		;;
	k)	SET - keep=$OPTARG
		;;
	l)	SET - local
		;;
	o)	SET - pipe-output=$OPTARG
		;;
	p)	SET - pipe-io=$OPTARG
		;;
	q)	SET - quiet=$OPTARG
		;;
	r)	SET - regular=$OPTARG
		;;
	t)	if	[[ $TEST_select ]]
		then	TEST_select="$TEST_select|${OPTARG//,/\|}"
		else	TEST_select="${OPTARG//,/\|}"
		fi
		;;
	x)	SET - trace=$OPTARG
		;;
	v)	SET - verbose=$OPTARG
		;;
	*)	GROUP=FINI
		exit 2
		;;
	esac
done
shift $OPTIND-1
case $# in
0)	FATAL test unit name omitted ;;
esac
export COLUMNS=80
SOURCE=$PWD
PATH=$SOURCE:${PATH#?(.):}
PATH=${PATH%%:?(.)}:/usr/5bin:/bin:/usr/bin
UNIT=$1
shift
if	[[ -f $UNIT && ! -x $UNIT ]]
then	REGRESS=$UNIT
else	REGRESS=${UNIT%.tst}
	REGRESS=$REGRESS.tst
	[[ -f $REGRESS ]] || FATAL $REGRESS: regression tests not found
fi
UNIT=${UNIT##*/}
UNIT=${UNIT%.tst}
MAIN=$UNIT
if	[[ $VPATH ]]
then	set -A VIEWS ${VPATH//:/' '}
	OFFSET=${SOURCE#${VIEWS[0]}}
	if	[[ $OFFSET ]]
	then	OFFSET=${OFFSET#/}/
	fi
fi
if	[[ $REGRESS == */* ]]
then	PREFIX=${REGRESS%/*}
	if	[[ ${#VIEWS[@]} ]]
	then	for i in ${VIEWS[@]}
		do	PREFIX=${PREFIX#$i/}
		done
	fi
	PREFIX=${PREFIX#$OFFSET}
	if	[[ $PREFIX ]]
	then	PREFIX=$PREFIX/
	fi
fi
TWD=$PWD/$UNIT.tmp
PMP=$(pwd -P)/$UNIT.tmp
UMASK_ORIG=$(umask)
UMASK=$UMASK_ORIG
ARGV=("$@")
if	[[ ${ARGV[0]} && ${ARGV[0]} != [-+]* ]]
then	UNIT "${ARGV[@]}"
	UNIT_READONLY=1
fi
trap 'code=$?; CLEANUP $code' EXIT
if	[[ ! $TEST_select ]]
then	TEST_select="[0123456789]*"
fi
TEST_select="@($TEST_select|+(0))"
if	[[ $TEST_trace ]]
then	export PS4=':$LINENO: '
	typeset -ft $(typeset +f)
	set -x
fi
if	[[ $TEST_verbose ]]
then	typeset SHOW
else	typeset -L70 SHOW
fi
if	[[ $TEST_keep ]] && (ulimit -c 0) >/dev/null 2>&1
then	ulimit -c 0
fi
set --pipefail

# some last minute shenanigans

alias BODY='BODY=BODY; function BODY'
alias CONTINUE='LINE=$LINENO; CONTINUE'
alias DO='(( $ITEM != $FLUSHED )) && RUN DO; DO &&'
alias DONE='DONE=DONE; function DONE'
alias EXEC='LINE=$LINENO; EXEC'
alias EXITED='LINE=$LINENO; EXITED'
alias INIT='INIT=INIT; function INIT'
alias JOB='LINE=$LINENO; JOB'
alias KILL='LINE=$LINENO; KILL'
alias PROG='LINE=$LINENO; FLUSH; PROG'
alias TEST='TESTLINE=$LINENO; TEST'
alias IF='LINE=$LINENO; FLUSH; IF'
alias ELIF='LINE=$LINENO; FLUSH; ELIF'
alias ELSE='LINE=$LINENO; FLUSH; ELSE'
alias FI='LINE=$LINENO; FLUSH; FI'

# do the tests

. $REGRESS
RUN
GROUP=FINI