feat(protocol): client-message channel and rsync partial exit 23 (2.30.0)
This commit is contained in:
@@ -203,9 +203,9 @@ class TestDeviceSpecial:
|
||||
flags=["--devices"], port=port)
|
||||
finally:
|
||||
out, err = _stop_captured_server(server)
|
||||
assert result.returncode != 0, (
|
||||
f"a failed device mknod must be a transfer error like rsync (got exit 0): "
|
||||
f"{(out + err)[:300]}"
|
||||
assert result.returncode == 23, (
|
||||
f"a failed device mknod must exit 23 (rsync partial transfer), got "
|
||||
f"{result.returncode}: {(out + err)[:300]}"
|
||||
)
|
||||
received = get_dest_received_dir(DEVICE_DEST, DEVICE_SOURCE)
|
||||
assert not os.path.lexists(os.path.join(received, "chardev")), (
|
||||
@@ -215,6 +215,38 @@ class TestDeviceSpecial:
|
||||
f"receiver did not log the device creation error: out={out!r} err={err!r}"
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="requires root to create device nodes")
|
||||
def test_devices_nonroot_partial_removes_transferred_sources(self):
|
||||
"""rsync parity for a partial receiver run under --remove-source-files:
|
||||
the successfully transferred regular source is still removed, the
|
||||
un-creatable device source is kept, and the client exits 23 (verified
|
||||
against rsync 3.4.1: it removes ok.txt/ok2.txt, keeps the device, and
|
||||
exits 23)."""
|
||||
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))
|
||||
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", "--remove-source-files"], port=port)
|
||||
finally:
|
||||
out, err = _stop_captured_server(server)
|
||||
assert result.returncode == 23, (
|
||||
f"a partial receiver run must exit 23, got {result.returncode}: "
|
||||
f"{(out + err)[:300]}"
|
||||
)
|
||||
assert not os.path.exists(os.path.join(DEVICE_SOURCE, "plain.txt")), (
|
||||
"a successfully transferred source must be removed even on a partial run"
|
||||
)
|
||||
assert os.path.exists(os.path.join(DEVICE_SOURCE, "chardev")), (
|
||||
"the source device that failed to materialize must be kept"
|
||||
)
|
||||
|
||||
@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
|
||||
@@ -334,6 +366,57 @@ def setup_test_data():
|
||||
shutil.rmtree(DEST_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
class TestClientStderrChannel:
|
||||
"""--stderr=client: the client's own diagnostics go to the peer's stderr."""
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_client_diagnostic_reaches_server_stderr(self):
|
||||
"""A client-side warning emitted during the transfer is forwarded over
|
||||
the STATUS_CLIENT_MSG channel and printed on the server's stderr, not the
|
||||
client's. A dangling symlink under -L is the deterministic trigger."""
|
||||
source = os.path.join(TEST_DATA_DIR, "client_msg_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "client_msg_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
with open(os.path.join(source, "plain.txt"), "wb") as f:
|
||||
f.write(b"payload\n")
|
||||
os.symlink("no-such-referent", os.path.join(source, "dangling"))
|
||||
server, port = _start_captured_server()
|
||||
try:
|
||||
result, _ = run_client(source, dest, flags=["-L", "--stderr=client"],
|
||||
port=port)
|
||||
finally:
|
||||
out, err = _stop_captured_server(server)
|
||||
assert "symlink has no referent" in (out + err), (
|
||||
f"client diagnostic did not reach the server stderr: out={out!r} err={err!r}"
|
||||
)
|
||||
assert "symlink has no referent" not in (result.stderr or ""), (
|
||||
f"client diagnostic must not also be written locally: {result.stderr!r}"
|
||||
)
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_no_msgs2stderr_alias_uses_client_channel(self):
|
||||
"""--no-msgs2stderr is rsync's spelling of --stderr=client and now
|
||||
forwards the client's diagnostics to the server too."""
|
||||
source = os.path.join(TEST_DATA_DIR, "client_msg_alias_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "client_msg_alias_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
with open(os.path.join(source, "plain.txt"), "wb") as f:
|
||||
f.write(b"payload\n")
|
||||
os.symlink("no-such-referent", os.path.join(source, "dangling"))
|
||||
server, port = _start_captured_server()
|
||||
try:
|
||||
result, _ = run_client(source, dest, flags=["-L", "--no-msgs2stderr"],
|
||||
port=port)
|
||||
finally:
|
||||
out, err = _stop_captured_server(server)
|
||||
assert "symlink has no referent" in (out + err), (
|
||||
f"--no-msgs2stderr did not route to the server: out={out!r} err={err!r}"
|
||||
)
|
||||
assert "symlink has no referent" not in (result.stderr or "")
|
||||
|
||||
|
||||
class TestDryRun:
|
||||
def test_trust_sender_transfer_completes(self, shared_server):
|
||||
"""--trust-sender is a receiver-local policy (never sent to the peer).
|
||||
|
||||
Reference in New Issue
Block a user