#!/bin/bash

#
# libseccomp regression test automation script
#
# Copyright IBM Corp. 2012
# Author: Corey Bryant <coreyb@linux.vnet.ibm.com>
#

#
# This library is free software; you can redistribute it and/or modify it
# under the terms of version 2.1 of the GNU Lesser General Public License as
# published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, see <http://www.gnu.org/licenses>.
#

GLBL_ARCH_LE_SUPPORT=" \
	x86 x86_64 x32 \
	arm aarch64 \
	mipsel mipsel64 mipsel64n32 \
	ppc64le \
	riscv64"
GLBL_ARCH_BE_SUPPORT=" \
	mips mips64 mips64n32 \
	parisc parisc64 \
	ppc ppc64 \
	s390 s390x"

GLBL_ARCH_32B_SUPPORT=" \
	x86 x32 \
	arm \
	mips mipsel mips64n32 mipsel64n32 \
	parisc \
	ppc \
	s390"

GLBL_ARCH_64B_SUPPORT=" \
	x86_64 \
	aarch64 \
	mips64 \
	parisc64 \
	ppc64 \
	riscv64 \
	s390x"

GLBL_SYS_ARCH="../tools/scmp_arch_detect"
GLBL_SYS_RESOLVER="../tools/scmp_sys_resolver"
GLBL_SYS_SIM="../tools/scmp_bpf_sim"
GLBL_SYS_API="../tools/scmp_api_level"

####
# functions

#
# Dependency check
#
# Arguments:
#     1    Dependency to check for
#
function check_deps() {
	[[ -z "$1" ]] && return
	which "$1" >& /dev/null
	return $?
}

#
# Dependency verification
#
# Arguments:
#     1    Dependency to check for
#
function verify_deps() {
	[[ -z "$1" ]] && return
	if ! check_deps "$1"; then
		echo "error: install \"$1\" and include it in your \$PATH"
		exit 1
	fi
}

#
# Print out script usage details
#
function usage() {
cat << EOF
usage: regression [-h] [-v] [-m MODE] [-a] [-b BATCH_NAME] [-l <LOG>]
                  [-s SINGLE_TEST] [-t <TEMP_DIR>] [-T <TEST_TYPE>]

libseccomp regression test automation script
optional arguments:
  -h             show this help message and exit
  -m MODE        specified the test mode [c (default), python]
                  can also be set via LIBSECCOMP_TSTCFG_MODE_LIST env variable
  -a             specifies all tests are to be run
  -b BATCH_NAME  specifies batch of tests to be run
                  can also be set via LIBSECCOMP_TSTCFG_BATCHES env variable
  -l [LOG]       specifies log file to write test results to
  -s SINGLE_TEST specifies individual test number to be run
  -t [TEMP_DIR]  specifies directory to create temporary files in
  -T [TEST_TYPE] only run tests matching the specified type
                  can also be set via LIBSECCOMP_TSTCFG_TYPE env variable
  -v             specifies that verbose output be provided
EOF
}

#
# Match on a single word/column in a CSV string
#
# Arguments:
#     1    string containing the CSV
#     2    string containing the word to match
#
# Returns true/0 if a match is found false/1 otherwise.
#
function match_csv_word() {
	[[ -z $1 || -z $2 ]] && return 1

	echo "$1" | sed 's/,/ /g' | grep -w "$2"
}

#
# Generate a string representing the test number
#
# Arguments:
#     1    string containing the batch name
#     2    value of the test number from the input test data file
#     3    value of the subtest number that corresponds to argument 1
#
#  The actual test number from the input test data file is 1 for the first
#  test found in the file, 2 for the second, etc.
#
#  The subtest number is useful for batches that generate multiple tests based
#  on a single line of input from the test data file.  The subtest number
#  should be set to zero if the  corresponding test data is actual test data
#  that was read from the input file, and should be set to a value greater than
#  zero if the corresponding test data is generated.
#
function generate_test_num() {
	local testnumstr=$(printf '%s%%%%%03d-%05d' "$1" $2 $3)
	echo "$testnumstr"
}

#
# Print the test data to the log file
#
# Arguments:
#     1    string containing generated test number
#     2    string containing line of test data
#
function print_data() {
	if [[ -n $verbose ]]; then
		printf "Test %s data:     %s\n" "$1" "$2" >&$logfd
	fi
}

