]> Sergey Matveev's repositories - bfs.git/blob - tests/tests.sh
tests: Fix make_deep() on FreeBSD
[bfs.git] / tests / tests.sh
1 #!/usr/bin/env bash
2
3 # Copyright © Tavian Barnes <tavianator@tavianator.com>
4 # SPDX-License-Identifier: 0BSD
5
6 set -euP
7 umask 022
8
9 export LC_ALL=C
10 export TZ=UTC0
11
12 SAN_OPTIONS="halt_on_error=1:log_to_syslog=0"
13 export ASAN_OPTIONS="$SAN_OPTIONS"
14 export LSAN_OPTIONS="$SAN_OPTIONS"
15 export MSAN_OPTIONS="$SAN_OPTIONS"
16 export TSAN_OPTIONS="$SAN_OPTIONS"
17 export UBSAN_OPTIONS="$SAN_OPTIONS"
18
19 export LS_COLORS=""
20 unset BFS_COLORS
21
22 if [ -t 1 ]; then
23     BLD=$'\033[01m'
24     RED=$'\033[01;31m'
25     GRN=$'\033[01;32m'
26     YLW=$'\033[01;33m'
27     BLU=$'\033[01;34m'
28     MAG=$'\033[01;35m'
29     CYN=$'\033[01;36m'
30     RST=$'\033[0m'
31 else
32     BLD=
33     RED=
34     GRN=
35     YLW=
36     BLU=
37     MAG=
38     CYN=
39     RST=
40 fi
41
42 UNAME=$(uname)
43
44 if [ "$UNAME" = Darwin ]; then
45     # ASan on macOS likes to report
46     #
47     #     malloc: nano zone abandoned due to inability to preallocate reserved vm space.
48     #
49     # to syslog, which as a side effect opens a socket which might take the
50     # place of one of the standard streams if the process is launched with it
51     # closed.  This environment variable avoids the message.
52     export MallocNanoZone=0
53 fi
54
55 if command -v capsh &>/dev/null; then
56     if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
57         if [ -n "${BFS_TRIED_DROP:-}" ]; then
58             cat >&2 <<EOF
59 ${RED}error:${RST} Failed to drop capabilities.
60 EOF
61
62             exit 1
63         fi
64
65         cat >&2 <<EOF
66 ${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended.  Dropping ${BLD}cap_dac_override${RST} and
67 ${BLD}cap_dac_read_search${RST}.
68
69 EOF
70
71         BFS_TRIED_DROP=y exec capsh \
72             --drop=cap_dac_override,cap_dac_read_search \
73             --caps=cap_dac_override,cap_dac_read_search-eip \
74             -- "$0" "$@"
75     fi
76 elif [ "$EUID" -eq 0 ]; then
77     UNLESS=
78     if [ "$UNAME" = "Linux" ]; then
79         UNLESS=" unless ${GRN}capsh${RST} is installed"
80     fi
81
82     cat >&2 <<EOF
83 ${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
84 will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
85 EOF
86     exit 1
87 fi
88
89 function usage() {
90     local pad=$(printf "%*s" ${#0} "")
91     cat <<EOF
92 Usage: ${GRN}$0${RST} [${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]] [${BLU}--stop${RST}]
93        $pad [${BLU}--noclean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}]
94        $pad [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLD}TEST${RST} [${BLD}TEST${RST} ...]]
95
96   ${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}
97       Set the path to the bfs executable to test (default: ${MAG}./bin/bfs${RST})
98
99   ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]
100       Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST}
101
102   ${BLU}--stop${RST}
103       Stop when the first error occurs
104
105   ${BLU}--noclean${RST}
106       Keep the test directories around after the run
107
108   ${BLU}--update${RST}
109       Update the expected outputs for the test cases
110
111   ${BLU}--verbose${RST}=${BLD}commands${RST}
112       Log the commands that get executed
113   ${BLU}--verbose${RST}=${BLD}errors${RST}
114       Don't redirect standard error
115   ${BLU}--verbose${RST}=${BLD}skipped${RST}
116       Log which tests get skipped
117   ${BLU}--verbose${RST}=${BLD}tests${RST}
118       Log all tests that get run
119   ${BLU}--verbose${RST}
120       Log everything
121
122   ${BLU}--help${RST}
123       This message
124
125   ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
126       Choose which test cases to run (default: ${BLU}--all${RST})
127
128   ${BLD}TEST${RST}
129       Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...)
130 EOF
131 }
132
133 PATTERNS=()
134 SUDO=()
135 STOP=
136 CLEAN=yes
137 UPDATE=
138 VERBOSE_COMMANDS=
139 VERBOSE_ERRORS=
140 VERBOSE_SKIPPED=
141 VERBOSE_TESTS=
142
143 for arg; do
144     case "$arg" in
145         --bfs=*)
146             BFS="${arg#*=}"
147             ;;
148         --posix)
149             PATTERNS+=("posix/*")
150             ;;
151         --bsd)
152             PATTERNS+=("posix/*" "common/*" "bsd/*")
153             ;;
154         --gnu)
155             PATTERNS+=("posix/*" "common/*" "gnu/*")
156             ;;
157         --all)
158             PATTERNS+=("*")
159             ;;
160         --sudo)
161             SUDO=(sudo)
162             ;;
163         --sudo=*)
164             read -a SUDO <<<"${arg#*=}"
165             ;;
166         --stop)
167             STOP=yes
168             ;;
169         --noclean)
170             CLEAN=
171             ;;
172         --update)
173             UPDATE=yes
174             ;;
175         --verbose=commands)
176             VERBOSE_COMMANDS=yes
177             ;;
178         --verbose=errors)
179             VERBOSE_ERRORS=yes
180             ;;
181         --verbose=skipped)
182             VERBOSE_SKIPPED=yes
183             ;;
184         --verbose=tests)
185             VERBOSE_TESTS=yes
186             ;;
187         --verbose)
188             VERBOSE_COMMANDS=yes
189             VERBOSE_ERRORS=yes
190             VERBOSE_SKIPPED=yes
191             VERBOSE_TESTS=yes
192             ;;
193         --help)
194             usage
195             exit 0
196             ;;
197         -*)
198             printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
199             usage >&2
200             exit 1
201             ;;
202         *)
203             PATTERNS+=("$arg")
204             ;;
205     esac
206 done
207
208 function _realpath() {
209     (
210         cd "$(dirname -- "$1")"
211         echo "$PWD/$(basename -- "$1")"
212     )
213 }
214
215 TESTS=$(_realpath "$(dirname -- "${BASH_SOURCE[0]}")")
216
217 if [ "${BUILDDIR-}" ]; then
218     BIN=$(_realpath "$BUILDDIR/bin")
219 else
220     BIN=$(_realpath "$TESTS/../bin")
221 fi
222 MKSOCK="$BIN/tests/mksock"
223 XTOUCH="$BIN/tests/xtouch"
224
225 # Try to resolve the path to $BFS before we cd, while also supporting
226 # --bfs="./bin/bfs -S ids"
227 read -a BFS <<<"${BFS:-$BIN/bfs}"
228 BFS[0]=$(_realpath "$(command -v "${BFS[0]}")")
229
230 # The temporary directory that will hold our test data
231 TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)
232 chown "$(id -u):$(id -g)" "$TMP"
233
234 cd "$TESTS"
235
236 if (( ${#PATTERNS[@]} == 0 )); then
237     PATTERNS=("*")
238 fi
239
240 TEST_CASES=()
241 for TEST in {posix,common,bsd,gnu,bfs}/*.sh; do
242     TEST="${TEST%.sh}"
243     for PATTERN in "${PATTERNS[@]}"; do
244         if [[ $TEST == $PATTERN ]]; then
245             TEST_CASES+=("$TEST")
246             break
247         fi
248     done
249 done
250
251 if (( ${#TEST_CASES[@]} == 0 )); then
252     printf "${RED}error:${RST} No tests matched" >&2
253     printf " ${BLD}%s${RST}" "${PATTERNS[@]}" >&2
254     printf ".\n\n" >&2
255     usage >&2
256     exit 1
257 fi
258
259 function bfs_sudo() {
260     if ((${#SUDO[@]})); then
261         "${SUDO[@]}" "$@"
262     else
263         return 1
264     fi
265 }
266
267 function clean_scratch() {
268     if [ -e "$TMP/scratch" ]; then
269         # Try to unmount anything left behind
270         if ((${#SUDO[@]})) && command -v mountpoint &>/dev/null; then
271             for path in "$TMP"/scratch/*; do
272                 if mountpoint -q "$path"; then
273                     sudo umount "$path"
274                 fi
275             done
276         fi
277
278         # Reset any modified permissions
279         chmod -R +rX "$TMP/scratch"
280
281         rm -rf "$TMP/scratch"
282     fi
283
284     mkdir "$TMP/scratch"
285 }
286
287 # Clean up temporary directories on exit
288 function cleanup() {
289     # Don't force rm to deal with long paths
290     for dir in "$TMP"/deep/*/*; do
291         if [ -d "$dir" ]; then
292             (cd "$dir" && rm -rf *)
293         fi
294     done
295
296     # In case a test left anything weird in scratch/
297     clean_scratch
298
299     rm -rf "$TMP"
300 }
301
302 if [ "$CLEAN" ]; then
303     trap cleanup EXIT
304 else
305     echo "Test files saved to $TMP"
306 fi
307
308 # Creates a simple file+directory structure for tests
309 function make_basic() {
310     "$XTOUCH" -p "$1"/{a,b,c/d,e/f,g/h/,i/}
311     "$XTOUCH" -p "$1"/{j/foo,k/foo/bar,l/foo/bar/baz}
312     echo baz >"$1/l/foo/bar/baz"
313 }
314 make_basic "$TMP/basic"
315
316 # Creates a file+directory structure with various permissions for tests
317 function make_perms() {
318     "$XTOUCH" -p -M000 "$1/0"
319     "$XTOUCH" -p -M444 "$1/r"
320     "$XTOUCH" -p -M222 "$1/w"
321     "$XTOUCH" -p -M644 "$1/rw"
322     "$XTOUCH" -p -M555 "$1/rx"
323     "$XTOUCH" -p -M311 "$1/wx"
324     "$XTOUCH" -p -M755 "$1/rwx"
325 }
326 make_perms "$TMP/perms"
327
328 # Creates a file+directory structure with various symbolic and hard links
329 function make_links() {
330     "$XTOUCH" -p "$1/file"
331     ln -s file "$1/symlink"
332     ln "$1/file" "$1/hardlink"
333     ln -s nowhere "$1/broken"
334     ln -s symlink/file "$1/notdir"
335     "$XTOUCH" -p "$1/deeply/nested"/{dir/,file}
336     ln -s file "$1/deeply/nested/link"
337     ln -s nowhere "$1/deeply/nested/broken"
338     ln -s deeply/nested "$1/skip"
339 }
340 make_links "$TMP/links"
341
342 # Creates a file+directory structure with symbolic link loops
343 function make_loops() {
344     "$XTOUCH" -p "$1/file"
345     ln -s file "$1/symlink"
346     ln -s nowhere "$1/broken"
347     ln -s symlink/file "$1/notdir"
348     ln -s loop "$1/loop"
349     mkdir -p "$1/deeply/nested/dir"
350     ln -s ../../deeply "$1/deeply/nested/loop"
351     ln -s deeply/nested/loop/nested "$1/skip"
352 }
353 make_loops "$TMP/loops"
354
355 # Creates a file+directory structure with varying timestamps
356 function make_times() {
357     "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a"
358     "$XTOUCH" -p -t "1991-12-14 00:01" "$1/b"
359     "$XTOUCH" -p -t "1991-12-14 00:02" "$1/c"
360     ln -s a "$1/l"
361     "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l"
362     "$XTOUCH" -p -t "1991-12-14 00:04" "$1"
363 }
364 make_times "$TMP/times"
365
366 # Creates a file+directory structure with various weird file/directory names
367 function make_weirdnames() {
368     "$XTOUCH" -p "$1/-/a"
369     "$XTOUCH" -p "$1/(/b"
370     "$XTOUCH" -p "$1/(-/c"
371     "$XTOUCH" -p "$1/!/d"
372     "$XTOUCH" -p "$1/!-/e"
373     "$XTOUCH" -p "$1/,/f"
374     "$XTOUCH" -p "$1/)/g"
375     "$XTOUCH" -p "$1/.../h"
376     "$XTOUCH" -p "$1/\\/i"
377     "$XTOUCH" -p "$1/ /j"
378     "$XTOUCH" -p "$1/[/k"
379 }
380 make_weirdnames "$TMP/weirdnames"
381
382 # Creates a very deep directory structure for testing PATH_MAX handling
383 function make_deep() {
384     mkdir -p "$1"
385
386     # $name will be 255 characters, aka _XOPEN_NAME_MAX
387     local name="0123456789ABCDEF"
388     name="${name}${name}${name}${name}"
389     name="${name}${name}${name}${name}"
390     name="${name:0:255}"
391
392     for i in {0..9} A B C D E F; do
393         "$XTOUCH" -p "$1/$i/$name"
394
395         (
396             cd "$1/$i"
397
398             # 8 * 512 == 4096 >= PATH_MAX
399             for _ in {1..8}; do
400                 mv "$name" ..
401                 mkdir -p "$name/$name"
402                 mv "../$name" "$name/$name/"
403             done
404         )
405     done
406 }
407 make_deep "$TMP/deep"
408
409 # Creates a directory structure with many different types, and therefore colors
410 function make_rainbow() {
411     "$XTOUCH" -p "$1/file.txt"
412     "$XTOUCH" -p "$1/file.dat"
413     "$XTOUCH" -p "$1/lower".{gz,tar,tar.gz}
414     "$XTOUCH" -p "$1/upper".{GZ,TAR,TAR.GZ}
415     "$XTOUCH" -p "$1/lu.tar.GZ" "$1/ul.TAR.gz"
416     ln -s file.txt "$1/link.txt"
417     "$XTOUCH" -p "$1/mh1"
418     ln "$1/mh1" "$1/mh2"
419     mkfifo "$1/pipe"
420     # TODO: block
421     ln -s /dev/null "$1/chardev_link"
422     ln -s nowhere "$1/broken"
423     "$MKSOCK" "$1/socket"
424     "$XTOUCH" -p "$1"/s{u,g,ug}id
425     chmod u+s "$1"/su{,g}id
426     chmod g+s "$1"/s{u,}gid
427     mkdir "$1/ow" "$1"/sticky{,_ow}
428     chmod o+w "$1"/*ow
429     chmod +t "$1"/sticky*
430     "$XTOUCH" -p "$1"/exec.sh
431     chmod +x "$1"/exec.sh
432     "$XTOUCH" -p "$1/"$'\e[1m/\e[0m'
433 }
434 make_rainbow "$TMP/rainbow"
435
436 # Close stdin so bfs doesn't think we're interactive
437 exec </dev/null
438
439 if [ "$VERBOSE_COMMANDS" ]; then
440     # dup stdout for verbose logging even when redirected
441     exec 3>&1
442 fi
443
444 function bfs_verbose() {
445     if [ "$VERBOSE_COMMANDS" ]; then
446         if [ -t 3 ]; then
447             printf "${GRN}%q${RST} " "${BFS[@]}" >&3
448
449             local expr_started=
450             for arg; do
451                 if [[ $arg == -[A-Z]* ]]; then
452                     printf "${CYN}%q${RST} " "$arg" >&3
453                 elif [[ $arg == [\(!] || $arg == -[ao] || $arg == -and || $arg == -or || $arg == -not ]]; then
454                     expr_started=yes
455                     printf "${RED}%q${RST} " "$arg" >&3
456                 elif [[ $expr_started && $arg == [\),] ]]; then
457                     printf "${RED}%q${RST} " "$arg" >&3
458                 elif [[ $arg == -?* ]]; then
459                     expr_started=yes
460                     printf "${BLU}%q${RST} " "$arg" >&3
461                 elif [ "$expr_started" ]; then
462                     printf "${BLD}%q${RST} " "$arg" >&3
463                 else
464                     printf "${MAG}%q${RST} " "$arg" >&3
465                 fi
466             done
467         else
468             printf '%q ' "${BFS[@]}" "$@" >&3
469         fi
470         printf '\n' >&3
471     fi
472 }
473
474 function invoke_bfs() {
475     bfs_verbose "$@"
476     "${BFS[@]}" "$@"
477     local status="$?"
478
479     # Allow bfs to fail, but not crash
480     if ((status > 125)); then
481         exit "$status"
482     else
483         return "$status"
484     fi
485 }
486
487 function check_exit() {
488     local expected="$1"
489     local actual="0"
490     shift
491     "$@" || actual="$?"
492     ((actual == expected))
493 }
494
495 # Detect colored diff support
496 if [ -t 2 ] && diff --color=always /dev/null /dev/null 2>/dev/null; then
497     DIFF="diff --color=always"
498 else
499     DIFF="diff"
500 fi
501
502 # Return value when a difference is detected
503 EX_DIFF=20
504 # Return value when a test is skipped
505 EX_SKIP=77
506
507 function sort_output() {
508     sort -o "$OUT" "$OUT"
509 }
510
511 function diff_output() {
512     local GOLD="$TESTS/$TEST.out"
513
514     if [ "$UPDATE" ]; then
515         cp "$OUT" "$GOLD"
516     else
517         $DIFF -u "$GOLD" "$OUT" >&2
518     fi
519 }
520
521 function bfs_diff() (
522     bfs_verbose "$@"
523
524     # Close the dup()'d stdout to make sure we have enough fd's for the process
525     # substitution, even with low ulimit -n
526     exec 3>&-
527
528     "${BFS[@]}" "$@" | sort >"$OUT"
529     local status="${PIPESTATUS[0]}"
530
531     diff_output || exit $EX_DIFF
532     return "$status"
533 )
534
535 function skip() {
536     if [ "$VERBOSE_SKIPPED" ]; then
537         caller | {
538             read -r line file
539             printf "${BOL}${CYN}%s skipped!${RST} (%s)\n" "$TEST" "$(awk "NR == $line" "$file")"
540         }
541     elif [ "$VERBOSE_TESTS" ]; then
542         printf "${BOL}${CYN}%s skipped!${RST}\n" "$TEST"
543     fi
544
545     exit $EX_SKIP
546 }
547
548 function closefrom() {
549     if [ -d /proc/self/fd ]; then
550         local fds=/proc/self/fd
551     else
552         local fds=/dev/fd
553     fi
554
555     for fd in "$fds"/*; do
556         if [ ! -e "$fd" ]; then
557             continue
558         fi
559
560         local fd="${fd##*/}"
561         if [ "$fd" -ge "$1" ]; then
562             eval "exec ${fd}<&-"
563         fi
564     done
565 }
566
567 function inum() {
568     ls -id "$@" | awk '{ print $1 }'
569 }
570
571 function set_acl() {
572     case "$UNAME" in
573         Darwin)
574             chmod +a "$(id -un) allow read,write" "$1"
575             ;;
576         FreeBSD)
577             if [ "$(getconf ACL_NFS4 "$1")" -gt 0 ]; then
578                 setfacl -m "u:$(id -un):rw::allow" "$1"
579             else
580                 setfacl -m "u:$(id -un):rw" "$1"
581             fi
582             ;;
583         *)
584             setfacl -m "u:$(id -un):rw" "$1"
585             ;;
586     esac
587 }
588
589 function make_xattrs() {
590     clean_scratch
591
592     "$XTOUCH" scratch/{normal,xattr,xattr_2}
593     ln -s xattr scratch/link
594     ln -s normal scratch/xattr_link
595
596     case "$UNAME" in
597         Darwin)
598             xattr -w bfs_test true scratch/xattr \
599                 && xattr -w bfs_test_2 true scratch/xattr_2 \
600                 && xattr -s -w bfs_test true scratch/xattr_link
601             ;;
602         FreeBSD)
603             setextattr user bfs_test true scratch/xattr \
604                 && setextattr user bfs_test_2 true scratch/xattr_2 \
605                 && setextattr -h user bfs_test true scratch/xattr_link
606             ;;
607         *)
608             # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
609             # namespace, which is writable by root and readable by others
610             bfs_sudo setfattr -n security.bfs_test scratch/xattr \
611                 && bfs_sudo setfattr -n security.bfs_test_2 scratch/xattr_2 \
612                 && bfs_sudo setfattr -h -n security.bfs_test scratch/xattr_link
613             ;;
614     esac
615 }
616
617 cd "$TMP"
618 set +e
619
620 BOL='\n'
621 EOL='\n'
622
623 function update_eol() {
624     # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead
625     local cols="${COLUMNS-}"
626     if [ -z "$cols" ]; then
627         cols=$(tput cols)
628     fi
629
630     # Put the cursor at the last column, then write a space so the next
631     # character will wrap
632     EOL="\\033[${cols}G "
633 }
634
635 if [ "$VERBOSE_TESTS" ]; then
636     BOL=''
637 elif [ -t 1 ]; then
638     BOL='\r\033[K'
639
640     # Workaround for bash 4: checkwinsize is off by default.  We can turn it on,
641     # but we also have to explicitly trigger a foreground job to finish so that
642     # it will update the window size before we use $COLUMNS
643     shopt -s checkwinsize
644     (:)
645
646     update_eol
647     trap update_eol WINCH
648 fi
649
650 passed=0
651 failed=0
652 skipped=0
653
654 for TEST in "${TEST_CASES[@]}"; do
655     if [[ -t 1 || "$VERBOSE_TESTS" ]]; then
656         printf "${BOL}${YLW}%s${RST}${EOL}" "$TEST"
657     else
658         printf "."
659     fi
660
661     OUT="$TMP/$TEST.out"
662     mkdir -p "${OUT%/*}"
663
664     if [ "$VERBOSE_ERRORS" ]; then
665         (set -e; . "$TESTS/$TEST.sh")
666     else
667         (set -e; . "$TESTS/$TEST.sh") 2>"$TMP/$TEST.err"
668     fi
669     status=$?
670
671     if ((status == 0)); then
672         ((++passed))
673     elif ((status == EX_SKIP)); then
674         ((++skipped))
675     else
676         ((++failed))
677         [ "$VERBOSE_ERRORS" ] || cat "$TMP/$TEST.err" >&2
678         printf "${BOL}${RED}%s failed!${RST}\n" "$TEST"
679         [ "$STOP" ] && break
680     fi
681 done
682
683 printf "${BOL}"
684
685 if ((passed > 0)); then
686     printf "${GRN}tests passed: %d${RST}\n" "$passed"
687 fi
688 if ((skipped > 0)); then
689     printf "${CYN}tests skipped: %s${RST}\n" "$skipped"
690 fi
691 if ((failed > 0)); then
692     printf "${RED}tests failed: %s${RST}\n" "$failed"
693     exit 1
694 fi