3 # Copyright © Tavian Barnes <tavianator@tavianator.com>
4 # SPDX-License-Identifier: 0BSD
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"
44 if [ "$UNAME" = Darwin ]; then
45 # ASan on macOS likes to report
47 # malloc: nano zone abandoned due to inability to preallocate reserved vm space.
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
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
59 ${RED}error:${RST} Failed to drop capabilities.
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}.
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 \
76 elif [ "$EUID" -eq 0 ]; then
78 if [ "$UNAME" = "Linux" ]; then
79 UNLESS=" unless ${GRN}capsh${RST} is installed"
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}.
90 local pad=$(printf "%*s" ${#0} "")
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} ...]]
96 ${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}
97 Set the path to the bfs executable to test (default: ${MAG}./bin/bfs${RST})
99 ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]
100 Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST}
103 Stop when the first error occurs
105 ${BLU}--noclean${RST}
106 Keep the test directories around after the run
109 Update the expected outputs for the test cases
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}
125 ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
126 Choose which test cases to run (default: ${BLU}--all${RST})
129 Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...)
149 PATTERNS+=("posix/*")
152 PATTERNS+=("posix/*" "common/*" "bsd/*")
155 PATTERNS+=("posix/*" "common/*" "gnu/*")
164 read -a SUDO <<<"${arg#*=}"
198 printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
208 function _realpath() {
210 cd "$(dirname -- "$1")"
211 echo "$PWD/$(basename -- "$1")"
215 TESTS=$(_realpath "$(dirname -- "${BASH_SOURCE[0]}")")
217 if [ "${BUILDDIR-}" ]; then
218 BIN=$(_realpath "$BUILDDIR/bin")
220 BIN=$(_realpath "$TESTS/../bin")
222 MKSOCK="$BIN/tests/mksock"
223 XTOUCH="$BIN/tests/xtouch"
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]}")")
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"
236 if (( ${#PATTERNS[@]} == 0 )); then
241 for TEST in {posix,common,bsd,gnu,bfs}/*.sh; do
243 for PATTERN in "${PATTERNS[@]}"; do
244 if [[ $TEST == $PATTERN ]]; then
245 TEST_CASES+=("$TEST")
251 if (( ${#TEST_CASES[@]} == 0 )); then
252 printf "${RED}error:${RST} No tests matched" >&2
253 printf " ${BLD}%s${RST}" "${PATTERNS[@]}" >&2
259 function bfs_sudo() {
260 if ((${#SUDO[@]})); then
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
278 # Reset any modified permissions
279 chmod -R +rX "$TMP/scratch"
281 rm -rf "$TMP/scratch"
287 # Clean up temporary directories on exit
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 *)
296 # In case a test left anything weird in scratch/
302 if [ "$CLEAN" ]; then
305 echo "Test files saved to $TMP"
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"
314 make_basic "$TMP/basic"
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"
326 make_perms "$TMP/perms"
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"
340 make_links "$TMP/links"
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"
349 mkdir -p "$1/deeply/nested/dir"
350 ln -s ../../deeply "$1/deeply/nested/loop"
351 ln -s deeply/nested/loop/nested "$1/skip"
353 make_loops "$TMP/loops"
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"
361 "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l"
362 "$XTOUCH" -p -t "1991-12-14 00:04" "$1"
364 make_times "$TMP/times"
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"
380 make_weirdnames "$TMP/weirdnames"
382 # Creates a very deep directory structure for testing PATH_MAX handling
383 function make_deep() {
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}"
392 for i in {0..9} A B C D E F; do
393 "$XTOUCH" -p "$1/$i/$name"
398 # 8 * 512 == 4096 >= PATH_MAX
401 mkdir -p "$name/$name"
402 mv "../$name" "$name/$name/"
407 make_deep "$TMP/deep"
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"
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}
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'
434 make_rainbow "$TMP/rainbow"
436 # Close stdin so bfs doesn't think we're interactive
439 if [ "$VERBOSE_COMMANDS" ]; then
440 # dup stdout for verbose logging even when redirected
444 function bfs_verbose() {
445 if [ "$VERBOSE_COMMANDS" ]; then
447 printf "${GRN}%q${RST} " "${BFS[@]}" >&3
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
455 printf "${RED}%q${RST} " "$arg" >&3
456 elif [[ $expr_started && $arg == [\),] ]]; then
457 printf "${RED}%q${RST} " "$arg" >&3
458 elif [[ $arg == -?* ]]; then
460 printf "${BLU}%q${RST} " "$arg" >&3
461 elif [ "$expr_started" ]; then
462 printf "${BLD}%q${RST} " "$arg" >&3
464 printf "${MAG}%q${RST} " "$arg" >&3
468 printf '%q ' "${BFS[@]}" "$@" >&3
474 function invoke_bfs() {
479 # Allow bfs to fail, but not crash
480 if ((status > 125)); then
487 function check_exit() {
492 ((actual == expected))
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"
502 # Return value when a difference is detected
504 # Return value when a test is skipped
507 function sort_output() {
508 sort -o "$OUT" "$OUT"
511 function diff_output() {
512 local GOLD="$TESTS/$TEST.out"
514 if [ "$UPDATE" ]; then
517 $DIFF -u "$GOLD" "$OUT" >&2
521 function bfs_diff() (
524 # Close the dup()'d stdout to make sure we have enough fd's for the process
525 # substitution, even with low ulimit -n
528 "${BFS[@]}" "$@" | sort >"$OUT"
529 local status="${PIPESTATUS[0]}"
531 diff_output || exit $EX_DIFF
536 if [ "$VERBOSE_SKIPPED" ]; then
539 printf "${BOL}${CYN}%s skipped!${RST} (%s)\n" "$TEST" "$(awk "NR == $line" "$file")"
541 elif [ "$VERBOSE_TESTS" ]; then
542 printf "${BOL}${CYN}%s skipped!${RST}\n" "$TEST"
548 function closefrom() {
549 if [ -d /proc/self/fd ]; then
550 local fds=/proc/self/fd
555 for fd in "$fds"/*; do
556 if [ ! -e "$fd" ]; then
561 if [ "$fd" -ge "$1" ]; then
568 ls -id "$@" | awk '{ print $1 }'
574 chmod +a "$(id -un) allow read,write" "$1"
577 if [ "$(getconf ACL_NFS4 "$1")" -gt 0 ]; then
578 setfacl -m "u:$(id -un):rw::allow" "$1"
580 setfacl -m "u:$(id -un):rw" "$1"
584 setfacl -m "u:$(id -un):rw" "$1"
589 function make_xattrs() {
592 "$XTOUCH" scratch/{normal,xattr,xattr_2}
593 ln -s xattr scratch/link
594 ln -s normal scratch/xattr_link
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
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
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
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
630 # Put the cursor at the last column, then write a space so the next
631 # character will wrap
632 EOL="\\033[${cols}G "
635 if [ "$VERBOSE_TESTS" ]; then
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
647 trap update_eol WINCH
654 for TEST in "${TEST_CASES[@]}"; do
655 if [[ -t 1 || "$VERBOSE_TESTS" ]]; then
656 printf "${BOL}${YLW}%s${RST}${EOL}" "$TEST"
664 if [ "$VERBOSE_ERRORS" ]; then
665 (set -e; . "$TESTS/$TEST.sh")
667 (set -e; . "$TESTS/$TEST.sh") 2>"$TMP/$TEST.err"
671 if ((status == 0)); then
673 elif ((status == EX_SKIP)); then
677 [ "$VERBOSE_ERRORS" ] || cat "$TMP/$TEST.err" >&2
678 printf "${BOL}${RED}%s failed!${RST}\n" "$TEST"
685 if ((passed > 0)); then
686 printf "${GRN}tests passed: %d${RST}\n" "$passed"
688 if ((skipped > 0)); then
689 printf "${CYN}tests skipped: %s${RST}\n" "$skipped"
691 if ((failed > 0)); then
692 printf "${RED}tests failed: %s${RST}\n" "$failed"