#!/usr/bin/env bash
# Bash functions to run an executable for testing.
#
# Version: 20231119
#
# When CHECK_WITH_ASAN is set to a non-empty value the test executable
# is run with asan, otherwise it is run without.
#
# When CHECK_WITH_GDB is set to a non-empty value the test executable
# is run with gdb, otherwise it is run without.
#
# When CHECK_WITH_STDERR is set to a non-empty value the test executable
# is run with error output to stderr.
#
# When CHECK_WITH_VALGRIND is set to a non-empty value the test executable
# is run with valgrind, otherwise it is run without.
#
# PYTHON is used to determine the Python interpreter.

EXIT_SUCCESS=0;
EXIT_FAILURE=1;
EXIT_IGNORE=77;

# Checks the availability of a binary and exits if not available.
#
# Arguments:
#   a string containing the name of the binary
#
assert_availability_binary()
{
	local BINARY=$1;

	which ${BINARY} > /dev/null 2>&1;
	if test $? -ne ${EXIT_SUCCESS};
	then
		echo "Missing binary: ${BINARY}";
		echo "";

		exit ${EXIT_FAILURE};
	fi
}

# Checks the availability of binaries and exits if not available.
#
# Globals:
#   CHECK_WITH_GDB
#   CHECK_WITH_VALGRIND
#
assert_availability_binaries()
{
	assert_availability_binary cat;
	assert_availability_binary cut;
	assert_availability_binary diff;
	assert_availability_binary file;
	assert_availability_binary gzip;
	assert_availability_binary ls;
	assert_availability_binary readlink;
	assert_availability_binary sed;
	assert_availability_binary tr;
	assert_availability_binary uname;
	assert_availability_binary wc;
	assert_availability_binary zcat;

	if test -n "${CHECK_WITH_GDB}";
	then
		assert_availability_binary gdb;

	elif test -n "${CHECK_WITH_VALGRIND}";
	then
		assert_availability_binary valgrind;
	fi
}

# Checks if the test set is in the ignore list.
#
# Arguments:
#   a string containing the test set
#   a string containing space separated ignore list
#
# Returns:
#   an integer containing the exit status to indicate the input directory
#   was found in the ignore list.
#
check_for_test_set_in_ignore_list()
{
	local TEST_SET=$1;
	local IGNORE_LIST=$2;

	local IFS=" ";

	for LIST_ELEMENT in ${IGNORE_LIST};
	do
		if test "${LIST_ELEMENT}" = "${TEST_SET}";
		then
			return ${EXIT_SUCCESS};
		fi
	done
	return ${EXIT_FAILURE};
}

# Checks if the input directory is in the ignore list.
#
# Arguments:
#   a string containing the path of the test input directory
#   a string containing space separated ignore list
#
# Returns:
#   an integer containing the exit status to indicate the input directory
#   was found in the ignore list.
#
check_for_directory_in_ignore_list()
{
	local INPUT_DIRECTORY=$1;
	local IGNORE_LIST=$2;

	local INPUT_BASENAME=`basename ${INPUT_DIRECTORY}`;

	local IFS=" ";

	for LIST_ELEMENT in ${IGNORE_LIST};
	do
		if test "${LIST_ELEMENT}" = "${INPUT_BASENAME}";
		then
			return ${EXIT_SUCCESS};
		fi
	done
	return ${EXIT_FAILURE};
}