#
# Print the test result to the log file
#
# Arguments:
#     1    string containing generated test number
#     2    string containing the test result (INFO, SUCCESS, ERROR, or FAILURE)
#     3    string containing addition details
#
function print_result() {
	if [[ $2 == "INFO" && -z $verbose ]]; then
		return
	fi
	if [[ $3 == "" ]]; then
		printf "Test %s result:   %s\n" "$1" "$2" >&$logfd
	else
		printf "Test %s result:   %s %s\n" "$1" "$2" "$3" >&$logfd
	fi
}

#
# Print the valgrind header to the log file
#
# Arguments:
#     1    string containing generated test number
#
function print_valgrind() {
	if [[ -n $verbose ]]; then
		printf "Test %s valgrind output\n" "$1" >&$logfd
	fi
}

#
# Get the low or high range value from a range specification
#
# Arguments:
#     1    value specifying range value to retrieve: low (1) or high (2)
#     2    string containing dash-separated range or a single value
#
function get_range() {
	if [[ $2 =~ ^[0-9a-fA-Fx]+-[0-9a-fA-Fx]+$ ]]; then
		# if there's a dash, get the low or high range value
		range_val=$(echo "$2" | cut -d'-' -f "$1")
	else
		# otherwise there should just be a single value
		range_val="$2"
	fi
	echo "$range_val"
}

#
# Get the number sequence for a given range with increments of 1, i.e.
# implement a specialized seq(1).
#
# We use our own implementation based on miniseq in favour to the standard seq
# tool as, at least, seq of coreutils v8.23 and v8.24 has problems on 32 bit
# ARM for large numbers (see the mailing thread at
# https://groups.google.com/forum/#!topic/libseccomp/VtrClkXxLGA).
#
# Arguments:
#     1    starting value
#     2    last value
#
function get_seq() {
	# NOTE: this whole thing is a bit hacky, but we need to search around
	#       for miniseq to fix 'make distcheck', someday we should fix this
	if [[ -x ./miniseq ]]; then
		./miniseq "$1" "$2"
	elif [[ -x $basedir/miniseq ]]; then
		$basedir/miniseq "$1" "$2"
	else
		# we're often run from a subshell, so we can't simply exit
		echo "error: unable to find miniseq" >&2
		kill $pid
	fi
}

#
# Run the specified test command (with valgrind if requested)
#
# Arguments:
#     1    string containing generated test number
#     2    string containing command name
#     3    string containing command options
#     4    number for the stdout fd
#     5    number for the stderr fd
#
function run_test_command() {
	local cmd

	if [[ $mode == "python" ]]; then
		cmd="PYTHONPATH=$PYTHONPATH"
		cmd="$cmd:$(cd $(pwd)/../src/python/build/lib.*; pwd)"
		# check and adjust if we are doing a VPATH build
		if [[ -e "./$2.py" ]]; then
			cmd="$cmd /usr/bin/env python3 $2.py $3"
		else
			cmd="$cmd /usr/bin/env python3 ${srcdir}/$2.py $3"
		fi
	else
		cmd="$2 $3"
	fi

	# setup the stdout/stderr redirects
	local stdout=$4
	local stderr=$5
	[[ -z $stdout ]] && stdout=$logfd
	[[ -z $stderr ]] && stderr=$logfd

	# run the command
	eval "$cmd" 1>&$stdout 2>&$stderr

	# return the command's return code
	return $?
}

#
# Generate pseudo-random string of alphanumeric characters
#
# The generated string will be no larger than the corresponding
# architecture's register size.
#
function generate_random_data() {
	local rcount
	local rdata
	if [[ $arch == "x86_64" ]]; then
		rcount=$[ ($RANDOM % 16) + 1 ]
	else
		rcount=$[ ($RANDOM % 8) + 1 ]
	fi
	rdata=$(dd if=/dev/urandom bs=64 count=1 status=none | \
		md5sum | awk '{ print $1 }' | head -c"$rcount")
	echo "$rdata"
}

