mirror of
https://github.com/trapexit/mergerfs.git
synced 2026-10-26 10:51:52 -04:00
Strengthen mergerfs integration tests
- Make the test runner fail on failed tests and report explicit skips - Strengthen assertions for O_DIRECT, LUP policy, EXDEV fallback, and xattrs - Fix cleanup and file descriptor handling across test scripts
This commit is contained in:
parent
ef1e01b5a5
commit
92b3af3127
33 changed files with 846 additions and 484 deletions
|
|
@ -66,4 +66,6 @@ Across tests, parity checks include combinations of:
|
|||
- Some behavior is runtime/environment dependent (kernel, mount options,
|
||||
uid/gid, capabilities). Tests compare mergerfs to native behavior in the same
|
||||
runtime to keep assertions robust.
|
||||
- Tests that cannot exercise a behavior in the current environment may exit 77
|
||||
and are reported as `SKIP` by `tests/run-tests`.
|
||||
- The xattr test uses a `user.*` key and assumes user xattrs are enabled.
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@
|
|||
import errno
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from posix_parity import cleanup_dir
|
||||
from posix_parity import fail
|
||||
from posix_parity import join
|
||||
from posix_parity import mergerfs_branches
|
||||
from posix_parity import mergerfs_get_option
|
||||
from posix_parity import mergerfs_set_option
|
||||
from posix_parity import parse_allpaths
|
||||
from posix_parity import temp_dir
|
||||
from posix_parity import touch
|
||||
|
||||
|
||||
SKIP = 77
|
||||
|
||||
|
||||
def get_errno(func):
|
||||
try:
|
||||
func()
|
||||
|
|
@ -22,46 +25,34 @@ def get_errno(func):
|
|||
return exc.errno
|
||||
|
||||
|
||||
def allpaths(path):
|
||||
raw = os.getxattr(path, "user.mergerfs.allpaths")
|
||||
return parse_allpaths(raw)
|
||||
def symlink_resolves(path):
|
||||
target = os.readlink(path)
|
||||
if os.path.isabs(target):
|
||||
return os.path.realpath(target)
|
||||
return os.path.realpath(join(os.path.dirname(path), target))
|
||||
|
||||
|
||||
def must_two_branches(paths, op):
|
||||
if len(paths) < 2:
|
||||
return f"{op}: requires at least 2 branches; got {paths!r}"
|
||||
def require_symlink(path, label):
|
||||
if not os.path.islink(path):
|
||||
return f"{label}: expected symlink fallback"
|
||||
if not os.path.exists(path):
|
||||
return f"{label}: symlink target does not exist: {os.readlink(path)!r}"
|
||||
return None
|
||||
|
||||
|
||||
def pick_cross_branch_paths(mount, merge_base):
|
||||
src = join(merge_base, "src")
|
||||
dst = join(merge_base, "sub/dst")
|
||||
def setup_paths(branches, mount, rel_base, name):
|
||||
src_rel = join(rel_base, f"{name}.src")
|
||||
dst_rel = join(rel_base, "dst", name)
|
||||
src = join(mount, src_rel)
|
||||
dst = join(mount, dst_rel)
|
||||
|
||||
touch(src, b"src")
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
cleanup_dir(join(branches[0], rel_base))
|
||||
cleanup_dir(join(branches[1], rel_base))
|
||||
os.makedirs(join(branches[0], rel_base), exist_ok=True)
|
||||
os.makedirs(join(branches[1], rel_base, "dst"), exist_ok=True)
|
||||
touch(join(branches[0], src_rel), b"src")
|
||||
|
||||
try:
|
||||
src_paths = allpaths(src)
|
||||
except OSError as exc:
|
||||
if exc.errno in (errno.EOPNOTSUPP, errno.ENOTSUP, errno.ENOSYS):
|
||||
raise RuntimeError("allpaths-xattr-unsupported")
|
||||
raise
|
||||
if len(src_paths) < 2:
|
||||
# Try to create destination first to force different existing-path decisions.
|
||||
touch(dst, b"old")
|
||||
os.unlink(dst)
|
||||
try:
|
||||
src_paths = allpaths(src)
|
||||
except OSError as exc:
|
||||
if exc.errno in (errno.EOPNOTSUPP, errno.ENOTSUP, errno.ENOSYS):
|
||||
raise RuntimeError("allpaths-xattr-unsupported")
|
||||
raise
|
||||
|
||||
err = must_two_branches(src_paths, "cross-branch setup")
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
|
||||
return src, dst
|
||||
return src, dst, join(branches[0], src_rel)
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -70,70 +61,79 @@ def main():
|
|||
return 1
|
||||
|
||||
mount = sys.argv[1]
|
||||
merge_base = temp_dir(mount)
|
||||
rel_base = f".mergerfs_exdev_test_{os.getpid()}_{uuid.uuid4().hex}"
|
||||
branches = mergerfs_branches(mount)
|
||||
if len(branches) < 2:
|
||||
print("EXDEV fallback test requires at least 2 branches", end="")
|
||||
return SKIP
|
||||
branches = branches[:2]
|
||||
if os.stat(branches[0]).st_dev == os.stat(branches[1]).st_dev:
|
||||
print("EXDEV fallback test requires branches on different devices", end="")
|
||||
return SKIP
|
||||
|
||||
try:
|
||||
try:
|
||||
src, dst = pick_cross_branch_paths(mount, merge_base)
|
||||
except PermissionError:
|
||||
return 0
|
||||
except RuntimeError as exc:
|
||||
# Environment-dependent; treat as soft skip.
|
||||
msg = str(exc)
|
||||
if "requires at least 2 branches" in msg or msg == "allpaths-xattr-unsupported":
|
||||
return 0
|
||||
return fail(msg)
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
try:
|
||||
orig_create = mergerfs_get_option(mount, "category.create")
|
||||
orig_link = mergerfs_get_option(mount, "link-exdev")
|
||||
orig_rename = mergerfs_get_option(mount, "rename-exdev")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
return 0
|
||||
print("EXDEV fallback runtime options unavailable", end="")
|
||||
return SKIP
|
||||
|
||||
try:
|
||||
# Maximize chance of EXDEV path by path-preserving create policy.
|
||||
mergerfs_set_option(mount, "category.create", "epmfs")
|
||||
|
||||
# link-exdev passthrough => EXDEV when cross-device path preserving applies.
|
||||
src, dst, src_base = setup_paths(branches, mount, rel_base, "link_pass")
|
||||
mergerfs_set_option(mount, "link-exdev", "passthrough")
|
||||
err = get_errno(lambda: os.link(src, dst + ".link.pass"))
|
||||
if err not in (0, errno.EXDEV):
|
||||
return fail(f"link-exdev=passthrough unexpected errno={err}")
|
||||
link_exdev_active = (err == errno.EXDEV)
|
||||
if err != errno.EXDEV:
|
||||
print(f"link-exdev passthrough did not exercise EXDEV, errno={err}", end="")
|
||||
return SKIP
|
||||
|
||||
# rel-symlink should not return EXDEV if fallback triggers.
|
||||
mergerfs_set_option(mount, "link-exdev", "rel-symlink")
|
||||
rel_link = dst + ".link.rel"
|
||||
err = get_errno(lambda: os.link(src, rel_link))
|
||||
if link_exdev_active and err == errno.EXDEV:
|
||||
return fail("link-exdev=rel-symlink still returned EXDEV")
|
||||
if link_exdev_active and err == 0 and not os.path.islink(rel_link):
|
||||
return fail("link-exdev=rel-symlink expected symlink fallback")
|
||||
for mode in ("rel-symlink", "abs-base-symlink", "abs-pool-symlink"):
|
||||
src, dst, src_base = setup_paths(branches, mount, rel_base, f"link_{mode}")
|
||||
mergerfs_set_option(mount, "link-exdev", mode)
|
||||
link_path = dst + ".link"
|
||||
err = get_errno(lambda: os.link(src, link_path))
|
||||
if err != 0:
|
||||
return fail(f"link-exdev={mode} returned errno={err}")
|
||||
err_msg = require_symlink(link_path, f"link-exdev={mode}")
|
||||
if err_msg:
|
||||
return fail(err_msg)
|
||||
resolved = symlink_resolves(link_path)
|
||||
if mode == "abs-base-symlink" and resolved != os.path.realpath(src_base):
|
||||
return fail(f"link-exdev={mode}: target {resolved!r} != {os.path.realpath(src_base)!r}")
|
||||
if mode != "abs-base-symlink" and resolved != os.path.realpath(src):
|
||||
return fail(f"link-exdev={mode}: target {resolved!r} != {os.path.realpath(src)!r}")
|
||||
|
||||
# rename-exdev passthrough => EXDEV in cross-device path preserving case.
|
||||
src_ren = src + ".ren"
|
||||
touch(src_ren, b"src")
|
||||
src_ren, dst_ren, _ = setup_paths(branches, mount, rel_base, "rename_pass")
|
||||
mergerfs_set_option(mount, "rename-exdev", "passthrough")
|
||||
err = get_errno(lambda: os.rename(src_ren, dst + ".ren.pass"))
|
||||
if err not in (0, errno.EXDEV):
|
||||
return fail(f"rename-exdev=passthrough unexpected errno={err}")
|
||||
rename_exdev_active = (err == errno.EXDEV)
|
||||
err = get_errno(lambda: os.rename(src_ren, dst_ren + ".ren.pass"))
|
||||
if err != errno.EXDEV:
|
||||
print(f"rename-exdev passthrough did not exercise EXDEV, errno={err}", end="")
|
||||
return SKIP
|
||||
|
||||
# rel-symlink should avoid EXDEV by fallback when applicable.
|
||||
src_ren2 = src + ".ren2"
|
||||
touch(src_ren2, b"src")
|
||||
mergerfs_set_option(mount, "rename-exdev", "rel-symlink")
|
||||
rel_ren = dst + ".ren.rel"
|
||||
err = get_errno(lambda: os.rename(src_ren2, rel_ren))
|
||||
if rename_exdev_active and err == errno.EXDEV:
|
||||
return fail("rename-exdev=rel-symlink still returned EXDEV")
|
||||
if rename_exdev_active and err == 0 and not os.path.islink(rel_ren):
|
||||
return fail("rename-exdev=rel-symlink expected symlink fallback")
|
||||
for mode in ("rel-symlink", "abs-symlink"):
|
||||
src_ren, dst_ren, _ = setup_paths(branches, mount, rel_base, f"rename_{mode}")
|
||||
mergerfs_set_option(mount, "rename-exdev", mode)
|
||||
ren_path = dst_ren + ".ren"
|
||||
err = get_errno(lambda: os.rename(src_ren, ren_path))
|
||||
if err != 0:
|
||||
return fail(f"rename-exdev={mode} returned errno={err}")
|
||||
err_msg = require_symlink(ren_path, f"rename-exdev={mode}")
|
||||
if err_msg:
|
||||
return fail(err_msg)
|
||||
target = symlink_resolves(ren_path)
|
||||
if ".mergerfs_rename_exdev" not in target:
|
||||
return fail(f"rename-exdev={mode}: target not moved under .mergerfs_rename_exdev: {target!r}")
|
||||
with open(ren_path, "rb") as fp:
|
||||
if fp.read() != b"src":
|
||||
return fail(f"rename-exdev={mode}: symlink target content mismatch")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
return 0
|
||||
return SKIP
|
||||
finally:
|
||||
try:
|
||||
mergerfs_set_option(mount, "category.create", orig_create)
|
||||
|
|
@ -144,7 +144,8 @@ def main():
|
|||
|
||||
return 0
|
||||
finally:
|
||||
cleanup_dir(merge_base)
|
||||
for branch in branches:
|
||||
cleanup_dir(join(branch, rel_base))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ def main():
|
|||
|
||||
try:
|
||||
orig = mergerfs_get_option(mount, "xattr")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
except (PermissionError, FileNotFoundError):
|
||||
return 0
|
||||
|
||||
try:
|
||||
|
|
@ -48,8 +48,9 @@ def main():
|
|||
get_err = get_errno(lambda: os.getxattr(path, xname))
|
||||
if set_err != 0:
|
||||
return fail(f"xattr=passthrough setxattr expected success got errno={set_err}")
|
||||
# Current known bug: getxattr may return ENODATA. We still enforce mode switch behavior.
|
||||
if get_err not in (0, errno.ENODATA):
|
||||
if get_err == errno.ENODATA:
|
||||
return fail(f"xattr=passthrough getxattr returned ENODATA (known bug): this masks a parity violation")
|
||||
if get_err != 0:
|
||||
return fail(f"xattr=passthrough getxattr unexpected errno={get_err}")
|
||||
|
||||
mergerfs_set_option(mount, "xattr", "noattr")
|
||||
|
|
@ -59,7 +60,11 @@ def main():
|
|||
err = get_errno(lambda: os.setxattr(path, xname, xval))
|
||||
if err != errno.ENODATA:
|
||||
return fail(f"xattr=noattr setxattr expected ENODATA got errno={err}")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
|
||||
# xattr=nosys is intentionally not tested here. It disables the
|
||||
# xattr API, including the runtime interface this test uses to
|
||||
# restore options safely.
|
||||
except (PermissionError, FileNotFoundError):
|
||||
return 0
|
||||
finally:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -3,101 +3,100 @@
|
|||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
# Test create + write + read (passthrough.io=rw)
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
test_data = b"passthrough io test data for create\n" * 100
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: TEST_io_passthrough_create_open <mountpoint>", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Write to newly created file
|
||||
bytes_written = os.write(fd, test_data)
|
||||
if bytes_written != len(test_data):
|
||||
print("create write failed: expected {} bytes, wrote {}".format(len(test_data), bytes_written))
|
||||
sys.exit(1)
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
# Seek and read back
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
read_data = os.read(fd, len(test_data))
|
||||
if read_data != test_data:
|
||||
print("create read failed: data mismatch")
|
||||
sys.exit(1)
|
||||
try:
|
||||
test_data = b"passthrough io test data for create\n" * 100
|
||||
|
||||
os.close(fd)
|
||||
bytes_written = os.write(fd, test_data)
|
||||
if bytes_written != len(test_data):
|
||||
print("create write failed: expected {} bytes, wrote {}".format(len(test_data), bytes_written))
|
||||
return 1
|
||||
|
||||
# Test open existing file + write + read
|
||||
fd = os.open(filepath, os.O_RDWR)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
read_data = os.read(fd, len(test_data))
|
||||
if read_data != test_data:
|
||||
print("create read failed: data mismatch")
|
||||
return 1
|
||||
|
||||
# Read existing data
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
read_data = os.read(fd, len(test_data))
|
||||
if read_data != test_data:
|
||||
print("open read failed: data mismatch")
|
||||
sys.exit(1)
|
||||
os.close(fd)
|
||||
fd = -1
|
||||
|
||||
# Write more data at end
|
||||
os.lseek(fd, 0, os.SEEK_END)
|
||||
more_data = b"additional passthrough data for open\n" * 50
|
||||
bytes_written = os.write(fd, more_data)
|
||||
if bytes_written != len(more_data):
|
||||
print("open write failed: expected {} bytes, wrote {}".format(len(more_data), bytes_written))
|
||||
sys.exit(1)
|
||||
fd = os.open(filepath, os.O_RDWR)
|
||||
|
||||
# Verify all data
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
all_data = os.read(fd, len(test_data) + len(more_data))
|
||||
if all_data != test_data + more_data:
|
||||
print("open final read failed: data mismatch")
|
||||
sys.exit(1)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
read_data = os.read(fd, len(test_data))
|
||||
if read_data != test_data:
|
||||
print("open read failed: data mismatch")
|
||||
return 1
|
||||
|
||||
# Test multiple opens of same file (single backing_id feature)
|
||||
# When a file is already open with passthrough, subsequent opens
|
||||
# must reuse the same backing_id rather than creating new ones
|
||||
fd2 = os.open(filepath, os.O_RDWR)
|
||||
os.lseek(fd, 0, os.SEEK_END)
|
||||
more_data = b"additional passthrough data for open\n" * 50
|
||||
bytes_written = os.write(fd, more_data)
|
||||
if bytes_written != len(more_data):
|
||||
print("open write failed: expected {} bytes, wrote {}".format(len(more_data), bytes_written))
|
||||
return 1
|
||||
|
||||
# Read from second fd - should see same data
|
||||
os.lseek(fd2, 0, os.SEEK_SET)
|
||||
read_data2 = os.read(fd2, len(test_data) + len(more_data))
|
||||
if read_data2 != test_data + more_data:
|
||||
print("second open read failed: data mismatch")
|
||||
os.close(fd)
|
||||
os.close(fd2)
|
||||
sys.exit(1)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
all_data = os.read(fd, len(test_data) + len(more_data))
|
||||
if all_data != test_data + more_data:
|
||||
print("open final read failed: data mismatch")
|
||||
return 1
|
||||
|
||||
# Write from second fd
|
||||
os.lseek(fd2, 0, os.SEEK_END)
|
||||
extra_data = b"data from second file descriptor\n" * 25
|
||||
bytes_written = os.write(fd2, extra_data)
|
||||
if bytes_written != len(extra_data):
|
||||
print("second open write failed: expected {} bytes, wrote {}".format(len(extra_data), bytes_written))
|
||||
os.close(fd)
|
||||
os.close(fd2)
|
||||
sys.exit(1)
|
||||
fd2 = os.open(filepath, os.O_RDWR)
|
||||
|
||||
# Verify write is visible from first fd (shared backing)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
combined_data = os.read(fd, len(test_data) + len(more_data) + len(extra_data))
|
||||
expected_data = test_data + more_data + extra_data
|
||||
if combined_data != expected_data:
|
||||
print("cross-fd read failed: data mismatch (writes from fd2 not visible on fd)")
|
||||
os.close(fd)
|
||||
os.close(fd2)
|
||||
sys.exit(1)
|
||||
try:
|
||||
os.lseek(fd2, 0, os.SEEK_SET)
|
||||
read_data2 = os.read(fd2, len(test_data) + len(more_data))
|
||||
if read_data2 != test_data + more_data:
|
||||
print("second open read failed: data mismatch")
|
||||
return 1
|
||||
|
||||
# Open a third fd while others are still open
|
||||
fd3 = os.open(filepath, os.O_RDONLY)
|
||||
os.lseek(fd3, 0, os.SEEK_SET)
|
||||
read_data3 = os.read(fd3, len(expected_data))
|
||||
if read_data3 != expected_data:
|
||||
print("third open read failed: data mismatch")
|
||||
os.close(fd)
|
||||
os.close(fd2)
|
||||
os.close(fd3)
|
||||
sys.exit(1)
|
||||
os.lseek(fd2, 0, os.SEEK_END)
|
||||
extra_data = b"data from second file descriptor\n" * 25
|
||||
bytes_written = os.write(fd2, extra_data)
|
||||
if bytes_written != len(extra_data):
|
||||
print("second open write failed: expected {} bytes, wrote {}".format(len(extra_data), bytes_written))
|
||||
return 1
|
||||
|
||||
os.close(fd3)
|
||||
os.close(fd2)
|
||||
os.close(fd)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
combined_data = os.read(fd, len(test_data) + len(more_data) + len(extra_data))
|
||||
expected_data = test_data + more_data + extra_data
|
||||
if combined_data != expected_data:
|
||||
print("cross-fd read failed: data mismatch (writes from fd2 not visible on fd)")
|
||||
return 1
|
||||
finally:
|
||||
os.close(fd2)
|
||||
|
||||
# Cleanup
|
||||
os.unlink(filepath)
|
||||
fd3 = os.open(filepath, os.O_RDONLY)
|
||||
try:
|
||||
os.lseek(fd3, 0, os.SEEK_SET)
|
||||
read_data3 = os.read(fd3, len(expected_data))
|
||||
if read_data3 != expected_data:
|
||||
print("third open read failed: data mismatch")
|
||||
return 1
|
||||
finally:
|
||||
os.close(fd3)
|
||||
|
||||
os.close(fd)
|
||||
fd = -1
|
||||
|
||||
return 0
|
||||
finally:
|
||||
if fd >= 0:
|
||||
os.close(fd)
|
||||
try:
|
||||
os.unlink(filepath)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,129 +4,129 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
NUM_THREADS = 50
|
||||
TEST_DATA = b"race condition test data\n"
|
||||
|
||||
|
||||
def open_and_operate(filepath, barrier, results, index):
|
||||
"""
|
||||
Wait at barrier, then open file, write, read, and store result.
|
||||
"""
|
||||
fd = None
|
||||
try:
|
||||
# Wait for all threads to be ready
|
||||
barrier.wait()
|
||||
|
||||
# All threads try to open simultaneously
|
||||
|
||||
fd = os.open(filepath, os.O_RDWR)
|
||||
|
||||
# Write thread-specific data at a unique offset
|
||||
|
||||
offset = index * len(TEST_DATA)
|
||||
os.lseek(fd, offset, os.SEEK_SET)
|
||||
bytes_written = os.write(fd, TEST_DATA)
|
||||
|
||||
if bytes_written != len(TEST_DATA):
|
||||
results[index] = ("write_error", "expected {} bytes, wrote {}".format(len(TEST_DATA), bytes_written))
|
||||
os.close(fd)
|
||||
fd = None
|
||||
return
|
||||
|
||||
# Read back what we wrote
|
||||
|
||||
os.lseek(fd, offset, os.SEEK_SET)
|
||||
read_data = os.read(fd, len(TEST_DATA))
|
||||
|
||||
|
||||
if read_data != TEST_DATA:
|
||||
results[index] = ("read_error", "data mismatch at offset {}".format(offset))
|
||||
os.close(fd)
|
||||
fd = None
|
||||
return
|
||||
|
||||
|
||||
results[index] = ("success", fd)
|
||||
|
||||
fd = None
|
||||
except Exception as e:
|
||||
if fd is not None:
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
results[index] = ("exception", str(e))
|
||||
|
||||
# Create test file with enough space for all threads
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
# Pre-allocate file to avoid issues with concurrent writes extending file
|
||||
total_size = NUM_THREADS * len(TEST_DATA)
|
||||
os.ftruncate(fd, total_size)
|
||||
os.close(fd)
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: TEST_io_passthrough_open_race <mountpoint>", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Set up synchronization
|
||||
barrier = threading.Barrier(NUM_THREADS)
|
||||
results = [None] * NUM_THREADS
|
||||
threads = []
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
# Create and start all threads
|
||||
for i in range(NUM_THREADS):
|
||||
t = threading.Thread(target=open_and_operate, args=(filepath, barrier, results, i))
|
||||
threads.append(t)
|
||||
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# Check results
|
||||
failed = False
|
||||
fds_to_close = []
|
||||
|
||||
for i, result in enumerate(results):
|
||||
if result is None:
|
||||
print("thread {} returned no result".format(i))
|
||||
failed = True
|
||||
elif result[0] == "success":
|
||||
fds_to_close.append(result[1])
|
||||
else:
|
||||
print("thread {} failed: {} - {}".format(i, result[0], result[1]))
|
||||
failed = True
|
||||
|
||||
if failed:
|
||||
for fd in fds_to_close:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
sys.exit(1)
|
||||
|
||||
# Verify all data is consistent by reading through one fd
|
||||
if fds_to_close:
|
||||
verify_fd = fds_to_close[0]
|
||||
os.lseek(verify_fd, 0, os.SEEK_SET)
|
||||
all_data = os.read(verify_fd, total_size)
|
||||
|
||||
expected_data = TEST_DATA * NUM_THREADS
|
||||
if all_data != expected_data:
|
||||
print("final verification failed: data mismatch")
|
||||
print("expected {} bytes, got {} bytes".format(len(expected_data), len(all_data)))
|
||||
for fd in fds_to_close:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
sys.exit(1)
|
||||
|
||||
# Test cross-fd visibility: write from one fd, read from another
|
||||
if len(fds_to_close) >= 2:
|
||||
fd_a = fds_to_close[0]
|
||||
fd_b = fds_to_close[1]
|
||||
|
||||
# Write new data from fd_a
|
||||
new_data = b"cross-fd visibility test\n"
|
||||
os.lseek(fd_a, 0, os.SEEK_SET)
|
||||
os.write(fd_a, new_data)
|
||||
|
||||
# Read from fd_b - should see the new data
|
||||
os.lseek(fd_b, 0, os.SEEK_SET)
|
||||
read_back = os.read(fd_b, len(new_data))
|
||||
|
||||
if read_back != new_data:
|
||||
print("cross-fd visibility failed: write from fd_a not visible on fd_b")
|
||||
for fd in fds_to_close:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
sys.exit(1)
|
||||
|
||||
# Close all file descriptors
|
||||
for fd in fds_to_close:
|
||||
total_size = NUM_THREADS * len(TEST_DATA)
|
||||
os.ftruncate(fd, total_size)
|
||||
os.close(fd)
|
||||
|
||||
# Cleanup
|
||||
os.unlink(filepath)
|
||||
barrier = threading.Barrier(NUM_THREADS)
|
||||
results = [None] * NUM_THREADS
|
||||
threads = []
|
||||
|
||||
for i in range(NUM_THREADS):
|
||||
t = threading.Thread(target=open_and_operate, args=(filepath, barrier, results, i))
|
||||
threads.append(t)
|
||||
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
failed = False
|
||||
fds_to_close = []
|
||||
|
||||
for i, result in enumerate(results):
|
||||
if result is None:
|
||||
print("thread {} returned no result".format(i))
|
||||
failed = True
|
||||
elif result[0] == "success":
|
||||
fds_to_close.append(result[1])
|
||||
else:
|
||||
print("thread {} failed: {} - {}".format(i, result[0], result[1]))
|
||||
failed = True
|
||||
|
||||
if failed:
|
||||
for fd in fds_to_close:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
return 1
|
||||
|
||||
if fds_to_close:
|
||||
verify_fd = fds_to_close[0]
|
||||
os.lseek(verify_fd, 0, os.SEEK_SET)
|
||||
all_data = os.read(verify_fd, total_size)
|
||||
|
||||
expected_data = TEST_DATA * NUM_THREADS
|
||||
if all_data != expected_data:
|
||||
print("final verification failed: data mismatch")
|
||||
print("expected {} bytes, got {} bytes".format(len(expected_data), len(all_data)))
|
||||
for fd in fds_to_close:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
return 1
|
||||
|
||||
if len(fds_to_close) >= 2:
|
||||
fd_a = fds_to_close[0]
|
||||
fd_b = fds_to_close[1]
|
||||
|
||||
new_data = b"cross-fd visibility test\n"
|
||||
os.lseek(fd_a, 0, os.SEEK_SET)
|
||||
os.write(fd_a, new_data)
|
||||
|
||||
os.lseek(fd_b, 0, os.SEEK_SET)
|
||||
read_back = os.read(fd_b, len(new_data))
|
||||
|
||||
if read_back != new_data:
|
||||
print("cross-fd visibility failed: write from fd_a not visible on fd_b")
|
||||
for fd in fds_to_close:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
return 1
|
||||
|
||||
for fd in fds_to_close:
|
||||
os.close(fd)
|
||||
|
||||
os.unlink(filepath)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -5,6 +5,7 @@ import shutil
|
|||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from posix_parity import fail
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ def main():
|
|||
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if p.returncode != 0:
|
||||
return 0
|
||||
return fail(f"mount lifecycle: mergerfs mount failed: {p.stderr.decode(errors='replace')}")
|
||||
|
||||
try:
|
||||
with open(os.path.join(mp, "lifecycle-file"), "wb") as fp:
|
||||
|
|
@ -54,9 +55,13 @@ def main():
|
|||
return fail("mount lifecycle: expected created file to exist")
|
||||
finally:
|
||||
um = shutil.which("fusermount3") or shutil.which("fusermount")
|
||||
u = subprocess.run([um, "-u", mp], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
for attempt in range(3):
|
||||
u = subprocess.run([um, "-u", mp], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if u.returncode == 0:
|
||||
break
|
||||
time.sleep(1)
|
||||
if u.returncode != 0:
|
||||
return fail("mount lifecycle: failed to unmount test mount")
|
||||
subprocess.run([um, "-z", mp], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import errno
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from posix_parity import cleanup_dir
|
||||
from posix_parity import fail
|
||||
|
|
@ -15,12 +14,7 @@ from posix_parity import temp_dir
|
|||
from posix_parity import touch
|
||||
|
||||
|
||||
def get_errno(func):
|
||||
try:
|
||||
func()
|
||||
return 0
|
||||
except OSError as exc:
|
||||
return exc.errno
|
||||
SKIP = 77
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -32,25 +26,54 @@ def main():
|
|||
|
||||
try:
|
||||
orig_moveonenospc = mergerfs_get_option(mount, "moveonenospc")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
return 0
|
||||
except (PermissionError, FileNotFoundError):
|
||||
print("moveonenospc runtime option unavailable", end="")
|
||||
return SKIP
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EOPNOTSUPP:
|
||||
print("moveonenospc runtime option unsupported", end="")
|
||||
return SKIP
|
||||
raise
|
||||
|
||||
branches = mergerfs_branches(mount)
|
||||
if len(branches) < 2:
|
||||
return 0
|
||||
print("moveonenospc test requires at least 2 branches", end="")
|
||||
return SKIP
|
||||
|
||||
# The documented feature only runs after write returns ENOSPC/EDQUOT. This
|
||||
# generic mountpoint test cannot safely force a branch into that state.
|
||||
merge_base = temp_dir(mount)
|
||||
|
||||
try:
|
||||
mergerfs_set_option(mount, "moveonenospc", "true")
|
||||
mergerfs_set_option(mount, "func.moveonenospc", "mfs")
|
||||
mergerfs_set_option(mount, "moveonenospc", "mfs")
|
||||
if mergerfs_get_option(mount, "moveonenospc") != "mfs":
|
||||
return fail("moveonenospc: failed to set documented policy value")
|
||||
|
||||
merge_file = join(merge_base, "enospc_test")
|
||||
touch(merge_file, b"test data for enospc")
|
||||
|
||||
if not os.path.exists(merge_file):
|
||||
return fail("moveonenospc: file was not created")
|
||||
|
||||
fullpath = None
|
||||
try:
|
||||
fullpath = os.getxattr(merge_file, "user.mergerfs.fullpath").decode()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if fullpath and not os.path.exists(fullpath):
|
||||
return fail(f"moveonenospc: fullpath does not exist on disk: {fullpath}")
|
||||
|
||||
with open(merge_file, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
if content != "test data for enospc":
|
||||
return fail(f"moveonenospc: file content mismatch: got {content!r}")
|
||||
|
||||
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
|
||||
|
||||
return 0
|
||||
print("moveonenospc ENOSPC path requires a constrained branch", end="")
|
||||
return SKIP
|
||||
except Exception as exc:
|
||||
try:
|
||||
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
|
||||
|
|
@ -62,4 +85,4 @@ def main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -4,16 +4,24 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
ino = os.fstat(fd).st_ino
|
||||
def main():
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(filepath)
|
||||
os.unlink(filepath)
|
||||
|
||||
for entry in os.scandir(sys.argv[1]):
|
||||
if entry.inode() == ino:
|
||||
print("found .fuse_hidden file {}".format(entry.name),end='')
|
||||
os.close(fd)
|
||||
sys.exit(1)
|
||||
found = False
|
||||
for entry in os.scandir(sys.argv[1]):
|
||||
if entry.name.startswith(".fuse_hidden"):
|
||||
print("found .fuse_hidden file {}".format(entry.name), end='')
|
||||
found = True
|
||||
|
||||
os.close(fd)
|
||||
os.close(fd)
|
||||
|
||||
if found:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,25 +1,66 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import ctypes
|
||||
import mmap
|
||||
import os
|
||||
import resource
|
||||
import sys
|
||||
import tempfile
|
||||
import mmap
|
||||
import resource
|
||||
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.close(fd)
|
||||
libc = ctypes.CDLL(None, use_errno=True)
|
||||
libc.read.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t]
|
||||
libc.read.restype = ctypes.c_ssize_t
|
||||
|
||||
fd = os.open(filepath,os.O_RDWR|os.O_DIRECT|os.O_TRUNC)
|
||||
|
||||
os.unlink(filepath)
|
||||
def aligned_read(fd, size):
|
||||
buf = mmap.mmap(-1, size)
|
||||
ptr = ctypes.addressof(ctypes.c_char.from_buffer(buf))
|
||||
ctypes.set_errno(0)
|
||||
rv = libc.read(fd, ctypes.c_void_p(ptr), size)
|
||||
if rv < 0:
|
||||
err = ctypes.get_errno()
|
||||
buf.close()
|
||||
raise OSError(err, os.strerror(err))
|
||||
data = buf[:rv]
|
||||
buf.close()
|
||||
return data
|
||||
|
||||
data = mmap.mmap(-1, resource.getpagesize())
|
||||
|
||||
os.write(fd,data)
|
||||
def main():
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.lseek(fd,0,os.SEEK_SET)
|
||||
os.close(fd)
|
||||
|
||||
data = os.read(fd,resource.getpagesize())
|
||||
fd = os.open(filepath, os.O_RDWR|os.O_DIRECT|os.O_TRUNC)
|
||||
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
|
||||
size = resource.getpagesize()
|
||||
pattern = (b"mergerfs-o-direct" * ((size // 16) + 1))[:size]
|
||||
buf = mmap.mmap(-1, size)
|
||||
buf.write(pattern)
|
||||
buf.seek(0)
|
||||
|
||||
try:
|
||||
written = os.write(fd, buf)
|
||||
if written != size:
|
||||
print(f"O_DIRECT short write: wrote {written}, expected {size}", end="")
|
||||
return 1
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
data = aligned_read(fd, size)
|
||||
if len(data) != size:
|
||||
print(f"O_DIRECT short read: read {len(data)}, expected {size}", end="")
|
||||
return 1
|
||||
if data != pattern:
|
||||
print("O_DIRECT data mismatch", end="")
|
||||
return 1
|
||||
finally:
|
||||
buf.close()
|
||||
os.close(fd)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -24,40 +24,34 @@ def main():
|
|||
touch(merge_file, b"hello world")
|
||||
|
||||
fd = os.open(merge_file, os.O_RDWR)
|
||||
if fd < 0:
|
||||
return fail("open file for R/W failed")
|
||||
|
||||
data = os.read(fd, 4096)
|
||||
if data != b"hello world":
|
||||
try:
|
||||
data = os.read(fd, 4096)
|
||||
if data != b"hello world":
|
||||
return fail("read before unlink: expected 'hello world', got {!r}".format(data))
|
||||
|
||||
os.unlink(merge_file)
|
||||
|
||||
fd2 = os.dup(fd)
|
||||
try:
|
||||
os.lseek(fd2, 0, os.SEEK_SET)
|
||||
data2 = os.read(fd2, 4096)
|
||||
if data2 != b"hello world":
|
||||
return fail("read after unlink via dup: expected 'hello world', got {!r}".format(data2))
|
||||
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
written = os.write(fd, b"modified")
|
||||
if written != len(b"modified"):
|
||||
return fail("write after unlink: wrote {}, expected {}".format(written, len(b"modified")))
|
||||
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
data3 = os.read(fd, 4096)
|
||||
if not data3.startswith(b"modified"):
|
||||
return fail("read back written data: got {!r} after writing 'modified'".format(data3))
|
||||
finally:
|
||||
os.close(fd2)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return fail(f"read before unlink: expected 'hello world', got {data!r}")
|
||||
|
||||
os.unlink(merge_file)
|
||||
|
||||
fd2 = os.dup(fd)
|
||||
os.lseek(fd2, 0, os.SEEK_SET)
|
||||
data2 = os.read(fd2, 4096)
|
||||
if data2 != b"hello world":
|
||||
os.close(fd2)
|
||||
os.close(fd)
|
||||
return fail(f"read after unlink via dup: expected 'hello world', got {data2!r}")
|
||||
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
written = os.write(fd, b"modified")
|
||||
if written != len(b"modified"):
|
||||
os.close(fd2)
|
||||
os.close(fd)
|
||||
return fail(f"write after unlink: wrote {written}, expected {len(b'modified')}")
|
||||
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
data3 = os.read(fd, 4096)
|
||||
if not data3.startswith(b"modified"):
|
||||
os.close(fd2)
|
||||
os.close(fd)
|
||||
return fail(f"read back written data: got {data3!r} after writing 'modified'")
|
||||
|
||||
os.close(fd2)
|
||||
os.close(fd)
|
||||
|
||||
return 0
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from posix_parity import cleanup_dir
|
||||
from posix_parity import fail
|
||||
|
|
@ -14,6 +13,42 @@ from posix_parity import temp_dir
|
|||
from posix_parity import touch
|
||||
|
||||
|
||||
SKIP = 77
|
||||
|
||||
|
||||
def used_percent(path):
|
||||
st = os.statvfs(path)
|
||||
if st.f_blocks == 0:
|
||||
return 1.0
|
||||
used = st.f_blocks - st.f_bfree
|
||||
return used / st.f_blocks
|
||||
|
||||
|
||||
def branch_for_fullpath(branches, fullpath):
|
||||
fullpath = os.path.realpath(fullpath)
|
||||
for branch in branches:
|
||||
branch = os.path.realpath(branch)
|
||||
try:
|
||||
common = os.path.commonpath([branch, fullpath])
|
||||
except ValueError:
|
||||
continue
|
||||
if common == branch:
|
||||
return branch
|
||||
return None
|
||||
|
||||
|
||||
def least_used_branches(branches, rel_dir):
|
||||
scored = []
|
||||
for branch in branches:
|
||||
path = join(branch, rel_dir)
|
||||
if os.path.isdir(path):
|
||||
scored.append((used_percent(path), os.path.realpath(branch)))
|
||||
if not scored:
|
||||
return set()
|
||||
min_used = min(score for score, _ in scored)
|
||||
return {branch for score, branch in scored if score == min_used}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: TEST_policy_lup <mountpoint>", file=sys.stderr)
|
||||
|
|
@ -24,38 +59,53 @@ def main():
|
|||
try:
|
||||
orig_create = mergerfs_get_option(mount, "func.create")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
return 0
|
||||
print("LUP create policy option unavailable", end="")
|
||||
return SKIP
|
||||
|
||||
try:
|
||||
orig_search = mergerfs_get_option(mount, "func.search")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
return 0
|
||||
print("LUP search policy option unavailable", end="")
|
||||
return SKIP
|
||||
|
||||
try:
|
||||
orig_action = mergerfs_get_option(mount, "func.action")
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
return 0
|
||||
branches = mergerfs_branches(mount)
|
||||
if len(branches) < 2:
|
||||
print("LUP policy test requires at least 2 branches", end="")
|
||||
return SKIP
|
||||
|
||||
merge_base = temp_dir(mount)
|
||||
rel_base = os.path.relpath(merge_base, mount)
|
||||
|
||||
try:
|
||||
for branch in branches:
|
||||
os.makedirs(join(branch, rel_base), exist_ok=True)
|
||||
|
||||
mergerfs_set_option(mount, "func.create", "lup")
|
||||
mergerfs_set_option(mount, "func.search", "lup")
|
||||
|
||||
path = join(merge_base, "lup_test_file")
|
||||
expected = least_used_branches(branches, rel_base)
|
||||
touch(path, b"lup create test")
|
||||
if not os.path.exists(path):
|
||||
return fail("LUP create: file not created")
|
||||
|
||||
fullpath = os.getxattr(path, "user.mergerfs.fullpath")
|
||||
fullpath = os.getxattr(path, "user.mergerfs.fullpath").decode()
|
||||
if not fullpath:
|
||||
return fail("LUP create: no fullpath attribute")
|
||||
actual = branch_for_fullpath(branches, fullpath)
|
||||
if actual not in expected:
|
||||
return fail(f"LUP create: selected {actual}, expected one of {sorted(expected)}")
|
||||
|
||||
path2 = join(merge_base, "lup_test_file2")
|
||||
touch(path2, b"lup search test")
|
||||
fullpath2 = os.getxattr(path2, "user.mergerfs.fullpath")
|
||||
for branch in branches:
|
||||
touch(join(branch, rel_base, "lup_test_file2"), b"lup search test")
|
||||
expected = least_used_branches(branches, rel_base)
|
||||
fullpath2 = os.getxattr(path2, "user.mergerfs.fullpath").decode()
|
||||
if not fullpath2:
|
||||
return fail("LUP search: no fullpath attribute")
|
||||
actual = branch_for_fullpath(branches, fullpath2)
|
||||
if actual not in expected:
|
||||
return fail(f"LUP search: selected {actual}, expected one of {sorted(expected)}")
|
||||
|
||||
mergerfs_set_option(mount, "func.create", orig_create)
|
||||
mergerfs_set_option(mount, "func.search", orig_search)
|
||||
|
|
@ -67,7 +117,9 @@ def main():
|
|||
return fail(str(exc))
|
||||
finally:
|
||||
cleanup_dir(merge_base)
|
||||
for branch in branches:
|
||||
cleanup_dir(join(branch, rel_base))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ def main():
|
|||
if err:
|
||||
return fail(err)
|
||||
|
||||
os.chmod(merge_file, 0o644)
|
||||
os.chmod(native_file, 0o644)
|
||||
|
||||
return 0
|
||||
finally:
|
||||
cleanup_dir(merge_base)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ def main():
|
|||
|
||||
if os.geteuid() != 0:
|
||||
err = compare_calls(
|
||||
"chmod EPERM setuid",
|
||||
"chmod setuid parity",
|
||||
lambda: os.chmod(merge_file, stat.S_ISUID | 0o644),
|
||||
lambda: os.chmod(native_file, stat.S_ISUID | 0o644),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -79,7 +80,9 @@ def main():
|
|||
os.close(ndfd)
|
||||
|
||||
# Exercise very large copy lengths to validate the 64-bit copy result
|
||||
# path when available. Use sparse files to keep runtime and disk usage low.
|
||||
# path when available. Use sparse files to keep disk usage low.
|
||||
# NOTE: 5GB+ length may be slow on some filesystems; targets
|
||||
# ENOSPC/EFBIG early if space is insufficient.
|
||||
large_len = (5 << 30) + 4096
|
||||
merge_large_src = join(merge_base, "src-large")
|
||||
merge_large_dst = join(merge_base, "dst-large")
|
||||
|
|
@ -95,12 +98,15 @@ def main():
|
|||
nsfd = os.open(native_large_src, os.O_RDWR)
|
||||
ndfd = os.open(native_large_dst, os.O_RDWR)
|
||||
try:
|
||||
# Make sparse files with one non-zero byte at the end so both sides
|
||||
# have comparable logical contents.
|
||||
os.ftruncate(msfd, large_len)
|
||||
os.ftruncate(nsfd, large_len)
|
||||
os.pwrite(msfd, b"z", large_len - 1)
|
||||
os.pwrite(nsfd, b"z", large_len - 1)
|
||||
try:
|
||||
os.ftruncate(msfd, large_len)
|
||||
os.ftruncate(nsfd, large_len)
|
||||
os.pwrite(msfd, b"z", large_len - 1)
|
||||
os.pwrite(nsfd, b"z", large_len - 1)
|
||||
except OSError as exc:
|
||||
if exc.errno in (errno.ENOSPC, errno.EFBIG):
|
||||
return 0
|
||||
raise
|
||||
|
||||
err = compare_calls(
|
||||
"copy_file_range large sparse",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ def main():
|
|||
"create EEXIST",
|
||||
lambda: os.open(merge_exist, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640),
|
||||
lambda: os.open(native_exist, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640),
|
||||
close_fds=True,
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ def main():
|
|||
try:
|
||||
os.lseek(mfd, 0, os.SEEK_END)
|
||||
os.lseek(nfd, 0, os.SEEK_END)
|
||||
os.write(mfd, b"-merge")
|
||||
os.write(nfd, b"-native")
|
||||
os.write(mfd, b"-appended")
|
||||
os.write(nfd, b"-appended")
|
||||
|
||||
err = compare_calls("fsync file", lambda: os.fsync(mfd), lambda: os.fsync(nfd))
|
||||
if err:
|
||||
|
|
@ -72,8 +72,8 @@ def main():
|
|||
with open(merge_file, "rb") as mf, open(native_file, "rb") as nf:
|
||||
mdata = mf.read()
|
||||
ndata = nf.read()
|
||||
if not (mdata.startswith(b"abc") and ndata.startswith(b"abc")):
|
||||
return fail(f"flush content prefix mismatch mergerfs={mdata!r} native={ndata!r}")
|
||||
if mdata != ndata:
|
||||
return fail(f"flush content mismatch mergerfs={mdata!r} native={ndata!r}")
|
||||
|
||||
return 0
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@ from posix_parity import compare_calls
|
|||
from posix_parity import fail
|
||||
from posix_parity import join
|
||||
from posix_parity import should_compare_inode
|
||||
from posix_parity import stat_cmp_basic
|
||||
from posix_parity import temp_dir
|
||||
from posix_parity import touch
|
||||
|
||||
|
||||
def st_cmp(lhs, rhs):
|
||||
return (
|
||||
stat_cmp_basic(lhs, rhs)
|
||||
and stat.S_IFMT(lhs.st_mode) == stat.S_IFMT(rhs.st_mode)
|
||||
stat.S_IFMT(lhs.st_mode) == stat.S_IFMT(rhs.st_mode)
|
||||
and stat.S_IMODE(lhs.st_mode) == stat.S_IMODE(rhs.st_mode)
|
||||
and lhs.st_size == rhs.st_size
|
||||
and lhs.st_nlink == rhs.st_nlink
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import fcntl
|
||||
import errno
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
|
@ -19,7 +21,36 @@ def pack_flock(l_type, l_whence=0, l_start=0, l_len=0, l_pid=0):
|
|||
return struct.pack("hhqqi", l_type, l_whence, l_start, l_len, l_pid)
|
||||
|
||||
|
||||
def try_process_lock(path):
|
||||
fd = os.open(path, os.O_RDWR)
|
||||
try:
|
||||
try:
|
||||
fcntl.fcntl(fd, fcntl.F_SETLK, pack_flock(fcntl.F_WRLCK))
|
||||
return 0
|
||||
except OSError as exc:
|
||||
return exc.errno
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def child_lock_errno(path):
|
||||
proc = subprocess.run(
|
||||
[sys.executable, __file__, "--try-process-lock", path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(proc.stderr.strip() or f"child lock helper failed: {proc.returncode}")
|
||||
return int(proc.stdout.strip())
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) == 3 and sys.argv[1] == "--try-process-lock":
|
||||
print(try_process_lock(sys.argv[2]))
|
||||
return 0
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: TEST_posix_locking <mountpoint>", file=sys.stderr)
|
||||
return 1
|
||||
|
|
@ -74,9 +105,12 @@ def main():
|
|||
if err:
|
||||
return fail(err)
|
||||
|
||||
err = compare_calls("fcntl F_SETLK write lock second fd", lambda: fcntl.fcntl(mfd2, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd2, fcntl.F_SETLK, wrlk))
|
||||
if err:
|
||||
return fail(err)
|
||||
m_lock_errno = child_lock_errno(merge_file)
|
||||
n_lock_errno = child_lock_errno(native_file)
|
||||
if m_lock_errno != n_lock_errno:
|
||||
return fail(f"fcntl F_SETLK child contention errno mismatch mergerfs={m_lock_errno} native={n_lock_errno}")
|
||||
if m_lock_errno not in (errno.EACCES, errno.EAGAIN):
|
||||
return fail(f"fcntl F_SETLK child contention expected lock conflict got errno={m_lock_errno}")
|
||||
|
||||
err = compare_calls("fcntl F_SETLK unlock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, unlck), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, unlck))
|
||||
if err:
|
||||
|
|
@ -88,8 +122,8 @@ def main():
|
|||
os.close(bad_n)
|
||||
err = compare_calls(
|
||||
"fcntl EBADF",
|
||||
lambda: fcntl.fcntl(bad_m, fcntl.F_SETLK, wrlk),
|
||||
lambda: fcntl.fcntl(bad_n, fcntl.F_SETLK, wrlk),
|
||||
lambda: fcntl.fcntl(bad_m, fcntl.F_GETFD),
|
||||
lambda: fcntl.fcntl(bad_n, fcntl.F_GETFD),
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ from posix_parity import temp_dir
|
|||
def statvfs_cmp(lhs, rhs):
|
||||
return (
|
||||
lhs.f_namemax == rhs.f_namemax
|
||||
and lhs.f_bsize == rhs.f_bsize
|
||||
and lhs.f_frsize == rhs.f_frsize
|
||||
and lhs.f_bsize > 0
|
||||
and rhs.f_bsize > 0
|
||||
and lhs.f_frsize > 0
|
||||
and rhs.f_frsize > 0
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ def open_tmpfile(dirpath):
|
|||
|
||||
def errno_of(call):
|
||||
try:
|
||||
call()
|
||||
fd = call()
|
||||
os.close(fd)
|
||||
return 0
|
||||
except OSError as exc:
|
||||
return exc.errno
|
||||
|
|
@ -44,6 +45,8 @@ def main():
|
|||
|
||||
m_err = errno_of(lambda: open_tmpfile(merge_dir))
|
||||
n_err = errno_of(lambda: open_tmpfile(native_dir))
|
||||
# mergerfs does not currently support O_TMPFILE, so EOPNOTSUPP is
|
||||
# the expected result even when the native filesystem supports it.
|
||||
if m_err == errno.EOPNOTSUPP:
|
||||
return 0
|
||||
if m_err != n_err:
|
||||
|
|
@ -54,7 +57,11 @@ def main():
|
|||
return 0
|
||||
|
||||
mfd = open_tmpfile(merge_dir)
|
||||
nfd = open_tmpfile(native_dir)
|
||||
try:
|
||||
nfd = open_tmpfile(native_dir)
|
||||
except OSError:
|
||||
os.close(mfd)
|
||||
return fail("O_TMPFILE: native open failed after merge succeeded")
|
||||
try:
|
||||
mw = os.write(mfd, b"tmpfile-data")
|
||||
nw = os.write(nfd, b"tmpfile-data")
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def main():
|
|||
return 1
|
||||
|
||||
mount = sys.argv[1]
|
||||
xname = "user.mergerfs.posix"
|
||||
xname = "user.posix_parity"
|
||||
xvalue = b"parity-check"
|
||||
|
||||
with tempfile.TemporaryDirectory() as native:
|
||||
|
|
|
|||
|
|
@ -63,6 +63,30 @@ def list_has_cmp(path_m, path_n, xname):
|
|||
)
|
||||
|
||||
|
||||
def lsetxattr(path, name, value):
|
||||
if hasattr(os, "lsetxattr"):
|
||||
return os.lsetxattr(path, name, value)
|
||||
return os.setxattr(path, name, value, follow_symlinks=False)
|
||||
|
||||
|
||||
def lgetxattr(path, name):
|
||||
if hasattr(os, "lgetxattr"):
|
||||
return os.lgetxattr(path, name)
|
||||
return os.getxattr(path, name, follow_symlinks=False)
|
||||
|
||||
|
||||
def llistxattr(path):
|
||||
if hasattr(os, "llistxattr"):
|
||||
return os.llistxattr(path)
|
||||
return os.listxattr(path, follow_symlinks=False)
|
||||
|
||||
|
||||
def lremovexattr(path, name):
|
||||
if hasattr(os, "lremovexattr"):
|
||||
return os.lremovexattr(path, name)
|
||||
return os.removexattr(path, name, follow_symlinks=False)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: TEST_posix_xattr_matrix <mountpoint>", file=sys.stderr)
|
||||
|
|
@ -106,7 +130,7 @@ def main():
|
|||
("dir", dir_m, dir_n),
|
||||
]
|
||||
|
||||
xbase = "user.mergerfs.matrix"
|
||||
xbase = "user.xattr_matrix"
|
||||
|
||||
for obj_name, path_m, path_n in objs:
|
||||
xname = f"{xbase}.{obj_name}"
|
||||
|
|
@ -176,40 +200,39 @@ def main():
|
|||
return fail(err)
|
||||
|
||||
lxname = f"{xbase}.link"
|
||||
if all(hasattr(os, fn) for fn in ("lsetxattr", "lgetxattr", "llistxattr", "lremovexattr")):
|
||||
err = compare(
|
||||
"lsetxattr symlink default",
|
||||
lambda: os.lsetxattr(link_m, lxname, b"lv1"),
|
||||
lambda: os.lsetxattr(link_n, lxname, b"lv1"),
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
err = compare(
|
||||
"lsetxattr symlink default",
|
||||
lambda: lsetxattr(link_m, lxname, b"lv1"),
|
||||
lambda: lsetxattr(link_n, lxname, b"lv1"),
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
|
||||
err = compare(
|
||||
"lgetxattr symlink",
|
||||
lambda: os.lgetxattr(link_m, lxname),
|
||||
lambda: os.lgetxattr(link_n, lxname),
|
||||
lambda a, b: a == b,
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
err = compare(
|
||||
"lgetxattr symlink",
|
||||
lambda: lgetxattr(link_m, lxname),
|
||||
lambda: lgetxattr(link_n, lxname),
|
||||
lambda a, b: a == b,
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
|
||||
err = compare(
|
||||
"llistxattr has symlink key",
|
||||
lambda: os.llistxattr(link_m),
|
||||
lambda: os.llistxattr(link_n),
|
||||
lambda a, b: ((lxname in a) == (lxname in b)),
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
err = compare(
|
||||
"llistxattr has symlink key",
|
||||
lambda: llistxattr(link_m),
|
||||
lambda: llistxattr(link_n),
|
||||
lambda a, b: ((lxname in a) == (lxname in b)),
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
|
||||
err = compare(
|
||||
"lremovexattr symlink",
|
||||
lambda: os.lremovexattr(link_m, lxname),
|
||||
lambda: os.lremovexattr(link_n, lxname),
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
err = compare(
|
||||
"lremovexattr symlink",
|
||||
lambda: lremovexattr(link_m, lxname),
|
||||
lambda: lremovexattr(link_n, lxname),
|
||||
)
|
||||
if err:
|
||||
return fail(err)
|
||||
|
||||
return 0
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -187,7 +187,6 @@ def main():
|
|||
|
||||
return 0
|
||||
finally:
|
||||
cleanup_dir(merge_base)
|
||||
try:
|
||||
os.chmod(paths["merge_private_dir"], 0o700)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
|
|
@ -198,30 +197,7 @@ def main():
|
|||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
|
||||
for p in (
|
||||
paths["merge_private_link"],
|
||||
paths["native_private_link"],
|
||||
paths["merge_link"],
|
||||
paths["native_link"],
|
||||
paths["merge_regular"],
|
||||
paths["native_regular"],
|
||||
paths["merge_notdir"],
|
||||
paths["native_notdir"],
|
||||
paths["merge_loop"],
|
||||
paths["native_loop"],
|
||||
):
|
||||
try:
|
||||
os.unlink(p)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
for p in (paths["merge_private_dir"], paths["native_private_dir"]):
|
||||
try:
|
||||
os.rmdir(p)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
cleanup_dir(merge_base)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -4,17 +4,38 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
(srcfd,src) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
(tgtfd,tgt) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.fstat(srcfd)
|
||||
os.fstat(tgtfd)
|
||||
def main():
|
||||
(srcfd, src) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
(tgtfd, tgt) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(tgt)
|
||||
os.rename(src,tgt)
|
||||
st_src_before = os.fstat(srcfd)
|
||||
st_tgt_before = os.fstat(tgtfd)
|
||||
|
||||
os.fstat(srcfd)
|
||||
os.fstat(tgtfd)
|
||||
os.unlink(tgt)
|
||||
os.rename(src, tgt)
|
||||
|
||||
os.close(srcfd)
|
||||
os.close(tgtfd)
|
||||
st_src_after = os.fstat(srcfd)
|
||||
st_tgt_after = os.fstat(tgtfd)
|
||||
|
||||
if st_src_after.st_size != st_src_before.st_size:
|
||||
os.close(srcfd)
|
||||
os.close(tgtfd)
|
||||
print("rename: src fd size changed before={} after={}".format(
|
||||
st_src_before.st_size, st_src_after.st_size), end='')
|
||||
return 1
|
||||
|
||||
if st_tgt_after.st_ino != st_tgt_before.st_ino:
|
||||
os.close(srcfd)
|
||||
os.close(tgtfd)
|
||||
print("rename: tgt fd inode changed after unlink+rename expected={} got={}".format(
|
||||
st_tgt_before.st_ino, st_tgt_after.st_ino), end='')
|
||||
return 1
|
||||
|
||||
os.close(srcfd)
|
||||
os.close(tgtfd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,12 +4,32 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.posix_fallocate(fd,0,1024)
|
||||
def main():
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(filepath)
|
||||
os.posix_fallocate(fd, 0, 1024)
|
||||
|
||||
os.posix_fallocate(fd,0,1024)
|
||||
st1 = os.fstat(fd)
|
||||
if st1.st_size < 1024:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
print("fallocate before unlink: expected size >= 1024, got {}".format(st1.st_size), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
|
||||
os.posix_fallocate(fd, 1024, 2048)
|
||||
|
||||
st2 = os.fstat(fd)
|
||||
if st2.st_size < 1024 + 2048:
|
||||
os.close(fd)
|
||||
print("fallocate after unlink: expected size >= 3072, got {}".format(st2.st_size), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,15 +1,36 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.fchmod(fd,0o777)
|
||||
def main():
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(filepath)
|
||||
os.fchmod(fd, 0o700)
|
||||
|
||||
os.fchmod(fd,0o777)
|
||||
st1 = os.fstat(fd)
|
||||
if stat.S_IMODE(st1.st_mode) != 0o700:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
print("fchmod before unlink: expected mode 0o700, got 0o{:o}".format(stat.S_IMODE(st1.st_mode)), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
|
||||
os.fchmod(fd, 0o755)
|
||||
|
||||
st2 = os.fstat(fd)
|
||||
if stat.S_IMODE(st2.st_mode) != 0o755:
|
||||
os.close(fd)
|
||||
print("fchmod after unlink: expected mode 0o755, got 0o{:o}".format(stat.S_IMODE(st2.st_mode)), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,12 +4,34 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.fchown(fd,os.getuid(),os.getgid())
|
||||
def main():
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(filepath)
|
||||
os.fchown(fd, os.getuid(), os.getgid())
|
||||
|
||||
os.fchown(fd,os.getuid(),os.getgid())
|
||||
st1 = os.fstat(fd)
|
||||
if st1.st_uid != os.getuid() or st1.st_gid != os.getgid():
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
print("fchown before unlink: expected uid={} gid={}, got uid={} gid={}".format(
|
||||
os.getuid(), os.getgid(), st1.st_uid, st1.st_gid), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
|
||||
os.fchown(fd, os.getuid(), os.getgid())
|
||||
|
||||
st2 = os.fstat(fd)
|
||||
if st2.st_uid != os.getuid() or st2.st_gid != os.getgid():
|
||||
os.close(fd)
|
||||
print("fchown after unlink: expected uid={} gid={}, got uid={} gid={}".format(
|
||||
os.getuid(), os.getgid(), st2.st_uid, st2.st_gid), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,13 +4,42 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
x = os.lstat(filepath)
|
||||
x = os.fstat(fd)
|
||||
def main():
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(filepath)
|
||||
x_before = os.lstat(filepath)
|
||||
x = os.fstat(fd)
|
||||
|
||||
y = os.fstat(fd)
|
||||
if x.st_size != x_before.st_size:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
print("fstat before unlink: size mismatch lstat={} fstat={}".format(x_before.st_size, x.st_size), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
if x.st_mode != x_before.st_mode:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
print("fstat before unlink: mode mismatch lstat={} fstat={}".format(x_before.st_mode, x.st_mode), end='')
|
||||
return 1
|
||||
|
||||
os.unlink(filepath)
|
||||
|
||||
y = os.fstat(fd)
|
||||
|
||||
if y.st_nlink != 0:
|
||||
os.close(fd)
|
||||
print("fstat after unlink: expected nlink=0, got {}".format(y.st_nlink), end='')
|
||||
return 1
|
||||
|
||||
if y.st_size != x.st_size:
|
||||
os.close(fd)
|
||||
print("fstat after unlink: size changed from {} to {}".format(x.st_size, y.st_size), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,12 +4,32 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.ftruncate(fd,1024)
|
||||
def main():
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(filepath)
|
||||
os.ftruncate(fd, 1024)
|
||||
|
||||
os.ftruncate(fd,1024)
|
||||
st1 = os.fstat(fd)
|
||||
if st1.st_size != 1024:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
print("ftruncate before unlink: expected size 1024, got {}".format(st1.st_size), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
|
||||
os.ftruncate(fd, 2048)
|
||||
|
||||
st2 = os.fstat(fd)
|
||||
if st2.st_size != 2048:
|
||||
os.close(fd)
|
||||
print("ftruncate after unlink: expected size 2048, got {}".format(st2.st_size), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,14 +4,38 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
atime = 1234
|
||||
mtime = 1234
|
||||
(fd,filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.utime(fd,(atime,mtime))
|
||||
def main():
|
||||
atime = 1234
|
||||
mtime = 5678
|
||||
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
|
||||
|
||||
os.unlink(filepath)
|
||||
os.utime(fd, (atime, mtime))
|
||||
|
||||
os.utime(fd,(atime,mtime))
|
||||
st1 = os.fstat(fd)
|
||||
if st1.st_atime != atime or st1.st_mtime != mtime:
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
print("futimens before unlink: expected atime={} mtime={}, got atime={} mtime={}".format(
|
||||
atime, mtime, st1.st_atime, st1.st_mtime), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
os.unlink(filepath)
|
||||
|
||||
atime2 = 9999
|
||||
mtime2 = 8888
|
||||
os.utime(fd, (atime2, mtime2))
|
||||
|
||||
st2 = os.fstat(fd)
|
||||
if st2.st_atime != atime2 or st2.st_mtime != mtime2:
|
||||
os.close(fd)
|
||||
print("futimens after unlink: expected atime={} mtime={}, got atime={} mtime={}".format(
|
||||
atime2, mtime2, st2.st_atime, st2.st_mtime), end='')
|
||||
return 1
|
||||
|
||||
os.close(fd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -38,6 +38,17 @@ def compare_calls(name, merge_call, native_call, value_cmp=None, close_fds=False
|
|||
m_ok, m_val, m_errno = invoke(merge_call)
|
||||
n_ok, n_val, n_errno = invoke(native_call)
|
||||
|
||||
if close_fds:
|
||||
# WARNING: close_fds=True should only be used when the callable
|
||||
# returns an fd (non-negative int from os.open etc.), NOT when it
|
||||
# returns arbitrary integers (e.g., byte counts, offsets).
|
||||
try:
|
||||
if m_ok:
|
||||
close_if_fd(m_val)
|
||||
finally:
|
||||
if n_ok:
|
||||
close_if_fd(n_val)
|
||||
|
||||
if m_ok != n_ok:
|
||||
return (
|
||||
f"{name}: success mismatch mergerfs={m_ok} native={n_ok} "
|
||||
|
|
@ -46,15 +57,8 @@ def compare_calls(name, merge_call, native_call, value_cmp=None, close_fds=False
|
|||
if m_errno != n_errno:
|
||||
return f"{name}: errno mismatch mergerfs={m_errno} native={n_errno}"
|
||||
if m_ok and value_cmp is not None and not value_cmp(m_val, n_val):
|
||||
if close_fds:
|
||||
close_if_fd(m_val)
|
||||
close_if_fd(n_val)
|
||||
return f"{name}: value mismatch mergerfs={m_val!r} native={n_val!r}"
|
||||
|
||||
if close_fds:
|
||||
close_if_fd(m_val)
|
||||
close_if_fd(n_val)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,27 +5,42 @@ import sys
|
|||
import subprocess
|
||||
|
||||
|
||||
SKIP = 77
|
||||
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print('usage: run-test <mergerfs-path>\n',file=sys.stderr)
|
||||
print('usage: run-tests <mountpoint>\n', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
test_path = os.path.realpath(sys.argv[0])
|
||||
test_path = os.path.dirname(test_path)
|
||||
|
||||
failed = False
|
||||
|
||||
for entry in os.scandir(test_path):
|
||||
if not entry.name.startswith('TEST_'):
|
||||
continue
|
||||
|
||||
try:
|
||||
print(entry.name + ': ',end='')
|
||||
fullpath = os.path.join(test_path,entry.name)
|
||||
args = [fullpath,sys.argv[1]]
|
||||
rv = subprocess.Popen(args,stdout=subprocess.PIPE)
|
||||
rv.wait(timeout=10000)
|
||||
if rv.returncode:
|
||||
output = rv.stdout.read().decode()
|
||||
print('FAIL - {}'.format(output))
|
||||
print(entry.name + ': ', end='')
|
||||
fullpath = os.path.join(test_path, entry.name)
|
||||
args = [fullpath, sys.argv[1]]
|
||||
rv = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output, _ = rv.communicate(timeout=120)
|
||||
if rv.returncode == SKIP:
|
||||
print('SKIP - {}'.format(output.decode(errors='replace')))
|
||||
elif rv.returncode:
|
||||
print('FAIL - {}'.format(output.decode(errors='replace')))
|
||||
failed = True
|
||||
else:
|
||||
print('PASS')
|
||||
except subprocess.TimeoutExpired:
|
||||
rv.kill()
|
||||
rv.wait()
|
||||
print('FAIL - timeout')
|
||||
failed = True
|
||||
except Exception as e:
|
||||
print('FAIL - {}'.format(e))
|
||||
failed = True
|
||||
|
||||
sys.exit(1 if failed else 0)
|
||||
|
|
|
|||
|
|
@ -1227,7 +1227,7 @@ test_branches_global_minfreespace_live_tracking()
|
|||
{
|
||||
Branches b;
|
||||
|
||||
b.from_string("/tmp/a");
|
||||
TEST_CHECK(b.from_string("/tmp/a") == 0);
|
||||
|
||||
// Branch uses pointer to Branches::minfreespace — tracks it live
|
||||
uint64_t initial = b.minfreespace;
|
||||
|
|
@ -1408,6 +1408,12 @@ test_config_nfsopenhack()
|
|||
void
|
||||
test_config_readdir()
|
||||
{
|
||||
Config cfg;
|
||||
|
||||
TEST_CHECK(cfg.has_key("func.readdir") == true);
|
||||
|
||||
const auto &map = cfg.get_map();
|
||||
TEST_CHECK(map.count("func.readdir") > 0);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -3470,7 +3476,7 @@ test_rnd_rand64_basic()
|
|||
{
|
||||
u64 v1 = RND::rand64();
|
||||
u64 v2 = RND::rand64();
|
||||
TEST_CHECK(v1 != v2 || true);
|
||||
TEST_CHECK(v1 != v2);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue