summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorTavian Barnes <tavianator@tavianator.com>2023-10-19 16:37:47 -0400
committerTavian Barnes <tavianator@tavianator.com>2023-10-19 16:37:47 -0400
commit785a3f2d777627f39bed44f4ae7a0180d5184109 (patch)
tree2bee8cb669ab91982a7d6d1e8ada9958a2369ffd /tests
parentd484cba3424dcfca4851ba867d8877e3a9381a0e (diff)
downloadbfs-785a3f2d777627f39bed44f4ae7a0180d5184109.tar.xz
tests: Refactor implementation into separate files
Diffstat (limited to 'tests')
-rw-r--r--tests/bfs/deep_strict.sh2
-rw-r--r--tests/color.sh43
-rw-r--r--tests/common/execdir_ulimit.sh1
-rw-r--r--tests/getopts.sh158
-rw-r--r--tests/gnu/printf_u_g_ulimit.sh1
-rwxr-xr-xtests/ls-color.sh6
-rw-r--r--tests/posix/deep.sh2
-rw-r--r--tests/posix/nogroup_ulimit.sh1
-rw-r--r--tests/posix/nouser_ulimit.sh1
-rw-r--r--tests/run.sh316
-rw-r--r--tests/stddirs.sh185
-rwxr-xr-xtests/tests.sh824
-rw-r--r--tests/util.sh189
13 files changed, 906 insertions, 823 deletions
diff --git a/tests/bfs/deep_strict.sh b/tests/bfs/deep_strict.sh
index e057310..50c8f05 100644
--- a/tests/bfs/deep_strict.sh
+++ b/tests/bfs/deep_strict.sh
@@ -1,5 +1,3 @@
-closefrom 4
-
# Not even enough fds to keep the root open
ulimit -n 7
bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/color.sh b/tests/color.sh
new file mode 100644
index 0000000..0d6ef68
--- /dev/null
+++ b/tests/color.sh
@@ -0,0 +1,43 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Colored output
+
+# Common escape sequences
+BLD=$'\e[01m'
+RED=$'\e[01;31m'
+GRN=$'\e[01;32m'
+YLW=$'\e[01;33m'
+BLU=$'\e[01;34m'
+MAG=$'\e[01;35m'
+CYN=$'\e[01;36m'
+RST=$'\e[0m'
+
+# Check if we should color output to the given fd
+color_fd() {
+ [ -z "${NO_COLOR:-}" ] && [ -t "$1" ]
+}
+
+# Cache the color status for std{out,err}
+color_fd 1 && COLOR_STDOUT=1 || COLOR_STDOUT=0
+color_fd 2 && COLOR_STDERR=1 || COLOR_STDERR=0
+
+# Save these in case the tests unset PATH
+CAT=$(command -v cat)
+SED=$(command -v sed)
+
+# Filter out escape sequences if necessary
+color() {
+ if color_fd 1; then
+ "$CAT"
+ else
+ "$SED" $'s/\e\\[[^m]*m//g'
+ fi
+}
+
+# printf with auto-detected color support
+cprintf() {
+ printf "$@" | color
+}
diff --git a/tests/common/execdir_ulimit.sh b/tests/common/execdir_ulimit.sh
index 8bd9edd..f7fc467 100644
--- a/tests/common/execdir_ulimit.sh
+++ b/tests/common/execdir_ulimit.sh
@@ -2,6 +2,5 @@ clean_scratch
mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
mkdir -p scratch/a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C
-closefrom 4
ulimit -n 13
bfs_diff scratch -execdir echo {} \;
diff --git a/tests/getopts.sh b/tests/getopts.sh
new file mode 100644
index 0000000..6616a4a
--- /dev/null
+++ b/tests/getopts.sh
@@ -0,0 +1,158 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Argument parsing
+
+# Print usage information
+usage() {
+ local pad=$(printf "%*s" ${#0} "")
+ color <<EOF
+Usage: ${GRN}$0${RST} [${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]] [${BLU}--stop${RST}]
+ $pad [${BLU}--no-clean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}]
+ $pad [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLD}TEST${RST} [${BLD}TEST${RST} ...]]
+
+ ${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}
+ Set the path to the bfs executable to test (default: ${MAG}./bin/bfs${RST})
+
+ ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]
+ Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST}
+
+ ${BLU}--stop${RST}
+ Stop when the first error occurs
+
+ ${BLU}--no-clean${RST}
+ Keep the test directories around after the run
+
+ ${BLU}--update${RST}
+ Update the expected outputs for the test cases
+
+ ${BLU}--verbose${RST}=${BLD}commands${RST}
+ Log the commands that get executed
+ ${BLU}--verbose${RST}=${BLD}errors${RST}
+ Don't redirect standard error
+ ${BLU}--verbose${RST}=${BLD}skipped${RST}
+ Log which tests get skipped
+ ${BLU}--verbose${RST}=${BLD}tests${RST}
+ Log all tests that get run
+ ${BLU}--verbose${RST}
+ Log everything
+
+ ${BLU}--help${RST}
+ This message
+
+ ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
+ Choose which test cases to run (default: ${BLU}--all${RST})
+
+ ${BLD}TEST${RST}
+ Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...)
+EOF
+}
+
+# Parse the command line
+parse_args() {
+ PATTERNS=()
+ SUDO=()
+ STOP=0
+ CLEAN=1
+ UPDATE=0
+ VERBOSE_COMMANDS=0
+ VERBOSE_ERRORS=0
+ VERBOSE_SKIPPED=0
+ VERBOSE_TESTS=0
+
+ for arg; do
+ case "$arg" in
+ --bfs=*)
+ BFS="${arg#*=}"
+ ;;
+ --posix)
+ PATTERNS+=("posix/*")
+ ;;
+ --bsd)
+ PATTERNS+=("posix/*" "common/*" "bsd/*")
+ ;;
+ --gnu)
+ PATTERNS+=("posix/*" "common/*" "gnu/*")
+ ;;
+ --all)
+ PATTERNS+=("*")
+ ;;
+ --sudo)
+ SUDO=(sudo)
+ ;;
+ --sudo=*)
+ read -a SUDO <<<"${arg#*=}"
+ ;;
+ --stop)
+ STOP=1
+ ;;
+ --no-clean|--noclean)
+ CLEAN=0
+ ;;
+ --update)
+ UPDATE=1
+ ;;
+ --verbose=commands)
+ VERBOSE_COMMANDS=1
+ ;;
+ --verbose=errors)
+ VERBOSE_ERRORS=1
+ ;;
+ --verbose=skipped)
+ VERBOSE_SKIPPED=1
+ ;;
+ --verbose=tests)
+ VERBOSE_TESTS=1
+ ;;
+ --verbose)
+ VERBOSE_COMMANDS=1
+ VERBOSE_ERRORS=1
+ VERBOSE_SKIPPED=1
+ VERBOSE_TESTS=1
+ ;;
+ --help)
+ usage
+ exit 0
+ ;;
+ -*)
+ cprintf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
+ usage >&2
+ exit 1
+ ;;
+ *)
+ PATTERNS+=("$arg")
+ ;;
+ esac
+ done
+
+ # Try to resolve the path to $BFS before we cd, while also supporting
+ # --bfs="./bin/bfs -S ids"
+ read -a BFS <<<"${BFS:-$BIN/bfs}"
+ BFS[0]=$(_realpath "$(command -v "${BFS[0]}")")
+
+ if ((${#PATTERNS[@]} == 0)); then
+ PATTERNS=("*")
+ fi
+
+ TEST_CASES=()
+ ALL_TESTS=($(cd "$TESTS" && quote {posix,common,bsd,gnu,bfs}/*.sh))
+ for TEST in "${ALL_TESTS[@]}"; do
+ TEST="${TEST%.sh}"
+ for PATTERN in "${PATTERNS[@]}"; do
+ if [[ $TEST == $PATTERN ]]; then
+ TEST_CASES+=("$TEST")
+ break
+ fi
+ done
+ done
+
+ if ((${#TEST_CASES[@]} == 0)); then
+ cprintf "${RED}error:${RST} No tests matched" >&2
+ cprintf " ${BLD}%s${RST}" "${PATTERNS[@]}" >&2
+ cprintf ".\n\n" >&2
+ usage >&2
+ exit 1
+ fi
+}
diff --git a/tests/gnu/printf_u_g_ulimit.sh b/tests/gnu/printf_u_g_ulimit.sh
index a84ee29..390ad48 100644
--- a/tests/gnu/printf_u_g_ulimit.sh
+++ b/tests/gnu/printf_u_g_ulimit.sh
@@ -1,3 +1,2 @@
-closefrom 4
ulimit -n 16
[ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ]
diff --git a/tests/ls-color.sh b/tests/ls-color.sh
index 9fdd59c..b9a0402 100755
--- a/tests/ls-color.sh
+++ b/tests/ls-color.sh
@@ -7,7 +7,7 @@
set -e
-function parse_ls_colors() {
+parse_ls_colors() {
for key; do
local -n var="$key"
if [[ "$LS_COLORS" =~ (^|:)$key=(([^:]|\\:)*) ]]; then
@@ -18,7 +18,7 @@ function parse_ls_colors() {
done
}
-function re_escape() {
+re_escape() {
# https://stackoverflow.com/a/29613573/502399
sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$1"
}
@@ -34,7 +34,7 @@ parse_ls_colors rs lc rc ec no
strip="(($(re_escape "$lc$no$rc"))?($(re_escape "$ec")|$(re_escape "$lc$rc")))+"
-function ls_color() {
+ls_color() {
# Strip the leading reset sequence from the ls output
ls -1d --color "$@" | sed -E "s/^$strip([a-z].*)$strip/\4/; s/^$strip//"
}
diff --git a/tests/posix/deep.sh b/tests/posix/deep.sh
index 3d1cd60..431705e 100644
--- a/tests/posix/deep.sh
+++ b/tests/posix/deep.sh
@@ -1,4 +1,2 @@
-closefrom 4
-
ulimit -n 16
bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/posix/nogroup_ulimit.sh b/tests/posix/nogroup_ulimit.sh
index 8f758c4..2186321 100644
--- a/tests/posix/nogroup_ulimit.sh
+++ b/tests/posix/nogroup_ulimit.sh
@@ -1,4 +1,3 @@
-closefrom 4
ulimit -n 16
# -mindepth 18, but POSIX
diff --git a/tests/posix/nouser_ulimit.sh b/tests/posix/nouser_ulimit.sh
index 2777589..be0a65f 100644
--- a/tests/posix/nouser_ulimit.sh
+++ b/tests/posix/nouser_ulimit.sh
@@ -1,4 +1,3 @@
-closefrom 4
ulimit -n 16
# -mindepth 18, but POSIX
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100644
index 0000000..70c9cc2
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,316 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Running test cases
+
+# Beginning/end of line escape sequences
+BOL=$'\n'
+EOL=$'\n'
+
+# Update $EOL for the terminal size
+update_eol() {
+ # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead
+ local cols="${COLUMNS-}"
+ if [ -z "$cols" ]; then
+ cols=$(tput cols)
+ fi
+
+ # Put the cursor at the last column, then write a space so the next
+ # character will wrap
+ EOL=$'\e['"${cols}G "
+}
+
+# ERR trap for tests
+debug_err() {
+ local ret=$? line func file
+ callers | while read -r line func file; do
+ if [ "$func" = source ]; then
+ local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || :
+ debug "$file" $line "${RED}error $ret${RST}" "$cmd" >&4
+ break
+ fi
+ done
+}
+
+# Run a single test
+run_test() (
+ set -eE
+ trap debug_err ERR
+ cd "$TMP"
+ source "$@"
+)
+
+# Run all the tests
+run_tests() {
+ if ((VERBOSE_TESTS)); then
+ BOL=''
+ elif ((COLOR_STDOUT)); then
+ # Carriage return + clear line
+ BOL=$'\r\e[K'
+
+ # Workaround for bash 4: checkwinsize is off by default. We can turn it
+ # on, but we also have to explicitly trigger a foreground job to finish
+ # so that it will update the window size before we use $COLUMNS
+ shopt -s checkwinsize
+ (:)
+
+ update_eol
+ trap update_eol WINCH
+ fi
+
+ passed=0
+ failed=0
+ skipped=0
+
+ if ((COLOR_STDOUT || VERBOSE_TESTS)); then
+ TEST_FMT="${BOL}${YLW}%s${RST}${EOL}"
+ else
+ TEST_FMT="."
+ fi
+
+ # Turn off set -e (but turn it back on in run_test)
+ set +e
+
+ for TEST in "${TEST_CASES[@]}"; do
+ printf "$TEST_FMT" "$TEST"
+
+ OUT="$TMP/$TEST.out"
+ mkdir -p "${OUT%/*}"
+
+ if ((VERBOSE_ERRORS)); then
+ run_test "$TESTS/$TEST.sh"
+ else
+ run_test "$TESTS/$TEST.sh" 2>"$TMP/$TEST.err"
+ fi
+ status=$?
+
+ if ((status == 0)); then
+ ((++passed))
+ elif ((status == EX_SKIP)); then
+ ((++skipped))
+ else
+ ((++failed))
+ ((VERBOSE_ERRORS)) || cat "$TMP/$TEST.err" >&2
+ cprintf "${BOL}${RED}%s failed!${RST}\n" "$TEST"
+ ((STOP)) && break
+ fi
+ done
+
+ printf "${BOL}"
+
+ if ((passed > 0)); then
+ cprintf "${GRN}tests passed: %d${RST}\n" "$passed"
+ fi
+ if ((skipped > 0)); then
+ cprintf "${CYN}tests skipped: %s${RST}\n" "$skipped"
+ fi
+ if ((failed > 0)); then
+ cprintf "${RED}tests failed: %s${RST}\n" "$failed"
+ exit 1
+ fi
+}
+
+## Utilities for the tests themselves
+
+# Return value when a test is skipped
+EX_SKIP=77
+
+# Skip the current test
+skip() {
+ if ((VERBOSE_SKIPPED)); then
+ caller | {
+ read -r line file
+ printf "${BOL}"
+ debug "$file" $line "${CYN}$TEST skipped!${RST}" "$(awk "NR == $line" "$file")" >&3
+ }
+ elif ((VERBOSE_TESTS)); then
+ cprintf "${BOL}${CYN}%s skipped!${RST}\n" "$TEST"
+ fi
+
+ exit $EX_SKIP
+}
+
+# Run a command and check its exit status
+check_exit() {
+ local expected="$1"
+ local actual="0"
+ shift
+ "$@" || actual="$?"
+ ((actual == expected))
+}
+
+# Run a command with sudo
+bfs_sudo() {
+ if ((${#SUDO[@]})); then
+ "${SUDO[@]}" "$@"
+ else
+ return 1
+ fi
+}
+
+# Get the inode number of a file
+inum() {
+ ls -id "$@" | awk '{ print $1 }'
+}
+
+# Set an ACL on a file
+set_acl() {
+ case "$UNAME" in
+ Darwin)
+ chmod +a "$(id -un) allow read,write" "$1"
+ ;;
+ FreeBSD)
+ if (($(getconf ACL_NFS4 "$1") > 0)); then
+ setfacl -m "u:$(id -un):rw::allow" "$1"
+ else
+ setfacl -m "u:$(id -un):rw" "$1"
+ fi
+ ;;
+ *)
+ setfacl -m "u:$(id -un):rw" "$1"
+ ;;
+ esac
+}
+
+# Print a bfs invocation for --verbose=commands
+bfs_verbose() (
+ if ((!VERBOSE_COMMANDS)); then
+ return
+ fi
+
+ # Free up an fd for the pipe
+ exec 4>&-
+
+ {
+ printf "${GRN}%q${RST} " "${BFS[@]}"
+
+ local expr_started=
+ for arg; do
+ if [[ $arg == -[A-Z]* ]]; then
+ printf "${CYN}%q${RST} " "$arg"
+ elif [[ $arg == [\(!] || $arg == -[ao] || $arg == -and || $arg == -or || $arg == -not ]]; then
+ expr_started=yes
+ printf "${RED}%q${RST} " "$arg"
+ elif [[ $expr_started && $arg == [\),] ]]; then
+ printf "${RED}%q${RST} " "$arg"
+ elif [[ $arg == -?* ]]; then
+ expr_started=yes
+ printf "${BLU}%q${RST} " "$arg"
+ elif [ "$expr_started" ]; then
+ printf "${BLD}%q${RST} " "$arg"
+ else
+ printf "${MAG}%q${RST} " "$arg"
+ fi
+ done
+
+ printf '\n'
+ } | color >&3
+)
+
+# Run the bfs we're testing
+invoke_bfs() {
+ bfs_verbose "$@"
+
+ local ret=0
+ # Close the logging fds
+ "${BFS[@]}" "$@" 3>&- 4>&- || ret=$?
+
+ # Allow bfs to fail, but not crash
+ if ((ret > 125)); then
+ exit "$ret"
+ else
+ return "$ret"
+ fi
+}
+
+if command -v unbuffer &>/dev/null; then
+ UNBUFFER=unbuffer
+elif command -v expect_unbuffer &>/dev/null; then
+ UNBUFFER=expect_unbuffer
+fi
+
+# Run bfs with a pseudo-terminal attached
+bfs_pty() {
+ test -n "${UNBUFFER:-}" || skip
+
+ bfs_verbose "$@"
+
+ local ret=0
+ "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@"' bash "${BFS[@]}" "$@" || ret=$?
+
+ if ((ret > 125)); then
+ exit "$ret"
+ else
+ return "$ret"
+ fi
+}
+
+# Create a directory tree with xattrs in scratch
+make_xattrs() {
+ clean_scratch
+
+ "$XTOUCH" scratch/{normal,xattr,xattr_2}
+ ln -s xattr scratch/link
+ ln -s normal scratch/xattr_link
+
+ case "$UNAME" in
+ Darwin)
+ xattr -w bfs_test true scratch/xattr \
+ && xattr -w bfs_test_2 true scratch/xattr_2 \
+ && xattr -s -w bfs_test true scratch/xattr_link
+ ;;
+ FreeBSD)
+ setextattr user bfs_test true scratch/xattr \
+ && setextattr user bfs_test_2 true scratch/xattr_2 \
+ && setextattr -h user bfs_test true scratch/xattr_link
+ ;;
+ *)
+ # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
+ # namespace, which is writable by root and readable by others
+ bfs_sudo setfattr -n security.bfs_test scratch/xattr \
+ && bfs_sudo setfattr -n security.bfs_test_2 scratch/xattr_2 \
+ && bfs_sudo setfattr -h -n security.bfs_test scratch/xattr_link
+ ;;
+ esac
+}
+
+## Snapshot testing
+
+# Return value when a difference is detected
+EX_DIFF=20
+
+# Detect colored diff support
+if ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null 2>/dev/null; then
+ DIFF="diff --color=always"
+else
+ DIFF="diff"
+fi
+
+# Sort the output file
+sort_output() {
+ sort -o "$OUT" "$OUT"
+}
+
+# Diff against the expected output
+diff_output() {
+ local GOLD="$TESTS/$TEST.out"
+
+ if ((UPDATE)); then
+ cp "$OUT" "$GOLD"
+ else
+ $DIFF -u "$GOLD" "$OUT" >&2
+ fi
+}
+
+# Run bfs, and diff it against the expected output
+bfs_diff() {
+ local ret=0
+ invoke_bfs "$@" >"$OUT" || ret=$?
+
+ sort_output
+ diff_output || exit $EX_DIFF
+
+ return $ret
+}
diff --git a/tests/stddirs.sh b/tests/stddirs.sh
new file mode 100644
index 0000000..e7f7246
--- /dev/null
+++ b/tests/stddirs.sh
@@ -0,0 +1,185 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Standard directory trees for tests
+
+# Creates a simple file+directory structure for tests
+make_basic() {
+ "$XTOUCH" -p "$1"/{a,b,c/d,e/f,g/h/,i/}
+ "$XTOUCH" -p "$1"/{j/foo,k/foo/bar,l/foo/bar/baz}
+ echo baz >"$1/l/foo/bar/baz"
+}
+
+# Creates a file+directory structure with various permissions for tests
+make_perms() {
+ "$XTOUCH" -p -M000 "$1/0"
+ "$XTOUCH" -p -M444 "$1/r"
+ "$XTOUCH" -p -M222 "$1/w"
+ "$XTOUCH" -p -M644 "$1/rw"
+ "$XTOUCH" -p -M555 "$1/rx"
+ "$XTOUCH" -p -M311 "$1/wx"
+ "$XTOUCH" -p -M755 "$1/rwx"
+}
+
+# Creates a file+directory structure with various symbolic and hard links
+make_links() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln "$1/file" "$1/hardlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ "$XTOUCH" -p "$1/deeply/nested"/{dir/,file}
+ ln -s file "$1/deeply/nested/link"
+ ln -s nowhere "$1/deeply/nested/broken"
+ ln -s deeply/nested "$1/skip"
+}
+
+# Creates a file+directory structure with symbolic link loops
+make_loops() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ ln -s loop "$1/loop"
+ mkdir -p "$1/deeply/nested/dir"
+ ln -s ../../deeply "$1/deeply/nested/loop"
+ ln -s deeply/nested/loop/nested "$1/skip"
+}
+
+# Creates a file+directory structure with varying timestamps
+make_times() {
+ "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a"
+ "$XTOUCH" -p -t "1991-12-14 00:01" "$1/b"
+ "$XTOUCH" -p -t "1991-12-14 00:02" "$1/c"
+ ln -s a "$1/l"
+ "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l"
+ "$XTOUCH" -p -t "1991-12-14 00:04" "$1"
+}
+
+# Creates a file+directory structure with various weird file/directory names
+make_weirdnames() {
+ "$XTOUCH" -p "$1/-/a"
+ "$XTOUCH" -p "$1/(/b"
+ "$XTOUCH" -p "$1/(-/c"
+ "$XTOUCH" -p "$1/!/d"
+ "$XTOUCH" -p "$1/!-/e"
+ "$XTOUCH" -p "$1/,/f"
+ "$XTOUCH" -p "$1/)/g"
+ "$XTOUCH" -p "$1/.../h"
+ "$XTOUCH" -p "$1/\\/i"
+ "$XTOUCH" -p "$1/ /j"
+ "$XTOUCH" -p "$1/[/k"
+}
+
+# Creates a very deep directory structure for testing PATH_MAX handling
+make_deep() {
+ mkdir -p "$1"
+
+ # $name will be 255 characters, aka _XOPEN_NAME_MAX
+ local name="0123456789ABCDEF"
+ name="${name}${name}${name}${name}"
+ name="${name}${name}${name}${name}"
+ name="${name:0:255}"
+
+ for i in {0..9} A B C D E F; do
+ "$XTOUCH" -p "$1/$i/$name"
+
+ (
+ cd "$1/$i"
+
+ # 8 * 512 == 4096 >= PATH_MAX
+ for _ in {1..8}; do
+ mv "$name" ..
+ mkdir -p "$name/$name"
+ mv "../$name" "$name/$name/"
+ done
+ )
+ done
+}
+
+# Creates a directory structure with many different types, and therefore colors
+make_rainbow() {
+ "$XTOUCH" -p "$1/file.txt"
+ "$XTOUCH" -p "$1/file.dat"
+ "$XTOUCH" -p "$1/lower".{gz,tar,tar.gz}
+ "$XTOUCH" -p "$1/upper".{GZ,TAR,TAR.GZ}
+ "$XTOUCH" -p "$1/lu.tar.GZ" "$1/ul.TAR.gz"
+ ln -s file.txt "$1/link.txt"
+ "$XTOUCH" -p "$1/mh1"
+ ln "$1/mh1" "$1/mh2"
+ mkfifo "$1/pipe"
+ # TODO: block
+ ln -s /dev/null "$1/chardev_link"
+ ln -s nowhere "$1/broken"
+ "$MKSOCK" "$1/socket"
+ "$XTOUCH" -p "$1"/s{u,g,ug}id
+ chmod u+s "$1"/su{,g}id
+ chmod g+s "$1"/s{u,}gid
+ mkdir "$1/ow" "$1"/sticky{,_ow}
+ chmod o+w "$1"/*ow
+ chmod +t "$1"/sticky*
+ "$XTOUCH" -p "$1"/exec.sh
+ chmod +x "$1"/exec.sh
+ "$XTOUCH" -p "$1/"$'\e[1m/\e[0m'
+}
+
+# Create all standard directory trees
+make_stddirs() {
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)
+
+ if ((CLEAN)); then
+ defer clean_stddirs
+ else
+ printf "Test files saved to ${BLD}%s${RST}\n" "$TMP"
+ fi
+
+ chown "$(id -u):$(id -g)" "$TMP"
+
+ make_basic "$TMP/basic"
+ make_perms "$TMP/perms"
+ make_links "$TMP/links"
+ make_loops "$TMP/loops"
+ make_times "$TMP/times"
+ make_weirdnames "$TMP/weirdnames"
+ make_deep "$TMP/deep"
+ make_rainbow "$TMP/rainbow"
+ mkdir "$TMP/scratch"
+}
+
+# Clean whatever was left in the scratch directory
+clean_scratch() {
+ if [ -e "$TMP/scratch" ]; then
+ # Try to unmount anything left behind
+ if ((${#SUDO[@]})) && command -v mountpoint &>/dev/null; then
+ for path in "$TMP/scratch"/*; do
+ if mountpoint -q "$path"; then
+ sudo umount "$path"
+ fi
+ done
+ fi
+
+ # Reset any modified permissions
+ chmod -R +rX "$TMP/scratch"
+
+ rm -rf "$TMP/scratch"
+ fi
+
+ mkdir "$TMP/scratch"
+}
+
+# Clean up temporary directories on exit
+clean_stddirs() {
+ # Don't force rm to deal with long paths
+ for dir in "$TMP"/deep/*/*; do
+ if [ -d "$dir" ]; then
+ (cd "$dir" && rm -rf *)
+ fi
+ done
+
+ # In case a test left anything weird in scratch/
+ clean_scratch
+
+ rm -rf "$TMP"
+}
diff --git a/tests/tests.sh b/tests/tests.sh
index dcef28e..3890243 100755
--- a/tests/tests.sh
+++ b/tests/tests.sh
@@ -6,815 +6,15 @@
set -euP
umask 022
-export LC_ALL=C
-export TZ=UTC0
-
-SAN_OPTIONS="halt_on_error=1:log_to_syslog=0"
-export ASAN_OPTIONS="$SAN_OPTIONS"
-export LSAN_OPTIONS="$SAN_OPTIONS"
-export MSAN_OPTIONS="$SAN_OPTIONS"
-export TSAN_OPTIONS="$SAN_OPTIONS"
-export UBSAN_OPTIONS="$SAN_OPTIONS"
-
-export LS_COLORS=""
-unset BFS_COLORS
-
-BLD=$'\e[01m'
-RED=$'\e[01;31m'
-GRN=$'\e[01;32m'
-YLW=$'\e[01;33m'
-BLU=$'\e[01;34m'
-MAG=$'\e[01;35m'
-CYN=$'\e[01;36m'
-RST=$'\e[0m'
-
-function color_fd() {
- [ -z "${NO_COLOR:-}" ] && [ -t "$1" ]
-}
-
-color_fd 1 && COLOR_STDOUT=1 || COLOR_STDOUT=0
-color_fd 2 && COLOR_STDERR=1 || COLOR_STDERR=0
-
-# Filter out escape sequences if necessary
-function color() {
- if color_fd 1; then
- cat
- else
- sed $'s/\e\\[[^m]*m//g'
- fi
-}
-
-# printf with auto-detected color support
-function cprintf() {
- printf "$@" | color
-}
-
-UNAME=$(uname)
-
-if [ "$UNAME" = Darwin ]; then
- # ASan on macOS likes to report
- #
- # malloc: nano zone abandoned due to inability to preallocate reserved vm space.
- #
- # to syslog, which as a side effect opens a socket which might take the
- # place of one of the standard streams if the process is launched with it
- # closed. This environment variable avoids the message.
- export MallocNanoZone=0
-fi
-
-if command -v capsh &>/dev/null; then
- if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
- if [ -n "${BFS_TRIED_DROP:-}" ]; then
- color >&2 <<EOF
-${RED}error:${RST} Failed to drop capabilities.
-EOF
-
- exit 1
- fi
-
- color >&2 <<EOF
-${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and
-${BLD}cap_dac_read_search${RST}.
-
-EOF
-
- BFS_TRIED_DROP=y exec capsh \
- --drop=cap_dac_override,cap_dac_read_search \
- --caps=cap_dac_override,cap_dac_read_search-eip \
- -- "$0" "$@"
- fi
-elif ((EUID == 0)); then
- UNLESS=
- if [ "$UNAME" = "Linux" ]; then
- UNLESS=" unless ${GRN}capsh${RST} is installed"
- fi
-
- color >&2 <<EOF
-${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
-will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
-EOF
- exit 1
-fi
-
-function usage() {
- local pad=$(printf "%*s" ${#0} "")
- color <<EOF
-Usage: ${GRN}$0${RST} [${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]] [${BLU}--stop${RST}]
- $pad [${BLU}--no-clean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}]
- $pad [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLD}TEST${RST} [${BLD}TEST${RST} ...]]
-
- ${BLU}--bfs${RST}=${MAG}path/to/bfs${RST}
- Set the path to the bfs executable to test (default: ${MAG}./bin/bfs${RST})
-
- ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]
- Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST}
-
- ${BLU}--stop${RST}
- Stop when the first error occurs
-
- ${BLU}--no-clean${RST}
- Keep the test directories around after the run
-
- ${BLU}--update${RST}
- Update the expected outputs for the test cases
-
- ${BLU}--verbose${RST}=${BLD}commands${RST}
- Log the commands that get executed
- ${BLU}--verbose${RST}=${BLD}errors${RST}
- Don't redirect standard error
- ${BLU}--verbose${RST}=${BLD}skipped${RST}
- Log which tests get skipped
- ${BLU}--verbose${RST}=${BLD}tests${RST}
- Log all tests that get run
- ${BLU}--verbose${RST}
- Log everything
-
- ${BLU}--help${RST}
- This message
-
- ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
- Choose which test cases to run (default: ${BLU}--all${RST})
-
- ${BLD}TEST${RST}
- Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...)
-EOF
-}
-
-PATTERNS=()
-SUDO=()
-STOP=0
-CLEAN=1
-UPDATE=0
-VERBOSE_COMMANDS=0
-VERBOSE_ERRORS=0
-VERBOSE_SKIPPED=0
-VERBOSE_TESTS=0
-
-for arg; do
- case "$arg" in
- --bfs=*)
- BFS="${arg#*=}"
- ;;
- --posix)
- PATTERNS+=("posix/*")
- ;;
- --bsd)
- PATTERNS+=("posix/*" "common/*" "bsd/*")
- ;;
- --gnu)
- PATTERNS+=("posix/*" "common/*" "gnu/*")
- ;;
- --all)
- PATTERNS+=("*")
- ;;
- --sudo)
- SUDO=(sudo)
- ;;
- --sudo=*)
- read -a SUDO <<<"${arg#*=}"
- ;;
- --stop)
- STOP=1
- ;;
- --no-clean|--noclean)
- CLEAN=0
- ;;
- --update)
- UPDATE=1
- ;;
- --verbose=commands)
- VERBOSE_COMMANDS=1
- ;;
- --verbose=errors)
- VERBOSE_ERRORS=1
- ;;
- --verbose=skipped)
- VERBOSE_SKIPPED=1
- ;;
- --verbose=tests)
- VERBOSE_TESTS=1
- ;;
- --verbose)
- VERBOSE_COMMANDS=1
- VERBOSE_ERRORS=1
- VERBOSE_SKIPPED=1
- VERBOSE_TESTS=1
- ;;
- --help)
- usage
- exit 0
- ;;
- -*)
- cprintf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
- usage >&2
- exit 1
- ;;
- *)
- PATTERNS+=("$arg")
- ;;
- esac
-done
-
-function _realpath() {
- (
- cd "$(dirname -- "$1")"
- echo "$PWD/$(basename -- "$1")"
- )
-}
-
-TESTS=$(_realpath "$(dirname -- "${BASH_SOURCE[0]}")")
-
-if [ "${BUILDDIR-}" ]; then
- BIN=$(_realpath "$BUILDDIR/bin")
-else
- BIN=$(_realpath "$TESTS/../bin")
-fi
-MKSOCK="$BIN/tests/mksock"
-XTOUCH="$BIN/tests/xtouch"
-
-# Try to resolve the path to $BFS before we cd, while also supporting
-# --bfs="./bin/bfs -S ids"
-read -a BFS <<<"${BFS:-$BIN/bfs}"
-BFS[0]=$(_realpath "$(command -v "${BFS[0]}")")
-
-# The temporary directory that will hold our test data
-TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)
-chown "$(id -u):$(id -g)" "$TMP"
-
-cd "$TESTS"
-
-if ((${#PATTERNS[@]} == 0)); then
- PATTERNS=("*")
-fi
-
-TEST_CASES=()
-for TEST in {posix,common,bsd,gnu,bfs}/*.sh; do
- TEST="${TEST%.sh}"
- for PATTERN in "${PATTERNS[@]}"; do
- if [[ $TEST == $PATTERN ]]; then
- TEST_CASES+=("$TEST")
- break
- fi
- done
-done
-
-if ((${#TEST_CASES[@]} == 0)); then
- cprintf "${RED}error:${RST} No tests matched" >&2
- cprintf " ${BLD}%s${RST}" "${PATTERNS[@]}" >&2
- cprintf ".\n\n" >&2
- usage >&2
- exit 1
-fi
-
-function quote() {
- printf '%q' "$1"
- shift
- if (($# > 0)); then
- printf ' %q' "$@"
- fi
-}
-
-# Run a command when this (sub)shell exits
-function defer() {
- trap -- KILL
- if ! trap -p EXIT | grep -q pop_defers; then
- DEFER_CMDS=()
- DEFER_LINES=()
- DEFER_FILES=()
- trap pop_defers EXIT
- fi
-
- DEFER_CMDS+=("$(quote "$@")")
-
- local line file
- read -r line file < <(caller)
- DEFER_LINES+=("$line")
- DEFER_FILES+=("$file")
-}
-
-function report_err() {
- local file="${1/#*\/tests\//tests\/}"
- set -- "$file" "${@:2}"
-
- if ((COLOR_STDERR)); then
- printf "${BLD}%s:%d:${RST} ${RED}error %d:${RST}\n %s\n" "$@" >&2
- else
- printf "%s:%d: error %d:\n %s\n" "$@" >&2
- fi
-}
-
-function pop_defer() {
- local cmd="${DEFER_CMDS[-1]}"
- local file="${DEFER_FILES[-1]}"
- local line="${DEFER_LINES[-1]}"
- unset "DEFER_CMDS[-1]"
- unset "DEFER_FILES[-1]"
- unset "DEFER_LINES[-1]"
-
- local ret=0
- eval "$cmd" || ret=$?
-
- if ((ret != 0)); then
- report_err "$file" $line $ret "defer $cmd"
- fi
-
- return $ret
-}
-
-function pop_defers() {
- local ret=0
-
- while ((${#DEFER_CMDS[@]} > 0)); do
- pop_defer || ret=$?
- done
-
- return $ret
-}
-
-function bfs_sudo() {
- if ((${#SUDO[@]})); then
- "${SUDO[@]}" "$@"
- else
- return 1
- fi
-}
-
-function clean_scratch() {
- if [ -e "$TMP/scratch" ]; then
- # Try to unmount anything left behind
- if ((${#SUDO[@]})) && command -v mountpoint &>/dev/null; then
- for path in "$TMP"/scratch/*; do
- if mountpoint -q "$path"; then
- sudo umount "$path"
- fi
- done
- fi
-
- # Reset any modified permissions
- chmod -R +rX "$TMP/scratch"
-
- rm -rf "$TMP/scratch"
- fi
-
- mkdir "$TMP/scratch"
-}
-
-# Clean up temporary directories on exit
-function cleanup() {
- # Don't force rm to deal with long paths
- for dir in "$TMP"/deep/*/*; do
- if [ -d "$dir" ]; then
- (cd "$dir" && rm -rf *)
- fi
- done
-
- # In case a test left anything weird in scratch/
- clean_scratch
-
- rm -rf "$TMP"
-}
-
-if ((CLEAN)); then
- defer cleanup
-else
- echo "Test files saved to $TMP"
-fi
-
-# Creates a simple file+directory structure for tests
-function make_basic() {
- "$XTOUCH" -p "$1"/{a,b,c/d,e/f,g/h/,i/}
- "$XTOUCH" -p "$1"/{j/foo,k/foo/bar,l/foo/bar/baz}
- echo baz >"$1/l/foo/bar/baz"
-}
-make_basic "$TMP/basic"
-
-# Creates a file+directory structure with various permissions for tests
-function make_perms() {
- "$XTOUCH" -p -M000 "$1/0"
- "$XTOUCH" -p -M444 "$1/r"
- "$XTOUCH" -p -M222 "$1/w"
- "$XTOUCH" -p -M644 "$1/rw"
- "$XTOUCH" -p -M555 "$1/rx"
- "$XTOUCH" -p -M311 "$1/wx"
- "$XTOUCH" -p -M755 "$1/rwx"
-}
-make_perms "$TMP/perms"
-
-# Creates a file+directory structure with various symbolic and hard links
-function make_links() {
- "$XTOUCH" -p "$1/file"
- ln -s file "$1/symlink"
- ln "$1/file" "$1/hardlink"
- ln -s nowhere "$1/broken"
- ln -s symlink/file "$1/notdir"
- "$XTOUCH" -p "$1/deeply/nested"/{dir/,file}
- ln -s file "$1/deeply/nested/link"
- ln -s nowhere "$1/deeply/nested/broken"
- ln -s deeply/nested "$1/skip"
-}
-make_links "$TMP/links"
-
-# Creates a file+directory structure with symbolic link loops
-function make_loops() {
- "$XTOUCH" -p "$1/file"
- ln -s file "$1/symlink"
- ln -s nowhere "$1/broken"
- ln -s symlink/file "$1/notdir"
- ln -s loop "$1/loop"
- mkdir -p "$1/deeply/nested/dir"
- ln -s ../../deeply "$1/deeply/nested/loop"
- ln -s deeply/nested/loop/nested "$1/skip"
-}
-make_loops "$TMP/loops"
-
-# Creates a file+directory structure with varying timestamps
-function make_times() {
- "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a"
- "$XTOUCH" -p -t "1991-12-14 00:01" "$1/b"
- "$XTOUCH" -p -t "1991-12-14 00:02" "$1/c"
- ln -s a "$1/l"
- "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l"
- "$XTOUCH" -p -t "1991-12-14 00:04" "$1"
-}
-make_times "$TMP/times"
-
-# Creates a file+directory structure with various weird file/directory names
-function make_weirdnames() {
- "$XTOUCH" -p "$1/-/a"
- "$XTOUCH" -p "$1/(/b"
- "$XTOUCH" -p "$1/(-/c"
- "$XTOUCH" -p "$1/!/d"
- "$XTOUCH" -p "$1/!-/e"
- "$XTOUCH" -p "$1/,/f"
- "$XTOUCH" -p "$1/)/g"
- "$XTOUCH" -p "$1/.../h"
- "$XTOUCH" -p "$1/\\/i"
- "$XTOUCH" -p "$1/ /j"
- "$XTOUCH" -p "$1/[/k"
-}
-make_weirdnames "$TMP/weirdnames"
-
-# Creates a very deep directory structure for testing PATH_MAX handling
-function make_deep() {
- mkdir -p "$1"
-
- # $name will be 255 characters, aka _XOPEN_NAME_MAX
- local name="0123456789ABCDEF"
- name="${name}${name}${name}${name}"
- name="${name}${name}${name}${name}"
- name="${name:0:255}"
-
- for i in {0..9} A B C D E F; do
- "$XTOUCH" -p "$1/$i/$name"
-
- (
- cd "$1/$i"
-
- # 8 * 512 == 4096 >= PATH_MAX
- for _ in {1..8}; do
- mv "$name" ..
- mkdir -p "$name/$name"
- mv "../$name" "$name/$name/"
- done
- )
- done
-}
-make_deep "$TMP/deep"
-
-# Creates a directory structure with many different types, and therefore colors
-function make_rainbow() {
- "$XTOUCH" -p "$1/file.txt"
- "$XTOUCH" -p "$1/file.dat"
- "$XTOUCH" -p "$1/lower".{gz,tar,tar.gz}
- "$XTOUCH" -p "$1/upper".{GZ,TAR,TAR.GZ}
- "$XTOUCH" -p "$1/lu.tar.GZ" "$1/ul.TAR.gz"
- ln -s file.txt "$1/link.txt"
- "$XTOUCH" -p "$1/mh1"
- ln "$1/mh1" "$1/mh2"
- mkfifo "$1/pipe"
- # TODO: block
- ln -s /dev/null "$1/chardev_link"
- ln -s nowhere "$1/broken"
- "$MKSOCK" "$1/socket"
- "$XTOUCH" -p "$1"/s{u,g,ug}id
- chmod u+s "$1"/su{,g}id
- chmod g+s "$1"/s{u,}gid
- mkdir "$1/ow" "$1"/sticky{,_ow}
- chmod o+w "$1"/*ow
- chmod +t "$1"/sticky*
- "$XTOUCH" -p "$1"/exec.sh
- chmod +x "$1"/exec.sh
- "$XTOUCH" -p "$1/"$'\e[1m/\e[0m'
-}
-make_rainbow "$TMP/rainbow"
-
-mkdir "$TMP/scratch"
-
-# Close stdin so bfs doesn't think we're interactive
-exec </dev/null
-
-if ((VERBOSE_COMMANDS)); then
- # dup stdout for verbose logging even when redirected
- exec 3>&1
-fi
-
-function bfs_verbose() {
- if ((!VERBOSE_COMMANDS)); then
- return
- fi
-
- {
- printf "${GRN}%q${RST} " "${BFS[@]}"
-
- local expr_started=
- for arg; do
- if [[ $arg == -[A-Z]* ]]; then
- printf "${CYN}%q${RST} " "$arg"
- elif [[ $arg == [\(!] || $arg == -[ao] || $arg == -and || $arg == -or || $arg == -not ]]; then
- expr_started=yes
- printf "${RED}%q${RST} " "$arg"
- elif [[ $expr_started && $arg == [\),] ]]; then
- printf "${RED}%q${RST} " "$arg"
- elif [[ $arg == -?* ]]; then
- expr_started=yes
- printf "${BLU}%q${RST} " "$arg"
- elif [ "$expr_started" ]; then
- printf "${BLD}%q${RST} " "$arg"
- else
- printf "${MAG}%q${RST} " "$arg"
- fi
- done
-
- printf '\n'
- } | color >&3
-}
-
-function invoke_bfs() {
- bfs_verbose "$@"
-
- local ret=0
- "${BFS[@]}" "$@" || ret=$?
-
- # Allow bfs to fail, but not crash
- if ((ret > 125)); then
- exit "$ret"
- else
- return "$ret"
- fi
-}
-
-if command -v unbuffer &>/dev/null; then
- UNBUFFER=unbuffer
-elif command -v expect_unbuffer &>/dev/null; then
- UNBUFFER=expect_unbuffer
-fi
-
-function bfs_pty() {
- test -n "${UNBUFFER:-}" || skip
-
- bfs_verbose "$@"
-
- local ret=0
- "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@"' bash "${BFS[@]}" "$@" || ret=$?
-
- if ((ret > 125)); then
- exit "$ret"
- else
- return "$ret"
- fi
-}
-
-function check_exit() {
- local expected="$1"
- local actual="0"
- shift
- "$@" || actual="$?"
- ((actual == expected))
-}
-
-# Detect colored diff support
-if ((COLOR_STDERR)) && diff --color=always /dev/null /dev/null 2>/dev/null; then
- DIFF="diff --color=always"
-else
- DIFF="diff"
-fi
-
-# Return value when a difference is detected
-EX_DIFF=20
-# Return value when a test is skipped
-EX_SKIP=77
-
-function sort_output() {
- sort -o "$OUT" "$OUT"
-}
-
-function diff_output() {
- local GOLD="$TESTS/$TEST.out"
-
- if ((UPDATE)); then
- cp "$OUT" "$GOLD"
- else
- $DIFF -u "$GOLD" "$OUT" >&2
- fi
-}
-
-function bfs_diff() (
- bfs_verbose "$@"
-
- # Close the dup()'d stdout to make sure we have enough fd's for the process
- # substitution, even with low ulimit -n
- exec 3>&-
-
- "${BFS[@]}" "$@" | sort >"$OUT"
- local status="${PIPESTATUS[0]}"
-
- diff_output || exit $EX_DIFF
- return "$status"
-)
-
-function skip() {
- if ((VERBOSE_SKIPPED)); then
- caller | {
- read -r line file
- cprintf "${BOL}${CYN}%s skipped!${RST} (%s)\n" "$TEST" "$(awk "NR == $line" "$file")"
- }
- elif ((VERBOSE_TESTS)); then
- cprintf "${BOL}${CYN}%s skipped!${RST}\n" "$TEST"
- fi
-
- exit $EX_SKIP
-}
-
-function closefrom() {
- if [ -d /proc/self/fd ]; then
- local fds=/proc/self/fd
- else
- local fds=/dev/fd
- fi
-
- for fd in "$fds"/*; do
- if [ ! -e "$fd" ]; then
- continue
- fi
-
- local fd="${fd##*/}"
- if ((fd >= $1)); then
- eval "exec ${fd}<&-"
- fi
- done
-}
-
-function inum() {
- ls -id "$@" | awk '{ print $1 }'
-}
-
-function set_acl() {
- case "$UNAME" in
- Darwin)
- chmod +a "$(id -un) allow read,write" "$1"
- ;;
- FreeBSD)
- if (($(getconf ACL_NFS4 "$1") > 0)); then
- setfacl -m "u:$(id -un):rw::allow" "$1"
- else
- setfacl -m "u:$(id -un):rw" "$1"
- fi
- ;;
- *)
- setfacl -m "u:$(id -un):rw" "$1"
- ;;
- esac
-}
-
-function make_xattrs() {
- clean_scratch
-
- "$XTOUCH" scratch/{normal,xattr,xattr_2}
- ln -s xattr scratch/link
- ln -s normal scratch/xattr_link
-
- case "$UNAME" in
- Darwin)
- xattr -w bfs_test true scratch/xattr \
- && xattr -w bfs_test_2 true scratch/xattr_2 \
- && xattr -s -w bfs_test true scratch/xattr_link
- ;;
- FreeBSD)
- setextattr user bfs_test true scratch/xattr \
- && setextattr user bfs_test_2 true scratch/xattr_2 \
- && setextattr -h user bfs_test true scratch/xattr_link
- ;;
- *)
- # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
- # namespace, which is writable by root and readable by others
- bfs_sudo setfattr -n security.bfs_test scratch/xattr \
- && bfs_sudo setfattr -n security.bfs_test_2 scratch/xattr_2 \
- && bfs_sudo setfattr -h -n security.bfs_test scratch/xattr_link
- ;;
- esac
-}
-
-cd "$TMP"
-set +e
-
-BOL='\n'
-EOL='\n'
-
-function update_eol() {
- # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead
- local cols="${COLUMNS-}"
- if [ -z "$cols" ]; then
- cols=$(tput cols)
- fi
-
- # Put the cursor at the last column, then write a space so the next
- # character will wrap
- EOL="\\033[${cols}G "
-}
-
-if ((VERBOSE_TESTS)); then
- BOL=''
-elif ((COLOR_STDOUT)); then
- BOL='\r\033[K'
-
- # Workaround for bash 4: checkwinsize is off by default. We can turn it on,
- # but we also have to explicitly trigger a foreground job to finish so that
- # it will update the window size before we use $COLUMNS
- shopt -s checkwinsize
- (:)
-
- update_eol
- trap update_eol WINCH
-fi
-
-function callers() {
- local frame=0
- while caller $frame; do
- ((++frame))
- done
-}
-
-function debug_err() {
- local ret=$? line func file
- callers | while read -r line func file; do
- if [ "$func" = source ]; then
- local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || :
- report_err "$file" $line $ret "$cmd"
- break
- fi
- done
-}
-
-function run_test() (
- set -eE
- trap debug_err ERR
- source "$@"
-)
-
-passed=0
-failed=0
-skipped=0
-
-if ((COLOR_STDOUT || VERBOSE_TESTS)); then
- TEST_FMT="${BOL}${YLW}%s${RST}${EOL}"
-else
- TEST_FMT="."
-fi
-
-for TEST in "${TEST_CASES[@]}"; do
- printf "$TEST_FMT" "$TEST"
-
- OUT="$TMP/$TEST.out"
- mkdir -p "${OUT%/*}"
-
- if ((VERBOSE_ERRORS)); then
- run_test "$TESTS/$TEST.sh"
- else
- run_test "$TESTS/$TEST.sh" 2>"$TMP/$TEST.err"
- fi
- status=$?
-
- if ((status == 0)); then
- ((++passed))
- elif ((status == EX_SKIP)); then
- ((++skipped))
- else
- ((++failed))
- ((VERBOSE_ERRORS)) || cat "$TMP/$TEST.err" >&2
- cprintf "${BOL}${RED}%s failed!${RST}\n" "$TEST"
- ((STOP)) && break
- fi
-done
-
-printf "${BOL}"
-
-if ((passed > 0)); then
- cprintf "${GRN}tests passed: %d${RST}\n" "$passed"
-fi
-if ((skipped > 0)); then
- cprintf "${CYN}tests skipped: %s${RST}\n" "$skipped"
-fi
-if ((failed > 0)); then
- cprintf "${RED}tests failed: %s${RST}\n" "$failed"
- exit 1
-fi
+TESTS="$(dirname -- "${BASH_SOURCE[0]}")"
+. "$TESTS/util.sh"
+. "$TESTS/color.sh"
+. "$TESTS/stddirs.sh"
+. "$TESTS/getopts.sh"
+. "$TESTS/run.sh"
+
+stdenv
+drop_root "$@"
+parse_args "$@"
+make_stddirs
+run_tests
diff --git a/tests/util.sh b/tests/util.sh
new file mode 100644
index 0000000..5131522
--- /dev/null
+++ b/tests/util.sh
@@ -0,0 +1,189 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Utility functions
+
+# Portable realpath(1)
+_realpath() (
+ cd "$(dirname -- "$1")"
+ echo "$PWD/$(basename -- "$1")"
+)
+
+# Globals
+TESTS=$(_realpath "$TESTS")
+if [ "${BUILDDIR-}" ]; then
+ BIN=$(_realpath "$BUILDDIR/bin")
+else
+ BIN=$(_realpath "$TESTS/../bin")
+fi
+MKSOCK="$BIN/tests/mksock"
+XTOUCH="$BIN/tests/xtouch"
+UNAME=$(uname)
+
+# Standardize the environment
+stdenv() {
+ export LC_ALL=C
+ export TZ=UTC0
+
+ local SAN_OPTIONS="halt_on_error=1:log_to_syslog=0"
+ export ASAN_OPTIONS="$SAN_OPTIONS"
+ export LSAN_OPTIONS="$SAN_OPTIONS"
+ export MSAN_OPTIONS="$SAN_OPTIONS"
+ export TSAN_OPTIONS="$SAN_OPTIONS"
+ export UBSAN_OPTIONS="$SAN_OPTIONS"
+
+ export LS_COLORS=""
+ unset BFS_COLORS
+
+ if [ "$UNAME" = Darwin ]; then
+ # ASan on macOS likes to report
+ #
+ # malloc: nano zone abandoned due to inability to preallocate reserved vm space.
+ #
+ # to syslog, which as a side effect opens a socket which might take the
+ # place of one of the standard streams if the process is launched with
+ # it closed. This environment variable avoids the message.
+ export MallocNanoZone=0
+ fi
+
+ # Close non-standard inherited fds
+ if [ -d /proc/self/fd ]; then
+ local fds=/proc/self/fd
+ else
+ local fds=/dev/fd
+ fi
+
+ for fd in "$fds"/*; do
+ if [ ! -e "$fd" ]; then
+ continue
+ fi
+
+ local fd="${fd##*/}"
+ if ((fd > 2)); then
+ eval "exec ${fd}<&-"
+ fi
+ done
+
+ # Close stdin so bfs doesn't think we're interactive
+ # dup() the standard fds for logging even when redirected
+ exec </dev/null 3>&1 4>&2
+}
+
+# Drop root priviliges or bail
+drop_root() {
+ if command -v capsh &>/dev/null; then
+ if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
+ if [ -n "${BFS_TRIED_DROP:-}" ]; then
+ color >&2 <<EOF
+${RED}error:${RST} Failed to drop capabilities.
+EOF
+
+ exit 1
+ fi
+
+ color >&2 <<EOF
+${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and
+${BLD}cap_dac_read_search${RST}.
+
+EOF
+
+ BFS_TRIED_DROP=y exec capsh \
+ --drop=cap_dac_override,cap_dac_read_search \
+ --caps=cap_dac_override,cap_dac_read_search-eip \
+ -- "$0" "$@"
+ fi
+ elif ((EUID == 0)); then
+ UNLESS=
+ if [ "$UNAME" = "Linux" ]; then
+ UNLESS=" unless ${GRN}capsh${RST} is installed"
+ fi
+
+ color >&2 <<EOF
+${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
+will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
+EOF
+ exit 1
+ fi
+}
+
+## Debugging
+
+# Get the bash call stack
+callers() {
+ local frame=0
+ while caller $frame; do
+ ((++frame))
+ done
+}
+
+# Print a message including path, line number, and command
+debug() {
+ local file="${1/#*\/tests\//tests\/}"
+ set -- "$file" "${@:2}"
+ cprintf "${BLD}%s:%d:${RST} %s\n %s\n" "$@"
+}
+
+## Deferred cleanup
+
+# Quote a command safely for eval
+quote() {
+ printf '%q' "$1"
+ shift
+ if (($# > 0)); then
+ printf ' %q' "$@"
+ fi
+}
+
+# Run a command when this (sub)shell exits
+defer() {
+ # Refresh trap state before trap -p
+ # See https://unix.stackexchange.com/a/556888/56202
+ trap -- KILL
+
+ # Check if the EXIT trap is already set
+ if ! trap -p EXIT | grep -q pop_defers; then
+ DEFER_CMDS=()
+ DEFER_LINES=()
+ DEFER_FILES=()
+ trap pop_defers EXIT
+ fi
+
+ DEFER_CMDS+=("$(quote "$@")")
+
+ local line file
+ read -r line file < <(caller)
+ DEFER_LINES+=("$line")
+ DEFER_FILES+=("$file")
+}
+
+# Pop a single command from the defer stack and run it
+pop_defer() {
+ local cmd="${DEFER_CMDS[-1]}"
+ local file="${DEFER_FILES[-1]}"
+ local line="${DEFER_LINES[-1]}"
+ unset "DEFER_CMDS[-1]"
+ unset "DEFER_FILES[-1]"
+ unset "DEFER_LINES[-1]"
+
+ local ret=0
+ eval "$cmd" || ret=$?
+
+ if ((ret != 0)); then
+ debug "$file" $line "${RED}error $ret${RST}" "defer $cmd" >&4
+ fi
+
+ return $ret
+}
+
+# Run all deferred commands
+pop_defers() {
+ local ret=0
+
+ while ((${#DEFER_CMDS[@]} > 0)); do
+ pop_defer || ret=$?
+ done
+
+ return $ret
+}