Add test harness and initial mount test

Tests are run using 'make check', and can be combined with the
existing Makefile targets, e.g. 'make check all' to run checks
on all available platforms.

Pass DEBUG=1 for additional test output.
This commit is contained in:
Tor Arne Vestbø
2018-09-08 22:24:46 +02:00
parent 46bfe7b547
commit d7a1f72062
6 changed files with 338 additions and 2 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,3 @@
tests
sparsebundlefs
*.o
*.dSYM

View File

@@ -3,6 +3,7 @@
#
# $ make clean all - cleans all available platforms
# $ make gcc - builds on all available GCC platforms
# $ make check 32 - run tests on all 32-bit platforms
#
# Copyright (c) 2018 Tor Arne Vestbø
#
@@ -94,7 +95,7 @@ else ifneq ($(NATIVE_PLATFORM),$(PLATFORMS))
linux-gcc-%: docker ;
docker:
$(call ensure_binary,docker-compose)
@docker-compose run --rm $(PLATFORMS) $(MFLAGS) $(ACTUAL_GOALS)
@docker-compose run --rm $(PLATFORMS) $(MFLAGS) $(ACTUAL_GOALS) DEBUG=$(DEBUG)
@stty sane # Work around docker-compose messing up the terminal
$(call make_noop,ACTUAL_GOALS)
@@ -142,8 +143,34 @@ FUSE_FLAGS := $(shell $(PKG_CONFIG) fuse --cflags --libs)
$(TARGET): sparsebundlefs.cpp
$(CXX) $< -o $@ $(CFLAGS) $(FUSE_FLAGS) $(LFLAGS) $(DEFINES)
SPARSEBUNDLEFS=$(abspath $(TARGET))
export SPARSEBUNDLEFS
TESTS_DIR=$(SRC_DIR)/tests
TESTDATA_DIR := $(TESTS_DIR)/data
TEST_BUNDLE := $(TESTDATA_DIR)/test.sparsebundle
export TEST_BUNDLE
ifneq ($(filter testdata,$(ACTUAL_GOALS)),)
.PHONY:: testdata
endif
vpath $(TESTDATA_DIR) $(SRC_DIR)
$(TESTDATA_DIR):
$(call ensure_binary,hdiutil)
@rm -Rf $(TESTDATA_DIR) && mkdir $(TESTDATA_DIR)
hdiutil create -size 1TB -type SPARSEBUNDLE -fs HFS+ $(TEST_BUNDLE)
check_%: check ; @:
check: $(TARGET) $(TESTDATA_DIR)
@echo "============== $(PLATFORMS) =============="
@$(SRC_DIR)/testrunner.sh $(TESTS_DIR)/*.sh $(subst check_,test_,$(filter check_%,$(ACTUAL_GOALS)))
clean:
rm -f $(TARGET)
rm -Rf $(TARGET).dSYM
distclean: clean
rm -Rf $(TESTDATA_DIR)
endif

View File

@@ -12,6 +12,10 @@ x-base-service: &base-service
- -f
- /src/Makefile
network_mode: none
devices:
- /dev/fuse
cap_add:
- SYS_ADMIN
services:
linux-gcc-32:

283
testrunner.sh Executable file
View File

@@ -0,0 +1,283 @@
#!/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.
#
# ----------------------------------------------------------
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"
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=($(declare -f | 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 2>&1
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() {
signal=${1:-KILL}
child_pids=($(ps -o pid= -g $$ | sort --reverse))
# Remove first three (ps in subshell) and last (self)
child_pids=("${child_pids[@]:3:${#child_pids[@]}-4}")
for pid in "${child_pids[@]}"; do
echo "Sending $signal to PID $pid ($(ps -o command= $pid))"
kill -s $signal $pid >/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 && ! -z "${testcases[@]}") ]]; 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 >&2
)
exec 4>&-
if [[ $interrupted -eq 1 ]]; then
break;
fi
done

1
tests/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
data

22
tests/basic.sh Normal file
View File

@@ -0,0 +1,22 @@
function setup() {
mount_dir=$(mktemp -d)
$SPARSEBUNDLEFS -s -f -D $TEST_BUNDLE $mount_dir &
for i in {0..50}; do
# FIXME: Find actual mount callback in fuse?
grep -q "bundle has" $test_output_file && break || sleep 0.1
done
pid=$!
dmg_file=$mount_dir/sparsebundle.dmg
}
function test_dmg_exists_after_mounting() {
ls -l $dmg_file
test -f $dmg_file
}
function teardown()
{
umount $mount_dir
rm -Rf $mount_dir
}