Improve test framework: --jobs (parallel test execution)

This commit is contained in:
Andrew Bettison 2012-06-27 17:22:37 +09:30
parent 3822d067e7
commit c0c95bf6ac

View File

@ -60,12 +60,51 @@ Usage: ${0##*/} [options] [--]
Options: Options:
-t, --trace Enable shell "set -x" tracing during tests, output to test log -t, --trace Enable shell "set -x" tracing during tests, output to test log
-v, --verbose Send test log to output during execution -v, --verbose Send test log to output during execution
-j, --jobs Run all tests in parallel (by default runs as --jobs=1)
--jobs=N Run tests in parallel, at most N at a time
-E, --stop-on-error Do not execute any tests after an ERROR occurs -E, --stop-on-error Do not execute any tests after an ERROR occurs
-F, --stop-on-failure Do not execute any tests after a FAIL occurs -F, --stop-on-failure Do not execute any tests after a FAIL occurs
--filter=PREFIX Only execute tests whose names start with PREFIX --filter=PREFIX Only execute tests whose names start with PREFIX
" "
} }
# Internal utility for setting shopt variables and restoring their original
# value:
# _tfw_shopt -s extglob -u extdebug
# ...
# _tfw_shopt_restore
_tfw_shopt() {
if [ -n "$_tfw_shopt_orig" ]; then
_tfw_fatal "unrestored shopt settings: $_tfw_shopt_orig"
fi
_tfw_shopt_orig=
local op=s
while [ $# -ne 0 ]
do
case "$1" in
-s) op=s;;
-u) op=u;;
*)
local opt="$1"
_tfw_shopt_orig="${restore:+$restore; }shopt -$(shopt -q $opt && echo s || echo u) $opt"
shopt -$op $opt
;;
esac
shift
done
}
_tfw_shopt_restore() {
if [ -n "$_tfw_shopt_orig" ]; then
eval "$_tfw_shopt_orig"
_tfw_shopt_orig=
fi
}
_tfw_shopt_orig=
declare -a _tfw_running_pids
# The rest of this file is parsed for extended glob patterns.
_tfw_shopt -s extglob
runTests() { runTests() {
_tfw_stdout=1 _tfw_stdout=1
_tfw_stderr=2 _tfw_stderr=2
@ -73,19 +112,28 @@ runTests() {
_tfw_invoking_script=$(abspath "${BASH_SOURCE[1]}") _tfw_invoking_script=$(abspath "${BASH_SOURCE[1]}")
_tfw_suite_name="${_tfw_invoking_script##*/}" _tfw_suite_name="${_tfw_invoking_script##*/}"
_tfw_cwd=$(abspath "$PWD") _tfw_cwd=$(abspath "$PWD")
_tfw_logfile="$_tfw_cwd/test.$_tfw_suite_name.log" _tfw_tmpdir="${TFW_TMPDIR:-${TMPDIR:-/tmp}}/_tfw-$$"
trap '_tfw_status=$?; rm -rf "$_tfw_tmpdir"; exit $_tfw_status' EXIT SIGHUP SIGINT SIGTERM
rm -rf "$_tfw_tmpdir"
mkdir -p "$_tfw_tmpdir" || return $?
_tfw_logdir="$_tfw_cwd/testlog/$_tfw_suite_name"
_tfw_trace=false _tfw_trace=false
_tfw_verbose=false _tfw_verbose=false
_tfw_stop_on_error=false _tfw_stop_on_error=false
_tfw_stop_on_failure=false _tfw_stop_on_failure=false
local allargs="$*" local allargs="$*"
local filter= local -a filters=()
local njobs=1
_tfw_shopt -s extglob
while [ $# -ne 0 ]; do while [ $# -ne 0 ]; do
case "$1" in case "$1" in
--help) usage; exit 0;; --help) usage; exit 0;;
-t|--trace) _tfw_trace=true;; -t|--trace) _tfw_trace=true;;
-v|--verbose) _tfw_verbose=true;; -v|--verbose) _tfw_verbose=true;;
--filter=*) filter="${1#*=}";; --filter=*) filters+=("${1#*=}");;
-j|--jobs) njobs=0;;
--jobs=+([0-9])) njobs="${1#*=}";;
--jobs=*) _tfw_fatal "invalid option: $1";;
-E|--stop-on-error) _tfw_stop_on_error=true;; -E|--stop-on-error) _tfw_stop_on_error=true;;
-F|--stop-on-failure) _tfw_stop_on_failure=true;; -F|--stop-on-failure) _tfw_stop_on_failure=true;;
--) shift; break;; --) shift; break;;
@ -93,26 +141,48 @@ runTests() {
*) _tfw_fatal "spurious argument: $1";; *) _tfw_fatal "spurious argument: $1";;
esac esac
shift shift
# Kick off the log file.
done done
{ _tfw_shopt_restore
date # Create an empty results directory.
echo "$0 $allargs" _tfw_results_dir="$_tfw_tmpdir/results"
} >$_tfw_logfile mkdir "$_tfw_results_dir" || return $?
# Iterate through all test cases. # Create an empty log directory.
local testcount=0 mkdir -p "$_tfw_logdir" || return $?
local passcount=0 rm -f "$_tfw_logdir"/*
local testName # Enumerate all the test cases.
for testName in `_tfw_find_tests` _tfw_find_tests "${filters[@]}"
do # Iterate through all test cases, starting a new test whenever the number of
_tfw_test_name="$testName" # running tests is less than the job limit.
if [ -z "$filter" -o "${_tfw_test_name#$filter}" != "$_tfw_test_name" ]; then _tfw_passcount=0
let testcount=testcount+1 _tfw_failcount=0
_tfw_errorcount=0
_tfw_fatalcount=0
_tfw_running_pids=()
local testNumber
for ((testNumber = 1; testNumber <= ${#_tfw_tests[*]}; ++testNumber)); do
testName="${_tfw_tests[$(($testNumber - 1))]}"
# Wait for any existing child process to finish.
while [ $njobs -ne 0 -a ${#_tfw_running_pids[*]} -ge $njobs ]; do
_tfw_harvest_processes
done
[ $_tfw_fatalcount -ne 0 ] && break
$_tfw_stop_on_error && [ $_tfw_errorcount -ne 0 ] && break
$_tfw_stop_on_failure && [ $_tfw_failcount -ne 0 ] && break
# Start the next test in a child process.
local docvar="doc_$testName"
echo -n "$testNumber. ${!docvar:-$testName}..."
[ $njobs -ne 1 ] && echo
(
echo "$testNumber $testName" >"$_tfw_results_dir/$BASHPID"
_tfw_tmp=/tmp/_tfw-$BASHPID
trap '_tfw_status=$?; rm -rf "$_tfw_tmp"; exit $_tfw_status' EXIT SIGHUP SIGINT SIGTERM
local start_time=$(_tfw_timestamp)
local finish_time=unknown
( (
local docvar="doc_$_tfw_test_name" _tfw_test_name="$testName"
_tfw_echo -n "$testcount. ${!docvar:-$_tfw_test_name}..." trap '_tfw_status=$?; _tfw_teardown; exit $_tfw_status' EXIT SIGHUP SIGINT SIGTERM
trap '_tfw_status=$?; _tfw_teardown; exit $_tfw_status' 0 1 2 15
_tfw_result=ERROR _tfw_result=ERROR
mkdir $_tfw_tmp || exit 255
_tfw_setup _tfw_setup
_tfw_result=FAIL _tfw_result=FAIL
_tfw_phase=testcase _tfw_phase=testcase
@ -120,32 +190,95 @@ runTests() {
$_tfw_trace && set -x $_tfw_trace && set -x
test_$_tfw_test_name test_$_tfw_test_name
_tfw_result=PASS _tfw_result=PASS
exit 0 case $_tfw_result in
PASS) exit 0;;
FAIL) exit 1;;
ERROR) exit 254;;
esac
exit 255
) )
local stat=$? local stat=$?
finish_time=$(_tfw_timestamp)
local result=FATAL
case $stat in case $stat in
255) 254) result=ERROR;;
# _tfw_fatal was called 1) result=FAIL;;
exit 255;; 0) result=PASS;;
254) esac
# _tfw_failexit was called in setup or teardown or _tfw_error was called anywhere echo "$testNumber $testName $result" >"$_tfw_results_dir/$BASHPID"
_tfw_echo " ERROR" {
$_tfw_stop_on_error && break echo "Name: $testName"
echo "Result: $result"
echo "Started: $start_time"
echo "Finished: $finish_time"
echo '++++++++++ log.stdout ++++++++++'
cat $_tfw_tmp/log.stdout
echo '++++++++++'
echo '++++++++++ log.stderr ++++++++++'
cat $_tfw_tmp/log.stderr
echo '++++++++++'
if $_tfw_trace; then
echo '++++++++++ log.xtrace ++++++++++'
cat $_tfw_tmp/log.xtrace
echo '++++++++++'
fi
} >"$_tfw_logdir/$testNumber.$testName.$result"
exit 0
) &
_tfw_running_pids+=($!)
done
# Wait for all child processes to finish.
while [ ${#_tfw_running_pids[*]} -ne 0 ]; do
_tfw_harvest_processes
done
# Clean up working directory.
rm -rf "$_tfw_tmpdir"
trap - EXIT SIGHUP SIGINT SIGTERM
# Echo result summary and exit with success if no failures or errors.
s=$([ ${#_tfw_tests[*]} -eq 1 ] || echo s)
echo "${#_tfw_tests[*]} test$s, $_tfw_passcount pass, $_tfw_failcount fail, $_tfw_errorcount error"
[ $_tfw_fatalcount -eq 0 -a $_tfw_failcount -eq 0 -a $_tfw_errorcount -eq 0 ]
}
_tfw_harvest_processes() {
trap 'kill $spid 2>/dev/null' SIGCHLD
sleep 1 &
spid=$!
set -m
wait $spid 2>/dev/null
trap - SIGCHLD
local -a surviving_pids=()
local pid
for pid in ${_tfw_running_pids[*]}; do
if kill -0 $pid 2>/dev/null; then
surviving_pids+=($pid)
elif [ -s "$_tfw_results_dir/$pid" ]; then
set -- $(<"$_tfw_results_dir/$pid")
local testNumber="$1"
local testName="$2"
local result="$3"
case "$result" in
ERROR)
let _tfw_errorcount=_tfw_errorcount+1
;; ;;
0) PASS)
_tfw_echo " PASS" let _tfw_passcount=_tfw_passcount+1
let passcount=passcount+1 ;;
FAIL)
let _tfw_failcount=_tfw_failcount+1
;; ;;
*) *)
_tfw_echo " FAIL" result=FATAL
$_tfw_stop_on_failure && break let _tfw_fatalcount=_tfw_fatalcount+1
;; ;;
esac esac
[ $njobs -ne 1 ] && echo -n "$testNumber. ..."
echo " $result"
else
_tfw_echoerr "${BASH_SOURCE[1]}: child process $pid terminated without result"
fi fi
done done
s=$([ $testcount -eq 1 ] || echo s) _tfw_running_pids=(${surviving_pids[*]})
_tfw_echo "$testcount test$s, $passcount passed"
[ $passcount -eq $testcount ]
} }
# The following functions can be overridden by a test script to provide a # The following functions can be overridden by a test script to provide a
@ -384,40 +517,6 @@ assertGrep() {
# Internal (private) functions that are not to be invoked directly from test # Internal (private) functions that are not to be invoked directly from test
# scripts. # scripts.
# Utility for setting shopt variables and restoring their original value:
# _tfw_shopt -s extglob -u extdebug
# ...
# _tfw_shopt_restore
_tfw_shopt() {
if [ -n "$_tfw_shopt_orig" ]; then
_tfw_fatal "unrestored shopt settings: $_tfw_shopt_orig"
fi
_tfw_shopt_orig=
local op=s
while [ $# -ne 0 ]
do
case "$1" in
-s) op=s;;
-u) op=u;;
*)
local opt="$1"
_tfw_shopt_orig="${restore:+$restore; }shopt -$(shopt -q $opt && echo s || echo u) $opt"
shopt -$op $opt
;;
esac
shift
done
}
_tfw_shopt_restore() {
if [ -n "$_tfw_shopt_orig" ]; then
eval "$_tfw_shopt_orig"
_tfw_shopt_orig=
fi
}
# The rest of this file is parsed for extended glob patterns.
_tfw_shopt -s extglob
# Add shell quotation to the given arguments, so that when expanded using # Add shell quotation to the given arguments, so that when expanded using
# 'eval', the exact same argument results. This makes argument handling fully # 'eval', the exact same argument results. This makes argument handling fully
# immune to spaces and shell metacharacters. # immune to spaces and shell metacharacters.
@ -472,10 +571,12 @@ _tfw_abspath() {
esac esac
} }
_tfw_timestamp() {
date '+%Y-%m-%d %H:%M:%S.%N'
}
_tfw_setup() { _tfw_setup() {
_tfw_phase=setup _tfw_phase=setup
_tfw_tmp=/tmp/_tfw-$$
mkdir $_tfw_tmp
exec <&- 5>&1 5>&2 6>$_tfw_tmp/log.stdout 1>&6 2>$_tfw_tmp/log.stderr 7>$_tfw_tmp/log.xtrace exec <&- 5>&1 5>&2 6>$_tfw_tmp/log.stdout 1>&6 2>$_tfw_tmp/log.stderr 7>$_tfw_tmp/log.xtrace
BASH_XTRACEFD=7 BASH_XTRACEFD=7
_tfw_log_fd=6 _tfw_log_fd=6
@ -483,9 +584,11 @@ _tfw_setup() {
_tfw_stderr=5 _tfw_stderr=5
if $_tfw_verbose; then if $_tfw_verbose; then
# These tail processes will die when the test case's subshell exits. # These tail processes will die when the test case's subshell exits.
tail --pid=$$ --follow $_tfw_tmp/log.stdout >&$_tfw_stdout 2>/dev/null & tail --pid=$BASHPID --follow $_tfw_tmp/log.stdout >&$_tfw_stdout 2>/dev/null &
tail --pid=$$ --follow $_tfw_tmp/log.stderr >&$_tfw_stderr 2>/dev/null & tail --pid=$BASHPID --follow $_tfw_tmp/log.stderr >&$_tfw_stderr 2>/dev/null &
fi fi
export TFWVAR=$_tfw_tmp/var
mkdir $TFWVAR
export TFWTMP=$_tfw_tmp/tmp export TFWTMP=$_tfw_tmp/tmp
mkdir $TFWTMP mkdir $TFWTMP
cd $TFWTMP cd $TFWTMP
@ -525,24 +628,6 @@ _tfw_teardown() {
;; ;;
esac esac
echo '# END TEARDOWN' echo '# END TEARDOWN'
{
local banner="==================== $_tfw_test_name ===================="
echo "$banner"
echo "TEST RESULT: $_tfw_result"
echo '++++++++++ log.stdout ++++++++++'
cat $_tfw_tmp/log.stdout
echo '++++++++++'
echo '++++++++++ log.stderr ++++++++++'
cat $_tfw_tmp/log.stderr
echo '++++++++++'
if $_tfw_trace; then
echo '++++++++++ log.xtrace ++++++++++'
cat $_tfw_tmp/log.xtrace
echo '++++++++++'
fi
echo "${banner//[^=]/=}"
} >>$_tfw_logfile
rm -rf $_tfw_tmp
} }
# Executes $_tfw_executable with the given arguments. # Executes $_tfw_executable with the given arguments.
@ -848,11 +933,6 @@ _tfw_assert_grep() {
return $ret return $ret
} }
# Write to the real stdout of the test script.
_tfw_echo() {
echo "$@" >&$_tfw_stdout
}
# Write a message to the real stderr of the test script, so the user sees it # Write a message to the real stderr of the test script, so the user sees it
# immediately. Also write the message to the test log, so it can be recovered # immediately. Also write the message to the test log, so it can be recovered
# later. # later.
@ -877,15 +957,29 @@ _tfw_checkBashVersion() {
_tfw_fatal "unsupported Bash version: $BASH_VERSION" _tfw_fatal "unsupported Bash version: $BASH_VERSION"
} }
# Return a list of test names in the order that the test_TestName functions were # Return a list of test names in the _tfw_tests array variable, in the order
# defined. # that the test_TestName functions were defined.
_tfw_find_tests() { _tfw_find_tests() {
_tfw_tests=()
_tfw_shopt -s extdebug _tfw_shopt -s extdebug
builtin declare -F | local name
sed -n -e '/^declare -f test_./s/^declare -f test_//p' | local filter
while read name; do builtin declare -F "test_$name"; done | for name in $(builtin declare -F |
sort --key 2,2n --key 3,3 | sed -n -e '/^declare -f test_./s/^declare -f test_//p' |
sed -e 's/^test_//' -e 's/[ ].*//' while read name; do builtin declare -F "test_$name"; done |
sort --key 2,2n --key 3,3 |
sed -e 's/^test_//' -e 's/[ ].*//')
do
if [ $# -eq 0 ]; then
_tfw_tests+=("$name")
else
for filter; do
case "$_tfw_test_name" in
"$filter"*) _tfw_tests+=("$name"); break;;
esac
done
fi
done
_tfw_shopt_restore _tfw_shopt_restore
} }