#
# Run the specified "bpf-sim-fuzz" test
#
# Tests that belong to the "bpf-sim-fuzz" test type generate a BPF filter and
# then run a simulated system call test with pseudo-random fuzz data for the
# syscall and argument values.  Tests that belong to this test type provide the
# following data on a single line in the input batch file:
#
#     Testname - The executable test name (e.g. 01-allow, 02-basic, etc.)
#     StressCount - The number of fuzz tests to run against the filter
#
# The following test data is output to the logfile for each generated test:
#
#     Testname - The executable test name (e.g. 01-allow, 02-basic, etc.)
#     Syscall - The fuzzed syscall value to be simulated against the filter
#     Arg0-5 - The fuzzed syscall arg values to be simulated against the filter
#
# Arguments:
#     1    string containing the batch name
#     2    value of test number from batch file
#     3    string containing line of test data from batch file
#
function run_test_bpf_sim_fuzz() {
	local rc

	# begin splitting the test data from the line into individual variables
	local line=($3)
	local testname=${line[0]}
	local stress_count=${line[1]}

	# check for stress count configuration via environment variables
	[[ -n $LIBSECCOMP_TSTCFG_STRESSCNT ]] && \
		stress_count=$LIBSECCOMP_TSTCFG_STRESSCNT

	for i in $(get_seq 1 $stress_count); do
		local sys=$(generate_random_data)
		local -a arg=($(generate_random_data) $(generate_random_data) \
			      $(generate_random_data) $(generate_random_data) \
			      $(generate_random_data) $(generate_random_data))

		# get the generated sub-test num string
		local testnumstr=$(generate_test_num "$1" $2 $i)

		# set up log file test data line for this individual test,
		# spacing is added to align the output in the correct columns
		local -a COL_WIDTH=(26 17 17 17 17 17 17)
		local testdata=$(printf "%-${COL_WIDTH[0]}s" $testname)
		testdata+=$(printf "%-${COL_WIDTH[1]}s" $sys)
		testdata+=$(printf "%-${COL_WIDTH[2]}s" ${arg[0]})
		testdata+=$(printf "%-${COL_WIDTH[3]}s" ${arg[1]})
		testdata+=$(printf "%-${COL_WIDTH[4]}s" ${arg[2]})
		testdata+=$(printf "%-${COL_WIDTH[5]}s" ${arg[3]})
		testdata+=$(printf "%-${COL_WIDTH[6]}s" ${arg[4]})
		testdata+=$(printf "%s" ${arg[5]})

		# print out the generated test data to the log file
		print_data "$testnumstr" "$testdata"

		# set up the syscall argument values to be passed to bpf_sim
		for i in {0..5}; do
			arg[$i]=" -$i ${arg[$i]} "
		done

		# run the test command and put the BPF filter in a temp file
		exec 4>$tmpfile
		run_test_command "$testnumstr" "./$testname" "-b" 4 ""
		rc=$?
		exec 4>&-
		if [[ $rc -ne 0 ]]; then
			print_result $testnumstr "ERROR" "$testname rc=$rc"
			stats_error=$(($stats_error+1))
			return
		fi

		# simulate the fuzzed syscall data against the BPF filter, we
		# don't verify the resulting action since we're just testing for
		# stability
		allow=$($GLBL_SYS_SIM -f $tmpfile -s $sys \
			${arg[0]} ${arg[1]} ${arg[2]} ${arg[3]} ${arg[4]} \
			${arg[5]})
		rc=$?
		if [[ $rc -ne 0 ]]; then
			print_result $testnumstr "ERROR" "bpf_sim rc=$rc"
			stats_error=$(($stats_error+1))
		else
			print_result $testnumstr "SUCCESS" ""
			stats_success=$(($stats_success+1))
		fi
		stats_all=$(($stats_all+1))
	done
}