# Searches for the binary variant of the executable in case the test executable
# refers to a libtool shell script.
#
# Arguments:
#   a string containing the path of the test executable
#
# Returns:
#   a string containing the path of the binary variant of the test executable
#
find_binary_executable()
{
	local TEST_EXECUTABLE=$1;

	TEST_EXECUTABLE=$( readlink_f "${TEST_EXECUTABLE}" );

	# Note that the behavior of `file -bi` is not helpful on Mac OS X.
	local EXECUTABLE_TYPE=`file -b ${TEST_EXECUTABLE}`;

	# Check if the test executable is a libtool shell script.
	# Linux: POSIX shell script, ASCII text executable, with very long lines
	# Mac OS X: POSIX shell script text executable
	echo "${EXECUTABLE_TYPE}" | grep "POSIX shell script" > /dev/null 2>&1;
	RESULT=$?;

	if test ${RESULT} -eq ${EXIT_SUCCESS};
	then
		local TEST_EXECUTABLE_BASENAME=`basename ${TEST_EXECUTABLE}`;
		local TEST_EXECUTABLE_DIRNAME=`dirname ${TEST_EXECUTABLE}`;

		TEST_EXECUTABLE="${TEST_EXECUTABLE_DIRNAME}/.libs/${TEST_EXECUTABLE_BASENAME}";

		if test -x ${TEST_EXECUTABLE};
		then
			# Note that the behavior of `file -bi` is not helpful on Mac OS X.
			EXECUTABLE_TYPE=`file -b ${TEST_EXECUTABLE}`;

			# Linux: ELF 64-bit LSB executable, x86-64, ...
			# Mac OS X: Mach-O 64-bit executable x86_64
			echo "${EXECUTABLE_TYPE}" | grep "executable" > /dev/null 2>&1;
			RESULT=$?;

			if test ${RESULT} -ne ${EXIT_SUCCESS};
			then
				echo "Invalid test executable: ${TEST_EXECUTABLE}";

				exit ${EXIT_FAILURE};
			fi
		fi
	fi
	echo ${TEST_EXECUTABLE};
}

# Searches for the path to the binary variant of the library.
#
# Arguments:
#   a string containing the path of the test executable
#
# Returns:
#   a string containing the path of the binary variant of the library.
#
find_binary_library_path()
{
	local TEST_EXECUTABLE=$1;
	local LIBRARY_NAME=`dirname ${TEST_EXECUTABLE}`;

	local NAME=`basename ${LIBRARY_NAME}`;

	if test ${NAME} = ".libs";
	then
		LIBRARY_NAME=`dirname ${LIBRARY_NAME}`;
		NAME=`basename ${LIBRARY_NAME}`;
	fi
	if test ${NAME} = "tests";
	then
		LIBRARY_NAME=`dirname ${LIBRARY_NAME}`;
		NAME=`basename ${LIBRARY_NAME}`;
	fi
	echo ${LIBRARY_NAME} | grep 'tools' > /dev/null 2>&1;

	if test $? -eq ${EXIT_SUCCESS};
	then
		LIBRARY_NAME=`basename ${LIBRARY_NAME} | sed 's/\(.*\)tools$/lib\1/'`;
	else
		LIBRARY_NAME=`basename ${LIBRARY_NAME} | sed 's/^py//' | sed 's/^\([^_]*\)_test_.*$/lib\1/'`;
	fi
	TEST_EXECUTABLE=`dirname ${TEST_EXECUTABLE}`;
	TEST_EXECUTABLE=`dirname ${TEST_EXECUTABLE}`;
	TEST_EXECUTABLE=`dirname ${TEST_EXECUTABLE}`;

	local LIBRARY_PATH="${TEST_EXECUTABLE}/${LIBRARY_NAME}/.libs";

	if ! test -d "${LIBRARY_PATH}";
	then
		LIBRARY_PATH="../${LIBRARY_NAME}/.libs";
	fi
	echo "${LIBRARY_PATH}";
}

# Searches for the path to the binary variant of the Python module
#
# Arguments:
#   a string containing the path of the test executable
#
# Returns:
#   a string containing the path of the binary variant of the Python module
#
find_binary_python_module_path()
{
	local TEST_EXECUTABLE=$1;

	local PYTHON_MODULE_NAME=`basename ${TEST_EXECUTABLE} | sed 's/^py\(.*\)_test_.*$/py\1/'`;

	TEST_EXECUTABLE=`dirname ${TEST_EXECUTABLE}`;
	TEST_EXECUTABLE=`dirname ${TEST_EXECUTABLE}`;
	TEST_EXECUTABLE=`dirname ${TEST_EXECUTABLE}`;

	local PYTHON_MODULE_PATH="${TEST_EXECUTABLE}/${PYTHON_MODULE_NAME}/.libs";

	if ! test -d "${PYTHON_MODULE_PATH}";
	then
		PYTHON_MODULE_PATH="../${PYTHON_MODULE_NAME}/.libs";
	fi
	echo "${PYTHON_MODULE_PATH}";
}

