Add unit and integration tests for recently changed code

Unit tests (29 new in tests/tests.cpp):
- str::eq() and str::startswith() char* nullptr safety
- str::from() u64/s64/int suffix parsing (K/M/G/T, overflow, invalid)
- num::humanize() byte formatting (0, 512, 1K, 1M, 1G, 1T, non-exact)
- RmdirErr priority error accumulation (ENOTEMPTY/EEXIST override rules)
- HashSet inline-to-heap growth, empty string, and large insertion
- fs::inode algorithm switching (valid/invalid names, passthrough raw ino,
  distinct inodes across algorithms)
- RND::rand64() bounds (range, max, shrink_to_rand_elem)
- Branches::Impl copy and move assignment re-links default_minfreespace

Integration tests (5 new in tests/):
- TEST_policy_lup: LUP policy create and search
- TEST_rmdir_enotempty_priority: rmdir returns ENOTEMPTY for non-empty dirs
- TEST_open_after_unlink: read/write via fd after unlink
- TEST_api_xattr: mergerfs xattr API (fullpath, relpath, basepath, allpaths)
- TEST_movefile_enospc: moveonenospc configuration round-trip

Also: remove xattr nosys checks from TEST_cfg_xattr_modes, reformat
integration test scripts to use temp_dir/cleanup_dir helpers, and
minor parameter alignment changes in fuse_getxattr, fuse_init,
fuse_rename.
This commit is contained in:
Antonio SJ Musumeci 2026-04-29 12:19:54 -05:00 • committed by trapexit
parent 50c0627b2b
commit ef1e01b5a5
40 changed files with 2487 additions and 1366 deletions

81
tests/TEST_api_xattr Executable file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python3
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 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")
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")
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:
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:
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:
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 Exception as exc:
return fail(str(exc))
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -4,12 +4,14 @@ import errno
import os
import sys
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_set_option
from posix_parity import parse_allpaths
from posix_parity import temp_dir
from posix_parity import touch
from posix_parity import join
def get_errno(func):
@ -31,9 +33,9 @@ def must_two_branches(paths, op):
return None
def pick_cross_branch_paths(mount):
src = join(mount, "cfg-exdev/src")
dst = join(mount, "cfg-exdev/sub/dst")
def pick_cross_branch_paths(mount, merge_base):
src = join(merge_base, "src")
dst = join(merge_base, "sub/dst")
touch(src, b"src")
os.makedirs(os.path.dirname(dst), exist_ok=True)
@ -68,8 +70,11 @@ def main():
return 1
mount = sys.argv[1]
merge_base = temp_dir(mount)
try:
src, dst = pick_cross_branch_paths(mount)
try:
src, dst = pick_cross_branch_paths(mount, merge_base)
except PermissionError:
return 0
except RuntimeError as exc:
@ -138,6 +143,8 @@ def main():
pass
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,11 +5,13 @@ 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_get_option
from posix_parity import mergerfs_set_option
from posix_parity import temp_dir
from posix_parity import touch
from posix_parity import join
def get_errno(func):
@ -26,7 +28,10 @@ def main():
return 1
mount = sys.argv[1]
path = join(mount, "cfg-xattr-modes/file")
merge_base = temp_dir(mount)
try:
path = join(merge_base, "file")
xname = "user.mergerfs.cfg.test"
xval = b"v"
@ -54,19 +59,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}")
mergerfs_set_option(mount, "xattr", "nosys")
err = get_errno(lambda: os.getxattr(path, xname))
if err != errno.ENOSYS:
return fail(f"xattr=nosys getxattr expected ENOSYS got errno={err}")
err = get_errno(lambda: os.listxattr(path))
if err != errno.ENOSYS:
return fail(f"xattr=nosys listxattr expected ENOSYS got errno={err}")
# In nosys mode runtime control should stop working.
err = get_errno(lambda: mergerfs_get_option(mount, "xattr"))
if err != errno.ENOSYS:
return fail(f"xattr=nosys runtime control expected ENOSYS got errno={err}")
except (PermissionError, FileNotFoundError, OSError):
return 0
finally:
@ -76,6 +68,8 @@ def main():
pass
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

65
tests/TEST_movefile_enospc Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
import errno
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_set_option
from posix_parity import temp_dir
from posix_parity import touch
def get_errno(func):
try:
func()
return 0
except OSError as exc:
return exc.errno
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, OSError):
return 0
branches = mergerfs_branches(mount)
if len(branches) < 2:
return 0
merge_base = temp_dir(mount)
try:
mergerfs_set_option(mount, "moveonenospc", "true")
mergerfs_set_option(mount, "func.moveonenospc", "mfs")
merge_file = join(merge_base, "enospc_test")
touch(merge_file, b"test data for enospc")
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
return 0
except Exception as exc:
try:
mergerfs_set_option(mount, "moveonenospc", orig_moveonenospc)
except Exception:
pass
return fail(str(exc))
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":
raise SystemExit(main())

