diff --git a/src/fuse_getxattr.cpp b/src/fuse_getxattr.cpp index 2a7a8a87..8bedd2d7 100644 --- a/src/fuse_getxattr.cpp +++ b/src/fuse_getxattr.cpp @@ -117,13 +117,13 @@ _getxattr_user_mergerfs_allpaths(const Branches::Ptr branches_, static int -_getxattr_user_mergerfs(const fs::path &basepath_, - const fs::path &fusepath_, - const fs::path &fullpath_, - const Branches::Ptr branches_, - const char *attrname_, - char *buf_, - const size_t count_) +_getxattr_user_mergerfs(const fs::path &basepath_, + const fs::path &fusepath_, + const fs::path &fullpath_, + const Branches::Ptr branches_, + const char *attrname_, + char *buf_, + const size_t count_) { std::string key; diff --git a/src/fuse_init.cpp b/src/fuse_init.cpp index 91d90893..75b1d5bd 100644 --- a/src/fuse_init.cpp +++ b/src/fuse_init.cpp @@ -37,7 +37,7 @@ static void _want(fuse_conn_info_t *conn_, - const int flag_) + const int flag_) { conn_->want |= flag_; } @@ -80,7 +80,7 @@ static const char MAX_PAGES_LIMIT_FILEPATH[] = "/proc/sys/fs/fuse/max_pages_limi static void _want_if_capable_max_pages(fuse_conn_info_t *conn_, - Config &cfg_) + Config &cfg_) { std::fstream f; u64 max_pages_limit; diff --git a/src/fuse_rename.cpp b/src/fuse_rename.cpp index ad550a2f..9aeebdba 100644 --- a/src/fuse_rename.cpp +++ b/src/fuse_rename.cpp @@ -70,8 +70,8 @@ _remove(const StrVec &toremove_) static int _rename_create_path(const Policy::Search &searchPolicy_, - const Policy::Action &actionPolicy_, - const Branches::Ptr branches_, + const Policy::Action &actionPolicy_, + const Branches::Ptr branches_, const fs::path &oldfusepath_, const fs::path &newfusepath_) { @@ -131,7 +131,7 @@ _rename_create_path(const Policy::Search &searchPolicy_, static int _rename_preserve_path(const Policy::Action &actionPolicy_, - const Branches::Ptr branches_, + const Branches::Ptr branches_, const fs::path &oldfusepath_, const fs::path &newfusepath_) { @@ -202,7 +202,7 @@ _rename_exdev_rename_back(const std::vector &branches_, static int _rename_exdev_rename_target(const Policy::Action &actionPolicy_, - const Branches::Ptr ibranches_, + const Branches::Ptr ibranches_, const fs::path &oldfusepath_, std::vector &obranches_) { @@ -249,8 +249,8 @@ _rename_exdev_rename_target(const Policy::Action &actionPolicy_, static int _rename_exdev_rel_symlink(const fuse_req_ctx_t *ctx_, - const Policy::Action &actionPolicy_, - const Branches::Ptr branches_, + const Policy::Action &actionPolicy_, + const Branches::Ptr branches_, const fs::path &oldfusepath_, const fs::path &newfusepath_) { @@ -278,8 +278,8 @@ _rename_exdev_rel_symlink(const fuse_req_ctx_t *ctx_, static int _rename_exdev_abs_symlink(const fuse_req_ctx_t *ctx_, - const Policy::Action &actionPolicy_, - const Branches::Ptr branches_, + const Policy::Action &actionPolicy_, + const Branches::Ptr branches_, const fs::path &mount_, const fs::path &oldfusepath_, const fs::path &newfusepath_) diff --git a/tests/TEST_api_xattr b/tests/TEST_api_xattr new file mode 100755 index 00000000..42384a6c --- /dev/null +++ b/tests/TEST_api_xattr @@ -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 ", 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()) \ No newline at end of file diff --git a/tests/TEST_cfg_link_rename_exdev b/tests/TEST_cfg_link_rename_exdev index ce1721a3..0d9c5bff 100755 --- a/tests/TEST_cfg_link_rename_exdev +++ b/tests/TEST_cfg_link_rename_exdev @@ -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,76 +70,81 @@ def main(): return 1 mount = sys.argv[1] + merge_base = temp_dir(mount) + try: - src, dst = pick_cross_branch_paths(mount) - except PermissionError: - return 0 - except RuntimeError as exc: - # Environment-dependent; treat as soft skip. - msg = str(exc) - if "requires at least 2 branches" in msg or msg == "allpaths-xattr-unsupported": + try: + src, dst = pick_cross_branch_paths(mount, merge_base) + except PermissionError: + return 0 + except RuntimeError as exc: + # Environment-dependent; treat as soft skip. + msg = str(exc) + if "requires at least 2 branches" in msg or msg == "allpaths-xattr-unsupported": + return 0 + return fail(msg) + except OSError: return 0 - return fail(msg) - except OSError: - return 0 - try: - orig_create = mergerfs_get_option(mount, "category.create") - orig_link = mergerfs_get_option(mount, "link-exdev") - orig_rename = mergerfs_get_option(mount, "rename-exdev") - except (PermissionError, FileNotFoundError, OSError): - return 0 + try: + orig_create = mergerfs_get_option(mount, "category.create") + orig_link = mergerfs_get_option(mount, "link-exdev") + orig_rename = mergerfs_get_option(mount, "rename-exdev") + except (PermissionError, FileNotFoundError, OSError): + return 0 - try: - # Maximize chance of EXDEV path by path-preserving create policy. - mergerfs_set_option(mount, "category.create", "epmfs") + 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. - mergerfs_set_option(mount, "link-exdev", "passthrough") - err = get_errno(lambda: os.link(src, dst + ".link.pass")) - if err not in (0, errno.EXDEV): - return fail(f"link-exdev=passthrough unexpected errno={err}") - link_exdev_active = (err == errno.EXDEV) + # link-exdev passthrough => EXDEV when cross-device path preserving applies. + mergerfs_set_option(mount, "link-exdev", "passthrough") + err = get_errno(lambda: os.link(src, dst + ".link.pass")) + if err not in (0, errno.EXDEV): + return fail(f"link-exdev=passthrough unexpected errno={err}") + link_exdev_active = (err == errno.EXDEV) - # rel-symlink should not return EXDEV if fallback triggers. - mergerfs_set_option(mount, "link-exdev", "rel-symlink") - rel_link = dst + ".link.rel" - err = get_errno(lambda: os.link(src, rel_link)) - if link_exdev_active and err == errno.EXDEV: - return fail("link-exdev=rel-symlink still returned EXDEV") - if link_exdev_active and err == 0 and not os.path.islink(rel_link): - return fail("link-exdev=rel-symlink expected symlink fallback") + # rel-symlink should not return EXDEV if fallback triggers. + mergerfs_set_option(mount, "link-exdev", "rel-symlink") + rel_link = dst + ".link.rel" + err = get_errno(lambda: os.link(src, rel_link)) + if link_exdev_active and err == errno.EXDEV: + return fail("link-exdev=rel-symlink still returned EXDEV") + if link_exdev_active and err == 0 and not os.path.islink(rel_link): + return fail("link-exdev=rel-symlink expected symlink fallback") - # rename-exdev passthrough => EXDEV in cross-device path preserving case. - src_ren = src + ".ren" - touch(src_ren, b"src") - mergerfs_set_option(mount, "rename-exdev", "passthrough") - err = get_errno(lambda: os.rename(src_ren, dst + ".ren.pass")) - if err not in (0, errno.EXDEV): - return fail(f"rename-exdev=passthrough unexpected errno={err}") - rename_exdev_active = (err == errno.EXDEV) + # rename-exdev passthrough => EXDEV in cross-device path preserving case. + src_ren = src + ".ren" + touch(src_ren, b"src") + mergerfs_set_option(mount, "rename-exdev", "passthrough") + err = get_errno(lambda: os.rename(src_ren, dst + ".ren.pass")) + if err not in (0, errno.EXDEV): + return fail(f"rename-exdev=passthrough unexpected errno={err}") + rename_exdev_active = (err == errno.EXDEV) + + # rel-symlink should avoid EXDEV by fallback when applicable. + src_ren2 = src + ".ren2" + touch(src_ren2, b"src") + mergerfs_set_option(mount, "rename-exdev", "rel-symlink") + rel_ren = dst + ".ren.rel" + err = get_errno(lambda: os.rename(src_ren2, rel_ren)) + if rename_exdev_active and err == errno.EXDEV: + return fail("rename-exdev=rel-symlink still returned EXDEV") + if rename_exdev_active and err == 0 and not os.path.islink(rel_ren): + return fail("rename-exdev=rel-symlink expected symlink fallback") + except (PermissionError, FileNotFoundError, OSError): + return 0 + finally: + try: + mergerfs_set_option(mount, "category.create", orig_create) + mergerfs_set_option(mount, "link-exdev", orig_link) + mergerfs_set_option(mount, "rename-exdev", orig_rename) + except OSError: + pass - # rel-symlink should avoid EXDEV by fallback when applicable. - src_ren2 = src + ".ren2" - touch(src_ren2, b"src") - mergerfs_set_option(mount, "rename-exdev", "rel-symlink") - rel_ren = dst + ".ren.rel" - err = get_errno(lambda: os.rename(src_ren2, rel_ren)) - if rename_exdev_active and err == errno.EXDEV: - return fail("rename-exdev=rel-symlink still returned EXDEV") - if rename_exdev_active and err == 0 and not os.path.islink(rel_ren): - return fail("rename-exdev=rel-symlink expected symlink fallback") - except (PermissionError, FileNotFoundError, OSError): return 0 finally: - try: - mergerfs_set_option(mount, "category.create", orig_create) - mergerfs_set_option(mount, "link-exdev", orig_link) - mergerfs_set_option(mount, "rename-exdev", orig_rename) - except OSError: - pass - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_cfg_xattr_modes b/tests/TEST_cfg_xattr_modes index 3a0ea749..b4956141 100755 --- a/tests/TEST_cfg_xattr_modes +++ b/tests/TEST_cfg_xattr_modes @@ -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,56 +28,48 @@ def main(): return 1 mount = sys.argv[1] - path = join(mount, "cfg-xattr-modes/file") - xname = "user.mergerfs.cfg.test" - xval = b"v" - - touch(path, b"x") + merge_base = temp_dir(mount) try: - orig = mergerfs_get_option(mount, "xattr") - except (PermissionError, FileNotFoundError, OSError): - return 0 + path = join(merge_base, "file") + xname = "user.mergerfs.cfg.test" + xval = b"v" - try: - mergerfs_set_option(mount, "xattr", "passthrough") - set_err = get_errno(lambda: os.setxattr(path, xname, xval)) - get_err = get_errno(lambda: os.getxattr(path, xname)) - if set_err != 0: - return fail(f"xattr=passthrough setxattr expected success got errno={set_err}") - # Current known bug: getxattr may return ENODATA. We still enforce mode switch behavior. - if get_err not in (0, errno.ENODATA): - return fail(f"xattr=passthrough getxattr unexpected errno={get_err}") + touch(path, b"x") - mergerfs_set_option(mount, "xattr", "noattr") - err = get_errno(lambda: os.getxattr(path, xname)) - if err != errno.ENODATA: - return fail(f"xattr=noattr getxattr expected ENODATA got errno={err}") - err = get_errno(lambda: os.setxattr(path, xname, xval)) - if err != errno.ENODATA: - return fail(f"xattr=noattr setxattr expected ENODATA got errno={err}") + try: + orig = mergerfs_get_option(mount, "xattr") + except (PermissionError, FileNotFoundError, OSError): + return 0 - 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}") + try: + mergerfs_set_option(mount, "xattr", "passthrough") + set_err = get_errno(lambda: os.setxattr(path, xname, xval)) + get_err = get_errno(lambda: os.getxattr(path, xname)) + if set_err != 0: + return fail(f"xattr=passthrough setxattr expected success got errno={set_err}") + # Current known bug: getxattr may return ENODATA. We still enforce mode switch behavior. + if get_err not in (0, errno.ENODATA): + return fail(f"xattr=passthrough getxattr unexpected errno={get_err}") + + mergerfs_set_option(mount, "xattr", "noattr") + err = get_errno(lambda: os.getxattr(path, xname)) + if err != errno.ENODATA: + return fail(f"xattr=noattr getxattr expected ENODATA got errno={err}") + err = get_errno(lambda: os.setxattr(path, xname, xval)) + if err != errno.ENODATA: + return fail(f"xattr=noattr setxattr expected ENODATA got errno={err}") + except (PermissionError, FileNotFoundError, OSError): + return 0 + finally: + try: + mergerfs_set_option(mount, "xattr", orig) + except OSError: + pass - # 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: - try: - mergerfs_set_option(mount, "xattr", orig) - except OSError: - pass - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_movefile_enospc b/tests/TEST_movefile_enospc new file mode 100755 index 00000000..2d5811a1 --- /dev/null +++ b/tests/TEST_movefile_enospc @@ -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 ", 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()) \ No newline at end of file diff --git a/tests/TEST_open_after_unlink b/tests/TEST_open_after_unlink new file mode 100755 index 00000000..0906707b --- /dev/null +++ b/tests/TEST_open_after_unlink @@ -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 ", 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()) \ No newline at end of file diff --git a/tests/TEST_policy_lup b/tests/TEST_policy_lup new file mode 100755 index 00000000..163dd4e8 --- /dev/null +++ b/tests/TEST_policy_lup @@ -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 ", 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()) \ No newline at end of file diff --git a/tests/TEST_posix_access b/tests/TEST_posix_access index 9e0f1e83..0054585d 100755 --- a/tests/TEST_posix_access +++ b/tests/TEST_posix_access @@ -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,48 +22,55 @@ 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) - cleanup_paths([merge_file, merge_notdir]) + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") + merge_notdir = join(merge_base, "notdir") + native_notdir = join(native_base, "notdir") + merge_missing = join(merge_base, "missing") + native_missing = join(native_base, "missing") - touch(merge_file, b"ok", 0o644) - touch(native_file, b"ok", 0o644) - touch(merge_notdir, b"x", 0o644) - touch(native_notdir, b"x", 0o644) + cleanup_paths([merge_file, merge_notdir]) - err = compare_access("F_OK existing", merge_file, native_file, os.F_OK) - if err: - return fail(err) + touch(merge_file, b"ok", 0o644) + touch(native_file, b"ok", 0o644) + touch(merge_notdir, b"x", 0o644) + touch(native_notdir, b"x", 0o644) - err = compare_access( - "F_OK missing", merge_missing, native_missing, os.F_OK, errno.ENOENT - ) - if err: - return fail(err) + err = compare_access("F_OK existing", merge_file, native_file, os.F_OK) + if err: + return fail(err) - err = compare_access( - "X_OK non-directory prefix", - join(merge_notdir, "child"), - join(native_notdir, "child"), - os.X_OK, - errno.ENOTDIR, - ) - if err: - return fail(err) + err = compare_access( + "F_OK missing", merge_missing, native_missing, os.F_OK, errno.ENOENT + ) + if err: + return fail(err) - os.chmod(merge_file, 0) - os.chmod(native_file, 0) + err = compare_access( + "X_OK non-directory prefix", + join(merge_notdir, "child"), + join(native_notdir, "child"), + os.X_OK, + errno.ENOTDIR, + ) + if err: + return fail(err) - err = compare_access("R_OK unreadable", merge_file, native_file, os.R_OK) - if err: - return fail(err) + os.chmod(merge_file, 0) + os.chmod(native_file, 0) - return 0 + err = compare_access("R_OK unreadable", merge_file, native_file, os.R_OK) + if err: + return fail(err) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_bmap b/tests/TEST_posix_bmap index 4cd49b19..d779afd6 100755 --- a/tests/TEST_posix_bmap +++ b/tests/TEST_posix_bmap @@ -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,38 +32,45 @@ 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") - touch(merge_file, b"x" * 8192) - touch(native_file, b"x" * 8192) - - mfd = os.open(merge_file, os.O_RDONLY) - nfd = os.open(native_file, os.O_RDONLY) + merge_base = temp_dir(mount) try: - # FIBMAP often requires CAP_SYS_RAWIO; compare errno parity if denied. + 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) + + mfd = os.open(merge_file, os.O_RDONLY) + nfd = os.open(native_file, os.O_RDONLY) + try: + # FIBMAP often requires CAP_SYS_RAWIO; compare errno parity if denied. + err = compare_calls( + "ioctl FIBMAP block0", + lambda: fibmap(mfd, 0), + lambda: fibmap(nfd, 0), + lambda a, b: (a >= 0) == (b >= 0), + ) + if err: + # If both denied by privilege checks, compare_calls already passes. + return fail(err) + finally: + os.close(mfd) + os.close(nfd) + + # EBADF parity on closed fds. err = compare_calls( - "ioctl FIBMAP block0", + "ioctl FIBMAP EBADF", lambda: fibmap(mfd, 0), lambda: fibmap(nfd, 0), - lambda a, b: (a >= 0) == (b >= 0), ) if err: - # If both denied by privilege checks, compare_calls already passes. return fail(err) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - # EBADF parity on closed fds. - err = compare_calls( - "ioctl FIBMAP EBADF", - lambda: fibmap(mfd, 0), - lambda: fibmap(nfd, 0), - ) - if err: - return fail(err) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_chmod b/tests/TEST_posix_chmod index a7dd7baa..67d72fd9 100755 --- a/tests/TEST_posix_chmod +++ b/tests/TEST_posix_chmod @@ -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,65 +30,72 @@ 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) - cleanup_paths([merge_file, merge_notdir]) + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") + merge_missing = join(merge_base, "missing") + native_missing = join(native_base, "missing") + merge_notdir = join(merge_base, "notdir") + native_notdir = join(native_base, "notdir") - touch(merge_file, b"x", 0o644) - touch(native_file, b"x", 0o644) - touch(merge_notdir, b"x", 0o644) - touch(native_notdir, b"x", 0o644) + cleanup_paths([merge_file, merge_notdir]) - err = compare_calls( - "chmod success", - lambda: os.chmod(merge_file, 0o600), - lambda: os.chmod(native_file, 0o600), - ) - if err: - return fail(err) + touch(merge_file, b"x", 0o644) + touch(native_file, b"x", 0o644) + touch(merge_notdir, b"x", 0o644) + touch(native_notdir, b"x", 0o644) - err = compare_calls( - "chmod stat parity", - lambda: os.lstat(merge_file), - lambda: os.lstat(native_file), - stcmp, - ) - if err: - return fail(err) - - err = compare_calls( - "chmod ENOENT", - lambda: os.chmod(merge_missing, 0o600), - lambda: os.chmod(native_missing, 0o600), - ) - if err: - return fail(err) - - err = compare_calls( - "chmod ENOTDIR", - lambda: os.chmod(join(merge_notdir, "child"), 0o600), - lambda: os.chmod(join(native_notdir, "child"), 0o600), - ) - if err: - return fail(err) - - if os.geteuid() != 0: err = compare_calls( - "chmod EPERM setuid", - lambda: os.chmod(merge_file, stat.S_ISUID | 0o644), - lambda: os.chmod(native_file, stat.S_ISUID | 0o644), + "chmod success", + lambda: os.chmod(merge_file, 0o600), + lambda: os.chmod(native_file, 0o600), ) if err: return fail(err) - else: - _ = errno.EPERM - return 0 + err = compare_calls( + "chmod stat parity", + lambda: os.lstat(merge_file), + lambda: os.lstat(native_file), + stcmp, + ) + if err: + return fail(err) + + err = compare_calls( + "chmod ENOENT", + lambda: os.chmod(merge_missing, 0o600), + lambda: os.chmod(native_missing, 0o600), + ) + if err: + return fail(err) + + err = compare_calls( + "chmod ENOTDIR", + lambda: os.chmod(join(merge_notdir, "child"), 0o600), + lambda: os.chmod(join(native_notdir, "child"), 0o600), + ) + if err: + return fail(err) + + if os.geteuid() != 0: + err = compare_calls( + "chmod EPERM setuid", + lambda: os.chmod(merge_file, stat.S_ISUID | 0o644), + lambda: os.chmod(native_file, stat.S_ISUID | 0o644), + ) + if err: + return fail(err) + else: + _ = errno.EPERM + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_chown b/tests/TEST_posix_chown index c989a958..e95caf71 100755 --- a/tests/TEST_posix_chown +++ b/tests/TEST_posix_chown @@ -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,54 +23,61 @@ 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) - cleanup_paths([merge_file, merge_notdir]) + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") + merge_missing = join(merge_base, "missing") + native_missing = join(native_base, "missing") + merge_notdir = join(merge_base, "notdir") + native_notdir = join(native_base, "notdir") - touch(merge_file, b"x", 0o644) - touch(native_file, b"x", 0o644) - touch(merge_notdir, b"x", 0o644) - touch(native_notdir, b"x", 0o644) + cleanup_paths([merge_file, merge_notdir]) - err = compare_calls( - "chown self", - lambda: os.chown(merge_file, uid, gid), - lambda: os.chown(native_file, uid, gid), - ) - if err: - return fail(err) + touch(merge_file, b"x", 0o644) + touch(native_file, b"x", 0o644) + touch(merge_notdir, b"x", 0o644) + touch(native_notdir, b"x", 0o644) - err = compare_calls( - "chown ENOENT", - lambda: os.chown(merge_missing, uid, gid), - lambda: os.chown(native_missing, uid, gid), - ) - if err: - return fail(err) - - err = compare_calls( - "chown ENOTDIR", - lambda: os.chown(join(merge_notdir, "child"), uid, gid), - lambda: os.chown(join(native_notdir, "child"), uid, gid), - ) - if err: - return fail(err) - - if os.geteuid() != 0: err = compare_calls( - "chown EPERM foreign uid", - lambda: os.chown(merge_file, 0, gid), - lambda: os.chown(native_file, 0, gid), + "chown self", + lambda: os.chown(merge_file, uid, gid), + lambda: os.chown(native_file, uid, gid), ) if err: return fail(err) - return 0 + err = compare_calls( + "chown ENOENT", + lambda: os.chown(merge_missing, uid, gid), + lambda: os.chown(native_missing, uid, gid), + ) + if err: + return fail(err) + + err = compare_calls( + "chown ENOTDIR", + lambda: os.chown(join(merge_notdir, "child"), uid, gid), + lambda: os.chown(join(native_notdir, "child"), uid, gid), + ) + if err: + return fail(err) + + if os.geteuid() != 0: + err = compare_calls( + "chown EPERM foreign uid", + lambda: os.chown(merge_file, 0, gid), + lambda: os.chown(native_file, 0, gid), + ) + if err: + return fail(err) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_copy_file_range b/tests/TEST_posix_copy_file_range index 36920a38..dbacff11 100755 --- a/tests/TEST_posix_copy_file_range +++ b/tests/TEST_posix_copy_file_range @@ -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,103 +23,110 @@ 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") - - payload = b"0123456789abcdefghijklmnopqrstuvwxyz" - touch(merge_src, payload) - touch(native_src, payload) - touch(merge_dst, b"") - touch(native_dst, b"") - - msfd = os.open(merge_src, os.O_RDONLY) - mdfd = os.open(merge_dst, os.O_WRONLY) - nsfd = os.open(native_src, os.O_RDONLY) - ndfd = os.open(native_dst, os.O_WRONLY) + merge_base = temp_dir(mount) try: - err = compare_calls( - "copy_file_range success", - lambda: os.copy_file_range(msfd, mdfd, 16), - lambda: os.copy_file_range(nsfd, ndfd, 16), - lambda a, b: a == b, - ) - if err: - return fail(err) - finally: - os.close(msfd) - os.close(mdfd) - os.close(nsfd) - os.close(ndfd) + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - with open(merge_dst, "rb") as mf, open(native_dst, "rb") as nf: - mdata = mf.read() - ndata = nf.read() - if mdata != ndata: - return fail(f"copy_file_range dst mismatch mergerfs={mdata!r} native={ndata!r}") + merge_src = join(merge_base, "src") + merge_dst = join(merge_base, "dst") + native_src = join(native_base, "src") + native_dst = join(native_base, "dst") - mdfd = os.open(merge_dst, os.O_WRONLY) - ndfd = os.open(native_dst, os.O_WRONLY) - try: - err = compare_calls( - "copy_file_range EBADF src", - lambda: os.copy_file_range(-1, mdfd, 1), - lambda: os.copy_file_range(-1, ndfd, 1), - ) - if err: - return fail(err) - finally: - os.close(mdfd) - os.close(ndfd) + payload = b"0123456789abcdefghijklmnopqrstuvwxyz" + touch(merge_src, payload) + touch(native_src, payload) + touch(merge_dst, b"") + touch(native_dst, b"") - # 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") - touch(merge_large_src, b"") - touch(merge_large_dst, b"") - touch(native_large_src, b"") - touch(native_large_dst, b"") - - msfd = os.open(merge_large_src, os.O_RDWR) - mdfd = os.open(merge_large_dst, os.O_RDWR) - nsfd = os.open(native_large_src, os.O_RDWR) - ndfd = os.open(native_large_dst, os.O_RDWR) - try: - # Make sparse files with one non-zero byte at the end so both sides - # have comparable logical contents. - os.ftruncate(msfd, large_len) - os.ftruncate(nsfd, large_len) - os.pwrite(msfd, b"z", large_len - 1) - os.pwrite(nsfd, b"z", large_len - 1) - - 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) - - mstat = os.fstat(mdfd) - nstat = os.fstat(ndfd) - if mstat.st_size != nstat.st_size: - return fail( - "copy_file_range large sparse dst size mismatch " - f"mergerfs={mstat.st_size} native={nstat.st_size}" + msfd = os.open(merge_src, os.O_RDONLY) + mdfd = os.open(merge_dst, os.O_WRONLY) + nsfd = os.open(native_src, os.O_RDONLY) + ndfd = os.open(native_dst, os.O_WRONLY) + try: + err = compare_calls( + "copy_file_range success", + lambda: os.copy_file_range(msfd, mdfd, 16), + lambda: os.copy_file_range(nsfd, ndfd, 16), + lambda a, b: a == b, ) - finally: - os.close(msfd) - os.close(mdfd) - os.close(nsfd) - os.close(ndfd) + if err: + return fail(err) + finally: + os.close(msfd) + os.close(mdfd) + os.close(nsfd) + os.close(ndfd) - return 0 + with open(merge_dst, "rb") as mf, open(native_dst, "rb") as nf: + mdata = mf.read() + ndata = nf.read() + if mdata != ndata: + return fail(f"copy_file_range dst mismatch mergerfs={mdata!r} native={ndata!r}") + + mdfd = os.open(merge_dst, os.O_WRONLY) + ndfd = os.open(native_dst, os.O_WRONLY) + try: + err = compare_calls( + "copy_file_range EBADF src", + lambda: os.copy_file_range(-1, mdfd, 1), + lambda: os.copy_file_range(-1, ndfd, 1), + ) + if err: + return fail(err) + finally: + os.close(mdfd) + os.close(ndfd) + + # Exercise very large copy lengths to validate the 64-bit copy result + # path when available. Use sparse files to keep runtime and disk usage low. + large_len = (5 << 30) + 4096 + merge_large_src = join(merge_base, "src-large") + merge_large_dst = join(merge_base, "dst-large") + native_large_src = join(native_base, "src-large") + native_large_dst = join(native_base, "dst-large") + touch(merge_large_src, b"") + touch(merge_large_dst, b"") + touch(native_large_src, b"") + touch(native_large_dst, b"") + + msfd = os.open(merge_large_src, os.O_RDWR) + mdfd = os.open(merge_large_dst, os.O_RDWR) + nsfd = os.open(native_large_src, os.O_RDWR) + ndfd = os.open(native_large_dst, os.O_RDWR) + try: + # Make sparse files with one non-zero byte at the end so both sides + # have comparable logical contents. + os.ftruncate(msfd, large_len) + os.ftruncate(nsfd, large_len) + os.pwrite(msfd, b"z", large_len - 1) + os.pwrite(nsfd, b"z", large_len - 1) + + 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) + + mstat = os.fstat(mdfd) + nstat = os.fstat(ndfd) + if mstat.st_size != nstat.st_size: + return fail( + "copy_file_range large sparse dst size mismatch " + f"mergerfs={mstat.st_size} native={nstat.st_size}" + ) + finally: + os.close(msfd) + os.close(mdfd) + os.close(nsfd) + os.close(ndfd) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_create_mknod b/tests/TEST_posix_create_mknod index 6f18e476..f9a37455 100755 --- a/tests/TEST_posix_create_mknod +++ b/tests/TEST_posix_create_mknod @@ -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,74 +21,78 @@ 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]) + cleanup_paths([merge_file, merge_exist, merge_notdir]) - err = compare_calls( - "create success", - lambda: os.open(merge_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), - lambda: os.open(native_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), - close_fds=True, - ) - if err: - return fail(err) + err = compare_calls( + "create success", + lambda: os.open(merge_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), + lambda: os.open(native_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), + close_fds=True, + ) + if err: + return fail(err) - with open(merge_exist, "wb"): - pass - with open(native_exist, "wb"): - pass + with open(merge_exist, "wb"): + pass + with open(native_exist, "wb"): + pass - err = compare_calls( - "create EEXIST", - lambda: os.open(merge_exist, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), - lambda: os.open(native_exist, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), - ) - if err: - return fail(err) + err = compare_calls( + "create EEXIST", + lambda: os.open(merge_exist, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), + lambda: os.open(native_exist, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o640), + ) + if err: + return fail(err) - with open(merge_notdir, "wb"): - pass - with open(native_notdir, "wb"): - pass + with open(merge_notdir, "wb"): + pass + with open(native_notdir, "wb"): + pass - err = compare_calls( - "create ENOTDIR", - lambda: os.open(join(merge_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640), - lambda: os.open(join(native_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640), - ) - if err: - return fail(err) + err = compare_calls( + "create ENOTDIR", + lambda: os.open(join(merge_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640), + lambda: os.open(join(native_notdir, "child"), os.O_CREAT | os.O_WRONLY, 0o640), + ) + if err: + return fail(err) - merge_node = join(mount, "posix-create/mknod") - native_node = join(native, "posix-create/mknod") - cleanup_paths([merge_node]) + merge_node = join(merge_base, "mknod") + native_node = join(native_base, "mknod") + cleanup_paths([merge_node]) - err = compare_calls( - "mknod regular", - lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0), - lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0), - ) - if err: - return fail(err) + err = compare_calls( + "mknod regular", + lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0), + lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0), + ) + if err: + return fail(err) - err = compare_calls( - "mknod EEXIST", - lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0), - lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0), - ) - if err: - return fail(err) + err = compare_calls( + "mknod EEXIST", + lambda: os.mknod(merge_node, stat.S_IFREG | 0o600, 0), + lambda: os.mknod(native_node, stat.S_IFREG | 0o600, 0), + ) + if err: + return fail(err) - return 0 + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_fsync_flush b/tests/TEST_posix_fsync_flush index 79da35c9..82f3fe13 100755 --- a/tests/TEST_posix_fsync_flush +++ b/tests/TEST_posix_fsync_flush @@ -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,57 +20,64 @@ 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") - - touch(merge_file, b"abc") - touch(native_file, b"abc") - os.makedirs(merge_dir, exist_ok=True) - os.makedirs(native_dir, exist_ok=True) - - mfd = os.open(merge_file, os.O_RDWR) - nfd = os.open(native_file, os.O_RDWR) + merge_base = temp_dir(mount) try: - os.lseek(mfd, 0, os.SEEK_END) - os.lseek(nfd, 0, os.SEEK_END) - os.write(mfd, b"-merge") - os.write(nfd, b"-native") + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - err = compare_calls("fsync file", lambda: os.fsync(mfd), lambda: os.fsync(nfd)) + 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") + os.makedirs(merge_dir, exist_ok=True) + os.makedirs(native_dir, exist_ok=True) + + mfd = os.open(merge_file, os.O_RDWR) + nfd = os.open(native_file, os.O_RDWR) + try: + os.lseek(mfd, 0, os.SEEK_END) + os.lseek(nfd, 0, os.SEEK_END) + os.write(mfd, b"-merge") + os.write(nfd, b"-native") + + err = compare_calls("fsync file", lambda: os.fsync(mfd), lambda: os.fsync(nfd)) + if err: + return fail(err) + finally: + os.close(mfd) + os.close(nfd) + + mdirfd = os.open(merge_dir, os.O_RDONLY | os.O_DIRECTORY) + ndirfd = os.open(native_dir, os.O_RDONLY | os.O_DIRECTORY) + try: + err = compare_calls("fsync dir", lambda: os.fsync(mdirfd), lambda: os.fsync(ndirfd)) + if err: + return fail(err) + finally: + os.close(mdirfd) + os.close(ndirfd) + + bad_m = os.open(merge_file, os.O_RDONLY) + bad_n = os.open(native_file, os.O_RDONLY) + os.close(bad_m) + os.close(bad_n) + err = compare_calls("fsync EBADF", lambda: os.fsync(bad_m), lambda: os.fsync(bad_n)) if err: return fail(err) + + # Close-to-flush parity check: both paths should preserve appended payload. + with open(merge_file, "rb") as mf, open(native_file, "rb") as nf: + mdata = mf.read() + ndata = nf.read() + if not (mdata.startswith(b"abc") and ndata.startswith(b"abc")): + return fail(f"flush content prefix mismatch mergerfs={mdata!r} native={ndata!r}") + + return 0 finally: - os.close(mfd) - os.close(nfd) - - mdirfd = os.open(merge_dir, os.O_RDONLY | os.O_DIRECTORY) - ndirfd = os.open(native_dir, os.O_RDONLY | os.O_DIRECTORY) - try: - err = compare_calls("fsync dir", lambda: os.fsync(mdirfd), lambda: os.fsync(ndirfd)) - if err: - return fail(err) - finally: - os.close(mdirfd) - os.close(ndirfd) - - bad_m = os.open(merge_file, os.O_RDONLY) - bad_n = os.open(native_file, os.O_RDONLY) - os.close(bad_m) - os.close(bad_n) - err = compare_calls("fsync EBADF", lambda: os.fsync(bad_m), lambda: os.fsync(bad_n)) - if err: - return fail(err) - - # Close-to-flush parity check: both paths should preserve appended payload. - with open(merge_file, "rb") as mf, open(native_file, "rb") as nf: - mdata = mf.read() - ndata = nf.read() - if not (mdata.startswith(b"abc") and ndata.startswith(b"abc")): - return fail(f"flush content prefix mismatch mergerfs={mdata!r} native={ndata!r}") - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_getattr_fgetattr b/tests/TEST_posix_getattr_fgetattr index f1ba2c4c..77ea97b4 100755 --- a/tests/TEST_posix_getattr_fgetattr +++ b/tests/TEST_posix_getattr_fgetattr @@ -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,59 +37,66 @@ 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") - - cleanup_paths([merge_file, merge_dir, merge_notdir]) - - touch(merge_file, b"abc", 0o640) - touch(native_file, b"abc", 0o640) - os.makedirs(merge_dir, exist_ok=True) - os.makedirs(native_dir, exist_ok=True) - touch(merge_notdir, b"x", 0o644) - touch(native_notdir, b"x", 0o644) - - err = compare_calls( - "lstat regular", lambda: os.lstat(merge_file), lambda: os.lstat(native_file), stcmp - ) - if err: - return fail(err) - - err = compare_calls( - "lstat directory", lambda: os.lstat(merge_dir), lambda: os.lstat(native_dir), stcmp - ) - if err: - return fail(err) - - mfd = os.open(merge_file, os.O_RDONLY) - nfd = os.open(native_file, os.O_RDONLY) + merge_base = temp_dir(mount) try: - err = compare_calls("fstat open fd", lambda: os.fstat(mfd), lambda: os.fstat(nfd), stcmp) + 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]) + + touch(merge_file, b"abc", 0o640) + touch(native_file, b"abc", 0o640) + os.makedirs(merge_dir, exist_ok=True) + os.makedirs(native_dir, exist_ok=True) + touch(merge_notdir, b"x", 0o644) + touch(native_notdir, b"x", 0o644) + + err = compare_calls( + "lstat regular", lambda: os.lstat(merge_file), lambda: os.lstat(native_file), stcmp + ) if err: return fail(err) + + err = compare_calls( + "lstat directory", lambda: os.lstat(merge_dir), lambda: os.lstat(native_dir), stcmp + ) + if err: + return fail(err) + + mfd = os.open(merge_file, os.O_RDONLY) + nfd = os.open(native_file, os.O_RDONLY) + try: + err = compare_calls("fstat open fd", lambda: os.fstat(mfd), lambda: os.fstat(nfd), stcmp) + if err: + return fail(err) + finally: + os.close(mfd) + os.close(nfd) + + err = compare_calls("lstat ENOENT", lambda: os.lstat(merge_missing), lambda: os.lstat(native_missing)) + if err: + return fail(err) + + err = compare_calls( + "lstat ENOTDIR", + lambda: os.lstat(join(merge_notdir, "child")), + lambda: os.lstat(join(native_notdir, "child")), + ) + if err: + return fail(err) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - err = compare_calls("lstat ENOENT", lambda: os.lstat(merge_missing), lambda: os.lstat(native_missing)) - if err: - return fail(err) - - err = compare_calls( - "lstat ENOTDIR", - lambda: os.lstat(join(merge_notdir, "child")), - lambda: os.lstat(join(native_notdir, "child")), - ) - if err: - return fail(err) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_ioctl b/tests/TEST_posix_ioctl index 8185da9b..27beb4c4 100755 --- a/tests/TEST_posix_ioctl +++ b/tests/TEST_posix_ioctl @@ -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,33 +29,40 @@ 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") - touch(merge_file, b"hello-world") - touch(native_file, b"hello-world") - - mfd = os.open(merge_file, os.O_RDONLY) - nfd = os.open(native_file, os.O_RDONLY) + merge_base = temp_dir(mount) try: - err = compare_calls("ioctl FIONREAD", lambda: fionread(mfd), lambda: fionread(nfd), lambda a, b: a == b) - if err: - return fail(err) + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - os.lseek(mfd, 5, os.SEEK_SET) - os.lseek(nfd, 5, os.SEEK_SET) - err = compare_calls( - "ioctl FIONREAD after seek", - lambda: fionread(mfd), - lambda: fionread(nfd), - lambda a, b: a == b, - ) - if err: - return fail(err) + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") + touch(merge_file, b"hello-world") + touch(native_file, b"hello-world") + + mfd = os.open(merge_file, os.O_RDONLY) + nfd = os.open(native_file, os.O_RDONLY) + try: + err = compare_calls("ioctl FIONREAD", lambda: fionread(mfd), lambda: fionread(nfd), lambda a, b: a == b) + if err: + return fail(err) + + os.lseek(mfd, 5, os.SEEK_SET) + os.lseek(nfd, 5, os.SEEK_SET) + err = compare_calls( + "ioctl FIONREAD after seek", + lambda: fionread(mfd), + lambda: fionread(nfd), + lambda a, b: a == b, + ) + if err: + return fail(err) + finally: + os.close(mfd) + os.close(nfd) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_link_symlink b/tests/TEST_posix_link_symlink index f4642c19..4ed1dca6 100755 --- a/tests/TEST_posix_link_symlink +++ b/tests/TEST_posix_link_symlink @@ -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,48 +21,55 @@ 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) - cleanup_paths([merge_src, merge_dst, merge_slnk]) - touch(merge_src, b"src", 0o644) - touch(native_src, b"src", 0o644) + merge_src = join(merge_base, "src") + native_src = join(native_base, "src") + merge_dst = join(merge_base, "dst") + native_dst = join(native_base, "dst") + merge_slnk = join(merge_base, "symlink") + native_slnk = join(native_base, "symlink") + merge_missing = join(merge_base, "missing") + native_missing = join(native_base, "missing") - err = compare_calls("link success", lambda: os.link(merge_src, merge_dst), lambda: os.link(native_src, native_dst)) - if err: - return fail(err) + cleanup_paths([merge_src, merge_dst, merge_slnk]) + touch(merge_src, b"src", 0o644) + touch(native_src, b"src", 0o644) - err = compare_calls( - "link ENOENT source", - lambda: os.link(merge_missing, join(mount, "posix-link/dst2")), - lambda: os.link(native_missing, join(native, "posix-link/dst2")), - ) - if err: - return fail(err) + err = compare_calls("link success", lambda: os.link(merge_src, merge_dst), lambda: os.link(native_src, native_dst)) + if err: + return fail(err) - err = compare_calls( - "symlink success", - lambda: os.symlink("src", merge_slnk), - lambda: os.symlink("src", native_slnk), - ) - if err: - return fail(err) + err = compare_calls( + "link ENOENT source", + lambda: os.link(merge_missing, join(merge_base, "dst2")), + lambda: os.link(native_missing, join(native_base, "dst2")), + ) + if err: + return fail(err) - err = compare_calls( - "symlink EEXIST", - lambda: os.symlink("src", merge_slnk), - lambda: os.symlink("src", native_slnk), - ) - if err: - return fail(err) + err = compare_calls( + "symlink success", + lambda: os.symlink("src", merge_slnk), + lambda: os.symlink("src", native_slnk), + ) + if err: + return fail(err) - return 0 + err = compare_calls( + "symlink EEXIST", + lambda: os.symlink("src", merge_slnk), + lambda: os.symlink("src", native_slnk), + ) + if err: + return fail(err) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_locking b/tests/TEST_posix_locking index 273faf84..ea447880 100755 --- a/tests/TEST_posix_locking +++ b/tests/TEST_posix_locking @@ -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,74 +27,81 @@ 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") - touch(merge_file, b"x") - touch(native_file, b"x") - - mfd1 = os.open(merge_file, os.O_RDWR) - mfd2 = os.open(merge_file, os.O_RDWR) - nfd1 = os.open(native_file, os.O_RDWR) - nfd2 = os.open(native_file, os.O_RDWR) - + merge_base = temp_dir(mount) try: - err = compare_calls( - "flock EX nonblock", - lambda: fcntl.flock(mfd1, fcntl.LOCK_EX | fcntl.LOCK_NB), - lambda: fcntl.flock(nfd1, fcntl.LOCK_EX | fcntl.LOCK_NB), - ) - if err: - return fail(err) + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - err = compare_calls( - "flock SH nonblock second fd", - lambda: fcntl.flock(mfd2, fcntl.LOCK_SH | fcntl.LOCK_NB), - lambda: fcntl.flock(nfd2, fcntl.LOCK_SH | fcntl.LOCK_NB), - ) - if err: - return fail(err) + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") + touch(merge_file, b"x") + touch(native_file, b"x") - err = compare_calls("flock unlock", lambda: fcntl.flock(mfd1, fcntl.LOCK_UN), lambda: fcntl.flock(nfd1, fcntl.LOCK_UN)) - if err: - return fail(err) - err = compare_calls("flock unlock second fd", lambda: fcntl.flock(mfd2, fcntl.LOCK_UN), lambda: fcntl.flock(nfd2, fcntl.LOCK_UN)) - if err: - return fail(err) + mfd1 = os.open(merge_file, os.O_RDWR) + mfd2 = os.open(merge_file, os.O_RDWR) + nfd1 = os.open(native_file, os.O_RDWR) + nfd2 = os.open(native_file, os.O_RDWR) - # POSIX record lock via fcntl - wrlk = pack_flock(fcntl.F_WRLCK) - unlck = pack_flock(fcntl.F_UNLCK) + try: + err = compare_calls( + "flock EX nonblock", + lambda: fcntl.flock(mfd1, fcntl.LOCK_EX | fcntl.LOCK_NB), + lambda: fcntl.flock(nfd1, fcntl.LOCK_EX | fcntl.LOCK_NB), + ) + if err: + return fail(err) - err = compare_calls("fcntl F_SETLK write lock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, wrlk)) - if err: - return fail(err) + err = compare_calls( + "flock SH nonblock second fd", + lambda: fcntl.flock(mfd2, fcntl.LOCK_SH | fcntl.LOCK_NB), + lambda: fcntl.flock(nfd2, fcntl.LOCK_SH | fcntl.LOCK_NB), + ) + if err: + return fail(err) - err = compare_calls("fcntl F_SETLK write lock second fd", lambda: fcntl.fcntl(mfd2, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd2, fcntl.F_SETLK, wrlk)) - if err: - return fail(err) + err = compare_calls("flock unlock", lambda: fcntl.flock(mfd1, fcntl.LOCK_UN), lambda: fcntl.flock(nfd1, fcntl.LOCK_UN)) + if err: + return fail(err) + err = compare_calls("flock unlock second fd", lambda: fcntl.flock(mfd2, fcntl.LOCK_UN), lambda: fcntl.flock(nfd2, fcntl.LOCK_UN)) + if err: + return fail(err) - err = compare_calls("fcntl F_SETLK unlock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, unlck), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, unlck)) - if err: - return fail(err) + # POSIX record lock via fcntl + wrlk = pack_flock(fcntl.F_WRLCK) + unlck = pack_flock(fcntl.F_UNLCK) - bad_m = os.open(merge_file, os.O_RDONLY) - bad_n = os.open(native_file, os.O_RDONLY) - os.close(bad_m) - os.close(bad_n) - err = compare_calls( - "fcntl EBADF", - lambda: fcntl.fcntl(bad_m, fcntl.F_SETLK, wrlk), - lambda: fcntl.fcntl(bad_n, fcntl.F_SETLK, wrlk), - ) - if err: - return fail(err) + err = compare_calls("fcntl F_SETLK write lock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, wrlk)) + if err: + return fail(err) + + err = compare_calls("fcntl F_SETLK write lock second fd", lambda: fcntl.fcntl(mfd2, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd2, fcntl.F_SETLK, wrlk)) + if err: + return fail(err) + + err = compare_calls("fcntl F_SETLK unlock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, unlck), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, unlck)) + if err: + return fail(err) + + bad_m = os.open(merge_file, os.O_RDONLY) + bad_n = os.open(native_file, os.O_RDONLY) + os.close(bad_m) + os.close(bad_n) + err = compare_calls( + "fcntl EBADF", + lambda: fcntl.fcntl(bad_m, fcntl.F_SETLK, wrlk), + lambda: fcntl.fcntl(bad_n, fcntl.F_SETLK, wrlk), + ) + if err: + return fail(err) + finally: + os.close(mfd1) + os.close(mfd2) + os.close(nfd1) + os.close(nfd2) + + return 0 finally: - os.close(mfd1) - os.close(mfd2) - os.close(nfd1) - os.close(nfd2) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_mkdir_rmdir b/tests/TEST_posix_mkdir_rmdir index 8b06e5f4..13a5e71d 100755 --- a/tests/TEST_posix_mkdir_rmdir +++ b/tests/TEST_posix_mkdir_rmdir @@ -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,52 +21,57 @@ 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) - cleanup_paths([merge_dir, merge_notdir]) - os.makedirs(join(mount, "posix-mkdir"), exist_ok=True) - os.makedirs(join(native, "posix-mkdir"), 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") - err = compare_calls("mkdir success", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755)) - if err: - return fail(err) + cleanup_paths([merge_dir, merge_notdir]) - err = compare_calls("mkdir EEXIST", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755)) - if err: - return fail(err) + err = compare_calls("mkdir success", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755)) + if err: + return fail(err) - touch(merge_notdir, b"x") - touch(native_notdir, b"x") - err = compare_calls( - "mkdir ENOTDIR", - lambda: os.mkdir(join(merge_notdir, "child"), 0o755), - lambda: os.mkdir(join(native_notdir, "child"), 0o755), - ) - if err: - return fail(err) + err = compare_calls("mkdir EEXIST", lambda: os.mkdir(merge_dir, 0o755), lambda: os.mkdir(native_dir, 0o755)) + if err: + return fail(err) - err = compare_calls("rmdir success", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir)) - if err: - return fail(err) + touch(merge_notdir, b"x") + touch(native_notdir, b"x") + err = compare_calls( + "mkdir ENOTDIR", + lambda: os.mkdir(join(merge_notdir, "child"), 0o755), + lambda: os.mkdir(join(native_notdir, "child"), 0o755), + ) + if err: + return fail(err) - err = compare_calls("rmdir ENOENT", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir)) - if err: - return fail(err) + err = compare_calls("rmdir success", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir)) + if err: + return fail(err) - touch(join(mount, "posix-mkdir/nonempty/file"), b"x") - touch(join(native, "posix-mkdir/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")), - ) - if err: - return fail(err) + err = compare_calls("rmdir ENOENT", lambda: os.rmdir(merge_dir), lambda: os.rmdir(native_dir)) + if err: + return fail(err) - return 0 + touch(join(merge_base, "nonempty/file"), b"x") + touch(join(native_base, "nonempty/file"), b"x") + err = compare_calls( + "rmdir ENOTEMPTY", + lambda: os.rmdir(join(merge_base, "nonempty")), + lambda: os.rmdir(join(native_base, "nonempty")), + ) + if err: + return fail(err) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_open_read_write b/tests/TEST_posix_open_read_write index 3c0e2af9..4a9beb4b 100755 --- a/tests/TEST_posix_open_read_write +++ b/tests/TEST_posix_open_read_write @@ -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,60 +21,67 @@ 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") - - cleanup_paths([merge_file, merge_notdir]) - touch(merge_file, b"123456", 0o644) - touch(native_file, b"123456", 0o644) - os.makedirs(merge_dir, exist_ok=True) - os.makedirs(native_dir, exist_ok=True) - touch(merge_notdir, b"x", 0o644) - touch(native_notdir, b"x", 0o644) - - err = compare_calls("open ENOENT", lambda: os.open(merge_missing, os.O_RDONLY), lambda: os.open(native_missing, os.O_RDONLY)) - if err: - return fail(err) - - err = compare_calls("open EISDIR", lambda: os.open(merge_dir, os.O_WRONLY), lambda: os.open(native_dir, os.O_WRONLY)) - if err: - return fail(err) - - err = compare_calls( - "open ENOTDIR", - lambda: os.open(join(merge_notdir, "child"), os.O_RDONLY), - lambda: os.open(join(native_notdir, "child"), os.O_RDONLY), - ) - if err: - return fail(err) - - mfd = os.open(merge_file, os.O_RDWR) - nfd = os.open(native_file, os.O_RDWR) + merge_base = temp_dir(mount) try: - os.lseek(mfd, 0, os.SEEK_SET) - os.lseek(nfd, 0, os.SEEK_SET) - m_data = os.read(mfd, 6) - n_data = os.read(nfd, 6) - if m_data != n_data: - return fail(f"read parity mismatch mergerfs={m_data!r} native={n_data!r}") + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - os.lseek(mfd, 0, os.SEEK_SET) - os.lseek(nfd, 0, os.SEEK_SET) - mw = os.write(mfd, b"abc") - nw = os.write(nfd, b"abc") - if mw != nw: - return fail(f"write count mismatch mergerfs={mw} native={nw}") + 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) + touch(native_file, b"123456", 0o644) + os.makedirs(merge_dir, exist_ok=True) + os.makedirs(native_dir, exist_ok=True) + touch(merge_notdir, b"x", 0o644) + touch(native_notdir, b"x", 0o644) + + err = compare_calls("open ENOENT", lambda: os.open(merge_missing, os.O_RDONLY), lambda: os.open(native_missing, os.O_RDONLY)) + if err: + return fail(err) + + err = compare_calls("open EISDIR", lambda: os.open(merge_dir, os.O_WRONLY), lambda: os.open(native_dir, os.O_WRONLY)) + if err: + return fail(err) + + err = compare_calls( + "open ENOTDIR", + lambda: os.open(join(merge_notdir, "child"), os.O_RDONLY), + lambda: os.open(join(native_notdir, "child"), os.O_RDONLY), + ) + if err: + return fail(err) + + mfd = os.open(merge_file, os.O_RDWR) + nfd = os.open(native_file, os.O_RDWR) + try: + os.lseek(mfd, 0, os.SEEK_SET) + os.lseek(nfd, 0, os.SEEK_SET) + m_data = os.read(mfd, 6) + n_data = os.read(nfd, 6) + if m_data != n_data: + return fail(f"read parity mismatch mergerfs={m_data!r} native={n_data!r}") + + os.lseek(mfd, 0, os.SEEK_SET) + os.lseek(nfd, 0, os.SEEK_SET) + mw = os.write(mfd, b"abc") + nw = os.write(nfd, b"abc") + if mw != nw: + return fail(f"write count mismatch mergerfs={mw} native={nw}") + finally: + os.close(mfd) + os.close(nfd) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_poll b/tests/TEST_posix_poll index 84b1fbec..3cdb9806 100755 --- a/tests/TEST_posix_poll +++ b/tests/TEST_posix_poll @@ -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,27 +30,34 @@ 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") - touch(merge_file, b"abcdef") - touch(native_file, b"abcdef") - - mfd = os.open(merge_file, os.O_RDONLY) - nfd = os.open(native_file, os.O_RDONLY) + merge_base = temp_dir(mount) try: - mmask = poll_mask(mfd) - nmask = poll_mask(nfd) + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - # For regular files POLLIN and POLLOUT are typically ready. - if bool(mmask & select.POLLIN) != bool(nmask & select.POLLIN): - return fail(f"poll POLLIN mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}") - if bool(mmask & select.POLLOUT) != bool(nmask & select.POLLOUT): - return fail(f"poll POLLOUT mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}") + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") + touch(merge_file, b"abcdef") + touch(native_file, b"abcdef") + + mfd = os.open(merge_file, os.O_RDONLY) + nfd = os.open(native_file, os.O_RDONLY) + try: + mmask = poll_mask(mfd) + nmask = poll_mask(nfd) + + # For regular files POLLIN and POLLOUT are typically ready. + if bool(mmask & select.POLLIN) != bool(nmask & select.POLLIN): + return fail(f"poll POLLIN mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}") + if bool(mmask & select.POLLOUT) != bool(nmask & select.POLLOUT): + return fail(f"poll POLLOUT mismatch mergerfs_mask=0x{mmask:x} native_mask=0x{nmask:x}") + finally: + os.close(mfd) + os.close(nfd) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_readdir b/tests/TEST_posix_readdir index aa3e010e..5f05564d 100755 --- a/tests/TEST_posix_readdir +++ b/tests/TEST_posix_readdir @@ -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,54 +118,61 @@ 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) - for idx in range(8): - touch(join(merge_dir, f"f{idx}"), b"x") - touch(join(native_dir, f"f{idx}"), b"x") + merge_dir = join(merge_base, "dir") + native_dir = join(native_base, "dir") + merge_notdir = join(merge_base, "notdir") + native_notdir = join(native_base, "notdir") - touch(merge_notdir, b"x") - touch(native_notdir, b"x") + for idx in range(8): + touch(join(merge_dir, f"f{idx}"), b"x") + touch(join(native_dir, f"f{idx}"), b"x") - err = compare_calls( - "opendir ENOENT", - lambda: opendir_or_raise(join(mount, "posix-readdir/missing")), - lambda: opendir_or_raise(join(native, "posix-readdir/missing")), - ) - if err: - return fail(err) + touch(merge_notdir, b"x") + touch(native_notdir, b"x") - err = compare_calls( - "opendir ENOTDIR", - lambda: opendir_or_raise(join(merge_notdir, "child")), - lambda: opendir_or_raise(join(native_notdir, "child")), - ) - if err: - return fail(err) - - merge_names = names_set(merge_dir) - native_names = names_set(native_dir) - if merge_names != native_names: - return fail(f"readdir names mismatch mergerfs={sorted(merge_names)} native={sorted(native_names)}") - - merge_consistent = seek_tell_consistent(merge_dir) - native_consistent = seek_tell_consistent(native_dir) - if merge_consistent != native_consistent: - return fail( - f"seekdir/telldir consistency mismatch mergerfs={merge_consistent} native={native_consistent}" + err = compare_calls( + "opendir ENOENT", + lambda: opendir_or_raise(join(merge_base, "missing")), + lambda: opendir_or_raise(join(native_base, "missing")), ) + if err: + return fail(err) - touch(join(merge_dir, "victim"), b"x") - touch(join(native_dir, "victim"), b"x") - merge_sig = deleted_visibility_signature(merge_dir, join(merge_dir, "victim")) - native_sig = deleted_visibility_signature(native_dir, join(native_dir, "victim")) - if merge_sig != native_sig: - return fail(f"deleted-entry visibility mismatch mergerfs={merge_sig} native={native_sig}") + err = compare_calls( + "opendir ENOTDIR", + lambda: opendir_or_raise(join(merge_notdir, "child")), + lambda: opendir_or_raise(join(native_notdir, "child")), + ) + if err: + return fail(err) - return 0 + merge_names = names_set(merge_dir) + native_names = names_set(native_dir) + if merge_names != native_names: + return fail(f"readdir names mismatch mergerfs={sorted(merge_names)} native={sorted(native_names)}") + + merge_consistent = seek_tell_consistent(merge_dir) + native_consistent = seek_tell_consistent(native_dir) + if merge_consistent != native_consistent: + return fail( + f"seekdir/telldir consistency mismatch mergerfs={merge_consistent} native={native_consistent}" + ) + + touch(join(merge_dir, "victim"), b"x") + touch(join(native_dir, "victim"), b"x") + merge_sig = deleted_visibility_signature(merge_dir, join(merge_dir, "victim")) + native_sig = deleted_visibility_signature(native_dir, join(native_dir, "victim")) + if merge_sig != native_sig: + return fail(f"deleted-entry visibility mismatch mergerfs={merge_sig} native={native_sig}") + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_readdir_plus b/tests/TEST_posix_readdir_plus index f723a12b..86875b92 100755 --- a/tests/TEST_posix_readdir_plus +++ b/tests/TEST_posix_readdir_plus @@ -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,40 +31,47 @@ 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") - - for i in range(40): - touch(join(merge_dir, f"f{i:03d}"), bytes(str(i), "ascii")) - touch(join(native_dir, f"f{i:03d}"), bytes(str(i), "ascii")) - - # Exercise mapping from readdir entries to immediate getattr calls. - m_map = list_and_stat(merge_dir) - n_map = list_and_stat(native_dir) - if m_map != n_map: - return fail("readdir+getattr map mismatch between mergerfs and native") - - # Optionally toggle readdir policy and ensure semantic output stays stable. + merge_base = temp_dir(mount) try: - orig = mergerfs_get_option(mount, "func.readdir") - except (PermissionError, FileNotFoundError, OSError): - return 0 + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) + + merge_dir = join(merge_base, "dir") + native_dir = join(native_base, "dir") + + for i in range(40): + touch(join(merge_dir, f"f{i:03d}"), bytes(str(i), "ascii")) + touch(join(native_dir, f"f{i:03d}"), bytes(str(i), "ascii")) + + # Exercise mapping from readdir entries to immediate getattr calls. + m_map = list_and_stat(merge_dir) + n_map = list_and_stat(native_dir) + if m_map != n_map: + return fail("readdir+getattr map mismatch between mergerfs and native") + + # Optionally toggle readdir policy and ensure semantic output stays stable. + try: + orig = mergerfs_get_option(mount, "func.readdir") + except (PermissionError, FileNotFoundError, OSError): + return 0 + + try: + for mode in ("seq", "cosr:2:2", "cor:2:2"): + mergerfs_set_option(mount, "func.readdir", mode) + got = list_and_stat(merge_dir) + if got != m_map: + return fail(f"func.readdir={mode} changed readdir+getattr semantic output") + except (PermissionError, FileNotFoundError, OSError): + return 0 + finally: + try: + mergerfs_set_option(mount, "func.readdir", orig) + except OSError: + pass - try: - for mode in ("seq", "cosr:2:2", "cor:2:2"): - mergerfs_set_option(mount, "func.readdir", mode) - got = list_and_stat(merge_dir) - if got != m_map: - return fail(f"func.readdir={mode} changed readdir+getattr semantic output") - except (PermissionError, FileNotFoundError, OSError): return 0 finally: - try: - mergerfs_set_option(mount, "func.readdir", orig) - except OSError: - pass - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_release b/tests/TEST_posix_release index 27c31ff7..d937b1c1 100755 --- a/tests/TEST_posix_release +++ b/tests/TEST_posix_release @@ -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,62 +19,69 @@ 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") - - 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) - nfd2 = os.dup(nfd1) - + merge_base = temp_dir(mount) try: - os.close(mfd1) - os.close(nfd1) + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - mw = os.write(mfd2, b"X") - nw = os.write(nfd2, b"X") - if mw != nw: - return fail(f"release dup write mismatch mergerfs={mw} native={nw}") + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") - os.lseek(mfd2, 0, os.SEEK_SET) - os.lseek(nfd2, 0, os.SEEK_SET) - mr = os.read(mfd2, 4) - nr = os.read(nfd2, 4) - if mr != nr: - return fail(f"release dup read mismatch mergerfs={mr!r} native={nr!r}") - finally: - os.close(mfd2) - os.close(nfd2) + touch(merge_file, b"abc") + touch(native_file, b"abc") - with open(merge_file, "rb") as mf, open(native_file, "rb") as nf: - md = mf.read() - nd = nf.read() - if md != nd: - return fail(f"release final data mismatch mergerfs={md!r} native={nd!r}") + # Open + dup so one close does not release the underlying open-file state. + mfd1 = os.open(merge_file, os.O_RDWR) + nfd1 = os.open(native_file, os.O_RDWR) + mfd2 = os.dup(mfd1) + nfd2 = os.dup(nfd1) - # Closing same fd twice should map to EBADF on both. - mfd = os.open(merge_file, os.O_RDONLY) - nfd = os.open(native_file, os.O_RDONLY) - os.close(mfd) - os.close(nfd) - try: + try: + os.close(mfd1) + os.close(nfd1) + + mw = os.write(mfd2, b"X") + nw = os.write(nfd2, b"X") + if mw != nw: + return fail(f"release dup write mismatch mergerfs={mw} native={nw}") + + os.lseek(mfd2, 0, os.SEEK_SET) + os.lseek(nfd2, 0, os.SEEK_SET) + mr = os.read(mfd2, 4) + nr = os.read(nfd2, 4) + if mr != nr: + return fail(f"release dup read mismatch mergerfs={mr!r} native={nr!r}") + finally: + os.close(mfd2) + os.close(nfd2) + + with open(merge_file, "rb") as mf, open(native_file, "rb") as nf: + md = mf.read() + nd = nf.read() + if md != nd: + return fail(f"release final data mismatch mergerfs={md!r} native={nd!r}") + + # Closing same fd twice should map to EBADF on both. + mfd = os.open(merge_file, os.O_RDONLY) + nfd = os.open(native_file, os.O_RDONLY) os.close(mfd) - m_err = 0 - except OSError as exc: - m_err = exc.errno - try: os.close(nfd) - n_err = 0 - except OSError as exc: - n_err = exc.errno - if m_err != n_err: - return fail(f"release EBADF mismatch mergerfs_errno={m_err} native_errno={n_err}") + try: + os.close(mfd) + m_err = 0 + except OSError as exc: + m_err = exc.errno + try: + os.close(nfd) + n_err = 0 + except OSError as exc: + n_err = exc.errno + if m_err != n_err: + return fail(f"release EBADF mismatch mergerfs_errno={m_err} native_errno={n_err}") - return 0 + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_releasedir b/tests/TEST_posix_releasedir index bcb52175..b4bfea95 100755 --- a/tests/TEST_posix_releasedir +++ b/tests/TEST_posix_releasedir @@ -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,17 +56,24 @@ 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") - touch(join(merge_dir, "a"), b"x") - touch(join(native_dir, "a"), b"x") + merge_base = temp_dir(mount) + try: + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - m_err = close_and_dirfd_errno(merge_dir) - n_err = close_and_dirfd_errno(native_dir) - if m_err != n_err: - return fail(f"releasedir EBADF parity mismatch mergerfs_errno={m_err} native_errno={n_err}") + merge_dir = join(merge_base, "dir") + native_dir = join(native_base, "dir") + touch(join(merge_dir, "a"), b"x") + touch(join(native_dir, "a"), b"x") - return 0 + m_err = close_and_dirfd_errno(merge_dir) + n_err = close_and_dirfd_errno(native_dir) + if m_err != n_err: + return fail(f"releasedir EBADF parity mismatch mergerfs_errno={m_err} native_errno={n_err}") + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_statfs b/tests/TEST_posix_statfs index 96e38c9a..c5524a10 100755 --- a/tests/TEST_posix_statfs +++ b/tests/TEST_posix_statfs @@ -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,24 +29,31 @@ def main(): mount = sys.argv[1] with tempfile.TemporaryDirectory() as native: - err = compare_calls( - "statvfs mount parity", - lambda: os.statvfs(mount), - lambda: os.statvfs(native), - statvfs_cmp, - ) - if err: - return fail(err) + merge_base = temp_dir(mount) + try: + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - err = compare_calls( - "statvfs ENOENT", - lambda: os.statvfs(join(mount, "posix-statfs/missing")), - lambda: os.statvfs(join(native, "posix-statfs/missing")), - ) - if err: - return fail(err) + err = compare_calls( + "statvfs mount parity", + lambda: os.statvfs(mount), + lambda: os.statvfs(native), + statvfs_cmp, + ) + if err: + return fail(err) - return 0 + err = compare_calls( + "statvfs ENOENT", + lambda: os.statvfs(join(merge_base, "missing")), + lambda: os.statvfs(join(native_base, "missing")), + ) + if err: + return fail(err) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_statx b/tests/TEST_posix_statx index f98b73f2..5d4d967b 100755 --- a/tests/TEST_posix_statx +++ b/tests/TEST_posix_statx @@ -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,88 +179,93 @@ 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") - - touch(merge_file, b"hello") + merge_base = temp_dir(mount) try: - native_file = mergerfs_fullpath(merge_file) - except OSError: - return 0 + merge_file = join(merge_base, "file") + merge_link = join(merge_base, "link") - try: - os.unlink(merge_link) - except FileNotFoundError: - pass - os.symlink("file", merge_link) + touch(merge_file, b"hello") - try: - native_link = mergerfs_fullpath(merge_link) - except OSError: - return 0 - - # user.mergerfs.fullpath for a symlink may resolve to the target file path - # depending on policy behavior. For AT_SYMLINK_NOFOLLOW comparisons we need - # the underlying symlink path itself. - expected_native_link = os.path.join(os.path.dirname(native_file), "link") - if not os.path.islink(native_link): - if os.path.islink(expected_native_link): - native_link = expected_native_link - else: + try: + native_file = mergerfs_fullpath(merge_file) + except OSError: return 0 - native_missing = os.path.join(os.path.dirname(native_file), "missing") + try: + os.unlink(merge_link) + except FileNotFoundError: + pass + os.symlink("file", merge_link) - mst, nst, err = call_pair( - "statx regular", - lambda: statx_call(merge_file), - lambda: statx_call(native_file), - ) - if err: - return fail(err) - if not stcmp(mst, nst): - return fail( - "statx basic mismatch\n" - f" mergerfs path: {merge_file}\n" - f" native path: {native_file}\n" - f" mergerfs statx: {statx_summary(mst)}\n" - f" native statx: {statx_summary(nst)}" + try: + native_link = mergerfs_fullpath(merge_link) + 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): + native_link = expected_native_link + else: + return 0 + + native_missing = os.path.join(os.path.dirname(native_file), "missing") + + mst, nst, err = call_pair( + "statx regular", + lambda: statx_call(merge_file), + lambda: statx_call(native_file), ) + if err: + return fail(err) + if not stcmp(mst, nst): + return fail( + "statx basic mismatch\n" + f" mergerfs path: {merge_file}\n" + f" native path: {native_file}\n" + f" mergerfs statx: {statx_summary(mst)}\n" + f" native statx: {statx_summary(nst)}" + ) - mst, nst, err = call_pair( - "statx symlink nofollow", - lambda: statx_call(merge_link, flags=AT_SYMLINK_NOFOLLOW), - lambda: statx_call(native_link, flags=AT_SYMLINK_NOFOLLOW), - ) - if err: - return fail(err) - if (mst.stx_mode & 0xF000) != (nst.stx_mode & 0xF000): - return fail( - "statx symlink type mismatch\n" - f" mergerfs path: {merge_link}\n" - f" native path: {native_link}\n" - f" mergerfs statx: {statx_summary(mst)}\n" - f" native statx: {statx_summary(nst)}" + mst, nst, err = call_pair( + "statx symlink nofollow", + lambda: statx_call(merge_link, flags=AT_SYMLINK_NOFOLLOW), + lambda: statx_call(native_link, flags=AT_SYMLINK_NOFOLLOW), ) + if err: + return fail(err) + if (mst.stx_mode & 0xF000) != (nst.stx_mode & 0xF000): + return fail( + "statx symlink type mismatch\n" + f" mergerfs path: {merge_link}\n" + f" native path: {native_link}\n" + f" mergerfs statx: {statx_summary(mst)}\n" + f" native statx: {statx_summary(nst)}" + ) - err = expect_same_errno( - "statx ENOENT", - lambda: statx_call(join(mount, "posix-statx/missing")), - lambda: statx_call(native_missing), - ) - if err: - return fail(err) + err = expect_same_errno( + "statx ENOENT", + lambda: statx_call(join(merge_base, "missing")), + lambda: statx_call(native_missing), + ) + if err: + return fail(err) - err = expect_same_errno( - "statx ENOTDIR", - lambda: statx_call(join(merge_file, "child")), - lambda: statx_call(join(native_file, "child")), - ) - if err: - return fail(err) + err = expect_same_errno( + "statx ENOTDIR", + lambda: statx_call(join(merge_file, "child")), + lambda: statx_call(join(native_file, "child")), + ) + if err: + return fail(err) - return 0 + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_syncfs b/tests/TEST_posix_syncfs index 8ad2b1d6..2738e925 100755 --- a/tests/TEST_posix_syncfs +++ b/tests/TEST_posix_syncfs @@ -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,26 +35,33 @@ 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") - touch(merge_file, b"x") - touch(native_file, b"x") - - mfd = os.open(merge_file, os.O_RDONLY) - nfd = os.open(native_file, os.O_RDONLY) + merge_base = temp_dir(mount) try: - err = compare_calls("syncfs success", lambda: syncfs_fd(mfd), lambda: syncfs_fd(nfd)) + 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") + + mfd = os.open(merge_file, os.O_RDONLY) + nfd = os.open(native_file, os.O_RDONLY) + try: + err = compare_calls("syncfs success", lambda: syncfs_fd(mfd), lambda: syncfs_fd(nfd)) + if err: + return fail(err) + finally: + os.close(mfd) + os.close(nfd) + + err = compare_calls("syncfs EBADF", lambda: syncfs_fd(-1), lambda: syncfs_fd(-1)) if err: return fail(err) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - err = compare_calls("syncfs EBADF", lambda: syncfs_fd(-1), lambda: syncfs_fd(-1)) - if err: - return fail(err) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_tmpfile b/tests/TEST_posix_tmpfile index 054424b8..19794822 100755 --- a/tests/TEST_posix_tmpfile +++ b/tests/TEST_posix_tmpfile @@ -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,32 +34,39 @@ 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) - - m_err = errno_of(lambda: open_tmpfile(merge_dir)) - n_err = errno_of(lambda: open_tmpfile(native_dir)) - 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 - - mfd = open_tmpfile(merge_dir) - nfd = open_tmpfile(native_dir) + merge_base = temp_dir(mount) try: - mw = os.write(mfd, b"tmpfile-data") - nw = os.write(nfd, b"tmpfile-data") - if mw != nw: - return fail(f"O_TMPFILE write count mismatch mergerfs={mw} native={nw}") - finally: - os.close(mfd) - os.close(nfd) + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - return 0 + 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}") + + # If unsupported on both, treat as skip-pass. + if m_err in (errno.EOPNOTSUPP, errno.EISDIR, errno.EINVAL, errno.ENOSYS): + return 0 + + mfd = open_tmpfile(merge_dir) + nfd = open_tmpfile(native_dir) + try: + mw = os.write(mfd, b"tmpfile-data") + nw = os.write(nfd, b"tmpfile-data") + if mw != nw: + return fail(f"O_TMPFILE write count mismatch mergerfs={mw} native={nw}") + finally: + os.close(mfd) + os.close(nfd) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_truncate_ftruncate b/tests/TEST_posix_truncate_ftruncate index 4fe6fad4..1990a420 100755 --- a/tests/TEST_posix_truncate_ftruncate +++ b/tests/TEST_posix_truncate_ftruncate @@ -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,38 +21,45 @@ 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") - - cleanup_paths([merge_file]) - touch(merge_file, b"1234567890") - touch(native_file, b"1234567890") - - err = compare_calls("truncate shrink", lambda: os.truncate(merge_file, 3), lambda: os.truncate(native_file, 3)) - if err: - return fail(err) - - err = compare_calls("truncate ENOENT", lambda: os.truncate(merge_missing, 1), lambda: os.truncate(native_missing, 1)) - if err: - return fail(err) - - mfd = os.open(merge_file, os.O_RDWR) - nfd = os.open(native_file, os.O_RDWR) + merge_base = temp_dir(mount) try: - err = compare_calls("ftruncate grow", lambda: os.ftruncate(mfd, 16), lambda: os.ftruncate(nfd, 16)) + 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") + touch(native_file, b"1234567890") + + err = compare_calls("truncate shrink", lambda: os.truncate(merge_file, 3), lambda: os.truncate(native_file, 3)) if err: return fail(err) + + err = compare_calls("truncate ENOENT", lambda: os.truncate(merge_missing, 1), lambda: os.truncate(native_missing, 1)) + if err: + return fail(err) + + mfd = os.open(merge_file, os.O_RDWR) + nfd = os.open(native_file, os.O_RDWR) + try: + err = compare_calls("ftruncate grow", lambda: os.ftruncate(mfd, 16), lambda: os.ftruncate(nfd, 16)) + if err: + return fail(err) + finally: + os.close(mfd) + os.close(nfd) + + err = compare_calls("ftruncate EBADF", lambda: os.ftruncate(-1, 4), lambda: os.ftruncate(-1, 4)) + if err: + return fail(err) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - err = compare_calls("ftruncate EBADF", lambda: os.ftruncate(-1, 4), lambda: os.ftruncate(-1, 4)) - if err: - return fail(err) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_unlink_rename b/tests/TEST_posix_unlink_rename index 2c4621d9..3babf657 100755 --- a/tests/TEST_posix_unlink_rename +++ b/tests/TEST_posix_unlink_rename @@ -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,51 +21,58 @@ 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) - cleanup_paths([merge_a, merge_b, merge_notdir]) - touch(merge_a, b"a") - touch(native_a, b"a") + merge_a = join(merge_base, "a") + native_a = join(native_base, "a") + merge_b = join(merge_base, "b") + native_b = join(native_base, "b") + merge_missing = join(merge_base, "missing") + native_missing = join(native_base, "missing") + merge_notdir = join(merge_base, "notdir") + native_notdir = join(native_base, "notdir") - err = compare_calls("unlink success", lambda: os.unlink(merge_a), lambda: os.unlink(native_a)) - if err: - return fail(err) + cleanup_paths([merge_a, merge_b, merge_notdir]) + touch(merge_a, b"a") + touch(native_a, b"a") - err = compare_calls("unlink ENOENT", lambda: os.unlink(merge_missing), lambda: os.unlink(native_missing)) - if err: - return fail(err) + err = compare_calls("unlink success", lambda: os.unlink(merge_a), lambda: os.unlink(native_a)) + if err: + return fail(err) - touch(merge_a, b"a") - touch(native_a, b"a") - touch(merge_b, b"b") - touch(native_b, b"b") + err = compare_calls("unlink ENOENT", lambda: os.unlink(merge_missing), lambda: os.unlink(native_missing)) + if err: + return fail(err) - err = compare_calls("rename overwrite", lambda: os.rename(merge_a, merge_b), lambda: os.rename(native_a, native_b)) - if err: - return fail(err) + touch(merge_a, b"a") + touch(native_a, b"a") + touch(merge_b, b"b") + touch(native_b, b"b") - err = compare_calls("rename ENOENT", lambda: os.rename(merge_missing, merge_a), lambda: os.rename(native_missing, native_a)) - if err: - return fail(err) + err = compare_calls("rename overwrite", lambda: os.rename(merge_a, merge_b), lambda: os.rename(native_a, native_b)) + if err: + return fail(err) - touch(merge_notdir, b"x") - touch(native_notdir, b"x") - err = compare_calls( - "rename ENOTDIR", - lambda: os.rename(join(merge_notdir, "child"), merge_a), - lambda: os.rename(join(native_notdir, "child"), native_a), - ) - if err: - return fail(err) + err = compare_calls("rename ENOENT", lambda: os.rename(merge_missing, merge_a), lambda: os.rename(native_missing, native_a)) + if err: + return fail(err) - return 0 + touch(merge_notdir, b"x") + touch(native_notdir, b"x") + err = compare_calls( + "rename ENOTDIR", + lambda: os.rename(join(merge_notdir, "child"), merge_a), + lambda: os.rename(join(native_notdir, "child"), native_a), + ) + if err: + return fail(err) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_utimens b/tests/TEST_posix_utimens index 59796d44..96f51a76 100755 --- a/tests/TEST_posix_utimens +++ b/tests/TEST_posix_utimens @@ -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,57 +22,64 @@ 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") - - cleanup_paths([merge_file]) - touch(merge_file, b"x") - touch(native_file, b"x") - - now = time.time() - times = (now - 1000, now - 500) - - err = compare_calls( - "utime path success", - lambda: os.utime(merge_file, times=times), - lambda: os.utime(native_file, times=times), - ) - if err: - return fail(err) - - mfd = os.open(merge_file, os.O_RDWR) - nfd = os.open(native_file, os.O_RDWR) + 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") + touch(native_file, b"x") + + now = time.time() + times = (now - 1000, now - 500) + err = compare_calls( - "utime fd success", - lambda: os.utime(mfd, times=times), - lambda: os.utime(nfd, times=times), + "utime path success", + lambda: os.utime(merge_file, times=times), + lambda: os.utime(native_file, times=times), ) if err: return fail(err) + + mfd = os.open(merge_file, os.O_RDWR) + nfd = os.open(native_file, os.O_RDWR) + try: + err = compare_calls( + "utime fd success", + lambda: os.utime(mfd, times=times), + lambda: os.utime(nfd, times=times), + ) + if err: + return fail(err) + finally: + os.close(mfd) + os.close(nfd) + + err = compare_calls( + "utime ENOENT", + lambda: os.utime(merge_missing, times=times), + lambda: os.utime(native_missing, times=times), + ) + if err: + return fail(err) + + err = compare_calls( + "utime EBADF", + lambda: os.utime(-1, times=times), + lambda: os.utime(-1, times=times), + ) + if err: + return fail(err) + + return 0 finally: - os.close(mfd) - os.close(nfd) - - err = compare_calls( - "utime ENOENT", - lambda: os.utime(merge_missing, times=times), - lambda: os.utime(native_missing, times=times), - ) - if err: - return fail(err) - - err = compare_calls( - "utime EBADF", - lambda: os.utime(-1, times=times), - lambda: os.utime(-1, times=times), - ) - if err: - return fail(err) - - return 0 + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_xattr b/tests/TEST_posix_xattr index a2762b37..fafe9760 100755 --- a/tests/TEST_posix_xattr +++ b/tests/TEST_posix_xattr @@ -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,119 +40,126 @@ 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) - touch(merge_file, b"x") - touch(native_file, b"x") - touch(merge_notdir, b"x") - touch(native_notdir, b"x") + merge_file = join(merge_base, "file") + native_file = join(native_base, "file") + merge_missing = join(merge_base, "missing") + native_missing = join(native_base, "missing") + merge_notdir = join(merge_base, "notdir") + native_notdir = join(native_base, "notdir") - err = compare_calls( - "setxattr success", - lambda: os.setxattr(merge_file, xname, xvalue), - lambda: os.setxattr(native_file, xname, xvalue), - ) - if err: - try: - os.setxattr(merge_file, xname, xvalue) - except OSError as exc: - err += " | " + format_oserror("mergerfs setxattr", exc) - try: - os.setxattr(native_file, xname, xvalue) - except OSError as exc: - err += " | " + format_oserror("native setxattr", exc) - return fail(err) + touch(merge_file, b"x") + touch(native_file, b"x") + touch(merge_notdir, b"x") + touch(native_notdir, b"x") - err = compare_calls( - "getxattr success", - lambda: os.getxattr(merge_file, xname), - lambda: os.getxattr(native_file, xname), - lambda lhs, rhs: lhs == rhs, - ) - if err: - try: - os.getxattr(merge_file, xname) - except OSError as exc: - err += " | " + format_oserror("mergerfs getxattr", exc) - try: - os.getxattr(native_file, xname) - except OSError as exc: - err += " | " + format_oserror("native getxattr", exc) - return fail(err) + err = compare_calls( + "setxattr success", + lambda: os.setxattr(merge_file, xname, xvalue), + lambda: os.setxattr(native_file, xname, xvalue), + ) + if err: + try: + os.setxattr(merge_file, xname, xvalue) + except OSError as exc: + err += " | " + format_oserror("mergerfs setxattr", exc) + try: + os.setxattr(native_file, xname, xvalue) + except OSError as exc: + err += " | " + format_oserror("native setxattr", exc) + return fail(err) - err = compare_calls( - "listxattr includes key", - lambda: os.listxattr(merge_file), - lambda: os.listxattr(native_file), - has_attr(xname), - ) - if err: - try: - m_list = os.listxattr(merge_file) - err += f" | mergerfs list={m_list!r}" - except OSError as exc: - err += " | " + format_oserror("mergerfs listxattr", exc) - try: - n_list = os.listxattr(native_file) - err += f" | native list={n_list!r}" - except OSError as exc: - err += " | " + format_oserror("native listxattr", exc) - return fail(err) + err = compare_calls( + "getxattr success", + lambda: os.getxattr(merge_file, xname), + lambda: os.getxattr(native_file, xname), + lambda lhs, rhs: lhs == rhs, + ) + if err: + try: + os.getxattr(merge_file, xname) + except OSError as exc: + err += " | " + format_oserror("mergerfs getxattr", exc) + try: + os.getxattr(native_file, xname) + except OSError as exc: + err += " | " + format_oserror("native getxattr", exc) + return fail(err) - err = compare_calls( - "removexattr success", - lambda: os.removexattr(merge_file, xname), - lambda: os.removexattr(native_file, xname), - ) - if err: - try: - os.removexattr(merge_file, xname) - except OSError as exc: - err += " | " + format_oserror("mergerfs removexattr", exc) - try: - os.removexattr(native_file, xname) - except OSError as exc: - err += " | " + format_oserror("native removexattr", exc) - return fail(err) + err = compare_calls( + "listxattr includes key", + lambda: os.listxattr(merge_file), + lambda: os.listxattr(native_file), + has_attr(xname), + ) + if err: + try: + m_list = os.listxattr(merge_file) + err += f" | mergerfs list={m_list!r}" + except OSError as exc: + err += " | " + format_oserror("mergerfs listxattr", exc) + try: + n_list = os.listxattr(native_file) + err += f" | native list={n_list!r}" + except OSError as exc: + err += " | " + format_oserror("native listxattr", exc) + return fail(err) - err = compare_calls( - "getxattr missing attr", - lambda: os.getxattr(merge_file, xname), - lambda: os.getxattr(native_file, xname), - ) - if err: - try: - os.getxattr(merge_file, xname) - except OSError as exc: - err += " | " + format_oserror("mergerfs getxattr missing", exc) - try: - os.getxattr(native_file, xname) - except OSError as exc: - err += " | " + format_oserror("native getxattr missing", exc) - return fail(err) + err = compare_calls( + "removexattr success", + lambda: os.removexattr(merge_file, xname), + lambda: os.removexattr(native_file, xname), + ) + if err: + try: + os.removexattr(merge_file, xname) + except OSError as exc: + err += " | " + format_oserror("mergerfs removexattr", exc) + try: + os.removexattr(native_file, xname) + except OSError as exc: + err += " | " + format_oserror("native removexattr", exc) + return fail(err) - err = compare_calls( - "setxattr ENOENT", - lambda: os.setxattr(merge_missing, xname, xvalue), - lambda: os.setxattr(native_missing, xname, xvalue), - ) - if err: - return fail(err) + err = compare_calls( + "getxattr missing attr", + lambda: os.getxattr(merge_file, xname), + lambda: os.getxattr(native_file, xname), + ) + if err: + try: + os.getxattr(merge_file, xname) + except OSError as exc: + err += " | " + format_oserror("mergerfs getxattr missing", exc) + try: + os.getxattr(native_file, xname) + except OSError as exc: + err += " | " + format_oserror("native getxattr missing", exc) + return fail(err) - err = compare_calls( - "getxattr ENOTDIR", - lambda: os.getxattr(join(merge_notdir, "child"), xname), - lambda: os.getxattr(join(native_notdir, "child"), xname), - ) - if err: - return fail(err) + err = compare_calls( + "setxattr ENOENT", + lambda: os.setxattr(merge_missing, xname, xvalue), + lambda: os.setxattr(native_missing, xname, xvalue), + ) + if err: + return fail(err) - return 0 + err = compare_calls( + "getxattr ENOTDIR", + lambda: os.getxattr(join(merge_notdir, "child"), xname), + lambda: os.getxattr(join(native_notdir, "child"), xname), + ) + if err: + return fail(err) + + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_posix_xattr_matrix b/tests/TEST_posix_xattr_matrix index 4f430e6e..579ce8a8 100755 --- a/tests/TEST_posix_xattr_matrix +++ b/tests/TEST_posix_xattr_matrix @@ -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,71 +71,50 @@ 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") - - file_m = join(root_m, "file") - file_n = join(root_n, "file") - dir_m = join(root_m, "dir") - dir_n = join(root_n, "dir") - link_m = join(root_m, "link") - link_n = join(root_n, "link") - - touch(file_m, b"x") - touch(file_n, b"x") - os.makedirs(dir_m, exist_ok=True) - os.makedirs(dir_n, exist_ok=True) + merge_base = temp_dir(mount) try: - os.unlink(link_m) - except FileNotFoundError: - pass - try: - os.unlink(link_n) - except FileNotFoundError: - pass - os.symlink("file", link_m) - os.symlink("file", link_n) + native_base = join(native, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) - objs = [ - ("file", file_m, file_n), - ("dir", dir_m, dir_n), - ] + root_m = merge_base + root_n = native_base - xbase = "user.mergerfs.matrix" + file_m = join(root_m, "file") + file_n = join(root_n, "file") + dir_m = join(root_m, "dir") + dir_n = join(root_n, "dir") + link_m = join(root_m, "link") + link_n = join(root_n, "link") - for obj_name, path_m, path_n in objs: - xname = f"{xbase}.{obj_name}" + touch(file_m, b"x") + touch(file_n, b"x") + os.makedirs(dir_m, exist_ok=True) + os.makedirs(dir_n, exist_ok=True) + try: + os.unlink(link_m) + except FileNotFoundError: + pass + try: + os.unlink(link_n) + except FileNotFoundError: + pass + os.symlink("file", link_m) + os.symlink("file", link_n) - err = compare( - f"setxattr default {obj_name}", - lambda p=path_m, n=xname: os.setxattr(p, n, b"v1"), - lambda p=path_n, n=xname: os.setxattr(p, n, b"v1"), - ) - if err: - return fail(err) + objs = [ + ("file", file_m, file_n), + ("dir", dir_m, dir_n), + ] - err = getxattr_cmp(path_m, path_n, xname) - if err: - return fail(err) + xbase = "user.mergerfs.matrix" - err = list_has_cmp(path_m, path_n, xname) - if err: - return fail(err) + for obj_name, path_m, path_n in objs: + xname = f"{xbase}.{obj_name}" - if hasattr(os, "XATTR_CREATE"): err = compare( - f"setxattr CREATE existing {obj_name}", - lambda p=path_m, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE), - lambda p=path_n, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE), - ) - if err: - return fail(err) - - if hasattr(os, "XATTR_REPLACE"): - err = compare( - f"setxattr REPLACE existing {obj_name}", - lambda p=path_m, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE), - lambda p=path_n, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE), + f"setxattr default {obj_name}", + lambda p=path_m, n=xname: os.setxattr(p, n, b"v1"), + lambda p=path_n, n=xname: os.setxattr(p, n, b"v1"), ) if err: return fail(err) @@ -142,69 +123,97 @@ def main(): if err: return fail(err) - missing_name = f"{xname}.missing" - if hasattr(os, "XATTR_REPLACE"): + err = list_has_cmp(path_m, path_n, xname) + if err: + return fail(err) + + if hasattr(os, "XATTR_CREATE"): + err = compare( + f"setxattr CREATE existing {obj_name}", + lambda p=path_m, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE), + lambda p=path_n, n=xname: os.setxattr(p, n, b"v2", os.XATTR_CREATE), + ) + if err: + return fail(err) + + if hasattr(os, "XATTR_REPLACE"): + err = compare( + f"setxattr REPLACE existing {obj_name}", + lambda p=path_m, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE), + lambda p=path_n, n=xname: os.setxattr(p, n, b"v3", os.XATTR_REPLACE), + ) + if err: + return fail(err) + + err = getxattr_cmp(path_m, path_n, xname) + if err: + return fail(err) + + missing_name = f"{xname}.missing" + if hasattr(os, "XATTR_REPLACE"): + err = compare( + f"setxattr REPLACE missing {obj_name}", + lambda p=path_m, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE), + lambda p=path_n, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE), + ) + if err: + return fail(err) + err = compare( - f"setxattr REPLACE missing {obj_name}", - lambda p=path_m, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE), - lambda p=path_n, n=missing_name: os.setxattr(p, n, b"v", os.XATTR_REPLACE), + f"removexattr existing {obj_name}", + lambda p=path_m, n=xname: os.removexattr(p, n), + lambda p=path_n, n=xname: os.removexattr(p, n), ) if err: return fail(err) - err = compare( - f"removexattr existing {obj_name}", - lambda p=path_m, n=xname: os.removexattr(p, n), - lambda p=path_n, n=xname: os.removexattr(p, n), - ) - if err: - return fail(err) + err = compare( + f"removexattr missing {obj_name}", + lambda p=path_m, n=xname: os.removexattr(p, n), + lambda p=path_n, n=xname: os.removexattr(p, n), + ) + if err: + return fail(err) - err = compare( - f"removexattr missing {obj_name}", - lambda p=path_m, n=xname: os.removexattr(p, n), - lambda p=path_n, n=xname: os.removexattr(p, n), - ) - if err: - return fail(err) + lxname = f"{xbase}.link" + if all(hasattr(os, fn) for fn in ("lsetxattr", "lgetxattr", "llistxattr", "lremovexattr")): + err = compare( + "lsetxattr symlink default", + lambda: os.lsetxattr(link_m, lxname, b"lv1"), + lambda: os.lsetxattr(link_n, lxname, b"lv1"), + ) + if err: + return fail(err) - lxname = f"{xbase}.link" - if all(hasattr(os, fn) for fn in ("lsetxattr", "lgetxattr", "llistxattr", "lremovexattr")): - err = compare( - "lsetxattr symlink default", - lambda: os.lsetxattr(link_m, lxname, b"lv1"), - lambda: os.lsetxattr(link_n, lxname, b"lv1"), - ) - if err: - return fail(err) + err = compare( + "lgetxattr symlink", + lambda: os.lgetxattr(link_m, lxname), + lambda: os.lgetxattr(link_n, lxname), + lambda a, b: a == b, + ) + if err: + return fail(err) - err = compare( - "lgetxattr symlink", - lambda: os.lgetxattr(link_m, lxname), - lambda: os.lgetxattr(link_n, lxname), - lambda a, b: a == b, - ) - if err: - return fail(err) + err = compare( + "llistxattr has symlink key", + lambda: os.llistxattr(link_m), + lambda: os.llistxattr(link_n), + lambda a, b: ((lxname in a) == (lxname in b)), + ) + if err: + return fail(err) - err = compare( - "llistxattr has symlink key", - lambda: os.llistxattr(link_m), - lambda: os.llistxattr(link_n), - lambda a, b: ((lxname in a) == (lxname in b)), - ) - if err: - return fail(err) + err = compare( + "lremovexattr symlink", + lambda: os.lremovexattr(link_m, lxname), + lambda: os.lremovexattr(link_n, lxname), + ) + if err: + return fail(err) - err = compare( - "lremovexattr symlink", - lambda: os.lremovexattr(link_m, lxname), - lambda: os.lremovexattr(link_n, lxname), - ) - if err: - return fail(err) - - return 0 + return 0 + finally: + cleanup_dir(merge_base) if __name__ == "__main__": diff --git a/tests/TEST_readlink_semantics b/tests/TEST_readlink_semantics index 10e8b89c..c8529e6e 100755 --- a/tests/TEST_readlink_semantics +++ b/tests/TEST_readlink_semantics @@ -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,62 +54,66 @@ def main(): target = "target-abcdefghijklmnopqrstuvwxyz" with tempfile.TemporaryDirectory() as native_dir: - 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"), - } - paths["merge_private_link"] = os.path.join(paths["merge_private_dir"], "link") - paths["native_private_link"] = os.path.join(paths["native_private_dir"], "link") + merge_base = temp_dir(mount) + try: + native_base = join(native_dir, os.path.basename(merge_base)) + os.makedirs(native_base, exist_ok=True) + + paths = { + "merge_link": join(merge_base, "readlink-semantics-link"), + "native_link": join(native_base, "readlink-semantics-link"), + "merge_regular": join(merge_base, "readlink-semantics-regular"), + "native_regular": join(native_base, "readlink-semantics-regular"), + "merge_notdir": join(merge_base, "readlink-semantics-notdir"), + "native_notdir": join(native_base, "readlink-semantics-notdir"), + "merge_loop": join(merge_base, "readlink-semantics-loop"), + "native_loop": join(native_base, "readlink-semantics-loop"), + "merge_private_dir": join(merge_base, "readlink-semantics-private"), + "native_private_dir": join(native_base, "readlink-semantics-private"), + } + paths["merge_private_link"] = os.path.join(paths["merge_private_dir"], "link") + paths["native_private_link"] = os.path.join(paths["native_private_dir"], "link") + + for p in ( + paths["merge_private_link"], + paths["merge_link"], + paths["merge_regular"], + paths["merge_notdir"], + paths["merge_loop"], + ): + try: + os.unlink(p) + except FileNotFoundError: + pass - for p in ( - paths["merge_private_link"], - paths["merge_link"], - paths["merge_regular"], - paths["merge_notdir"], - paths["merge_loop"], - ): try: - os.unlink(p) + os.rmdir(paths["merge_private_dir"]) except FileNotFoundError: pass + except OSError: + pass - try: - os.rmdir(paths["merge_private_dir"]) - except FileNotFoundError: - pass - except OSError: - pass + os.symlink(target, paths["merge_link"]) + os.symlink(target, paths["native_link"]) - os.symlink(target, paths["merge_link"]) - os.symlink(target, paths["native_link"]) + with open(paths["merge_regular"], "w", encoding="ascii"): + pass + with open(paths["native_regular"], "w", encoding="ascii"): + pass - with open(paths["merge_regular"], "w", encoding="ascii"): - pass - with open(paths["native_regular"], "w", encoding="ascii"): - pass + with open(paths["merge_notdir"], "w", encoding="ascii"): + pass + with open(paths["native_notdir"], "w", encoding="ascii"): + pass - with open(paths["merge_notdir"], "w", encoding="ascii"): - pass - with open(paths["native_notdir"], "w", encoding="ascii"): - pass + os.symlink("readlink-semantics-loop", paths["merge_loop"]) + os.symlink("readlink-semantics-loop", paths["native_loop"]) - os.symlink("readlink-semantics-loop", paths["merge_loop"]) - os.symlink("readlink-semantics-loop", paths["native_loop"]) + os.makedirs(paths["merge_private_dir"], exist_ok=True) + os.makedirs(paths["native_private_dir"], exist_ok=True) + os.symlink(target, paths["merge_private_link"]) + os.symlink(target, paths["native_private_link"]) - os.makedirs(paths["merge_private_dir"], exist_ok=True) - os.makedirs(paths["native_private_dir"], exist_ok=True) - os.symlink(target, paths["merge_private_link"]) - os.symlink(target, paths["native_private_link"]) - - 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): diff --git a/tests/TEST_rmdir_enotempty_priority b/tests/TEST_rmdir_enotempty_priority new file mode 100755 index 00000000..1f32797f --- /dev/null +++ b/tests/TEST_rmdir_enotempty_priority @@ -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 ", 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()) \ No newline at end of file diff --git a/tests/posix_parity.py b/tests/posix_parity.py index 77b96a0b..9de14a55 100644 --- a/tests/posix_parity.py +++ b/tests/posix_parity.py @@ -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 diff --git a/tests/tests.cpp b/tests/tests.cpp index c016237e..d81f33d7 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -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 #include #include +#include #include #include +#include #include #include @@ -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 _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 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 algos = { + "path-hash", + "devino-hash", + "hybrid-hash", + }; + + std::vector 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 v = {1}; + RND::shrink_to_rand_elem(v); + TEST_CHECK(v.size() == 1); + + std::vector 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(branch._minfreespace)) + TEST_CHECK(std::get(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(branch._minfreespace)) + TEST_CHECK(std::get(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},