serval-dna/testframework.sh
Andrew Bettison 2e0cb4e6dc Improve test framework error handling
If runTests() terminated with a FATAL condition, the cause was not
always apparent, because the error message usually got overwritten by
the test progress output on the user's terminal.  To fix this, the main
loop's standard error is now collected in a temporary file which is sent
to standard error just before exit.

The test framework was not always handling its internal FATAL error code
(255) correctly, so this has been fixed.

One cause of a FATAL termination was if a test's unique temporary
directory, which was based on its Process ID, already existed.  Anything
that left a temporary directory behind increased the likelihood of a
FATAL in a subsequent test run.  (For example, one keyring test case was
not killing its Serval DNA daemon, and the daemon was re-creating its
instance directory before eventually terminating itself.  This test case
has been fixed.)

The test framework now allocates its temporary directory by re-trying
different random numbers until mkdir(1) succeeds, and only fataling
after 20 failures.
2018-03-29 15:09:45 +10:30

2261 lines
69 KiB
Bash

#!/bin/bash
#
# Serval Project testing framework for Bash shell
# Copyright 2012 Serval Project, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# This file is sourced by all testing scripts. A typical test script looks
# like this:
#
# #!/bin/bash
# source testframework.sh
# setup() {
# export BLAH_CONFIG=$TFWTMP/blah.conf
# echo "username=$LOGNAME" >$BLAH_CONFIG
# }
# teardown() {
# # $TFWTMP is always removed after every test, so no need to
# # remove blah.conf ourselves.
# }
# doc_feature1='Feature one works'
# test_feature1() {
# execute programUnderTest --feature1 arg1 arg2
# assertExitStatus '==' 0
# assertRealTime --message='ran in under half a second' '<=' 0.5
# assertStdoutIs ""
# assertStderrIs ""
# tfw_cat arg1
# }
# doc_feature2='Feature two fails with status 1'
# setup_feature2() {
# # Overrides setup(), so we have to call it ourselves explicitly
# # here if we still want it.
# setup
# echo "option=specialValue" >>$BLAH_CONFIG
# }
# test_feature2() {
# execute programUnderTest --feature2 arg1 arg2
# assertExitStatus '==' 1
# assertStdoutIs -e "Response:\tok\n"
# assertStderrGrep "^ERROR: missing arg3$"
# }
# runTests "$@"
AWK=awk
SED=sed
GREP=grep
TSFMT='+%Y-%m-%d %H:%M:%S'
SYSTYPE="$(uname -s)"
case "$SYSTYPE" in
SunOS | Darwin)
AWK=gawk
SED=gsed
GREP=ggrep
;;
Linux)
# Get nanosecond resolution
TSFMT='+%Y-%m-%d %H:%M:%S.%N'
;;
esac
usage() {
echo -n "\
Usage: $0 [options] [--]
Options:
-h --help Print this usage message
-t --trace Log shell "set -x" trace during tests
-v --verbose Send test log to output during execution
-j 0 --jobs Run all tests in parallel
-j N --jobs=N Run tests in parallel, at most N at a time (default
N=1)
-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
-t N --timeout=N Override default timeout, make it N seconds instead
of 60
-f PRE --filter=PRE Only execute tests whose names start with PRE
-f N --filter=N Only execute test number N
-f M-N --filter=M-N Only execute tests with numbers in range M-N inclusive
-f -N --filter=-N Only execute tests with numbers <= N
-f N- --filter=N- Only execute tests with numbers >= N
-f ... --filter=M,N,... Only execute tests with number M or N or ...
-c --coverage Collect test coverage data
-cg --geninfo Invoke geninfo(1) to produce one coverage.info file
per test case (requires at least one --gcno-dir)
-cd DIR --gcno-dir=DIR Use test coverage GCNO files under DIR (overrides
TFW_GCNO_PATH env var)
"
}
# Utility functions for setting shopt variables and restoring their original
# value:
# local oo
# tfw_shopt oo -s extglob -u extdebug
# ...
# tfw_shopt_restore oo
tfw_shopt() {
local _var="$1"
shift
local op=s
local restore=
while [ $# -ne 0 ]
do
case "$1" in
-s) op=s;;
-u) op=u;;
*)
local opt="$1"
restore="${restore:+$restore; }shopt -$(shopt -q $opt && echo s || echo u) $opt"
shopt -$op $opt
;;
esac
shift
done
eval $_var='"$restore"'
}
tfw_shopt_restore() {
local _var="$1"
[ -n "${!_var}" ] && eval "${!_var}"
}
declare -a _tfw_included_tests=()
declare -a _tfw_test_names=()
declare -a _tfw_test_sourcefiles=()
declare -a _tfw_job_pgids=()
declare -a _tfw_forked_pids=()
declare -a _tfw_forked_labels=()
# The rest of this file is parsed for extended glob patterns.
tfw_shopt _tfw_orig_shopt -s extglob
includeTests() {
local arg
for arg; do
local path
case "$arg" in
/*) path="$(abspath "$arg")";;
*) path="$(abspath "$_tfw_script_dir/arg")";;
esac
local n=$((${#BASH_LINENO[*]} - 1))
while [ $n -gt 0 -a ${BASH_LINENO[$n]} -eq 0 ]; do
let n=n-1
done
_tfw_included_tests+=("${BASH_LINENO[$n]} $arg")
done
}
runTests() {
[ -n "${_tfw_recursive_source}" ] && return 0
_tfw_stdout=1
_tfw_stderr=2
_tfw_log_fd=
_tfw_checkBashVersion
_tfw_checkTerminfo
_tfw_checkCommandInPATH tfw_createfile
_tfw_invoking_script=$(abspath "${BASH_SOURCE[1]}")
_tfw_script_name="${_tfw_invoking_script##*/}"
_tfw_script_dir="${_tfw_invoking_script%/*}"
_tfw_cwd=$(abspath "$PWD")
_tfw_tmpdir="$(_tfw_abspath -P "${TFW_TMPDIR:-${TMPDIR:-/tmp}}")"
_tfw_tmpmain="$_tfw_tmpdir/_tfw-$$"
_tfw_njobs=1
_tfw_log_noise=true
_tfw_assert_noise=true
_tfw_logdir="${TFW_LOGDIR:-$_tfw_cwd/testlog}"
_tfw_logdir_script="$_tfw_logdir/$_tfw_script_name"
_tfw_list=false
_tfw_trace=false
_tfw_verbose=false
_tfw_stop_on_error=false
_tfw_stop_on_failure=false
_tfw_default_execute_timeout=60
_tfw_default_wait_until_timeout=60
_tfw_timeout_override=
_tfw_coverage=false
_tfw_geninfo=false
_tfw_gcno_path=()
local allargs="$*"
local -a filters=()
local oo
tfw_shopt oo -s extglob
while [ $# -ne 0 ]; do
case "$1" in
-h|--help) usage; exit 0;;
-l|--list) _tfw_list=true;;
-t|--trace) _tfw_trace=true;;
-v|--verbose) _tfw_verbose=true;;
-E|--stop-on-error) _tfw_stop_on_error=true;;
-F|--stop-on-failure) _tfw_stop_on_failure=true;;
-f) [ -n "$2" ] || _tfw_fatal "missing argument after option: $1"
filters+=("$2")
shift
;;
-f*) filters+=("${1#-?}");;
--filter=*) filters+=("${1#*=}");;
-j) [ -n "$2" ] || _tfw_fatal "missing argument after option: $1"
_tfw_is_uint "${2?}" || _tfw_fatal "invalid option: $1 $2"
_tfw_njobs=${2?}
shift
;;
-j+([0-9])) _tfw_njobs="${1#-?}";;
-j*) _tfw_fatal "invalid option: $1";;
--jobs=+([0-9])) _tfw_njobs="${1#*=}";;
--jobs=*) _tfw_fatal "invalid option: $1";;
--jobs) _tfw_njobs=0;;
-t) [ -n "$2" ] || _tfw_fatal "missing argument after option: $1"
_tfw_is_float "${2?}" || _tfw_fatal "invalid option: $1 $2"
_tfw_timeout_override="${2?}"
shift
;;
-t*)
_tfw_is_float "${1#-?}" || _tfw_fatal "invalid option: $1"
_tfw_timeout_override="${1#-?}"
;;
--timeout=*)
_tfw_is_float "${1#*=}" || _tfw_fatal "invalid option: $1"
_tfw_timeout_override="${1#*=}"
;;
-c|--coverage) _tfw_coverage=true;;
-cg|--geninfo) _tfw_coverage=true; _tfw_geninfo=true;;
-cd) [ -n "$2" ] || _tfw_fatal "missing argument after option: $1"
_tfw_gcno_path+=("$2")
shift
;;
-cd*) _tfw_gcno_path+=("${1#-?}");;
--gcno-dir=*) _tfw_gcno_path+=("${1#*=}");;
--) shift; break;;
-*) _tfw_fatal "unsupported option: $1";;
*) _tfw_fatal "spurious argument: $1";;
esac
shift
done
tfw_shopt_restore oo
if $_tfw_verbose && [ $_tfw_njobs -ne 1 ]; then
_tfw_fatal "--verbose is incompatible with --jobs=$_tfw_njobs"
fi
# Handle --gcno-dir arguments, or if none given, $TFW_GCNO_PATH env var.
# Convert into a list of absolute directory paths.
if [ ${#_tfw_gcno_path[*]} -eq -0 ]; then
local oIFS="$IFS"
IFS=:
_tfw_gcno_path=($TFW_GCNO_PATH)
IFS="$oIFS"
else
local pathdir
for pathdir in "${_tfw_gcno_path[@]}"; do
[ -d "$pathdir" ] || _tfw_fatal "--gcno-dir: no such directory: '$pathdir'"
done
fi
_tfw_gcno_dirs=()
local pathdir
for pathdir in "${_tfw_gcno_path[@]}"; do
[ -d "$pathdir" ] && _tfw_gcno_dirs+=("$(abspath "$pathdir")")
done
# Handle --geninfo option.
if $_tfw_geninfo; then
if [ ${#_tfw_gcno_dirs[*]} -eq 0 ]; then
_tfw_fatal "--geninfo: requires at least one --gcno-dir=DIR or \$TFW_GCNO_PATH env var"
fi
_tfw_checkCommandInPATH geninfo
_tfw_checkCommandInPATH gcov _tfw_gcov_path
# Check that all source files are available.
_tfw_extract_source_files_from_gcno "${_tfw_gcno_dirs[@]}"
_tfw_coverage_source_basedir=.
if [ -n "$TFW_COVERAGE_SOURCE_BASE_DIR" ]; then
[ -d "$TFW_COVERAGE_SOURCE_BASE_DIR" ] || _tfw_fatal "--geninfo: no such directory '$TFW_COVERAGE_SOURCE_BASE_DIR' (\$TFW_COVERAGE_SOURCE_BASE_DIR)"
_tfw_coverage_source_basedir="$TFW_COVERAGE_SOURCE_BASE_DIR"
fi
local src
for src in "${_tfw_coverage_source_files[@]}"; do
local path="$_tfw_coverage_source_basedir/$src"
[ -r "$path" ] || _tfw_fatal "--geninfo: missing source file $path"
done
fi
# Enumerate all the test cases.
_tfw_list_tests
# If we are only asked to list them, then do so and finish.
if $_tfw_list; then
local testNumber
for ((testNumber = 1; testNumber <= ${#_tfw_test_names[*]}; ++testNumber)); do
local testSourceFile="${_tfw_test_sourcefiles[$(($testNumber - 1))]}"
local testName="${_tfw_test_names[$(($testNumber - 1))]}"
if _tfw_filter_predicate "$testNumber" "$testName" "${filters[@]}"; then
echo "$testNumber $testName ${testSourceFile#$_tfw_script_dir/}"
fi
done
return 0
fi
# Create base temporary working directory.
trap '_tfw_status=$?; _tfw_killtests 2>/dev/null; rm -rf "$_tfw_tmpmain"; trap - EXIT; exit $_tfw_status' EXIT SIGHUP SIGINT SIGTERM
rm -rf "$_tfw_tmpmain"
mkdir -p "$_tfw_tmpmain" || return $?
# Create an empty results directory.
_tfw_results_dir="$_tfw_tmpmain/results"
mkdir "$_tfw_results_dir" || return $?
# Create an empty log directory.
mkdir -p "$_tfw_logdir_script" || return $?
rm -r -f "$_tfw_logdir_script"/*
# Enable job control.
set -m
# Iterate through all test cases, starting a new test whenever the number of
# running tests is less than the job limit.
_tfw_testcount=0
_tfw_passcount=0
_tfw_failcount=0
_tfw_errorcount=0
_tfw_fatalcount=0
_tfw_job_pgids=()
_tfw_job_pgids[0]=
_tfw_test_number_watermark=0
local testNumber
local testPosition=0
for ((testNumber = 1; testNumber <= ${#_tfw_test_names[*]}; ++testNumber)); do
local testSourceFile="${_tfw_test_sourcefiles[$(($testNumber - 1))]}"
local testName="${_tfw_test_names[$(($testNumber - 1))]}"
local scriptName="${testSourceFile#$_tfw_script_dir/}"
_tfw_filter_predicate "$testNumber" "$testName" "${filters[@]}" || continue
let ++testPosition
let ++_tfw_testcount
# Wait for any existing child process to finish.
while [ $_tfw_njobs -ne 0 -a $(_tfw_count_running_jobs) -ge $_tfw_njobs ]; do
_tfw_wait_job_finish
_tfw_harvest_jobs
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.
_tfw_echo_progress $testPosition $testNumber "$testSourceFile" $testName
if $_tfw_verbose || [ $_tfw_njobs -ne 1 ]; then
echo
fi
echo "$testPosition $testNumber $testName" NONE "$testSourceFile" >"$_tfw_results_dir/$testNumber"
( #)#<-- fixes Vim syntax highlighting
_tfw_status=
# The directory where this test's log.txt and other artifacts are
# deposited.
_tfw_logdir_test="$_tfw_logdir_script/$testNumber.$testName"
mkdir "$_tfw_logdir_test" || _tfw_fatalexit
# All files created by this test belong inside a temporary directory,
# whose path must be kept short because it is used to construct named
# socket paths, which have a limited length. The directory name is
# based on a unique decimal number that does not coincide with other
# tests being run concurrently, _including tests that may be running in
# other test scripts on the same host_, so $testNumber may not be
# unique. The following loop attempts to create the temporary
# directory, trying successive unique numbers, until it succeeds. It
# should be immune to races because the mkdir(2) system call is atomic.
local failmsg
_tfw_tmp=
for try in {0..20}; do
_tfw_unique=$RANDOM
local tmpdir="$_tfw_tmpdir/_tf-$_tfw_unique"
if failmsg="$(mkdir "$tmpdir" 2>&1)"; then
_tfw_tmp="$tmpdir"
break
fi
done
[ -d "$_tfw_tmp" ] || _tfw_fatal "$failmsg"
trap '_tfw_status=$?; rm -rf "$_tfw_tmp"; _tfw_exit' EXIT SIGHUP SIGINT SIGTERM
# Set up test coverage data directory, which contains all the .gcno
# files of the executable(s) under test. If using geninfo(1) to
# generate coverage info files, then link to all the source files, to
# ensure that temporary .gcov files are created in this directory and
# not in the repository's base directory (which would cause race
# conditions).
if $_tfw_coverage; then
export GCOV_PREFIX="$_tfw_logdir_test/gcov"
export GCOV_PREFIX_STRIP=0
mkdir "$GCOV_PREFIX" || _tfw_fatalexit
# Link to GCNO files.
if [ ${#_tfw_gcno_dirs[*]} -ne 0 ]; then
find "${_tfw_gcno_dirs[@]}" -type f -name '*.gcno' -print0 | cpio -0pdl --quiet "$GCOV_PREFIX"
fi
# Link source files to where geninfo(1) will always find them before
# finding the original source files.
if $_tfw_geninfo; then
pushd "$_tfw_coverage_source_basedir" >/dev/null || _tfw_fatalexit
find "${_tfw_coverage_source_files[@]}" -maxdepth 0 -print0 | cpio -0pdl --quiet "$GCOV_PREFIX"
popd >/dev/null
fi
fi
## XXX _tfw_geninfo_initial "$scriptName/$testName" >$_tfw_tmp/log.geninfo 2>&1
local start_time=$(_tfw_timestamp)
local finish_time=unknown
( #)#<-- fixes Vim syntax highlighting
_tfw_result=FATAL
_tfw_stdout=5
_tfw_stderr=5
_tfw_log_fd=6
BASH_XTRACEFD=7
# Disable job control.
set +m
# If the test is from a different source script than the one
# invoking us, then source that file to pull in all the test
# definitions.
if [ "$testSourceFile" != "$_tfw_invoking_script" ]; then
_tfw_recursive_source=true
source "$testSourceFile"
unset _tfw_recursive_source
fi
declare -f test_$testName >/dev/null || _tfw_fatal "test_$testName not defined"
# Where per-forked-process files get stored -- see fork().
_tfw_process_tmp="$_tfw_tmp"
# Environment variables and temporary directories that test cases
# depend upon.
export COLUMNS=80 # for ls(1) multi-column output
export TFWSOURCE="$testSourceFile"
export TFWLOG="$_tfw_logdir_test"
export TFWUNIQUE=$_tfw_unique
export TFWVAR="$_tfw_tmp/var"
mkdir $TFWVAR || _tfw_fatalexit
export TFWTMP="$_tfw_tmp/tmp"
mkdir $TFWTMP || _tfw_fatalexit
cd $TFWTMP || _tfw_fatalexit
if $_tfw_verbose; then
# Find the PID of the current subshell process. Cannot use $BASHPID
# because MacOS only has Bash-3.2, and $BASHPID was introduced in Bash-4.
local mypid=$($BASH -c 'echo $PPID')
# These tail processes will die when the current subshell exits.
tail --pid=$mypid --follow $_tfw_tmp/log.stdout >&$_tfw_stdout 2>/dev/null &
tail --pid=$mypid --follow $_tfw_tmp/log.stderr >&$_tfw_stderr 2>/dev/null &
fi
# Execute the test case.
_tfw_phase=setup
_tfw_result=ERROR
_tfw_status=
trap "_tfw_status=\$?; _tfw_finalise $testName; _tfw_teardown $testName; _tfw_exit" EXIT SIGHUP SIGINT SIGTERM
_tfw_setup $testName
_tfw_result=FAIL
_tfw_phase=testcase
tfw_log "# CALL test_$testName()"
$_tfw_trace && set -x
test_$testName
set +x
case $_tfw_phase in
testcase-setup) _tfw_error "test terminated within fixture (missing a end_fixture call?)";;
testcase);;
*) _tfw_fatal "internal error: _tfw_phase=$_tfw_phase";;
esac
_tfw_result=PASS
exit 0
) <&- 5>&1 5>&2 6>"$_tfw_tmp/log.stdout" 1>&6 2>"$_tfw_tmp/log.stderr" 7>"$_tfw_tmp/log.xtrace"
local stat=$?
finish_time=$(_tfw_timestamp)
local result=FATAL
case $stat in
254) result=ERROR;;
1) result=FAIL;;
0) result=PASS;;
esac
echo "$testPosition $testNumber $testName $result $testSourceFile" >|"$_tfw_results_dir/$testNumber"
{
echo "Name: $testName ($scriptName)"
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_test/log.txt"
mv "$_tfw_logdir_test" "$_tfw_logdir_test.$result"
_tfw_logdir_test="$_tfw_logdir_test.$result"
if $_tfw_geninfo; then
local testname=$(_tfw_string_to_identifier "$scriptName/$testName")
local coverage="$_tfw_logdir_test/coverage.info"
{
echo '++++++++++ log.geninfo ++++++++++'
_tfw_run_geninfo "$coverage" --test-name "$testname" 2>&1
echo '++++++++++'
} >>"$_tfw_logdir_test/log.txt"
fi
exit 0
) </dev/null &
local job=$(jobs %% 2>/dev/null | $SED -n -e '1s/^\[\([0-9]\{1,\}\)\].*/\1/p')
if [ -n "${_tfw_job_pgids[$job]}" ]; then
_tfw_harvest_job $job
fi
_tfw_job_pgids[$job]=$(jobs -p %$job 2>/dev/null)
ln -f -s "$_tfw_results_dir/$testNumber" "$_tfw_results_dir/job-$job"
done 2>>"$_tfw_tmpmain/stderr"
# Wait for all child processes to finish.
while _tfw_any_running_jobs; do
_tfw_wait_job_finish
_tfw_harvest_jobs
done 2>>"$_tfw_tmpmain/stderr"
# Echo result summary and exit with success if no failures or errors.
s=$([ $_tfw_testcount -eq 1 ] || echo s)
echo "$_tfw_testcount test$s, $_tfw_passcount pass, $_tfw_failcount fail, $_tfw_errorcount error"
cat "$_tfw_tmpmain/stderr" >&2
# Clean up working directory.
rm -rf "$_tfw_tmpmain"
trap - EXIT SIGHUP SIGINT SIGTERM
[ $_tfw_fatalcount -eq 0 -a $_tfw_failcount -eq 0 -a $_tfw_errorcount -eq 0 ]
}
_tfw_killtests() {
if [ $_tfw_njobs -eq 1 ]; then
echo " killing..."
else
echo -n -e "\r\rKilling tests..."
fi
trap '' SIGHUP SIGINT SIGTERM
local pgid
for pgid in $(jobs -p); do
kill -TERM -$pgid
done
wait
}
_tfw_count_running_jobs() {
local count=0
local job
for ((job = 1; job < ${#_tfw_job_pgids[*]}; ++job)); do
[ -n "${_tfw_job_pgids[$job]}" ] && let count=count+1
done
echo $count
}
_tfw_any_running_jobs() {
local job
for ((job = 1; job < ${#_tfw_job_pgids[*]}; ++job)); do
[ -n "${_tfw_job_pgids[$job]}" ] && return 0
done
return 1
}
_tfw_wait_job_finish() {
if [ $_tfw_njobs -eq 1 ]; then
wait >/dev/null 2>/dev/null
else
# This is the only way known to get the effect of a 'wait' builtin that
# will return when _any_ child dies (or after a one-second timeout).
set -m
sleep 1 &
local spid=$!
trap "kill -TERM $spid 2>/dev/null" SIGCHLD
wait $spid >/dev/null 2>/dev/null
trap - SIGCHLD
fi
}
_tfw_harvest_jobs() {
local job
for ((job = 1; job < ${#_tfw_job_pgids[*]}; ++job)); do
if [ -n "${_tfw_job_pgids[$job]}" ]; then
jobs %$job >/dev/null 2>/dev/null || _tfw_harvest_job $job
fi
done
}
_tfw_harvest_job() {
local job="$1"
# Kill any residual processes from the test case.
local pgid=${_tfw_job_pgids[$job]}
[ -n "$pgid" ] && kill -TERM -$pgid 2>/dev/null
_tfw_job_pgids[$job]=
# Report the test script outcome.
if [ -s "$_tfw_results_dir/job-$job" ]; then
local testPosition
local testNumber
local testName
local result
local testSourceFile
_tfw_unpack_words "$(<"$_tfw_results_dir/job-$job")" testPosition testNumber testName result testSourceFile
case "$result" in
ERROR)
let _tfw_errorcount=_tfw_errorcount+1
;;
PASS)
let _tfw_passcount=_tfw_passcount+1
;;
FAIL)
let _tfw_failcount=_tfw_failcount+1
;;
*)
result=FATAL
let _tfw_fatalcount=_tfw_fatalcount+1
;;
esac
local lines
if ! $_tfw_verbose && [ $_tfw_njobs -eq 1 ]; then
_tfw_echo_progress $testPosition $testNumber "$testSourceFile" $testName $result
echo
elif ! $_tfw_verbose && lines=$($_tfw_tput lines); then
local travel=$(($_tfw_test_number_watermark - $testPosition + 1))
if [ $travel -gt 0 -a $travel -lt $lines ] && $_tfw_tput cuu $travel ; then
_tfw_echo_progress $testPosition $testNumber "$testSourceFile" $testName $result
echo
travel=$(($_tfw_test_number_watermark - $testPosition))
[ $travel -gt 0 ] && $_tfw_tput cud $travel
fi
else
_tfw_echo_progress $testPosition $testNumber "$testSourceFile" $testName $result
echo
fi
else
_tfw_echoerr "${BASH_SOURCE[1]}: job %$job terminated without result"
fi
rm -f "$_tfw_results_dir/job-$job"
}
_tfw_echo_progress() {
(
local script=
if [ "$testSourceFile" != "$_tfw_invoking_script" ]; then
_tfw_recursive_source=true
source "$testSourceFile"
unset _tfw_recursive_source
script=" (${3#$_tfw_script_dir/})"
fi
local docvar="doc_$4"
echo -n -e '\r'
echo -n "$2 ["
_tfw_echo_result "$5"
echo -n "]$script ${!docvar:-$4}"
)
[ $1 -gt $_tfw_test_number_watermark ] && _tfw_test_number_watermark=$1
}
_tfw_echo_result() {
local result="$1"
case "$result" in
ERROR | FATAL)
$_tfw_tput setaf 1
$_tfw_tput rev
echo -n "$result"
$_tfw_tput sgr0
$_tfw_tput op
;;
PASS)
$_tfw_tput setaf 2
echo -n "$result"
$_tfw_tput op
echo -n "."
;;
FAIL)
$_tfw_tput setaf 1
echo -n "$result"
$_tfw_tput op
echo -n "."
;;
*)
result="$result....."
echo -n "${result:0:5}"
;;
esac
}
_tfw_extract_source_files_from_gcno() {
# This should possibly be done by creating a binary utility that knows how to
# disassemble GCNO files. In the meantime, this approach seems to work:
# simply extract all strings from all GCNO files that match *.c or *.h.
local IFS='
'
_tfw_coverage_source_files=($(find "$@" -type f -name '*.gcno' -print0 | xargs -0 strings | grep '\.[ch]$' | sort -u))
}
_tfw_run_geninfo() {
local infofile="$1"
shift
geninfo \
--rc lcov_tmp_dir="$_tfw_tmp" \
--gcov-tool "$_tfw_gcov_path" \
--output-file "$infofile" \
--no-external \
"$@" \
"$_tfw_logdir_test/gcov"
# Cook the absolute source file paths in the info file to refer to the
# original source files, not the links we placed into the gcov subdirectory
# in order to avoid race conditions.
local basedir="$(abspath "$_tfw_coverage_source_basedir")"
$SED -i -e "/^SF:/s:$_tfw_logdir_test/gcov:$basedir:" "$infofile"
}
_tfw_string_to_identifier() {
echo "$1" | $SED -e 's/\//__/g' -e 's/[^0-9a-zA-Z_]/_/g'
}
# Internal (private) functions that are not to be invoked directly from test
# scripts.
# Add shell quotation to the given arguments, so that when expanded using
# 'eval', the exact same argument results. This makes argument handling fully
# immune to spaces and shell metacharacters.
_tfw_shellarg() {
local arg
_tfw_args=()
for arg; do
case "$arg" in
'' | *[^A-Za-z_0-9.,:=+\/-]* ) _tfw_args+=("'${arg//'/'\\''}'");;
*) _tfw_args+=("$arg");;
esac
done
}
# Echo the absolute path of the given path, using only Bash builtins.
_tfw_abspath() {
cdopt=-L
if [ $# -gt 1 -a "${1:0:1}" = - ]; then
cdopt="$1"
shift
fi
case "$1" in
*/)
builtin echo $(_tfw_abspath $cdopt "${1%/}")/
;;
/*/*)
if [ -d "$1" ]; then
(CDPATH= builtin cd $cdopt "$1" && builtin echo "$PWD")
else
builtin echo $(_tfw_abspath $cdopt "${1%/*}")/"${1##*/}"
fi
;;
/*)
echo "$1"
;;
*/*)
if [ -d "$1" ]; then
(CDPATH= builtin cd $cdopt "$1" && builtin echo "$PWD")
else
builtin echo $(_tfw_abspath $cdopt "${1%/*}")/"${1##*/}"
fi
;;
. | ..)
(CDPATH= builtin cd $cdopt "$1" && builtin echo "$PWD")
;;
*)
(CDPATH= builtin cd $cdopt . && builtin echo "$PWD/$1")
;;
esac
}
_tfw_timestamp() {
local ts=$(date "$TSFMT")
echo "${ts%[0-9][0-9][0-9][0-9][0-9][0-9]}"
}
_tfw_setup() {
local testName="$1"
tfw_log '# SETUP'
case `type -t setup_$testName` in
function)
tfw_log "# call setup_$testName()"
$_tfw_trace && set -x
setup_$testName $testName
set +x
;;
*)
tfw_log "# call setup($testName)"
$_tfw_trace && set -x
setup $testName
set +x
;;
esac
tfw_log '# END SETUP'
}
_tfw_finalise() {
local testName="$1"
_tfw_phase=finalise
tfw_log '# FINALISE'
case `type -t finally_$testName` in
function)
tfw_log "# CALL finally_$testName()"
$_tfw_trace && set -x
finally_$testName
set +x
;;
*)
tfw_log "# CALL finally($testName)"
$_tfw_trace && set -x
finally $testName
set +x
;;
esac
fork_terminate_all
fork_wait_all
tfw_log '# END FINALLY'
}
_tfw_teardown() {
local testName="$1"
_tfw_phase=teardown
tfw_log '# TEARDOWN'
case `type -t teardown_$testName` in
function)
tfw_log "# CALL teardown_$testName()"
$_tfw_trace && set -x
teardown_$testName
set +x
;;
*)
tfw_log "# CALL teardown($testName)"
$_tfw_trace && set -x
teardown $testName
set +x
;;
esac
tfw_log '# END TEARDOWN'
}
_tfw_exit() {
case $_tfw_status:$_tfw_result in
255:* | *:FATAL ) exit 255;;
254:* | *:ERROR ) exit 254;;
1:* | *:FAIL ) exit 1;;
0:* | *:PASS ) exit 0;;
esac
_tfw_fatal "_tfw_status='$_tfw_status' _tfw_result='$_tfw_result'"
}
# Executes $_tfw_executable with the given arguments.
_tfw_execute() {
local _tfw_stdout_file_default="$_tfw_process_tmp/stdout"
local _tfw_stderr_file_default="$_tfw_process_tmp/stderr"
export TFWSTDOUT="${_tfw_stdout_file:-$_tfw_stdout_file_default}"
export TFWSTDERR="${_tfw_stderr_file:-$_tfw_stderr_file_default}"
>|"$TFWSTDOUT"
>|"$TFWSTDERR"
if ! [ "$TFWSTDOUT" -ef "$_tfw_stdout_file_default" ]; then
rm -f "$_tfw_stdout_file_default"
ln "$TFWSTDOUT" "$_tfw_stdout_file_default"
fi
if ! [ "$TFWSTDERR" -ef "$_tfw_stderr_file_default" ]; then
rm -f "$_tfw_stderr_file_default"
ln "$TFWSTDERR" "$_tfw_stderr_file_default"
fi
export TFWEXECUTED=$(shellarg "${_tfw_executable##*/}" "$@")
echo "$TFWEXECUTED" >"$_tfw_process_tmp/executing"
if $_tfw_opt_core_backtrace; then
ulimit -S -c unlimited
rm -f core
fi
{
time -p "$_tfw_executable" "$@" >>"$TFWSTDOUT" 2>>"$TFWSTDERR"
} 2>"$_tfw_process_tmp/times" &
local subshell_pid=$!
local timer_pid=
local timeout=${_tfw_timeout_override:-${_tfw_opt_timeout:-${TFW_EXECUTE_TIMEOUT:-$_tfw_default_execute_timeout}}}
if [ -n "$timeout" ]; then
_tfw_is_float "$timeout" || error "invalid timeout '$timeout'"
fi
if [ -n "$timeout" ]; then
if type pgrep >/dev/null 2>/dev/null; then
( #)#( <<- fixes Vim syntax colouring
# For some reason, set -e does not work here. So all the following
# commands are postfixed with || exit $?
local executable_pid=$(pgrep -P $subshell_pid) || exit $?
[ -n "$executable_pid" ] || exit $?
if [ -n "$timeout" ]; then
sleep $timeout || exit $?
fi
kill -0 $executable_pid || exit $?
tfw_log "# timeout after $timeout seconds, sending SIGABRT to pid $executable_pid ($TFWEXECUTED)" || exit $?
kill -ABRT $executable_pid || exit $?
sleep 2 || exit $?
kill -0 $executable_pid || exit $?
tfw_log "# sending second SIGABRT to pid $executable_pid ($TFWEXECUTED)" || exit $?
kill -ABRT $executable_pid || exit $?
sleep 2 || exit $?
kill -0 $executable_pid || exit $?
tfw_log "# sending SIGKILL to pid $executable_pid ($TFWEXECUTED)" || exit $?
kill -KILL $executable_pid || exit $?
exit 0
) 2>/dev/null &
timer_pid=$!
else
tfw_log "# execution timeout ($timeout seconds) not supported because pgrep(1) not available"
fi
fi
wait $subshell_pid
_tfw_exitStatus=$?
if [ -n "$timer_pid" ]; then
while kill -0 $timer_pid 2>/dev/null; do
pkill -P $timer_pid 2>/dev/null
done
wait $timer_pid
fi
rm -f "$_tfw_process_tmp/executing"
# Deal with core dump.
if $_tfw_opt_core_backtrace && [ -s core ]; then
tfw_core_backtrace "$_tfw_executable" core
fi
# Deal with exit status.
if [ -n "$_tfw_opt_exit_status" ]; then
_tfw_message="exit status ($_tfw_exitStatus) of ($TFWEXECUTED) is $_tfw_opt_exit_status"
_tfw_assert [ "$_tfw_exitStatus" -eq "$_tfw_opt_exit_status" ] || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
else
$_tfw_assert_noise && tfw_log "# exit status of ($TFWEXECUTED) = $_tfw_exitStatus"
fi
# Parse execution time report.
if true || [ -s "$_tfw_process_tmp/times" ]; then
if ! _tfw_parse_times_to_milliseconds real realtime_ms ||
! _tfw_parse_times_to_milliseconds user usertime_ms ||
! _tfw_parse_times_to_milliseconds sys systime_ms
then
tfw_log '# malformed output from time:'
tfw_cat -v "$_tfw_process_tmp/times"
fi
else
realtime_ms=
usertime_ms=
systime_ms=
fi
return 0
}
_tfw_parse_times_to_milliseconds() {
local label="$1"
local var="$2"
local milliseconds=$($AWK '$1 == "'"$label"'" {
value = $2
minutes = 0
if (match(value, "[0-9]+m")) {
minutes = substr(value, RSTART, RLENGTH - 1)
value = substr(value, 1, RSTART - 1) substr(value, RSTART + RLENGTH)
}
if (substr(value, length(value)) == "s") {
value = substr(value, 1, length(value) - 1)
}
if (match(value, "^[0-9]+(\\.[0-9]+)?$")) {
seconds = value + 0
print (minutes * 60 + seconds) * 1000
}
}' $_tfw_process_tmp/times)
[ -z "$milliseconds" ] && return 1
[ -n "$var" ] && eval $var=$milliseconds
return 0
}
_tfw_assert() {
if ! tfw_run "$@"; then
_tfw_failmsg "assertion failed: ${_tfw_message:-$*}"
_tfw_backtrace
return 1
fi
return 0
}
declare -a _tfw_opt_dump_on_fail
_tfw_dump_on_fail() {
local arg
for arg; do
local _found=false
local _f
for _f in "${_tfw_opt_dump_on_fail[@]}"; do
if [ "$_f" = "$arg" ]; then
_found=true
break
fi
done
$_found || _tfw_opt_dump_on_fail+=("$arg")
done
}
_tfw_getopts() {
local context="$1"
shift
_tfw_executable=
_tfw_stdout_file=
_tfw_stderr_file=
_tfw_opt_core_backtrace=false
_tfw_message=
_tfw_opt_dump_on_fail=()
_tfw_opt_error_on_fail=false
_tfw_opt_exit_status=
_tfw_opt_timeout=
_tfw_opt_sleep=
_tfw_opt_matches=
_tfw_opt_line=
_tfw_opt_line_sed=
_tfw_opt_line_msg=
_tfw_opt_grepopts=()
_tfw_getopts_shift=0
local oo
tfw_shopt oo -s extglob
while [ $# -ne 0 ]; do
case "$context:$1" in
*:--stdout) _tfw_dump_on_fail --stdout;;
*:--stderr) _tfw_dump_on_fail --stderr;;
assert*:--dump-on-fail=*) _tfw_dump_on_fail "${1#*=}";;
@(assert*|execute*|fork_wait*):--error-on-fail) _tfw_opt_error_on_fail=true;;
assert*:--message=*) _tfw_message="${1#*=}";;
execute:--exit-status=+([0-9])) _tfw_opt_exit_status="${1#*=}";;
execute:--exit-status=*) _tfw_error "invalid value: $1";;
execute*:--executable=) _tfw_error "missing value: $1";;
execute*:--executable=*) _tfw_executable="${1#*=}";;
execute*:--stdout-file=) _tfw_error "missing value: $1";;
execute*:--stdout-file=*) _tfw_stdout_file="${1#*=}";;
execute*:--stderr-file=) _tfw_error "missing value: $1";;
execute*:--stderr-file=*) _tfw_stderr_file="${1#*=}";;
execute*:--core-backtrace) _tfw_opt_core_backtrace=true;;
@(execute*|wait_until):--timeout=@(+([0-9])?(.+([0-9]))|*([0-9]).+([0-9]))) _tfw_opt_timeout="${1#*=}";;
@(execute*|wait_until):--timeout=*) _tfw_error "invalid value: $1";;
wait_until:--sleep=*) _tfw_is_float "${1#*=}" || _tfw_error "invalid value: $1"; _tfw_opt_sleep="${1#*=}";;
*grep:--fixed-strings) _tfw_opt_grepopts+=(-F);;
assertcontentgrep:--matches=+([0-9])) _tfw_opt_matches="${1#*=}";;
assertcontentgrep:--matches=*) _tfw_error "invalid value: $1";;
assertcontentgrep:--ignore-case) _tfw_opt_grepopts+=(-i);;
assertcontent*:--line=+([0-9])) _tfw_opt_line="${1#*=}"; _tfw_opt_line_msg="line $_tfw_opt_line";;
assertcontent*:--line=+([0-9])..) _tfw_opt_line="${1#*=}\$"; _tfw_opt_line_msg="lines $_tfw_opt_line";;
assertcontent*:--line=..+([0-9])) _tfw_opt_line="1${1#*=}"; _tfw_opt_line_msg="lines $_tfw_opt_line";;
assertcontent*:--line=+([0-9])..+([0-9])) _tfw_opt_line="${1#*=}"; _tfw_opt_line_msg="lines $_tfw_opt_line";;
assertcontent*:--line=*) _tfw_error "invalid value: $1";;
*:--) let _tfw_getopts_shift=_tfw_getopts_shift+1; shift; break;;
*:--*) _tfw_error "unsupported option: $1";;
*) break;;
esac
let _tfw_getopts_shift=_tfw_getopts_shift+1
shift
done
[ -n "$_tfw_opt_line" ] && _tfw_opt_line_sed="${_tfw_opt_line/../,}"
case "$context" in
execute*)
if [ -z "$_tfw_executable" ]; then
_tfw_executable="$1"
let _tfw_getopts_shift=_tfw_getopts_shift+1
shift
fi
[ -z "$_tfw_executable" ] && _tfw_error "missing executable argument"
;;
esac
tfw_shopt_restore oo
return 0
}
_tfw_is_uint() {
local oo
tfw_shopt oo -s extglob
local ret=1
case "$1" in
+([0-9])) ret=0;;
esac
tfw_shopt_restore oo
return $ret
}
_tfw_is_float() {
local oo
tfw_shopt oo -s extglob
local ret=1
case "$1" in
@(+([0-9])?(.+([0-9]))|*([0-9]).+([0-9]))) ret=0;;
esac
tfw_shopt_restore oo
return $ret
}
_tfw_matches_rexp() {
local rexp="$1"
shift
local arg
for arg; do
if ! echo "$arg" | $GREP -q -e "$rexp"; then
return 1
fi
done
return 0
}
_tfw_parse_expr() {
local _expr="$*"
_tfw_expr=()
while [ $# -ne 0 ]; do
case "$1" in
'&&' | '||' | '!' | '(' | ')')
_tfw_expr+=("$1")
shift
;;
*)
if [ $# -lt 3 ]; then
_tfw_error "invalid expression: $_expr"
return $?
fi
case "$2" in
'==') _tfw_expr+=("[" "$1" "-eq" "$3" "]");;
'!=') _tfw_expr+=("[" "$1" "-ne" "$3" "]");;
'<=') _tfw_expr+=("[" "$1" "-le" "$3" "]");;
'<') _tfw_expr+=("[" "$1" "-lt" "$3" "]");;
'>=') _tfw_expr+=("[" "$1" "-ge" "$3" "]");;
'>') _tfw_expr+=("[" "$1" "-gt" "$3" "]");;
'~') _tfw_expr+=("_tfw_matches_rexp" "$3" "$1");;
'!~') _tfw_expr+=("!" "_tfw_matches_rexp" "$3" "$1");;
*)
_tfw_error "invalid expression: $_expr"
return $?
;;
esac
shift 3
;;
esac
done
return 0
}
_tfw_assertExpr() {
_tfw_parse_expr "$@" || return $?
_tfw_shellarg "${_tfw_expr[@]}"
_tfw_assert eval "${_tfw_args[@]}"
}
_tfw_get_content() {
case "$_tfw_opt_line_sed" in
'') cat "$1" >|"$_tfw_process_tmp/content" || error "cat failed";;
*) $SED -n -e "${_tfw_opt_line_sed}p" "$1" >|"$_tfw_process_tmp/content" || error "sed failed";;
esac
}
_tfw_assert_stdxxx_is() {
local qual="$1"
shift
_tfw_getopts assertcontentis --$qual --stderr "$@"
shift $((_tfw_getopts_shift - 2))
if [ $# -lt 1 ]; then
_tfw_error "incorrect arguments"
return $?
fi
[ -r "$_tfw_process_tmp/$qual" ] || fail "no $qual" || return $?
_tfw_get_content "$_tfw_process_tmp/$qual" || return $?
local message="${_tfw_message:-${_tfw_opt_line_msg:+$_tfw_opt_line_msg of }$qual of ($TFWEXECUTED) is $(shellarg "$@")}"
echo -n "$@" >"$_tfw_process_tmp/stdxxx_is.tmp"
if ! cmp -s "$_tfw_process_tmp/stdxxx_is.tmp" "$_tfw_process_tmp/content"; then
_tfw_failmsg "assertion failed: $message"
_tfw_backtrace
return 1
fi
$_tfw_assert_noise && tfw_log "# assert $message"
return 0
}
_tfw_assert_stdxxx_linecount() {
local qual="$1"
shift
_tfw_getopts assert --$qual --stderr "$@"
shift $((_tfw_getopts_shift - 2))
if [ $# -lt 1 ]; then
_tfw_error "incorrect arguments"
return $?
fi
[ -r "$_tfw_process_tmp/$qual" ] || fail "no $qual" || return $?
local lineCount=$(( $(cat "$_tfw_process_tmp/$qual" | wc -l) + 0 ))
[ -z "$_tfw_message" ] && _tfw_message="$qual line count ($lineCount) $*"
_tfw_assertExpr "$lineCount" "$@" || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
return 0
}
_tfw_assert_stdxxx_grep() {
local qual="$1"
shift
_tfw_getopts assertcontentgrep --$qual --stderr "$@"
shift $((_tfw_getopts_shift - 2))
if [ $# -ne 1 ]; then
_tfw_error "incorrect arguments"
return $?
fi
[ -r "$_tfw_process_tmp/$qual" ] || fail "no $qual" || return $?
_tfw_get_content "$_tfw_process_tmp/$qual" || return $?
_tfw_assert_grep "${_tfw_opt_line_msg:+$_tfw_opt_line_msg of }$qual of ($TFWEXECUTED)" "$_tfw_process_tmp/content" "$@"
}
_tfw_assert_grep() {
local label="$1"
local file="$2"
local pattern="$3"
local message=
if ! [ -e "$file" ]; then
_tfw_error "$file does not exist"
ret=$?
elif ! [ -f "$file" ]; then
_tfw_error "$file is not a regular file"
ret=$?
elif ! [ -r "$file" ]; then
_tfw_error "$file is not readable"
ret=$?
else
local matches=$(( $($GREP "${_tfw_opt_grepopts[@]}" --regexp="$pattern" "$file" | wc -l) + 0 ))
local done=false
local ret=0
local info="$matches match"$([ $matches -ne 1 ] && echo "es")
local oo
tfw_shopt oo -s extglob
case "$_tfw_opt_matches" in
'')
done=true
message="${_tfw_message:-$label contains a line matching \"$pattern\"}"
if [ $matches -ne 0 ]; then
$_tfw_assert_noise && tfw_log "# assert $message"
else
_tfw_failmsg "assertion failed ($info): $message"
ret=1
fi
;;
esac
case "$_tfw_opt_matches" in
+([0-9]))
done=true
local s=$([ $_tfw_opt_matches -ne 1 ] && echo s)
message="${_tfw_message:-$label contains exactly $_tfw_opt_matches line$s matching \"$pattern\"}"
if [ $matches -eq $_tfw_opt_matches ]; then
$_tfw_assert_noise && tfw_log "# assert $message"
else
_tfw_failmsg "assertion failed ($info): $message"
ret=1
fi
;;
esac
case "$_tfw_opt_matches" in
+([0-9])-*([0-9]))
done=true
local bound=${_tfw_opt_matches%-*}
local s=$([ $bound -ne 1 ] && echo s)
message="${_tfw_message:-$label contains at least $bound line$s matching \"$pattern\"}"
if [ $matches -ge $bound ]; then
$_tfw_assert_noise && tfw_log "# assert $message"
else
_tfw_failmsg "assertion failed ($info): $message"
ret=1
fi
;;
esac
case "$_tfw_opt_matches" in
*([0-9])-+([0-9]))
done=true
local bound=${_tfw_opt_matches#*-}
local s=$([ $bound -ne 1 ] && echo s)
message="${_tfw_message:-$label contains at most $bound line$s matching \"$pattern\"}"
if [ $matches -le $bound ]; then
$_tfw_assert_noise && tfw_log "# assert $message"
else
_tfw_failmsg "assertion failed ($info): $message"
ret=1
fi
;;
esac
if ! $done; then
_tfw_error "unsupported value for --matches=$_tfw_opt_matches"
ret=$?
fi
tfw_shopt_restore oo
fi
if [ $ret -ne 0 ]; then
_tfw_backtrace
fi
return $ret
}
# 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
# later.
_tfw_echoerr() {
echo "$@" >&$_tfw_stderr
if [ $_tfw_stderr -ne 2 ]; then
echo "$@" >&2
fi
}
_tfw_checkBashVersion() {
[ -z "$BASH_VERSION" ] && _tfw_fatal "not running in Bash (/bin/bash) shell"
if [ -n "${BASH_VERSINFO[*]}" ]; then
[ ${BASH_VERSINFO[0]} -gt 3 ] && return 0
if [ ${BASH_VERSINFO[0]} -eq 3 ]; then
[ ${BASH_VERSINFO[1]} -gt 2 ] && return 0
if [ ${BASH_VERSINFO[1]} -eq 2 ]; then
[ ${BASH_VERSINFO[2]} -ge 48 ] && return 0
fi
fi
fi
_tfw_fatal "unsupported Bash version: $BASH_VERSION"
}
_tfw_checkTerminfo() {
_tfw_tput=false
case $(type -p tput) in
*/tput) _tfw_tput=tput;;
esac
}
_tfw_checkCommandInPATH() {
local __var="$2"
local __path="$(type -p "$1")"
case "$__path" in
*/"${1##*/}") ;;
*) _tfw_fatal "command not found: $1 (PATH=$PATH)"
esac
[ -n "$__var" ] && eval $__var='"$__path"'
return 0
}
_tfw_count_path_components() {
local path="$1"
local i=0
while [ -n "$path" ]; do
case "$path" in
*/) path="${path%/}";;
*/*) i=$(($i + 1)); path="${path%/*}";;
*) i=$(($i + 1)); path=;;
esac
done
echo $i
}
_tfw_unpack_words() {
local __text="$1"
shift
while [ $# -gt 1 ]; do
eval "$1=\"\${__text%% *}\""
shift
__text="${__text#* }"
done
if [ $# -ne 0 ]; then
eval "$1=\"\$__text\""
fi
}
_tfw_find_tests() {
(
local oo
tfw_shopt oo -s extdebug
local func
for func in $(builtin declare -F | $SED -n -e '/^declare -f test_[A-Za-z]/s/^declare -f //p'); do
local funcname
local lineno
local path
_tfw_unpack_words "$(builtin declare -F $func)" funcname lineno path
echo $lineno 0 ${funcname#test_} "$(abspath "$path")"
done
local include
for include in "${_tfw_included_tests[@]}"; do
local lineno
local script
_tfw_unpack_words "$include" lineno script
local listline
"$BASH" "$_tfw_script_dir/$script" --list 2>/dev/null | while read listline; do
local number
local name
local path
_tfw_unpack_words "$listline" number name path
case "$path" in
/*) ;;
*) path="$(abspath "$_tfw_script_dir/$path")";;
esac
echo $lineno $number $name "$path"
done
done
tfw_shopt_restore oo
) | sort -n -k1 -k2 | $SED -e 's/^[0-9][0-9]* [0-9][0-9]* //'
}
# Return a list of test names in the _tfw_test_sourcefiles and _tfw_test_names
# array variables, in the order that the test_TestName functions were defined.
# Test names must start with an alphabetic character (not numeric or '_').
_tfw_list_tests() {
_tfw_test_sourcefiles=()
_tfw_test_names=()
local oIFS="$IFS"
IFS="
"
local -a testlines=($(_tfw_find_tests))
IFS="$oIFS"
local testline
for testline in "${testlines[@]}"; do
local name
local path
_tfw_unpack_words "$testline" name path
_tfw_test_names+=("$name")
_tfw_test_sourcefiles+=("$path")
done
}
_tfw_filter_predicate() {
local number="$1"
local name="$2"
shift 2
local -a filters=("$@")
local ret=1
local oo
tfw_shopt oo -s extglob
if [ ${#filters[*]} -eq 0 ]; then
ret=0
else
local filter
for filter in "${filters[@]}"; do
case "$filter" in
+([0-9]))
if [ $number -eq $filter ]; then
ret=0
break
fi
;;
+([0-9])*(,+([0-9])))
local oIFS="$IFS"
IFS=,
local -a numbers=($filter)
IFS="$oIFS"
local n
for n in ${numbers[*]}; do
if [ $number -eq $n ]; then
ret=0
break 2
fi
done
;;
+([0-9])-)
local start=${filter%-}
if [ $number -ge $start ]; then
ret=0
break
fi
;;
-+([0-9]))
local end=${filter#-}
if [ $number -le $end ]; then
ret=0
break
fi
;;
+([0-9])-+([0-9]))
local start=${filter%-*}
local end=${filter#*-}
if [ $number -ge $start -a $number -le $end ]; then
ret=0
break
fi
;;
*)
case "$name" in
"$filter"*) ret=0; break;;
esac
;;
esac
done
fi
tfw_shopt_restore oo
return $ret
}
# A "fail" event occurs when any assertion fails, and indicates that the test
# has not passed. Other tests may still proceed. A "fail" event during setup
# or teardown is treated as an error, not a failure.
_tfw_failmsg() {
case $_tfw_phase in
testcase|finalise)
if ! $_tfw_opt_error_on_fail; then
tfw_log "FAIL: $*"
return 0;
fi
;;
esac
# A failure during setup or teardown is treated as an error.
tfw_log "ERROR: $*"
}
_tfw_fail() {
_tfw_failmsg "$*"
_tfw_backtrace
_tfw_failexit
}
_tfw_backtrace() {
tfw_log '#----- shell backtrace -----'
local -i up=1
while [ "${BASH_SOURCE[$up]}" == "${BASH_SOURCE[0]}" -a "${FUNCNAME[$up]}" != '_tfw_finalise' ]; do
let up=up+1
done
local -i i=0
while [ $up -lt $((${#FUNCNAME[*]} - 1)) ]; do
if [ "${BASH_SOURCE[$up]}" != "${BASH_SOURCE[0]}" ]; then
echo "[$i] ${FUNCNAME[$(($up-1))]}() called from ${FUNCNAME[$up]}() at line ${BASH_LINENO[$(($up-1))]} of ${BASH_SOURCE[$up]}" >&$_tfw_log_fd
let i=i+1
fi
let up=up+1
done
tfw_log '#-----'
}
_tfw_failexit() {
# When exiting a test case due to a failure, log any diagnostic output that
# has been requested.
tfw_cat "${_tfw_opt_dump_on_fail[@]}"
# A failure during setup or teardown is treated as an error. A failure
# during finalise does not terminate execution.
case $_tfw_phase in
testcase)
if ! $_tfw_opt_error_on_fail; then
exit 1
fi
;;
finalise)
if ! $_tfw_opt_error_on_fail; then
case $_tfw_result in
PASS) _tfw_result=FAIL; _tfw_status=1;;
esac
return 1
fi
;;
esac
_tfw_errorexit
}
# An "error" event prevents a test from running, so it neither passes nor fails.
# Other tests may still proceed.
_tfw_errormsg() {
[ $# -eq 0 ] && set -- "(no message)"
local -i up=1
local -i top=${#FUNCNAME[*]}
let top=top-1
while [ $up -lt $top -a "${BASH_SOURCE[$up]}" = "${BASH_SOURCE[0]}" -a "${FUNCNAME[$up]}" != '_tfw_finalise' ]; do
let up=up+1
done
if [ "${BASH_SOURCE[$up]}" = "${BASH_SOURCE[0]}" ]; then
tfw_log "ERROR: $*"
else
tfw_log "ERROR: in ${FUNCNAME[$up]}: $*"
fi
}
_tfw_error() {
_tfw_errormsg "$*"
_tfw_backtrace
_tfw_errorexit
}
_tfw_errorexit() {
# Do not exit process during finalise or teardown
_tfw_result=ERROR
case $_tfw_phase in
finalise|teardown) [ $_tfw_status -lt 254 ] && _tfw_status=254;;
*) exit 254;;
esac
return 254
}
# A "fatal" event stops the entire test run, and generally indicates an
# insurmountable problem in the test script or in the test framework itself.
_tfw_fatalmsg() {
_tfw_echoerr "${BASH_SOURCE[1]}: FATAL: $*"
}
_tfw_fatal() {
[ $# -eq 0 ] && set -- exiting
_tfw_echoerr "${BASH_SOURCE[1]}: FATAL: $*"
_tfw_fatalexit
}
_tfw_fatalexit() {
exit 255
}
# The following functions can be overridden by a test script to provide a
# default fixture for all test cases.
setup() {
:
}
finally() {
:
}
teardown() {
:
}
# The following functions are essential for writing test cases and fixtures.
# Within a test_xxx() function, commands executed between begin_fixture and
# end_fixture invocations are treated as setup, not test, so that a failure
# results in ERROR not FAIL.
begin_fixture() {
case $_tfw_phase in
testcase) _tfw_phase=testcase-setup;;
testcase-setup) _tfw_error "begin_fixture: already in fixture (missing a prior end_fixture call?)";;
*) _tfw_error "begin_fixture: not allowed here (only in test_xxx functions)";;
esac
}
end_fixture() {
case $_tfw_phase in
testcase-setup) _tfw_phase=testcase;;
testcase) _tfw_error "end_fixture: not in fixture (missing a prior begin_fixture call?)";;
*) _tfw_error "end_fixture: not allowed here (only in test_xxx functions)";;
esac
}
# Executes its arguments as a command in the current shell process (not in a
# child process), so that side effects like functions setting variables will
# have effect.
# - if the exit status is non-zero, then fails the current test
# - otherwise, logs a message indicating the assertion passed
assert() {
_tfw_getopts assert "$@"
shift $_tfw_getopts_shift
_tfw_message="${_tfw_message:+$_tfw_message }("$@")"
_tfw_assert "$@" || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
return 0
}
assertExpr() {
_tfw_getopts assertexpr "$@"
shift $_tfw_getopts_shift
_tfw_parse_expr "$@" || return $?
_tfw_message="${_tfw_message:+$_tfw_message }("$@")"
_tfw_shellarg "${_tfw_expr[@]}"
_tfw_assert eval "${_tfw_args[@]}" || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
return 0
}
fail() {
_tfw_getopts fail "$@"
shift $_tfw_getopts_shift
[ $# -ne 0 ] && _tfw_failmsg "$1"
_tfw_backtrace
_tfw_failexit
}
error() {
_tfw_getopts error "$@"
shift $_tfw_getopts_shift
[ $# -ne 0 ] && _tfw_errormsg "$1"
_tfw_backtrace
_tfw_errorexit
}
fatal() {
[ $# -eq 0 ] && set -- "no reason given"
_tfw_fatalmsg "$@"
_tfw_backtrace
_tfw_fatalexit
}
# Append a time stamped message to the test case's stdout log. Will work even
# in a context that stdout (fd 1) is redirected. Will not log anything in a
# quietened region.
tfw_log() {
if $_tfw_log_noise; then
local ts=$(_tfw_timestamp)
cat >&$_tfw_log_fd <<EOF
${ts##* } $*
EOF
fi
}
# Append the contents of a file to the test case's stdout log. A normal 'cat'
# to stdout would also do this, but tfw_cat echoes header and footer delimiter
# lines around to content to help distinguish it, and also works even in a
# context that stdout (fd 1) is redirected.
tfw_cat() {
local header=
local -a show=(cat)
for file; do
case $file in
--header=*)
header="${1#*=}"
continue
;;
-v|--show-nonprinting)
show=(cat -v)
continue
;;
-h|--hexdump)
show=(hd '</dev/null')
continue
;;
--stdout)
file="${TFWSTDOUT?}"
header="${header:-stdout of ($TFWEXECUTED)}"
;;
--stderr)
file="${TFWSTDERR?}"
header="${header:-stderr of ($TFWEXECUTED)}"
;;
*)
header="${header:-${file#$_tfw_tmp/}}"
;;
esac
local missing_nl=
tfw_log "#----- $header -----"
eval "${show[@]}" "$file" >&$_tfw_log_fd
if [ "${show[0]}" = cat -a "$(tail -1c "$file" | wc -l)" -eq 0 ]; then
echo >&$_tfw_log_fd
missing_nl=" (no newline at end)"
fi
tfw_log "#-----$missing_nl"
header=
show=(cat)
done
}
# Copy the given file(s) into the log directory, so they form part of the residue
# of the log execution (together with log.txt).
tfw_preserve() {
local arg
for arg; do
if cp -a "$arg" "$_tfw_logdir_test"; then
$_tfw_assert_noise && tfw_log "# PRESERVE" $(ls -l -d "$arg")
else
error "cp failed"
fi
done
}
# Execute the given command with log messages quietened.
tfw_nolog() {
if [ "$1" = ! ]; then
shift
! tfw_nolog "$@"
else
local old_log_noise=$_tfw_log_noise
_tfw_log_noise=false
"$@" >/dev/null
local stat=$?
_tfw_log_noise=$old_log_noise
return $stat
fi
}
# Execute the given command with successful assert log messages quietened.
tfw_quietly() {
if [ "$1" = ! ]; then
shift
! tfw_quietly "$@"
else
local old_assert_noise=$_tfw_assert_noise
_tfw_assert_noise=false
"$@" >/dev/null
local stat=$?
_tfw_assert_noise=$old_assert_noise
return $stat
fi
}
# The following functions are extremely useful for writing test cases.
# Executes its arguments as a command:
# - captures the standard output and error in temporary files for later
# examination
# - captures the exit status for later assertions
# - sets the $TFWEXECUTED variable to a description of the command that was
# executed
execute() {
$_tfw_assert_noise && tfw_log "# execute" $(shellarg "$@")
_tfw_getopts execute "$@"
shift $_tfw_getopts_shift
_tfw_execute "$@"
}
executeOk() {
$_tfw_assert_noise && tfw_log "# executeOk" $(shellarg "$@")
_tfw_getopts executeok "$@"
_tfw_opt_exit_status=0
_tfw_dump_on_fail --stderr
shift $_tfw_getopts_shift
_tfw_execute "$@"
}
tfw_core_backtrace() {
local executable="$1"
local corefile="$2"
echo backtrace >"$_tfw_process_tmp/backtrace.gdb"
tfw_log "#----- gdb backtrace from $executable $corefile -----"
gdb -n -batch -x "$_tfw_process_tmp/backtrace.gdb" "$executable" "$corefile" </dev/null
tfw_log "#-----"
rm -f "$_tfw_process_tmp/backtrace.gdb"
}
assertExitStatus() {
_tfw_getopts assertexitstatus "$@"
shift $_tfw_getopts_shift
[ -z "$_tfw_message" ] && _tfw_message="exit status ($_tfw_exitStatus) of ($TFWEXECUTED) $*"
_tfw_assertExpr "$_tfw_exitStatus" "$@" || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
return 0
}
assertRealTime() {
_tfw_getopts assertrealtime "$@"
shift $_tfw_getopts_shift
[ -z "$_tfw_message" ] && _tfw_message="real execution time ($realtime) of ($TFWEXECUTED) $*"
_tfw_assertExpr "$realtime" "$@" || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
return 0
}
replayStdout() {
cat ${TFWSTDOUT?}
}
replayStderr() {
cat ${TFWSTDERR?}
}
assertStdoutIs() {
_tfw_assert_stdxxx_is stdout "$@" || _tfw_failexit
}
assertStderrIs() {
_tfw_assert_stdxxx_is stderr "$@" || _tfw_failexit
}
assertStdoutLineCount() {
_tfw_assert_stdxxx_linecount stdout "$@" || _tfw_failexit
}
assertStderrLineCount() {
_tfw_assert_stdxxx_linecount stderr "$@" || _tfw_failexit
}
assertStdoutGrep() {
_tfw_assert_stdxxx_grep stdout "$@" || _tfw_failexit
}
assertStderrGrep() {
_tfw_assert_stdxxx_grep stderr "$@" || _tfw_failexit
}
# Execute the given args as a command, like the Bash 'eval' builtin but without
# expanding the arguments. The only difference is that leading '!' arguments
# are stripped off and invert the exit status: non-zero becomes zero, and zero
# becomes 1. Also, barfs with an error if the command is missing (no args).
# Very useful for writing functions that take a command-with-args as a predicate
# expression, because it allows the '!' notation for inverting the sense of the
# expression.
tfw_run() {
local sense=true
while [ "$1" = '!' ]; do
sense=$($sense && echo false || echo true)
shift
done
[ $# -eq 0 ] && error "missing command"
"$@"
local status=$?
$sense && return $status
[ $status -eq 0 ] && return 1
return 0
}
# Wait until a given condition is met:
# - can specify the timeout with --timeout=SECONDS (overridden by command-line
# option)
# - can specify the sleep interval with --sleep=SECONDS (can be a float)
# - the condition is a command that is executed repeatedly until returns zero
# status
# where SECONDS may be fractional, eg, 1.5
wait_until() {
$_tfw_assert_noise && tfw_log "# wait_until" $(shellarg "$@")
local start=$SECONDS
_tfw_getopts wait_until "$@"
shift $_tfw_getopts_shift
local timeout=${_tfw_timeout_override:-${_tfw_opt_timeout:-${TFW_WAIT_UNTIL_TIMEOUT:-${_tfw_default_wait_until_timeout:-1}}}}
_tfw_is_float "$timeout" || error "invalid timeout '$timeout'"
sleep $timeout &
local timeout_pid=$!
while true; do
tfw_run "$@" && break
if ! kill -0 $timeout_pid 2>/dev/null; then
local end=$SECONDS
fail "timeout after $((end - start)) seconds" || return $?
fi
sleep ${_tfw_opt_sleep:-1}
done
local end=$SECONDS
$_tfw_assert_noise && tfw_log "# waited for" $((end - start)) "seconds"
kill $timeout_pid 2>/dev/null
return 0
}
# For managing concurrent processes that will be automatically killed when
# cleaning up.
fork() {
_tfw_getopts fork "$@"
shift $_tfw_getopts_shift
local forkid=${#_tfw_forked_pids[*]}
if _tfw_set_forklabel "$1"; then
shift
[ -n "$_tfw_forkid" ] && error "fork label '%$_tfw_forklabel' already in use"
fi
local desc="fork[$forkid]${_tfw_forklabel:+ %$_tfw_forklabel}"
local _tfw_process_tmp="$_tfw_tmp/fork-$forkid"
local _tfw_fork_stdout_var="TFWFORKSTDOUT_$_tfw_forklabel"
local _tfw_fork_stderr_var="TFWFORKSTDERR_$_tfw_forklabel"
local _tfw_fork_pid_var="TFWFORKPID_$_tfw_forklabel"
local _tfw_fork_stdout_path="$_tfw_process_tmp/log.stdout"
local _tfw_fork_stderr_path="$_tfw_process_tmp/log.stderr"
local _tfw_fork_xtrace_path="$_tfw_process_tmp/log.xtrace"
eval export $_tfw_fork_stdout_var='"$_tfw_fork_stdout_path"'
eval export $_tfw_fork_stderr_var='"$_tfw_fork_stderr_path"'
mkdir "$_tfw_process_tmp" || _tfw_fatalexit
$_tfw_assert_noise && tfw_log "# $desc START" $(shellarg "$@")
"$@" 6>"$_tfw_fork_stdout_path" 1>&6 2>"$_tfw_fork_stderr_path" 7>"$_tfw_fork_xtrace_path" &
_tfw_forked_pids[$forkid]=$!
_tfw_forked_labels[$forkid]="$_tfw_forklabel"
[ -n "$_tfw_forklabel" ] && eval _tfw_fork_label_$_tfw_forklabel=$forkid
$_tfw_assert_noise && tfw_log "# $desc pid=$! STARTED"
eval export $_tfw_fork_pid_var='$!'
}
fork_is_running() {
local ret=0
for arg; do
_tfw_set_forklabel "$arg" || error "not a fork label '$arg'"
[ -n "$_tfw_forkid" ] || error "no such fork: %$_tfw_forklabel"
_tfw_fork_is_running $_tfw_forkid || ret=1
done
return $ret
}
assert_fork_is_running() {
_tfw_getopts assert_fork_is_running "$@"
shift $_tfw_getopts_shift
local message_opt="$_tfw_message"
for arg; do
_tfw_set_forklabel "$arg" || error "not a fork label '$arg'"
[ -n "$_tfw_forkid" ] || error "no such fork: %$_tfw_forklabel"
[ -z "$message_opt" ] && _tfw_message="fork $arg is running"
_tfw_assert _tfw_fork_is_running $_tfw_forkid || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
done
return 0
}
assert_fork_is_not_running() {
_tfw_getopts assert_fork_is_not_running "$@"
shift $_tfw_getopts_shift
local message_opt="$_tfw_message"
for arg; do
_tfw_set_forklabel "$arg" || error "not a fork label '$arg'"
[ -n "$_tfw_forkid" ] || error "no such fork: %$_tfw_forklabel"
[ -z "$message_opt" ] && _tfw_message="fork $arg is not running"
_tfw_assert ! _tfw_fork_is_running $_tfw_forkid || _tfw_failexit || return $?
$_tfw_assert_noise && tfw_log "# assert $_tfw_message"
done
return 0
}
fork_terminate() {
_tfw_getopts fork_terminate "$@"
shift $_tfw_getopts_shift
$_tfw_assert_noise && tfw_log "# fork_terminate $*"
for arg; do
_tfw_set_forklabel "$arg" || error "not a fork label '$arg'"
[ -n "$_tfw_forkid" ] || error "no such fork: %$_tfw_forklabel"
_tfw_forkterminate $_tfw_forkid
done
}
fork_wait() {
_tfw_getopts fork_wait "$@"
shift $_tfw_getopts_shift
$_tfw_assert_noise && tfw_log "# fork_wait $*"
while true; do
local running=0
for arg; do
_tfw_set_forklabel "$arg" || error "not a fork label '$arg'"
[ -n "$_tfw_forkid" ] || error "no such fork: %$_tfw_forklabel"
if ! _tfw_forkwait $_tfw_forkid; then
let running+=1
fi
done
[ $running -eq 0 ] && return 0
sleep 1
done
}
fork_terminate_all() {
_tfw_getopts fork_terminate_all "$@"
shift $_tfw_getopts_shift
[ $# -eq 0 ] || error "unsupported arguments: $*"
$_tfw_assert_noise && tfw_log "# fork_terminate_all"
local forkid
for ((forkid=0; forkid < ${#_tfw_forked_pids[*]}; ++forkid)); do
_tfw_forkterminate $forkid
done
}
fork_wait_all() {
_tfw_getopts fork_wait_all "$@"
shift $_tfw_getopts_shift
[ $# -eq 0 ] || error "unsupported arguments: $*"
$_tfw_assert_noise && tfw_log "# fork_wait_all"
while true; do
local running=0
local forkid
for ((forkid=0; forkid < ${#_tfw_forked_pids[*]}; ++forkid)); do
if ! _tfw_forkwait $forkid; then
let running+=1
fi
done
[ $running -eq 0 ] && return 0
sleep 1
done
}
_tfw_set_forklabel() {
local oo
tfw_shopt oo -s extglob
local ret=1
case "$1" in
'%'+([[A-Za-z0-9]))
_tfw_forklabel="${1#%}"
eval _tfw_forkid="\$_tfw_fork_label_$_tfw_forklabel"
ret=0
;;
'%'*)
error "malformed fork label '$1'"
ret=0
;;
esac
tfw_shopt_restore oo
return $ret
}
_tfw_forkterminate() {
local forkid="$1"
[ -z "$forkid" ] && return 1
local pid=${_tfw_forked_pids[$forkid]}
local label=${_tfw_forked_labels[$forkid]}
local desc="fork[$forkid]${label:+ %$label}"
[ -z "$pid" ] && return 1
$_tfw_assert_noise && tfw_log "# $desc kill -TERM $pid"
kill -TERM $pid 2>/dev/null
}
_tfw_fork_is_running() {
local forkid="$1"
[ -z "$forkid" ] && return 1 # forkid never used
local pid=${_tfw_forked_pids[$forkid]}
local label=${_tfw_forked_labels[$forkid]}
[ -z "$pid" ] && return 1 # forkid already waited for
kill -0 $pid 2>/dev/null
}
_tfw_forkwait() {
local forkid="$1"
[ -z "$forkid" ] && return 0
local pid=${_tfw_forked_pids[$forkid]}
local label=${_tfw_forked_labels[$forkid]}
[ -z "$pid" ] && return 0 # forkid already waited for
kill -0 $pid 2>/dev/null && return 1 # not killed yet
_tfw_forked_pids[$forkid]=
wait $pid # process has already exited, so should not block
local status=$?
local desc="fork[$forkid]${label:+ %$label}"
$_tfw_assert_noise && tfw_log "# $desc pid=$pid EXIT status=$status"
echo "++++++++++ $desc log.stdout ++++++++++"
cat $_tfw_tmp/fork-$forkid/log.stdout
echo "++++++++++"
echo "++++++++++ $desc log.stderr ++++++++++"
cat $_tfw_tmp/fork-$forkid/log.stderr
echo "++++++++++"
if $_tfw_trace; then
echo "++++++++++ $desc log.xtrace ++++++++++"
cat $_tfw_tmp/fork-$forkid/log.xtrace
echo "++++++++++"
fi
if [ -s $_tfw_tmp/fork-$forkid/executing ]; then
local executed="$(cat $_tfw_tmp/fork-$forkid/executing)"
echo "++++++++++ $desc stdout of ($executed) ++++++++++"
cat $_tfw_tmp/fork-$forkid/stdout
echo "++++++++++"
echo "++++++++++ $desc stderr of ($executed) ++++++++++"
cat $_tfw_tmp/fork-$forkid/stderr
echo "++++++++++"
fi
case $status in
0) ;;
143) ;; # terminated with SIGTERM (probably from fork_terminate)
1) _tfw_fail "$desc process exited with FAIL status";;
254) _tfw_error "$desc process exited with ERROR status";;
255) _tfw_fatal "$desc process exited with FATAL status";;
*) _tfw_error "$desc process exited with status=$status";;
esac
return 0
}
# The following functions are very convenient when writing test cases.
assertGrep() {
_tfw_getopts assertcontentgrep "$@"
shift $_tfw_getopts_shift
if [ $# -ne 2 ]; then
_tfw_error "incorrect arguments"
return $?
fi
_tfw_dump_on_fail "$1"
_tfw_get_content "$1" || return $?
_tfw_assert_grep "${_tfw_opt_line_msg:+$_tfw_opt_line_msg of }$1" "$_tfw_tmp/content" "$2" || _tfw_failexit
}
# Compare the two arguments as dotted ascii decimal version strings.
# Return 0 if they are equal, 1 if arg1 < arg2, 2 if arg1 > arg2
tfw_cmp_version() {
local IFS=.
local i=0 a=($1) b=($2)
for (( i=0; i < ${#a[@]} || i < ${#b[@]}; ++i )); do
local ai="${a[i]:-0}"
local bi="${b[i]:-0}"
(( 10#$ai < 10#$bi )) && return 1
(( 10#$ai > 10#$bi )) && return 2
done
return 0
}
# Format the standard input into multi columns within an output width set by the
# COLUMNS env var.
tfw_multicolumn() {
$AWK '
function pad(s, n) {
return sprintf("%-" n "s", s)
}
{
line[nlines++] = $0
if (length($0) > colwid)
colwid = length($0)
}
END {
wid = 0 + ENVIRON["COLUMNS"]
if (wid != 0) {
ncol = int(wid / (colwid + 2))
if (ncol < 1)
ncol = 1
}
collen = int((nlines + ncol - 1) / ncol)
for (r = 0; r < collen; ++r) {
for (c = 0; c < ncol; ++c) {
i = c * collen + r
if (i >= nlines)
break
printf "%s ", pad(line[i], colwid)
}
printf "\n"
}
}
'
}
# Create a file with the given size (default 0).
# Usage: create_file [--append] [create_file opts] [--] <path> [<size>]
# where: if <path> is - then writes to standard output
# <size> is of the form Nu
# N is decimal integer
# u is one of kKmMgG (k=10^3, K=2^10, m=10^6, M=2^20, g=10^9, G=2^30)
create_file() {
local args=("$@")
local opt_append=false
local opt_label=
local opts=()
while [ $# -ne 0 ]; do
case "$1" in
--) shift; break;;
--append) opt_append=true; shift;;
--label=*) opt_label="${1#*=}"; shift;;
--*) opts+=("$1"); shift;;
*) break;;
esac
done
local path="${1?}"
local size="$2"
case "$path" in
-)
tfw_createfile ${opt_label:+--label="$opt_label"} "${opts[@]}" ${size:+--size=$size}
;;
*)
[ -z "$opt_label" ] && opt_label="$path"
tfw_createfile ${opt_label:+--label="$opt_label"} "${opts[@]}" ${size:+--size=$size} >>"$path"
;;
esac
[ $? -eq 0 ] || error "failed command: create_file ${args[*]}"
}
# Add quotations to the given arguments to allow them to be expanded intact
# in eval expressions.
shellarg() {
_tfw_shellarg "$@"
echo "${_tfw_args[*]}"
}
# Echo the absolute path (containing symlinks if given) of the given
# file/directory, which does not have to exist or even be accessible.
abspath() {
_tfw_abspath -L "$1"
}
# Echo the absolute path (resolving all symlinks) of the given file/directory,
# which does not have to exist or even be accessible.
realpath() {
_tfw_abspath -P "$1"
}
# Return true if all the arguments arg2... match the given grep(1) regular
# expression arg1.
matches_rexp() {
_tfw_matches_rexp "$@"
}
# Escape all grep(1) basic regular expression metacharacters.
escape_grep_basic() {
local re="$1"
local nil=''
re="${re//[\\]/\\\\$nil}"
re="${re//./\\.}"
re="${re//\*/\\*}"
re="${re//^/\\^}"
re="${re//\$/\\$}"
re="${re//\[/\\[}"
re="${re//\]/\\]}"
echo "$re"
}
# Escape all egrep(1) extended regular expression metacharacters.
escape_grep_extended() {
local re="$1"
local nil=''
re="${re//[\\]/\\\\$nil}"
re="${re//./\\.}"
re="${re//\*/\\*}"
re="${re//\?/\\?}"
re="${re//+/\\+}"
re="${re//^/\\^}"
re="${re//\$/\\$}"
re="${re//(/\\(}";#"(fix Vim syntax highlighting)
re="${re//)/\\)}"
re="${re//|/\\|}"
re="${re//\[/\\[}"
re="${re//{/\\{}";#"(fix Vim syntax highlighting)
echo "$re"
}
# Restore the caller's shopt preferences before returning.
tfw_shopt_restore _tfw_orig_shopt