#
# Run the specified "bpf-sim" test
#
# Tests that belong to the "bpf-sim" test type generate a BPF filter and then
# run a simulated system call test to validate the filter.  Tests that belong to
# this test type provide the following data on a single line in the input batch
# file:
#
#     Testname - The executable test name (e.g. 01-allow, 02-basic, etc.)
#     Arch - The architecture that the test should be run on (all, x86, x86_64)
#     Syscall - The syscall to simulate against the generated filter
#     Arg0-5 - The syscall arguments to simulate against the generated filter
#     Result - The expected simulation result (ALLOW, KILL, etc.)
#
# If a range of syscall or argument values are specified (e.g. 1-9), a test is
# generated for every combination of range values.  Otherwise, the individual
# test is run.
#
# Arguments:
#     1    string containing the batch name
#     2    value of test number from batch file
#     3    string containing line of test data from batch file
#
function run_test_bpf_sim() {
	local rc
	local LOW=1
	local HIGH=2
	local -a arg_empty=(false false false false false false)

	# begin splitting the test data from the line into individual variables
	local line=($3)
	local testname=${line[0]}
	local testarch=${line[1]}
	local low_syscall  #line[2]
	local high_syscall #line[2]
	local -a low_arg   #line[3-8]
	local -a high_arg  #line[3-8]
	local result=${line[9]}

	# expand the architecture list
	local simarch_tmp
	local simarch_avoid
	simarch_tmp=""
	simarch_avoid=""
	for arch_i in $(echo $testarch | sed -e 's/,/ /g'); do
		case $arch_i in
		all)
			# add the native arch
			simarch_tmp+=" $arch"
			;;
		all_le)
			# add the native arch only if it is little endian
			if echo "$GLBL_ARCH_LE_SUPPORT" | grep -qw "$arch"; then
				simarch_tmp+=" $arch"
			fi
			;;
		+all_le)
			# add all of the little endian architectures
			simarch_tmp+=" $GLBL_ARCH_LE_SUPPORT"
			;;
		all_be)
			# add the native arch only if it is big endian
			if echo "$GLBL_ARCH_BE_SUPPORT" | grep -qw "$arch"; then
				simarch_tmp+=" $arch"
			fi
			;;
		+all_be)
			# add all of the big endian architectures
			simarch_tmp+=" $GLBL_ARCH_BE_SUPPORT"
			;;
		all_32)
			# add the native arch only if it is 32-bit
			if echo "$GLBL_ARCH_32B_SUPPORT" | grep -qw "$arch"; then
				simarch_tmp+=" $arch"
			fi
			;;
		+all_32)
			# add all of the 32-bit architectures
			simarch_tmp+=" $GLBL_ARCH_32B_SUPPORT"
			;;
		all_64)
			# add the native arch only if it is 64-bit
			if echo "$GLBL_ARCH_64B_SUPPORT" | grep -qw "$arch"; then
				simarch_tmp+=" $arch"
			fi
			;;
		+all_64)
			# add all of the 64-bit architectures
			simarch_tmp+=" $GLBL_ARCH_64B_SUPPORT"
			;;
		+*)
			# add the architecture specified
			simarch_tmp+=" ${arch_i:1}"
			;;
		-*)
			# remove the architecture specified
			simarch_avoid+=" ${arch_i:1}"
			;;
		*)
			# add the architecture specified if it is native
			if [[ "$arch_i" == "$arch" ]]; then
				simarch_tmp+=" $arch_i"
			fi
			;;
		esac
	done

	# make sure we remove any undesired architectures
	local simarch_list
	simarch_list=""
	for arch_i in $simarch_tmp; do
		if echo "$simarch_avoid" | grep -q -v -w "$arch_i"; then
			simarch_list+=" $arch_i"
		fi
	done
	simarch_list=$(echo $simarch_list | sed -e 's/  / /g;s/^ //;')

	# do we have any architectures remaining in the list?
	if [[ $simarch_list == "" ]]; then
		print_result $(generate_test_num "$1" $2 1) "SKIPPED" \
			"(architecture difference)"
		stats_skipped=$(($stats_skipped+1))
		return
	fi

	# get low and high range arg values
	line_i=3
	for arg_i in {0..5}; do
		low_arg[$arg_i]=$(get_range $LOW "${line[$line_i]}")
		high_arg[$arg_i]=$(get_range $HIGH "${line[$line_i]}")

		# fix up empty arg values so the nested loops work
		if [[ ${low_arg[$arg_i]} == "N" ]]; then
			arg_empty[$arg_i]=true
			low_arg[$arg_i]=0
			high_arg[$arg_i]=0
		fi

		line_i=$(($line_i+1))
	done

	# loop through the selected architectures
	for simarch in $simarch_list; do
		# print architecture header if necessary
		if [[ $simarch != $simarch_list ]]; then
			echo " test arch:  $simarch" >&$logfd
		fi

		# reset the subtest number
		local subtestnum=1

		# get low and high syscall values and convert them to numbers
		low_syscall=$(get_range $LOW "${line[2]}")
		if [[ ! $low_syscall =~ ^\-?[0-9]+$ ]]; then
			low_syscall=$($GLBL_SYS_RESOLVER -a $simarch -t \
				      $low_syscall)
			if [[ $? -ne 0 ]]; then
				print_result $(generate_test_num "$1" $2 1) \
					     "ERROR" "sys_resolver rc=$?"
				stats_error=$(($stats_error+1))
				return
			fi
		fi
		high_syscall=$(get_range $HIGH "${line[2]}")
		if [[ ! $high_syscall =~ ^\-?[0-9]+$ ]]; then
			high_syscall=$($GLBL_SYS_RESOLVER -a $simarch -t \
				       $high_syscall)
			if [[ $? -ne 0 ]]; then
				print_result $(generate_test_num "$1" $2 1) \
					     "ERROR" "sys_resolver rc=$?"
				stats_error=$(($stats_error+1))
				return
			fi
		fi

		# if ranges exist, the following will loop through all syscall
		# and arg ranges and generate/run every combination of requested
		# tests; if no ranges were specifed, then the single test is
		# run
		for sys in $(get_seq $low_syscall $high_syscall); do
		for arg0 in $(get_seq ${low_arg[0]} ${high_arg[0]}); do
		for arg1 in $(get_seq ${low_arg[1]} ${high_arg[1]}); do
		for arg2 in $(get_seq ${low_arg[2]} ${high_arg[2]}); do
		for arg3 in $(get_seq ${low_arg[3]} ${high_arg[3]}); do
		for arg4 in $(get_seq ${low_arg[4]} ${high_arg[4]}); do
		for arg5 in $(get_seq ${low_arg[5]} ${high_arg[5]}); do
			local -a arg=($arg0 $arg1 $arg2 $arg3 $arg4 $arg5)

			# Get the generated sub-test num string
			local testnumstr=$(generate_test_num "$1" $2 \
					   $subtestnum)

			# format any empty args to print to log file
			for i in {0..5}; do
				if ${arg_empty[$i]}; then
					arg[$i]="N"
				fi
			done

			# set up log file test data line for this
			# individual test, spacing is added to align
			# the output in the correct columns
			local -a COL_WIDTH=(26 08 14 11 17 21 09 06 06)
			local testdata=$(printf "%-${COL_WIDTH[0]}s" $testname)
			testdata+=$(printf "%-${COL_WIDTH[1]}s" $simarch)
			testdata+=$(printf "%-${COL_WIDTH[2]}s" $sys)
			testdata+=$(printf "%-${COL_WIDTH[3]}s" ${arg[0]})
			testdata+=$(printf "%-${COL_WIDTH[4]}s" ${arg[1]})
			testdata+=$(printf "%-${COL_WIDTH[5]}s" ${arg[2]})
			testdata+=$(printf "%-${COL_WIDTH[6]}s" ${arg[3]})
			testdata+=$(printf "%-${COL_WIDTH[7]}s" ${arg[4]})
			testdata+=$(printf "%-${COL_WIDTH[8]}s" ${arg[5]})
			testdata+=$(printf "%-${COL_WIDTH[9]}s" $result)

			# print out the test data to the log file
			print_data "$testnumstr" "$testdata"

			# set up the syscall arguments to be passed to bpf_sim
			for i in {0..5}; do
				if ${arg_empty[$i]}; then
					arg[$i]=""
				else
					arg[$i]=" -$i ${arg[$i]} "
				fi
			done

			# run the test command and put the BPF in a temp file
			exec 4>$tmpfile
			run_test_command "$testnumstr" "./$testname" "-b" 4 ""
			rc=$?
			exec 4>&-
			if [[ $rc -ne 0 ]]; then
				print_result $testnumstr \
					     "ERROR" "$testname rc=$rc"
				stats_error=$(($stats_error+1))
				return
			fi

			# simulate the specifed syscall against the BPF filter
			# and verify the results
			action=$($GLBL_SYS_SIM -a $simarch -f $tmpfile \
				 -s $sys ${arg[0]} ${arg[1]} ${arg[2]} \
				 ${arg[3]} ${arg[4]} ${arg[5]})
			rc=$?
			if [[ $rc -ne 0 ]]; then
				print_result $testnumstr \
					     "ERROR" "bpf_sim rc=$rc"
				stats_error=$(($stats_error+1))
			elif [[ "$action" != "$result" ]]; then
				print_result $testnumstr "FAILURE" \
					     "bpf_sim resulted in $action"
				stats_failure=$(($stats_failure+1))
			else
				print_result $testnumstr "SUCCESS" ""
				stats_success=$(($stats_success+1))
			fi
			stats_all=$(($stats_all+1))

			subtestnum=$(($subtestnum+1))
		done # syscall
		done # arg0
		done # arg1
		done # arg2
		done # arg3
		done # arg4
		done # arg5
	done # architecture
}