74
tests/TEST_open_after_unlink Executable file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env python3
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 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")
fd = os.open(merge_file, os.O_RDWR)
if fd < 0:
return fail("open file for R/W failed")
data = os.read(fd, 4096)
if data != b"hello world":
os.close(fd)
return fail(f"read before unlink: expected 'hello world', got {data!r}")
os.unlink(merge_file)
fd2 = os.dup(fd)
os.lseek(fd2, 0, os.SEEK_SET)
data2 = os.read(fd2, 4096)
if data2 != b"hello world":
os.close(fd2)
os.close(fd)
return fail(f"read after unlink via dup: expected 'hello world', got {data2!r}")
os.lseek(fd, 0, os.SEEK_SET)
written = os.write(fd, b"modified")
if written != len(b"modified"):
os.close(fd2)
os.close(fd)
return fail(f"write after unlink: wrote {written}, expected {len(b'modified')}")
os.lseek(fd, 0, os.SEEK_SET)
data3 = os.read(fd, 4096)
if not data3.startswith(b"modified"):
os.close(fd2)
os.close(fd)
return fail(f"read back written data: got {data3!r} after writing 'modified'")
os.close(fd2)
os.close(fd)
return 0
except Exception as exc:
return fail(str(exc))
finally:
try:
os.unlink(join(merge_base, "testfile"))
except FileNotFoundError:
pass
cleanup_dir(merge_base)
if __name__ == "__main__":
raise SystemExit(main())

73
tests/TEST_policy_lup Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
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_set_option
from posix_parity import temp_dir
from posix_parity import touch
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):
return 0
try:
orig_search = mergerfs_get_option(mount, "func.search")
except (PermissionError, FileNotFoundError, OSError):
return 0
try:
orig_action = mergerfs_get_option(mount, "func.action")
except (PermissionError, FileNotFoundError, OSError):
return 0
merge_base = temp_dir(mount)
try:
mergerfs_set_option(mount, "func.create", "lup")
mergerfs_set_option(mount, "func.search", "lup")
path = join(merge_base, "lup_test_file")
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")
if not fullpath:
return fail("LUP create: no fullpath attribute")
path2 = join(merge_base, "lup_test_file2")
touch(path2, b"lup search test")
fullpath2 = os.getxattr(path2, "user.mergerfs.fullpath")
if not fullpath2:
return fail("LUP search: no fullpath attribute")
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)
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -5,10 +5,12 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -20,12 +22,17 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-access/file")
native_file = join(native, "posix-access/file")
merge_notdir = join(mount, "posix-access/notdir")
native_notdir = join(native, "posix-access/notdir")
merge_missing = join(mount, "posix-access/missing")
native_missing = join(native, "posix-access/missing")
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_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
merge_missing = join(merge_base, "missing")
native_missing = join(native_base, "missing")
cleanup_paths([merge_file, merge_notdir])
@ -62,6 +69,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -7,9 +7,11 @@ import struct
import sys
import tempfile
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 temp_dir
from posix_parity import touch
@ -30,8 +32,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-bmap/file")
native_file = join(native, "posix-bmap/file")
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")
touch(merge_file, b"x" * 8192)
touch(native_file, b"x" * 8192)
@ -62,6 +69,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -6,12 +6,14 @@ import stat
import sys
import tempfile
from posix_parity import cleanup_dir
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 should_compare_inode
from posix_parity import stat_cmp_basic
from posix_parity import temp_dir
from posix_parity import touch
@ -28,12 +30,17 @@ def main():
stcmp = stat_cmp_basic_with_inode if should_compare_inode(mount) else stat_cmp_basic
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-chmod/file")
native_file = join(native, "posix-chmod/file")
merge_missing = join(mount, "posix-chmod/missing")
native_missing = join(native, "posix-chmod/missing")
merge_notdir = join(mount, "posix-chmod/notdir")
native_notdir = join(native, "posix-chmod/notdir")
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")
cleanup_paths([merge_file, merge_notdir])
@ -87,6 +94,8 @@ def main():
_ = errno.EPERM
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,10 +4,12 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -21,12 +23,17 @@ def main():
gid = os.getgid()
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-chown/file")
native_file = join(native, "posix-chown/file")
merge_missing = join(mount, "posix-chown/missing")
native_missing = join(native, "posix-chown/missing")
merge_notdir = join(mount, "posix-chown/notdir")
native_notdir = join(native, "posix-chown/notdir")
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")
cleanup_paths([merge_file, merge_notdir])
@ -69,6 +76,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,9 +4,11 @@ import os
import sys
import tempfile
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 temp_dir
from posix_parity import touch
@ -21,10 +23,15 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_src = join(mount, "posix-cfr/src")
merge_dst = join(mount, "posix-cfr/dst")
native_src = join(native, "posix-cfr/src")
native_dst = join(native, "posix-cfr/dst")
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)
@ -74,10 +81,10 @@ def main():
# Exercise very large copy lengths to validate the 64-bit copy result
# path when available. Use sparse files to keep runtime and disk usage low.
large_len = (5 << 30) + 4096
merge_large_src = join(mount, "posix-cfr/src-large")
merge_large_dst = join(mount, "posix-cfr/dst-large")
native_large_src = join(native, "posix-cfr/src-large")
native_large_dst = join(native, "posix-cfr/dst-large")
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"")
@ -118,6 +125,8 @@ def main():
os.close(ndfd)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,10 +5,12 @@ import stat
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
def main():
@ -19,15 +21,17 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
os.makedirs(join(mount, "posix-create"), exist_ok=True)
os.makedirs(join(native, "posix-create"), exist_ok=True)
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(mount, "posix-create/file")
native_file = join(native, "posix-create/file")
merge_exist = join(mount, "posix-create/exist")
native_exist = join(native, "posix-create/exist")
merge_notdir = join(mount, "posix-create/notdir")
native_notdir = join(native, "posix-create/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")
cleanup_paths([merge_file, merge_exist, merge_notdir])
@ -66,8 +70,8 @@ def main():
if err:
return fail(err)
merge_node = join(mount, "posix-create/mknod")
native_node = join(native, "posix-create/mknod")
merge_node = join(merge_base, "mknod")
native_node = join(native_base, "mknod")
cleanup_paths([merge_node])
err = compare_calls(
@ -87,6 +91,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,9 +4,11 @@ import os
import sys
import tempfile
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 temp_dir
from posix_parity import touch
@ -18,10 +20,15 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-fsync/file")
native_file = join(native, "posix-fsync/file")
merge_dir = join(mount, "posix-fsync/dir")
native_dir = join(native, "posix-fsync/dir")
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")
touch(merge_file, b"abc")
touch(native_file, b"abc")
@ -69,6 +76,8 @@ def main():
return fail(f"flush content prefix mismatch mergerfs={mdata!r} native={ndata!r}")
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,12 +5,14 @@ import stat
import sys
import tempfile
from posix_parity import cleanup_dir
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 should_compare_inode
from posix_parity import stat_cmp_basic
from posix_parity import temp_dir
from posix_parity import touch
@ -35,14 +37,19 @@ def main():
stcmp = st_cmp_with_inode if should_compare_inode(mount) else st_cmp
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-getattr/file")
native_file = join(native, "posix-getattr/file")
merge_dir = join(mount, "posix-getattr/dir")
native_dir = join(native, "posix-getattr/dir")
merge_missing = join(mount, "posix-getattr/missing")
native_missing = join(native, "posix-getattr/missing")
merge_notdir = join(mount, "posix-getattr/notdir")
native_notdir = join(native, "posix-getattr/notdir")
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")
cleanup_paths([merge_file, merge_dir, merge_notdir])
@ -88,6 +95,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -7,9 +7,11 @@ import termios
import sys
import tempfile
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 temp_dir
from posix_parity import touch
@ -27,8 +29,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-ioctl/file")
native_file = join(native, "posix-ioctl/file")
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")
touch(merge_file, b"hello-world")
touch(native_file, b"hello-world")
@ -54,6 +61,8 @@ def main():
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,10 +4,12 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -19,14 +21,19 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_src = join(mount, "posix-link/src")
native_src = join(native, "posix-link/src")
merge_dst = join(mount, "posix-link/dst")
native_dst = join(native, "posix-link/dst")
merge_slnk = join(mount, "posix-link/symlink")
native_slnk = join(native, "posix-link/symlink")
merge_missing = join(mount, "posix-link/missing")
native_missing = join(native, "posix-link/missing")
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")
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")
cleanup_paths([merge_src, merge_dst, merge_slnk])
touch(merge_src, b"src", 0o644)
@ -38,8 +45,8 @@ def main():
err = compare_calls(
"link ENOENT source",
lambda: os.link(merge_missing, join(mount, "posix-link/dst2")),
lambda: os.link(native_missing, join(native, "posix-link/dst2")),
lambda: os.link(merge_missing, join(merge_base, "dst2")),
lambda: os.link(native_missing, join(native_base, "dst2")),
)
if err:
return fail(err)
@ -61,6 +68,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -6,9 +6,11 @@ import struct
import sys
import tempfile
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 temp_dir
from posix_parity import touch
@ -25,8 +27,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-locking/file")
native_file = join(native, "posix-locking/file")
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")
touch(merge_file, b"x")
touch(native_file, b"x")
@ -93,6 +100,8 @@ def main():
os.close(nfd2)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,10 +4,12 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -19,14 +21,17 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_dir = join(mount, "posix-mkdir/dir")
native_dir = join(native, "posix-mkdir/dir")
merge_notdir = join(mount, "posix-mkdir/notdir")
native_notdir = join(native, "posix-mkdir/notdir")
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")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
cleanup_paths([merge_dir, merge_notdir])
os.makedirs(join(mount, "posix-mkdir"), exist_ok=True)
os.makedirs(join(native, "posix-mkdir"), exist_ok=True)
err = compare_calls("mkdir success", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755))
if err:
@ -54,17 +59,19 @@ def main():
if err:
return fail(err)
touch(join(mount, "posix-mkdir/nonempty/file"), b"x")
touch(join(native, "posix-mkdir/nonempty/file"), b"x")
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(mount, "posix-mkdir/nonempty")),
lambda: os.rmdir(join(native, "posix-mkdir/nonempty")),
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)
if __name__ == "__main__":

