mergerfs/tests
Antonio S. Musumeci 947024914a Replace std::filesystem::path with custom fs::relpath
std::filesystem::path is wrong for mergerfs's hot path:
- `branch->path / fusepath_` heap-allocates per FUSE op (3 allocs each
  on libstdc++; the SSO buffer is 15 bytes and any joined path exceeds it).
- The implicit `operator string_type()` adds another std::string when
  passing to `fs::*` wrappers that take `const std::string&`.
- Per-branch loops rebuild fullpath from scratch every iteration.

The new fs::relpath is a fixed 8192-byte (2 * PATH_MAX) inline buffer,
NUL-terminated, never heap-allocates. The rel data is anchored at offset
REL_OFFSET = 4096; the 4096 bytes before the anchor form the "prefix
area" so a long base/prefix can be written there in place via
set_prefix() without disturbing the rel. The remaining ~4096 bytes
after the rel are available for trailing append (filenames in
readdir-style loops, etc.).

set_prefix's cost is O(prefix size) -- only the actually-used bytes
are touched -- so the buffer size is a stack-footprint question, not a
hot-path cost. FUSE worker threads use the pthread default stack
(~8 MB on Linux); a 4-relpath frame in fuse_link/fuse_rename is 32 KB,
~0.4% of the stack budget.

The per-branch hot loop:

  fs::relpath fullpath = fusepath_;       // one copy of the rel
  for (auto branch : branches) {
    fullpath.set_prefix(branch->path);   // writes only [start, 4096)
    fs::syscall(fullpath);
  }

set_prefix only rewrites the prefix area; the rel stays anchored. A
cached _rel_size means no recomputation across iterations.

Microbenchmark (8 branches, 30-char fusepath, -O3, 4 M path builds):

  variant                                 ns/build   allocs/build
  --------------------------------------  --------   ------------
  origin/master (std::filesystem::path)   158        3.00
  fs::relpath assign_concat (single-shot) 9.1        0
  fs::relpath set_prefix (loop-optimized) 4.5        0

35x faster, zero allocations in the hot path.

Contract: paths flow through fs::relpath in canonical form -- prefix
without trailing '/', rel without leading '/'. No sanitization;
violations yield double slashes. Mergerfs satisfies this by
construction:
- libfuse's try_get_path() walks leaf-to-root via prepend(), then
  calls consolidate_to_rel(strip=true) which drops the leading '/'
  that prepend produces.
- branch paths are realpath-canonicalized at config load so they
  never have a trailing slash.

Mergerfs has exactly one path-manipulation type: a relative fusepath.
Branch paths and other absolute paths are stored as std::string -- they
are spliced into the prefix area of an fs::relpath at syscall time but
are never themselves mutated as path objects.

Major changes:
- vendored/libfuse/{include/fs_path.hpp,lib/fs_path.cpp}: new
  fs::relpath class (8 KB inline, REL anchor at 4096, set_prefix /
  clear_prefix / consolidate_to_rel, _rel_size cache, self-aliasing-
  safe assign_concat, sticky ENAMETOOLONG via errnum()).
- 30+ FUSE-op call sites converted from `fullpath = branch / fusepath`
  to the `set_prefix` loop pattern (fuse_unlink, fuse_chmod,
  fuse_chown, fuse_listxattr, fuse_truncate, fuse_utimens,
  fuse_setxattr, fuse_rmdir, fuse_removexattr, fuse_link's
  preserve-path and create-path loops, fuse_rename's create-path
  and preserve-path loops, fuse_statfs FULL mode, fs::findallfiles,
  fs::findonfs).
- fs::* syscall wrappers that previously only took
  `const std::string&` got `const char*` overloads so fs::relpath's
  implicit `operator const char*()` lands cleanly (fs::link,
  fs::lutimens, fs::umount_lazy, fs::open_dir_ro, fs::readlink,
  fs::lremovexattr, fs::lstatvfs, fs::info, fs::statvfs_cache).
- fs::exists gained a (fs::relpath&, basepath, ...) overload that
  calls set_prefix internally so per-branch policy fan-out loops
  share a single fullpath, rewriting only the prefix area on each
  iteration.
- All policies (epall, epff, eplfs, eplus, epmfs, eppfrd, ff, lup,
  msplfs, msplus, mspmfs, msppfrd, newest) refactored to hoist the
  fs::relpath fullpath construction out of the per-branch loop and
  use the set_prefix-based fs::exists overload.
- fs::inode::ReaddirCalc::_fusepath migrated from std::string to
  fs::relpath with the same resize/append API.
- std::filesystem removed entirely from the project: branches.cpp
  and option_parser.cpp's exists/is_directory/equivalent calls now
  use direct ::stat(); mergerfs_fsck.cpp's recursive_directory_iterator
  was replaced with an opendir-based walk.

Tests (tests/tests.cpp): 27 new fs::relpath test cases covering
construction, set_prefix in/out of REL anchor, prefix()/rel()
accessors, clear_prefix, concat canonical form, self-aliasing
(`p /= "x"`), operator/=, resize zero-fill, swap, copy/move,
equality, implicit conversions, lexically_relative, remove_filename,
_rel_size invariant across every mutator, capacity bounds.