# Determines the test data option file.
#
# Arguments:
#   a string containing the path of the test set directory
#   a string containing the path of the test input file
#   a string containing the name of the test data option set
#
# Returns:
#   a string containing the test input files
#
get_test_data_option_file()
{
	local TEST_SET_DIRECTORY=$1;
	local INPUT_FILE=$2;
	local OPTION_SET=$3;

	local INPUT_NAME=`basename "${INPUT_FILE}"`;
	local TEST_DATA_OPTION_FILE="${TEST_SET_DIRECTORY}/${INPUT_NAME}.${OPTION_SET}";

	echo "${TEST_DATA_OPTION_FILE}";
}

# Determines the test profile directory.
# The directory is created if it does not exist.
#
# Arguments:
#   a string containing the path of the test input directory
#   a string containing the name of the test profile
#
# Returns:
#   a string containing the path of the test profile directory
#
get_test_profile_directory()
{
	local TEST_INPUT_DIRECTORY=$1;
	local TEST_PROFILE=$2;

	local TEST_PROFILE_DIRECTORY="${TEST_INPUT_DIRECTORY}/.${TEST_PROFILE}";

	if ! test -d "${TEST_PROFILE_DIRECTORY}";
	then
		mkdir "${TEST_PROFILE_DIRECTORY}";
	fi
	echo "${TEST_PROFILE_DIRECTORY}";
}

# Determines the test set directory.
# The directory is created if it does not exist.
#
# Arguments:
#   a string containing the path of the test profile directory
#   a string containing the path of the test set input directory
#
# Returns:
#   a string containing the path of the test set directory
#
get_test_set_directory()
{
	local TEST_PROFILE_DIRECTORY=$1;
	local TEST_SET_INPUT_DIRECTORY=$2;

	local TEST_SET=`basename ${TEST_SET_INPUT_DIRECTORY}`;
	local TEST_SET_DIRECTORY="${TEST_PROFILE_DIRECTORY}/${TEST_SET}";

	if ! test -d "${TEST_SET_DIRECTORY}";
	then
		mkdir "${TEST_SET_DIRECTORY}";
	fi
	echo "${TEST_SET_DIRECTORY}";
}

# Provides a cross-platform variant of "readlink -f"
#
# Arguments:
#   a string containing a path
#
# Returns:
#   a string containing the path with all symbolic links resolved
#
readlink_f() {
	local TARGET="$1"

	if test -f "${TARGET}";
	then
		while test -L "${TARGET}";
		do
			TARGET=`readlink "${TARGET}"`;
		done
	fi
	local BASENAME=`basename "${TARGET}"`;
	local DIRNAME=`dirname "${TARGET}"`;

	DIRNAME=`(cd "${DIRNAME}" && pwd -P)`;

	echo "${DIRNAME}/${BASENAME}";
}

# Reads the test profile ignore file if it exists.
#
# Arguments:
#   a string containing the path of the test profile directory
#
# Returns:
#   a string containing a space separated ignore list
#
read_ignore_list()
{
	local TEST_PROFILE_DIRECTORY=$1;
	local IGNORE_FILE="${TEST_PROFILE_DIRECTORY}/ignore";
	local IGNORE_LIST="";

	if test -f "${IGNORE_FILE}";
	then
		IGNORE_LIST=`cat ${IGNORE_FILE} | sed '/^#/d' | tr '\n' ' '`;
	fi
	echo ${IGNORE_LIST};
}

# Reads a test data option file.
#
# Arguments:
#   a string containing the path of the test set directory
#   a string containing the path of the test input file
#   a string containing the name of the test data option set
#
# Returns:
#   a string containing the test data specific options
#
read_test_data_option_file()
{
	local TEST_SET_DIRECTORY=$1;
	local INPUT_FILE=$2;
	local OPTION_SET=$3;

	local TEST_DATA_OPTION_FILE="${INPUT_FILE}.${OPTION_SET}";

	if ! test -f "${TEST_DATA_OPTION_FILE}";
	then
		TEST_DATA_OPTION_FILE=$(get_test_data_option_file "${TEST_SET_DIRECTORY}" "${INPUT_FILE}" "${OPTION_SET}");
	fi

	local OPTIONS_STRING=`head -n 1 "${TEST_DATA_OPTION_FILE}" | sed 's/[\r\n]*$//'`;

	if test "${OPTIONS_STRING}" = "# libyal test data options";
	then
		OPTIONS_STRING=`tail -n +2 "${TEST_DATA_OPTION_FILE}" | sed 's/^offset=/-o/;s/^password=/-p/;s/^recovery_password=/-r/;s/^startup_key=/-s/;s/virtual_address=/-v/' | tr '\n' ' '`;
	fi
	echo "${OPTIONS_STRING}";
}

