Files
sparsebundlefs-mirror/tests/testrunner.sh
Tor Arne Vestbø 64a1462dfb Use process group to kill children instead of parent PIDs
Using the process group (now that we've got one that's exclusive
to the testrunner) allows us to also clean up children that have
been detached from their original parent (and now have PID 1 as
their parent).
2021-06-27 22:07:56 +02:00

315 lines
9.1 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.
#
# ----------------------------------------------------------
pgid=$(ps -o pgid= $$)
if [[ $pgid -ne $$ ]]; then
if [[ $(uname -s) == "Darwin" ]]; then
exec script -q /dev/null $0 $*
else
exec setsid $0 $*
fi
fi
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_test() {
local testcase=$1
local pretty_testcase=${testcase#test_}
local pretty_testcase=${pretty_testcase//[_]/ }
printf -- "- ${pretty_testcase} "
test_failure=""
trap 'testrunner::register_failure "$BASH_COMMAND" $? && return' 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
return 0
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
return 1
fi
}
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 ${pretty_testsuite}${kReset}\n\n"
return;
fi
printf "${kUnderline}Running ${#testcases[@]} tests from ${pretty_testsuite}...${kReset}\n\n"
if testrunner::function_declared setup; then
testrunner::run_test setup
test $? -eq 0 || return
fi
local test_failure
for testcase in "${testcases[@]}" ; do
tests_total+=1
testrunner::run_test $testcase
test $? -eq 0 || break
done
if testrunner::function_declared teardown; then
testrunner::run_test teardown
fi
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 pgid,ppid,pid)
unset IFS
{
read -r # Skip header
while IFS=' ' read -r pgid ppid cpid; do
# Child processes
#test $ppid -eq $pid && child_pids+=($cpid)
# Process group children
test $pgid -eq $pid && test $cpid -ne $pid && child_pids+=($cpid)
done
}<<<"$res"
IFS=$'\n' child_pids=($(sort --reverse <<<"${child_pids[*]}"))
for p in "${child_pids[@]}"; do
#testrunner::signal_children $signal $p
#echo "Signaling $p ($(ps -o pid=,command= $p)) $signal"
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 $$
testrunner::signal_children KILL $$
)
exec 4>&-
if [[ $interrupted -eq 1 ]]; then
break;
fi
done