Full suite: 213/213 pass.
2026-05-23 21:15:09 -04:00
..
OPTIONS_TEST_PLAN.md Remove timed node removal 2026-05-06 20:49:46 -04:00
posix_parity.py make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
README.md make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
run-tests make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_api_xattr make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_cfg_link_rename_exdev make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_cfg_statfs_ignore make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_cfg_xattr_modes make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_io_passthrough_create_open make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_io_passthrough_open_race make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_mount_lifecycle make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_movefile_enospc make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_no_fuse_hidden make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_o_direct make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_open_after_unlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_policy_lup make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_access make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_bmap make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_chmod make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_chown make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_copy_file_range make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_create_mknod make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_fsync_flush make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_getattr_fgetattr make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_ioctl make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_link_symlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_locking make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_mkdir_rmdir make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_open_read_write make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_poll make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_readdir make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_readdir_plus make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_release make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_releasedir make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_statfs make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_statx make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_syncfs make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_tmpfile make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_truncate_ftruncate make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_unlink_rename make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_utimens make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_xattr make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_posix_xattr_matrix make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_readlink_semantics make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_rmdir_enotempty_priority make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_unlink_rename make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_use_fallocate_after_unlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_use_fchmod_after_unlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_use_fchown_after_unlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_use_fstat_after_unlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_use_ftruncate_after_unlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
TEST_use_futimens_after_unlink make all integration tests self-contained with ephemeral mounts 2026-05-07 21:47:40 -04:00
tests.cpp Replace std::filesystem::path with custom fs::relpath 2026-05-23 21:15:09 -04:00

POSIX Soft Conformance Suite

This directory contains integration tests that exercise mergerfs FUSE operations and compare behavior against equivalent native syscalls on a local filesystem.

The suite is "soft POSIX": it focuses on practical parity for return values, errno, and key side effects, including common error paths.

Each test mounts its own ephemeral mergerfs instance with random temporary branches and cleans everything up automatically.

Run with:

python3 tests/run-tests

Coverage Matrix

  • access -> TEST_posix_access
  • chmod / fchmod -> TEST_posix_chmod, TEST_use_fchmod_after_unlink
  • chown / fchown -> TEST_posix_chown, TEST_use_fchown_after_unlink
  • create / mknod -> TEST_posix_create_mknod
  • getattr / fgetattr -> TEST_posix_getattr_fgetattr, TEST_use_fstat_after_unlink
  • open / read / write -> TEST_posix_open_read_write, TEST_o_direct
  • flush / release / fsync / fsyncdir -> TEST_posix_fsync_flush
  • release close semantics -> TEST_posix_release
  • releasedir close semantics -> TEST_posix_releasedir
  • truncate / ftruncate -> TEST_posix_truncate_ftruncate, TEST_use_ftruncate_after_unlink
  • utimens / futimens -> TEST_posix_utimens, TEST_use_futimens_after_unlink
  • fallocate -> TEST_use_fallocate_after_unlink
  • copy_file_range -> TEST_posix_copy_file_range
  • ioctl -> TEST_posix_ioctl
  • poll -> TEST_posix_poll
  • tmpfile (O_TMPFILE) -> TEST_posix_tmpfile
  • bmap (via FIBMAP ioctl parity) -> TEST_posix_bmap
  • readlink -> TEST_readlink_semantics
  • link / symlink -> TEST_posix_link_symlink
  • mkdir / rmdir -> TEST_posix_mkdir_rmdir
  • unlink / rename -> TEST_posix_unlink_rename, TEST_unlink_rename
  • statfs -> TEST_posix_statfs
  • statx -> TEST_posix_statx
  • syncfs -> TEST_posix_syncfs
  • flock / lock -> TEST_posix_locking
  • setxattr / getxattr / listxattr / removexattr -> TEST_posix_xattr
  • xattr object/flag matrix (file, dir, symlink l*, create/replace) -> TEST_posix_xattr_matrix
  • xattr mode runtime toggles (passthrough/noattr/nosys) -> TEST_cfg_xattr_modes
  • statfs mode and statfs-ignore runtime toggles -> TEST_cfg_statfs_ignore
  • EXDEV fallback runtime toggles for link/rename -> TEST_cfg_link_rename_exdev
  • readdir (+ seek/tell behavior) -> TEST_posix_readdir
  • readdir_plus semantic parity (list+stat and readdir policy toggles) -> TEST_posix_readdir_plus
  • lifecycle (init/destroy) harness check -> TEST_mount_lifecycle
  • hidden temp exposure checks -> TEST_no_fuse_hidden

Error Conditions Emphasized

Across tests, parity checks include combinations of:

  • ENOENT
  • ENOTDIR
  • EEXIST
  • ENOTEMPTY
  • EISDIR
  • EBADF
  • privilege-sensitive outcomes (for example EPERM) where applicable

Notes

  • Some behavior is runtime/environment dependent (kernel, mount options, uid/gid, capabilities). Tests compare mergerfs to native behavior in the same runtime to keep assertions robust.
  • Tests that cannot exercise a behavior in the current environment may exit 77 and are reported as SKIP by tests/run-tests.
  • The xattr test uses a user.* key and assumes user xattrs are enabled.
  • Tests mount their own ephemeral mergerfs instances using the mergerfs_mount context manager in posix_parity.py. Random directories are created and cleaned up automatically.