#
# Run the specified "basic" test
#
# Tests that belong to the "basic" test type will simply have the command
# specified in the input batch file.  The command must return zero for success
# and non-zero for failure.
#
# Arguments:
#     1    value of test number from batch file
#     2    string containing line of test data from batch file
#
function run_test_basic() {
	local rc
	local cmd

	# if the test is a script, only run it in native/c mode
	if [[ $mode != "c" && "$2" == *.sh ]]; then
		print_result "$1" "SKIPPED" "(only valid in native/c mode)"
		stats_skipped=$(($stats_skipped+1))
		return
	fi

	# print out the input test data to the log file
	print_data "$1" "$2"

	# check and adjust if we are doing a VPATH build
	if [[ -x "./$2" ]]; then
		cmd="./$2"
	else
		cmd="${srcdir}/$2"
	fi

	# run the command
	run_test_command "$1" "$cmd" "" "" ""
	rc=$?
	if [[ $rc -ne 0 ]]; then
		print_result $1 "FAILURE" "$2 rc=$rc"
		stats_failure=$(($stats_failure+1))
	else
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	fi
	stats_all=$(($stats_all+1))
}

#
# Run the specified "bpf-valgrind" test
#
# Tests that belong to the "bpf-valgrind" test type generate a BPF filter
# while running under valgrind to detect any memory errors.
#
# Arguments:
#     1    value of test number from batch file
#     2    string containing line of test data from batch file
#
function run_test_bpf_valgrind() {
	local rc

	# we only support the native/c test mode here
	if [[ $mode != "c" ]]; then
		print_result "$1" "SKIPPED" "(only valid in native/c mode)"
		stats_skipped=$(($stats_skipped+1))
		return
	fi

	# print out the input test data to the log file
	print_data "$1" "$2"

	# build the command
	testvalgrind="valgrind \
		       --tool=memcheck \
		       --error-exitcode=1 \
		       --leak-check=full \
		       --read-var-info=yes \
		       --track-origins=yes \
		       --suppressions=$basedir/valgrind_test.supp"
	if [[ -n $logfile ]]; then
		testvalgrind+=" --log-fd=$logfd"
	fi
	if [[ -z $verbose ]]; then
		testvalgrind+=" --quiet --log-fd=4"
	fi

	# run the command
	exec 4>/dev/null
	print_valgrind "$1"
	run_test_command "$1" "$testvalgrind --" "./$2 -b" 4 2
	rc=$?
	exec 4>&-
	if [[ $rc -ne 0 ]]; then
		print_result $1 "FAILURE" "$2 rc=$rc"
		stats_failure=$(($stats_failure+1))
	else
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	fi
	stats_all=$(($stats_all+1))
}

