#!/usr/bin/ksh
#
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source.  A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#

#
# Copyright (c) 2019, Joyent, Inc.
# Copyright 2025 Oxide Computer Company
#

#
# Run all of the various CTF tests
#

unalias -a
#set -o xtrace

if [[ -z "$TMPDIR" ]]; then
	TMPDIR="/tmp"
fi


ctf_arg0=$(basename $0)
ctf_root=$(cd $(dirname $0) && echo $PWD)
ctf_tests=
ctf_cc="gcc"
ctf_cxx="g++"
ctf_as="gas"
ctf_convert="ctfconvert"
ctf_merge="ctfmerge"
ctf_debugdef="-gdwarf-2"
ctf_debugflags=
ctf_mach32="-m32"
ctf_mach64="-m64"
ctf_temp="$TMPDIR/ctftest.$$.o"
ctf_makefile="Makefile.ctftest"
ctf_nerrs=0
ctf_cc_type=
ctf_cc_vers=

usage()
{
	typeset msg="$*"
	[[ -z "$msg" ]] || echo "$msg" >&2
	cat <<USAGE >&2
Usage: $ctf_arg0 [-a as] [-c cc] [-C CC] [-g flags] [-m ctfmerge] [-t ctfconvert]

	Runs the CTF test suite

	-a assembler		Use the specified assembler, defaults to 'as'
	-c compiler		Use the specified C compiler, defaults to 'gcc'
	-C compiler		Use the specified C++ compiler, defaults to 'g++'
	-g flags		Use flags to generate debug info. Defaults to
				"-gdwarf-2".
	-m ctfmerge		Use the specified ctfmerge, defaults to
				'ctfmerge'
	-t ctfconvert		Use the specified ctfconvert, defaults to
				'ctfconvert'
USAGE
	exit 2
}


test_fail()
{
	typeset msg="$*"
	[[ -z "$msg" ]] && msg="failed"
	echo "TEST FAILED: $msg" >&2
	((ctf_nerrs++))
}

warn()
{
	typeset msg="$*"
	echo "$ctf_arg0: $msg" >&2
}

fatal()
{
	typeset msg="$*"
	[[ -z "$msg" ]] && msg="failed"
	echo "$ctf_arg0: $msg" >&2
	rm -f "$ctf_temp"
	exit 1
}

#
# Attempt to try and figure out what class and version of compiler we
# are dealing with so we can try and skip known failures due to existing
# bugs in the compiler.
#
determine_compiler()
{
	typeset name=$($ctf_cc --version | awk '{ print $1; exit; }')
	typeset version

	if [[ "$name" == "gcc" ]]; then
		version=$($ctf_cc --version | awk '{ print $NF; exit; }')
	elif [[ "$name" == "clang" ]]; then
		version=$($ctf_cc --version | awk '{ print $NF; exit; }')
	else
		warn "failed to parse compiler name from $ctf_cc, will " \
		    "not make any assumptions about expected failures"
		name="unknown"
		version="unknown"
	fi

	ctf_cc_type="$name"
	ctf_cc_version="$version"
}

announce()
{
	cat << EOF
Beginning CTF tests with the following settings:
cc:		$(which $ctf_cc)
detected:	$ctf_cc_type $ctf_cc_version
CC:		$(which $ctf_cxx)
as:		$(which $ctf_as)
ctfconvert:	$(which $ctf_convert)
ctfmerge:	$(which $ctf_merge)
32-bit CFLAGS:	$ctf_32cflags
64-bit CFLAGS:	$ctf_64cflags

EOF
}

run_one()
{
	typeset source=$1 checker=$2 flags=$3

	if ! "$ctf_cc" $flags -o "$ctf_temp" -c "$source"; then
		test_fail "failed to compile $source with flags: $flags"
		return
	fi

	if ! "$ctf_convert" "$ctf_temp"; then
		test_fail "failed to convert CTF in $source"
		return
	fi

	if ! "$checker" "$ctf_temp"; then
		test_fail "check for $source, $checker, failed"
		return
	fi

	rm -f "$ctf_temp"
	echo "TEST PASSED: $source $flags"
}

