Fix building tinc and running tests on Solaris
[tinc] / test / integration / 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 if [ "$(uname -s)" = SunOS ]; then
49   gnu=/usr/gnu/bin
50   grep="$gnu/grep"
51
52   grep() { $gnu/grep "$@"; }
53   tail() { $gnu/tail "$@"; }
54
55   if ! tail /dev/null || ! echo '' | grep ''; then
56     echo >&2 'Sorry, native Solaris tools are not supported. Please install GNU Coreutils.'
57     exit $EXIT_SKIP_TEST
58   fi
59 else
60   grep='grep'
61 fi
62
63 # Alias gtimeout to timeout if it exists.
64 if type gtimeout >/dev/null; then
65   timeout() { gtimeout "$@"; }
66 fi
67
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.
70 sed_cmd() {
71   sed -i.orig "$@"
72 }
73
74 # Are the shell tools provided by busybox?
75 is_busybox() {
76   timeout --help 2>&1 | grep -q -i busybox
77 }
78
79 # busybox timeout returns 128 + signal number (which is TERM by default)
80 if is_busybox; then
81   # shellcheck disable=SC2034
82   EXIT_TIMEOUT=$((128 + 15))
83 else
84   # shellcheck disable=SC2034
85   EXIT_TIMEOUT=124
86 fi
87
88 # Is this msys2?
89 is_windows() {
90   test "$(uname -o)" = Msys
91 }
92
93 # Are we running on a CI server?
94 is_ci() {
95   test "$CI"
96 }
97
98 # Dump error message and exit with an error.
99 bail() {
100   echo >&2 "$@"
101   exit 1
102 }
103
104 # Remove carriage returns to normalize strings on Windows for easier comparisons.
105 rm_cr() {
106   tr -d '\r'
107 }
108
109 if is_windows; then
110   normalize_path() { cygpath --mixed -- "$@"; }
111 else
112   normalize_path() { echo "$@"; }
113 fi
114
115 # Executes whatever is passed to it, checking that the resulting exit code is non-zero.
116 must_fail() {
117   if "$@"; then
118     bail "expected a non-zero exit code"
119   fi
120 }
121
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
126 expect_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 "expected message '$message'"
136   fi
137 }
138
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.
141 fail_on_msg() {
142   message=$1
143   shift
144
145   if ! output=$("$@" 2>&1); then
146     bail 'expected 0 exit code'
147   fi
148
149   if echo "$output" | grep -q -i "$message"; then
150     bail "unexpected message '$message'"
151   fi
152 }
153
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() {
157   message=$1
158   shift
159
160   if output=$("$@" 2>&1); then
161     bail "expected a non-zero exit code"
162   fi
163
164   if ! echo "$output" | grep -i -q "$message"; then
165     bail "expected message '$message'"
166   fi
167 }
168
169 # Is the legacy protocol enabled?
170 with_legacy() {
171   tincd foo --version | grep -q legacy_protocol
172 }
173
174 # Are we running with EUID 0?
175 is_root() {
176   test "$(id -u)" = 0
177 }
178
179 # Executes whatever is passed to it, checking that the resulting exit code is equal to the first argument.
180 expect_code() {
181   expected=$1
182   shift
183
184   code=0
185   "$@" || code=$?
186
187   if [ $code != "$expected" ]; then
188     bail "wrong exit code $code, expected $expected"
189   fi
190 }
191
192 # wc -l on mac prints whitespace before the actual number.
193 # This is simplest cross-platform alternative without that behavior.
194 count_lines() {
195   awk 'END{ print NR }'
196 }
197
198 # Calls compiled tinc, passing any supplied arguments.
199 # Usage: tinc { foo | bar | baz } --arg1 val1 "$args"
200 tinc() {
201   peer=$1
202   shift
203
204   case "$peer" in
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 $*]]" ;;
209   esac
210 }
211
212 # Calls compiled tincd, passing any supplied arguments.
213 # Usage: tincd { foo | bar | baz } --arg1 val1 "$args"
214 tincd() {
215   peer=$1
216   shift
217
218   case "$peer" in
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 $*]]" ;;
223   esac
224 }
225
226 # Start the specified tinc daemon.
227 # usage: start_tinc { foo | bar | baz }
228 start_tinc() {
229   peer=$1
230   shift
231
232   case "$peer" in
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" ;;
237   esac
238 }
239
240 # Stop all tinc clients.
241 stop_all_tincs() {
242   (
243     # In case these pid files are mangled.
244     set +e
245     [ -f "$DIR_FOO/pid" ] && tinc foo stop
246     [ -f "$DIR_BAR/pid" ] && tinc bar stop
247     [ -f "$DIR_BAZ/pid" ] && tinc baz stop
248     true
249   )
250 }
251
252 # Checks that the number of reachable nodes matches what is expected.
253 # usage: require_nodes node_name expected_number
254 require_nodes() {
255   echo >&2 "Check that we're able to reach tincd"
256   test "$(tinc "$1" pid | count_lines)" = 1
257
258   echo >&2 "Check the number of reachable nodes for $1 (expecting $2)"
259   actual="$(tinc "$1" dump reachable nodes | count_lines)"
260
261   if [ "$actual" != "$2" ]; then
262     echo >&2 "tinc $1: expected $2 reachable nodes, got $actual"
263     exit 1
264   fi
265 }
266
267 peer_directory() {
268   peer=$1
269   case "$peer" in
270   foo) echo "$DIR_FOO" ;;
271   bar) echo "$DIR_BAR" ;;
272   baz) echo "$DIR_BAZ" ;;
273   *) bail "invalid peer $peer" ;;
274   esac
275 }
276
277 # This is an append-only log of all scripts executed by all peers.
278 script_runs_log() {
279   echo "$(peer_directory "$1")/script-runs.log"
280 }
281
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'
284 create_script() {
285   peer=$1
286   script=$2
287   shift 2
288
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")
293
294   # We must reassign it here in case the script is recreated.
295   # shellcheck disable=SC2229
296   read -r "$line_var" <<EOF
297 1
298 EOF
299
300   # Full path to the script.
301   script_path=$(peer_directory "$peer")/$script
302
303   # Full path to the script execution log (one for each peer).
304   script_log=$(script_runs_log "$peer")
305   printf '' >"$script_log"
306
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
310 #!/bin/sh
311 (
312   cd "$PWD" || exit 1
313   SCRIPTNAME="$SCRIPTNAME" . "$TESTLIB_PATH"
314   $@
315   echo "$script,\$$,$TINC_SCRIPT_VARS" >>"$script_log"
316 ) >/dev/null 2>&1 || kill -TERM $$
317 EOF
318
319   chmod u+x "$script_path"
320
321   if is_windows; then
322     echo "@$MINGW_SHELL '$script_path'" >"$script_path.cmd"
323   fi
324 }
325
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
329 next_line_var() {
330   peer=$1
331   script=$(echo "$2" | sed 's/[^a-zA-Z0-9]/_/g')
332   printf "%s" "next_line_${peer}_${script}"
333 }
334
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]
337 wait_script() {
338   peer=$1
339   script=$2
340   count=$3
341
342   if [ -z "$count" ] || [ "$count" -lt 1 ]; then
343     count=1
344   fi
345
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")
350
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\"")
357
358   # This is the file that we monitor for script execution records.
359   script_log=$(script_runs_log "$peer")
360
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).
370   fifo=$(mktemp)
371   rm -f "$fifo"
372   mkfifo "$fifo"
373
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") &
376
377   new_line=$(
378     sh -c "
379       $grep -n -m $count '^$script,' <'$fifo'
380     " | awk -F: 'END { print $1 }'
381   )
382
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).
386   kill $! || true
387   wait || true
388   rm -f "$fifo"
389
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
393 $((line + new_line))
394 EOF
395 }
396
397 # Cleanup after running each script.
398 cleanup() {
399   (
400     set +ex
401
402     if command -v cleanup_hook 2>/dev/null; then
403       echo >&2 "Cleanup hook found, calling..."
404       cleanup_hook
405     fi
406
407     stop_all_tincs
408   ) || true
409 }
410
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).
414 require_root() {
415   if is_root; then
416     return
417   fi
418   if is_ci; then
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"
422     fi
423     if ! sudo --preserve-env --non-interactive true; then
424       bail "sudo is not allowed or requires a password for user $USER"
425     fi
426     exec sudo --preserve-env "$@"
427   else
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"
431   fi
432 }
433
434 # Generate path to current shell which can be used from Windows applications.
435 if is_windows; then
436   MINGW_SHELL=$(normalize_path "$SHELL")
437 fi
438
439 # This was called from a tincd script. Skip executing commands with side effects.
440 [ -n "$NAME" ] && return
441
442 echo [STEP] Check for leftover tinc daemons and test directories
443
444 # Cleanup leftovers from previous runs.
445 stop_all_tincs
446
447 rm -rf "$DIR_FOO" "$DIR_BAR" "$DIR_BAZ"
448
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