# Runs the test with optional arguments.
#
# Globals:
#   CHECK_WITH_ASAN
#   CHECK_WITH_GDB
#   CHECK_WITH_STDERR
#   CHECK_WITH_VALGRIND
#
# Arguments:
#   a string containing the test description
#   a string containing the path of the test executable
#   an array containing the arguments for the test executable
#
# Returns:
#   an integer containg the exit status of the test executable
#
run_test_with_arguments()
{
	local TEST_DESCRIPTION=$1;
	local TEST_EXECUTABLE=$2;
	shift 2;
	local ARGUMENTS=("$@");

	if ! test -f "${TEST_EXECUTABLE}";
	then
		echo "Missing test executable: ${TEST_EXECUTABLE}";
		echo "";

		return ${EXIT_FAILURE};
	fi
	local PLATFORM=`uname -s | sed 's/-.*$//'`;

	# Note that the behavior of `file -bi` is not helpful on Mac OS X.
	local EXECUTABLE_TYPE=`file -b ${TEST_EXECUTABLE}`;

	# Check if the test executable is a Python script.
	# Linux: Python script, ASCII text executable
	# Mac OS X: a python script text executable
	echo "${EXECUTABLE_TYPE}" | grep -i "python script" > /dev/null 2>&1;
	local IS_PYTHON_SCRIPT=$?;

	if test ${IS_PYTHON_SCRIPT} -eq 0 && test -z ${PYTHON};
	then
		local PYTHON=`which python 2> /dev/null`;

		if ! test -x ${PYTHON};
		then
			echo "Missing executable: ${PYTHON}";

			exit ${EXIT_FAILURE};
		fi
	fi
	local RESULT=0;

	if test -n "${CHECK_WITH_ASAN}";
	then
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		local LSAN_SUPPRESSIONS="lsan.suppressions";

		if ! test -f ${LSAN_SUPPRESSIONS};
		then
			LSAN_SUPPRESSIONS="../lsan.suppressions";
		fi
		if test "${PLATFORM}" = "Darwin";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" DYLD_LIBRARY_PATH="${LIBRARY_PATH}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi
		else
			local CONFIG_LOG="../config.log";

			if ! test -f ${CONFIG_LOG};
			then
				CONFIG_LOG="../../config.log";
			fi
			local CC=`cat ${CONFIG_LOG} | grep -e "^CC=" | sed "s/CC='\\(.*\\)'/\1/"`;
			local LIBASAN="";

			if test -z ${CC} || test ${CC} != "clang";
			then
				local LDCONFIG=`which ldconfig 2> /dev/null`;

				if test -z ${LDCONFIG} || ! test -x ${LDCONFIG};
				then
					LDCONFIG="/sbin/ldconfig";
				fi
				if test -z ${LDCONFIG} || ! test -x ${LDCONFIG};
				then
					echo "Missing binary: ldconfig";
					echo "";

					exit ${EXIT_FAILURE};
				fi
				LIBASAN=`${LDCONFIG} -p | grep libasan | sed 's/^.* => //' | sort | tail -n 1`;

				if ! test -f ${LIBASAN};
				then
					echo "Missing library: ${BINARY}";
					echo "";

					exit ${EXIT_FAILURE};
				fi
			fi
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" LD_PRELOAD="${LIBASAN}" LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" LD_PRELOAD="${LIBASAN}" LD_LIBRARY_PATH="${LIBRARY_PATH}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi
		fi

	elif test -n "${CHECK_WITH_GDB}";
	then
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		if test "${PLATFORM}" = "Darwin";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi

		elif test "${PLATFORM}" = "CYGWIN_NT";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				PATH="${LIBRARY_PATH}:${PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi

		else
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				LD_LIBRARY_PATH="${LIBRARY_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi
		fi

	elif test -n "${CHECK_WITH_VALGRIND}";
	then
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		local VALGRIND_LOG="valgrind.log-$$";
		local VALGRIND_OPTIONS=("--tool=memcheck" "--leak-check=full" "--show-leak-kinds=definite,indirect,possible" "--track-origins=yes" "--log-file=${VALGRIND_LOG}");

		if test "${PLATFORM}" = "Darwin";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi

		elif test "${PLATFORM}" = "CYGWIN_NT";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				PATH="${LIBRARY_PATH}:${PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi

		else
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				LD_LIBRARY_PATH="${LIBRARY_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			fi
		fi
		if test ${RESULT} -eq ${EXIT_SUCCESS};
		then
			grep "All heap blocks were freed -- no leaks are possible" ${VALGRIND_LOG} > /dev/null 2>&1;

			if test $? -ne ${EXIT_SUCCESS};
			then
				# Ignore "still reachable"
				# Also see: http://valgrind.org/docs/manual/faq.html#faq.deflost

				grep "definitely lost: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_DIRECTLY_LOST=$?;

				grep "indirectly lost: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_INDIRECTLY_LOST=$?;

				grep "possibly lost: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_POSSIBLY_LOST=$?;

				grep "suppressed: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_SUPPRESSED=$?;

				if test ${RESULT_DIRECTLY_LOST} -ne ${EXIT_SUCCESS} || test ${RESULT_INDIRECTLY_LOST} -ne ${EXIT_SUCCESS} || test ${RESULT_POSSIBLY_LOST} -ne ${EXIT_SUCCESS} || test ${RESULT_SUPPRESSED} -ne ${EXIT_SUCCESS};
				then
					echo "Memory leakage detected.";
					cat ${VALGRIND_LOG};

					RESULT=${EXIT_FAILURE};
				fi
			fi
			# Detect valgrind warnings.
			local NUMBER_OF_LINES=`wc -l ${VALGRIND_LOG} | awk '{ print $1 }'`;

			if test ${NUMBER_OF_LINES} -ne 15 && test ${NUMBER_OF_LINES} -ne 22;
			then
				echo "Unsupported number of lines: ${NUMBER_OF_LINES}";
				cat ${VALGRIND_LOG};

				RESULT=${EXIT_FAILURE};
			fi
		fi
		rm -f ${VALGRIND_LOG};

	elif test ${IS_PYTHON_SCRIPT} -eq 0;
	then
		if ! test -f "${TEST_EXECUTABLE}";
		then
			echo "Invalid test Python script: ${TEST_EXECUTABLE}";
			echo "";

			return ${EXIT_FAILURE};
		fi
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		if test "${PLATFORM}" = "Darwin";
		then
			if test -n "${CHECK_WITH_STDERR}";
			then
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} 2> /dev/null;
				RESULT=$?;
			fi

		elif test "${PLATFORM}" = "CYGWIN_NT";
		then
			if test -n "${CHECK_WITH_STDERR}";
			then
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} 2> /dev/null;
				RESULT=$?;
			fi

		else
			if test -n "${CHECK_WITH_STDERR}";
			then
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]};
				RESULT=$?;
			else
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} 2> /dev/null;
				RESULT=$?;
			fi
		fi
	else
		if ! test -x "${TEST_EXECUTABLE}";
		then
			echo "Invalid test executable: ${TEST_EXECUTABLE}";
			echo "";

			return ${EXIT_FAILURE};
		fi

		if test -n "${CHECK_WITH_STDERR}";
		then
			${TEST_EXECUTABLE} ${ARGUMENTS[@]};
			RESULT=$?;
		else
			${TEST_EXECUTABLE} ${ARGUMENTS[@]} 2> /dev/null;
			RESULT=$?;
		fi
	fi
	if test -n "${TEST_DESCRIPTION}";
	then
		echo -n "${TEST_DESCRIPTION}";

		if test ${RESULT} -ne ${EXIT_SUCCESS};
		then
			echo " (FAIL)";
		else
			echo " (PASS)";
		fi
	fi
	return ${RESULT};
}

