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