#!/usr/bin/env python3 import errno import os import sys from posix_parity import fail from posix_parity import mergerfs_get_option from posix_parity import mergerfs_set_option from posix_parity import parse_allpaths from posix_parity import touch from posix_parity import join def get_errno(func): try: func() return 0 except OSError as exc: return exc.errno def allpaths(path): raw = os.getxattr(path, "user.mergerfs.allpaths") return parse_allpaths(raw) def must_two_branches(paths, op): if len(paths) < 2: return f"{op}: requires at least 2 branches; got {paths!r}" return None def pick_cross_branch_paths(mount): src = join(mount, "cfg-exdev/src") dst = join(mount, "cfg-exdev/sub/dst") touch(src, b"src") os.makedirs(os.path.dirname(dst), exist_ok=True) try: src_paths = allpaths(src) except OSError as exc: if exc.errno in (errno.EOPNOTSUPP, errno.ENOTSUP, errno.ENOSYS): raise RuntimeError("allpaths-xattr-unsupported") raise if len(src_paths) < 2: # Try to create destination first to force different existing-path decisions. touch(dst, b"old") os.unlink(dst) try: src_paths = allpaths(src) except OSError as exc: if exc.errno in (errno.EOPNOTSUPP, errno.ENOTSUP, errno.ENOSYS): raise RuntimeError("allpaths-xattr-unsupported") raise err = must_two_branches(src_paths, "cross-branch setup") if err: raise RuntimeError(err) return src, dst def main(): if len(sys.argv) != 2: print("usage: TEST_cfg_link_rename_exdev ", file=sys.stderr) return 1 mount = sys.argv[1] 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": 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: # 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) # 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) # 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 if __name__ == "__main__": raise SystemExit(main())