mirror of
https://github.com/servalproject/serval-dna.git
synced 2024-12-23 15:02:27 +00:00
2e0cb4e6dc
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.
2261 lines
69 KiB
Bash
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
|