# Runs the test with an input file and optional arguments.
#
# Globals:
#   CHECK_WITH_ASAN
#   CHECK_WITH_GDB
#   CHECK_WITH_STDERR
#   CHECK_WITH_VALGRIND
#
# Arguments:
#   a string containing the path of the test executable
#   a string containing the path of the test input file
#   an array containing the arguments for the test executable
#
# Returns:
#   an integer containg the exit status of the test executable
#
run_test_with_input_and_arguments()
{
	local TEST_EXECUTABLE=$1;
	local INPUT_FILE=$2;
	shift 2;
	local ARGUMENTS=("$@");

	if ! test -f "${TEST_EXECUTABLE}";
	then
		echo "Missing test executable: ${TEST_EXECUTABLE}";
		echo "";

		return ${EXIT_FAILURE};
	fi
	local PLATFORM=`uname -s | sed 's/-.*$//'`;

	# Note that the behavior of `file -bi` is not helpful on Mac OS X.
	local EXECUTABLE_TYPE=`file -b ${TEST_EXECUTABLE}`;

	# Check if the test executable is a Python script.
	# Linux: Python script, ASCII text executable
	# Mac OS X: a python script text executable
	echo "${EXECUTABLE_TYPE}" | grep -i "python script" > /dev/null 2>&1;
	local IS_PYTHON_SCRIPT=$?;

	if test ${IS_PYTHON_SCRIPT} -eq 0 && test -z ${PYTHON};
	then
		local PYTHON=`which python 2> /dev/null`;

		if ! test -x ${PYTHON};
		then
			echo "Missing executable: ${PYTHON}";

			exit ${EXIT_FAILURE};
		fi
	fi
	local RESULT=0;

	if test -n "${CHECK_WITH_ASAN}";
	then
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		local LSAN_SUPPRESSIONS="lsan.suppressions";

		if ! test -f ${LSAN_SUPPRESSIONS};
		then
			LSAN_SUPPRESSIONS="../lsan.suppressions";
		fi
		if test "${PLATFORM}" = "Darwin";
		then
			# TODO DYLD_INSERT_LIBRARIES=/Library/Developer/CommandLineTools/usr/lib/clang/8.1.0/lib/darwin/libclang_rt.asan_osx_dynamic.dylib
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" DYLD_LIBRARY_PATH="${LIBRARY_PATH}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi
		else
			local CONFIG_LOG="../config.log";

			if ! test -f ${CONFIG_LOG};
			then
				CONFIG_LOG="../../config.log";
			fi
			local CC=`cat ${CONFIG_LOG} | grep -e "^CC=" | sed "s/CC='\\(.*\\)'/\1/"`;
			local LIBASAN="";

			if test -z ${CC} || test ${CC} != "clang";
			then
				local LDCONFIG=`which ldconfig 2> /dev/null`;

				if test -z ${LDCONFIG} || ! test -x ${LDCONFIG};
				then
					LDCONFIG="/sbin/ldconfig";
				fi
				if test -z ${LDCONFIG} || ! test -x ${LDCONFIG};
				then
					echo "Missing binary: ldconfig";
					echo "";

					exit ${EXIT_FAILURE};
				fi
				LIBASAN=`${LDCONFIG} -p | grep libasan | sed 's/^.* => //' | sort | tail -n 1`;

				if ! test -f ${LIBASAN};
				then
					echo "Missing library: ${BINARY}";
					echo "";

					exit ${EXIT_FAILURE};
				fi
			fi
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" LD_PRELOAD="${LIBASAN}" LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				LSAN_OPTIONS=suppressions="${LSAN_SUPPRESSIONS}" LD_PRELOAD="${LIBASAN}" LD_LIBRARY_PATH="${LIBRARY_PATH}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi
		fi

	elif test -n "${CHECK_WITH_GDB}";
	then
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		if test "${PLATFORM}" = "Darwin";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi

		elif test "${PLATFORM}" = "CYGWIN_NT";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				PATH="${LIBRARY_PATH}:${PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi

		else
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				LD_LIBRARY_PATH="${LIBRARY_PATH}" gdb -ex "set non-stop on" -ex "run" -ex "quit" --args "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi
		fi

	elif test -n "${CHECK_WITH_VALGRIND}";
	then
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		local VALGRIND_LOG="valgrind.log-$$";
		local VALGRIND_OPTIONS=("--tool=memcheck" "--leak-check=full" "--show-leak-kinds=definite,indirect,possible" "--track-origins=yes" "--log-file=${VALGRIND_LOG}");

		if test "${PLATFORM}" = "Darwin";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi

		elif test "${PLATFORM}" = "CYGWIN_NT";
		then
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				PATH="${LIBRARY_PATH}:${PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi

		else
			if test ${IS_PYTHON_SCRIPT} -eq 0;
			then
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				LD_LIBRARY_PATH="${LIBRARY_PATH}" valgrind ${VALGRIND_OPTIONS[@]} "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			fi
		fi
		if test ${RESULT} -eq ${EXIT_SUCCESS};
		then
			grep "All heap blocks were freed -- no leaks are possible" ${VALGRIND_LOG} > /dev/null 2>&1;

			if test $? -ne ${EXIT_SUCCESS};
			then
				# Ignore "still reachable"
				# Also see: http://valgrind.org/docs/manual/faq.html#faq.deflost

				grep "definitely lost: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_DIRECTLY_LOST=$?;

				grep "indirectly lost: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_INDIRECTLY_LOST=$?;

				grep "possibly lost: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_POSSIBLY_LOST=$?;

				grep "suppressed: 0 bytes in 0 blocks" ${VALGRIND_LOG} > /dev/null 2>&1;
				RESULT_SUPPRESSED=$?;

				if test ${RESULT_DIRECTLY_LOST} -ne ${EXIT_SUCCESS} || test ${RESULT_INDIRECTLY_LOST} -ne ${EXIT_SUCCESS} || test ${RESULT_POSSIBLY_LOST} -ne ${EXIT_SUCCESS} || test ${RESULT_SUPPRESSED} -ne ${EXIT_SUCCESS};
				then
					echo "Memory leakage detected.";
					cat ${VALGRIND_LOG};

					RESULT=${EXIT_FAILURE};
				fi
			fi
			# Detect valgrind warnings.
			local NUMBER_OF_LINES=`wc -l ${VALGRIND_LOG} | awk '{ print $1 }'`;

			if test ${NUMBER_OF_LINES} -ne 15 && test ${NUMBER_OF_LINES} -ne 22;
			then
				echo "Unsupported number of lines: ${NUMBER_OF_LINES}";
				cat ${VALGRIND_LOG};

				RESULT=${EXIT_FAILURE};
			fi
		fi
		rm -f ${VALGRIND_LOG};

	elif test ${IS_PYTHON_SCRIPT} -eq 0;
	then
		if ! test -f "${TEST_EXECUTABLE}";
		then
			echo "Invalid test Python script: ${TEST_EXECUTABLE}";
			echo "";

			return ${EXIT_FAILURE};
		fi
		local TEST_EXECUTABLE=$( find_binary_executable ${TEST_EXECUTABLE} );
		local LIBRARY_PATH=$( find_binary_library_path ${TEST_EXECUTABLE} );
		local PYTHON_MODULE_PATH=$( find_binary_python_module_path ${TEST_EXECUTABLE} );

		if test "${PLATFORM}" = "Darwin";
		then
			if test -n "${CHECK_WITH_STDERR}";
			then
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				DYLD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}" 2> /dev/null;
				RESULT=$?;
			fi

		elif test "${PLATFORM}" = "CYGWIN_NT";
		then
			if test -n "${CHECK_WITH_STDERR}";
			then
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				PATH="${LIBRARY_PATH}:${PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}" 2> /dev/null;
				RESULT=$?;
			fi

		else
			if test -n "${CHECK_WITH_STDERR}";
			then
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}";
				RESULT=$?;
			else
				LD_LIBRARY_PATH="${LIBRARY_PATH}" PYTHONPATH="${PYTHON_MODULE_PATH}" "${PYTHON}" "${TEST_EXECUTABLE}" ${ARGUMENTS[@]} "${INPUT_FILE}" 2> /dev/null;
				RESULT=$?;
			fi
		fi
	else
		if ! test -x "${TEST_EXECUTABLE}";
		then
			echo "Invalid test executable: ${TEST_EXECUTABLE}";
			echo "";

			return ${EXIT_FAILURE};
		fi

		if test -n "${CHECK_WITH_STDERR}";
		then
			${TEST_EXECUTABLE} ${ARGUMENTS[@]} "${INPUT_FILE}";
			RESULT=$?;
		else
			${TEST_EXECUTABLE} ${ARGUMENTS[@]} "${INPUT_FILE}" 2> /dev/null;
			RESULT=$?;
		fi
	fi
	return ${RESULT};
}

