X-Git-Url: https://www.tinc-vpn.org/git/browse?p=tinc;a=blobdiff_plain;f=test%2Ftestlib.sh.in;h=185aec0b135531d3d94370ebcbc879bc78729adb;hp=6a091cff1adb0c794d2c100ce54272efdefe3114;hb=046a10d692d1ac22de4daf783ee4fe025c4eb6ec;hpb=ada609f3ab838fdcb522de54510c414452be5950 diff --git a/test/testlib.sh.in b/test/testlib.sh.in index 6a091cff..185aec0b 100644 --- a/test/testlib.sh.in +++ b/test/testlib.sh.in @@ -1,46 +1,368 @@ #!/bin/sh -# Paths to executables +set -ex + +echo [STEP] Initialize test library + +# Paths to compiled executables + +# realpath on FreeBSD fails if the path does not exist. +realdir() { + [ -e "$1" ] || mkdir -p "$1" + if type realpath >/dev/null; then + realpath "$1" + else + readlink -f "$1" + fi +} + +tincd_path=$(realdir "../src/tincd@EXEEXT@") +tinc_path=$(realdir "../src/tinc@EXEEXT@") + +SPTPS_TEST=$(realdir "../src/sptps_test@EXEEXT@") +SPTPS_KEYPAIR=$(realdir "../src/sptps_keypair@EXEEXT@") + +# Exit status list +EXIT_SKIP_TEST=77 -tincd=../src/tincd@EXEEXT@ -tinc=../src/tinc@EXEEXT@ -sptps_test=../src/sptps_test@EXEEXT@ -sptps_keypair=../src/sptps_keypair@EXEEXT@ +# The list of the environment variables that tinc injects into the scripts it calls. +# shellcheck disable=SC2016 +TINC_SCRIPT_VARS='$NETNAME,$NAME,$DEVICE,$IFACE,$NODE,$REMOTEADDRESS,$REMOTEPORT,$SUBNET,$WEIGHT,$INVITATION_FILE,$INVITATION_URL,$DEBUG' # Test directories -scriptname=`basename $0` +# Reuse script name if it was passed in an env var (when imported from tinc scripts). +if [ -z "$SCRIPTNAME" ]; then + SCRIPTNAME=$(basename "$0") +fi -n1=$scriptname.1 -n2=$scriptname.2 -n3=$scriptname.3 +# Network names for tincd daemons. +net1=$SCRIPTNAME.1 +net2=$SCRIPTNAME.2 +net3=$SCRIPTNAME.3 -d1=$PWD/$n1 -d2=$PWD/$n2 -d3=$PWD/$n3 +# Configuration/pidfile directories for tincd daemons. +DIR_FOO=$(realdir "$PWD/$net1") +DIR_BAR=$(realdir "$PWD/$net2") +DIR_BAZ=$(realdir "$PWD/$net3") -# Default arguments for both tinc and tincd +# Register helper functions -c1="-n $n1 --config=$d1 --pidfile=$d1/pid" -c2="-n $n2 --config=$d2 --pidfile=$d2/pid" -c3="-n $n3 --config=$d3 --pidfile=$d3/pid" +# Alias gtimeout to timeout if it exists. +if type gtimeout >/dev/null; then + timeout() { gtimeout "$@"; } +fi -# Arguments when running tincd +# Are the shell tools provided by busybox? +is_busybox() { + timeout --help 2>&1 | grep -q -i busybox +} -r1="--logfile=$d1/log -d5" -r2="--logfile=$d2/log -d5" -r3="--logfile=$d3/log -d5" +# busybox timeout returns 128 + signal number (which is TERM by default) +if is_busybox; then + EXIT_TIMEOUT=$((128 + 15)) +else + EXIT_TIMEOUT=124 +fi -# Check for leftover tinc daemons +# Is this msys2? +is_windows() { + test "$(uname -o)" = Msys +} -[ -f $d1/pid ] && $tinc $c1 stop -[ -f $d2/pid ] && $tinc $c2 stop -[ -f $d3/pid ] && $tinc $c3 stop +# Are we running on a CI server? +is_ci() { + test "$CI" +} -# Remove test directories +# Dump error message and exit with an error. +bail() { + echo >&2 "$@" + exit 1 +} -rm -rf $d1 $d2 $d3 +# Remove carriage returns to normalize strings on Windows for easier comparisons. +rm_cr() { + tr -d '\r' +} -# Exit on errors, log all commands being executed +# Executes whatever is passed to it, checking that the resulting exit code is non-zero. +must_fail() { + if "$@"; then + bail "expected a non-zero exit code" + fi +} -set -ex +# Runs its arguments with timeout(1) or gtimeout(1) if either are installed. +# Usage: try_limit_time 10 command --with --args +if type timeout >/dev/null; then + if is_busybox; then + # busybox does not support --foreground + try_limit_time() { + time=$1 + shift + timeout "$time" "$@" + } + else + # BSD and GNU timeout do not require special handling + try_limit_time() { + time=$1 + shift + timeout --foreground "$time" "$@" + } + fi +else + try_limit_time() { + echo >&2 "timeout was not found, running without time limits!" + shift + "$@" + } +fi + +# wc -l on mac prints whitespace before the actual number. +# This is simplest cross-platform alternative without that behavior. +count_lines() { + awk 'END{ print NR }' +} + +# Calls compiled tinc, passing any supplied arguments. +# Usage: tinc { foo | bar | baz } --arg1 val1 "$args" +tinc() { + peer=$1 + shift + + case "$peer" in + foo) try_limit_time 30 "$tinc_path" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" "$@" ;; + bar) try_limit_time 30 "$tinc_path" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" "$@" ;; + baz) try_limit_time 30 "$tinc_path" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" "$@" ;; + *) bail "invalid command [[$peer $*]]" ;; + esac +} + +# Calls compiled tincd, passing any supplied arguments. +# Usage: tincd { foo | bar | baz } --arg1 val1 "$args" +tincd() { + peer=$1 + shift + + case "$peer" in + foo) try_limit_time 30 "$tincd_path" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" --logfile="$DIR_FOO/log" -d5 "$@" ;; + bar) try_limit_time 30 "$tincd_path" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" --logfile="$DIR_BAR/log" -d5 "$@" ;; + baz) try_limit_time 30 "$tincd_path" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" --logfile="$DIR_BAZ/log" -d5 "$@" ;; + *) bail "invalid command [[$peer $*]]" ;; + esac +} + +# Start the specified tinc daemon. +# usage: start_tinc { foo | bar | baz } +start_tinc() { + peer=$1 + shift + + case "$peer" in + foo) tinc "$peer" start --logfile="$DIR_FOO/log" -d5 "$@" ;; + bar) tinc "$peer" start --logfile="$DIR_BAR/log" -d5 "$@" ;; + baz) tinc "$peer" start --logfile="$DIR_BAZ/log" -d5 "$@" ;; + *) bail "invalid peer $peer" ;; + esac +} + +# Stop all tinc clients. +stop_all_tincs() { + ( + # In case these pid files are mangled. + set +e + [ -f "$DIR_FOO/pid" ] && tinc foo stop + [ -f "$DIR_BAR/pid" ] && tinc bar stop + [ -f "$DIR_BAZ/pid" ] && tinc baz stop + true + ) +} + +# Checks that the number of reachable nodes matches what is expected. +# usage: require_nodes node_name expected_number +require_nodes() { + echo >&2 "Check that we're able to reach tincd" + test "$(tinc "$1" pid | count_lines)" = 1 + + echo >&2 "Check the number of reachable nodes for $1 (expecting $2)" + actual="$(tinc "$1" dump reachable nodes | count_lines)" + + if [ "$actual" != "$2" ]; then + echo >&2 "tinc $1: expected $2 reachable nodes, got $actual" + exit 1 + fi +} + +peer_directory() { + case "$peer" in + foo) echo "$DIR_FOO" ;; + bar) echo "$DIR_BAR" ;; + baz) echo "$DIR_BAZ" ;; + *) bail "invalid peer $peer" ;; + esac +} + +# This is an append-only log of all scripts executed by all peers. +script_runs_log() { + echo "$(peer_directory "$1")/script-runs.log" +} + +# Create tincd script. If it fails, it kills the test script with SIGTERM. +# usage: create_script { foo | bar | baz } { tinc-up | host-down | ... } 'script content' +create_script() { + peer=$1 + script=$2 + shift 2 + + # This is the line that we should start from when reading the script execution log while waiting + # for $script from $peer. It is a poor man's hash map to avoid polluting tinc's home directory with + # "last seen" files. There seem to be no good solutions to this that are compatible with all shells. + line_var=$(next_line_var "$peer" "$script") + + # We must reassign it here in case the script is recreated. + # shellcheck disable=SC2229 + read -r "$line_var" <"$script_log" + + # Script output is redirected into /dev/null. Otherwise, it ends up + # in tinc's output and breaks things like 'tinc invite'. + cat >"$script_path" <>"$script_log" +) >/dev/null 2>&1 || kill -TERM $$ +EOF + + chmod u+x "$script_path" + + if is_windows; then + echo "@$MINGW_SHELL '$script_path'" >"$script_path.cmd" + fi +} + +# Returns the name of the variable that contains the line number +# we should read next when waiting on $script from $peer. +# usage: next_line_var foo host-up +next_line_var() { + peer=$1 + script=$(echo "$2" | sed 's/[^a-zA-Z0-9]/_/g') + printf "%s" "next_line_${peer}_${script}" +} + +# Waits for `peer`'s script `script` to finish `count` number of times. +# usage: wait_script { foo | bar | baz } { tinc-up | host-up | ... } [count=1] +wait_script() { + peer=$1 + script=$2 + count=$3 + + if [ -z "$count" ] || [ "$count" -lt 1 ]; then + count=1 + fi + + # Find out the location of the log and how many lines we should skip + # (because we've already seen them in previous invocations of wait_script + # for current $peer and $script). + line_var=$(next_line_var "$peer" "$script") + + # eval is the only solution supported by POSIX shells. + # https://github.com/koalaman/shellcheck/wiki/SC3053 + # 1. $line_var expands into 'next_line_foo_hosts_bar_up' + # 2. the name is substituted and the command becomes 'echo "$next_line_foo_hosts_bar_up"' + # 3. the command is evaluated and the line number is assigned to $line + line=$(eval "echo \"\$$line_var\"") + + # This is the file that we monitor for script execution records. + script_log=$(script_runs_log "$peer") + + # Starting from $line, read until $count matches are found. + # Print the number of the last matching line and exit. + # GNU tail 2.82 and newer terminates by itself when the pipe breaks. + # To support other tails we do an explicit `kill`. + # FIFO is useful here because otherwise it's difficult to determine + # which tail process should be killed. We could stick them in a process + # group by enabling job control, but this results in weird behavior when + # running tests in parallel on some interactive shells + # (e.g. when /bin/sh is symlinked to dash). + new_line=$( + try_limit_time 60 sh -c " + fifo=\$$.fifo + cleanup() { rm -f \$fifo; } + cleanup && trap cleanup EXIT + + mkfifo \$$.fifo + tail -n '+$line' -f '$script_log' >\$fifo & + grep -n -m '$count' '^$script,' <\$fifo + kill \$! + " | awk -F: 'END { print $1 }' + ) + + # Remember the next line number for future reference. We'll use it if + # wait_script is called again with same $peer and $script. + read -r "${line_var?}" <&2 "CI server detected, performing aggressive cleanup" + kill_processes TERM tinc tincd + kill_processes KILL tinc tincd + fi + ) || true +} + +# Generate path to current shell which can be used from Windows applications. +if is_windows; then + MINGW_SHELL=$(cygpath --mixed -- "$SHELL") +fi + +# This was called from a tincd script. Skip executing commands with side effects. +[ -n "$NAME" ] && return + +echo [STEP] Check for leftover tinc daemons and test directories + +# Cleanup leftovers from previous runs. +stop_all_tincs + +# On Windows this can actually fail. We don't want to suppress possible failure with -f. +if [ -d "$DIR_FOO" ]; then rm -r "$DIR_FOO"; fi +if [ -d "$DIR_BAR" ]; then rm -r "$DIR_BAR"; fi +if [ -d "$DIR_BAZ" ]; then rm -r "$DIR_BAZ"; fi + +# Register cleanup function so we don't have to call it everywhere +# (and failed scripts do not leave stray tincd running). +trap cleanup EXIT INT TERM