mirror of
https://github.com/torarnv/sparsebundlefs.git
synced 2026-02-26 18:35:50 +01:00
337 lines
9.7 KiB
Bash
Executable File
337 lines
9.7 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")"
|
|
}
|
|
|
|
function testrunner::pid() {
|
|
# Portable subshell-aware PID
|
|
exec bash -c 'echo $PPID'
|
|
}
|
|
|
|
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_]*"))
|
|
for testcase_num in "${!all_testcases[@]}"; do
|
|
testcase="${all_testcases[$testcase_num]}"
|
|
# Make sure testcase is actually a defined function
|
|
if ! testrunner::function_declared $testcase; then
|
|
unset 'all_testcases[testcase_num]'
|
|
fi
|
|
done
|
|
|
|
if [[ -z $all_testcases ]]; then
|
|
printf "${kUnderline}No tests in ${pretty_testsuite}${kReset}\n\n"
|
|
return;
|
|
fi
|
|
|
|
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
|
|
|
|
# Clean up if test didn't do it
|
|
testrunner::signal_children TERM
|
|
testrunner::signal_children KILL
|
|
|
|
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 subshell_pid=$(testrunner::pid)
|
|
local pid=${2:-${BASHPID:-${subshell_pid:-$$}}}
|
|
|
|
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
|
|
|
|
testrunner::signal_children TERM $$
|
|
testrunner::signal_children KILL $$
|
|
)
|
|
exec 4>&-
|
|
if [[ $interrupted -eq 1 ]]; then
|
|
break;
|
|
fi
|
|
done
|