#
# Perform a more complex build. The Makefile present will drive the
# building of the artifacts and the running of the tests based on the
# variables that we pass to it.
#
run_dir()
{
	typeset dir outdir check32 check64 flags32 flags64

	dir=$1
	outdir="$TMPDIR/ctftest.$$-$(basename $d)"
	check32=$2
	flags32=$3
	check64=$4
	flags64=$5

	if ! mkdir $outdir; then
		fatal "failed to make temporary directory '$outdir'"
	fi

	if ! make -C $dir -f Makefile.ctftest \
	    BUILDDIR="$outdir" \
	    CC="$ctf_cc" \
	    CFLAGS32="$ctf_mach32" \
	    CFLAGS64="$ctf_mach64" \
	    DEBUGFLAGS="$ctf_debugflags" \
	    CTFCONVERT="$ctf_convert" \
	    CTFMERGE="$ctf_merge" \
	    build 1>/dev/null; then
		rm -rf $outdir
		test_fail "failed to build $dir"
		return
	fi

	if ! make -C $dir -f Makefile.ctftest \
	    BUILDDIR="$outdir" \
	    CHECK32="$check32" \
	    CHECK64="$check64" \
	    run-test 1>/dev/null; then
		rm -rf $outdir
		test_fail "failed to run tests for $dir"
		return
	fi

	rm -rf $outdir
	echo "TEST PASSED: $dir (dir)"
}

#
# Find all of the tests that exist and then try to run them all. Tests
# may either be a single file or a directory.
#
run_tests()
{
	typeset t base check
	ctf_tests=$(ls "$ctf_root"/*.c)
	for t in $ctf_tests; do
		base=$(basename "$t" .c)
		check=$(echo "$base" | sed s/test-/check-/)
		if [[ -f "$ctf_root/$check" ]]; then
			run_one $t "$ctf_root/$check" "$ctf_32cflags"
			run_one $t "$ctf_root/$check" "$ctf_64cflags"
		elif [[ -f "$ctf_root/$check-32" && \
		    -f "$ctf_root/$check-64" ]]; then
			run_one $t "$ctf_root/$check-32" "$ctf_32cflags"
			run_one $t "$ctf_root/$check-64" "$ctf_64cflags"
		else
			test_fail "missing checker for $t"
		fi
	done

	for d in $(find "$ctf_root" -maxdepth 1 -type d -name 'test-*'); do
		[[ ! -f "$d/$ctf_makefile" ]] && continue
		base=$(basename "$d")
		check=$(echo "$base" | sed s/test-/check-/)
		if [[ -f "$ctf_root/$check" ]]; then
			run_dir $d "$ctf_root/$check" "$ctf_32cflags" \
			    "$ctf_root/$check" "$ctf_64cflags"
		elif [[ -f "$ctf_root/$check-32" && \
		    -f "$ctf_root/$check-64" ]]; then
			run_dir $d "$ctf_root/$check-32" "$ctf_32cflags" \
			    "$ctf_root/$check-64" "$ctf_64cflags"
		else
			test_fail "missing checker for $t"
		fi
	done

	outdir="$TMPDIR/ctftest.$$"

	for f in $(find "$ctf_root" -maxdepth 1 -type f -name 'ctftest-*'); do
		if ! mkdir $outdir; then
			fatal "failed to make temporary directory '$outdir'"
		fi

		echo "Running $f in $outdir"

		(cd $outdir && $f)

		if (( $? != 0 )); then
			test_fail "$f failed"
		else
			echo "TEST PASSED: $f"
		fi

		rm -rf $outdir
	done
}

while getopts ":a:C:c:g:m:t:" c $@; do
	case "$c" in
	a)
		ctf_as=$OPTARG
		;;
	C)
		ctf_cxx=$OPTARG
		;;
	c)
		ctf_cc=$OPTARG
		;;
	g)
		ctf_debugflags+=" -g$OPTARG"
		;;
	m)
		ctf_merge=$OPTARG
		;;
	t)
		ctf_convert=$OPTARG
		;;
	:)
		usage "option requires an argument -- $OPTARG"
		;;
	*)
		usage "invalid option -- $OPTARG"
		;;
	esac
done

if [[ -z "$ctf_debugflags" ]]; then
	ctf_debugflags=$ctf_debugdef
fi

ctf_32cflags="$ctf_mach32 $ctf_debugflags"
ctf_64cflags="$ctf_mach64 $ctf_debugflags"

determine_compiler

export ctf_as ctf_cc ctf_cxx ctf_debugflags ctf_merge ctf_convert
export ctf_cc_type ctf_cc_version

announce

run_tests

if (( ctf_nerrs != 0 )); then
	if (( ctf_nerrs == 1 )); then
		printf "\n%s: %u test failed\n" "$ctf_arg0" "$ctf_nerrs"
	else
		printf "\n%s: %u tests failed\n" "$ctf_arg0" "$ctf_nerrs"
	fi
	exit 1
else
	printf "\n%s: All tests passed successfully\n" "$ctf_arg0"
	exit 0
fi