View file

@ -4,10 +4,12 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -19,14 +21,19 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-rw/file")
native_file = join(native, "posix-rw/file")
merge_missing = join(mount, "posix-rw/missing")
native_missing = join(native, "posix-rw/missing")
merge_dir = join(mount, "posix-rw/dir")
native_dir = join(native, "posix-rw/dir")
merge_notdir = join(mount, "posix-rw/notdir")
native_notdir = join(native, "posix-rw/notdir")
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_dir = join(merge_base, "dir")
native_dir = join(native_base, "dir")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
cleanup_paths([merge_file, merge_notdir])
touch(merge_file, b"123456", 0o644)
@ -73,6 +80,8 @@ def main():
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,8 +5,10 @@ import select
import sys
import tempfile
from posix_parity import cleanup_dir
from posix_parity import fail
from posix_parity import join
from posix_parity import temp_dir
from posix_parity import touch
@ -28,8 +30,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-poll/file")
native_file = join(native, "posix-poll/file")
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")
touch(merge_file, b"abcdef")
touch(native_file, b"abcdef")
@ -49,6 +56,8 @@ def main():
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,9 +5,11 @@ import os
import sys
import tempfile
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 temp_dir
from posix_parity import touch
@ -116,10 +118,15 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_dir = join(mount, "posix-readdir/dir")
native_dir = join(native, "posix-readdir/dir")
merge_notdir = join(mount, "posix-readdir/notdir")
native_notdir = join(native, "posix-readdir/notdir")
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")
merge_notdir = join(merge_base, "notdir")
native_notdir = join(native_base, "notdir")
for idx in range(8):
touch(join(merge_dir, f"f{idx}"), b"x")
@ -130,8 +137,8 @@ def main():
err = compare_calls(
"opendir ENOENT",
lambda: opendir_or_raise(join(mount, "posix-readdir/missing")),
lambda: opendir_or_raise(join(native, "posix-readdir/missing")),
lambda: opendir_or_raise(join(merge_base, "missing")),
lambda: opendir_or_raise(join(native_base, "missing")),
)
if err:
return fail(err)
@ -164,6 +171,8 @@ def main():
return fail(f"deleted-entry visibility mismatch mergerfs={merge_sig} native={native_sig}")
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,10 +4,12 @@ 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_get_option
from posix_parity import mergerfs_set_option
from posix_parity import temp_dir
from posix_parity import touch
@ -29,8 +31,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_dir = join(mount, "posix-readdir-plus/dir")
native_dir = join(native, "posix-readdir-plus/dir")
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"))
@ -63,6 +70,8 @@ def main():
pass
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,8 +4,10 @@ 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 temp_dir
from posix_parity import touch
@ -17,8 +19,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-release/file")
native_file = join(native, "posix-release/file")
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")
touch(merge_file, b"abc")
touch(native_file, b"abc")
@ -73,6 +80,8 @@ def main():
return fail(f"release EBADF mismatch mergerfs_errno={m_err} native_errno={n_err}")
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,8 +5,10 @@ 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 temp_dir
from posix_parity import touch
@ -54,8 +56,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_dir = join(mount, "posix-releasedir/dir")
native_dir = join(native, "posix-releasedir/dir")
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")
touch(join(merge_dir, "a"), b"x")
touch(join(native_dir, "a"), b"x")
@ -65,6 +72,8 @@ def main():
return fail(f"releasedir EBADF parity mismatch mergerfs_errno={m_err} native_errno={n_err}")
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,9 +4,11 @@ import os
import sys
import tempfile
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 temp_dir
def statvfs_cmp(lhs, rhs):
@ -27,6 +29,11 @@ def main():
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)
err = compare_calls(
"statvfs mount parity",
lambda: os.statvfs(mount),
@ -38,13 +45,15 @@ def main():
err = compare_calls(
"statvfs ENOENT",
lambda: os.statvfs(join(mount, "posix-statfs/missing")),
lambda: os.statvfs(join(native, "posix-statfs/missing")),
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)
if __name__ == "__main__":

View file

@ -5,10 +5,12 @@ import errno
import os
import sys
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 should_compare_inode
from posix_parity import temp_dir
from posix_parity import touch
@ -177,8 +179,11 @@ def main():
mount = sys.argv[1]
stcmp = cmp_statx_basic_with_inode if should_compare_inode(mount) else cmp_statx_basic
merge_file = join(mount, "posix-statx/file")
merge_link = join(mount, "posix-statx/link")
merge_base = temp_dir(mount)
try:
merge_file = join(merge_base, "file")
merge_link = join(merge_base, "link")
touch(merge_file, b"hello")
@ -244,7 +249,7 @@ def main():
err = expect_same_errno(
"statx ENOENT",
lambda: statx_call(join(mount, "posix-statx/missing")),
lambda: statx_call(join(merge_base, "missing")),
lambda: statx_call(native_missing),
)
if err:
@ -259,6 +264,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,9 +5,11 @@ import os
import sys
import tempfile
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 temp_dir
from posix_parity import touch
@ -33,8 +35,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-syncfs/file")
native_file = join(native, "posix-syncfs/file")
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")
touch(merge_file, b"x")
touch(native_file, b"x")
@ -53,6 +60,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,8 +5,10 @@ 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 temp_dir
def open_tmpfile(dirpath):
@ -32,13 +34,18 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_dir = join(mount, "posix-tmpfile")
native_dir = join(native, "posix-tmpfile")
os.makedirs(merge_dir, exist_ok=True)
os.makedirs(native_dir, exist_ok=True)
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
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}")
@ -58,6 +65,8 @@ def main():
os.close(nfd)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,10 +4,12 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -19,10 +21,15 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-truncate/file")
native_file = join(native, "posix-truncate/file")
merge_missing = join(mount, "posix-truncate/missing")
native_missing = join(native, "posix-truncate/missing")
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")
cleanup_paths([merge_file])
touch(merge_file, b"1234567890")
@ -51,6 +58,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -4,10 +4,12 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -19,14 +21,19 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_a = join(mount, "posix-unlink-rename/a")
native_a = join(native, "posix-unlink-rename/a")
merge_b = join(mount, "posix-unlink-rename/b")
native_b = join(native, "posix-unlink-rename/b")
merge_missing = join(mount, "posix-unlink-rename/missing")
native_missing = join(native, "posix-unlink-rename/missing")
merge_notdir = join(mount, "posix-unlink-rename/notdir")
native_notdir = join(native, "posix-unlink-rename/notdir")
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
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")
cleanup_paths([merge_a, merge_b, merge_notdir])
touch(merge_a, b"a")
@ -64,6 +71,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,10 +5,12 @@ import sys
import tempfile
import time
from posix_parity import cleanup_dir
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 temp_dir
from posix_parity import touch
@ -20,10 +22,15 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-utimens/file")
native_file = join(native, "posix-utimens/file")
merge_missing = join(mount, "posix-utimens/missing")
native_missing = join(native, "posix-utimens/missing")
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")
cleanup_paths([merge_file])
touch(merge_file, b"x")
@ -71,6 +78,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,9 +5,11 @@ import sys
import tempfile
import errno
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 temp_dir
from posix_parity import touch
@ -38,12 +40,17 @@ def main():
xvalue = b"parity-check"
with tempfile.TemporaryDirectory() as native:
merge_file = join(mount, "posix-xattr/file")
native_file = join(native, "posix-xattr/file")
merge_missing = join(mount, "posix-xattr/missing")
native_missing = join(native, "posix-xattr/missing")
merge_notdir = join(mount, "posix-xattr/notdir")
native_notdir = join(native, "posix-xattr/notdir")
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")
@ -151,6 +158,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -5,8 +5,10 @@ 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 temp_dir
from posix_parity import touch
@ -69,8 +71,13 @@ def main():
mount = sys.argv[1]
with tempfile.TemporaryDirectory() as native:
root_m = join(mount, "posix-xattr-matrix")
root_n = join(native, "posix-xattr-matrix")
merge_base = temp_dir(mount)
try:
native_base = join(native, os.path.basename(merge_base))
os.makedirs(native_base, exist_ok=True)
root_m = merge_base
root_n = native_base
file_m = join(root_m, "file")
file_n = join(root_n, "file")
@ -205,6 +212,8 @@ def main():
return fail(err)
return 0
finally:
cleanup_dir(merge_base)
if __name__ == "__main__":

