diff options
author | Tavian Barnes <tavianator@tavianator.com> | 2023-10-19 16:37:47 -0400 |
---|---|---|
committer | Tavian Barnes <tavianator@tavianator.com> | 2023-10-19 16:37:47 -0400 |
commit | 785a3f2d777627f39bed44f4ae7a0180d5184109 (patch) | |
tree | 2bee8cb669ab91982a7d6d1e8ada9958a2369ffd | |
parent | d484cba3424dcfca4851ba867d8877e3a9381a0e (diff) |
tests: Refactor implementation into separate files
-rw-r--r-- | tests/bfs/deep_strict.sh | 2 | ||||
-rw-r--r-- | tests/color.sh | 43 | ||||
-rw-r--r-- | tests/common/execdir_ulimit.sh | 1 | ||||
-rw-r--r-- | tests/getopts.sh | 158 | ||||
-rw-r--r-- | tests/gnu/printf_u_g_ulimit.sh | 1 | ||||
-rwxr-xr-x | tests/ls-color.sh | 6 | ||||
-rw-r--r-- | tests/posix/deep.sh | 2 | ||||
-rw-r--r-- | tests/posix/nogroup_ulimit.sh | 1 | ||||
-rw-r--r-- | tests/posix/nouser_ulimit.sh | 1 | ||||
-rw-r--r-- | tests/run.sh | 316 | ||||
-rw-r--r-- | tests/stddirs.sh | 185 | ||||
-rwxr-xr-x | tests/tests.sh | 824 | ||||
-rw-r--r-- | tests/util.sh | 189 |
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 -} - |