: ksh regression test harness :

command=shtests

setslocale='*@(locale).sh'
timesensitive='*@(options|sigchld|subshell).sh'
valgrindflags='--xml=yes --log-file=/dev/null --track-origins=yes --read-var-info=yes'

USAGE=$'
[-s8?
@(#)$Id: shtests (ksh 93u+m) 2024-07-27 $
]
[-author?David Korn <dgk@research.att.com>]
[-author?Glenn Fowler <gsf@research.att.com>]
[-author?Contributors to https://github.com/ksh93/ksh]
[-copyright?(c) 2000-2012 AT&T Intellectual Property]
[-copyright?(c) 2020-2023 Contributors to ksh 93u+m]
[-license?https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html]
[+NAME?shtests - ksh regression test harness]
[+DESCRIPTION?\bshtests\b is the \bksh\b(1) regression test harness for
    \b$SHELL\b or \bksh\b if \bSHELL\b is not defined and exported. If
    none of the \b--posix\b \b--utf8\b \b--compile\b options are specified then
    all three are enabled for most tests, whereas only \b--posix\b \b--utf8\b
    are enabled for the \bpty\b tests for the interactive shell.]
[+INPUT FILES?\bshtests\b regression test files are shell scripts that
    run in an environment controlled by \bshtests\b. An identification
    message is printed before and after each test on the standard output.
    The default environment settings are:]
    {
        [+unset LANG]
        [+unset LC_ALL]
        [+LC_NUMERIC=C?\b.\b radix point assumed by all test scripts.]
    }
[c:compile?Run test scripts using \bshcomp\b(1).]
[d:debug?Enable \bshtests\b execution trace.]
[k:keep?Keep temporary files after test run; shtests will report the location.]
[l:locale?Disable \b--utf8\b and run the \b--posix\b and \b--compile\b
    tests, if enabled, in the locale of the caller. However, for locales
    where \b.\b is not the radix point, \bLC_NUMERIC\b is set to \bC\b
    to avoid invalid regressions.]
[p:posix?Run the test scripts in the posix/C locale.]
[t!:time?Include the current date/time in the test identification
    messages.]
[u:utf8?Run the test scripts in the ast-specific C.UTF-8 locale.]
[V:valgrind?Run the test scripts with
    \bvalgrind\b(1) on \bksh\b. If \b$SHELL-g\b exists and is executable,
    then it is used instead of \b$SHELL\b.]
[x:trace?Enable script execution trace.]

[ test.sh ... ] [ name=value ... ]

[+SEE ALSO?\bksh\b(1), \bregress\b(1), \brt\b(1)]
'

function usage
{
	OPTIND=0
	getopts -a $command "$USAGE" OPT '--??long'
	exit 2
}

function valxml
{
	typeset state=INIT data dir file fn line what
	integer errors=0

	#print === $1 ===; cat $1; print === ===
	while	read data
	do	case $state in
		INIT)	case $data in
			'<error>')
				state=ERROR
				;;
			esac
			;;
		ERROR)	case $data in
			'<kind>'Leak*'</kind>')
				state=SKIP
				;;
			'<kind>'*'</kind>')
				state=KEEP
				what=UNKNOWN
				;;
			esac
			;;
		FRAME)	case $data in
			'<dir>'*'</dir>')
				dir=${data#'<dir>'}
				dir=${dir%'</dir>'}
				;;
			'<file>'*'</file>')
				file=${data#'<file>'}
				file=${file%'</file>'}
				;;
			'<fn>'*'</fn>')
				fn=${data#'<fn>'}
				fn=${fn%'</fn>'}
				;;
			'<line>'*'</line>')
				line=${data#'<line>'}
				line=${line%'</line>'}
				;;
			'</frame>')
				[[ $dir ]] && dir+=/
				dir+=$file
				[[ $dir ]] && dir+=:
				[[ $line ]] && dir+=$line:
				[[ $fn ]] && dir+=$fn
				[[ $dir ]] && echo $'\t    '$dir
				state=KEEP
				;;
			esac
			;;
		KEEP)	case $data in
			'<auxwhat>'*'</auxwhat>')
				what=${data#'<auxwhat>'}
				what=${what%'</auxwhat>'}
				echo $'\t'"$what"
				;;
			'<frame>')
				state=FRAME
				dir=
				file=
				fn=
				line=
				;;
			'<what>Syscall param mount(type) points to unaddressable byte(s)</what>')
				state=SKIP
				;;
			'<what>'*'</what>')
				(( errors++ ))
				what=${data#'<what>'}
				what=${what%'</what>'}
				echo $'\n\t'"$what"
				;;
			'<xwhat>')
				state=WHAT
				;;
			'</error>')
				state=INIT
				;;
			esac
			;;
		SKIP)	case $data in
			'</error>')
				state=INIT
				;;
			esac
			;;
		WHAT)	case $data in
			'<text>'*'</text>')
				(( errors++ ))
				what=${data#'<text>'}
				what=${what%'</text>'}
				echo $'\n\t'"$what"
				;;
			'</xwhat>')
				state=KEEP
				;;
			esac
			;;
		esac
	done < "$1"
	(( errors )) && echo
	return $errors
}

