Blob Blame History Raw
#!/bin/bash
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#

#
# Copyright (c) 2014, Joyent, Inc.
#

#
# catest: a simple testing tool and framework. See usage below for details.
#

shopt -s xpg_echo

#
# Global configuration
#
cat_arg0=$(basename $0)			# canonical name of "catest"
cat_outbase="catest.$$"			# output directory name
cat_tstdir="test"			# test directory

#
# Options and arguments
#
cat_tests=""				# list of tests (absolute paths)
opt_a=false				# run all tests
opt_c=false				# colorize test results
opt_k=false				# keep output of successful tests
opt_o="/var/tmp"			# parent directory for output directory
opt_t=					# TAP format output file
opt_S=false				# Non-strict mode for js tests

#
# Current state
#
cat_outdir=				# absolute path to output directory
cat_tapfile=				# absolute path of TAP output file
cat_ntests=				# total number of tests
cat_nfailed=0				# number of failed tests run
cat_npassed=0				# number of successful tests run
cat_nrun=0				# total number of tests run

#
# Allow environment-specific customizations.
#
[[ -f $(dirname $0)/catest_init.sh ]] && . $(dirname $0)/catest_init.sh

#
# fail MSG: emits the given error message to stderr and exits non-zero.
#
function fail
{
	echo "$cat_arg0: $@" >&2

	[[ -n $cat_tapfile ]] && echo "Bail out! $@" >> $cat_tapfile

	exit 1
}

#
# usage [MSG]: emits the given message, if any, and a usage message, then exits.
#
function usage
{
	[[ $# -ne 0 ]] && echo "$cat_arg0: $@\n" >&2

	cat <<USAGE >&2
Usage: $cat_arg0 [-k] [-c] [-o dir] [-t file] test1 ...
       $cat_arg0 [-k] [-c] [-o dir] [-t file] -a

In the first form, runs specified tests.  In the second form, runs all tests
found under "$cat_tstdir" of the form "tst*.<ext>" for supported extensions.

TESTS

    Tests are just files to be executed by some interpreter. In most cases, a
    test succeeds if it exits successfully and fails otherwise.  You can also
    specify the expected stdout of the test in a file with the same name as the
    test plus a ".out" suffix, in which case the test will also fail if the
    actual output does not match the expected output.

    Supported interpreter extensions are "sh" (bash) and "js" (node).

    This framework does not provide per-test setup/teardown facilities, but
    test files can do whatever they want, including making use of common
    libraries for setup and teardown.

TEST OUTPUT

    Summary output is printed to stdout.  TAP output can be emitted with "-t".

    Per-test output is placed in a new temporary directory inside the directory
    specified by the -o option, or /var/tmp if -o is not specified.

    Within the output directory will be a directory for each failed test which
    includes a README describing why the test failed (e.g., exited non-zero), a
    copy of the test file itself, the actual stdout and stderr of the test, and
    the expected stdout of the test (if specified).

    If -k is specified, the output directory will also include a directory for
    each test that passed including the stdout and stderr from the test.

The following options may be specified:

	-a 		Runs all tests under $cat_tstdir
			(ignores other non-option arguments)
	-c		Color code test result messages
	-h		Output this message
	-k		Keep output from all tests, not just failures
	-o directory	Specifies the output directory for tests
			(default: /var/tmp)
	-S		Turn off strict mode for tests
	-t file		Emit summary output in TAP format

USAGE

	exit 2
}

#
# abspath FILE: emits a canonical, absolute path to the given file or directory.
#
function abspath
{
	local dir=$(dirname $1) base=$(basename $1)

	if [[ $base = ".." ]]; then
		cd "$dir"/.. > /dev/null || fail "abspath '$1': failed to chdir"
		pwd
		cd - > /dev/null || fail "abspath '$1': failed to chdir back"
	else
		cd "$dir" || fail "abspath '$1': failed to chdir"
		echo "$(pwd)/$base"
		cd - > /dev/null || fail "abspath '$1': failed to chdir back"
	fi
}

#
# cleanup_test TESTDIR "success" | "failure": cleans up the output directory
#     for this test
#
function cleanup_test
{
	local test_odir="$1" result=$2
	local newdir

	if [[ $result = "success" ]]; then
		newdir="$(dirname $test_odir)/success.$cat_npassed"
	else
		newdir="$(dirname $test_odir)/failure.$cat_nfailed"
	fi

	mv "$test_odir" "$newdir"
	echo $newdir
}

#
# emit_failure TEST ODIR REASON: indicate that a test has failed
#
function emit_failure
{
	local test_label=$1 odir=$2 reason=$3

	if [[ $cat_tapfile ]]; then
		echo "not ok $(($cat_nrun+1)) $test_label" >> $cat_tapfile
	fi

	echo "${TRED}FAILED.${TCLEAR}"
	echo "$test_path failed: $reason" > "$odir/README"

	[[ -n "$odir" ]] && echo ">>> failure details in $odir\n"
	((cat_nfailed++))
}

#
# emit_pass TEST: indicate that a test has passed
#
function emit_pass
{
	local test_label=$1

	if [[ $cat_tapfile ]]; then
		echo "ok $((cat_nrun+1)) $test_label" >> $cat_tapfile
	fi

	echo "${TGREEN}success.${TCLEAR}"
	((cat_npassed++))
}

#
# Executes a single test
#
# Per-test actions:
# - Make a directory for that test
# - cd into that directory and exec the test
# - Redirect standard output and standard error to files
# - Tests return 0 to indicate success, non-zero to indicate failure
#
function execute_test
{
	[[ $# -eq 1 ]] || fail "Missing test to execute"
	local test_path=$1
	local test_name=$(basename $1)
	local test_dir=$(dirname $1)
	local test_label=$(echo $test_path | sed -e s#^$SRC/##)
	local test_odir="$cat_outdir/test.$cat_nrun"
	local ext=${test_name##*.}
	local faildir
	local EXEC

	echo "Executing test $test_label ... \c "
	mkdir "$test_odir" >/dev/null || fail "failed to create test directory"
	cp "$test_path" "$test_odir"

	case "$ext" in
	"sh") 	EXEC=bash ;;
	"js") 	EXEC=node ;;
	*) 	faildir=$(cleanup_test "$test_odir" "failure")
		emit_failure "$test_label" "$faildir" "unknown file extension"
		return 0
		;;
	esac

	pushd "$test_dir" >/dev/null
	if [[ $opt_S ]]; then
		$EXEC $test_name -S >$test_odir/$$.out 2>$test_odir/$$.err
	else
		$EXEC $test_name >$test_odir/$$.out 2>$test_odir/$$.err
	fi
	execres=$?
	popd > /dev/null

	if [[ $execres != 0 ]]; then
		faildir=$(cleanup_test "$test_odir" "failure")
		emit_failure "$test_label" "$faildir" "test returned $execres"
		return 0
	fi

	if [[ -f $test_path.out ]] && \
	    ! diff $test_path.out $test_odir/$$.out > /dev/null ; then
		cp $test_path.out $test_odir/$test_name.out
		faildir=$(cleanup_test "$test_odir" "failure")
		emit_failure "$test_label" "$faildir" "stdout mismatch"
		return 0
	fi

	cleanup_test "$test_odir" "success" > /dev/null
	emit_pass "$test_label"
}

while getopts ":o:t:ackSh?" c $@; do
	case "$c" in
	a|c|k|S) 	eval opt_$c=true ;;
	o|t) 	eval opt_$c="$OPTARG" ;;
	h)	usage ;;
	:) 	usage "option requires an argument -- $OPTARG" ;;
	*) 	usage "invalid option: $OPTARG" ;;
	esac