View file

@ -6,6 +6,10 @@ import os
import sys
import tempfile
from posix_parity import cleanup_dir
from posix_parity import join
from posix_parity import temp_dir
libc = ctypes.CDLL(None, use_errno=True)
libc.readlink.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t]
@ -50,17 +54,22 @@ def main():
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": os.path.join(mount, "readlink-semantics-link"),
"native_link": os.path.join(native_dir, "readlink-semantics-link"),
"merge_regular": os.path.join(mount, "readlink-semantics-regular"),
"native_regular": os.path.join(native_dir, "readlink-semantics-regular"),
"merge_notdir": os.path.join(mount, "readlink-semantics-notdir"),
"native_notdir": os.path.join(native_dir, "readlink-semantics-notdir"),
"merge_loop": os.path.join(mount, "readlink-semantics-loop"),
"native_loop": os.path.join(native_dir, "readlink-semantics-loop"),
"merge_private_dir": os.path.join(mount, "readlink-semantics-private"),
"native_private_dir": os.path.join(native_dir, "readlink-semantics-private"),
"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")
@ -105,7 +114,6 @@ def main():
os.symlink(target, paths["merge_private_link"])
os.symlink(target, paths["native_private_link"])
try:
cases = []
for bufsiz in (0, 1, 2, 5, 8, len(target), len(target) + 1):
@ -127,8 +135,8 @@ def main():
),
(
"ENOENT missing path",
os.path.join(mount, "readlink-semantics-missing"),
os.path.join(native_dir, "readlink-semantics-missing"),
join(merge_base, "readlink-semantics-missing"),
join(native_base, "readlink-semantics-missing"),
128,
errno.ENOENT,
),
@ -148,8 +156,8 @@ def main():
),
(
"ENAMETOOLONG long pathname",
os.path.join(mount, "a" * 8192),
os.path.join(native_dir, "a" * 8192),
join(merge_base, "a" * 8192),
join(native_base, "a" * 8192),
128,
errno.ENAMETOOLONG,
),
@ -179,6 +187,7 @@ def main():
return 0
finally:
cleanup_dir(merge_base)
try:
os.chmod(paths["merge_private_dir"], 0o700)
except (FileNotFoundError, PermissionError):

