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.
+8 -12
View File
@@ -2,31 +2,25 @@
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:
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")
@@ -72,7 +66,9 @@ def main():
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))
+6 -9
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,13 +57,9 @@ 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]
try:
with mergerfs_mount(num_branches=2) as (mount, branches):
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
@@ -81,10 +78,8 @@ def main():
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"))
@@ -108,7 +103,6 @@ def main():
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"))
@@ -146,6 +140,9 @@ def main():
finally:
for branch in branches:
cleanup_dir(join(branch, rel_base))
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -7
View File
@@ -5,16 +5,13 @@ 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:
with mergerfs_mount() as (mount, _):
try:
orig_statfs = mergerfs_get_option(mount, "statfs")
orig_ignore = mergerfs_get_option(mount, "statfs-ignore")
@@ -33,7 +30,6 @@ def main():
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}"
@@ -59,6 +55,9 @@ def main():
pass
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+8 -11
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,16 +24,13 @@ 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]
try:
with mergerfs_mount() as (mount, _):
merge_base = temp_dir(mount)
try:
path = join(merge_base, "file")
xname = "user.mergerfs.cfg.test"
xname = "user.test"
xval = b"v"
touch(path, b"x")
@@ -49,7 +47,7 @@ def main():
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")
return fail(f"xattr=passthrough getxattr returned ENODATA, expected success")
if get_err != 0:
return fail(f"xattr=passthrough getxattr unexpected errno={get_err}")
@@ -60,10 +58,6 @@ def main():
err = get_errno(lambda: os.setxattr(path, xname, xval))
if err != errno.ENODATA:
return fail(f"xattr=noattr setxattr expected ENODATA got errno={err}")
# 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:
@@ -75,6 +69,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+8 -6
View File
@@ -1,16 +1,15 @@
#!/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:
from posix_parity import mergerfs_mount
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
try:
test_data = b"passthrough io test data for create\n" * 100
@@ -96,6 +95,9 @@ def main():
os.unlink(filepath)
except FileNotFoundError:
pass
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+8 -5
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,11 +48,9 @@ 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
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
total_size = NUM_THREADS * len(TEST_DATA)
os.ftruncate(fd, total_size)
@@ -126,6 +126,9 @@ def main():
os.unlink(filepath)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+11 -47
View File
@@ -1,69 +1,33 @@
#!/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:
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
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -9
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,12 +19,8 @@ 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:
with mergerfs_mount(num_branches=2) as (mount, branches):
try:
orig_moveonenospc = mergerfs_get_option(mount, "moveonenospc")
except (PermissionError, FileNotFoundError):
@@ -35,13 +32,10 @@ def main():
return SKIP
raise
branches = mergerfs_branches(mount)
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)
try:
@@ -82,6 +76,9 @@ def main():
return fail(str(exc))
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+10 -3
View File
@@ -1,17 +1,21 @@
#!/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)
found = False
for entry in os.scandir(sys.argv[1]):
for entry in os.scandir(mount):
if entry.name.startswith(".fuse_hidden"):
print("found .fuse_hidden file {}".format(entry.name), end='')
found = True
@@ -21,6 +25,9 @@ def main():
if found:
return 1
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+9 -2
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,7 +30,9 @@ def aligned_read(fd, size):
def main():
(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1])
try:
with mergerfs_mount() as (mount, _):
(fd, filepath) = tempfile.mkstemp(dir=mount)
os.close(fd)
@@ -60,6 +64,9 @@ def main():
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -2,21 +2,18 @@
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]
try:
with mergerfs_mount() as (mount, _):
merge_base = temp_dir(mount)
try:
@@ -62,6 +59,9 @@ def main():
except FileNotFoundError:
pass
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -7
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,12 +51,8 @@ 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:
with mergerfs_mount(num_branches=2) as (mount, branches):
try:
orig_create = mergerfs_get_option(mount, "func.create")
except (PermissionError, FileNotFoundError, OSError):
@@ -68,7 +65,6 @@ def main():
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
@@ -119,6 +115,9 @@ def main():
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__":
+6 -6
View File
@@ -10,17 +10,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -74,6 +71,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+7 -9
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,12 +27,8 @@ def fibmap(fd, block=0):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_bmap <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -45,7 +43,6 @@ def main():
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),
@@ -53,13 +50,11 @@ def main():
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),
@@ -71,6 +66,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -5
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,11 +23,8 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
stcmp = stat_cmp_basic_with_inode if should_compare_inode(mount) else stat_cmp_basic
with tempfile.TemporaryDirectory() as native:
@@ -96,6 +94,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -5
View File
@@ -9,16 +9,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
uid = os.getuid()
gid = os.getgid()
@@ -78,6 +76,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+63 -19
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import errno
import hashlib
import os
import shutil
import sys
import tempfile
@@ -9,21 +11,34 @@ 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_copy_file_range <mountpoint>", file=sys.stderr)
return 1
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():
try:
with mergerfs_mount() as (mount, _):
if not hasattr(os, "copy_file_range"):
return 0
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
with NativeTmp() as native:
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
@@ -79,10 +94,6 @@ def main():
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")
@@ -108,14 +119,23 @@ def main():
return 0
raise
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)
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)
@@ -124,6 +144,27 @@ def main():
"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)
@@ -133,6 +174,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -10,16 +10,13 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -94,6 +91,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -7
View File
@@ -8,17 +8,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -68,7 +65,6 @@ def main():
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()
@@ -78,6 +74,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+15 -16
View File
@@ -10,31 +10,27 @@ 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
mount = sys.argv[1]
stcmp = st_cmp_with_inode if should_compare_inode(mount) else st_cmp
try:
with mergerfs_mount() as (mount, _):
compare_ino = should_compare_inode(mount)
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
@@ -61,13 +57,13 @@ def main():
touch(native_notdir, b"x", 0o644)
err = compare_calls(
"lstat regular", lambda: os.lstat(merge_file), lambda: os.lstat(native_file), stcmp
"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 directory", lambda: os.lstat(merge_dir), lambda: os.lstat(native_dir), stcmp
"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)
@@ -75,7 +71,7 @@ def main():
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)
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:
@@ -97,6 +93,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
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,12 +23,8 @@ def fionread(fd):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_ioctl <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -63,6 +60,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -9,17 +9,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -70,6 +67,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -8
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,12 +51,8 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -97,7 +93,6 @@ def main():
if err:
return fail(err)
# POSIX record lock via fcntl
wrlk = pack_flock(fcntl.F_WRLCK)
unlck = pack_flock(fcntl.F_UNLCK)
@@ -136,6 +131,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -9,17 +9,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -72,6 +69,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -9,17 +9,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -82,6 +79,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -7
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,12 +24,8 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -46,7 +43,6 @@ def main():
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):
@@ -58,6 +54,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
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,12 +112,8 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -173,6 +170,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -8
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,12 +25,8 @@ 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]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -43,13 +40,11 @@ def main():
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):
@@ -72,6 +67,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -8
View File
@@ -7,17 +7,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -30,7 +27,6 @@ def main():
touch(merge_file, b"abc")
touch(native_file, b"abc")
# 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)
@@ -61,7 +57,6 @@ def main():
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)
@@ -82,6 +77,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
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,12 +50,8 @@ def close_and_dirfd_errno(path):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_releasedir <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -74,6 +71,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
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,12 +23,8 @@ def statvfs_cmp(lhs, rhs):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_statfs <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -54,6 +51,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -8
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,11 +173,8 @@ 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]
try:
with mergerfs_mount() as (mount, _):
stcmp = cmp_statx_basic_with_inode if should_compare_inode(mount) else cmp_statx_basic
merge_base = temp_dir(mount)
@@ -203,9 +201,6 @@ def main():
except OSError:
return 0
# 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):
@@ -266,6 +261,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
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,12 +29,8 @@ def syncfs_fd(fd):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_syncfs <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -62,6 +59,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -9
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,15 +26,11 @@ 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:
@@ -45,14 +42,11 @@ def main():
m_err = errno_of(lambda: open_tmpfile(merge_dir))
n_err = errno_of(lambda: open_tmpfile(native_dir))
# mergerfs does not currently support O_TMPFILE, so EOPNOTSUPP is
# the expected result even when the native filesystem supports it.
if m_err == errno.EOPNOTSUPP:
return 0
if m_err != n_err:
return fail(f"O_TMPFILE support mismatch mergerfs_errno={m_err} native_errno={n_err}")
# If unsupported on both, treat as skip-pass.
if m_err in (errno.EOPNOTSUPP, errno.EISDIR, errno.EINVAL, errno.ENOSYS):
return 0
@@ -74,6 +68,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -9,17 +9,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -60,6 +57,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -9,17 +9,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -73,6 +70,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
View File
@@ -10,17 +10,14 @@ 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
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -80,6 +77,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+9 -8
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,14 +32,8 @@ 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"
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -57,6 +52,9 @@ def main():
touch(merge_notdir, b"x")
touch(native_notdir, b"x")
xname = "user.posix_parity"
xvalue = b"parity-check"
err = compare_calls(
"setxattr success",
lambda: os.setxattr(merge_file, xname, xvalue),
@@ -160,6 +158,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
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,12 +89,8 @@ def lremovexattr(path, name):
def main():
if len(sys.argv) != 2:
print("usage: TEST_posix_xattr_matrix <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
with tempfile.TemporaryDirectory() as native:
merge_base = temp_dir(mount)
try:
@@ -237,6 +234,9 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+12 -12
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,11 +47,8 @@ def compare_case(name: str,
def main():
if len(sys.argv) != 2:
print("usage: TEST_readlink_semantics <mountpoint>", file=sys.stderr)
return 1
mount = sys.argv[1]
try:
with mergerfs_mount() as (mount, _):
target = "target-abcdefghijklmnopqrstuvwxyz"
with tempfile.TemporaryDirectory() as native_dir:
@@ -179,7 +177,6 @@ def main():
print(err, end="")
return 1
# 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="")
@@ -198,6 +195,9 @@ def main():
pass
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+6 -6
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,15 +24,11 @@ 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]
try:
with mergerfs_mount(num_branches=2) as (mount, branches):
merge_base = temp_dir(mount)
try:
branches = mergerfs_branches(mount)
if len(branches) < 2:
return 0
@@ -57,6 +54,9 @@ def main():
return fail(str(exc))
finally:
cleanup_dir(merge_base)
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+10 -3
View File
@@ -1,13 +1,17 @@
#!/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)
@@ -35,6 +39,9 @@ def main():
os.close(srcfd)
os.close(tgtfd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+9 -2
View File
@@ -1,12 +1,16 @@
#!/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)
@@ -29,6 +33,9 @@ def main():
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+9 -2
View File
@@ -1,13 +1,17 @@
#!/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)
@@ -30,6 +34,9 @@ def main():
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+18 -8
View File
@@ -1,36 +1,46 @@
#!/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()
os.fchown(fd, uid, gid)
st1 = os.fstat(fd)
if st1.st_uid != os.getuid() or st1.st_gid != os.getgid():
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(
os.getuid(), os.getgid(), st1.st_uid, st1.st_gid), end='')
uid, gid, st1.st_uid, st1.st_gid), end='')
return 1
os.unlink(filepath)
os.fchown(fd, os.getuid(), os.getgid())
os.fchown(fd, uid, gid)
st2 = os.fstat(fd)
if st2.st_uid != os.getuid() or st2.st_gid != os.getgid():
if st2.st_uid != uid or st2.st_gid != gid:
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='')
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__":
+9 -2
View File
@@ -1,12 +1,16 @@
#!/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)
@@ -39,6 +43,9 @@ def main():
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+15 -8
View File
@@ -1,34 +1,41 @@
#!/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:
if st1.st_size != 10:
os.close(fd)
os.unlink(filepath)
print("ftruncate before unlink: expected size 1024, got {}".format(st1.st_size), end='')
print("ftruncate before unlink: expected size 10, got {}".format(st1.st_size), end='')
return 1
os.unlink(filepath)
os.ftruncate(fd, 2048)
os.ftruncate(fd, 20)
st2 = os.fstat(fd)
if st2.st_size != 2048:
if st2.st_size != 20:
os.close(fd)
print("ftruncate after unlink: expected size 2048, got {}".format(st2.st_size), end='')
print("ftruncate after unlink: expected size 20, got {}".format(st2.st_size), end='')
return 1
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__main__":
+17 -12
View File
@@ -1,40 +1,45 @@
#!/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:
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(
atime, mtime, st1.st_atime, st1.st_mtime), end='')
times[0], times[1], st1.st_atime, st1.st_mtime), end='')
return 1
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:
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(
atime2, mtime2, st2.st_atime, st2.st_mtime), end='')
times2[0], times2[1], st2.st_atime, st2.st_mtime), end='')
return 1
os.close(fd)
return 0
except RuntimeError as exc:
print(str(exc), end="")
return 77
if __name__ == "__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: