5 echo [STEP] Initialize test library
7 # Paths to compiled executables
9 # realpath on FreeBSD fails if the path does not exist.
11 [ -e "$1" ] || mkdir -p "$1"
12 if type realpath >/dev/null; then
20 # shellcheck disable=SC2034
22 # shellcheck disable=SC2034
25 # The list of the environment variables that tinc injects into the scripts it calls.
26 # shellcheck disable=SC2016
27 TINC_SCRIPT_VARS='$NETNAME,$NAME,$DEVICE,$IFACE,$NODE,$REMOTEADDRESS,$REMOTEPORT,$SUBNET,$WEIGHT,$INVITATION_FILE,$INVITATION_URL,$DEBUG'
31 # Reuse script name if it was passed in an env var (when imported from tinc scripts).
32 if [ -z "$SCRIPTNAME" ]; then
33 SCRIPTNAME=$(basename "$0")
36 # Network names for tincd daemons.
41 # Configuration/pidfile directories for tincd daemons.
42 DIR_FOO=$(realdir "$PWD/$net1")
43 DIR_BAR=$(realdir "$PWD/$net2")
44 DIR_BAZ=$(realdir "$PWD/$net3")
46 # Register helper functions
48 if [ "$(uname -s)" = SunOS ]; then
52 grep() { $gnu/grep "$@"; }
53 tail() { $gnu/tail "$@"; }
55 if ! tail /dev/null || ! echo '' | grep ''; then
56 echo >&2 'Sorry, native Solaris tools are not supported. Please install GNU Coreutils.'
63 # Alias gtimeout to timeout if it exists.
64 if type gtimeout >/dev/null; then
65 timeout() { gtimeout "$@"; }
68 # As usual, BSD tools require special handling, as they do not support -i without a suffix.
69 # Note that there must be no space after -i, or it won't work on GNU sed.
74 # Are the shell tools provided by busybox?
76 timeout --help 2>&1 | grep -q -i busybox
79 # busybox timeout returns 128 + signal number (which is TERM by default)
81 # shellcheck disable=SC2034
82 EXIT_TIMEOUT=$((128 + 15))
84 # shellcheck disable=SC2034
90 test "$(uname -o)" = Msys
93 # Are we running on a CI server?
98 # Dump error message and exit with an error.
104 # Remove carriage returns to normalize strings on Windows for easier comparisons.
110 normalize_path() { cygpath --mixed -- "$@"; }
112 normalize_path() { echo "$@"; }
115 # Executes whatever is passed to it, checking that the resulting exit code is non-zero.
118 bail "expected a non-zero exit code"
122 # Executes the passed command and checks two conditions:
123 # 1. it must exit successfully (with code 0)
124 # 2. its output (stdout + stderr) must include the substring from the first argument (ignoring case)
125 # usage: expect_msg 'expected message' command --with --args
130 if ! output=$("$@" 2>&1); then
131 bail 'expected 0 exit code'
134 if ! echo "$output" | grep -q -i "$message"; then
135 bail "expected message '$message'"
139 # The reverse of expect_msg. We cannot simply wrap expect_msg with must_fail
140 # because there should be a separate check for tinc exit code.
145 if ! output=$("$@" 2>&1); then
146 bail 'expected 0 exit code'
149 if echo "$output" | grep -q -i "$message"; then
150 bail "unexpected message '$message'"
154 # Like expect_msg, but the command must fail with a non-zero exit code.
155 # usage: must_fail_with_msg 'expected message' command --with --args
156 must_fail_with_msg() {
160 if output=$("$@" 2>&1); then
161 bail "expected a non-zero exit code"
164 if ! echo "$output" | grep -i -q "$message"; then
165 bail "expected message '$message'"
169 # Is the legacy protocol enabled?
171 tincd foo --version | grep -q legacy_protocol
174 # Are we running with EUID 0?
179 # Executes whatever is passed to it, checking that the resulting exit code is equal to the first argument.
187 if [ $code != "$expected" ]; then
188 bail "wrong exit code $code, expected $expected"
192 # wc -l on mac prints whitespace before the actual number.
193 # This is simplest cross-platform alternative without that behavior.
195 awk 'END{ print NR }'
198 # Calls compiled tinc, passing any supplied arguments.
199 # Usage: tinc { foo | bar | baz } --arg1 val1 "$args"
205 foo) "$TINC_PATH" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" "$@" ;;
206 bar) "$TINC_PATH" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" "$@" ;;
207 baz) "$TINC_PATH" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" "$@" ;;
208 *) bail "invalid command [[$peer $*]]" ;;
212 # Calls compiled tincd, passing any supplied arguments.
213 # Usage: tincd { foo | bar | baz } --arg1 val1 "$args"
219 foo) "$TINCD_PATH" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" --logfile="$DIR_FOO/log" -d5 "$@" ;;
220 bar) "$TINCD_PATH" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" --logfile="$DIR_BAR/log" -d5 "$@" ;;
221 baz) "$TINCD_PATH" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" --logfile="$DIR_BAZ/log" -d5 "$@" ;;
222 *) bail "invalid command [[$peer $*]]" ;;
226 # Start the specified tinc daemon.
227 # usage: start_tinc { foo | bar | baz }
233 foo) tinc "$peer" start --logfile="$DIR_FOO/log" -d5 "$@" ;;
234 bar) tinc "$peer" start --logfile="$DIR_BAR/log" -d5 "$@" ;;
235 baz) tinc "$peer" start --logfile="$DIR_BAZ/log" -d5 "$@" ;;
236 *) bail "invalid peer $peer" ;;
240 # Stop all tinc clients.
243 # In case these pid files are mangled.
245 [ -f "$DIR_FOO/pid" ] && tinc foo stop
246 [ -f "$DIR_BAR/pid" ] && tinc bar stop
247 [ -f "$DIR_BAZ/pid" ] && tinc baz stop
252 # Checks that the number of reachable nodes matches what is expected.
253 # usage: require_nodes node_name expected_number
255 echo >&2 "Check that we're able to reach tincd"
256 test "$(tinc "$1" pid | count_lines)" = 1
258 echo >&2 "Check the number of reachable nodes for $1 (expecting $2)"
259 actual="$(tinc "$1" dump reachable nodes | count_lines)"
261 if [ "$actual" != "$2" ]; then
262 echo >&2 "tinc $1: expected $2 reachable nodes, got $actual"
270 foo) echo "$DIR_FOO" ;;
271 bar) echo "$DIR_BAR" ;;
272 baz) echo "$DIR_BAZ" ;;
273 *) bail "invalid peer $peer" ;;
277 # This is an append-only log of all scripts executed by all peers.
279 echo "$(peer_directory "$1")/script-runs.log"
282 # Create tincd script. If it fails, it kills the test script with SIGTERM.
283 # usage: create_script { foo | bar | baz } { tinc-up | host-down | ... } 'script content'
289 # This is the line that we should start from when reading the script execution log while waiting
290 # for $script from $peer. It is a poor man's hash map to avoid polluting tinc's home directory with
291 # "last seen" files. There seem to be no good solutions to this that are compatible with all shells.
292 line_var=$(next_line_var "$peer" "$script")
294 # We must reassign it here in case the script is recreated.
295 # shellcheck disable=SC2229
296 read -r "$line_var" <<EOF
300 # Full path to the script.
301 script_path=$(peer_directory "$peer")/$script
303 # Full path to the script execution log (one for each peer).
304 script_log=$(script_runs_log "$peer")
305 printf '' >"$script_log"
307 # Script output is redirected into /dev/null. Otherwise, it ends up
308 # in tinc's output and breaks things like 'tinc invite'.
309 cat >"$script_path" <<EOF
313 SCRIPTNAME="$SCRIPTNAME" . "$TESTLIB_PATH"
315 echo "$script,\$$,$TINC_SCRIPT_VARS" >>"$script_log"
316 ) >/dev/null 2>&1 || kill -TERM $$
319 chmod u+x "$script_path"
322 echo "@$MINGW_SHELL '$script_path'" >"$script_path.cmd"
326 # Returns the name of the variable that contains the line number
327 # we should read next when waiting on $script from $peer.
328 # usage: next_line_var foo host-up
331 script=$(echo "$2" | sed 's/[^a-zA-Z0-9]/_/g')
332 printf "%s" "next_line_${peer}_${script}"
335 # Waits for `peer`'s script `script` to finish `count` number of times.
336 # usage: wait_script { foo | bar | baz } { tinc-up | host-up | ... } [count=1]
342 if [ -z "$count" ] || [ "$count" -lt 1 ]; then
346 # Find out the location of the log and how many lines we should skip
347 # (because we've already seen them in previous invocations of wait_script
348 # for current $peer and $script).
349 line_var=$(next_line_var "$peer" "$script")
351 # eval is the only solution supported by POSIX shells.
352 # https://github.com/koalaman/shellcheck/wiki/SC3053
353 # 1. $line_var expands into 'next_line_foo_hosts_bar_up'
354 # 2. the name is substituted and the command becomes 'echo "$next_line_foo_hosts_bar_up"'
355 # 3. the command is evaluated and the line number is assigned to $line
356 line=$(eval "echo \"\$$line_var\"")
358 # This is the file that we monitor for script execution records.
359 script_log=$(script_runs_log "$peer")
361 # Starting from $line, read until $count matches are found.
362 # Print the number of the last matching line and exit.
363 # GNU tail 2.82 and newer terminates by itself when the pipe breaks.
364 # To support other tails we do an explicit `kill`.
365 # FIFO is useful here because otherwise it's difficult to determine
366 # which tail process should be killed. We could stick them in a process
367 # group by enabling job control, but this results in weird behavior when
368 # running tests in parallel on some interactive shells
369 # (e.g. when /bin/sh is symlinked to dash).
374 # This weird thing is required to support old versions of ksh on NetBSD 8.2 and the like.
375 (tail -n +"$line" -f "$script_log" >"$fifo") &
379 $grep -n -m $count '^$script,' <'$fifo'
380 " | awk -F: 'END { print $1 }'
383 # Try to stop the background tail, ignoring possible failure (some tails
384 # detect EOF, some don't, so it may have already exited), but do wait on
385 # it (which is required at least by old ksh).
390 # Remember the next line number for future reference. We'll use it if
391 # wait_script is called again with same $peer and $script.
392 read -r "${line_var?}" <<EOF
397 # Cleanup after running each script.
402 if command -v cleanup_hook 2>/dev/null; then
403 echo >&2 "Cleanup hook found, calling..."
411 # If we're on a CI server, the test requires superuser privileges to run, and we're not
412 # currently a superuser, try running the test as one and fail if it doesn't work (the
413 # system must be configured to provide passwordless sudo for our user).
419 echo "root is required for test $SCRIPTNAME, but we're a regular user; elevating privileges..."
420 if ! command -v sudo 2>/dev/null; then
421 bail "please install sudo and configure passwordless auth for user $USER"
423 if ! sudo --preserve-env --non-interactive true; then
424 bail "sudo is not allowed or requires a password for user $USER"
426 exec sudo --preserve-env "$@"
428 # Avoid these kinds of surprises outside CI. Just skip the test.
429 echo "root is required for test $SCRIPTNAME, but we're a regular user; skipping"
430 exit "$EXIT_SKIP_TEST"
434 # Generate path to current shell which can be used from Windows applications.
436 MINGW_SHELL=$(normalize_path "$SHELL")
439 # This was called from a tincd script. Skip executing commands with side effects.
440 [ -n "$NAME" ] && return
442 echo [STEP] Check for leftover tinc daemons and test directories
444 # Cleanup leftovers from previous runs.
447 rm -rf "$DIR_FOO" "$DIR_BAR" "$DIR_BAZ"
449 # Register cleanup function so we don't have to call it everywhere
450 # (and failed scripts do not leave stray tincd running).
451 trap cleanup EXIT INT TERM