#
# Run the specified "live" test
#
# Tests that belong to the "live" test type will attempt to run a live test
# of the libseccomp library on the host system; for obvious reasons the host
# system must support seccomp mode 2 for this to work correctly.
#
# Arguments:
#     1    value of test number from batch file
#     2    string containing line of test data from batch file
#
function run_test_live() {
	local rc
	local api
	local line=($2)

	# parse the test line
	line_cmd=${line[0]}
	line_api=${line[1]}
	line_act=${line[2]}
	line_test="$line_cmd $line_api $line_act"

	# check the api level
	api=$($GLBL_SYS_API)
	if [[ $api -lt $line_api ]]; then
		# runtime api level is too low
		print_result "$1" "SKIPPED" "(api level)"
		stats_skipped=$(($stats_skipped+1))
		return
	fi

	# print out the input test data to the log file
	print_data "$1" "$2"

	# run the command
	exec 4>/dev/null
	run_test_command "$1" "./$line_cmd" "$line_act" "" 4
	rc=$?
	exec 4>&-
	stats_all=$(($stats_all+1))

	# setup the arch specific return values
	case "$arch" in
	x86|x86_64|x32|arm|aarch64|parisc|parisc64|ppc|ppc64|ppc64le|ppc|s390|s390x|riscv64)
		rc_kill_process=159
		rc_kill=159
		rc_allow=160
		rc_trap=161
		rc_trace=162
		rc_errno=163
		rc_log=164
		;;
	mips|mipsel|mips64|mips64n32|mipsel64|mipsel64n32)
		rc_kill_process=140
		rc_kill=140
		rc_allow=160
		rc_trap=161
		rc_trace=162
		rc_errno=163
		rc_log=164
		;;
	*)
		print_result $testnumstr "ERROR" "arch $arch not supported"
		stats_error=$(($stats_error+1))
		return
		;;
	esac

	# verify the results
	if [[ $line_act == "KILL_PROCESS" && $rc -eq $rc_kill_process ]]; then
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	elif [[ $line_act == "KILL" && $rc -eq $rc_kill ]]; then
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	elif [[ $line_act == "ALLOW" && $rc -eq $rc_allow ]]; then
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	elif [[ $line_act == "TRAP" && $rc -eq $rc_trap ]]; then
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	elif [[ $line_act == "TRACE" ]]; then
		print_result $1 "ERROR" "unsupported action \"$line_act\""
		stats_error=$(($stats_error+1))
	elif [[ $line_act == "ERRNO" && $rc -eq $rc_errno ]]; then
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	elif [[ $line_act == "LOG" && $rc -eq $rc_log ]]; then
		print_result $1 "SUCCESS" ""
		stats_success=$(($stats_success+1))
	else
		print_result $1 "FAILURE" "$line_test rc=$rc"
		stats_failure=$(($stats_failure+1))
	fi
}