# Runs the test on the input file.
#
# Note that this function is not intended to be directly invoked
# from outside the test runner script.
#
# Arguments:
#   a string containing the path of the test set directory
#   a string containing the description of the test
#   a string containing the test mode
#   a string containing the name of the test data option set
#   a string containing the path of the test executable
#   a string containing the path of the test input file
#   an array containing the arguments for the test executable
#
# Returns:
#   an integer containg the exit status of the test executable
#
run_test_on_input_file()
{
	local TEST_SET_DIRECTORY=$1;
	local TEST_DESCRIPTION=$2;
	local TEST_MODE=$3;
	local OPTION_SET=$4;
	local TEST_EXECUTABLE=$5;
	local INPUT_FILE=$6;
	shift 6;
	local ARGUMENTS=("$@");

	local INPUT_NAME=`basename "${INPUT_FILE}"`;
	local TEST_OUTPUT="${INPUT_NAME}";

	if test -n "${OPTION_SET}";
	then
		TEST_OUTPUT="${INPUT_NAME}-${OPTION_SET}";
	fi

	local TMPDIR="tmp$$";
	local RESULT=0;

	rm -rf ${TMPDIR};
	mkdir ${TMPDIR};

	if test "${TEST_MODE}" = "with_callback";
	then
		test_callback "${TMPDIR}" "${TEST_SET_DIRECTORY}" "${TEST_OUTPUT}" "${TEST_EXECUTABLE}" "${TEST_INPUT}" ${ARGUMENTS[@]};
		RESULT=$?;

	elif test "${TEST_MODE}" = "with_stdout_reference";
	then
		TEST_EXECUTABLE=$( readlink_f "${TEST_EXECUTABLE}" );

		if ! test -x ${TEST_EXECUTABLE};
		then
			echo "Invalid test executable: ${TEST_EXECUTABLE}";
			echo "";

			return ${EXIT_FAILURE};
		fi
		local INPUT_FILE_FULL_PATH=$( readlink_f "${INPUT_FILE}" );
		local TEST_LOG="${TEST_OUTPUT}.log";

		(cd ${TMPDIR} && run_test_with_input_and_arguments "${TEST_EXECUTABLE}" "${INPUT_FILE_FULL_PATH}" ${ARGUMENTS[@]} > "${TEST_LOG}");
		RESULT=$?;

		# Compare output if test ran successfully.
		if test ${RESULT} -eq ${EXIT_SUCCESS};
		then
			local TEST_RESULTS="${TMPDIR}/${TEST_LOG}";
			local STORED_TEST_RESULTS="${TEST_SET_DIRECTORY}/${TEST_LOG}.gz";

			# Strip header with version.
			sed -i'~' '1,2d' "${TEST_RESULTS}";

			if test -f "${STORED_TEST_RESULTS}";
			then
				# Using zcat here since zdiff has issues on Mac OS X.
				# Note that zcat on Mac OS X requires the input from stdin.
				zcat < "${STORED_TEST_RESULTS}" | diff "${TEST_RESULTS}" -;
				RESULT=$?;
			else
				gzip "${TEST_RESULTS}";

				mv "${TEST_RESULTS}.gz" ${TEST_SET_DIRECTORY};
			fi
		fi

	else
		run_test_with_input_and_arguments "${TEST_EXECUTABLE}" "${INPUT_FILE}" ${ARGUMENTS[@]};
		RESULT=$?;
	fi

	rm -rf ${TMPDIR};

	if test -n "${TEST_DESCRIPTION}";
	then
		ARGUMENTS=`echo "${ARGUMENTS[*]}" | tr '\n' ' ' | sed 's/[ ]\$//'`;

		if test -z "${ARGUMENTS}";
		then
			echo -n "${TEST_DESCRIPTION} with input: ${INPUT_FILE}";
		else
			echo -n "${TEST_DESCRIPTION} with options: '${ARGUMENTS}' and input: ${INPUT_FILE}";
		fi

		if test ${RESULT} -ne ${EXIT_SUCCESS};
		then
			echo " (FAIL)";
		else
			echo " (PASS)";
		fi
	fi
	return ${RESULT};
}