command set +o posix 2>/dev/null
unset DISPLAY FIGNORE HISTFILE POSIXLY_CORRECT _AST_FEATURES
export ENV=/./dev/null SHTESTS_COMMON=$PWD/_common
trap + $(kill -l)	# unadvertised -- set all signals to SIG_DFL

integer compile=-1 posix=-1 utf8=-1
integer debug=0 keep=0 locale=0 time=1

while	getopts -a $command "$USAGE" OPT
do	case $OPT in
	c)	if	(( $OPTARG ))
		then	compile=2
		else	compile=0
		fi
		;;
	d)	debug=$OPTARG
		;;
	k)	keep=$OPTARG
		;;
	l)	locale=$OPTARG
		;;
	p)	posix=$OPTARG
		;;
	t)	time=$OPTARG
		;;
	u)	utf8=$OPTARG
		;;
	V)	valgrind="${VALGRIND:-valgrind} ${VALGRINDFLAGS:-$valgrindflags}"
		;;
	x)	trace=-x
		;;
	*)	usage
		;;
	esac
done
shift $OPTIND-1

if	(( debug )) || [[ $trace ]]
then	export PS4='+ [${SECONDS:+${SECONDS%????}s|}${.sh.pid:+P${.sh.pid},}${.sh.subshell:+S${.sh.subshell},}${.sh.file:+${.sh.file#${.sh.file%/*/*}/},}${.sh.fun:+${.sh.fun},}${LINENO:+L$LINENO,}e$?] '
	typeset -F SECONDS  # show fractional seconds in xtrace output
	if	(( debug ))
	then	set -x
	fi
fi

while	[[ $1 == *=* ]]
do	eval export "$1"
	shift
done

if	(( compile <= 0 && posix <= 0 && utf8 <= 0 ))
then	(( compile )) && compile=1
	(( posix )) && posix=1
	(( utf8 )) && utf8=1
fi
(( compile < 0 )) && compile=0
(( posix < 0 )) && posix=0
(( utf8 < 0 )) && utf8=0
if	(( locale ))
then	utf8=0
	if	[[ $LC_ALL ]]
	then	export LANG=$LC_ALL
		unset ${!LC_*}
	fi
	if	! let 1.0 2>/dev/null
	then	export LC_NUMERIC=C
	fi