done

#
# If configured to use terminal colors, record the escape sequences here.
#
if [[ $opt_c == "true" && -t 1 ]]; then
    TGREEN="$(tput setaf 2)"
    TRED="$(tput setaf 1)"
    TCLEAR="$(tput sgr0)"
fi

shift $((OPTIND-1))
[[ $# -eq 0 && $opt_a == "false" ]] && \
    usage "must specify \"-a\" or list of tests"

#
# Initialize paths and other environment variables.
#
export SRC=$(abspath $(dirname $0)/../..)
export PATH=$SRC/deps/ctf2json:$PATH
[[ -n $HOST ]] || export HOST=$(hostname)

#
# We create and set CATMPDIR as a place for the tests to store temporary files.
#
export CATMPDIR="/var/tmp/catest.$$_tmpfiles"

if [[ $opt_a = "true" ]]; then
	cat_tests=$(find $SRC/$cat_tstdir \
	    -name 'tst*.js' -o -name 'tst*.sh') || \
	    fail "failed to locate tests in $SRC/$cat_tstdir"
	cat_tests=$(sort <<< "$cat_tests")
else
	for t in $@; do
		[[ -f $t ]] || fail "cannot find test $t"
		cat_tests="$cat_tests $(abspath $t)"
	done
fi

mkdir -p "$opt_o/$cat_outbase"
cat_outdir=$(abspath $opt_o/$cat_outbase)

mkdir -p $CATMPDIR || fail "failed to create $CATMPDIR"

cat_ntests=$(echo $cat_tests | wc -w)
printf "Configuration:\n"
printf "    SRC:                         $SRC\n"
printf "    Output directory:            $cat_outdir\n"
printf "    Temp directory:              $CATMPDIR\n"
if [[ -n "$opt_t" ]]; then
    cat_tapfile=$(abspath $opt_t)
    printf "    TAP output:                  $cat_tapfile\n"
fi
printf "    Keep successful test output: $opt_k\n"
printf "    Found %d test(s) to run\n\n" $cat_ntests

#
# Validate parameters and finish setup.
#
[[ $cat_ntests -gt 0 ]] || fail "no tests found"

if [[ -n "$cat_tapfile" ]]; then
	echo "1..$(($cat_ntests))" > $cat_tapfile || \
	    fail "failed to emit TAP output"
fi

#
# Allow for environment-specific customizations.  These are optionally loaded
# by the catest_init.sh file sourced earlier.
#
if type catest_init > /dev/null 2>&1 && ! catest_init; then
	fail "catest_init failed"
fi

#
# Start the test run.
#
printf "===================================================\n\n"

for t in $cat_tests; do
	execute_test $t
	((cat_nrun++))
done

printf "\n===================================================\n\n"
printf "Results:\n"
printf "\tTests passed:\t%2d/%2d\n" $cat_npassed $cat_nrun
printf "\tTests failed:\t%2d/%2d\n" $cat_nfailed $cat_nrun
printf "\n===================================================\n"

if [[ $opt_k == "false" ]]; then
	echo "Cleaning up output from successful tests ... \c "
	rm -rf $cat_outdir/success.*
	rm -rf $CATMPDIR
	echo "done."
fi

exit $cat_nfailed