Files
sparsebundlefs-mirror/testrunner.sh
Tor Arne Vestbø ba76dfa458 Properly clean up subprocesses in tests
Relying on the process group to figure out which processes to treat
as children and grandchildren of the test run was not correct, as
the testrunner didn't ensure it was creating a new process group.

When running inside 'make check' the process group was actually that
of make, not testrunner.sh.

Since setpgid is not universally available, we iterate the process
tree via ps, and kill the children recursively.
2018-09-13 16:46:31 +02:00

299 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Minimal test runner with pretty output
#
# Copyright (c) 2018 Tor Arne Vestbø
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# ----------------------------------------------------------
if [[ -t 1 ]] && [[ $(tput colors) -ge 8 ]]; then
declare -i counter=0
for color in Black Red Green Yellow Blue Magenta Cyan White; do
declare -r k${color}="\033[$((30 + $counter))m"
declare -r k${color}Background="\033[$((40 + $counter))m"
counter+=1
done
declare -r kReset="\033[0m"
declare -r kBold="\033[1m"
declare -r kDark="\033[2m"
declare -r kUnderline="\033[4m"
declare -r kInverse="\033[7m"
fi
function testrunner::function_declared() {
test "$(type -t $1)" = 'function'
}
function testrunner::absolute_path() {
printf "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
}
declare test_output_dir=$(mktemp -d)
function testrunner::run_tests() {
local pretty_testsuite=$(basename $testsuite)
local test_output_file="${test_output_dir}/${pretty_testsuite}.log"
touch $test_output_file
exec 4< $test_output_file
local all_testcases=($(grep "function .*()" $testsuite | grep -o "test_[a-zA-Z_]*"))
local requested_testcases=$testcases
if [[ -z $testcases ]]; then
testcases=("${all_testcases[@]}")
else
local -a matching_testcases
for testcase in "${testcases[@]}"; do
if [[ "${all_testcases[*]}" =~ (^| )(test_)?${testcase}( |$) ]]; then
matching_testcases+=(${BASH_REMATCH[0]})
fi
done
testcases=("${matching_testcases[@]}")
fi
if [[ -z $testcases ]]; then
printf "${kUnderline}No matching tests for '$requested_testcases' in ${testsuite}${kReset}\n\n"
return;
fi
printf "${kUnderline}Running ${#testcases[@]} tests from ${pretty_testsuite}...${kReset}\n"
if testrunner::function_declared setup; then
setup >>$test_output_file 2>&1
fi
if [[ $DEBUG -eq 0 ]] || ! testrunner::print_test_output "Setup"; then
printf "\n"
fi
local test_failure
for testcase in "${testcases[@]}" ; do
tests_total+=1
local pretty_testcase=${testcase#test_}
local pretty_testcase=${pretty_testcase//[_]/ }
printf -- "- ${pretty_testcase} "
test_failure=""
trap 'testrunner::register_failure "$BASH_COMMAND" $?' ERR INT
# Work around older bash versions not getting location correct on error
set -o functrace
local -a actual_lineno
local -a actual_source
trap 'actual_lineno+=($LINENO); actual_source+=(${BASH_SOURCE[0]})' DEBUG
${testcase} >>$test_output_file 2>&1
trap - ERR INT DEBUG
if [[ -z "$test_failure" ]]; then
printf "${kGreen}${kReset}\n"
if [[ $DEBUG -eq 1 ]]; then
testrunner::print_test_output
fi
else
tests_failed+=1
printf "${kRed}${kReset}\n"
IFS='|' read -r filename line_number expression \
evaluated_expression exit_code <<< "$test_failure"
testrunner::print_location $filename $line_number
printf "Expression:\n\n"
printf " ${kBold}${expression}${kReset}"
if [[ $evaluated_expression != $expression ]]; then
printf " (${evaluated_expression})"
fi
printf "\n\nFailed with exit code ${kBold}${exit_code}${kReset}\n"
testrunner::print_test_output
if [[ ${exit_code} -eq 130 ]]; then
break; # Interrupted
fi
fi
done
if testrunner::function_declared teardown; then
teardown >>$test_output_file
fi
[[ $DEBUG -eq 1 ]] && testrunner::print_test_output "Teardown"
if [[ -z "$test_failure" ]]; then
printf "\n" # Blank line in case the last test passed
fi
exec 4>&-
}
set -o errtrace
function testrunner::register_failure() {
trap - DEBUG
if [[ ! -z "$test_failure" ]]; then
return; # Already processing a failure
fi
#for (( f=${#actual_source[@]}; f >= 0; f-- )); do
# echo "${actual_source[$f]}:${actual_lineno[$f]}"
#done
local line=${actual_lineno[${#actual_lineno[@]} - 4]}
local filename=${actual_source[${#actual_source[@]} - 5]}
local command=$1
local exit_code=$2
test_failure="${filename}|${line}|${command}|$(eval "echo ${command}")|${exit_code}"
}
function testrunner::print_location() {
local filename=$1
local line_number=$2
printf "\n${kBlack}${kBold}${filename}:${line_number}${kReset}\n\n"
local -r -i context_lines=2
# FIXME: Start at function?
local -i context_above=$context_lines
local -i context_below=$context_lines
test $context_above -ge $line_number && context_above=$(($line_number - 1))
local -i diff_start=${line_number}-${context_above}
local -i total_lines=$(($context_above + 1 + $context_below))
local -i current_line=${diff_start}
tail -n "+${diff_start}" ${filename} | head -n $total_lines | while IFS='' read -r line; do
if [ $current_line -eq $line_number ]; then
# FIXME: Compute longest line and color all the way
printf " ${kRedBackground}${kBold}${current_line}:${kReset}${kRedBackground}"
else
printf " ${kBlack}${kBold}${current_line}:${kReset}"
fi
printf " ${line}${kReset}\n"
current_line+=1
done
printf "\n"
}
function testrunner::print_test_output {
header=${1:-Output}
local -i wrote_header=0
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ ! $wrote_header -eq 1 ]]; then
printf "\n${header}:\n\n"
wrote_header=1
fi
printf " ${kMagenta}|${kReset} $line\n"
done <&4
if [[ $wrote_header -eq 1 ]]; then
printf "\n"
return 0
else
return 1
fi
}
function testrunner::signal_children()
{
local signal=${1:-TERM}
local pid=${2:-${BASHPID:-$$}}
local child_pids=()
IFS=
res=$(ps -o ppid,pid)
unset IFS
{
read # Skip header
while IFS=' ' read -r ppid cpid; do
test $ppid -eq $pid && child_pids+=($cpid)
done
}<<<"$res"
local p
for p in "${child_pids[@]}"; do
testrunner::signal_children $signal $p
kill -$signal $p >/dev/null 2>&1
done
}
function testrunner::teardown() {
testrunner::signal_children KILL
rm -Rf $test_output_dir
}
function testrunner::print_summary() {
if [[ $tests_failed -gt 0 || ($tests_total -eq 0 && ${#testcases[@]} -gt 0) ]]; then
printf "${kRed}FAIL${kReset}"
else
printf "${kGreen}OK${kReset}"
fi
printf ": $tests_total tests"
if [[ $tests_total -gt 0 ]]; then
printf ", $tests_failed failures\n"
return $tests_failed
else
printf "\n"
return 1
fi
}
trap 'testrunner::teardown; testrunner::print_summary; exit $?' EXIT
declare -a testsuites
declare -a testcases
for argument in "$@"; do
if [[ -f "$argument" ]]; then
testsuites+=("$argument")
else
testcases+=("$argument")
fi
done
declare -i tests_total=0
declare -i tests_failed=0
declare interrupted=0
trap 'interrupted=1' INT
printf "\n"
for testsuite in "${testsuites[@]}"; do
exec 4>&1
eval $(
exec 3>&1 # Set up file descriptor for exporting variables
exec 1>&4- # Ensure stdout still goes to the right place
source "$testsuite"
tests_total=0
tests_failed=0
testrunner::run_tests
# Export results out of sub-shell
printf "tests_total+=${tests_total}; tests_failed+=${tests_failed}" >&3
# Clean up if test didn't do it
testrunner::signal_children TERM
)
exec 4>&-
if [[ $interrupted -eq 1 ]]; then
break;
fi
done