Merge feat/p7-devices: Phase 7 Wave C (device/special statuses; --copy-devices --sendfile non-regular fallback)
This commit is contained in:
@@ -3,6 +3,7 @@ import filecmp
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -14,7 +15,8 @@ from common import (
|
||||
PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR,
|
||||
run_client, CountingProxy,
|
||||
generate_test_files, verify_transfer, clean_dir, make_result,
|
||||
get_dest_received_dir, CLIENT_CMD, ServerManager,
|
||||
get_dest_received_dir, CLIENT_CMD, SERVER_CMD, ServerManager,
|
||||
_find_free_port, _wait_for_port, _wait_proc,
|
||||
)
|
||||
|
||||
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "feature_source")
|
||||
@@ -23,6 +25,31 @@ DEVICE_SOURCE = os.path.join(TEST_DATA_DIR, "device_source")
|
||||
DEVICE_DEST = os.path.join(TEST_DATA_DIR, "device_dest")
|
||||
|
||||
|
||||
def _start_captured_server(prefix=None, extra_args=None):
|
||||
"""Start a plain-TCP server with captured stdout/stderr for one test.
|
||||
|
||||
Returns (proc, port). The caller owns `proc` and must terminate it via
|
||||
`_wait_proc` so a server that ignores SIGTERM is killed instead of leaving
|
||||
a zombie or raising TimeoutExpired. The shared session server discards its
|
||||
output, so tests that lock in a receiver-side warning need their own. The
|
||||
server's SIGTERM handler exits via `_exit`, which does not flush stdio, so
|
||||
`stdbuf -oL` keeps stdout line-buffered and the warning observable."""
|
||||
port = _find_free_port()
|
||||
cmd = ["stdbuf", "-oL"] + (prefix or []) + SERVER_CMD + ["-p", str(port), "--allow-unauthenticated"]
|
||||
if extra_args:
|
||||
cmd += extra_args
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
_wait_for_port(port)
|
||||
return proc, port
|
||||
|
||||
|
||||
def _stop_captured_server(proc):
|
||||
"""Terminate a captured server and return its (stdout, stderr) text."""
|
||||
proc.terminate()
|
||||
_wait_proc(proc)
|
||||
return proc.communicate()
|
||||
|
||||
|
||||
class TestDeviceSpecial:
|
||||
"""Phase 4: --devices / --specials / -D / --copy-devices / --write-devices.
|
||||
|
||||
@@ -64,14 +91,56 @@ class TestDeviceSpecial:
|
||||
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||
assert stat.S_ISFIFO(os.stat(os.path.join(received, "pipe.fifo")).st_mode)
|
||||
|
||||
def test_copy_devices_non_crash(self, shared_server):
|
||||
"""--copy-devices treats a special/device source as a regular-file copy;
|
||||
a FIFO (st_size 0) must transfer without hanging or crashing."""
|
||||
@pytest.mark.ci
|
||||
def test_specials_socket_source_skipped_safely(self):
|
||||
"""A socket cannot be recreated by any standard filesystem call, so
|
||||
--specials must skip it with a note and still complete the run (the
|
||||
adjacent regular file transfers normally; no socket node appears)."""
|
||||
self._setup()
|
||||
sock_path = os.path.join(DEVICE_SOURCE, "source.sock")
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server, port = _start_captured_server()
|
||||
try:
|
||||
s.bind(sock_path)
|
||||
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||
flags=["--specials"], port=port)
|
||||
finally:
|
||||
s.close()
|
||||
out, err = _stop_captured_server(server)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||
with open(os.path.join(received, "plain.txt")) as f:
|
||||
assert f.read() == "regular content\n"
|
||||
assert not os.path.lexists(os.path.join(received, "source.sock")), (
|
||||
"socket source must be skipped, not materialized"
|
||||
)
|
||||
assert "socket not recreated" in (out + err), (
|
||||
f"receiver did not log the documented socket skip: out={out!r} err={err!r}"
|
||||
)
|
||||
|
||||
@pytest.mark.ci
|
||||
@pytest.mark.parametrize("flags", [["--copy-devices"], ["--copy-devices", "--sendfile"]])
|
||||
def test_copy_devices_fifo_becomes_regular_file(self, shared_server, flags):
|
||||
"""--copy-devices treats a special source as an ordinary regular-file
|
||||
copy: a FIFO (st_size 0) becomes a zero-length REGULAR file on the
|
||||
destination (never a FIFO, never a hang), and the run succeeds. The
|
||||
--sendfile variant previously blocked forever in the sendfile open();
|
||||
the non-regular source now falls back to the buffered read path, so it
|
||||
must complete within the bounded-time assertion below."""
|
||||
self._setup()
|
||||
os.mkfifo(os.path.join(DEVICE_SOURCE, "device_copy.fifo"))
|
||||
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||
flags=["--copy-devices"], port=shared_server.port)
|
||||
result, dur = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||
flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||
copied = os.path.join(received, "device_copy.fifo")
|
||||
assert os.path.lexists(copied), "copy-devices source was not transferred"
|
||||
st = os.lstat(copied)
|
||||
assert stat.S_ISREG(st.st_mode), (
|
||||
f"copy-devices must produce a regular file, got mode {oct(st.st_mode)}"
|
||||
)
|
||||
assert st.st_size == 0, f"expected a size-bounded 0-byte copy, got {st.st_size}"
|
||||
assert dur < 60, f"{' '.join(flags)} hung on a FIFO source"
|
||||
|
||||
def test_write_devices_non_crash(self, shared_server):
|
||||
"""--write-devices writes into an existing device only; when the
|
||||
@@ -85,6 +154,56 @@ class TestDeviceSpecial:
|
||||
flags=["--write-devices"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_write_devices_regular_file_target_skipped(self, shared_server):
|
||||
"""--write-devices only ever writes into an existing char/block node: a
|
||||
pre-existing REGULAR file at the destination path is left byte-identical
|
||||
(not clobbered) and the run still succeeds."""
|
||||
self._setup()
|
||||
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||
os.makedirs(received, exist_ok=True)
|
||||
target = os.path.join(received, "plain.txt")
|
||||
with open(target, "wb") as f:
|
||||
f.write(b"pre-existing local content\n")
|
||||
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||
flags=["--write-devices"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:200]}"
|
||||
with open(target, "rb") as f:
|
||||
assert f.read() == b"pre-existing local content\n", (
|
||||
"write-devices clobbered a non-device destination"
|
||||
)
|
||||
|
||||
@pytest.mark.setpriv
|
||||
def test_devices_nonroot_receiver_skips_safely(self):
|
||||
"""A receiver without CAP_MKNOD must skip a device entry with a warning
|
||||
and never abort. A root runner drops the receiver (server) to nobody
|
||||
via setpriv; on a non-root runner (or without setpriv) the test skips."""
|
||||
if os.geteuid() != 0 or shutil.which("setpriv") is None:
|
||||
pytest.skip("requires root + setpriv to run the receiver unprivileged")
|
||||
self._setup()
|
||||
os.mknod(os.path.join(DEVICE_SOURCE, "chardev"), stat.S_IFCHR | 0o666,
|
||||
os.makedev(1, 3))
|
||||
# The unprivileged receiver must be able to create the destination tree.
|
||||
os.makedirs(DEVICE_DEST, exist_ok=True)
|
||||
os.chmod(DEVICE_DEST, 0o777)
|
||||
server, port = _start_captured_server(
|
||||
prefix=["setpriv", "--reuid=65534", "--regid=65534", "--clear-groups"])
|
||||
try:
|
||||
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||
flags=["--devices"], port=port)
|
||||
finally:
|
||||
out, err = _stop_captured_server(server)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:300]}"
|
||||
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||
with open(os.path.join(received, "plain.txt")) as f:
|
||||
assert f.read() == "regular content\n"
|
||||
assert not os.path.lexists(os.path.join(received, "chardev")), (
|
||||
"a receiver without CAP_MKNOD must skip the device node, not create it"
|
||||
)
|
||||
assert "cannot create device node" in (out + err), (
|
||||
f"receiver did not log the documented CAP_MKNOD skip: out={out!r} err={err!r}"
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="requires root to create device nodes")
|
||||
def test_devices_recreates_real_char_device(self, shared_server):
|
||||
"""Root-only: a source char device node is recreated on the destination
|
||||
|
||||
Reference in New Issue
Block a user