else	unset LANG ${!LC_*}
fi
timesensitive=.
export PATH PWD SHCOMP SHELL
PWD=$(pwd)
SHELL=${SHELL-ksh}
case $0 in
/*)	d=$(dirname $0);;
*/*)	d=$PWD/$(dirname $0);;
*)	d=$PWD;;
esac
case $SHELL in
/*)	;;
*/*)	SHELL=$d/$SHELL;;
*)	SHELL=$(whence $SHELL);;
esac
export userPATH=$PATH  # in case test scripts need it
PATH=$(
	builtin getconf 2>/dev/null || PATH=/run/current-system/sw/bin:/usr/xpg7/bin:/usr/xpg6/bin:/usr/xpg4/bin:/bin:/usr/bin:$PATH
	getconf PATH 2>/dev/null
) || PATH=/bin:/usr/bin
if	[[ -d /usr/ucb ]]
then	PATH=$PATH:/usr/ucb
fi
PATH=$PATH:$d
if	[[ ! -v INSTALLROOT || $INSTALLROOT != /* ]]
then	echo "$0: cannot continue: INSTALLROOT must be set" >&2
	echo "$0: invoke me after 'bin/package use', or via 'bin/shtests' or 'bin/package test'" >&2
	exit 1
fi
PATH=$INSTALLROOT/bin:$PATH
if	[[ ${SHELL%/*} != $INSTALLROOT/bin && ${SHELL%/*} != $INSTALLROOT/dyn/bin ]]
then	PATH=${SHELL%/*}:$PATH
fi
if	[[ ! $SHCOMP ]]
then	s=${SHELL:##*sh}
	s=${SHELL:%/*}/shcomp$s
	if	[[ -x $s ]]
	then	SHCOMP=$s
	elif	[[ -x ${s%-g} ]]
	then	SHCOMP=${s%-g}
	else	SHCOMP=shcomp
	fi
fi

# when testing a shell outside of INSTALLROOT, make sure libraries in INSTALLROOT are not loaded
if	! [[ ${SHELL%/*} -ef $INSTALLROOT/bin || ${SHELL%/*} -ef $INSTALLROOT/dyn/bin ]]
then	unset -v LD_LIBRARY_PATH LIBPATH SHLIB_PATH DYLD_LIBRARY_PATH _RLD_ROOT
fi

tmp=$(
	d=${TMPDIR:-/tmp}/ksh93.shtests.$$.${RANDOM:-0}
	mkdir -m700 -- "$d" && CDPATH= cd -P -- "$d" && pwd
) || {
	echo 'mkdir failed' >&2
	exit 1
}
if	(( keep ))
then	trap 'printf "\nTemporary files left in: %s\n" "$tmp"' EXIT
else	trap 'cd / && rm -rf "$tmp"' EXIT
fi

# for interactive shell ('ksh -i') tests: avoid affecting ~/.sh_history
# (some tests must unset or modify $HISTFILE, so set $HOME instead)
export HOME=$tmp

# make the SHOPT_* macros available to the tests as environment variables
command unset "${!SHOPT_@}" 2>/dev/null
SHOPT()
{
	[[ $1 == *=* ]] && eval "export SHOPT_${ printf %q "${1%%=*}"; }=${ printf %q "${1#*=}"; }"
}
. "${SHOPTFILE:-../SHOPT.sh}"
unset -f SHOPT
# override with probed values from shopt.h:
# change '#define SHOPT_foo = bar' to 'export SHOPT_foo=bar' and evaluate
if	[[ -f $INSTALLROOT/src/cmd/ksh93/shopt.h ]] &&
	i=$(sed -n $'/^#[ \t]*define[ \t][ \t]*SHOPT_/ {
				s/.*define[ \t][ \t]*//
				s/[ \t][ \t]*/=/
				s/^/export /
				p
			}' "$INSTALLROOT"/src/cmd/ksh93/shopt.h)
then	eval "$i" || exit
else	echo "$0: cannot grep shopt.h" >&2
	exit 1
fi

SHOPT_MULTIBYTE=$( LC_ALL=C.UTF-8; x=$'\xc3\xa9'; print $(( ${#x}==1 )) )
if	(( !SHOPT_MULTIBYTE && utf8 && !posix && !compile ))
then	echo "-u/--utf8 is unavailable because multibyte support was not compiled in." >&2
	exit 1
fi

if	(( compile ))
then	if	whence $SHCOMP > /dev/null
	then	:
	elif	(( compile > 1 ))
	then	echo $0: --compile: $SHCOMP not found >&2
		exit 1
	else	compile=0
	fi
fi
if	[[ $valgrind ]]
then	if	[[ -x $SHELL-g ]]
	then	SHELL=$SHELL-g
	fi
	valxml=$tmp/valgrind.xml
	valgrind+=" --xml-file=$valxml"
fi
typeset -A tests
typeset -i total_e=0
for i in ${*-*.sh}
do	[[ $i == *.sh ]] || i+='.sh'
	if	[[ ! -r $i ]]
	then	echo $0: $i: not found >&2
		(( ++total_e ))
		continue
	fi
	t=$(	case $i in
		glob.sh)	grep -c '^[[:blank:]]*test_[a-z]\{3,\}' $i ;;
		leaks.sh)	grep -c ^TEST $i ;;
		printf.sh)	grep -c -E '([[:blank:]]err_exit|^[[:blank:]]*T)[[:blank:]]' $i ;;
		pty.sh)		grep -c 'tst ' $i ;;
		*)		grep -c err_exit $i ;;
		esac	)
	tests[$i]=$t
	T=test
	if	(( t != 1 ))
	then	T=${T}s
	fi
	u=${i##*/}
	u=${u%.sh}
	if	(( posix || utf8 ))
	then	locales=
		(( posix )) && locales+=" ${LANG:-C}"
		[[ $utf8 == 0 || $i == $setslocale ]] || ((!SHOPT_MULTIBYTE)) || locales+=" C.UTF-8"
		for lang in $locales
		do	o=$u
			tmp_s=$tmp/$u.$lang
			mkdir -m700 "$tmp_s" || exit
			cd "$tmp_s" || exit
			if	[[ $lang == C ]]
			then	lang=
			else	o="$o($lang)"
				lang=LANG=$lang
			fi
			echo test $o begins ${time:+"at $(date +%Y-%m-%d+%H:%M:%S)"}
			(
				export ${lang:+"$lang"} "tmp=$tmp_s"
				$valgrind $SHELL $trace "$OLDPWD/$i"
			)
			e=$?
			cd "$OLDPWD" || exit
			if	(( !keep ))
			then	rm -rf "$tmp_s"
			fi
			if	[[ $valgrind ]]
			then	valxml $valxml
				(( e += $? ))
			fi
			if	(( e > 128 && ++total_e ))
			then	E="(killed by SIG$(kill -l "$e"))"  # report signal if test crashes
			elif	(( e == 1 && ++total_e ))
			then	E="1 error"
			else	E="$e errors"
				(( total_e += e ))
			fi
			if	(( e == 0 ))
			then	echo test $o passed ${time:+"at $(date +%Y-%m-%d+%H:%M:%S)"} "[ $t $T $E ]"
			else	echo test $o failed ${time:+"at $(date +%Y-%m-%d+%H:%M:%S)"} with exit code $e "[ $t $T $E ]"
			fi
			case	$E in
			*INT\))	kill -s INT $$ ;;  # if test was ^C'd, don't keep going but pass down SIGINT
			esac
		done
	fi
	# Skip pty tests for shcomp, unless -c was explicitly specified (compile==2)
	if	[[ compile -gt 0 && ! ($u == pty && compile -lt 2) ]]
	then	c=$tmp/shcomp-$u.ksh
		o="$u(shcomp)"
		echo test $o begins ${time:+"at $(date +%Y-%m-%d+%H:%M:%S)"}
		tmp_s=$tmp/$u.shcomp
		mkdir -m700 "$tmp_s" || exit
		# Must define aliases from _common in script itself so they are expanded at compile time.
		# Replace the first few lines with those to avoid affecting $LINENO; they are comments anyway.
		num_alias=$(grep -c '^alias [[:alnum:]_]\{2,\}=' _common)
		{ grep '^alias [[:alnum:]_]\{2,\}=' _common; sed "1,$num_alias d" "$i"; } >$c.orig
		cd "$tmp_s" || exit
		if	"$SHCOMP" "$c.orig" > $c
		then	if	tmp=$tmp_s $valgrind $SHELL $trace $c
			then	echo test $o passed ${time:+"at $(date +%Y-%m-%d+%H:%M:%S)"} "[ $t $T 0 errors ]"
			else	e=$?
				if	(( e > 128 && ++total_e ))
				then	E="(killed by SIG$(kill -l "$e"))"  # report signal if test crashes
				elif	(( e == 1 && ++total_e ))
				then	E="1 error"
				else	E="$e errors"
					(( total_e += e ))
				fi
				echo test $o failed ${time:+"at $(date +%Y-%m-%d+%H:%M:%S)"} with exit code $e "[ $t $T $E ]"
				case	$E in
				*INT\))	kill -s INT $$ ;;  # if test was ^C'd, don't keep going but pass down SIGINT
				esac
			fi
		else	e=$?
			(( ++total_e ))
			echo test $o failed to compile ${time:+"at $(date +%Y-%m-%d+%H:%M:%S)"} with exit code $e "[ 1 test 1 error ]"
		fi
		cd "$OLDPWD" || exit
		if	(( !keep ))
		then	rm -rf "$tmp_s" "$c"
		fi
	fi
done
print "Total errors: $total_e"

# This test harness should continue to work with old and buggy versions of ksh93, so check
# if we have the 'times' builtin and can use process substitution with output redirection.
# See:	https://github.com/ksh93/ksh/commit/65d363fd
#	https://github.com/ksh93/ksh/issues/2
if 	(	ulimit -t unlimited 2> /dev/null  # fork to circumvent old ksh bugs
		unalias times
		PATH=/dev/null whence -q times || exit
		cd "$tmp" || exit
		echo ok > >(cat > procsubst_test)
		wait
		[[ $(< procsubst_test) == ok ]]
	) 2>/dev/null
then	# Transforming 'times' output requires a process substitution.
	# A regular 'times | ...' pipeline would cause 'times' to be
	# run in a subshell, which would reset all the times to zero.
	times > >(
		t[0]='CPU time'	t[1]='user:'	t[2]='system:'
		t[3]='main:'; 	read t[4]	t[5]
		t[6]='tests:';	read t[7]	t[8]
		printf '%-8s%12s %12s\n' "${t[@]}"
	)
	wait "$!"  # process substitutions are executed asynchronously
else	# No pretty-printing.
	times
fi

exit $((total_e > 125 ? 125 : total_e))
