test(dry-run): strengthen no-mutation coverage and refresh docs

Extend _snapshot_tree to record mode, inode, xattrs, directories and
special nodes, and add coverage proving a server-contacting --dry-run
leaves the destination structurally identical for --delay-updates,
--backup, symlinks, hardlinks, FIFOs, and daemon modules (including a
read-only module).  Add a regression test for the --read-batch --dry-run
refusal and for a missing/non-directory receive root failing a dry-run
exactly like a real run.

Fix stale version comments (2.20.0/633 -> 2.21.0/637) and RSYNC_COMPAT's
current --protocol value, and add a unit assertion that
--server-port/--port (and --server-host) set the dry-run routing bit.
This commit is contained in:
2026-09-13 12:57:37 +02:00
parent 07f7555c1d
commit 6269ae54e5
5 changed files with 258 additions and 18 deletions
+30
View File
@@ -43,6 +43,9 @@ from common import (
_find_free_port,
_wait_for_port,
)
# The dry-run no-mutation contract is asserted with the same structural snapshot
# (mode/inode/mtime/xattr/content) the feature suite uses.
from test_features import _snapshot_tree
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "daemon_source")
MODULE_ROOT = os.path.join(TEST_DATA_DIR, "daemon_modules")
@@ -355,6 +358,33 @@ class TestDaemonRejection:
assert result.returncode != 0
assert self._tree_files() == before, "read-only rejection wrote under the module root"
@pytest.mark.ci
def test_read_only_module_allows_dry_run(self, daemon):
"""A server-contacting --dry-run IS a read-only wire operation, so a
`read only = yes` module is the safest dry-run target and must accept it
while writing nothing."""
result, _ = run_client(SOURCE_DIR, "127.0.0.1::readonly", flags=["--dry-run"],
port=daemon.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
assert "Dry run:" in result.stdout, result.stdout[:200]
assert _tree_file_count(READONLY_MODULE) == 0, "read-only dry-run wrote a file"
@pytest.mark.ci
def test_module_dry_run_mutates_nothing(self, daemon):
"""A daemon-module dry-run reports would-transfer entries but leaves the
module tree structurally identical (mode/inode/mtime/xattr/content)."""
result = _push("127.0.0.1::files", daemon.port)
assert result.returncode == 0, result.stderr or result.stdout
before = _snapshot_tree(FILES_MODULE)
# --ignore-times forces every regular file to be reported as
# would-transfer, so the dry-run exercises the receiver decision rather
# than an all-skip shortcut -- while still mutating nothing.
result, _ = run_client(SOURCE_DIR, "127.0.0.1::files",
flags=["--dry-run", "--ignore-times"], port=daemon.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
assert "Dry run:" in result.stdout, result.stdout[:200]
assert _snapshot_tree(FILES_MODULE) == before, "daemon dry-run mutated the module root"
def test_unknown_module_rejected(self, daemon):
result = _push("127.0.0.1::no-such-module", daemon.port)
assert result.returncode != 0