make all integration tests self-contained with ephemeral mounts

- Add mergerfs_mount() context manager in posix_parity.py that creates
  temporary branch dirs in tests/.test_tmp/, mounts an ephemeral mergerfs
  instance, and cleans up after use.
- Add find_mergerfs(), find_fusermount(), mount_mergerfs(), unmount_mergerfs()
  helpers.
- Update run-tests to invoke each TEST_* script without a mountpoint arg.
- Update README.md to document the new `python3 tests/run-tests` invocation.
- Fix 44 tests to use mergerfs_mount() instead of expecting a pre-mounted path.
- Add tempfile import to 10 tests that were missing it.
- Fix TEST_mount_lifecycle to use mergerfs_mount() context manager.
- Fix TEST_posix_tmpfile missing join import.
- Fix TEST_cfg_xattr_modes error message framing (no "(known bug)").
- Fix TEST_posix_copy_file_range: loop-copy full size, compare st_size and
  SHA256 hash; add NativeTmp for space management; route native temp to
  tests/.test_tmp/ to avoid ENOSPC on /tmp.
- Fix TEST_posix_getattr_fgetattr: st_cmp skips st_size for dirs; add
  compare_ino param for safe inode comparison when inodecalc=passthrough.
- Fix TEST_unlink_rename, TEST_use_* tests: namespace cleanup, consistent
  variable naming, None check for cmpfunc.
- Fix TEST_readlink_semantics: remove type annotations to support older
  Python; wrap in mergerfs_mount() try/except.
