Add cmocka packages to CI jobs
[tinc] / test / testlib.sh
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 # Exit status list
20 # shellcheck disable=SC2034
21 EXIT_FAILURE=1
22 # shellcheck disable=SC2034
23 EXIT_SKIP_TEST=77
24
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'
28
29 # Test directories
30
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")
34 fi
35
36 # Network names for tincd daemons.
37 net1=$SCRIPTNAME.1
38 net2=$SCRIPTNAME.2
39 net3=$SCRIPTNAME.3
40
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")
45
46 # Register helper functions
47
48 # Alias gtimeout to timeout if it exists.
49 if type gtimeout >/dev/null; then
50   timeout() { gtimeout "$@"; }
51 fi
52
53 # As usual, BSD tools require special handling, as they do not support -i without a suffix.
54 # Note that there must be no space after -i, or it won't work on GNU sed.
55 sed_cmd() {
56   sed -i.orig "$@"
57 }
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 if is_windows; then
95   normalize_path() { cygpath --mixed -- "$@"; }
96 else
97   normalize_path() { echo "$@"; }
98 fi
99
100 # Executes whatever is passed to it, checking that the resulting exit code is non-zero.
101 must_fail() {
102   if "$@"; then
103     bail "expected a non-zero exit code"
104   fi
105 }
106
107 # Executes the passed command and checks two conditions:
108 #   1. it must exit successfully (with code 0)
109 #   2. its output (stdout + stderr) must include the substring from the first argument (ignoring case)
110 # usage: expect_msg 'expected message' command --with --args
111 expect_msg() {
112   message=$1
113   shift
114
115   if ! output=$("$@" 2>&1); then
116     bail 'expected 0 exit code'
117   fi
118
119   if ! echo "$output" | grep -q -i "$message"; then
120     bail "expected message '$message'"
121   fi
122 }
123
124 # The reverse of expect_msg. We cannot simply wrap expect_msg with must_fail
125 # because there should be a separate check for tinc exit code.
126 fail_on_msg() {
127   message=$1
128   shift
129
130   if ! output=$("$@" 2>&1); then
131     bail 'expected 0 exit code'
132   fi
133
134   if echo "$output" | grep -q -i "$message"; then
135     bail "unexpected message '$message'"
136   fi
137 }
138
139 # Like expect_msg, but the command must fail with a non-zero exit code.
140 # usage: must_fail_with_msg 'expected message' command --with --args
141 must_fail_with_msg() {
142   message=$1
143   shift
144
145   if output=$("$@" 2>&1); then
146     bail "expected a non-zero exit code"
147   fi
148
149   if ! echo "$output" | grep -i -q "$message"; then
150     bail "expected message '$message'"
151   fi
152 }
153
154 # Is the legacy protocol enabled?
155 with_legacy() {
156   tincd foo --version | grep -q legacy_protocol
157 }
158
159 # Are we running with EUID 0?
160 is_root() {
161   test "$(id -u)" = 0
162 }
163
164 # Executes whatever is passed to it, checking that the resulting exit code is equal to the first argument.
165 expect_code() {
166   expected=$1
167   shift
168
169   code=0
170   "$@" || code=$?
171
172   if [ $code != "$expected" ]; then
173     bail "wrong exit code $code, expected $expected"
174   fi
175 }
176
177 # wc -l on mac prints whitespace before the actual number.
178 # This is simplest cross-platform alternative without that behavior.
179 count_lines() {
180   awk 'END{ print NR }'
181 }
182
183 # Calls compiled tinc, passing any supplied arguments.
184 # Usage: tinc { foo | bar | baz } --arg1 val1 "$args"
185 tinc() {
186   peer=$1
187   shift
188
189   case "$peer" in
190   foo) "$TINC_PATH" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" "$@" ;;
191   bar) "$TINC_PATH" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" "$@" ;;
192   baz) "$TINC_PATH" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" "$@" ;;
193   *) bail "invalid command [[$peer $*]]" ;;
194   esac
195 }
196
197 # Calls compiled tincd, passing any supplied arguments.
198 # Usage: tincd { foo | bar | baz } --arg1 val1 "$args"
199 tincd() {
200   peer=$1
201   shift
202
203   case "$peer" in
204   foo) "$TINCD_PATH" -n "$net1" --config="$DIR_FOO" --pidfile="$DIR_FOO/pid" --logfile="$DIR_FOO/log" -d5 "$@" ;;
205   bar) "$TINCD_PATH" -n "$net2" --config="$DIR_BAR" --pidfile="$DIR_BAR/pid" --logfile="$DIR_BAR/log" -d5 "$@" ;;
206   baz) "$TINCD_PATH" -n "$net3" --config="$DIR_BAZ" --pidfile="$DIR_BAZ/pid" --logfile="$DIR_BAZ/log" -d5 "$@" ;;
207   *) bail "invalid command [[$peer $*]]" ;;
208   esac
209 }
210
211 # Start the specified tinc daemon.
212 # usage: start_tinc { foo | bar | baz }
213 start_tinc() {
214   peer=$1
215   shift
216
217   case "$peer" in
218   foo) tinc "$peer" start --logfile="$DIR_FOO/log" -d5 "$@" ;;
219   bar) tinc "$peer" start --logfile="$DIR_BAR/log" -d5 "$@" ;;
220   baz) tinc "$peer" start --logfile="$DIR_BAZ/log" -d5 "$@" ;;
221   *) bail "invalid peer $peer" ;;
222   esac
223 }
224
225 # Stop all tinc clients.
226 stop_all_tincs() {
227   (
228     # In case these pid files are mangled.
229     set +e
230     [ -f "$DIR_FOO/pid" ] && tinc foo stop
231     [ -f "$DIR_BAR/pid" ] && tinc bar stop
232     [ -f "$DIR_BAZ/pid" ] && tinc baz stop
233     true
234   )
235 }
236
237 # Checks that the number of reachable nodes matches what is expected.
238 # usage: require_nodes node_name expected_number
239 require_nodes() {
240   echo >&2 "Check that we're able to reach tincd"
241   test "$(tinc "$1" pid | count_lines)" = 1
242
243   echo >&2 "Check the number of reachable nodes for $1 (expecting $2)"
244   actual="$(tinc "$1" dump reachable nodes | count_lines)"
245
246   if [ "$actual" != "$2" ]; then
247     echo >&2 "tinc $1: expected $2 reachable nodes, got $actual"
248     exit 1
249   fi
250 }
251
252 peer_directory() {
253   peer=$1
254   case "$peer" in
255   foo) echo "$DIR_FOO" ;;
256   bar) echo "$DIR_BAR" ;;
257   baz) echo "$DIR_BAZ" ;;
258   *) bail "invalid peer $peer" ;;
259   esac
260 }
261
262 # This is an append-only log of all scripts executed by all peers.
263 script_runs_log() {
264   echo "$(peer_directory "$1")/script-runs.log"
265 }
266
267 # Create tincd script. If it fails, it kills the test script with SIGTERM.
268 # usage: create_script { foo | bar | baz } { tinc-up | host-down | ... } 'script content'
269 create_script() {
270   peer=$1
271   script=$2
272   shift 2
273
274   # This is the line that we should start from when reading the script execution log while waiting
275   # for $script from $peer. It is a poor man's hash map to avoid polluting tinc's home directory with
276   # "last seen" files. There seem to be no good solutions to this that are compatible with all shells.
277   line_var=$(next_line_var "$peer" "$script")
278
279   # We must reassign it here in case the script is recreated.
280   # shellcheck disable=SC2229
281   read -r "$line_var" <<EOF
282 1
283 EOF
284
285   # Full path to the script.
286   script_path=$(peer_directory "$peer")/$script
287
288   # Full path to the script execution log (one for each peer).
289   script_log=$(script_runs_log "$peer")
290   printf '' >"$script_log"
291
292   # Script output is redirected into /dev/null. Otherwise, it ends up
293   # in tinc's output and breaks things like 'tinc invite'.
294   cat >"$script_path" <<EOF
295 #!/bin/sh
296 (
297   cd "$PWD" || exit 1
298   SCRIPTNAME="$SCRIPTNAME" . "$TESTLIB_PATH"
299   $@
300   echo "$script,\$$,$TINC_SCRIPT_VARS" >>"$script_log"
301 ) >/dev/null 2>&1 || kill -TERM $$
302 EOF
303
304   chmod u+x "$script_path"
305
306   if is_windows; then
307     echo "@$MINGW_SHELL '$script_path'" >"$script_path.cmd"
308   fi
309 }
310
311 # Returns the name of the variable that contains the line number
312 # we should read next when waiting on $script from $peer.
313 # usage: next_line_var foo host-up
314 next_line_var() {
315   peer=$1
316   script=$(echo "$2" | sed 's/[^a-zA-Z0-9]/_/g')
317   printf "%s" "next_line_${peer}_${script}"
318 }
319
320 # Waits for `peer`'s script `script` to finish `count` number of times.
321 # usage: wait_script { foo | bar | baz } { tinc-up | host-up | ... } [count=1]
322 wait_script() {
323   peer=$1
324   script=$2
325   count=$3
326
327   if [ -z "$count" ] || [ "$count" -lt 1 ]; then
328     count=1
329   fi
330
331   # Find out the location of the log and how many lines we should skip
332   # (because we've already seen them in previous invocations of wait_script
333   # for current $peer and $script).
334   line_var=$(next_line_var "$peer" "$script")
335
336   # eval is the only solution supported by POSIX shells.
337   # https://github.com/koalaman/shellcheck/wiki/SC3053
338   #   1. $line_var expands into 'next_line_foo_hosts_bar_up'
339   #   2. the name is substituted and the command becomes 'echo "$next_line_foo_hosts_bar_up"'
340   #   3. the command is evaluated and the line number is assigned to $line
341   line=$(eval "echo \"\$$line_var\"")
342
343   # This is the file that we monitor for script execution records.
344   script_log=$(script_runs_log "$peer")
345
346   # Starting from $line, read until $count matches are found.
347   # Print the number of the last matching line and exit.
348   # GNU tail 2.82 and newer terminates by itself when the pipe breaks.
349   # To support other tails we do an explicit `kill`.
350   # FIFO is useful here because otherwise it's difficult to determine
351   # which tail process should be killed. We could stick them in a process
352   # group by enabling job control, but this results in weird behavior when
353   # running tests in parallel on some interactive shells
354   # (e.g. when /bin/sh is symlinked to dash).
355   fifo=$(mktemp)
356   rm -f "$fifo"
357   mkfifo "$fifo"
358
359   # This weird thing is required to support old versions of ksh on NetBSD 8.2 and the like.
360   (tail -n +"$line" -f "$script_log" >"$fifo") &
361
362   new_line=$(
363     sh -c "
364       grep -n -m $count '^$script,' <'$fifo'
365     " | awk -F: 'END { print $1 }'
366   )
367
368   # Try to stop the background tail, ignoring possible failure (some tails
369   # detect EOF, some don't, so it may have already exited), but do wait on
370   # it (which is required at least by old ksh).
371   kill $! || true
372   wait || true
373   rm -f "$fifo"
374
375   # Remember the next line number for future reference. We'll use it if
376   # wait_script is called again with same $peer and $script.
377   read -r "${line_var?}" <<EOF
378 $((line + new_line))
379 EOF
380 }
381
382 # Cleanup after running each script.
383 cleanup() {
384   (
385     set +ex
386
387     if command -v cleanup_hook 2>/dev/null; then
388       echo >&2 "Cleanup hook found, calling..."
389       cleanup_hook
390     fi
391
392     stop_all_tincs
393   ) || true
394 }
395
396 # If we're on a CI server, the test requires superuser privileges to run, and we're not
397 # currently a superuser, try running the test as one and fail if it doesn't work (the
398 # system must be configured to provide passwordless sudo for our user).
399 require_root() {
400   if is_root; then
401     return
402   fi
403   if is_ci; then
404     echo "root is required for test $SCRIPTNAME, but we're a regular user; elevating privileges..."
405     if ! command -v sudo 2>/dev/null; then
406       bail "please install sudo and configure passwordless auth for user $USER"
407     fi
408     if ! sudo --preserve-env --non-interactive true; then
409       bail "sudo is not allowed or requires a password for user $USER"
410     fi
411     exec sudo --preserve-env "$@"
412   else
413     # Avoid these kinds of surprises outside CI. Just skip the test.
414     echo "root is required for test $SCRIPTNAME, but we're a regular user; skipping"
415     exit "$EXIT_SKIP_TEST"
416   fi
417 }
418
419 # Generate path to current shell which can be used from Windows applications.
420 if is_windows; then
421   MINGW_SHELL=$(normalize_path "$SHELL")
422 fi
423
424 # This was called from a tincd script. Skip executing commands with side effects.
425 [ -n "$NAME" ] && return
426
427 echo [STEP] Check for leftover tinc daemons and test directories
428
429 # Cleanup leftovers from previous runs.
430 stop_all_tincs
431
432 rm -rf "$DIR_FOO" "$DIR_BAR" "$DIR_BAZ"
433
434 # Register cleanup function so we don't have to call it everywhere
435 # (and failed scripts do not leave stray tincd running).
436 trap cleanup EXIT INT TERM