Files
mergerfs/tests/TEST_cfg_link_rename_exdev
T
Antonio SJ Musumeci 84e4cdeb7e make all integration tests self-contained with ephemeral mounts
- Add mergerfs_mount() context manager in posix_parity.py that creates
  temporary branch dirs in tests/.test_tmp/, mounts an ephemeral mergerfs
  instance, and cleans up after use.
- Add find_mergerfs(), find_fusermount(), mount_mergerfs(), unmount_mergerfs()
  helpers.
- Update run-tests to invoke each TEST_* script without a mountpoint arg.
- Update README.md to document the new `python3 tests/run-tests` invocation.
- Fix 44 tests to use mergerfs_mount() instead of expecting a pre-mounted path.
- Add tempfile import to 10 tests that were missing it.
- Fix TEST_mount_lifecycle to use mergerfs_mount() context manager.
- Fix TEST_posix_tmpfile missing join import.
- Fix TEST_cfg_xattr_modes error message framing (no "(known bug)").
- Fix TEST_posix_copy_file_range: loop-copy full size, compare st_size and
  SHA256 hash; add NativeTmp for space management; route native temp to
  tests/.test_tmp/ to avoid ENOSPC on /tmp.
- Fix TEST_posix_getattr_fgetattr: st_cmp skips st_size for dirs; add
  compare_ino param for safe inode comparison when inodecalc=passthrough.
- Fix TEST_unlink_rename, TEST_use_* tests: namespace cleanup, consistent
  variable naming, None check for cmpfunc.
- Fix TEST_readlink_semantics: remove type annotations to support older
  Python; wrap in mergerfs_mount() try/except.
- Add flush=True to run-tests print so test names show immediately.
2026-05-07 21:47:40 -04:00

150 lines
6.3 KiB
Python
Executable File

#!/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())