- Add flush=True to run-tests print so test names show immediately.
This commit is contained in:
Antonio SJ Musumeci
2026-05-07 21:47:40 -04:00
committed by trapexit
parent 6d6d73c7b3
commit 84e4cdeb7e
51 changed files with 2660 additions and 2536 deletions
+7 -1
View File
@@ -6,10 +6,13 @@ and compare behavior against equivalent native syscalls on a local filesystem.
The suite is "soft POSIX": it focuses on practical parity for return values,
`errno`, and key side effects, including common error paths.
Each test mounts its own ephemeral mergerfs instance with random temporary
branches and cleans everything up automatically.
Run with:
```bash
python3 tests/run-tests /mnt/mergerfs/
python3 tests/run-tests
```
## Coverage Matrix
@@ -69,3 +72,6 @@ Across tests, parity checks include combinations of:
- 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.
- Tests mount their own ephemeral mergerfs instances using the `mergerfs_mount`
context manager in `posix_parity.py`. Random directories are created and
cleaned up automatically.
+50 -54
View File
@@ -2,80 +2,76 @@
import os
import sys
import tempfile
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_mount
from posix_parity import mergerfs_set_option
from posix_parity import temp_dir
from posix_parity import touch
from posix_parity import mergerfs_get_option
from posix_parity import mergerfs_set_option
from posix_parity import mergerfs_branches
def main():
if len(sys.argv) != 2:
print("usage: TEST_api_xattr <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
ctrl = join(mount, ".mergerfs")
if not os.path.exists(ctrl):
return fail(".mergerfs control file not found")
with mergerfs_mount() as (mount, branches):
ctrl = join(mount, ".mergerfs")
if not os.path.exists(ctrl):
return fail(".mergerfs control file not found")
branches = mergerfs_branches(mount)
if not branches:
return fail("no branches found")
merge_base = temp_dir(mount)
try:
merge_file = join(merge_base, "api_test")
touch(merge_file, b"test")
if not branches:
return fail("no branches found")
merge_base = temp_dir(mount)
try:
fullpath = os.getxattr(merge_file, "user.mergerfs.fullpath")
fullpath_str = fullpath.decode("utf-8", errors="surrogateescape")
if not fullpath_str:
return fail("fullpath xattr is empty")
if not os.path.exists(fullpath_str):
return fail(f"fullpath '{fullpath_str}' does not exist on disk")
except OSError as exc:
return fail(f"getxattr fullpath: errno={exc.errno}")
merge_file = join(merge_base, "api_test")
touch(merge_file, b"test")
try:
relpath = os.getxattr(merge_file, "user.mergerfs.relpath")
relpath_str = relpath.decode("utf-8", errors="surrogateescape")
if not relpath_str:
return fail("relpath xattr is empty")
except OSError as exc:
return fail(f"getxattr relpath: errno={exc.errno}")
try:
fullpath = os.getxattr(merge_file, "user.mergerfs.fullpath")
fullpath_str = fullpath.decode("utf-8", errors="surrogateescape")
if not fullpath_str:
return fail("fullpath xattr is empty")
if not os.path.exists(fullpath_str):
return fail(f"fullpath '{fullpath_str}' does not exist on disk")
except OSError as exc:
return fail(f"getxattr fullpath: errno={exc.errno}")
try:
basepath = os.getxattr(merge_file, "user.mergerfs.basepath")
basepath_str = basepath.decode("utf-8", errors="surrogateescape")
if not basepath_str:
return fail("basepath xattr is empty")
except OSError as exc:
return fail(f"getxattr basepath: errno={exc.errno}")
try:
relpath = os.getxattr(merge_file, "user.mergerfs.relpath")
relpath_str = relpath.decode("utf-8", errors="surrogateescape")
if not relpath_str:
return fail("relpath xattr is empty")
except OSError as exc:
return fail(f"getxattr relpath: errno={exc.errno}")
try:
allpaths = os.getxattr(merge_file, "user.mergerfs.allpaths")
allpaths_str = allpaths.decode("utf-8", errors="surrogateescape")
if not allpaths_str:
return fail("allpaths xattr is empty")
except OSError as exc:
return fail(f"getxattr allpaths: errno={exc.errno}")
try:
basepath = os.getxattr(merge_file, "user.mergerfs.basepath")
basepath_str = basepath.decode("utf-8", errors="surrogateescape")
if not basepath_str:
return fail("basepath xattr is empty")
except OSError as exc:
return fail(f"getxattr basepath: errno={exc.errno}")
return 0
finally:
cleanup_dir(merge_base)
try:
allpaths = os.getxattr(merge_file, "user.mergerfs.allpaths")
allpaths_str = allpaths.decode("utf-8", errors="surrogateescape")
if not allpaths_str:
return fail("allpaths xattr is empty")
except OSError as exc:
return fail(f"getxattr allpaths: errno={exc.errno}")
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
except Exception as exc:
return fail(str(exc))
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+82 -85
View File
@@ -10,6 +10,7 @@ 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_mount
from posix_parity import mergerfs_set_option
from posix_parity import touch
@@ -56,96 +57,92 @@ def setup_paths(branches, mount, rel_base, name):
def main():
if len(sys.argv) != 2:
print("usage: TEST_cfg_link_rename_exdev <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
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:
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):
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 != errno.EXDEV:
print(f"link-exdev passthrough did not exercise EXDEV, errno={err}", end="")
with mergerfs_mount(num_branches=2) as (mount, branches):
rel_base = f".mergerfs_exdev_test_{os.getpid()}_{uuid.uuid4().hex}"
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
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, 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 + ".ren.pass"))
if err != errno.EXDEV:
print(f"rename-exdev passthrough did not exercise EXDEV, errno={err}", end="")
return SKIP
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 SKIP
finally:
try:
mergerfs_set_option(mount, "category.create", orig_create)
mergerfs_set_option(mount, "link-exdev", orig_link)
mergerfs_set_option(mount, "rename-exdev", orig_rename)
except OSError:
pass
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):
print("EXDEV fallback runtime options unavailable", end="")
return SKIP
return 0
finally:
for branch in branches:
cleanup_dir(join(branch, rel_base))
try:
mergerfs_set_option(mount, "category.create", "epmfs")
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 != errno.EXDEV:
print(f"link-exdev passthrough did not exercise EXDEV, errno={err}", end="")
return SKIP
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}")
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 + ".ren.pass"))
if err != errno.EXDEV:
print(f"rename-exdev passthrough did not exercise EXDEV, errno={err}", end="")
return SKIP
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 SKIP
finally:
try:
mergerfs_set_option(mount, "category.create", orig_create)
mergerfs_set_option(mount, "link-exdev", orig_link)
mergerfs_set_option(mount, "rename-exdev", orig_rename)
except OSError:
pass
return 0
finally:
for branch in branches:
cleanup_dir(join(branch, rel_base))
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+41 -42
View File
@@ -5,60 +5,59 @@ import sys
from posix_parity import fail
from posix_parity import mergerfs_get_option
from posix_parity import mergerfs_mount
from posix_parity import mergerfs_set_option
def main():
if len(sys.argv) != 2:
print("usage: TEST_cfg_statfs_ignore <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
orig_statfs = mergerfs_get_option(mount, "statfs")
orig_ignore = mergerfs_get_option(mount, "statfs-ignore")
except (PermissionError, FileNotFoundError, OSError):
return 0
with mergerfs_mount() as (mount, _):
try:
orig_statfs = mergerfs_get_option(mount, "statfs")
orig_ignore = mergerfs_get_option(mount, "statfs-ignore")
except (PermissionError, FileNotFoundError, OSError):
return 0
try:
mergerfs_set_option(mount, "statfs", "base")
try:
mergerfs_set_option(mount, "statfs", "base")
mergerfs_set_option(mount, "statfs-ignore", "none")
s_none = os.statvfs(mount)
mergerfs_set_option(mount, "statfs-ignore", "none")
s_none = os.statvfs(mount)
mergerfs_set_option(mount, "statfs-ignore", "nc")
s_nc = os.statvfs(mount)
mergerfs_set_option(mount, "statfs-ignore", "nc")
s_nc = os.statvfs(mount)
mergerfs_set_option(mount, "statfs-ignore", "ro")
s_ro = os.statvfs(mount)
mergerfs_set_option(mount, "statfs-ignore", "ro")
s_ro = os.statvfs(mount)
# Ignoring additional branch classes should never increase available blocks.
if s_nc.f_bavail > s_none.f_bavail:
return fail(
f"statfs-ignore=nc increased bavail unexpectedly nc={s_nc.f_bavail} none={s_none.f_bavail}"
)
if s_ro.f_bavail > s_nc.f_bavail:
return fail(
f"statfs-ignore=ro increased bavail unexpectedly ro={s_ro.f_bavail} nc={s_nc.f_bavail}"
)
if s_nc.f_bavail > s_none.f_bavail:
return fail(
f"statfs-ignore=nc increased bavail unexpectedly nc={s_nc.f_bavail} none={s_none.f_bavail}"
)
if s_ro.f_bavail > s_nc.f_bavail:
return fail(
f"statfs-ignore=ro increased bavail unexpectedly ro={s_ro.f_bavail} nc={s_nc.f_bavail}"
)
mergerfs_set_option(mount, "statfs", "full")
s_full = os.statvfs(mount)
if s_full.f_bsize <= 0 or s_full.f_frsize <= 0:
return fail(
f"statfs=full invalid block sizes bsize={s_full.f_bsize} frsize={s_full.f_frsize}"
)
except (PermissionError, FileNotFoundError, OSError):
return 0
finally:
try:
mergerfs_set_option(mount, "statfs", orig_statfs)
mergerfs_set_option(mount, "statfs-ignore", orig_ignore)
except OSError:
pass
mergerfs_set_option(mount, "statfs", "full")
s_full = os.statvfs(mount)
if s_full.f_bsize <= 0 or s_full.f_frsize <= 0:
return fail(
f"statfs=full invalid block sizes bsize={s_full.f_bsize} frsize={s_full.f_frsize}"
)
except (PermissionError, FileNotFoundError, OSError):
return 0
finally:
try:
mergerfs_set_option(mount, "statfs", orig_statfs)
mergerfs_set_option(mount, "statfs-ignore", orig_ignore)
except OSError:
pass
return 0
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+45 -48
View File
@@ -9,6 +9,7 @@ from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_get_option
from posix_parity import mergerfs_mount
from posix_parity import mergerfs_set_option
from posix_parity import temp_dir
from posix_parity import touch
@@ -23,58 +24,54 @@ def get_errno(func):
def main():
if len(sys.argv) != 2:
print("usage: TEST_cfg_xattr_modes <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
merge_base = temp_dir(mount)
try:
path = join(merge_base, "file")
xname = "user.mergerfs.cfg.test"
xval = b"v"
with mergerfs_mount() as (mount, _):
merge_base = temp_dir(mount)
touch(path, b"x")
try:
orig = mergerfs_get_option(mount, "xattr")
except (PermissionError, FileNotFoundError):
return 0
try:
mergerfs_set_option(mount, "xattr", "passthrough")
set_err = get_errno(lambda: os.setxattr(path, xname, xval))
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}")
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")
err = get_errno(lambda: os.getxattr(path, xname))
if err != errno.ENODATA:
return fail(f"xattr=noattr getxattr expected ENODATA got errno={err}")
err = get_errno(lambda: os.setxattr(path, xname, xval))
if err != errno.ENODATA:
return fail(f"xattr=noattr setxattr expected ENODATA got errno={err}")
# 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:
mergerfs_set_option(mount, "xattr", orig)
except OSError:
pass
path = join(merge_base, "file")
xname = "user.test"
xval = b"v"
return 0
finally:
cleanup_dir(merge_base)
touch(path, b"x")
try:
orig = mergerfs_get_option(mount, "xattr")
except (PermissionError, FileNotFoundError):
return 0
try:
mergerfs_set_option(mount, "xattr", "passthrough")
set_err = get_errno(lambda: os.setxattr(path, xname, xval))
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}")
if get_err == errno.ENODATA:
return fail(f"xattr=passthrough getxattr returned ENODATA, expected success")
if get_err != 0:
return fail(f"xattr=passthrough getxattr unexpected errno={get_err}")
mergerfs_set_option(mount, "xattr", "noattr")
err = get_errno(lambda: os.getxattr(path, xname))
if err != errno.ENODATA:
return fail(f"xattr=noattr getxattr expected ENODATA got errno={err}")
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):
return 0
finally:
try:
mergerfs_set_option(mount, "xattr", orig)
except OSError:
pass
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+79 -77
View File
@@ -1,102 +1,104 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
def main():
if len(sys.argv) != 2:
print("usage: TEST_io_passthrough_create_open <mountpoint>", file=sys.stderr)
return 1
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
test_data = b"passthrough io test data for create\n" * 100
from posix_parity import mergerfs_mount
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
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
try:
test_data = b"passthrough io test data for create\n" * 100
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
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
os.close(fd)
fd = -1
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
fd = os.open(filepath, os.O_RDWR)
os.close(fd)
fd = -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
fd = 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
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
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
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
fd2 = os.open(filepath, os.O_RDWR)
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
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
fd2 = os.open(filepath, os.O_RDWR)
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
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
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)
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
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.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)
os.close(fd)
fd = -1
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)
return 0
finally:
if fd >= 0:
os.close(fd)
try:
os.unlink(filepath)
except FileNotFoundError:
pass
os.close(fd)
fd = -1
return 0
finally:
if fd >= 0:
os.close(fd)
try:
os.unlink(filepath)
except FileNotFoundError:
pass
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+77 -74
View File
@@ -5,6 +5,8 @@ import sys
import tempfile
import threading
from posix_parity import mergerfs_mount
NUM_THREADS = 50
TEST_DATA = b"race condition test data\n"
@@ -46,87 +48,88 @@ def open_and_operate(filepath, barrier, results, index):
def main():
if len(sys.argv) != 2:
print("usage: TEST_io_passthrough_open_race <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
total_size = NUM_THREADS * len(TEST_DATA)
os.ftruncate(fd, total_size)
os.close(fd)
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:
total_size = NUM_THREADS * len(TEST_DATA)
os.ftruncate(fd, total_size)
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)
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
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
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+14 -50
View File
@@ -1,70 +1,34 @@
#!/usr/bin/env python3
import os
import shutil
import subprocess
import sys
import tempfile
import time
from posix_parity import fail
def has_tool(name):
return shutil.which(name) is not None
from posix_parity import find_fusermount
from posix_parity import find_mergerfs
from posix_parity import mergerfs_mount
def main():
if len(sys.argv) != 2:
print("usage: TEST_mount_lifecycle <mountpoint>", file=sys.stderr)
return 1
# This is a soft integration harness check for init/destroy wiring.
# Skip when environment is not prepared for nested mount tests.
if os.geteuid() != 0:
return 0
if not has_tool("mergerfs"):
if find_mergerfs() is None:
return 0
if not has_tool("fusermount") and not has_tool("fusermount3"):
if find_fusermount() is None:
return 0
with tempfile.TemporaryDirectory() as td:
b1 = os.path.join(td, "b1")
b2 = os.path.join(td, "b2")
mp = os.path.join(td, "mp")
os.makedirs(b1, exist_ok=True)
os.makedirs(b2, exist_ok=True)
os.makedirs(mp, exist_ok=True)
cmd = [
"mergerfs",
f"{b1}:{b2}",
mp,
"-o",
"defaults,allow_other,use_ino,category.create=mfs",
]
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if p.returncode != 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:
try:
with mergerfs_mount() as (mount, _):
with open(os.path.join(mount, "lifecycle-file"), "wb") as fp:
fp.write(b"ok")
if not os.path.exists(os.path.join(mp, "lifecycle-file")):
if not os.path.exists(os.path.join(mount, "lifecycle-file")):
return fail("mount lifecycle: expected created file to exist")
finally:
um = shutil.which("fusermount3") or shutil.which("fusermount")
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:
subprocess.run([um, "-z", mp], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return 0
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+49 -52
View File
@@ -9,6 +9,7 @@ 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_mount
from posix_parity import mergerfs_set_option
from posix_parity import temp_dir
from posix_parity import touch
@@ -18,70 +19,66 @@ SKIP = 77
def main():
if len(sys.argv) != 2:
print("usage: TEST_movefile_enospc <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
orig_moveonenospc = mergerfs_get_option(mount, "moveonenospc")
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
with mergerfs_mount(num_branches=2) as (mount, branches):
try:
orig_moveonenospc = mergerfs_get_option(mount, "moveonenospc")
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:
print("moveonenospc test requires at least 2 branches", end="")
return SKIP
if len(branches) < 2:
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)
merge_base = temp_dir(mount)
try:
mergerfs_set_option(mount, "moveonenospc", "mfs")
if mergerfs_get_option(mount, "moveonenospc") != "mfs":
return fail("moveonenospc: failed to set documented policy value")
try:
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")
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")
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
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}")
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()
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}")
if content != "test data for enospc":
return fail(f"moveonenospc: file content mismatch: got {content!r}")
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
print("moveonenospc ENOSPC path requires a constrained branch", end="")
return SKIP
except Exception as exc:
try:
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
except Exception:
pass
return fail(str(exc))
finally:
cleanup_dir(merge_base)
print("moveonenospc ENOSPC path requires a constrained branch", end="")
return SKIP
except Exception as exc:
try:
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
except Exception:
pass
return fail(str(exc))
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+20 -13
View File
@@ -1,27 +1,34 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
os.unlink(filepath)
os.unlink(filepath)
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
found = False
for entry in os.scandir(mount):
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 found:
return 1
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+38 -31
View File
@@ -1,11 +1,13 @@
#!/usr/bin/env python3
import tempfile
import ctypes
import mmap
import os
import resource
import sys
import tempfile
from posix_parity import mergerfs_mount
libc = ctypes.CDLL(None, use_errno=True)
@@ -28,38 +30,43 @@ def aligned_read(fd, size):
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
os.close(fd)
fd = os.open(filepath, os.O_RDWR|os.O_DIRECT|os.O_TRUNC)
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)
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
return 0
os.close(fd)
fd = os.open(filepath, os.O_RDWR|os.O_DIRECT|os.O_TRUNC)
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
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+46 -46
View File
@@ -2,67 +2,67 @@
import os
import sys
import tempfile
from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_open_after_unlink <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
merge_base = temp_dir(mount)
try:
merge_file = join(merge_base, "testfile")
touch(merge_file, b"hello world")
with mergerfs_mount() as (mount, _):
merge_base = temp_dir(mount)
fd = os.open(merge_file, os.O_RDWR)
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))
merge_file = join(merge_base, "testfile")
touch(merge_file, b"hello world")
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")))
fd = os.open(merge_file, os.O_RDWR)
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))
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 0
except Exception as exc:
return fail(str(exc))
finally:
os.close(fd2)
finally:
os.close(fd)
return 0
except Exception as exc:
return fail(str(exc))
finally:
try:
os.unlink(join(merge_base, "testfile"))
except FileNotFoundError:
pass
cleanup_dir(merge_base)
try:
os.unlink(join(merge_base, "testfile"))
except FileNotFoundError:
pass
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+57 -58
View File
@@ -8,6 +8,7 @@ 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_mount
from posix_parity import mergerfs_set_option
from posix_parity import temp_dir
from posix_parity import touch
@@ -50,75 +51,73 @@ def least_used_branches(branches, rel_dir):
def main():
if len(sys.argv) != 2:
print("usage: TEST_policy_lup <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
orig_create = mergerfs_get_option(mount, "func.create")
except (PermissionError, FileNotFoundError, OSError):
print("LUP create policy option unavailable", end="")
return SKIP
with mergerfs_mount(num_branches=2) as (mount, branches):
try:
orig_create = mergerfs_get_option(mount, "func.create")
except (PermissionError, FileNotFoundError, OSError):
print("LUP create policy option unavailable", end="")
return SKIP
try:
orig_search = mergerfs_get_option(mount, "func.search")
except (PermissionError, FileNotFoundError, OSError):
print("LUP search policy option unavailable", end="")
return SKIP
try:
orig_search = mergerfs_get_option(mount, "func.search")
except (PermissionError, FileNotFoundError, OSError):
print("LUP search policy option unavailable", end="")
return SKIP
branches = mergerfs_branches(mount)
if len(branches) < 2:
print("LUP policy test requires at least 2 branches", end="")
return SKIP
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)
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)
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")
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")
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").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)}")
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")
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)}")
path2 = join(merge_base, "lup_test_file2")
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)
mergerfs_set_option(mount, "func.create", orig_create)
mergerfs_set_option(mount, "func.search", orig_search)
return 0
except Exception as exc:
mergerfs_set_option(mount, "func.create", orig_create)
mergerfs_set_option(mount, "func.search", orig_search)
return fail(str(exc))
finally:
cleanup_dir(merge_base)
for branch in branches:
cleanup_dir(join(branch, rel_base))
return 0
except Exception as exc:
mergerfs_set_option(mount, "func.create", orig_create)
mergerfs_set_option(mount, "func.search", orig_search)
return fail(str(exc))
finally:
cleanup_dir(merge_base)
for branch in branches:
cleanup_dir(join(branch, rel_base))
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+49 -49
View File
@@ -10,70 +10,70 @@ from posix_parity import cleanup_paths
from posix_parity import compare_access
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_access <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_file, merge_notdir])
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
touch(merge_file, b"ok", 0o644)
touch(native_file, b"ok", 0o644)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
cleanup_paths([merge_file, merge_notdir])
err = compare_access("F_OK existing", merge_file, native_file, os.F_OK)
if err:
return fail(err)
touch(merge_file, b"ok", 0o644)
touch(native_file, b"ok", 0o644)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
err = compare_access(
"F_OK missing", merge_missing, native_missing, os.F_OK, errno.ENOENT
)
if err:
return fail(err)
err = compare_access("F_OK existing", merge_file, native_file, os.F_OK)
if err:
return fail(err)
err = compare_access(
"X_OK non-directory prefix",
join(merge_notdir, "child"),
join(native_notdir, "child"),
os.X_OK,
errno.ENOTDIR,
)
if err:
return fail(err)
err = compare_access(
"F_OK missing", merge_missing, native_missing, os.F_OK, errno.ENOENT
)
if err:
return fail(err)
os.chmod(merge_file, 0)
os.chmod(native_file, 0)
err = compare_access(
"X_OK non-directory prefix",
join(merge_notdir, "child"),
join(native_notdir, "child"),
os.X_OK,
errno.ENOTDIR,
)
if err:
return fail(err)
err = compare_access("R_OK unreadable", merge_file, native_file, os.R_OK)
if err:
return fail(err)
os.chmod(merge_file, 0)
os.chmod(native_file, 0)
os.chmod(merge_file, 0o644)
os.chmod(native_file, 0o644)
err = compare_access("R_OK unreadable", merge_file, native_file, os.R_OK)
if err:
return fail(err)
os.chmod(merge_file, 0o644)
os.chmod(native_file, 0o644)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+40 -42
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import ctypes
import errno
import fcntl
import os
@@ -11,6 +12,7 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -25,52 +27,48 @@ def fibmap(fd, block=0):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_bmap <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"x" * 8192)
touch(native_file, b"x" * 8192)
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
err = compare_calls(
"ioctl FIBMAP block0",
lambda: fibmap(mfd, 0),
lambda: fibmap(nfd, 0),
lambda a, b: (a >= 0) == (b >= 0),
)
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"x" * 8192)
touch(native_file, b"x" * 8192)
err = compare_calls(
"ioctl FIBMAP EBADF",
lambda: fibmap(mfd, 0),
lambda: fibmap(nfd, 0),
)
if err:
return fail(err)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
# FIBMAP often requires CAP_SYS_RAWIO; compare errno parity if denied.
err = compare_calls(
"ioctl FIBMAP block0",
lambda: fibmap(mfd, 0),
lambda: fibmap(nfd, 0),
lambda a, b: (a >= 0) == (b >= 0),
)
if err:
# If both denied by privilege checks, compare_calls already passes.
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
# EBADF parity on closed fds.
err = compare_calls(
"ioctl FIBMAP EBADF",
lambda: fibmap(mfd, 0),
lambda: fibmap(nfd, 0),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+65 -64
View File
@@ -11,6 +11,7 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import should_compare_inode
from posix_parity import stat_cmp_basic
from posix_parity import temp_dir
@@ -22,80 +23,80 @@ def stat_cmp_basic_with_inode(lhs, rhs):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_chmod <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
stcmp = stat_cmp_basic_with_inode if should_compare_inode(mount) else stat_cmp_basic
mount = sys.argv[1]
stcmp = stat_cmp_basic_with_inode if should_compare_inode(mount) else stat_cmp_basic
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
cleanup_paths([merge_file, merge_notdir])
cleanup_paths([merge_file, merge_notdir])
touch(merge_file, b"x", 0o644)
touch(native_file, b"x", 0o644)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
touch(merge_file, b"x", 0o644)
touch(native_file, b"x", 0o644)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
err = compare_calls(
"chmod success",
lambda: os.chmod(merge_file, 0o600),
lambda: os.chmod(native_file, 0o600),
)
if err:
return fail(err)
err = compare_calls(
"chmod success",
lambda: os.chmod(merge_file, 0o600),
lambda: os.chmod(native_file, 0o600),
)
if err:
return fail(err)
err = compare_calls(
"chmod stat parity",
lambda: os.lstat(merge_file),
lambda: os.lstat(native_file),
stcmp,
)
if err:
return fail(err)
err = compare_calls(
"chmod stat parity",
lambda: os.lstat(merge_file),
lambda: os.lstat(native_file),
stcmp,
)
if err:
return fail(err)
err = compare_calls(
"chmod ENOENT",
lambda: os.chmod(merge_missing, 0o600),
lambda: os.chmod(native_missing, 0o600),
)
if err:
return fail(err)
err = compare_calls(
"chmod ENOENT",
lambda: os.chmod(merge_missing, 0o600),
lambda: os.chmod(native_missing, 0o600),
)
if err:
return fail(err)
err = compare_calls(
"chmod ENOTDIR",
lambda: os.chmod(join(merge_notdir, "child"), 0o600),
lambda: os.chmod(join(native_notdir, "child"), 0o600),
)
if err:
return fail(err)
err = compare_calls(
"chmod ENOTDIR",
lambda: os.chmod(join(merge_notdir, "child"), 0o600),
lambda: os.chmod(join(native_notdir, "child"), 0o600),
)
if err:
return fail(err)
if os.geteuid() != 0:
err = compare_calls(
"chmod setuid parity",
lambda: os.chmod(merge_file, stat.S_ISUID | 0o644),
lambda: os.chmod(native_file, stat.S_ISUID | 0o644),
)
if err:
return fail(err)
else:
_ = errno.EPERM
if os.geteuid() != 0:
err = compare_calls(
"chmod setuid parity",
lambda: os.chmod(merge_file, stat.S_ISUID | 0o644),
lambda: os.chmod(native_file, stat.S_ISUID | 0o644),
)
if err:
return fail(err)
else:
_ = errno.EPERM
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+56 -55
View File
@@ -9,75 +9,76 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_chown <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
uid = os.getuid()
gid = os.getgid()
mount = sys.argv[1]
uid = os.getuid()
gid = os.getgid()
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
cleanup_paths([merge_file, merge_notdir])
cleanup_paths([merge_file, merge_notdir])
touch(merge_file, b"x", 0o644)
touch(native_file, b"x", 0o644)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
touch(merge_file, b"x", 0o644)
touch(native_file, b"x", 0o644)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
err = compare_calls(
"chown self",
lambda: os.chown(merge_file, uid, gid),
lambda: os.chown(native_file, uid, gid),
)
if err:
return fail(err)
err = compare_calls(
"chown self",
lambda: os.chown(merge_file, uid, gid),
lambda: os.chown(native_file, uid, gid),
)
if err:
return fail(err)
err = compare_calls(
"chown ENOENT",
lambda: os.chown(merge_missing, uid, gid),
lambda: os.chown(native_missing, uid, gid),
)
if err:
return fail(err)
err = compare_calls(
"chown ENOENT",
lambda: os.chown(merge_missing, uid, gid),
lambda: os.chown(native_missing, uid, gid),
)
if err:
return fail(err)
err = compare_calls(
"chown ENOTDIR",
lambda: os.chown(join(merge_notdir, "child"), uid, gid),
lambda: os.chown(join(native_notdir, "child"), uid, gid),
)
if err:
return fail(err)
err = compare_calls(
"chown ENOTDIR",
lambda: os.chown(join(merge_notdir, "child"), uid, gid),
lambda: os.chown(join(native_notdir, "child"), uid, gid),
)
if err:
return fail(err)
if os.geteuid() != 0:
err = compare_calls(
"chown EPERM foreign uid",
lambda: os.chown(merge_file, 0, gid),
lambda: os.chown(native_file, 0, gid),
)
if err:
return fail(err)
if os.geteuid() != 0:
err = compare_calls(
"chown EPERM foreign uid",
lambda: os.chown(merge_file, 0, gid),
lambda: os.chown(native_file, 0, gid),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+159 -115
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import errno
import hashlib
import os
import shutil
import sys
import tempfile
@@ -9,131 +11,173 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def _native_tmp():
tests_dir = os.path.dirname(os.path.realpath(__file__))
parent = os.path.join(tests_dir, ".test_tmp")
os.makedirs(parent, exist_ok=True)
return tempfile.mkdtemp(prefix="native_", dir=parent)
class NativeTmp:
def __enter__(self):
self.path = _native_tmp()
return self.path
def __exit__(self, *args):
shutil.rmtree(self.path, ignore_errors=True)
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_copy_file_range <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
if not hasattr(os, "copy_file_range"):
return 0
if not hasattr(os, "copy_file_range"):
return 0
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
merge_src = join(merge_base, "src")
merge_dst = join(merge_base, "dst")
native_src = join(native_base, "src")
native_dst = join(native_base, "dst")
payload = b"0123456789abcdefghijklmnopqrstuvwxyz"
touch(merge_src, payload)
touch(native_src, payload)
touch(merge_dst, b"")
touch(native_dst, b"")
msfd = os.open(merge_src, os.O_RDONLY)
mdfd = os.open(merge_dst, os.O_WRONLY)
nsfd = os.open(native_src, os.O_RDONLY)
ndfd = os.open(native_dst, os.O_WRONLY)
try:
err = compare_calls(
"copy_file_range success",
lambda: os.copy_file_range(msfd, mdfd, 16),
lambda: os.copy_file_range(nsfd, ndfd, 16),
lambda a, b: a == b,
)
if err:
return fail(err)
finally:
os.close(msfd)
os.close(mdfd)
os.close(nsfd)
os.close(ndfd)
with open(merge_dst, "rb") as mf, open(native_dst, "rb") as nf:
mdata = mf.read()
ndata = nf.read()
if mdata != ndata:
return fail(f"copy_file_range dst mismatch mergerfs={mdata!r} native={ndata!r}")
mdfd = os.open(merge_dst, os.O_WRONLY)
ndfd = os.open(native_dst, os.O_WRONLY)
try:
err = compare_calls(
"copy_file_range EBADF src",
lambda: os.copy_file_range(-1, mdfd, 1),
lambda: os.copy_file_range(-1, ndfd, 1),
)
if err:
return fail(err)
finally:
os.close(mdfd)
os.close(ndfd)
# Exercise very large copy lengths to validate the 64-bit copy result
# 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")
native_large_src = join(native_base, "src-large")
native_large_dst = join(native_base, "dst-large")
touch(merge_large_src, b"")
touch(merge_large_dst, b"")
touch(native_large_src, b"")
touch(native_large_dst, b"")
msfd = os.open(merge_large_src, os.O_RDWR)
mdfd = os.open(merge_large_dst, os.O_RDWR)
nsfd = os.open(native_large_src, os.O_RDWR)
ndfd = os.open(native_large_dst, os.O_RDWR)
try:
with NativeTmp() as native:
merge_base = temp_dir(mount)
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
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
err = compare_calls(
"copy_file_range large sparse",
lambda: os.copy_file_range(msfd, mdfd, large_len),
lambda: os.copy_file_range(nsfd, ndfd, large_len),
lambda a, b: a == b,
)
if err:
return fail(err)
merge_src = join(merge_base, "src")
merge_dst = join(merge_base, "dst")
native_src = join(native_base, "src")
native_dst = join(native_base, "dst")
mstat = os.fstat(mdfd)
nstat = os.fstat(ndfd)
if mstat.st_size != nstat.st_size:
return fail(
"copy_file_range large sparse dst size mismatch "
f"mergerfs={mstat.st_size} native={nstat.st_size}"
)
finally:
os.close(msfd)
os.close(mdfd)
os.close(nsfd)
os.close(ndfd)
payload = b"0123456789abcdefghijklmnopqrstuvwxyz"
touch(merge_src, payload)
touch(native_src, payload)
touch(merge_dst, b"")
touch(native_dst, b"")
return 0
finally:
cleanup_dir(merge_base)
msfd = os.open(merge_src, os.O_RDONLY)
mdfd = os.open(merge_dst, os.O_WRONLY)
nsfd = os.open(native_src, os.O_RDONLY)
ndfd = os.open(native_dst, os.O_WRONLY)
try:
err = compare_calls(
"copy_file_range success",
lambda: os.copy_file_range(msfd, mdfd, 16),
lambda: os.copy_file_range(nsfd, ndfd, 16),
lambda a, b: a == b,
)
if err:
return fail(err)
finally:
os.close(msfd)
os.close(mdfd)
os.close(nsfd)
os.close(ndfd)
with open(merge_dst, "rb") as mf, open(native_dst, "rb") as nf:
mdata = mf.read()
ndata = nf.read()
if mdata != ndata:
return fail(f"copy_file_range dst mismatch mergerfs={mdata!r} native={ndata!r}")
mdfd = os.open(merge_dst, os.O_WRONLY)
ndfd = os.open(native_dst, os.O_WRONLY)
try:
err = compare_calls(
"copy_file_range EBADF src",
lambda: os.copy_file_range(-1, mdfd, 1),
lambda: os.copy_file_range(-1, ndfd, 1),
)
if err:
return fail(err)
finally:
os.close(mdfd)
os.close(ndfd)
large_len = (5 << 30) + 4096
merge_large_src = join(merge_base, "src-large")
merge_large_dst = join(merge_base, "dst-large")
native_large_src = join(native_base, "src-large")
native_large_dst = join(native_base, "dst-large")
touch(merge_large_src, b"")
touch(merge_large_dst, b"")
touch(native_large_src, b"")
touch(native_large_dst, b"")
msfd = os.open(merge_large_src, os.O_RDWR)
mdfd = os.open(merge_large_dst, os.O_RDWR)
nsfd = os.open(native_large_src, os.O_RDWR)
ndfd = os.open(native_large_dst, os.O_RDWR)
try:
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
total_m = 0
while True:
n = os.copy_file_range(msfd, mdfd, large_len)
if n == 0:
break
total_m += n
if total_m >= large_len:
break
total_n = 0
while True:
n = os.copy_file_range(nsfd, ndfd, large_len)
if n == 0:
break
total_n += n
if total_n >= large_len:
break
mstat = os.fstat(mdfd)
nstat = os.fstat(ndfd)
if mstat.st_size != nstat.st_size:
return fail(
"copy_file_range large sparse dst size mismatch "
f"mergerfs={mstat.st_size} native={nstat.st_size}"
)
def sha256_partial(fd, size):
h = hashlib.sha256()
pos = os.lseek(fd, 0, os.SEEK_SET)
remaining = size
while remaining > 0:
chunk = os.read(fd, min(remaining, 65536))
if not chunk:
break
h.update(chunk)
remaining -= len(chunk)
os.lseek(fd, pos, os.SEEK_SET)
return h.digest()
if os.path.getsize(merge_large_dst) != os.path.getsize(native_large_dst):
return fail("copy_file_range large: final sizes differ")
mhash = sha256_partial(mdfd, mstat.st_size)
nhash = sha256_partial(ndfd, nstat.st_size)
if mhash != nhash:
return fail("copy_file_range large: content hash mismatch")
finally:
os.close(msfd)
os.close(mdfd)
os.close(nsfd)
os.close(ndfd)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+69 -69
View File
@@ -10,90 +10,90 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_create_mknod <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_exist = join(merge_base, "exist")
native_exist = join(native_base, "exist")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_file, merge_exist, merge_notdir])
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_exist = join(merge_base, "exist")
native_exist = join(native_base, "exist")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
err = compare_calls(
"create success",
lambda: os.open(merge_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640),
lambda: os.open(native_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640),
close_fds=True,
)
if err:
return fail(err)
cleanup_paths([merge_file, merge_exist, merge_notdir])
with open(merge_exist, "wb"):
pass
with open(native_exist, "wb"):
pass
err = compare_calls(
"create success",
lambda: os.open(merge_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640),
lambda: os.open(native_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640),
close_fds=True,
)
if err:
return fail(err)
err = compare_calls(
"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)
with open(merge_exist, "wb"):
pass
with open(native_exist, "wb"):
pass
with open(merge_notdir, "wb"):
pass
with open(native_notdir, "wb"):
pass
err = compare_calls(
"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)
err = compare_calls(
"create ENOTDIR",
lambda: os.open(join(merge_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640),
lambda: os.open(join(native_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640),
)
if err:
return fail(err)
with open(merge_notdir, "wb"):
pass
with open(native_notdir, "wb"):
pass
merge_node = join(merge_base, "mknod")
native_node = join(native_base, "mknod")
cleanup_paths([merge_node])
err = compare_calls(
"create ENOTDIR",
lambda: os.open(join(merge_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640),
lambda: os.open(join(native_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640),
)
if err:
return fail(err)
err = compare_calls(
"mknod regular",
lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0),
lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0),
)
if err:
return fail(err)
merge_node = join(merge_base, "mknod")
native_node = join(native_base, "mknod")
cleanup_paths([merge_node])
err = compare_calls(
"mknod EEXIST",
lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0),
lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0),
)
if err:
return fail(err)
err = compare_calls(
"mknod regular",
lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0),
lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0),
)
if err:
return fail(err)
err = compare_calls(
"mknod EEXIST",
lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0),
lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+56 -57
View File
@@ -8,76 +8,75 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_fsync_flush <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
touch(merge_file, b"abc")
touch(native_file, b"abc")
os.makedirs(merge_dir, exist_ok=True)
os.makedirs(native_dir, exist_ok=True)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
os.lseek(mfd, 0, os.SEEK_END)
os.lseek(nfd, 0, os.SEEK_END)
os.write(mfd, b"-appended")
os.write(nfd, b"-appended")
touch(merge_file, b"abc")
touch(native_file, b"abc")
os.makedirs(merge_dir, exist_ok=True)
os.makedirs(native_dir, exist_ok=True)
err = compare_calls("fsync file", lambda: os.fsync(mfd), lambda: os.fsync(nfd))
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
os.lseek(mfd, 0, os.SEEK_END)
os.lseek(nfd, 0, os.SEEK_END)
os.write(mfd, b"-appended")
os.write(nfd, b"-appended")
mdirfd = os.open(merge_dir, os.O_RDONLY | os.O_DIRECTORY)
ndirfd = os.open(native_dir, os.O_RDONLY | os.O_DIRECTORY)
try:
err = compare_calls("fsync dir", lambda: os.fsync(mdirfd), lambda: os.fsync(ndirfd))
if err:
return fail(err)
finally:
os.close(mdirfd)
os.close(ndirfd)
err = compare_calls("fsync file", lambda: os.fsync(mfd), lambda: os.fsync(nfd))
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
bad_m = os.open(merge_file, os.O_RDONLY)
bad_n = os.open(native_file, os.O_RDONLY)
os.close(bad_m)
os.close(bad_n)
err = compare_calls("fsync EBADF", lambda: os.fsync(bad_m), lambda: os.fsync(bad_n))
if err:
return fail(err)
mdirfd = os.open(merge_dir, os.O_RDONLY | os.O_DIRECTORY)
ndirfd = os.open(native_dir, os.O_RDONLY | os.O_DIRECTORY)
try:
err = compare_calls("fsync dir", lambda: os.fsync(mdirfd), lambda: os.fsync(ndirfd))
if err:
return fail(err)
finally:
os.close(mdirfd)
os.close(ndirfd)
with open(merge_file, "rb") as mf, open(native_file, "rb") as nf:
mdata = mf.read()
ndata = nf.read()
if mdata != ndata:
return fail(f"flush content mismatch mergerfs={mdata!r} native={ndata!r}")
bad_m = os.open(merge_file, os.O_RDONLY)
bad_n = os.open(native_file, os.O_RDONLY)
os.close(bad_m)
os.close(bad_n)
err = compare_calls("fsync EBADF", lambda: os.fsync(bad_m), lambda: os.fsync(bad_n))
if err:
return fail(err)
# Close-to-flush parity check: both paths should preserve appended payload.
with open(merge_file, "rb") as mf, open(native_file, "rb") as nf:
mdata = mf.read()
ndata = nf.read()
if mdata != ndata:
return fail(f"flush content mismatch mergerfs={mdata!r} native={ndata!r}")
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+64 -65
View File
@@ -10,93 +10,92 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import should_compare_inode
from posix_parity import temp_dir
from posix_parity import touch
def st_cmp(lhs, rhs):
return (
def st_cmp(lhs, rhs, compare_ino=False):
base = (
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
)
def st_cmp_with_inode(lhs, rhs):
return st_cmp(lhs, rhs) and lhs.st_ino == rhs.st_ino
if stat.S_ISDIR(lhs.st_mode) or stat.S_ISDIR(rhs.st_mode):
return base and (not compare_ino or lhs.st_ino == rhs.st_ino)
return base and lhs.st_size == rhs.st_size and (not compare_ino or lhs.st_ino == rhs.st_ino)
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_getattr_fgetattr <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
compare_ino = should_compare_inode(mount)
mount = sys.argv[1]
stcmp = st_cmp_with_inode if should_compare_inode(mount) else st_cmp
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
cleanup_paths([merge_file, merge_dir, merge_notdir])
cleanup_paths([merge_file, merge_dir, merge_notdir])
touch(merge_file, b"abc", 0o640)
touch(native_file, b"abc", 0o640)
os.makedirs(merge_dir, exist_ok=True)
os.makedirs(native_dir, exist_ok=True)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
touch(merge_file, b"abc", 0o640)
touch(native_file, b"abc", 0o640)
os.makedirs(merge_dir, exist_ok=True)
os.makedirs(native_dir, exist_ok=True)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
err = compare_calls(
"lstat regular", lambda: os.lstat(merge_file), lambda: os.lstat(native_file), lambda l, r: st_cmp(l, r, compare_ino)
)
if err:
return fail(err)
err = compare_calls(
"lstat regular", lambda: os.lstat(merge_file), lambda: os.lstat(native_file), stcmp
)
if err:
return fail(err)
err = compare_calls(
"lstat directory", lambda: os.lstat(merge_dir), lambda: os.lstat(native_dir), lambda l, r: st_cmp(l, r, compare_ino)
)
if err:
return fail(err)
err = compare_calls(
"lstat directory", lambda: os.lstat(merge_dir), lambda: os.lstat(native_dir), stcmp
)
if err:
return fail(err)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
err = compare_calls("fstat open fd", lambda: os.fstat(mfd), lambda: os.fstat(nfd), lambda l, r: st_cmp(l, r, compare_ino))
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
err = compare_calls("fstat open fd", lambda: os.fstat(mfd), lambda: os.fstat(nfd), stcmp)
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
err = compare_calls("lstat ENOENT", lambda: os.lstat(merge_missing), lambda: os.lstat(native_missing))
if err:
return fail(err)
err = compare_calls("lstat ENOENT", lambda: os.lstat(merge_missing), lambda: os.lstat(native_missing))
if err:
return fail(err)
err = compare_calls(
"lstat ENOTDIR",
lambda: os.lstat(join(merge_notdir, "child")),
lambda: os.lstat(join(native_notdir, "child")),
)
if err:
return fail(err)
err = compare_calls(
"lstat ENOTDIR",
lambda: os.lstat(join(merge_notdir, "child")),
lambda: os.lstat(join(native_notdir, "child")),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+37 -37
View File
@@ -11,6 +11,7 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -22,47 +23,46 @@ def fionread(fd):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_ioctl <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"hello-world")
touch(native_file, b"hello-world")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
err = compare_calls("ioctl FIONREAD", lambda: fionread(mfd), lambda: fionread(nfd), lambda a, b: a == b)
if err:
return fail(err)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"hello-world")
touch(native_file, b"hello-world")
os.lseek(mfd, 5, os.SEEK_SET)
os.lseek(nfd, 5, os.SEEK_SET)
err = compare_calls(
"ioctl FIONREAD after seek",
lambda: fionread(mfd),
lambda: fionread(nfd),
lambda a, b: a == b,
)
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
err = compare_calls("ioctl FIONREAD", lambda: fionread(mfd), lambda: fionread(nfd), lambda a, b: a == b)
if err:
return fail(err)
os.lseek(mfd, 5, os.SEEK_SET)
os.lseek(nfd, 5, os.SEEK_SET)
err = compare_calls(
"ioctl FIONREAD after seek",
lambda: fionread(mfd),
lambda: fionread(nfd),
lambda a, b: a == b,
)
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+49 -49
View File
@@ -9,67 +9,67 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_link_symlink <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_src = join(merge_base, "src")
native_src = join(native_base, "src")
merge_dst = join(merge_base, "dst")
native_dst = join(native_base, "dst")
merge_slnk = join(merge_base, "symlink")
native_slnk = join(native_base, "symlink")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_src, merge_dst, merge_slnk])
touch(merge_src, b"src", 0o644)
touch(native_src, b"src", 0o644)
merge_src = join(merge_base, "src")
native_src = join(native_base, "src")
merge_dst = join(merge_base, "dst")
native_dst = join(native_base, "dst")
merge_slnk = join(merge_base, "symlink")
native_slnk = join(native_base, "symlink")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
err = compare_calls("link success", lambda: os.link(merge_src, merge_dst), lambda: os.link(native_src, native_dst))
if err:
return fail(err)
cleanup_paths([merge_src, merge_dst, merge_slnk])
touch(merge_src, b"src", 0o644)
touch(native_src, b"src", 0o644)
err = compare_calls(
"link ENOENT source",
lambda: os.link(merge_missing, join(merge_base, "dst2")),
lambda: os.link(native_missing, join(native_base, "dst2")),
)
if err:
return fail(err)
err = compare_calls("link success", lambda: os.link(merge_src, merge_dst), lambda: os.link(native_src, native_dst))
if err:
return fail(err)
err = compare_calls(
"symlink success",
lambda: os.symlink("src", merge_slnk),
lambda: os.symlink("src", native_slnk),
)
if err:
return fail(err)
err = compare_calls(
"link ENOENT source",
lambda: os.link(merge_missing, join(merge_base, "dst2")),
lambda: os.link(native_missing, join(native_base, "dst2")),
)
if err:
return fail(err)
err = compare_calls(
"symlink EEXIST",
lambda: os.symlink("src", merge_slnk),
lambda: os.symlink("src", native_slnk),
)
if err:
return fail(err)
err = compare_calls(
"symlink success",
lambda: os.symlink("src", merge_slnk),
lambda: os.symlink("src", native_slnk),
)
if err:
return fail(err)
err = compare_calls(
"symlink EEXIST",
lambda: os.symlink("src", merge_slnk),
lambda: os.symlink("src", native_slnk),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+73 -75
View File
@@ -12,12 +12,12 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def pack_flock(l_type, l_whence=0, l_start=0, l_len=0, l_pid=0):
# struct flock (common glibc x86_64 layout)
return struct.pack("hhqqi", l_type, l_whence, l_start, l_len, l_pid)
@@ -51,91 +51,89 @@ def main():
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
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"x")
touch(native_file, b"x")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mfd1 = os.open(merge_file, os.O_RDWR)
mfd2 = os.open(merge_file, os.O_RDWR)
nfd1 = os.open(native_file, os.O_RDWR)
nfd2 = os.open(native_file, os.O_RDWR)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"x")
touch(native_file, b"x")
try:
err = compare_calls(
"flock EX nonblock",
lambda: fcntl.flock(mfd1, fcntl.LOCK_EX | fcntl.LOCK_NB),
lambda: fcntl.flock(nfd1, fcntl.LOCK_EX | fcntl.LOCK_NB),
)
if err:
return fail(err)
mfd1 = os.open(merge_file, os.O_RDWR)
mfd2 = os.open(merge_file, os.O_RDWR)
nfd1 = os.open(native_file, os.O_RDWR)
nfd2 = os.open(native_file, os.O_RDWR)
err = compare_calls(
"flock SH nonblock second fd",
lambda: fcntl.flock(mfd2, fcntl.LOCK_SH | fcntl.LOCK_NB),
lambda: fcntl.flock(nfd2, fcntl.LOCK_SH | fcntl.LOCK_NB),
)
if err:
return fail(err)
try:
err = compare_calls(
"flock EX nonblock",
lambda: fcntl.flock(mfd1, fcntl.LOCK_EX | fcntl.LOCK_NB),
lambda: fcntl.flock(nfd1, fcntl.LOCK_EX | fcntl.LOCK_NB),
)
if err:
return fail(err)
err = compare_calls("flock unlock", lambda: fcntl.flock(mfd1, fcntl.LOCK_UN), lambda: fcntl.flock(nfd1, fcntl.LOCK_UN))
if err:
return fail(err)
err = compare_calls("flock unlock second fd", lambda: fcntl.flock(mfd2, fcntl.LOCK_UN), lambda: fcntl.flock(nfd2, fcntl.LOCK_UN))
if err:
return fail(err)
err = compare_calls(
"flock SH nonblock second fd",
lambda: fcntl.flock(mfd2, fcntl.LOCK_SH | fcntl.LOCK_NB),
lambda: fcntl.flock(nfd2, fcntl.LOCK_SH | fcntl.LOCK_NB),
)
if err:
return fail(err)
wrlk = pack_flock(fcntl.F_WRLCK)
unlck = pack_flock(fcntl.F_UNLCK)
err = compare_calls("flock unlock", lambda: fcntl.flock(mfd1, fcntl.LOCK_UN), lambda: fcntl.flock(nfd1, fcntl.LOCK_UN))
if err:
return fail(err)
err = compare_calls("flock unlock second fd", lambda: fcntl.flock(mfd2, fcntl.LOCK_UN), lambda: fcntl.flock(nfd2, fcntl.LOCK_UN))
if err:
return fail(err)
err = compare_calls("fcntl F_SETLK write lock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, wrlk))
if err:
return fail(err)
# POSIX record lock via fcntl
wrlk = pack_flock(fcntl.F_WRLCK)
unlck = pack_flock(fcntl.F_UNLCK)
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 write lock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, wrlk))
if err:
return fail(err)
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:
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}")
bad_m = os.open(merge_file, os.O_RDONLY)
bad_n = os.open(native_file, os.O_RDONLY)
os.close(bad_m)
os.close(bad_n)
err = compare_calls(
"fcntl EBADF",
lambda: fcntl.fcntl(bad_m, fcntl.F_GETFD),
lambda: fcntl.fcntl(bad_n, fcntl.F_GETFD),
)
if err:
return fail(err)
finally:
os.close(mfd1)
os.close(mfd2)
os.close(nfd1)
os.close(nfd2)
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:
return fail(err)
bad_m = os.open(merge_file, os.O_RDONLY)
bad_n = os.open(native_file, os.O_RDONLY)
os.close(bad_m)
os.close(bad_n)
err = compare_calls(
"fcntl EBADF",
lambda: fcntl.fcntl(bad_m, fcntl.F_GETFD),
lambda: fcntl.fcntl(bad_n, fcntl.F_GETFD),
)
if err:
return fail(err)
finally:
os.close(mfd1)
os.close(mfd2)
os.close(nfd1)
os.close(nfd2)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+49 -49
View File
@@ -9,69 +9,69 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_mkdir_rmdir <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_dir, merge_notdir])
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
err = compare_calls("mkdir success", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755))
if err:
return fail(err)
cleanup_paths([merge_dir, merge_notdir])
err = compare_calls("mkdir EEXIST", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755))
if err:
return fail(err)
err = compare_calls("mkdir success", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755))
if err:
return fail(err)
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
err = compare_calls(
"mkdir ENOTDIR",
lambda: os.mkdir(join(merge_notdir, "child"), 0o755),
lambda: os.mkdir(join(native_notdir, "child"), 0o755),
)
if err:
return fail(err)
err = compare_calls("mkdir EEXIST", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755))
if err:
return fail(err)
err = compare_calls("rmdir success", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir))
if err:
return fail(err)
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
err = compare_calls(
"mkdir ENOTDIR",
lambda: os.mkdir(join(merge_notdir, "child"), 0o755),
lambda: os.mkdir(join(native_notdir, "child"), 0o755),
)
if err:
return fail(err)
err = compare_calls("rmdir ENOENT", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir))
if err:
return fail(err)
err = compare_calls("rmdir success", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir))
if err:
return fail(err)
touch(join(merge_base, "nonempty/file"), b"x")
touch(join(native_base, "nonempty/file"), b"x")
err = compare_calls(
"rmdir ENOTEMPTY",
lambda: os.rmdir(join(merge_base, "nonempty")),
lambda: os.rmdir(join(native_base, "nonempty")),
)
if err:
return fail(err)
err = compare_calls("rmdir ENOENT", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir))
if err:
return fail(err)
touch(join(merge_base, "nonempty/file"), b"x")
touch(join(native_base, "nonempty/file"), b"x")
err = compare_calls(
"rmdir ENOTEMPTY",
lambda: os.rmdir(join(merge_base, "nonempty")),
lambda: os.rmdir(join(native_base, "nonempty")),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+60 -60
View File
@@ -9,79 +9,79 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_open_read_write <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_file, merge_notdir])
touch(merge_file, b"123456", 0o644)
touch(native_file, b"123456", 0o644)
os.makedirs(merge_dir, exist_ok=True)
os.makedirs(native_dir, exist_ok=True)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
err = compare_calls("open ENOENT", lambda: os.open(merge_missing, os.O_RDONLY), lambda: os.open(native_missing, os.O_RDONLY))
if err:
return fail(err)
cleanup_paths([merge_file, merge_notdir])
touch(merge_file, b"123456", 0o644)
touch(native_file, b"123456", 0o644)
os.makedirs(merge_dir, exist_ok=True)
os.makedirs(native_dir, exist_ok=True)
touch(merge_notdir, b"x", 0o644)
touch(native_notdir, b"x", 0o644)
err = compare_calls("open EISDIR", lambda: os.open(merge_dir, os.O_WRONLY), lambda: os.open(native_dir, os.O_WRONLY))
if err:
return fail(err)
err = compare_calls("open ENOENT", lambda: os.open(merge_missing, os.O_RDONLY), lambda: os.open(native_missing, os.O_RDONLY))
if err:
return fail(err)
err = compare_calls(
"open ENOTDIR",
lambda: os.open(join(merge_notdir, "child"), os.O_RDONLY),
lambda: os.open(join(native_notdir, "child"), os.O_RDONLY),
)
if err:
return fail(err)
err = compare_calls("open EISDIR", lambda: os.open(merge_dir, os.O_WRONLY), lambda: os.open(native_dir, os.O_WRONLY))
if err:
return fail(err)
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
os.lseek(mfd, 0, os.SEEK_SET)
os.lseek(nfd, 0, os.SEEK_SET)
m_data = os.read(mfd, 6)
n_data = os.read(nfd, 6)
if m_data != n_data:
return fail(f"read parity mismatch mergerfs={m_data!r} native={n_data!r}")
err = compare_calls(
"open ENOTDIR",
lambda: os.open(join(merge_notdir, "child"), os.O_RDONLY),
lambda: os.open(join(native_notdir, "child"), os.O_RDONLY),
)
if err:
return fail(err)
os.lseek(mfd, 0, os.SEEK_SET)
os.lseek(nfd, 0, os.SEEK_SET)
mw = os.write(mfd, b"abc")
nw = os.write(nfd, b"abc")
if mw != nw:
return fail(f"write count mismatch mergerfs={mw} native={nw}")
finally:
os.close(mfd)
os.close(nfd)
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
os.lseek(mfd, 0, os.SEEK_SET)
os.lseek(nfd, 0, os.SEEK_SET)
m_data = os.read(mfd, 6)
n_data = os.read(nfd, 6)
if m_data != n_data:
return fail(f"read parity mismatch mergerfs={m_data!r} native={n_data!r}")
os.lseek(mfd, 0, os.SEEK_SET)
os.lseek(nfd, 0, os.SEEK_SET)
mw = os.write(mfd, b"abc")
nw = os.write(nfd, b"abc")
if mw != nw:
return fail(f"write count mismatch mergerfs={mw} native={nw}")
finally:
os.close(mfd)
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+30 -31
View File
@@ -8,6 +8,7 @@ import tempfile
from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -23,41 +24,39 @@ def poll_mask(fd, timeout_ms=25):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_poll <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"abcdef")
touch(native_file, b"abcdef")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
mmask = poll_mask(mfd)
nmask = poll_mask(nfd)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"abcdef")
touch(native_file, b"abcdef")
if bool(mmask & select.POLLIN) != bool(nmask & select.POLLIN):
return fail(f"poll POLLIN mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}")
if bool(mmask & select.POLLOUT) != bool(nmask & select.POLLOUT):
return fail(f"poll POLLOUT mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}")
finally:
os.close(mfd)
os.close(nfd)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
mmask = poll_mask(mfd)
nmask = poll_mask(nfd)
# For regular files POLLIN and POLLOUT are typically ready.
if bool(mmask & select.POLLIN) != bool(nmask & select.POLLIN):
return fail(f"poll POLLIN mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}")
if bool(mmask & select.POLLOUT) != bool(nmask & select.POLLOUT):
return fail(f"poll POLLOUT mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}")
finally:
os.close(mfd)
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+53 -53
View File
@@ -9,6 +9,7 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -111,68 +112,67 @@ def deleted_visibility_signature(dirpath, victim_path):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_readdir <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
for idx in range(8):
touch(join(merge_dir, f"f{idx}"), b"x")
touch(join(native_dir, f"f{idx}"), b"x")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
for idx in range(8):
touch(join(merge_dir, f"f{idx}"), b"x")
touch(join(native_dir, f"f{idx}"), b"x")
err = compare_calls(
"opendir ENOENT",
lambda: opendir_or_raise(join(merge_base, "missing")),
lambda: opendir_or_raise(join(native_base, "missing")),
)
if err:
return fail(err)
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
err = compare_calls(
"opendir ENOTDIR",
lambda: opendir_or_raise(join(merge_notdir, "child")),
lambda: opendir_or_raise(join(native_notdir, "child")),
)
if err:
return fail(err)
err = compare_calls(
"opendir ENOENT",
lambda: opendir_or_raise(join(merge_base, "missing")),
lambda: opendir_or_raise(join(native_base, "missing")),
)
if err:
return fail(err)
merge_names = names_set(merge_dir)
native_names = names_set(native_dir)
if merge_names != native_names:
return fail(f"readdir names mismatch mergerfs={sorted(merge_names)} native={sorted(native_names)}")
err = compare_calls(
"opendir ENOTDIR",
lambda: opendir_or_raise(join(merge_notdir, "child")),
lambda: opendir_or_raise(join(native_notdir, "child")),
)
if err:
return fail(err)
merge_consistent = seek_tell_consistent(merge_dir)
native_consistent = seek_tell_consistent(native_dir)
if merge_consistent != native_consistent:
return fail(
f"seekdir/telldir consistency mismatch mergerfs={merge_consistent} native={native_consistent}"
)
merge_names = names_set(merge_dir)
native_names = names_set(native_dir)
if merge_names != native_names:
return fail(f"readdir names mismatch mergerfs={sorted(merge_names)} native={sorted(native_names)}")
touch(join(merge_dir, "victim"), b"x")
touch(join(native_dir, "victim"), b"x")
merge_sig = deleted_visibility_signature(merge_dir, join(merge_dir, "victim"))
native_sig = deleted_visibility_signature(native_dir, join(native_dir, "victim"))
if merge_sig != native_sig:
return fail(f"deleted-entry visibility mismatch mergerfs={merge_sig} native={native_sig}")
merge_consistent = seek_tell_consistent(merge_dir)
native_consistent = seek_tell_consistent(native_dir)
if merge_consistent != native_consistent:
return fail(
f"seekdir/telldir consistency mismatch mergerfs={merge_consistent} native={native_consistent}"
)
touch(join(merge_dir, "victim"), b"x")
touch(join(native_dir, "victim"), b"x")
merge_sig = deleted_visibility_signature(merge_dir, join(merge_dir, "victim"))
native_sig = deleted_visibility_signature(native_dir, join(native_dir, "victim"))
if merge_sig != native_sig:
return fail(f"deleted-entry visibility mismatch mergerfs={merge_sig} native={native_sig}")
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+44 -46
View File
@@ -8,6 +8,7 @@ from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_get_option
from posix_parity import mergerfs_mount
from posix_parity import mergerfs_set_option
from posix_parity import temp_dir
from posix_parity import touch
@@ -24,54 +25,51 @@ def list_and_stat(dirpath):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_readdir_plus <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
for i in range(40):
touch(join(merge_dir, f"f{i:03d}"), bytes(str(i), "ascii"))
touch(join(native_dir, f"f{i:03d}"), bytes(str(i), "ascii"))
# Exercise mapping from readdir entries to immediate getattr calls.
m_map = list_and_stat(merge_dir)
n_map = list_and_stat(native_dir)
if m_map != n_map:
return fail("readdir+getattr map mismatch between mergerfs and native")
# Optionally toggle readdir policy and ensure semantic output stays stable.
try:
orig = mergerfs_get_option(mount, "func.readdir")
except (PermissionError, FileNotFoundError, OSError):
return 0
try:
for mode in ("seq", "cosr:2:2", "cor:2:2"):
mergerfs_set_option(mount, "func.readdir", mode)
got = list_and_stat(merge_dir)
if got != m_map:
return fail(f"func.readdir={mode} changed readdir+getattr semantic output")
except (PermissionError, FileNotFoundError, OSError):
return 0
finally:
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
mergerfs_set_option(mount, "func.readdir", orig)
except OSError:
pass
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
return 0
finally:
cleanup_dir(merge_base)
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
for i in range(40):
touch(join(merge_dir, f"f{i:03d}"), bytes(str(i), "ascii"))
touch(join(native_dir, f"f{i:03d}"), bytes(str(i), "ascii"))
m_map = list_and_stat(merge_dir)
n_map = list_and_stat(native_dir)
if m_map != n_map:
return fail("readdir+getattr map mismatch between mergerfs and native")
try:
orig = mergerfs_get_option(mount, "func.readdir")
except (PermissionError, FileNotFoundError, OSError):
return 0
try:
for mode in ("seq", "cosr:2:2", "cor:2:2"):
mergerfs_set_option(mount, "func.readdir", mode)
got = list_and_stat(merge_dir)
if got != m_map:
return fail(f"func.readdir={mode} changed readdir+getattr semantic output")
except (PermissionError, FileNotFoundError, OSError):
return 0
finally:
try:
mergerfs_set_option(mount, "func.readdir", orig)
except OSError:
pass
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+59 -61
View File
@@ -7,81 +7,79 @@ import tempfile
from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_release <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
touch(merge_file, b"abc")
touch(native_file, b"abc")
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
mfd1 = os.open(merge_file, os.O_RDWR)
nfd1 = os.open(native_file, os.O_RDWR)
mfd2 = os.dup(mfd1)
nfd2 = os.dup(nfd1)
touch(merge_file, b"abc")
touch(native_file, b"abc")
try:
os.close(mfd1)
os.close(nfd1)
# Open + dup so one close does not release the underlying open-file state.
mfd1 = os.open(merge_file, os.O_RDWR)
nfd1 = os.open(native_file, os.O_RDWR)
mfd2 = os.dup(mfd1)
nfd2 = os.dup(nfd1)
mw = os.write(mfd2, b"X")
nw = os.write(nfd2, b"X")
if mw != nw:
return fail(f"release dup write mismatch mergerfs={mw} native={nw}")
try:
os.close(mfd1)
os.close(nfd1)
os.lseek(mfd2, 0, os.SEEK_SET)
os.lseek(nfd2, 0, os.SEEK_SET)
mr = os.read(mfd2, 4)
nr = os.read(nfd2, 4)
if mr != nr:
return fail(f"release dup read mismatch mergerfs={mr!r} native={nr!r}")
finally:
os.close(mfd2)
os.close(nfd2)
mw = os.write(mfd2, b"X")
nw = os.write(nfd2, b"X")
if mw != nw:
return fail(f"release dup write mismatch mergerfs={mw} native={nw}")
with open(merge_file, "rb") as mf, open(native_file, "rb") as nf:
md = mf.read()
nd = nf.read()
if md != nd:
return fail(f"release final data mismatch mergerfs={md!r} native={nd!r}")
os.lseek(mfd2, 0, os.SEEK_SET)
os.lseek(nfd2, 0, os.SEEK_SET)
mr = os.read(mfd2, 4)
nr = os.read(nfd2, 4)
if mr != nr:
return fail(f"release dup read mismatch mergerfs={mr!r} native={nr!r}")
finally:
os.close(mfd2)
os.close(nfd2)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
os.close(mfd)
os.close(nfd)
try:
os.close(mfd)
m_err = 0
except OSError as exc:
m_err = exc.errno
try:
os.close(nfd)
n_err = 0
except OSError as exc:
n_err = exc.errno
if m_err != n_err:
return fail(f"release EBADF mismatch mergerfs_errno={m_err} native_errno={n_err}")
with open(merge_file, "rb") as mf, open(native_file, "rb") as nf:
md = mf.read()
nd = nf.read()
if md != nd:
return fail(f"release final data mismatch mergerfs={md!r} native={nd!r}")
# Closing same fd twice should map to EBADF on both.
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
os.close(mfd)
os.close(nfd)
try:
os.close(mfd)
m_err = 0
except OSError as exc:
m_err = exc.errno
try:
os.close(nfd)
n_err = 0
except OSError as exc:
n_err = exc.errno
if m_err != n_err:
return fail(f"release EBADF mismatch mergerfs_errno={m_err} native_errno={n_err}")
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+22 -22
View File
@@ -8,6 +8,7 @@ import tempfile
from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -49,31 +50,30 @@ def close_and_dirfd_errno(path):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_releasedir <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
touch(join(merge_dir, "a"), b"x")
touch(join(native_dir, "a"), b"x")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
m_err = close_and_dirfd_errno(merge_dir)
n_err = close_and_dirfd_errno(native_dir)
if m_err != n_err:
return fail(f"releasedir EBADF parity mismatch mergerfs_errno={m_err} native_errno={n_err}")
merge_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
touch(join(merge_dir, "a"), b"x")
touch(join(native_dir, "a"), b"x")
m_err = close_and_dirfd_errno(merge_dir)
n_err = close_and_dirfd_errno(native_dir)
if m_err != n_err:
return fail(f"releasedir EBADF parity mismatch mergerfs_errno={m_err} native_errno={n_err}")
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+29 -29
View File
@@ -8,6 +8,7 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
@@ -22,38 +23,37 @@ def statvfs_cmp(lhs, rhs):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_statfs <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
err = compare_calls(
"statvfs mount parity",
lambda: os.statvfs(mount),
lambda: os.statvfs(native),
statvfs_cmp,
)
if err:
return fail(err)
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
err = compare_calls(
"statvfs ENOENT",
lambda: os.statvfs(join(merge_base, "missing")),
lambda: os.statvfs(join(native_base, "missing")),
)
if err:
return fail(err)
err = compare_calls(
"statvfs mount parity",
lambda: os.statvfs(mount),
lambda: os.statvfs(native),
statvfs_cmp,
)
if err:
return fail(err)
err = compare_calls(
"statvfs ENOENT",
lambda: os.statvfs(join(merge_base, "missing")),
lambda: os.statvfs(join(native_base, "missing")),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+85 -87
View File
@@ -9,6 +9,7 @@ from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_fullpath
from posix_parity import mergerfs_mount
from posix_parity import should_compare_inode
from posix_parity import temp_dir
from posix_parity import touch
@@ -172,100 +173,97 @@ def expect_same_errno(name, mcall, ncall):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_statx <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
stcmp = cmp_statx_basic_with_inode if should_compare_inode(mount) else cmp_statx_basic
merge_base = temp_dir(mount)
try:
merge_file = join(merge_base, "file")
merge_link = join(merge_base, "link")
with mergerfs_mount() as (mount, _):
stcmp = cmp_statx_basic_with_inode if should_compare_inode(mount) else cmp_statx_basic
touch(merge_file, b"hello")
merge_base = temp_dir(mount)
try:
native_file = mergerfs_fullpath(merge_file)
except OSError:
return 0
try:
merge_file = join(merge_base, "file")
merge_link = join(merge_base, "link")
try:
os.unlink(merge_link)
except FileNotFoundError:
pass
os.symlink("file", merge_link)
touch(merge_file, b"hello")
try:
native_link = mergerfs_fullpath(merge_link)
except OSError:
return 0
try:
native_file = mergerfs_fullpath(merge_file)
except OSError:
return 0
try:
os.unlink(merge_link)
except FileNotFoundError:
pass
os.symlink("file", merge_link)
try:
native_link = mergerfs_fullpath(merge_link)
except OSError:
return 0
expected_native_link = os.path.join(os.path.dirname(native_file), "link")
if not os.path.islink(native_link):
if os.path.islink(expected_native_link):
native_link = expected_native_link
else:
return 0
native_missing = os.path.join(os.path.dirname(native_file), "missing")
mst, nst, err = call_pair(
"statx regular",
lambda: statx_call(merge_file),
lambda: statx_call(native_file),
)
if err:
return fail(err)
if not stcmp(mst, nst):
return fail(
"statx basic mismatch\n"
f" mergerfs path: {merge_file}\n"
f" native path: {native_file}\n"
f" mergerfs statx: {statx_summary(mst)}\n"
f" native statx: {statx_summary(nst)}"
)
mst, nst, err = call_pair(
"statx symlink nofollow",
lambda: statx_call(merge_link, flags=AT_SYMLINK_NOFOLLOW),
lambda: statx_call(native_link, flags=AT_SYMLINK_NOFOLLOW),
)
if err:
return fail(err)
if (mst.stx_mode & 0xF000) != (nst.stx_mode & 0xF000):
return fail(
"statx symlink type mismatch\n"
f" mergerfs path: {merge_link}\n"
f" native path: {native_link}\n"
f" mergerfs statx: {statx_summary(mst)}\n"
f" native statx: {statx_summary(nst)}"
)
err = expect_same_errno(
"statx ENOENT",
lambda: statx_call(join(merge_base, "missing")),
lambda: statx_call(native_missing),
)
if err:
return fail(err)
err = expect_same_errno(
"statx ENOTDIR",
lambda: statx_call(join(merge_file, "child")),
lambda: statx_call(join(native_file, "child")),
)
if err:
return fail(err)
# user.mergerfs.fullpath for a symlink may resolve to the target file path
# depending on policy behavior. For AT_SYMLINK_NOFOLLOW comparisons we need
# the underlying symlink path itself.
expected_native_link = os.path.join(os.path.dirname(native_file), "link")
if not os.path.islink(native_link):
if os.path.islink(expected_native_link):
native_link = expected_native_link
else:
return 0
native_missing = os.path.join(os.path.dirname(native_file), "missing")
mst, nst, err = call_pair(
"statx regular",
lambda: statx_call(merge_file),
lambda: statx_call(native_file),
)
if err:
return fail(err)
if not stcmp(mst, nst):
return fail(
"statx basic mismatch\n"
f" mergerfs path: {merge_file}\n"
f" native path: {native_file}\n"
f" mergerfs statx: {statx_summary(mst)}\n"
f" native statx: {statx_summary(nst)}"
)
mst, nst, err = call_pair(
"statx symlink nofollow",
lambda: statx_call(merge_link, flags=AT_SYMLINK_NOFOLLOW),
lambda: statx_call(native_link, flags=AT_SYMLINK_NOFOLLOW),
)
if err:
return fail(err)
if (mst.stx_mode & 0xF000) != (nst.stx_mode & 0xF000):
return fail(
"statx symlink type mismatch\n"
f" mergerfs path: {merge_link}\n"
f" native path: {native_link}\n"
f" mergerfs statx: {statx_summary(mst)}\n"
f" native statx: {statx_summary(nst)}"
)
err = expect_same_errno(
"statx ENOENT",
lambda: statx_call(join(merge_base, "missing")),
lambda: statx_call(native_missing),
)
if err:
return fail(err)
err = expect_same_errno(
"statx ENOTDIR",
lambda: statx_call(join(merge_file, "child")),
lambda: statx_call(join(native_file, "child")),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+30 -30
View File
@@ -9,6 +9,7 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -28,40 +29,39 @@ def syncfs_fd(fd):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_syncfs <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"x")
touch(native_file, b"x")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
err = compare_calls("syncfs success", lambda: syncfs_fd(mfd), lambda: syncfs_fd(nfd))
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
touch(merge_file, b"x")
touch(native_file, b"x")
err = compare_calls("syncfs EBADF", lambda: syncfs_fd(-1), lambda: syncfs_fd(-1))
if err:
return fail(err)
mfd = os.open(merge_file, os.O_RDONLY)
nfd = os.open(native_file, os.O_RDONLY)
try:
err = compare_calls("syncfs success", lambda: syncfs_fd(mfd), lambda: syncfs_fd(nfd))
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
err = compare_calls("syncfs EBADF", lambda: syncfs_fd(-1), lambda: syncfs_fd(-1))
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+38 -41
View File
@@ -7,6 +7,7 @@ import tempfile
from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import mergerfs_mount
from posix_parity import join
from posix_parity import temp_dir
@@ -25,55 +26,51 @@ def errno_of(call):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_tmpfile <mountpoint>", file=sys.stderr)
return 1
if not hasattr(os, "O_TMPFILE"):
return 0
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
merge_dir = merge_base
native_dir = native_base
merge_dir = merge_base
native_dir = native_base
m_err = errno_of(lambda: open_tmpfile(merge_dir))
n_err = errno_of(lambda: open_tmpfile(native_dir))
if m_err == errno.EOPNOTSUPP:
return 0
if m_err != n_err:
return fail(f"O_TMPFILE support mismatch mergerfs_errno={m_err} native_errno={n_err}")
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:
return fail(f"O_TMPFILE support mismatch mergerfs_errno={m_err} native_errno={n_err}")
if m_err in (errno.EOPNOTSUPP, errno.EISDIR, errno.EINVAL, errno.ENOSYS):
return 0
# If unsupported on both, treat as skip-pass.
if m_err in (errno.EOPNOTSUPP, errno.EISDIR, errno.EINVAL, errno.ENOSYS):
return 0
mfd = open_tmpfile(merge_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")
if mw != nw:
return fail(f"O_TMPFILE write count mismatch mergerfs={mw} native={nw}")
finally:
os.close(mfd)
os.close(nfd)
mfd = open_tmpfile(merge_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")
if mw != nw:
return fail(f"O_TMPFILE write count mismatch mergerfs={mw} native={nw}")
finally:
os.close(mfd)
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+39 -39
View File
@@ -9,57 +9,57 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_truncate_ftruncate <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_file])
touch(merge_file, b"1234567890")
touch(native_file, b"1234567890")
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
err = compare_calls("truncate shrink", lambda: os.truncate(merge_file, 3), lambda: os.truncate(native_file, 3))
if err:
return fail(err)
cleanup_paths([merge_file])
touch(merge_file, b"1234567890")
touch(native_file, b"1234567890")
err = compare_calls("truncate ENOENT", lambda: os.truncate(merge_missing, 1), lambda: os.truncate(native_missing, 1))
if err:
return fail(err)
err = compare_calls("truncate shrink", lambda: os.truncate(merge_file, 3), lambda: os.truncate(native_file, 3))
if err:
return fail(err)
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
err = compare_calls("ftruncate grow", lambda: os.ftruncate(mfd, 16), lambda: os.ftruncate(nfd, 16))
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
err = compare_calls("truncate ENOENT", lambda: os.truncate(merge_missing, 1), lambda: os.truncate(native_missing, 1))
if err:
return fail(err)
err = compare_calls("ftruncate EBADF", lambda: os.ftruncate(-1, 4), lambda: os.ftruncate(-1, 4))
if err:
return fail(err)
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
err = compare_calls("ftruncate grow", lambda: os.ftruncate(mfd, 16), lambda: os.ftruncate(nfd, 16))
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
err = compare_calls("ftruncate EBADF", lambda: os.ftruncate(-1, 4), lambda: os.ftruncate(-1, 4))
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+50 -50
View File
@@ -9,70 +9,70 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_unlink_rename <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_a = join(merge_base, "a")
native_a = join(native_base, "a")
merge_b = join(merge_base, "b")
native_b = join(native_base, "b")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_a, merge_b, merge_notdir])
touch(merge_a, b"a")
touch(native_a, b"a")
merge_a = join(merge_base, "a")
native_a = join(native_base, "a")
merge_b = join(merge_base, "b")
native_b = join(native_base, "b")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
err = compare_calls("unlink success", lambda: os.unlink(merge_a), lambda: os.unlink(native_a))
if err:
return fail(err)
cleanup_paths([merge_a, merge_b, merge_notdir])
touch(merge_a, b"a")
touch(native_a, b"a")
err = compare_calls("unlink ENOENT", lambda: os.unlink(merge_missing), lambda: os.unlink(native_missing))
if err:
return fail(err)
err = compare_calls("unlink success", lambda: os.unlink(merge_a), lambda: os.unlink(native_a))
if err:
return fail(err)
touch(merge_a, b"a")
touch(native_a, b"a")
touch(merge_b, b"b")
touch(native_b, b"b")
err = compare_calls("unlink ENOENT", lambda: os.unlink(merge_missing), lambda: os.unlink(native_missing))
if err:
return fail(err)
err = compare_calls("rename overwrite", lambda: os.rename(merge_a, merge_b), lambda: os.rename(native_a, native_b))
if err:
return fail(err)
touch(merge_a, b"a")
touch(native_a, b"a")
touch(merge_b, b"b")
touch(native_b, b"b")
err = compare_calls("rename ENOENT", lambda: os.rename(merge_missing, merge_a), lambda: os.rename(native_missing, native_a))
if err:
return fail(err)
err = compare_calls("rename overwrite", lambda: os.rename(merge_a, merge_b), lambda: os.rename(native_a, native_b))
if err:
return fail(err)
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
err = compare_calls(
"rename ENOTDIR",
lambda: os.rename(join(merge_notdir, "child"), merge_a),
lambda: os.rename(join(native_notdir, "child"), native_a),
)
if err:
return fail(err)
err = compare_calls("rename ENOENT", lambda: os.rename(merge_missing, merge_a), lambda: os.rename(native_missing, native_a))
if err:
return fail(err)
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
err = compare_calls(
"rename ENOTDIR",
lambda: os.rename(join(merge_notdir, "child"), merge_a),
lambda: os.rename(join(native_notdir, "child"), native_a),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+57 -57
View File
@@ -10,76 +10,76 @@ from posix_parity import cleanup_paths
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_utimens <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
cleanup_paths([merge_file])
touch(merge_file, b"x")
touch(native_file, b"x")
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
now = time.time()
times = (now - 1000, now - 500)
cleanup_paths([merge_file])
touch(merge_file, b"x")
touch(native_file, b"x")
err = compare_calls(
"utime path success",
lambda: os.utime(merge_file, times=times),
lambda: os.utime(native_file, times=times),
)
if err:
return fail(err)
now = time.time()
times = (now - 1000, now - 500)
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
err = compare_calls(
"utime fd success",
lambda: os.utime(mfd, times=times),
lambda: os.utime(nfd, times=times),
)
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
err = compare_calls(
"utime path success",
lambda: os.utime(merge_file, times=times),
lambda: os.utime(native_file, times=times),
)
if err:
return fail(err)
err = compare_calls(
"utime ENOENT",
lambda: os.utime(merge_missing, times=times),
lambda: os.utime(native_missing, times=times),
)
if err:
return fail(err)
mfd = os.open(merge_file, os.O_RDWR)
nfd = os.open(native_file, os.O_RDWR)
try:
err = compare_calls(
"utime fd success",
lambda: os.utime(mfd, times=times),
lambda: os.utime(nfd, times=times),
)
if err:
return fail(err)
finally:
os.close(mfd)
os.close(nfd)
err = compare_calls(
"utime EBADF",
lambda: os.utime(-1, times=times),
lambda: os.utime(-1, times=times),
)
if err:
return fail(err)
err = compare_calls(
"utime ENOENT",
lambda: os.utime(merge_missing, times=times),
lambda: os.utime(native_missing, times=times),
)
if err:
return fail(err)
err = compare_calls(
"utime EBADF",
lambda: os.utime(-1, times=times),
lambda: os.utime(-1, times=times),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+122 -121
View File
@@ -9,6 +9,7 @@ from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -31,135 +32,135 @@ def has_attr(name):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_xattr <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
xname = "user.posix_parity"
xvalue = b"parity-check"
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
touch(merge_file, b"x")
touch(native_file, b"x")
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
err = compare_calls(
"setxattr success",
lambda: os.setxattr(merge_file, xname, xvalue),
lambda: os.setxattr(native_file, xname, xvalue),
)
if err:
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
os.setxattr(merge_file, xname, xvalue)
except OSError as exc:
err += " | " + format_oserror("mergerfs setxattr", exc)
try:
os.setxattr(native_file, xname, xvalue)
except OSError as exc:
err += " | " + format_oserror("native setxattr", exc)
return fail(err)
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
err = compare_calls(
"getxattr success",
lambda: os.getxattr(merge_file, xname),
lambda: os.getxattr(native_file, xname),
lambda lhs, rhs: lhs == rhs,
)
if err:
try:
os.getxattr(merge_file, xname)
except OSError as exc:
err += " | " + format_oserror("mergerfs getxattr", exc)
try:
os.getxattr(native_file, xname)
except OSError as exc:
err += " | " + format_oserror("native getxattr", exc)
return fail(err)
merge_file = join(merge_base, "file")
native_file = join(native_base, "file")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
err = compare_calls(
"listxattr includes key",
lambda: os.listxattr(merge_file),
lambda: os.listxattr(native_file),
has_attr(xname),
)
if err:
try:
m_list = os.listxattr(merge_file)
err += f" | mergerfs list={m_list!r}"
except OSError as exc:
err += " | " + format_oserror("mergerfs listxattr", exc)
try:
n_list = os.listxattr(native_file)
err += f" | native list={n_list!r}"
except OSError as exc:
err += " | " + format_oserror("native listxattr", exc)
return fail(err)
touch(merge_file, b"x")
touch(native_file, b"x")
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
err = compare_calls(
"removexattr success",
lambda: os.removexattr(merge_file, xname),
lambda: os.removexattr(native_file, xname),
)
if err:
try:
os.removexattr(merge_file, xname)
except OSError as exc:
err += " | " + format_oserror("mergerfs removexattr", exc)
try:
os.removexattr(native_file, xname)
except OSError as exc:
err += " | " + format_oserror("native removexattr", exc)
return fail(err)
xname = "user.posix_parity"
xvalue = b"parity-check"
err = compare_calls(
"getxattr missing attr",
lambda: os.getxattr(merge_file, xname),
lambda: os.getxattr(native_file, xname),
)
if err:
try:
os.getxattr(merge_file, xname)
except OSError as exc:
err += " | " + format_oserror("mergerfs getxattr missing", exc)
try:
os.getxattr(native_file, xname)
except OSError as exc:
err += " | " + format_oserror("native getxattr missing", exc)
return fail(err)
err = compare_calls(
"setxattr success",
lambda: os.setxattr(merge_file, xname, xvalue),
lambda: os.setxattr(native_file, xname, xvalue),
)
if err:
try:
os.setxattr(merge_file, xname, xvalue)
except OSError as exc:
err += " | " + format_oserror("mergerfs setxattr", exc)
try:
os.setxattr(native_file, xname, xvalue)
except OSError as exc:
err += " | " + format_oserror("native setxattr", exc)
return fail(err)
err = compare_calls(
"setxattr ENOENT",
lambda: os.setxattr(merge_missing, xname, xvalue),
lambda: os.setxattr(native_missing, xname, xvalue),
)
if err:
return fail(err)
err = compare_calls(
"getxattr success",
lambda: os.getxattr(merge_file, xname),
lambda: os.getxattr(native_file, xname),
lambda lhs, rhs: lhs == rhs,
)
if err:
try:
os.getxattr(merge_file, xname)
except OSError as exc:
err += " | " + format_oserror("mergerfs getxattr", exc)
try:
os.getxattr(native_file, xname)
except OSError as exc:
err += " | " + format_oserror("native getxattr", exc)
return fail(err)
err = compare_calls(
"getxattr ENOTDIR",
lambda: os.getxattr(join(merge_notdir, "child"), xname),
lambda: os.getxattr(join(native_notdir, "child"), xname),
)
if err:
return fail(err)
err = compare_calls(
"listxattr includes key",
lambda: os.listxattr(merge_file),
lambda: os.listxattr(native_file),
has_attr(xname),
)
if err:
try:
m_list = os.listxattr(merge_file)
err += f" | mergerfs list={m_list!r}"
except OSError as exc:
err += " | " + format_oserror("mergerfs listxattr", exc)
try:
n_list = os.listxattr(native_file)
err += f" | native list={n_list!r}"
except OSError as exc:
err += " | " + format_oserror("native listxattr", exc)
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
err = compare_calls(
"removexattr success",
lambda: os.removexattr(merge_file, xname),
lambda: os.removexattr(native_file, xname),
)
if err:
try:
os.removexattr(merge_file, xname)
except OSError as exc:
err += " | " + format_oserror("mergerfs removexattr", exc)
try:
os.removexattr(native_file, xname)
except OSError as exc:
err += " | " + format_oserror("native removexattr", exc)
return fail(err)
err = compare_calls(
"getxattr missing attr",
lambda: os.getxattr(merge_file, xname),
lambda: os.getxattr(native_file, xname),
)
if err:
try:
os.getxattr(merge_file, xname)
except OSError as exc:
err += " | " + format_oserror("mergerfs getxattr missing", exc)
try:
os.getxattr(native_file, xname)
except OSError as exc:
err += " | " + format_oserror("native getxattr missing", exc)
return fail(err)
err = compare_calls(
"setxattr ENOENT",
lambda: os.setxattr(merge_missing, xname, xvalue),
lambda: os.setxattr(native_missing, xname, xvalue),
)
if err:
return fail(err)
err = compare_calls(
"getxattr ENOTDIR",
lambda: os.getxattr(join(merge_notdir, "child"), xname),
lambda: os.getxattr(join(native_notdir, "child"), xname),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+121 -121
View File
@@ -8,6 +8,7 @@ import tempfile
from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -88,155 +89,154 @@ def lremovexattr(path, name):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_xattr_matrix <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
mount = sys.argv[1]
root_m = merge_base
root_n = native_base
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
file_m = join(root_m, "file")
file_n = join(root_n, "file")
dir_m = join(root_m, "dir")
dir_n = join(root_n, "dir")
link_m = join(root_m, "link")
link_n = join(root_n, "link")
root_m = merge_base
root_n = native_base
touch(file_m, b"x")
touch(file_n, b"x")
os.makedirs(dir_m, exist_ok=True)
os.makedirs(dir_n, exist_ok=True)
try:
os.unlink(link_m)
except FileNotFoundError:
pass
try:
os.unlink(link_n)
except FileNotFoundError:
pass
os.symlink("file", link_m)
os.symlink("file", link_n)
file_m = join(root_m, "file")
file_n = join(root_n, "file")
dir_m = join(root_m, "dir")
dir_n = join(root_n, "dir")
link_m = join(root_m, "link")
link_n = join(root_n, "link")
objs = [
("file", file_m, file_n),
("dir", dir_m, dir_n),
]
touch(file_m, b"x")
touch(file_n, b"x")
os.makedirs(dir_m, exist_ok=True)
os.makedirs(dir_n, exist_ok=True)
try:
os.unlink(link_m)
except FileNotFoundError:
pass
try:
os.unlink(link_n)
except FileNotFoundError:
pass
os.symlink("file", link_m)
os.symlink("file", link_n)
xbase = "user.xattr_matrix"
objs = [
("file", file_m, file_n),
("dir", dir_m, dir_n),
]
for obj_name, path_m, path_n in objs:
xname = f"{xbase}.{obj_name}"
xbase = "user.xattr_matrix"
err = compare(
f"setxattr default {obj_name}",
lambda p=path_m, n=xname: os.setxattr(p, n, b"v1"),
lambda p=path_n, n=xname: os.setxattr(p, n, b"v1"),
)
if err:
return fail(err)
for obj_name, path_m, path_n in objs:
xname = f"{xbase}.{obj_name}"
err = getxattr_cmp(path_m, path_n, xname)
if err:
return fail(err)
err = compare(
f"setxattr default {obj_name}",
lambda p=path_m, n=xname: os.setxattr(p, n, b"v1"),
lambda p=path_n, n=xname: os.setxattr(p, n, b"v1"),
)
if err:
return fail(err)
err = list_has_cmp(path_m, path_n, xname)
if err:
return fail(err)
err = getxattr_cmp(path_m, path_n, xname)
if err:
return fail(err)
if hasattr(os, "XATTR_CREATE"):
err = compare(
f"setxattr CREATE existing {obj_name}",
lambda p=path_m, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE),
lambda p=path_n, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE),
)
if err:
return fail(err)
err = list_has_cmp(path_m, path_n, xname)
if err:
return fail(err)
if hasattr(os, "XATTR_REPLACE"):
err = compare(
f"setxattr REPLACE existing {obj_name}",
lambda p=path_m, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE),
lambda p=path_n, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE),
)
if err:
return fail(err)
if hasattr(os, "XATTR_CREATE"):
err = getxattr_cmp(path_m, path_n, xname)
if err:
return fail(err)
missing_name = f"{xname}.missing"
if hasattr(os, "XATTR_REPLACE"):
err = compare(
f"setxattr REPLACE missing {obj_name}",
lambda p=path_m, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE),
lambda p=path_n, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE),
)
if err:
return fail(err)
err = compare(
f"removexattr existing {obj_name}",
lambda p=path_m, n=xname: os.removexattr(p, n),
lambda p=path_n, n=xname: os.removexattr(p, n),
)
if err:
return fail(err)
err = compare(
f"removexattr missing {obj_name}",
lambda p=path_m, n=xname: os.removexattr(p, n),
lambda p=path_n, n=xname: os.removexattr(p, n),
)
if err:
return fail(err)
lxname = f"{xbase}.link"
err = compare(
f"setxattr CREATE existing {obj_name}",
lambda p=path_m, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE),
lambda p=path_n, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE),
"lsetxattr symlink default",
lambda: lsetxattr(link_m, lxname, b"lv1"),
lambda: lsetxattr(link_n, lxname, b"lv1"),
)
if err:
return fail(err)
if hasattr(os, "XATTR_REPLACE"):
err = compare(
f"setxattr REPLACE existing {obj_name}",
lambda p=path_m, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE),
lambda p=path_n, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE),
"lgetxattr symlink",
lambda: lgetxattr(link_m, lxname),
lambda: lgetxattr(link_n, lxname),
lambda a, b: a == b,
)
if err:
return fail(err)
err = getxattr_cmp(path_m, path_n, xname)
if err:
return fail(err)
missing_name = f"{xname}.missing"
if hasattr(os, "XATTR_REPLACE"):
err = compare(
f"setxattr REPLACE missing {obj_name}",
lambda p=path_m, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE),
lambda p=path_n, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE),
"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(
f"removexattr existing {obj_name}",
lambda p=path_m, n=xname: os.removexattr(p, n),
lambda p=path_n, n=xname: os.removexattr(p, n),
)
if err:
return fail(err)
err = compare(
"lremovexattr symlink",
lambda: lremovexattr(link_m, lxname),
lambda: lremovexattr(link_n, lxname),
)
if err:
return fail(err)
err = compare(
f"removexattr missing {obj_name}",
lambda p=path_m, n=xname: os.removexattr(p, n),
lambda p=path_n, n=xname: os.removexattr(p, n),
)
if err:
return fail(err)
lxname = f"{xbase}.link"
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: 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: 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: lremovexattr(link_m, lxname),
lambda: lremovexattr(link_n, lxname),
)
if err:
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+141 -141
View File
@@ -8,6 +8,7 @@ import tempfile
from posix_parity import cleanup_dir
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
@@ -16,7 +17,7 @@ libc.readlink.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t]
libc.readlink.restype = ctypes.c_ssize_t
def readlink_raw(path: str, bufsiz: int):
def readlink_raw(path, bufsiz):
buf = ctypes.create_string_buffer(bufsiz)
ctypes.set_errno(0)
rv = libc.readlink(path.encode(), buf, bufsiz)
@@ -25,11 +26,11 @@ def readlink_raw(path: str, bufsiz: int):
return rv, err, bytes(buf)
def compare_case(name: str,
merge_path: str,
native_path: str,
bufsiz: int,
expect_errno: int = None):
def compare_case(name,
merge_path,
native_path,
bufsiz,
expect_errno=None):
m_rv, m_errno, m_buf = readlink_raw(merge_path, bufsiz)
n_rv, n_errno, n_buf = readlink_raw(native_path, bufsiz)
@@ -46,158 +47,157 @@ def compare_case(name: str,
def main():
if len(sys.argv) != 2:
print("usage: TEST_readlink_semantics <mountpoint>", file=sys.stderr)
return 1
try:
with mergerfs_mount() as (mount, _):
target = "target-abcdefghijklmnopqrstuvwxyz"
mount = sys.argv[1]
target = "target-abcdefghijklmnopqrstuvwxyz"
with tempfile.TemporaryDirectory() as native_dir:
merge_base = temp_dir(mount)
try:
native_base = join(native_dir, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
paths = {
"merge_link": join(merge_base, "readlink-semantics-link"),
"native_link": join(native_base, "readlink-semantics-link"),
"merge_regular": join(merge_base, "readlink-semantics-regular"),
"native_regular": join(native_base, "readlink-semantics-regular"),
"merge_notdir": join(merge_base, "readlink-semantics-notdir"),
"native_notdir": join(native_base, "readlink-semantics-notdir"),
"merge_loop": join(merge_base, "readlink-semantics-loop"),
"native_loop": join(native_base, "readlink-semantics-loop"),
"merge_private_dir": join(merge_base, "readlink-semantics-private"),
"native_private_dir": join(native_base, "readlink-semantics-private"),
}
paths["merge_private_link"] = os.path.join(paths["merge_private_dir"], "link")
paths["native_private_link"] = os.path.join(paths["native_private_dir"], "link")
for p in (
paths["merge_private_link"],
paths["merge_link"],
paths["merge_regular"],
paths["merge_notdir"],
paths["merge_loop"],
):
with tempfile.TemporaryDirectory() as native_dir:
merge_base = temp_dir(mount)
try:
os.unlink(p)
except FileNotFoundError:
pass
native_base = join(native_dir, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
try:
os.rmdir(paths["merge_private_dir"])
except FileNotFoundError:
pass
except OSError:
pass
paths = {
"merge_link": join(merge_base, "readlink-semantics-link"),
"native_link": join(native_base, "readlink-semantics-link"),
"merge_regular": join(merge_base, "readlink-semantics-regular"),
"native_regular": join(native_base, "readlink-semantics-regular"),
"merge_notdir": join(merge_base, "readlink-semantics-notdir"),
"native_notdir": join(native_base, "readlink-semantics-notdir"),
"merge_loop": join(merge_base, "readlink-semantics-loop"),
"native_loop": join(native_base, "readlink-semantics-loop"),
"merge_private_dir": join(merge_base, "readlink-semantics-private"),
"native_private_dir": join(native_base, "readlink-semantics-private"),
}
paths["merge_private_link"] = os.path.join(paths["merge_private_dir"], "link")
paths["native_private_link"] = os.path.join(paths["native_private_dir"], "link")
os.symlink(target, paths["merge_link"])
os.symlink(target, paths["native_link"])
for p in (
paths["merge_private_link"],
paths["merge_link"],
paths["merge_regular"],
paths["merge_notdir"],
paths["merge_loop"],
):
try:
os.unlink(p)
except FileNotFoundError:
pass
with open(paths["merge_regular"], "w", encoding="ascii"):
pass
with open(paths["native_regular"], "w", encoding="ascii"):
pass
try:
os.rmdir(paths["merge_private_dir"])
except FileNotFoundError:
pass
except OSError:
pass
with open(paths["merge_notdir"], "w", encoding="ascii"):
pass
with open(paths["native_notdir"], "w", encoding="ascii"):
pass
os.symlink(target, paths["merge_link"])
os.symlink(target, paths["native_link"])
os.symlink("readlink-semantics-loop", paths["merge_loop"])
os.symlink("readlink-semantics-loop", paths["native_loop"])
with open(paths["merge_regular"], "w", encoding="ascii"):
pass
with open(paths["native_regular"], "w", encoding="ascii"):
pass
os.makedirs(paths["merge_private_dir"], exist_ok=True)
os.makedirs(paths["native_private_dir"], exist_ok=True)
os.symlink(target, paths["merge_private_link"])
os.symlink(target, paths["native_private_link"])
with open(paths["merge_notdir"], "w", encoding="ascii"):
pass
with open(paths["native_notdir"], "w", encoding="ascii"):
pass
cases = []
os.symlink("readlink-semantics-loop", paths["merge_loop"])
os.symlink("readlink-semantics-loop", paths["native_loop"])
for bufsiz in (0, 1, 2, 5, 8, len(target), len(target) + 1):
cases.append((
f"success/truncation bufsiz={bufsiz}",
paths["merge_link"],
paths["native_link"],
bufsiz,
errno.EINVAL if bufsiz == 0 else None,
))
os.makedirs(paths["merge_private_dir"], exist_ok=True)
os.makedirs(paths["native_private_dir"], exist_ok=True)
os.symlink(target, paths["merge_private_link"])
os.symlink(target, paths["native_private_link"])
cases.extend([
(
"EINVAL non-symlink",
paths["merge_regular"],
paths["native_regular"],
128,
errno.EINVAL,
),
(
"ENOENT missing path",
join(merge_base, "readlink-semantics-missing"),
join(native_base, "readlink-semantics-missing"),
128,
errno.ENOENT,
),
(
"ENOTDIR non-directory prefix",
os.path.join(paths["merge_notdir"], "child"),
os.path.join(paths["native_notdir"], "child"),
128,
errno.ENOTDIR,
),
(
"ELOOP prefix symlink loop",
os.path.join(paths["merge_loop"], "child"),
os.path.join(paths["native_loop"], "child"),
128,
errno.ELOOP,
),
(
"ENAMETOOLONG long pathname",
join(merge_base, "a" * 8192),
join(native_base, "a" * 8192),
128,
errno.ENAMETOOLONG,
),
])
cases = []
os.chmod(paths["merge_private_dir"], 0o600)
os.chmod(paths["native_private_dir"], 0o600)
cases.append((
"EACCES unreadable path prefix",
paths["merge_private_link"],
paths["native_private_link"],
128,
errno.EACCES if os.geteuid() != 0 else None,
))
for bufsiz in (0, 1, 2, 5, 8, len(target), len(target) + 1):
cases.append((
f"success/truncation bufsiz={bufsiz}",
paths["merge_link"],
paths["native_link"],
bufsiz,
errno.EINVAL if bufsiz == 0 else None,
))
for case in cases:
err = compare_case(*case)
if err is not None:
print(err, end="")
return 1
cases.extend([
(
"EINVAL non-symlink",
paths["merge_regular"],
paths["native_regular"],
128,
errno.EINVAL,
),
(
"ENOENT missing path",
join(merge_base, "readlink-semantics-missing"),
join(native_base, "readlink-semantics-missing"),
128,
errno.ENOENT,
),
(
"ENOTDIR non-directory prefix",
os.path.join(paths["merge_notdir"], "child"),
os.path.join(paths["native_notdir"], "child"),
128,
errno.ENOTDIR,
),
(
"ELOOP prefix symlink loop",
os.path.join(paths["merge_loop"], "child"),
os.path.join(paths["native_loop"], "child"),
128,
errno.ELOOP,
),
(
"ENAMETOOLONG long pathname",
join(merge_base, "a" * 8192),
join(native_base, "a" * 8192),
128,
errno.ENAMETOOLONG,
),
])
# Explicitly guard the prior regression.
rv, err, _ = readlink_raw(paths["merge_link"], 1)
if rv != 1 or err != 0:
print(f"bufsiz=1 expected rv=1 errno=0, got rv={rv} errno={err}", end="")
return 1
os.chmod(paths["merge_private_dir"], 0o600)
os.chmod(paths["native_private_dir"], 0o600)
cases.append((
"EACCES unreadable path prefix",
paths["merge_private_link"],
paths["native_private_link"],
128,
errno.EACCES if os.geteuid() != 0 else None,
))
return 0
finally:
try:
os.chmod(paths["merge_private_dir"], 0o700)
except (FileNotFoundError, PermissionError):
pass
for case in cases:
err = compare_case(*case)
if err is not None:
print(err, end="")
return 1
try:
os.chmod(paths["native_private_dir"], 0o700)
except (FileNotFoundError, PermissionError):
pass
rv, err, _ = readlink_raw(paths["merge_link"], 1)
if rv != 1 or err != 0:
print(f"bufsiz=1 expected rv=1 errno=0, got rv={rv} errno={err}", end="")
return 1
cleanup_dir(merge_base)
return 0
finally:
try:
os.chmod(paths["merge_private_dir"], 0o700)
except (FileNotFoundError, PermissionError):
pass
try:
os.chmod(paths["native_private_dir"], 0o700)
except (FileNotFoundError, PermissionError):
pass
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+29 -29
View File
@@ -10,6 +10,7 @@ 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_mount
from posix_parity import temp_dir
from posix_parity import touch
@@ -23,41 +24,40 @@ def get_errno(func):
def main():
if len(sys.argv) != 2:
print("usage: TEST_rmdir_enotempty_priority <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
merge_base = temp_dir(mount)
try:
branches = mergerfs_branches(mount)
if len(branches) < 2:
return 0
with mergerfs_mount(num_branches=2) as (mount, branches):
merge_base = temp_dir(mount)
merge_dir = join(merge_base, "nonempty_dir")
os.makedirs(merge_dir)
try:
if len(branches) < 2:
return 0
child_file = join(merge_dir, "child.txt")
branch0 = branches[0]
native_child = join(branch0, os.path.relpath(child_file, mount))
touch(child_file, b"prevents rmdir")
merge_dir = join(merge_base, "nonempty_dir")
os.makedirs(merge_dir)
err = get_errno(lambda: os.rmdir(merge_dir))
if err != errno.ENOTEMPTY:
return fail(f"rmdir non-empty dir: expected ENOTEMPTY({errno.ENOTEMPTY}), got {err}")
child_file = join(merge_dir, "child.txt")
branch0 = branches[0]
native_child = join(branch0, os.path.relpath(child_file, mount))
touch(child_file, b"prevents rmdir")
os.unlink(child_file)
err = get_errno(lambda: os.rmdir(merge_dir))
if err != 0:
return fail(f"rmdir empty dir after removing child: expected success, got errno={err}")
err = get_errno(lambda: os.rmdir(merge_dir))
if err != errno.ENOTEMPTY:
return fail(f"rmdir non-empty dir: expected ENOTEMPTY({errno.ENOTEMPTY}), got {err}")
return 0
except Exception as exc:
return fail(str(exc))
finally:
cleanup_dir(merge_base)
os.unlink(child_file)
err = get_errno(lambda: os.rmdir(merge_dir))
if err != 0:
return fail(f"rmdir empty dir after removing child: expected success, got errno={err}")
return 0
except Exception as exc:
return fail(str(exc))
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+32 -25
View File
@@ -1,41 +1,48 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
(srcfd, src) = tempfile.mkstemp(dir=sys.argv[1])
(tgtfd, tgt) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(srcfd, src) = tempfile.mkstemp(dir=mount)
(tgtfd, tgt) = tempfile.mkstemp(dir=mount)
st_src_before = os.fstat(srcfd)
st_tgt_before = os.fstat(tgtfd)
st_src_before = os.fstat(srcfd)
st_tgt_before = os.fstat(tgtfd)
os.unlink(tgt)
os.rename(src, tgt)
os.unlink(tgt)
os.rename(src, tgt)
st_src_after = os.fstat(srcfd)
st_tgt_after = os.fstat(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_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
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
os.close(srcfd)
os.close(tgtfd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+26 -19
View File
@@ -1,35 +1,42 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
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
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.unlink(filepath)
os.unlink(filepath)
os.posix_fallocate(fd, 1024, 2048)
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
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
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+26 -19
View File
@@ -1,36 +1,43 @@
#!/usr/bin/env python3
import tempfile
import os
import stat
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
os.fchmod(fd, 0o700)
os.fchmod(fd, 0o700)
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
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.unlink(filepath)
os.unlink(filepath)
os.fchmod(fd, 0o755)
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
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
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+31 -21
View File
@@ -1,37 +1,47 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
os.fchown(fd, os.getuid(), os.getgid())
uid = os.getuid()
gid = 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.fchown(fd, uid, gid)
os.unlink(filepath)
st1 = os.fstat(fd)
if st1.st_uid != uid or st1.st_gid != gid:
os.close(fd)
os.unlink(filepath)
print("fchown before unlink: expected uid={} gid={}, got uid={} gid={}".format(
uid, gid, st1.st_uid, st1.st_gid), end='')
return 1
os.fchown(fd, os.getuid(), os.getgid())
os.unlink(filepath)
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.fchown(fd, uid, gid)
os.close(fd)
return 0
st2 = os.fstat(fd)
if st2.st_uid != uid or st2.st_gid != gid:
os.close(fd)
print("fchown after unlink: expected uid={} gid={}, got uid={} gid={}".format(
uid, gid, st2.st_uid, st2.st_gid), end='')
return 1
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+34 -27
View File
@@ -1,45 +1,52 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
x_before = os.lstat(filepath)
x = os.fstat(fd)
x_before = os.lstat(filepath)
x = 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
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
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
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)
os.unlink(filepath)
y = os.fstat(fd)
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_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
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
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+26 -19
View File
@@ -1,35 +1,42 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
os.ftruncate(fd, 1024)
os.ftruncate(fd, 10)
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
st1 = os.fstat(fd)
if st1.st_size != 10:
os.close(fd)
os.unlink(filepath)
print("ftruncate before unlink: expected size 10, got {}".format(st1.st_size), end='')
return 1
os.unlink(filepath)
os.unlink(filepath)
os.ftruncate(fd, 2048)
os.ftruncate(fd, 20)
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
st2 = os.fstat(fd)
if st2.st_size != 20:
os.close(fd)
print("ftruncate after unlink: expected size 20, got {}".format(st2.st_size), end='')
return 1
os.close(fd)
return 0
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+30 -25
View File
@@ -1,41 +1,46 @@
#!/usr/bin/env python3
import tempfile
import os
import sys
import tempfile
from posix_parity import mergerfs_mount
def main():
atime = 1234
mtime = 5678
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
os.utime(fd, (atime, mtime))
times = (1234567890, 9876543210)
os.utime(fd, times=times)
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
st1 = os.fstat(fd)
if st1.st_atime != times[0] or st1.st_mtime != times[1]:
os.close(fd)
os.unlink(filepath)
print("futimens before unlink: expected atime={} mtime={}, got atime={} mtime={}".format(
times[0], times[1], st1.st_atime, st1.st_mtime), end='')
return 1
os.unlink(filepath)
os.unlink(filepath)
atime2 = 9999
mtime2 = 8888
os.utime(fd, (atime2, mtime2))
times2 = (1111111111, 2222222222)
os.utime(fd, times=times2)
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
st2 = os.fstat(fd)
if st2.st_atime != times2[0] or st2.st_mtime != times2[1]:
os.close(fd)
print("futimens after unlink: expected atime={} mtime={}, got atime={} mtime={}".format(
times2[0], times2[1], st2.st_atime, st2.st_mtime), end='')
return 1
os.close(fd)
return 0
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+73
View File
@@ -5,7 +5,11 @@ import errno
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import time
from contextlib import contextmanager
libc = ctypes.CDLL(None, use_errno=True)
@@ -13,6 +17,75 @@ libc.access.argtypes = [ctypes.c_char_p, ctypes.c_int]
libc.access.restype = ctypes.c_int
def find_mergerfs():
script_dir = os.path.dirname(os.path.realpath(__file__))
build_path = os.path.join(script_dir, "..", "build", "mergerfs")
if os.path.isfile(build_path):
return os.path.realpath(build_path)
return shutil.which("mergerfs")
def find_fusermount():
return shutil.which("fusermount3") or shutil.which("fusermount")
def has_tool(name):
return shutil.which(name) is not None
def mount_mergerfs(branches, mountpoint, options=None):
mergerfs_bin = find_mergerfs()
if mergerfs_bin is None:
raise RuntimeError("mergerfs binary not found")
if options is None:
options = "defaults,use_ino,category.create=mfs"
if os.geteuid() == 0:
options += ",allow_other"
branch_arg = ":".join(branches)
cmd = [mergerfs_bin, branch_arg, mountpoint, "-o", options]
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def unmount_mergerfs(mountpoint):
um = find_fusermount()
if um is None:
raise RuntimeError("fusermount3/fusermount not found")
for attempt in range(3):
u = subprocess.run([um, "-u", mountpoint], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if u.returncode == 0:
return
time.sleep(1)
subprocess.run([um, "-z", mountpoint], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@contextmanager
def mergerfs_mount(num_branches=2, options=None):
mergerfs_bin = find_mergerfs()
if mergerfs_bin is None:
raise RuntimeError("mergerfs binary not found")
if find_fusermount() is None:
raise RuntimeError("fusermount3/fusermount not found")
tests_dir = os.path.dirname(os.path.realpath(__file__))
tmp_parent = os.path.join(tests_dir, ".test_tmp")
os.makedirs(tmp_parent, exist_ok=True)
tmpdir = tempfile.mkdtemp(prefix="mergerfs_test_", dir=tmp_parent)
try:
branches = [os.path.join(tmpdir, f"branch{i}") for i in range(num_branches)]
for b in branches:
os.makedirs(b, exist_ok=True)
mountpoint = os.path.join(tmpdir, "mp")
os.makedirs(mountpoint, exist_ok=True)
mount_mergerfs(branches, mountpoint, options)
yield mountpoint, branches
finally:
try:
unmount_mergerfs(mountpoint)
except Exception:
pass
shutil.rmtree(tmpdir, ignore_errors=True)
def temp_dir(root):
"""Create a temporary directory within the given root directory."""
return tempfile.mkdtemp(dir=root)
+2 -6
View File
@@ -8,10 +8,6 @@ import subprocess
SKIP = 77
if len(sys.argv) != 2:
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)
@@ -22,9 +18,9 @@ for entry in os.scandir(test_path):
continue
try:
print(entry.name + ': ', end='')
print(entry.name + ': ', end='', flush=True)
fullpath = os.path.join(test_path, entry.name)
args = [fullpath, sys.argv[1]]
args = [fullpath]
rv = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output, _ = rv.communicate(timeout=120)
if rv.returncode == SKIP: