Improve test framework: orderly shutdown on signal

Uses bash job control internally instead of PIDs, because job control
allows all processes in a job to be killed.

Changes the way _tfw_shopt/_tfw_shopt_restore work, to avoid "unrestored shopt"
errors on interrupt.
This commit is contained in:
Andrew Bettison 2012-08-16 12:17:15 +09:30
parent e1fb64e9d4
commit 3c736b1f6c

View File

@ -75,15 +75,15 @@ Options:
# Internal utility for setting shopt variables and restoring their original # Internal utility for setting shopt variables and restoring their original
# value: # value:
# _tfw_shopt -s extglob -u extdebug # local oo
# _tfw_shopt oo -s extglob -u extdebug
# ... # ...
# _tfw_shopt_restore # _tfw_shopt_restore oo
_tfw_shopt() { _tfw_shopt() {
if [ -n "$_tfw_shopt_orig" ]; then local _var="$1"
_tfw_fatal "unrestored shopt settings: $_tfw_shopt_orig" shift
fi
_tfw_shopt_orig=
local op=s local op=s
local restore=
while [ $# -ne 0 ] while [ $# -ne 0 ]
do do
case "$1" in case "$1" in
@ -91,24 +91,23 @@ _tfw_shopt() {
-u) op=u;; -u) op=u;;
*) *)
local opt="$1" local opt="$1"
_tfw_shopt_orig="${restore:+$restore; }shopt -$(shopt -q $opt && echo s || echo u) $opt" restore="${restore:+$restore; }shopt -$(shopt -q $opt && echo s || echo u) $opt"
shopt -$op $opt shopt -$op $opt
;; ;;
esac esac
shift shift
done done
eval $_var='"$restore"'
} }
_tfw_shopt_restore() { _tfw_shopt_restore() {
if [ -n "$_tfw_shopt_orig" ]; then local _var="$1"
eval "$_tfw_shopt_orig" [ -n "${!_var}" ] && eval "${!_var}"
_tfw_shopt_orig=
fi
} }
_tfw_shopt_orig=
declare -a _tfw_running_pids declare -a _tfw_running_jobs
# The rest of this file is parsed for extended glob patterns. # The rest of this file is parsed for extended glob patterns.
_tfw_shopt -s extglob _tfw_shopt _tfw_orig_shopt -s extglob
runTests() { runTests() {
_tfw_stdout=1 _tfw_stdout=1
@ -119,7 +118,7 @@ runTests() {
_tfw_suite_name="${_tfw_invoking_script##*/}" _tfw_suite_name="${_tfw_invoking_script##*/}"
_tfw_cwd=$(abspath "$PWD") _tfw_cwd=$(abspath "$PWD")
_tfw_tmpdir="${TFW_TMPDIR:-${TMPDIR:-/tmp}}/_tfw-$$" _tfw_tmpdir="${TFW_TMPDIR:-${TMPDIR:-/tmp}}/_tfw-$$"
trap '_tfw_status=$?; rm -rf "$_tfw_tmpdir"; exit $_tfw_status' EXIT SIGHUP SIGINT SIGTERM trap '_tfw_status=$?; _tfw_killtests; rm -rf "$_tfw_tmpdir"; exit $_tfw_status' EXIT SIGHUP SIGINT SIGTERM
rm -rf "$_tfw_tmpdir" rm -rf "$_tfw_tmpdir"
mkdir -p "$_tfw_tmpdir" || return $? mkdir -p "$_tfw_tmpdir" || return $?
_tfw_logdir="${TFW_LOGDIR:-$_tfw_cwd/testlog}/$_tfw_suite_name" _tfw_logdir="${TFW_LOGDIR:-$_tfw_cwd/testlog}/$_tfw_suite_name"
@ -131,7 +130,8 @@ runTests() {
local allargs="$*" local allargs="$*"
local -a filters=() local -a filters=()
local njobs=1 local njobs=1
_tfw_shopt -s extglob local oo
_tfw_shopt oo -s extglob
while [ $# -ne 0 ]; do while [ $# -ne 0 ]; do
case "$1" in case "$1" in
--help) usage; exit 0;; --help) usage; exit 0;;
@ -149,7 +149,7 @@ runTests() {
esac esac
shift shift
done done
_tfw_shopt_restore _tfw_shopt_restore oo
if $_tfw_verbose && [ $njobs -ne 1 ]; then if $_tfw_verbose && [ $njobs -ne 1 ]; then
_tfw_fatal "--verbose is incompatible with --jobs=$njobs" _tfw_fatal "--verbose is incompatible with --jobs=$njobs"
fi fi
@ -161,6 +161,8 @@ runTests() {
rm -f "$_tfw_logdir"/* rm -f "$_tfw_logdir"/*
# Enumerate all the test cases. # Enumerate all the test cases.
_tfw_find_tests "${filters[@]}" _tfw_find_tests "${filters[@]}"
# Enable job control.
set -m
# Iterate through all test cases, starting a new test whenever the number of # Iterate through all test cases, starting a new test whenever the number of
# running tests is less than the job limit. # running tests is less than the job limit.
_tfw_testcount=0 _tfw_testcount=0
@ -168,7 +170,7 @@ runTests() {
_tfw_failcount=0 _tfw_failcount=0
_tfw_errorcount=0 _tfw_errorcount=0
_tfw_fatalcount=0 _tfw_fatalcount=0
_tfw_running_pids=() _tfw_running_jobs=()
_tfw_test_number_watermark=0 _tfw_test_number_watermark=0
local testNumber local testNumber
local testPosition=0 local testPosition=0
@ -178,7 +180,7 @@ runTests() {
let ++testPosition let ++testPosition
let ++_tfw_testcount let ++_tfw_testcount
# Wait for any existing child process to finish. # Wait for any existing child process to finish.
while [ $njobs -ne 0 -a ${#_tfw_running_pids[*]} -ge $njobs ]; do while [ $njobs -ne 0 -a ${#_tfw_running_jobs[*]} -ge $njobs ]; do
_tfw_harvest_processes _tfw_harvest_processes
done done
[ $_tfw_fatalcount -ne 0 ] && break [ $_tfw_fatalcount -ne 0 ] && break
@ -251,12 +253,13 @@ runTests() {
fi fi
} >"$_tfw_logdir/$testNumber.$testName.$result" } >"$_tfw_logdir/$testNumber.$testName.$result"
exit 0 exit 0
) & ) </dev/null &
_tfw_running_pids+=($!) local job=$(jobs %% | sed -n -e '1s/^\[\([0-9]\+\)\].*/\1/p')
ln -s "$_tfw_results_dir/$testName" "$_tfw_results_dir/pid-$!" _tfw_running_jobs+=($job)
ln -f -s "$_tfw_results_dir/$testName" "$_tfw_results_dir/job-$job"
done done
# Wait for all child processes to finish. # Wait for all child processes to finish.
while [ ${#_tfw_running_pids[*]} -ne 0 ]; do while [ ${#_tfw_running_jobs[*]} -ne 0 ]; do
_tfw_harvest_processes _tfw_harvest_processes
done done
# Clean up working directory. # Clean up working directory.
@ -268,6 +271,22 @@ runTests() {
[ $_tfw_fatalcount -eq 0 -a $_tfw_failcount -eq 0 -a $_tfw_errorcount -eq 0 ] [ $_tfw_fatalcount -eq 0 -a $_tfw_failcount -eq 0 -a $_tfw_errorcount -eq 0 ]
} }
_tfw_killtests() {
if [ $njobs -eq 1 ]; then
echo -n " killing..."
else
echo -n -e "\r\rKilling tests...\r"
fi
trap '' SIGHUP SIGINT SIGTERM
local job
for job in ${_tfw_running_jobs[*]}; do
kill %$job 2>/dev/null
done
while [ ${#_tfw_running_jobs[*]} -ne 0 ]; do
_tfw_harvest_processes
done
}
_tfw_echo_intro() { _tfw_echo_intro() {
local docvar="doc_$3" local docvar="doc_$3"
echo -n "$2. ${!docvar:-$3}..." echo -n "$2. ${!docvar:-$3}..."
@ -282,16 +301,16 @@ _tfw_harvest_processes() {
sleep 1 & sleep 1 &
spid=$! spid=$!
set -m set -m
wait $spid 2>/dev/null wait $spid >/dev/null 2>/dev/null
trap - SIGCHLD trap - SIGCHLD
# </incantation> # </incantation>
local -a surviving_pids=() local -a surviving_jobs=()
local pid local job
for pid in ${_tfw_running_pids[*]}; do for job in ${_tfw_running_jobs[*]}; do
if kill -0 $pid 2>/dev/null; then if jobs %$job >/dev/null 2>/dev/null; then
surviving_pids+=($pid) surviving_jobs+=($job)
elif [ -s "$_tfw_results_dir/pid-$pid" ]; then elif [ -s "$_tfw_results_dir/job-$job" ]; then
set -- $(<"$_tfw_results_dir/pid-$pid") set -- $(<"$_tfw_results_dir/job-$job")
local testPosition="$1" local testPosition="$1"
local testNumber="$2" local testNumber="$2"
local testName="$3" local testName="$3"
@ -331,11 +350,13 @@ _tfw_harvest_processes() {
_tfw_echo_result "$result" _tfw_echo_result "$result"
echo echo
fi fi
rm -f "$_tfw_results_dir/job-$job"
else else
_tfw_echoerr "${BASH_SOURCE[1]}: child process $pid terminated without result" _tfw_echoerr "${BASH_SOURCE[1]}: job %$job terminated without result"
rm -f "$_tfw_results_dir/job-$job"
fi fi
done done
_tfw_running_pids=(${surviving_pids[*]}) _tfw_running_jobs=(${surviving_jobs[*]})
} }
_tfw_echo_result() { _tfw_echo_result() {
@ -649,14 +670,12 @@ assertGrep() {
_tfw_shellarg() { _tfw_shellarg() {
local arg local arg
_tfw_args=() _tfw_args=()
_tfw_shopt -s extglob
for arg; do for arg; do
case "$arg" in case "$arg" in
+([A-Za-z_0-9.,:=+\/-])) _tfw_args+=("$arg");; '' | *[^A-Za-z_0-9.,:=+\/-]* ) _tfw_args+=("'${arg//'/'\\''}'");;
*) _tfw_args+=("'${arg//'/'\\''}'");; *) _tfw_args+=("$arg");;
esac esac
done done
_tfw_shopt_restore
} }
# Echo the absolute path of the given path, using only Bash builtins. # Echo the absolute path of the given path, using only Bash builtins.
@ -855,7 +874,8 @@ _tfw_getopts() {
_tfw_opt_matches= _tfw_opt_matches=
_tfw_opt_line= _tfw_opt_line=
_tfw_getopts_shift=0 _tfw_getopts_shift=0
_tfw_shopt -s extglob local oo
_tfw_shopt oo -s extglob
while [ $# -ne 0 ]; do while [ $# -ne 0 ]; do
case "$context:$1" in case "$context:$1" in
*:--stdout) _tfw_dump_on_fail --stdout;; *:--stdout) _tfw_dump_on_fail --stdout;;
@ -893,7 +913,7 @@ _tfw_getopts() {
[ -z "$_tfw_executable" ] && _tfw_error "missing executable argument" [ -z "$_tfw_executable" ] && _tfw_error "missing executable argument"
;; ;;
esac esac
_tfw_shopt_restore _tfw_shopt_restore oo
return 0 return 0
} }
@ -1019,7 +1039,8 @@ _tfw_assert_grep() {
local matches=$(( $(grep --regexp="$pattern" "$file" | wc -l) + 0 )) local matches=$(( $(grep --regexp="$pattern" "$file" | wc -l) + 0 ))
local done=false local done=false
local ret=0 local ret=0
_tfw_shopt -s extglob local oo
_tfw_shopt oo -s extglob
case "$_tfw_opt_matches" in case "$_tfw_opt_matches" in
'') '')
done=true done=true
@ -1077,7 +1098,7 @@ _tfw_assert_grep() {
_tfw_error "unsupported value for --matches=$_tfw_opt_matches" _tfw_error "unsupported value for --matches=$_tfw_opt_matches"
ret=$? ret=$?
fi fi
_tfw_shopt_restore _tfw_shopt_restore oo
fi fi
if [ $ret -ne 0 ]; then if [ $ret -ne 0 ]; then
_tfw_backtrace _tfw_backtrace
@ -1121,7 +1142,8 @@ _tfw_checkTerminfo() {
# an alphabetic character (not numeric or '_'). # an alphabetic character (not numeric or '_').
_tfw_find_tests() { _tfw_find_tests() {
_tfw_tests=() _tfw_tests=()
_tfw_shopt -s extdebug local oo
_tfw_shopt oo -s extdebug
local name local name
for name in $(builtin declare -F | for name in $(builtin declare -F |
sed -n -e '/^declare -f test_[A-Za-z]/s/^declare -f test_//p' | sed -n -e '/^declare -f test_[A-Za-z]/s/^declare -f test_//p' |
@ -1188,7 +1210,7 @@ _tfw_find_tests() {
fi fi
_tfw_tests+=("$testName") _tfw_tests+=("$testName")
done done
_tfw_shopt_restore _tfw_shopt_restore oo
} }
# A "fail" event occurs when any assertion fails, and indicates that the test # A "fail" event occurs when any assertion fails, and indicates that the test
@ -1286,4 +1308,4 @@ _tfw_fatalexit() {
} }
# Restore the caller's shopt preferences before returning. # Restore the caller's shopt preferences before returning.
_tfw_shopt_restore _tfw_shopt_restore _tfw_orig_shopt