#!/usr/bin/env bash # Destructive only inside a uniquely named directory created by this script. set -Eeuo pipefail usage() { echo "usage: $0 MOUNTPOINT [MOUNTPOINT ...]" >&2 echo "example: $0 /mnt/on-demand-testmount /mnt/testmount" >&2 exit 2 } (( $# > 0 )) || usage command -v python3 >/dev/null || { echo "python3 is required" >&2; exit 2; } command -v findmnt >/dev/null || { echo "findmnt is required" >&2; exit 2; } mounts=("$@") run_id="pdfs-acceptance-$(date +%Y%m%dT%H%M%S)-$$" roots=() cleanup() { local root for root in "${roots[@]}"; do [[ -n "$root" && "$root" == */pdfs-acceptance-* ]] || continue rm -rf -- "$root" 2>/dev/null || true done } trap cleanup EXIT INT TERM for index in "${!mounts[@]}"; do mountpoint="${mounts[$index]}" [[ -d "$mountpoint" ]] || { echo "not a directory: $mountpoint" >&2; exit 2; } fs_type="$(findmnt -T "$mountpoint" -n -o FSTYPE | head -n 1)" if (( index == 0 )) && [[ "$fs_type" != fuse* ]]; then echo "refusing primary non-FUSE path $mountpoint (type: ${fs_type:-unknown})" >&2 exit 2 fi roots+=("${mountpoint%/}/$run_id") done step() { printf '[test] %s\n' "$1"; } assert_text() { python3 - "$1" "$2" <<'PY' import sys p, expected = sys.argv[1], sys.argv[2].encode() with open(p, "rb") as f: got = f.read() if got != expected: raise AssertionError( f"{p}: expected {expected!r} ({len(expected)} bytes), " f"got {got!r} ({len(got)} bytes)" ) PY } run_suite() { local primary="$1" printf '[mount] testing %s\n' "${primary%/*}" mkdir -- "$primary" step "create, read, overwrite, truncate" printf 'alpha\n' > "$primary/plain.txt" assert_text "$primary/plain.txt" $'alpha\n' printf 'a much longer replacement\n' > "$primary/plain.txt" truncate -s 4 "$primary/plain.txt" assert_text "$primary/plain.txt" "a mu" step "binary round-trip and random-access writes" python3 - "$primary/binary.dat" <<'PY' import hashlib, sys p = sys.argv[1] data = bytes(range(256)) * 16384 with open(p, "wb") as f: f.write(data) with open(p, "r+b", buffering=0) as f: f.seek(4093); f.write(b"boundary-write") with open(p, "rb") as f: got = f.read() expected = data[:4093] + b"boundary-write" + data[4107:] assert got == expected print(hashlib.sha256(got).hexdigest()) PY step "rename, move, and replacement semantics" mkdir -- "$primary/dir-a" "$primary/dir-b" printf 'source' > "$primary/source" printf 'victim' > "$primary/victim" mv -- "$primary/source" "$primary/victim" assert_text "$primary/victim" "source" [[ ! -e "$primary/source" ]] mv -- "$primary/victim" "$primary/dir-a/moved" assert_text "$primary/dir-a/moved" "source" step "POSIX error behavior" if rmdir -- "$primary/dir-a/moved" 2>/dev/null; then echo "rmdir removed a file" >&2; exit 1; fi if rm -- "$primary/dir-a" 2>/dev/null; then echo "rm removed a directory" >&2; exit 1; fi if mv -- "$primary/dir-a" "$primary/dir-a/child" 2>/dev/null; then echo "moving a directory below itself succeeded" >&2; exit 1 fi step "open file remains readable after unlink" python3 - "$primary/open-unlink" <<'PY' import os, sys p = sys.argv[1]; data = b"held-open\0" * 4096 with open(p, "wb") as f: f.write(data) f = open(p, "rb", buffering=0); os.unlink(p) assert not os.path.exists(p) and f.read() == data f.close() PY step "concurrent independent writers" for i in $(seq 1 12); do (python3 - "$primary/concurrent-$i" "$i" <<'PY' import sys p, seed = sys.argv[1], int(sys.argv[2]); data = bytes([seed]) * (131072 + seed) with open(p, "wb") as f: f.write(data) with open(p, "rb") as f: assert f.read() == data PY ) & done wait step "directory enumeration agrees with stat" python3 - "$primary" <<'PY' import os, sys for name in os.listdir(sys.argv[1]): os.lstat(os.path.join(sys.argv[1], name)) PY } for root in "${roots[@]}"; do run_suite "$root" done if [[ "${PDFS_ACCEPTANCE_CONVERGENCE:-0}" == 1 ]] && (( ${#roots[@]} > 1 )); then step "cross-mount convergence" primary="${roots[0]}" expected="$(sha256sum "$primary/binary.dat" | cut -d' ' -f1)" for secondary in "${roots[@]:1}"; do deadline=$((SECONDS + ${PDFS_ACCEPTANCE_SYNC_TIMEOUT:-120})) while (( SECONDS < deadline )); do if [[ -f "$secondary/binary.dat" ]] && [[ "$(sha256sum "$secondary/binary.dat" 2>/dev/null | cut -d' ' -f1)" == "$expected" ]]; then break; fi sleep 2 done [[ -f "$secondary/binary.dat" ]] || { echo "did not appear on $secondary" >&2; exit 1; } [[ "$(sha256sum "$secondary/binary.dat" | cut -d' ' -f1)" == "$expected" ]] || { echo "content mismatch on $secondary" >&2; exit 1 } done fi step "cleanup" cleanup roots=() echo "PASS: FUSE acceptance suite completed ($run_id)"