Add cleanup hook for integration tests
[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_SKIP_TEST=77
30
31 # The list of the environment variables that tinc injects into the scripts it calls.
32 # shellcheck disable=SC2016
33 TINC_SCRIPT_VARS='$NETNAME,$NAME,$DEVICE,$IFACE,$NODE,$REMOTEADDRESS,$REMOTEPORT,$SUBNET,$WEIGHT,$INVITATION_FILE,$INVITATION_URL,$DEBUG'
34
35 # Test directories
36
37 # Reuse script name if it was passed in an env var (when imported from tinc scripts).
38 if [ -z "$SCRIPTNAME" ]; then
39   SCRIPTNAME=$(basename "$0")
40 fi
41
42 # Network names for tincd daemons.
43 net1=$SCRIPTNAME.1
44 net2=$SCRIPTNAME.2
45 net3=$SCRIPTNAME.3
46
47 # Configuration/pidfile directories for tincd daemons.
48 DIR_FOO=$(realdir "$PWD/$net1")
49 DIR_BAR=$(realdir "$PWD/$net2")
50 DIR_BAZ=$(realdir "$PWD/$net3")
51
52 # Register helper functions
53
54 # Alias gtimeout to timeout if it exists.
55 if type gtimeout >/dev/null; then
56   timeout() { gtimeout "$@"; }
57 fi
58
59 # Are the shell tools provided by busybox?
60 is_busybox() {
61   timeout --help 2>&1 | grep -q -i busybox
62 }
63
64 # busybox timeout returns 128 + signal number (which is TERM by default)
65 if is_busybox; then
66   # shellcheck disable=SC2034
67   EXIT_TIMEOUT=$((128 + 15))
68 else
69   # shellcheck disable=SC2034
70   EXIT_TIMEOUT=124
71 fi
72
73 # Is this msys2?
74 is_windows() {
75   test "$(uname -o)" = Msys
76 }
77
78 # Are we running on a CI server?
79 is_ci() {
80   test "$CI"
81 }
82
83 # Dump error message and exit with an error.
84 bail() {
85   echo >&2 "$@"
86   exit 1
87 }
88
89 # Remove carriage returns to normalize strings on Windows for easier comparisons.
90 rm_cr() {
91   tr -d '\r'
92 }
93
94 # Executes whatever is passed to it, checking that the resulting exit code is non-zero.
95 must_fail() {
96   if "$@"; then
97     bail "expected a non-zero exit code"
98   fi
99 }
100
101 # Runs its arguments with timeout(1) or gtimeout(1) if either are installed.
102 # Usage: try_limit_time 10 command --with --args
103 if type timeout >/dev/null; then
104   if is_busybox; then
105     # busybox does not support --foreground
106     try_limit_time() {
107       time=$1
108       shift
109       timeout "$time" "$@"
110     }
111   else
112     # BSD and GNU timeout do not require special handling
113     try_limit_time() {
114       time=$1
115       shift
116       timeout --foreground "$time" "$@"
117     }
118   fi
119 else
120   try_limit_time() {
121     echo >&2 "timeout was not found, running without time limits!"
122     shift
123     "$@"
124   }
125 fi
126
127 # wc -l on mac prints whitespace before the actual number.
128 # This is simplest cross-platform alternative without that behavior.
129 count_lines() {
130   awk 'END{ print NR }'
131 }
132
133 # Calls compiled tinc, passing any supplied arguments.
134 # Usage: tinc { foo | bar | baz } --arg1 val1 "$args"
135 tinc() {
136   peer=$1
137   shift
138
139   case "$peer" in
140   foo) try_limit_time 30 "$tinc_path" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" "$@" ;;
141   bar) try_limit_time 30 "$tinc_path" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" "$@" ;;
142   baz) try_limit_time 30 "$tinc_path" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" "$@" ;;
143   *) bail "invalid command [[$peer $*]]" ;;
144   esac
145 }
146
147 # Calls compiled tincd, passing any supplied arguments.
148 # Usage: tincd { foo | bar | baz } --arg1 val1 "$args"
149 tincd() {
150   peer=$1
151   shift
152
153   case "$peer" in
154   foo) try_limit_time 30 "$tincd_path" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" --logfile="$DIR_FOO/log" -d5 "$@" ;;
155   bar) try_limit_time 30 "$tincd_path" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" --logfile="$DIR_BAR/log" -d5 "$@" ;;
156   baz) try_limit_time 30 "$tincd_path" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" --logfile="$DIR_BAZ/log" -d5 "$@" ;;
157   *) bail "invalid command [[$peer $*]]" ;;
158   esac
159 }
160
161 # Start the specified tinc daemon.
162 # usage: start_tinc { foo | bar | baz }
163 start_tinc() {
164   peer=$1
165   shift
166
167   case "$peer" in
168   foo) tinc "$peer" start --logfile="$DIR_FOO/log" -d5 "$@" ;;
169   bar) tinc "$peer" start --logfile="$DIR_BAR/log" -d5 "$@" ;;
170   baz) tinc "$peer" start --logfile="$DIR_BAZ/log" -d5 "$@" ;;
171   *) bail "invalid peer $peer" ;;
172   esac
173 }
174
175 # Stop all tinc clients.
176 stop_all_tincs() {
177   (
178     # In case these pid files are mangled.
179     set +e
180     [ -f "$DIR_FOO/pid" ] && tinc foo stop
181     [ -f "$DIR_BAR/pid" ] && tinc bar stop
182     [ -f "$DIR_BAZ/pid" ] && tinc baz stop
183     true
184   )
185 }
186
187 # Checks that the number of reachable nodes matches what is expected.
188 # usage: require_nodes node_name expected_number
189 require_nodes() {
190   echo >&2 "Check that we're able to reach tincd"
191   test "$(tinc "$1" pid | count_lines)" = 1
192
193   echo >&2 "Check the number of reachable nodes for $1 (expecting $2)"
194   actual="$(tinc "$1" dump reachable nodes | count_lines)"
195
196   if [ "$actual" != "$2" ]; then
197     echo >&2 "tinc $1: expected $2 reachable nodes, got $actual"
198     exit 1
199   fi
200 }
201
202 peer_directory() {
203   case "$peer" in
204   foo) echo "$DIR_FOO" ;;
205   bar) echo "$DIR_BAR" ;;
206   baz) echo "$DIR_BAZ" ;;
207   *) bail "invalid peer $peer" ;;
208   esac
209 }
210
211 # This is an append-only log of all scripts executed by all peers.
212 script_runs_log() {
213   echo "$(peer_directory "$1")/script-runs.log"
214 }
215
216 # Create tincd script. If it fails, it kills the test script with SIGTERM.
217 # usage: create_script { foo | bar | baz } { tinc-up | host-down | ... } 'script content'
218 create_script() {
219   peer=$1
220   script=$2
221   shift 2
222
223   # This is the line that we should start from when reading the script execution log while waiting
224   # for $script from $peer. It is a poor man's hash map to avoid polluting tinc's home directory with
225   # "last seen" files. There seem to be no good solutions to this that are compatible with all shells.
226   line_var=$(next_line_var "$peer" "$script")
227
228   # We must reassign it here in case the script is recreated.
229   # shellcheck disable=SC2229
230   read -r "$line_var" <<EOF
231 1
232 EOF
233
234   # Full path to the script.
235   script_path=$(peer_directory "$peer")/$script
236
237   # Full path to the script execution log (one for each peer).
238   script_log=$(script_runs_log "$peer")
239   printf '' >"$script_log"
240
241   # Script output is redirected into /dev/null. Otherwise, it ends up
242   # in tinc's output and breaks things like 'tinc invite'.
243   cat >"$script_path" <<EOF
244 #!/bin/sh
245 (
246   cd "$PWD" || exit 1
247   SCRIPTNAME="$SCRIPTNAME" . ./testlib.sh
248   $@
249   echo "$script,\$$,$TINC_SCRIPT_VARS" >>"$script_log"
250 ) >/dev/null 2>&1 || kill -TERM $$
251 EOF
252
253   chmod u+x "$script_path"
254
255   if is_windows; then
256     echo "@$MINGW_SHELL '$script_path'" >"$script_path.cmd"
257   fi
258 }
259
260 # Returns the name of the variable that contains the line number
261 # we should read next when waiting on $script from $peer.
262 # usage: next_line_var foo host-up
263 next_line_var() {
264   peer=$1
265   script=$(echo "$2" | sed 's/[^a-zA-Z0-9]/_/g')
266   printf "%s" "next_line_${peer}_${script}"
267 }
268
269 # Waits for `peer`'s script `script` to finish `count` number of times.
270 # usage: wait_script { foo | bar | baz } { tinc-up | host-up | ... } [count=1]
271 wait_script() {
272   peer=$1
273   script=$2
274   count=$3
275
276   if [ -z "$count" ] || [ "$count" -lt 1 ]; then
277     count=1
278   fi
279
280   # Find out the location of the log and how many lines we should skip
281   # (because we've already seen them in previous invocations of wait_script
282   # for current $peer and $script).
283   line_var=$(next_line_var "$peer" "$script")
284
285   # eval is the only solution supported by POSIX shells.
286   # https://github.com/koalaman/shellcheck/wiki/SC3053
287   #   1. $line_var expands into 'next_line_foo_hosts_bar_up'
288   #   2. the name is substituted and the command becomes 'echo "$next_line_foo_hosts_bar_up"'
289   #   3. the command is evaluated and the line number is assigned to $line
290   line=$(eval "echo \"\$$line_var\"")
291
292   # This is the file that we monitor for script execution records.
293   script_log=$(script_runs_log "$peer")
294
295   # Starting from $line, read until $count matches are found.
296   # Print the number of the last matching line and exit.
297   # GNU tail 2.82 and newer terminates by itself when the pipe breaks.
298   # To support other tails we do an explicit `kill`.
299   # FIFO is useful here because otherwise it's difficult to determine
300   # which tail process should be killed. We could stick them in a process
301   # group by enabling job control, but this results in weird behavior when
302   # running tests in parallel on some interactive shells
303   # (e.g. when /bin/sh is symlinked to dash).
304   new_line=$(
305     try_limit_time 60 sh -c "
306       fifo=\$$.fifo
307       cleanup() { rm -f \$fifo; }
308       cleanup && trap cleanup EXIT
309
310       mkfifo \$$.fifo
311       tail -n '+$line' -f '$script_log' >\$fifo &
312       grep -n -m '$count' '^$script,'   <\$fifo
313       kill \$!
314     " | awk -F: 'END { print $1 }'
315   )
316
317   # Remember the next line number for future reference. We'll use it if
318   # wait_script is called again with same $peer and $script.
319   read -r "${line_var?}" <<EOF
320 $((line + new_line))
321 EOF
322 }
323
324 # Are we running tests in parallel?
325 is_parallel() {
326   # Grep the make flags for any of: '-j', '-j5', '-j 42', but not 'n-j', '-junk'.
327   echo "$MAKEFLAGS" | grep -E -q '(^|[[:space:]])-j[[:digit:]]*([[:space:]]|$)'
328 }
329
330 # Cleanup after running each script.
331 cleanup() {
332   (
333     set +ex
334
335     if command -v cleanup_hook 2>/dev/null; then
336       echo >&2 "Cleanup hook found, calling..."
337       cleanup_hook
338     fi
339
340     stop_all_tincs
341
342     # Ask nicely, then kill anything that's left.
343     if is_ci && ! is_parallel; then
344       kill_processes() {
345         signal=$1
346         shift
347         for process in "$@"; do
348           pkill -"SIG$signal" -x -u "$(id -u)" "$process"
349         done
350       }
351       echo >&2 "CI server detected, performing aggressive cleanup"
352       kill_processes TERM tinc tincd
353       kill_processes KILL tinc tincd
354     fi
355   ) || true
356 }
357
358 # Generate path to current shell which can be used from Windows applications.
359 if is_windows; then
360   MINGW_SHELL=$(cygpath --mixed -- "$SHELL")
361 fi
362
363 # This was called from a tincd script. Skip executing commands with side effects.
364 [ -n "$NAME" ] && return
365
366 echo [STEP] Check for leftover tinc daemons and test directories
367
368 # Cleanup leftovers from previous runs.
369 stop_all_tincs
370
371 # On Windows this can actually fail. We don't want to suppress possible failure with -f.
372 if [ -d "$DIR_FOO" ]; then rm -r "$DIR_FOO"; fi
373 if [ -d "$DIR_BAR" ]; then rm -r "$DIR_BAR"; fi
374 if [ -d "$DIR_BAZ" ]; then rm -r "$DIR_BAZ"; fi
375
376 # Register cleanup function so we don't have to call it everywhere
377 # (and failed scripts do not leave stray tincd running).
378 trap cleanup EXIT INT TERM