1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
|
#!/hint/bash
# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD
## Running test cases
# ERR trap for tests
debug_err() {
local ret=$? line func file
callers | while read -r line func file; do
if [ "$func" = source ]; then
debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
break
fi
done
}
# Source a test
source_test() (
set -eE
trap debug_err ERR
if ((${#MAKE[@]})); then
# Close the jobserver pipes
exec {READY_PIPE}<&- {DONE_PIPE}>&-
fi
cd "$TMP"
source "$@"
)
# Run a test
run_test() {
if ((VERBOSE_ERRORS)); then
source_test "$1"
else
source_test "$1" 2>"$TMP/$TEST.err"
fi
ret=$?
if ((${#MAKE[@]})); then
# Write one byte to the done pipe
printf . >&$DONE_PIPE
fi
case $ret in
0)
if ((VERBOSE_TESTS)); then
color printf "${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST"
fi
;;
$EX_SKIP)
if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then
color printf "${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST"
fi
;;
*)
if ((!VERBOSE_ERRORS)); then
cat "$TMP/$TEST.err" >&2
fi
color printf "${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST"
;;
esac
return $ret
}
# Count the tests running in the background
BG=0
# Run a test in the background
bg_test() {
run_test "$1" &
((++BG))
}
# Reap a finished background test
reap_test() {
((BG--))
case "$1" in
0)
((++passed))
;;
$EX_SKIP)
((++skipped))
;;
*)
((++failed))
;;
esac
}
# Wait for a background test to finish
wait_test() {
local pid line ret
while :; do
line=$((LINENO + 1))
_wait -n -ppid
ret=$?
if [ "${pid:-}" ]; then
break
else
debug "${BASH_SOURCE[0]}" $line "${RED}error $ret${RST}" >&$DUPERR
exit 1
fi
done
reap_test $ret
}
# Wait until we're ready to run another test
wait_ready() {
if ((${#MAKE[@]})); then
# We'd like to parse the output of jobs -n, but we can't run it in a
# subshell or we won't get the right output
jobs -n >"$TMP/jobs"
local job status ret rest
while read -r job status ret rest; do
case "$status" in
Done)
reap_test 0
;;
Exit)
reap_test $ret
;;
esac
done <"$TMP/jobs"
# Read one byte from the ready pipe
read -r -N1 -u$READY_PIPE
elif ((BG >= JOBS)); then
wait_test
fi
}
# Run make as a co-process to use its job control
comake() {
coproc {
# We can't just use std{in,out}, due to
# https://www.gnu.org/software/make/manual/html_node/Parallel-Input.html
exec {DONE_PIPE}<&0 {READY_PIPE}>&1
exec "${MAKE[@]}" -s \
-f "$TESTS/tests.mk" \
DONE=$DONE_PIPE \
READY=$READY_PIPE \
"${!TEST_CASES[@]}" \
</dev/null >/dev/null
}
# coproc pipes aren't inherited by subshells, so dup them
exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]}
}
# Print the current test progress
progress() {
if [ "${BAR:-}" ]; then
print_bar "$(printf "$@")"
elif ((VERBOSE_TESTS)); then
color printf "$@"
fi
}
# Run all the tests
run_tests() {
passed=0
failed=0
skipped=0
ran=0
total=${#TEST_CASES[@]}
TEST_FMT="${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n"
if ((${#MAKE[@]})); then
comake
fi
# Turn off set -e (but turn it back on in run_test)
set +e
if ((COLOR_STDOUT && !VERBOSE_TESTS)); then
show_bar
fi
for TEST in "${TEST_CASES[@]}"; do
wait_ready
if ((STOP && failed > 0)); then
break
fi
percent=$((100 * ran / total))
progress "${YLW}[%3d%%]${RST} ${BLD}%s${RST}\\n" $percent "$TEST"
mkdir -p "$TMP/$TEST"
OUT="$TMP/$TEST.out"
bg_test "$TESTS/$TEST.sh"
((++ran))
done
while ((BG > 0)); do
wait_test
done
if [ "${BAR:-}" ]; then
hide_bar
fi
if ((passed > 0)); then
color printf "${GRN}[PASS]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $passed $total
fi
if ((skipped > 0)); then
color printf "${CYN}[SKIP]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $skipped $total
fi
if ((failed > 0)); then
color printf "${RED}[FAIL]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $failed $total
exit 1
fi
}
## Utilities for the tests themselves
# Default return value for failed tests
EX_FAIL=1
# Fail the current test
fail() {
exit $EX_FAIL
}
# Return value when a test is skipped
EX_SKIP=77
# Skip the current test
skip() {
if ((VERBOSE_SKIPPED)); then
caller | {
read -r line file
debug "$file" $line "" >&$DUPOUT
}
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
(
# Close some fds to make room for the pipe,
# even with extremely low ulimit -n
exec >&- {DUPERR}>&-
exec >&$DUPOUT {DUPOUT}>&-
color bfs_verbose_impl "$@"
)
fi
}
bfs_verbose_impl() {
printf "${GRN}%q${RST}" "${BFS[0]}"
if ((${#BFS[@]} > 1)); then
printf " ${GRN}%q${RST}" "${BFS[@]:1}"
fi
local expr_started=0 color
for arg; do
case "$arg" in
-[A-Z]*|-[dsxf]|-j*)
color="${CYN}"
;;
\(|!|-[ao]|-and|-or|-not|-exclude)
expr_started=1
color="${RED}"
;;
\)|,)
if ((expr_started)); then
color="${RED}"
else
color="${MAG}"
fi
;;
-?*)
expr_started=1
color="${BLU}"
;;
*)
if ((expr_started)); then
color="${BLD}"
else
color="${MAG}"
fi
;;
esac
printf " ${color}%q${RST}" "$arg"
done
printf '\n'
}
# Run the bfs we're testing
invoke_bfs() {
bfs_verbose "$@"
local ret=0
# Close the logging fds
"${BFS[@]}" "$@" {DUPOUT}>&- {DUPERR}>&- || ret=$?
# Allow bfs to fail, but not crash
if ((ret > 125)); then
exit $ret
else
return $ret
fi
}
# Run bfs with a pseudo-terminal attached
bfs_pty() {
bfs_verbose "$@"
local ret=0
"$PTYX" -w80 -h24 -- "${BFS[@]}" "$@" || ret=$?
if ((ret > 125)); then
exit $ret
else
return $ret
fi
}
# Create a directory tree with xattrs in scratch
make_xattrs() {
cd "$TEST"
"$XTOUCH" normal xattr xattr_2
ln -s xattr link
ln -s normal xattr_link
case "$UNAME" in
Darwin)
xattr -w bfs_test true xattr \
&& xattr -w bfs_test_2 true xattr_2 \
&& xattr -s -w bfs_test true xattr_link
;;
FreeBSD)
setextattr user bfs_test true xattr \
&& setextattr user bfs_test_2 true xattr_2 \
&& setextattr -h user bfs_test true 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 xattr \
&& bfs_sudo setfattr -n security.bfs_test_2 xattr_2 \
&& bfs_sudo setfattr -h -n security.bfs_test xattr_link
;;
esac
}
# Get the Unix epoch time in seconds
epoch_time() {
if [ "${EPOCHSECONDS:-}" ]; then
# Added in bash 5
printf '%d' "$EPOCHSECONDS"
else
# https://stackoverflow.com/a/12746260/502399
awk 'BEGIN { srand(); print srand(); }'
fi
}
## 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 &>/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"
elif ! cmp -s "$GOLD" "$OUT"; then
$DIFF -u "$GOLD" "$OUT" >&$DUPERR
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
}
|