fix(output): emit directory/root lines for -i and --out-format (#292)

This commit is contained in:
2026-09-22 16:20:03 +02:00
parent 8cc3dd993b
commit 1f5f8dc5a7
6 changed files with 306 additions and 34 deletions
+6 -1
View File
@@ -2786,7 +2786,12 @@ class TestItemizeChanges:
flags=["--preserve", "-i", "--incremental"],
port=shared_server.port)
assert result.returncode == 0, f"incremental itemize failed: {result.stderr[:200]}"
itemized = [line for line in result.stdout.splitlines() if line and line[0] in ">.<c"]
# -i also emits the transfer-root and directory lines; only FILE entries
# matter here, so drop any line whose name has a trailing '/'.
itemized = [
line for line in result.stdout.splitlines()
if line and line[0] in ">.<c" and not line.rsplit(" ", 1)[-1].endswith("/")
]
assert itemized == [], f"unchanged files were itemized: {itemized[:5]}"
def test_multithreaded_emits_same_itemize_lines(self, shared_server):
+63 -5
View File
@@ -26,6 +26,17 @@ def _rsync(args):
)
def _file_entry_line(text):
"""The file entry line for a single-file transfer.
-i/--out-format emit the transfer-root (and directory) lines too, so the
file entry is not necessarily the first line; for the one-file corpora used
by the wire-counter tests it is the last non-empty line.
"""
lines = [line for line in text.splitlines() if line.strip()]
return lines[-1] if lines else ""
def _make_selection_tree(root):
clean_dir(root)
os.makedirs(os.path.join(root, "sub"))
@@ -184,6 +195,51 @@ class TestItemizeParity:
)
assert fast_lines == rsync_lines, f"rsync={rsync_lines} fastsync={fast_lines}"
@requires_rsync
@pytest.mark.ci
def test_itemize_directory_lines_match_rsync(self, shared_server):
"""#292: -i/--out-format emit rsync's directory lines (including the
transfer root) in rsync's depth-first order."""
source = os.path.join(TEST_DATA_DIR, "out_itemdir_src")
dest = os.path.join(TEST_DATA_DIR, "out_itemdir_dst")
rdst = os.path.join(TEST_DATA_DIR, "out_itemdir_rdst")
clean_dir(source)
os.makedirs(os.path.join(source, "sub", "deep"))
os.makedirs(os.path.join(source, "emptydir"))
with open(os.path.join(source, "a.txt"), "wb") as fh:
fh.write(b"hello\n")
with open(os.path.join(source, "sub", "b.txt"), "wb") as fh:
fh.write(b"world\n")
with open(os.path.join(source, "sub", "deep", "d.txt"), "wb") as fh:
fh.write(b"deep\n")
clean_dir(dest)
clean_dir(rdst)
def dir_lines(text):
# Any line whose name ends with '/' is a directory entry.
return sorted(
line for line in text.splitlines()
if line.rsplit(" ", 1)[-1].endswith("/")
)
for fmt in (None, "%i %n%L"):
rsync_flags = ["-a", "-i"] if fmt is None else ["-a", "--out-format=" + fmt]
fast_flags = rsync_flags
clean_dir(rdst)
clean_dir(dest)
rsync_result = _rsync(rsync_flags + [source + "/", rdst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
result, _ = run_client(source, dest, flags=fast_flags,
port=shared_server.port)
assert result.returncode == 0, result.stderr[:300]
expected = [l for l in dir_lines(rsync_result.stdout)
if not l.rsplit(" ", 1)[-1] == "./"]
fast = dir_lines(result.stdout)
assert [l for l in fast if not l.rsplit(" ", 1)[-1] == "./"] == expected, (
f"fmt={fmt} rsync={rsync_result.stdout!r} fastsync={result.stdout!r}"
)
assert ".d..t...... ./" in fast, f"missing root line: {result.stdout!r}"
@requires_rsync
@pytest.mark.ci
def test_itemize_modified_file_matches_rsync(self, shared_server):
@@ -387,8 +443,8 @@ class TestWireStatsParity:
result, _ = run_client(source, dest, flags=["-a", "--out-format=" + fmt],
port=shared_server.port)
assert result.returncode == 0, result.stderr[:300]
rb, rl = (int(x) for x in rsync_result.stdout.split()[:2])
fb, fl = (int(x) for x in result.stdout.split()[:2])
rb, rl = (int(x) for x in _file_entry_line(rsync_result.stdout).split()[:2])
fb, fl = (int(x) for x in _file_entry_line(result.stdout).split()[:2])
assert rl == fl == 5000, (rsync_result.stdout, result.stdout)
assert rb > rl, f"rsync %b must include framing: {rsync_result.stdout!r}"
assert fb > fl, f"fastsync %b must include framing: {result.stdout!r}"
@@ -421,7 +477,9 @@ class TestWireStatsParity:
assert file_lines(result.stdout) == file_lines(rsync_result.stdout), (
f"rsync={rsync_result.stdout!r} fastsync={result.stdout!r}"
)
assert result.stdout.split()[0] == rsync_result.stdout.split()[0] == "16", (
fs_c = _file_entry_line(result.stdout).split()[0]
rs_c = _file_entry_line(rsync_result.stdout).split()[0]
assert fs_c == rs_c == "16", (
f"%c must be rsync's 16-byte sum header: {result.stdout!r}"
)
@@ -450,8 +508,8 @@ class TestWireStatsParity:
"--out-format=" + fmt],
port=shared_server.port)
assert result.returncode == 0, result.stderr[:300]
rs_c = int(rsync_result.stdout.split()[0])
fs_c = int(result.stdout.split()[0])
rs_c = int(_file_entry_line(rsync_result.stdout).split()[0])
fs_c = int(_file_entry_line(result.stdout).split()[0])
# No basis exists, so rsync still reports only its sum header.
assert rs_c == 16, rsync_result.stdout
# FastSync reports its own handshake bytes and is not aligned.