#!/usr/bin/env python3 import errno import os import sys import uuid from posix_parity import cleanup_dir from posix_parity import fail from posix_parity import join from posix_parity import mergerfs_branches from posix_parity import mergerfs_get_option from posix_parity import mergerfs_mount from posix_parity import mergerfs_set_option from posix_parity import touch SKIP = 77 def get_errno(func): try: func() return 0 except OSError as exc: return exc.errno def symlink_resolves(path): target = os.readlink(path) if os.path.isabs(target): return os.path.realpath(target) return os.path.realpath(join(os.path.dirname(path), target)) def require_symlink(path, label): if not os.path.islink(path): return f"{label}: expected symlink fallback" if not os.path.exists(path): return f"{label}: symlink target does not exist: {os.readlink(path)!r}" return None def setup_paths(branches, mount, rel_base, name): src_rel = join(rel_base, f"{name}.src") dst_rel = join(rel_base, "dst", name) src = join(mount, src_rel) dst = join(mount, dst_rel) cleanup_dir(join(branches[0], rel_base)) cleanup_dir(join(branches[1], rel_base)) os.makedirs(join(branches[0], rel_base), exist_ok=True) os.makedirs(join(branches[1], rel_base, "dst"), exist_ok=True) touch(join(branches[0], src_rel), b"src") return src, dst, join(branches[0], src_rel) def main(): try: with mergerfs_mount(num_branches=2) as (mount, branches): rel_base = f".mergerfs_exdev_test_{os.getpid()}_{uuid.uuid4().hex}" if len(branches) < 2: print("EXDEV fallback test requires at least 2 branches", end="") return SKIP branches = branches[:2] if os.stat(branches[0]).st_dev == os.stat(branches[1]).st_dev: print("EXDEV fallback test requires branches on different devices", end="") return SKIP try: try: orig_create = mergerfs_get_option(mount, "category.create") orig_link = mergerfs_get_option(mount, "link-exdev") orig_rename = mergerfs_get_option(mount, "rename-exdev") except (PermissionError, FileNotFoundError, OSError): print("EXDEV fallback runtime options unavailable", end="") return SKIP try: mergerfs_set_option(mount, "category.create", "epmfs") src, dst, src_base = setup_paths(branches, mount, rel_base, "link_pass") mergerfs_set_option(mount, "link-exdev", "passthrough") err = get_errno(lambda: os.link(src, dst + ".link.pass")) if err != errno.EXDEV: print(f"link-exdev passthrough did not exercise EXDEV, errno={err}", end="") return SKIP for mode in ("rel-symlink", "abs-base-symlink", "abs-pool-symlink"): src, dst, src_base = setup_paths(branches, mount, rel_base, f"link_{mode}") mergerfs_set_option(mount, "link-exdev", mode) link_path = dst + ".link" err = get_errno(lambda: os.link(src, link_path)) if err != 0: return fail(f"link-exdev={mode} returned errno={err}") err_msg = require_symlink(link_path, f"link-exdev={mode}") if err_msg: return fail(err_msg) resolved = symlink_resolves(link_path) if mode == "abs-base-symlink" and resolved != os.path.realpath(src_base): return fail(f"link-exdev={mode}: target {resolved!r} != {os.path.realpath(src_base)!r}") if mode != "abs-base-symlink" and resolved != os.path.realpath(src): return fail(f"link-exdev={mode}: target {resolved!r} != {os.path.realpath(src)!r}") src_ren, dst_ren, _ = setup_paths(branches, mount, rel_base, "rename_pass") mergerfs_set_option(mount, "rename-exdev", "passthrough") err = get_errno(lambda: os.rename(src_ren, dst_ren + ".ren.pass")) if err != errno.EXDEV: print(f"rename-exdev passthrough did not exercise EXDEV, errno={err}", end="") return SKIP for mode in ("rel-symlink", "abs-symlink"): src_ren, dst_ren, _ = setup_paths(branches, mount, rel_base, f"rename_{mode}") mergerfs_set_option(mount, "rename-exdev", mode) ren_path = dst_ren + ".ren" err = get_errno(lambda: os.rename(src_ren, ren_path)) if err != 0: return fail(f"rename-exdev={mode} returned errno={err}") err_msg = require_symlink(ren_path, f"rename-exdev={mode}") if err_msg: return fail(err_msg) target = symlink_resolves(ren_path) if ".mergerfs_rename_exdev" not in target: return fail(f"rename-exdev={mode}: target not moved under .mergerfs_rename_exdev: {target!r}") with open(ren_path, "rb") as fp: if fp.read() != b"src": return fail(f"rename-exdev={mode}: symlink target content mismatch") except (PermissionError, FileNotFoundError, OSError): return SKIP finally: try: mergerfs_set_option(mount, "category.create", orig_create) mergerfs_set_option(mount, "link-exdev", orig_link) mergerfs_set_option(mount, "rename-exdev", orig_rename) except OSError: pass return 0 finally: for branch in branches: cleanup_dir(join(branch, rel_base)) except RuntimeError as exc: print(str(exc), end="") return 77 if __name__ == "__main__": raise SystemExit(main())