#!/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 <&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*." 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