81 lines
3.2 KiB
Python
81 lines
3.2 KiB
Python
"""End-to-end coverage for the `--temp-dir` EXDEV (cross-filesystem) fallback.
|
|
|
|
`file_to_disk_secure_impl` installs a completed temp file with `renameat(2)`;
|
|
when the scratch dir lives on a different filesystem the rename fails with
|
|
`EXDEV` and the engine retries with no scratch dir, writing the file directly in
|
|
the destination directory (a non-atomic copy), matching rsync.
|
|
|
|
The daemon receiver confines `--temp-dir` to the authorized receive root, so a
|
|
genuine cross-fs scratch there would require an in-root mount point. Bind/tmpfs
|
|
mounting is not permitted in the CI container (no `CAP_SYS_ADMIN`, and
|
|
unprivileged user namespaces are disabled), so this test reaches the exact same
|
|
code path through the local `--read-batch` apply instead: it has no
|
|
authorized-root confinement, so a relative `--temp-dir` that is a symlink to a
|
|
tmpfs (`/dev/shm`) is accepted and the final install then crosses filesystems.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from common import CLIENT_CMD, get_dest_received_dir
|
|
|
|
TMPFS = "/dev/shm"
|
|
|
|
|
|
def _run(args):
|
|
return subprocess.run(CLIENT_CMD + args, capture_output=True, text=True, timeout=180)
|
|
|
|
|
|
def _read(path):
|
|
with open(path, "rb") as fh:
|
|
return fh.read()
|
|
|
|
|
|
def test_read_batch_temp_dir_cross_filesystem_fallback(tmp_path):
|
|
if not os.path.isdir(TMPFS):
|
|
pytest.skip("no /dev/shm tmpfs available to force a cross-filesystem install")
|
|
|
|
source = tmp_path / "src"
|
|
dest = tmp_path / "dst"
|
|
source.mkdir()
|
|
dest.mkdir()
|
|
files = {
|
|
"payload.bin": bytes(range(256)) * 64,
|
|
"sub/nested.txt": b"nested exdev fallback\n" * 8,
|
|
}
|
|
for rel, data in files.items():
|
|
full = source / rel
|
|
full.parent.mkdir(parents=True, exist_ok=True)
|
|
full.write_bytes(data)
|
|
|
|
batch = tmp_path / "tree.batch"
|
|
r = _run(["--only-write-batch", str(batch), str(source)])
|
|
assert r.returncode == 0, (r.stdout, r.stderr)
|
|
|
|
# A cross-filesystem scratch dir, reached through a relative --temp-dir
|
|
# symlink (the local batch apply performs no authorized-root confinement).
|
|
scratch = os.path.join(TMPFS, "fastsync_exdev_%d" % os.getpid())
|
|
shutil.rmtree(scratch, ignore_errors=True)
|
|
os.makedirs(scratch)
|
|
os.symlink(scratch, dest / "scratch")
|
|
try:
|
|
assert os.stat(scratch).st_dev != os.stat(dest).st_dev, (
|
|
"scratch and destination share a filesystem; EXDEV cannot be exercised"
|
|
)
|
|
r = _run(["--read-batch", str(batch), str(dest), "--temp-dir=scratch"])
|
|
assert r.returncode == 0, (r.stdout, r.stderr)
|
|
# The engine must report the non-atomic cross-fs fallback rather than
|
|
# silently claiming an atomic install.
|
|
assert "different filesystem" in (r.stdout + r.stderr), (r.stdout, r.stderr)
|
|
# The tree is still byte-exact and the scratch dir is left clean.
|
|
received = get_dest_received_dir(str(dest), str(source))
|
|
for rel, data in files.items():
|
|
assert _read(os.path.join(received, rel)) == data, f"content mismatch for {rel}"
|
|
assert os.listdir(scratch) == [], "cross-fs temp file was not cleaned up"
|
|
finally:
|
|
shutil.rmtree(scratch, ignore_errors=True)
|