#
# Run a single test from the specified batch
#
# Arguments:
#     1    string containing the batch name
#     2    value of test number from batch file
#     3    string containing line of test data from batch file
#     4    string containing test type that this test belongs to
#
function run_test() {
	# generate the test number string for the line of batch test data
	local testnumstr=$(generate_test_num "$1" $2 1)

	# ensure we only run tests which match the specified type
	match_csv_word "$type" "$4"
	local type_match=$?
	[[ -n $type && $type_match -eq 1 ]] && return

	# execute the function corresponding to the test type
	if [[ "$4" == "basic" ]]; then
		run_test_basic "$testnumstr" "$3"
	elif [[ "$4" == "bpf-sim" ]]; then
		run_test_bpf_sim "$1" $2 "$3"
	elif [[ "$4" == "bpf-sim-fuzz" ]]; then
		run_test_bpf_sim_fuzz "$1" $2 "$3"
	elif [[ "$4" == "bpf-valgrind" ]]; then
		# only run this test if valgrind is installed
		if check_deps valgrind; then
			run_test_bpf_valgrind "$testnumstr" "$3"
		else
			print_result $testnumstr "SKIPPED" \
				"(valgrind not installed)"
			stats_skipped=$(($stats_skipped+1))
		fi
	elif [[ "$4" == "live" ]]; then
		# only run this test if explicitly requested
		if [[ -n $type ]]; then
			run_test_live "$testnumstr" "$3"
		else
			print_result $testnumstr "SKIPPED" \
				"(must specify live tests)"
			stats_skipped=$(($stats_skipped+1))
		fi
	else
		print_result $testnumstr "ERROR" "test type $4 not supported"
		stats_error=$(($stats_error+1))
	fi
}

#
# Run the requested tests
#
function run_tests() {
	# loop through all test files
	for file in $basedir/*.tests; do
		local testnum=1
		local batch_requested=false
		local batch_name=""

		# extract the batch name from the file name
		batch_name=$(basename $file .tests)

		# check if this batch was requested
		if [[ ${batch_list[@]} ]]; then
			for b in ${batch_list[@]}; do
				if [[ $b == $batch_name ]]; then
					batch_requested=true
					break
				fi
			done
			if ! $batch_requested; then
				continue
			fi
		fi

		# print a test batch header
		echo " batch name: $batch_name" >&$logfd

		# loop through each line and run the requested tests
		while read line; do
			# strip whitespace, comments, and blank lines
			line=$(echo "$line" | \
			       sed -e 's/^[\t ]*//;s/[\t ]*$//;' | \
			       sed -e '/^[#].*$/d;/^$/d')
			if [[ -z $line ]]; then
				continue
			fi

			if [[ $line =~ ^"test type": ]]; then
				test_type=$(echo "$line" | \
					    sed -e 's/^test type: //;')
				# print a test mode and type header
				echo " test mode:  $mode" >&$logfd
				echo " test type:  $test_type" >&$logfd
				continue
			fi

			if [[ ${single_list[@]} ]]; then
				for i in ${single_list[@]}; do
					if [ $i -eq $testnum ]; then
						# we're running a single test
						run_test "$batch_name" \
							 $testnum "$line" \
							 "$test_type"
					fi
				done
			else
				# we're running a test from a batch
				run_test "$batch_name" \
					 $testnum "$line" "$test_type"
			fi
			testnum=$(($testnum+1))
		done < "$file"
	done
}