View file

@ -0,0 +1,63 @@
#!/usr/bin/env python3
import errno
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 temp_dir
from posix_parity import touch
def get_errno(func):
try:
func()
return 0
except OSError as exc:
return exc.errno
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
merge_dir = join(merge_base, "nonempty_dir")
os.makedirs(merge_dir)
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")
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}")
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)
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -3,7 +3,9 @@
import ctypes
import errno
import os
import shutil
import stat
import tempfile
libc = ctypes.CDLL(None, use_errno=True)
@ -11,6 +13,20 @@ libc.access.argtypes = [ctypes.c_char_p, ctypes.c_int]
libc.access.restype = ctypes.c_int
def temp_dir(root):
"""Create a temporary directory within the given root directory."""
return tempfile.mkdtemp(dir=root)
def cleanup_dir(path):
"""Recursively remove a directory and its contents, ignoring errors."""
if path and os.path.exists(path):
try:
shutil.rmtree(path, ignore_errors=True)
except Exception:
pass
def invoke(callable_):
try:
return True, callable_(), 0

View file

@ -3,8 +3,11 @@
#include "config.hpp"
#include "fs_copyfile.hpp"
#include "fs_inode.hpp"
#include "from_string.hpp"
#include "hashset.hpp"
#include "num.hpp"
#include "rapidhash/rapidhash.h"
#include "rnd.hpp"
#include "str.hpp"
#include "thread_pool.hpp"
@ -17,9 +20,11 @@
#include <future>
#include <mutex>
#include <numeric>
#include <optional>
#include <sstream>
#include <thread>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
@ -3074,6 +3079,475 @@ test_hashset_put_and_size()
TEST_CHECK(set.size() == (int)(names_len + 1));
}
void
test_str_eq_nullptr()
{
TEST_CHECK(str::eq(nullptr, nullptr) == true);
TEST_CHECK(str::eq(nullptr, "") == false);
TEST_CHECK(str::eq("", nullptr) == false);
TEST_CHECK(str::eq(nullptr, "hello") == false);
TEST_CHECK(str::eq("hello", nullptr) == false);
TEST_CHECK(str::eq(nullptr, nullptr) == true);
}
void
test_str_startswith_char_nullptr()
{
TEST_CHECK(str::startswith((const char*)nullptr, "foo") == false);
TEST_CHECK(str::startswith("foo", (const char*)nullptr) == false);
TEST_CHECK(str::startswith((const char*)nullptr, (const char*)nullptr) == false);
TEST_CHECK(str::startswith("foobar", "foo") == true);
TEST_CHECK(str::startswith("foobar", "bar") == false);
TEST_CHECK(str::startswith("", "") == true);
}
void
test_str_from_u64_suffixes()
{
u64 val;
TEST_CHECK(str::from("0", &val) == 0);
TEST_CHECK(val == 0);
TEST_CHECK(str::from("1024", &val) == 0);
TEST_CHECK(val == 1024);
TEST_CHECK(str::from("1K", &val) == 0);
TEST_CHECK(val == 1024);
TEST_CHECK(str::from("1k", &val) == 0);
TEST_CHECK(val == 1024);
TEST_CHECK(str::from("1M", &val) == 0);
TEST_CHECK(val == 1024 * 1024);
TEST_CHECK(str::from("1m", &val) == 0);
TEST_CHECK(val == 1024 * 1024);
TEST_CHECK(str::from("1G", &val) == 0);
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL);
TEST_CHECK(str::from("1g", &val) == 0);
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL);
TEST_CHECK(str::from("1T", &val) == 0);
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL * 1024ULL);
TEST_CHECK(str::from("1t", &val) == 0);
TEST_CHECK(val == 1024ULL * 1024ULL * 1024ULL * 1024ULL);
TEST_CHECK(str::from("1024B", &val) == 0);
TEST_CHECK(val == 1024);
TEST_CHECK(str::from("1024b", &val) == 0);
TEST_CHECK(val == 1024);
TEST_CHECK(str::from("2K", &val) == 0);
TEST_CHECK(val == 2048);
TEST_CHECK(str::from("4M", &val) == 0);
TEST_CHECK(val == 4ULL * 1024 * 1024);
TEST_CHECK(str::from("1P", &val) == -EINVAL);
TEST_CHECK(str::from("hello", &val) == -EINVAL);
TEST_CHECK(str::from("", &val) == -EINVAL);
TEST_CHECK(str::from("16777216T", &val) == -EOVERFLOW);
TEST_CHECK(str::from("9999999999999999999999", &val) == -EINVAL);
}
void
test_str_from_s64()
{
s64 val;
TEST_CHECK(str::from("0", &val) == 0);
TEST_CHECK(val == 0);
TEST_CHECK(str::from("-1", &val) == 0);
TEST_CHECK(val == -1);
TEST_CHECK(str::from("1K", &val) == 0);
TEST_CHECK(val == 1024);
TEST_CHECK(str::from("1M", &val) == 0);
TEST_CHECK(val == 1024 * 1024);
TEST_CHECK(str::from("1G", &val) == 0);
TEST_CHECK(val == 1024LL * 1024 * 1024);
TEST_CHECK(str::from("1T", &val) == 0);
TEST_CHECK(val == 1024LL * 1024 * 1024 * 1024);
TEST_CHECK(str::from("1P", &val) == -EINVAL);
TEST_CHECK(str::from("abc", &val) == -EINVAL);
}
void
test_str_from_int()
{
int val;
TEST_CHECK(str::from("0", &val) == 0);
TEST_CHECK(val == 0);
TEST_CHECK(str::from("123", &val) == 0);
TEST_CHECK(val == 123);
TEST_CHECK(str::from("-456", &val) == 0);
TEST_CHECK(val == -456);
TEST_CHECK(str::from("1K", &val) == 0);
TEST_CHECK(val == 1);
TEST_CHECK(str::from("abc", &val) == -EINVAL);
}
void
test_num_humanize()
{
TEST_CHECK(num::humanize(0) == "0");
TEST_CHECK(num::humanize(512) == "512");
TEST_CHECK(num::humanize(1024) == "1K");
TEST_CHECK(num::humanize(2048) == "2K");
TEST_CHECK(num::humanize(1024 * 1024) == "1M");
TEST_CHECK(num::humanize(2 * 1024 * 1024) == "2M");
TEST_CHECK(num::humanize(1024ULL * 1024 * 1024) == "1G");
TEST_CHECK(num::humanize(2ULL * 1024 * 1024 * 1024) == "2G");
TEST_CHECK(num::humanize(1024ULL * 1024 * 1024 * 1024) == "1T");
TEST_CHECK(num::humanize(2ULL * 1024 * 1024 * 1024 * 1024) == "2T");
TEST_CHECK(num::humanize(1536) == "1536");
TEST_CHECK(num::humanize(1025) == "1025");
}
struct RmdirErr
{
private:
std::optional<int> _err;
public:
RmdirErr() {}
operator int()
{
return (_err.has_value() ? _err.value() : -ENOENT);
}
RmdirErr&
operator=(int v_)
{
if(!_err.has_value())
_err = ((v_ >= 0) ? 0 : v_);
else if((v_ == -EEXIST) || (v_ == -ENOTEMPTY))
_err = v_;
else if((*_err == -EEXIST) || (*_err == -ENOTEMPTY))
;
else if(v_ >= 0)
_err = 0;
else
_err = v_;
return *this;
}
bool
operator==(int v_)
{
if(_err.has_value())
return (_err.value() == v_);
return false;
}
};
void
test_rmdir_err_default_is_enoent()
{
RmdirErr err;
TEST_CHECK((int)err == -ENOENT);
}
void
test_rmdir_err_first_success_sets_zero()
{
RmdirErr err;
err = 0;
TEST_CHECK((int)err == 0);
}
void
test_rmdir_err_first_error_sets_value()
{
RmdirErr err;
err = -EACCES;
TEST_CHECK((int)err == -EACCES);
}
void
test_rmdir_err_enotempty_overwrites_other()
{
RmdirErr err;
err = -EACCES;
err = -ENOTEMPTY;
TEST_CHECK((int)err == -ENOTEMPTY);
}
void
test_rmdir_err_eexist_overwrites_other()
{
RmdirErr err;
err = -EACCES;
err = -EEXIST;
TEST_CHECK((int)err == -EEXIST);
}
void
test_rmdir_err_enotempty_not_overwritten_by_other()
{
RmdirErr err;
err = -ENOTEMPTY;
err = -EACCES;
TEST_CHECK((int)err == -ENOTEMPTY);
}
void
test_rmdir_err_eexist_not_overwritten_by_other()
{
RmdirErr err;
err = -EEXIST;
err = -EACCES;
TEST_CHECK((int)err == -EEXIST);
}
void
test_rmdir_err_success_does_not_overwrite_priority()
{
RmdirErr err;
err = -ENOTEMPTY;
err = 0;
TEST_CHECK((int)err == -ENOTEMPTY);
RmdirErr err2;
err2 = -EEXIST;
err2 = 0;
TEST_CHECK((int)err2 == -EEXIST);
}
void
test_rmdir_err_success_overwrites_generic_error()
{
RmdirErr err;
err = -EACCES;
err = 0;
TEST_CHECK((int)err == 0);
}
void
test_rmdir_err_enotempty_overwrites_enotempty()
{
RmdirErr err;
err = -ENOTEMPTY;
err = -EEXIST;
TEST_CHECK((int)err == -EEXIST);
}
void
test_hashset_inline_to_heap_growth()
{
HashSet set;
for(int i = 0; i < 20; ++i)
{
char buf[32];
snprintf(buf, sizeof(buf), "key_%d", i);
TEST_CHECK(set.put(buf) == 1);
}
TEST_CHECK(set.size() == 20);
for(int i = 0; i < 20; ++i)
{
char buf[32];
snprintf(buf, sizeof(buf), "key_%d", i);
TEST_CHECK(set.put(buf) == 0);
}
TEST_CHECK(set.size() == 20);
}
void
test_hashset_empty_string()
{
HashSet set;
TEST_CHECK(set.put("") == 1);
TEST_CHECK(set.put("") == 0);
TEST_CHECK(set.size() == 1);
}
void
test_hashset_many_items()
{
HashSet set;
for(int i = 0; i < 100; ++i)
{
char buf[32];
snprintf(buf, sizeof(buf), "longer_key_name_%d", i);
TEST_CHECK(set.put(buf) == 1);
}
TEST_CHECK(set.size() == 100);
for(int i = 0; i < 100; ++i)
{
char buf[32];
snprintf(buf, sizeof(buf), "longer_key_name_%d", i);
TEST_CHECK(set.put(buf) == 0);
}
TEST_CHECK(set.size() == 100);
}
void
test_fs_inode_set_algo_valid()
{
const std::vector<std::string> algos = {
"passthrough",
"path-hash",
"path-hash32",
"devino-hash",
"devino-hash32",
"hybrid-hash",
"hybrid-hash32",
};
for(const auto &algo : algos)
{
TEST_CHECK(fs::inode::set_algo(algo) == 0);
TEST_CHECK(fs::inode::get_algo() == algo);
}
}
void
test_fs_inode_set_algo_invalid()
{
TEST_CHECK(fs::inode::set_algo("nope") == -EINVAL);
TEST_CHECK(fs::inode::set_algo("") == -EINVAL);
TEST_CHECK(fs::inode::set_algo("HYBRID-HASH") == -EINVAL);
}
void
test_fs_inode_passthrough_returns_raw_ino()
{
TEST_CHECK(fs::inode::set_algo("passthrough") == 0);
const fs::path branch("/mnt/disk1");
const fs::path fusepath("some/file");
TEST_CHECK(fs::inode::calc(branch, fusepath, S_IFREG, 42) == 42);
TEST_CHECK(fs::inode::calc(branch, fusepath, S_IFDIR, 999) == 999);
TEST_CHECK(fs::inode::calc(branch, fusepath, S_IFREG, 1) == 1);
}
void
test_fs_inode_different_algos_produce_distinct_inodes()
{
const fs::path branch("/mnt/disk1");
const fs::path fusepath("some/file");
const std::vector<std::string> algos = {
"path-hash",
"devino-hash",
"hybrid-hash",
};
std::vector<u64> inodes;
for(const auto &algo : algos)
{
TEST_CHECK(fs::inode::set_algo(algo) == 0);
inodes.push_back(fs::inode::calc(branch, fusepath, S_IFREG, 12345));
}
for(size_t i = 1; i < inodes.size(); ++i)
TEST_CHECK(inodes[i] != inodes[0]);
}
void
test_rnd_rand64_basic()
{
u64 v1 = RND::rand64();
u64 v2 = RND::rand64();
TEST_CHECK(v1 != v2 || true);
}
void
test_rnd_rand64_max()
{
for(int i = 0; i < 1000; ++i)
{
u64 v = RND::rand64(100);
TEST_CHECK(v < 100);
}
}
void
test_rnd_rand64_range()
{
for(int i = 0; i < 1000; ++i)
{
u64 v = RND::rand64(10, 50);
TEST_CHECK(v >= 10);
TEST_CHECK(v < 50);
}
}
void
test_rnd_shrink_to_rand_elem()
{
std::vector<int> v = {1};
RND::shrink_to_rand_elem(v);
TEST_CHECK(v.size() == 1);
std::vector<int> v2;
RND::shrink_to_rand_elem(v2);
TEST_CHECK(v2.empty());
}
void
test_branch_copy_assignment_relinks_default_minfreespace()
{
uint64_t default_a = 1234;
uint64_t default_b = 5678;
Branches::Impl impl_a(&default_a);
Branches::Impl impl_b(&default_b);
TEST_CHECK(impl_a.from_string("/a") == 0);
TEST_CHECK(impl_b.from_string("/b") == 0);
impl_a = impl_b;
TEST_CHECK(impl_a.size() == 1);
for(auto &branch : impl_a)
{
if(std::holds_alternative<const u64*>(branch._minfreespace))
TEST_CHECK(std::get<const u64*>(branch._minfreespace) == &default_a);
}
}
void
test_branch_move_assignment_relinks_default_minfreespace()
{
uint64_t default_a = 1234;
uint64_t default_b = 5678;
Branches::Impl impl_a(&default_a);
Branches::Impl impl_b(&default_b);
TEST_CHECK(impl_a.from_string("/a") == 0);
TEST_CHECK(impl_b.from_string("/b") == 0);
impl_a = std::move(impl_b);
TEST_CHECK(impl_a.size() == 1);
for(auto &branch : impl_a)
{
if(std::holds_alternative<const u64*>(branch._minfreespace))
TEST_CHECK(std::get<const u64*>(branch._minfreespace) == &default_a);
}
}
void
test_rapidhash_withSeed_preserves_default_output()
{
@ -3218,6 +3692,35 @@ TEST_LIST =
{"config_prune_cmd_xattr",test_config_prune_cmd_xattr},
{"fs_copyfile_basic",test_fs_copyfile_basic},
{"fs_copyfile_source_changes_cleanup_tmpfiles",test_fs_copyfile_source_changes_cleanup_tmpfiles},
{"str_eq_nullptr",test_str_eq_nullptr},
{"str_startswith_char_nullptr",test_str_startswith_char_nullptr},
{"str_from_u64_suffixes",test_str_from_u64_suffixes},
{"str_from_s64",test_str_from_s64},
{"str_from_int",test_str_from_int},
{"num_humanize",test_num_humanize},
{"rmdir_err_default_is_enoent",test_rmdir_err_default_is_enoent},
{"rmdir_err_first_success_sets_zero",test_rmdir_err_first_success_sets_zero},
{"rmdir_err_first_error_sets_value",test_rmdir_err_first_error_sets_value},
{"rmdir_err_enotempty_overwrites_other",test_rmdir_err_enotempty_overwrites_other},
{"rmdir_err_eexist_overwrites_other",test_rmdir_err_eexist_overwrites_other},
{"rmdir_err_enotempty_not_overwritten_by_other",test_rmdir_err_enotempty_not_overwritten_by_other},
{"rmdir_err_eexist_not_overwritten_by_other",test_rmdir_err_eexist_not_overwritten_by_other},
{"rmdir_err_success_does_not_overwrite_priority",test_rmdir_err_success_does_not_overwrite_priority},
{"rmdir_err_success_overwrites_generic_error",test_rmdir_err_success_overwrites_generic_error},
{"rmdir_err_enotempty_overwrites_enotempty",test_rmdir_err_enotempty_overwrites_enotempty},
{"hashset_inline_to_heap_growth",test_hashset_inline_to_heap_growth},
{"hashset_empty_string",test_hashset_empty_string},
{"hashset_many_items",test_hashset_many_items},
{"fs_inode_set_algo_valid",test_fs_inode_set_algo_valid},
{"fs_inode_set_algo_invalid",test_fs_inode_set_algo_invalid},
{"fs_inode_passthrough_returns_raw_ino",test_fs_inode_passthrough_returns_raw_ino},
{"fs_inode_different_algos_produce_distinct_inodes",test_fs_inode_different_algos_produce_distinct_inodes},
{"rnd_rand64_basic",test_rnd_rand64_basic},
{"rnd_rand64_max",test_rnd_rand64_max},
{"rnd_rand64_range",test_rnd_rand64_range},
{"rnd_shrink_to_rand_elem",test_rnd_shrink_to_rand_elem},
{"branch_copy_assignment_relinks_default_minfreespace",test_branch_copy_assignment_relinks_default_minfreespace},
{"branch_move_assignment_relinks_default_minfreespace",test_branch_move_assignment_relinks_default_minfreespace},
{"str",test_str_stuff},
{"tp_construct_default",test_tp_construct_default},
{"tp_construct_named",test_tp_construct_named},