c65bdde8777610f8288fda9551f1710e4dcd497d
[tinc] / test / testlib.sh.in
1 #!/bin/sh
2
3 set -ex
4
5 echo [STEP] Initialize test library
6
7 # Paths to compiled executables
8
9 # realpath on FreeBSD fails if the path does not exist.
10 realdir() {
11   [ -e "$1" ] || mkdir -p "$1"
12   if type realpath >/dev/null; then
13     realpath "$1"
14   else
15     readlink -f "$1"
16   fi
17 }
18
19 tincd_path=$(realdir "../src/tincd@EXEEXT@")
20 tinc_path=$(realdir "../src/tinc@EXEEXT@")
21
22 # shellcheck disable=SC2034
23 SPTPS_TEST=$(realdir "../src/sptps_test@EXEEXT@")
24 # shellcheck disable=SC2034
25 SPTPS_KEYPAIR=$(realdir "../src/sptps_keypair@EXEEXT@")
26
27 # Exit status list
28 # shellcheck disable=SC2034
29 EXIT_FAILURE=1
30 # shellcheck disable=SC2034
31 EXIT_SKIP_TEST=77
32
33 # The list of the environment variables that tinc injects into the scripts it calls.
34 # shellcheck disable=SC2016
35 TINC_SCRIPT_VARS='$NETNAME,$NAME,$DEVICE,$IFACE,$NODE,$REMOTEADDRESS,$REMOTEPORT,$SUBNET,$WEIGHT,$INVITATION_FILE,$INVITATION_URL,$DEBUG'
36
37 # Test directories
38
39 # Reuse script name if it was passed in an env var (when imported from tinc scripts).
40 if [ -z "$SCRIPTNAME" ]; then
41   SCRIPTNAME=$(basename "$0")
42 fi
43
44 # Network names for tincd daemons.
45 net1=$SCRIPTNAME.1
46 net2=$SCRIPTNAME.2
47 net3=$SCRIPTNAME.3
48
49 # Configuration/pidfile directories for tincd daemons.
50 DIR_FOO=$(realdir "$PWD/$net1")
51 DIR_BAR=$(realdir "$PWD/$net2")
52 DIR_BAZ=$(realdir "$PWD/$net3")
53
54 # Register helper functions
55
56 # Alias gtimeout to timeout if it exists.
57 if type gtimeout >/dev/null; then
58   timeout() { gtimeout "$@"; }
59 fi
60
61 # As usual, BSD tools require special handling, as they do not support -i without a suffix.
62 # Note that there must be no space after -i, or it won't work on GNU sed.
63 sed_cmd() {
64   sed -i.orig "$@"
65 }
66
67 # Are the shell tools provided by busybox?
68 is_busybox() {
69   timeout --help 2>&1 | grep -q -i busybox
70 }
71
72 # busybox timeout returns 128 + signal number (which is TERM by default)
73 if is_busybox; then
74   # shellcheck disable=SC2034
75   EXIT_TIMEOUT=$((128 + 15))
76 else
77   # shellcheck disable=SC2034
78   EXIT_TIMEOUT=124
79 fi
80
81 # Is this msys2?
82 is_windows() {
83   test "$(uname -o)" = Msys
84 }
85
86 # Are we running on a CI server?
87 is_ci() {
88   test "$CI"
89 }
90
91 # Dump error message and exit with an error.
92 bail() {
93   echo >&2 "$@"
94   exit 1
95 }
96
97 # Remove carriage returns to normalize strings on Windows for easier comparisons.
98 rm_cr() {
99   tr -d '\r'
100 }
101
102 if is_windows; then
103   normalize_path() { cygpath --mixed -- "$@"; }
104 else
105   normalize_path() { echo "$@"; }
106 fi
107
108 # Executes whatever is passed to it, checking that the resulting exit code is non-zero.
109 must_fail() {
110   if "$@"; then
111     bail "expected a non-zero exit code"
112   fi
113 }
114
115 # Executes the passed command and checks two conditions:
116 #   1. it must exit successfully (with code 0)
117 #   2. its output (stdout + stderr) must include the substring from the first argument (ignoring case)
118 # usage: expect_msg 'expected message' command --with --args
119 expect_msg() {
120   message=$1
121   shift
122
123   if ! output=$("$@" 2>&1); then
124     bail 'expected 0 exit code'
125   fi
126
127   if ! echo "$output" | grep -q -i "$message"; then
128     bail "expected message '$message'"
129   fi
130 }
131
132 # The reverse of expect_msg. We cannot simply wrap expect_msg with must_fail
133 # because there should be a separate check for tinc exit code.
134 fail_on_msg() {
135   message=$1
136   shift
137
138   if ! output=$("$@" 2>&1); then
139     bail 'expected 0 exit code'
140   fi
141
142   if echo "$output" | grep -q -i "$message"; then
143     bail "unexpected message '$message'"
144   fi
145 }
146
147 # Like expect_msg, but the command must fail with a non-zero exit code.
148 # usage: must_fail_with_msg 'expected message' command --with --args
149 must_fail_with_msg() {
150   message=$1
151   shift
152
153   if output=$("$@" 2>&1); then
154     bail "expected a non-zero exit code"
155   fi
156
157   if ! echo "$output" | grep -i -q "$message"; then
158     bail "expected message '$message'"
159   fi
160 }
161
162 # Is the legacy protocol enabled?
163 with_legacy() {
164   tincd foo --version | grep -q legacy_protocol
165 }
166
167 # Are we running with EUID 0?
168 is_root() {
169   test "$(id -u)" = 0
170 }
171
172 # Executes whatever is passed to it, checking that the resulting exit code is equal to the first argument.
173 expect_code() {
174   expected=$1
175   shift
176
177   code=0
178   "$@" || code=$?
179
180   if [ $code != "$expected" ]; then
181     bail "wrong exit code $code, expected $expected"
182   fi
183 }
184
185 # Runs its arguments with timeout(1) or gtimeout(1) if either are installed.
186 # Usage: try_limit_time 10 command --with --args
187 if type timeout >/dev/null; then
188   if is_busybox; then
189     # busybox does not support --foreground
190     try_limit_time() {
191       time=$1
192       shift
193       timeout "$time" "$@"
194     }
195   else
196     # BSD and GNU timeout do not require special handling
197     try_limit_time() {
198       time=$1
199       shift
200       timeout --foreground "$time" "$@"
201     }
202   fi
203 else
204   try_limit_time() {
205     echo >&2 "timeout was not found, running without time limits!"
206     shift
207     "$@"
208   }
209 fi
210
211 # wc -l on mac prints whitespace before the actual number.
212 # This is simplest cross-platform alternative without that behavior.
213 count_lines() {
214   awk 'END{ print NR }'
215 }
216
217 # Calls compiled tinc, passing any supplied arguments.
218 # Usage: tinc { foo | bar | baz } --arg1 val1 "$args"
219 tinc() {
220   peer=$1
221   shift
222
223   case "$peer" in
224   foo) try_limit_time 30 "$tinc_path" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" "$@" ;;
225   bar) try_limit_time 30 "$tinc_path" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" "$@" ;;
226   baz) try_limit_time 30 "$tinc_path" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" "$@" ;;
227   *) bail "invalid command [[$peer $*]]" ;;
228   esac
229 }
230
231 # Calls compiled tincd, passing any supplied arguments.
232 # Usage: tincd { foo | bar | baz } --arg1 val1 "$args"
233 tincd() {
234   peer=$1
235   shift
236
237   case "$peer" in
238   foo) try_limit_time 30 "$tincd_path" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" --logfile="$DIR_FOO/log" -d5 "$@" ;;
239   bar) try_limit_time 30 "$tincd_path" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" --logfile="$DIR_BAR/log" -d5 "$@" ;;
240   baz) try_limit_time 30 "$tincd_path" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" --logfile="$DIR_BAZ/log" -d5 "$@" ;;
241   *) bail "invalid command [[$peer $*]]" ;;
242   esac
243 }
244
245 # Start the specified tinc daemon.
246 # usage: start_tinc { foo | bar | baz }
247 start_tinc() {
248   peer=$1
249   shift
250
251   case "$peer" in
252   foo) tinc "$peer" start --logfile="$DIR_FOO/log" -d5 "$@" ;;
253   bar) tinc "$peer" start --logfile="$DIR_BAR/log" -d5 "$@" ;;
254   baz) tinc "$peer" start --logfile="$DIR_BAZ/log" -d5 "$@" ;;
255   *) bail "invalid peer $peer" ;;
256   esac
257 }
258
259 # Stop all tinc clients.
260 stop_all_tincs() {
261   (
262     # In case these pid files are mangled.
263     set +e
264     [ -f "$DIR_FOO/pid" ] && tinc foo stop
265     [ -f "$DIR_BAR/pid" ] && tinc bar stop
266     [ -f "$DIR_BAZ/pid" ] && tinc baz stop
267     true
268   )
269 }
270
271 # Checks that the number of reachable nodes matches what is expected.
272 # usage: require_nodes node_name expected_number
273 require_nodes() {
274   echo >&2 "Check that we're able to reach tincd"
275   test "$(tinc "$1" pid | count_lines)" = 1
276
277   echo >&2 "Check the number of reachable nodes for $1 (expecting $2)"
278   actual="$(tinc "$1" dump reachable nodes | count_lines)"
279
280   if [ "$actual" != "$2" ]; then
281     echo >&2 "tinc $1: expected $2 reachable nodes, got $actual"
282     exit 1
283   fi
284 }
285
286 peer_directory() {
287   peer=$1
288   case "$peer" in
289   foo) echo "$DIR_FOO" ;;
290   bar) echo "$DIR_BAR" ;;
291   baz) echo "$DIR_BAZ" ;;
292   *) bail "invalid peer $peer" ;;
293   esac
294 }
295
296 # This is an append-only log of all scripts executed by all peers.
297 script_runs_log() {
298   echo "$(peer_directory "$1")/script-runs.log"
299 }
300
301 # Create tincd script. If it fails, it kills the test script with SIGTERM.
302 # usage: create_script { foo | bar | baz } { tinc-up | host-down | ... } 'script content'
303 create_script() {
304   peer=$1
305   script=$2
306   shift 2
307
308   # This is the line that we should start from when reading the script execution log while waiting
309   # for $script from $peer. It is a poor man's hash map to avoid polluting tinc's home directory with
310   # "last seen" files. There seem to be no good solutions to this that are compatible with all shells.
311   line_var=$(next_line_var "$peer" "$script")
312
313   # We must reassign it here in case the script is recreated.
314   # shellcheck disable=SC2229
315   read -r "$line_var" <<EOF
316 1
317 EOF
318
319   # Full path to the script.
320   script_path=$(peer_directory "$peer")/$script
321
322   # Full path to the script execution log (one for each peer).
323   script_log=$(script_runs_log "$peer")
324   printf '' >"$script_log"
325
326   # Script output is redirected into /dev/null. Otherwise, it ends up
327   # in tinc's output and breaks things like 'tinc invite'.
328   cat >"$script_path" <<EOF
329 #!/bin/sh
330 (
331   cd "$PWD" || exit 1
332   SCRIPTNAME="$SCRIPTNAME" . ./testlib.sh
333   $@
334   echo "$script,\$$,$TINC_SCRIPT_VARS" >>"$script_log"
335 ) >/dev/null 2>&1 || kill -TERM $$
336 EOF
337
338   chmod u+x "$script_path"
339
340   if is_windows; then
341     echo "@$MINGW_SHELL '$script_path'" >"$script_path.cmd"
342   fi
343 }
344
345 # Returns the name of the variable that contains the line number
346 # we should read next when waiting on $script from $peer.
347 # usage: next_line_var foo host-up
348 next_line_var() {
349   peer=$1
350   script=$(echo "$2" | sed 's/[^a-zA-Z0-9]/_/g')
351   printf "%s" "next_line_${peer}_${script}"
352 }
353
354 # Waits for `peer`'s script `script` to finish `count` number of times.
355 # usage: wait_script { foo | bar | baz } { tinc-up | host-up | ... } [count=1]
356 wait_script() {
357   peer=$1
358   script=$2
359   count=$3
360
361   if [ -z "$count" ] || [ "$count" -lt 1 ]; then
362     count=1
363   fi
364
365   # Find out the location of the log and how many lines we should skip
366   # (because we've already seen them in previous invocations of wait_script
367   # for current $peer and $script).
368   line_var=$(next_line_var "$peer" "$script")
369
370   # eval is the only solution supported by POSIX shells.
371   # https://github.com/koalaman/shellcheck/wiki/SC3053
372   #   1. $line_var expands into 'next_line_foo_hosts_bar_up'
373   #   2. the name is substituted and the command becomes 'echo "$next_line_foo_hosts_bar_up"'
374   #   3. the command is evaluated and the line number is assigned to $line
375   line=$(eval "echo \"\$$line_var\"")
376
377   # This is the file that we monitor for script execution records.
378   script_log=$(script_runs_log "$peer")
379
380   # Starting from $line, read until $count matches are found.
381   # Print the number of the last matching line and exit.
382   # GNU tail 2.82 and newer terminates by itself when the pipe breaks.
383   # To support other tails we do an explicit `kill`.
384   # FIFO is useful here because otherwise it's difficult to determine
385   # which tail process should be killed. We could stick them in a process
386   # group by enabling job control, but this results in weird behavior when
387   # running tests in parallel on some interactive shells
388   # (e.g. when /bin/sh is symlinked to dash).
389   new_line=$(
390     try_limit_time 60 sh -c "
391       fifo=\$$.fifo
392       cleanup() { rm -f \$fifo; }
393       cleanup && trap cleanup EXIT
394
395       mkfifo \$$.fifo
396       tail -n '+$line' -f '$script_log' >\$fifo &
397       grep -n -m '$count' '^$script,'   <\$fifo
398       kill \$!
399     " | awk -F: 'END { print $1 }'
400   )
401
402   # Remember the next line number for future reference. We'll use it if
403   # wait_script is called again with same $peer and $script.
404   read -r "${line_var?}" <<EOF
405 $((line + new_line))
406 EOF
407 }
408
409 # Are we running tests in parallel?
410 is_parallel() {
411   # Grep the make flags for any of: '-j', '-j5', '-j 42', but not 'n-j', '-junk'.
412   echo "$MAKEFLAGS" | grep -E -q '(^|[[:space:]])-j[[:digit:]]*([[:space:]]|$)'
413 }
414
415 # Cleanup after running each script.
416 cleanup() {
417   (
418     set +ex
419
420     if command -v cleanup_hook 2>/dev/null; then
421       echo >&2 "Cleanup hook found, calling..."
422       cleanup_hook
423     fi
424
425     stop_all_tincs
426
427     # Ask nicely, then kill anything that's left.
428     if is_ci && ! is_parallel; then
429       kill_processes() {
430         signal=$1
431         shift
432         for process in "$@"; do
433           pkill -"SIG$signal" -x -u "$(id -u)" "$process"
434         done
435       }
436       echo >&2 "CI server detected, performing aggressive cleanup"
437       kill_processes TERM tinc tincd
438       kill_processes KILL tinc tincd
439     fi
440   ) || true
441 }
442
443 # If we're on a CI server, the test requires superuser privileges to run, and we're not
444 # currently a superuser, try running the test as one and fail if it doesn't work (the
445 # system must be configured to provide passwordless sudo for our user).
446 require_root() {
447   if is_root; then
448     return
449   fi
450   if is_ci; then
451     echo "root is required for test $SCRIPTNAME, but we're a regular user; elevating privileges..."
452     if ! command -v sudo 2>/dev/null; then
453       bail "please install sudo and configure passwordless auth for user $USER"
454     fi
455     if ! sudo --preserve-env --non-interactive true; then
456       bail "sudo is not allowed or requires a password for user $USER"
457     fi
458     exec sudo --preserve-env "$@"
459   else
460     # Avoid these kinds of surprises outside CI. Just skip the test.
461     echo "root is required for test $SCRIPTNAME, but we're a regular user; skipping"
462     exit $EXIT_SKIP_TEST
463   fi
464 }
465
466 # Generate path to current shell which can be used from Windows applications.
467 if is_windows; then
468   MINGW_SHELL=$(normalize_path "$SHELL")
469 fi
470
471 # This was called from a tincd script. Skip executing commands with side effects.
472 [ -n "$NAME" ] && return
473
474 echo [STEP] Check for leftover tinc daemons and test directories
475
476 # Cleanup leftovers from previous runs.
477 stop_all_tincs
478
479 if [ -d "$DIR_FOO" ]; then rm -rf "$DIR_FOO"; fi
480 if [ -d "$DIR_BAR" ]; then rm -rf "$DIR_BAR"; fi
481 if [ -d "$DIR_BAZ" ]; then rm -rf "$DIR_BAZ"; fi
482
483 # Register cleanup function so we don't have to call it everywhere
484 # (and failed scripts do not leave stray tincd running).
485 trap cleanup EXIT INT TERM