####
# main

# verify general script dependencies
verify_deps head
verify_deps sed
verify_deps awk
verify_deps tr

# global variables
declare -a batch_list
declare -a single_list
arch=
batch_count=0
logfile=
logfd=
mode_list=""
runall=
singlecount=0
tmpfile=""
tmpdir=""
type=
verbose=
stats_all=0
stats_skipped=0
stats_success=0
stats_failure=0
stats_error=0

# set the test root directory
basedir=$(dirname $0)

# set the test harness pid
pid=$$

# parse the command line
while getopts "ab:gl:m:s:t:T:vh" opt; do
	case $opt in
	a)
		runall=1
		;;
	b)
		batch_list[batch_count]="$OPTARG"
		batch_count=$(($batch_count+1))
		;;
	l)
		logfile="$OPTARG"
		;;
	m)
		case $OPTARG in
		c)
			mode_list="$mode_list c"
			;;
		python)
			verify_deps python
			mode_list="$mode_list python"
			;;
		*)
			usage
			exit 1
		esac
		;;
	s)
		single_list[single_count]=$OPTARG
		single_count=$(($single_count+1))
		;;
	t)
		tmpdir="$OPTARG"
		;;
	T)
		type="$OPTARG"
		;;
	v)
		verbose=1
		;;
	h|*)
		usage
		exit 1
		;;
	esac
done

# use mode list from environment if provided
[[ -z $mode_list && -n $LIBSECCOMP_TSTCFG_MODE_LIST ]] && mode_list=$LIBSECCOMP_TSTCFG_MODE_LIST

# determine the mode test automatically
if [[ -z $mode_list ]]; then
	# always perform the native c tests
	mode_list="c"

	# query the build configuration
	if [[ -r "../configure.h" ]]; then
		# python tests
		[[ "$(grep "ENABLE_PYTHON" ../configure.h | \
		   awk '{ print $3 }')" = "1" ]] && \
			mode_list="$mode_list python"
	fi
fi

# check if we specified a list of tests via the environment variable
if [[ -n $LIBSECCOMP_TSTCFG_BATCHES ]]; then
	for i in $(echo "$LIBSECCOMP_TSTCFG_BATCHES" | sed 's/,/ /g'); do
		batch_list[batch_count]="$i"
		batch_count=$(($batch_count+1))
	done
fi

# default to all tests if batch or single tests not requested
if [[ -z $batch_list ]] && [[ -z $single_list ]]; then
	runall=1
fi

# drop any requested batch and single tests if all tests were requested
if [[ -n $runall ]]; then
	batch_list=()
	single_list=()
fi

# check for configuration via environment variables
[[ -z $type && -n $LIBSECCOMP_TSTCFG_TYPE ]] && type=$LIBSECCOMP_TSTCFG_TYPE

# open log file for append (default to stdout)
if [[ -n $logfile ]]; then
	logfd=3
	exec 3>>"$logfile"
else
	logfd=1
fi

# open temporary file
if [[ -n $tmpdir ]]; then
	tmpfile=$(mktemp -t regression_XXXXXX --tmpdir=$tmpdir)
else
	tmpfile=$(mktemp -t regression_XXXXXX)
fi

# determine the current system's architecture
arch=$($GLBL_SYS_ARCH)

# display the test output and run the requested tests
echo "=============== $(date) ===============" >&$logfd
echo "Regression Test Report (\"regression $*\")" >&$logfd
for mode in $mode_list; do
	run_tests
done
echo "Regression Test Summary" >&$logfd
echo " tests run: $stats_all" >&$logfd
echo " tests skipped: $stats_skipped" >&$logfd
echo " tests passed: $stats_success" >&$logfd
echo " tests failed: $stats_failure" >&$logfd
echo " tests errored: $stats_error" >&$logfd
echo "============================================================" >&$logfd

# cleanup and exit
rm -f $tmpfile
rc=0
[[ $stats_failure -gt 0 ]] && rc=$(($rc + 2))
[[ $stats_error -gt 0 ]] && rc=$(($rc + 4))

exit $rc
