rsync 3.4.1 drop-in parity (#285-#297) + parity completion (protocol 2.26.0) #298
@@ -233,6 +233,42 @@ class TestDeleteTimingFinalStateParity:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeleteTimingTypeConflictParity:
|
||||||
|
"""A destination entry whose type differs from the source is replaced, in
|
||||||
|
both per-directory timings and in both directions, exactly like rsync."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("timing", ["--delete-during", "--delete-delay"])
|
||||||
|
@requires_rsync
|
||||||
|
def test_type_conflicts_match_rsync(self, timing):
|
||||||
|
source = os.path.join(TEST_DATA_DIR, "dtc_src")
|
||||||
|
clean_dir(source)
|
||||||
|
_write(os.path.join(source, "foo"), b"now a file\n")
|
||||||
|
_write(os.path.join(source, "bar", "inner.txt"), b"now a dir\n")
|
||||||
|
|
||||||
|
def seed_dest(root):
|
||||||
|
clean_dir(root)
|
||||||
|
_write(os.path.join(root, "foo", "inner.txt"), b"was a dir\n")
|
||||||
|
_write(os.path.join(root, "bar"), b"was a file\n")
|
||||||
|
|
||||||
|
rsync_dst = os.path.join(TEST_DATA_DIR, "dtc_rsync_dst")
|
||||||
|
seed_dest(rsync_dst)
|
||||||
|
rsync_result = _rsync(["-a", timing, source + "/", rsync_dst + "/"])
|
||||||
|
assert rsync_result.returncode == 0, rsync_result.stderr
|
||||||
|
rsync_tree = _tree(rsync_dst)
|
||||||
|
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, "dtc_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
seed_dest(received)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, flags=[timing], port=server.port)
|
||||||
|
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
|
||||||
|
assert _tree(received) == rsync_tree, (
|
||||||
|
f"{timing}: fastsync tree {_tree(received)} != rsync tree {rsync_tree}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteTimingFailure:
|
class TestDeleteTimingFailure:
|
||||||
"""A mid-transfer failure distinguishes during from delay."""
|
"""A mid-transfer failure distinguishes during from delay."""
|
||||||
|
|
||||||
|
|||||||
@@ -3820,10 +3820,11 @@ class TestDeleteTiming:
|
|||||||
assert _read_file(os.path.join(received, "sub", "deep.txt")) == b"deeply nested file\n"
|
assert _read_file(os.path.join(received, "sub", "deep.txt")) == b"deeply nested file\n"
|
||||||
assert not os.path.exists(extra), "--delete-delay did not remove the extra"
|
assert not os.path.exists(extra), "--delete-delay did not remove the extra"
|
||||||
|
|
||||||
def test_early_flag_respected_when_server_refuses_delete(self, shared_server):
|
@pytest.mark.parametrize("flag", ["--delete-before", "--delete-during", "--delete-delay"])
|
||||||
"""With an --allow-delete-less server the client's early timing still
|
def test_early_flag_respected_when_server_refuses_delete(self, flag, shared_server):
|
||||||
completes (no deadlock on the pre-delete ack) and simply never deletes,
|
"""With an --allow-delete-less server the client's timing still completes
|
||||||
exactly like the plain server policy."""
|
(no deadlock on the pre-delete ack, no per-directory deletion) and simply
|
||||||
|
never deletes, exactly like the plain server policy."""
|
||||||
source = self._seed("refused")
|
source = self._seed("refused")
|
||||||
dest = os.path.join(TEST_DATA_DIR, "deltiming_refused_dst")
|
dest = os.path.join(TEST_DATA_DIR, "deltiming_refused_dst")
|
||||||
clean_dir(dest)
|
clean_dir(dest)
|
||||||
@@ -3833,9 +3834,9 @@ class TestDeleteTiming:
|
|||||||
extra = os.path.join(received, "extra.txt")
|
extra = os.path.join(received, "extra.txt")
|
||||||
with open(extra, "wb") as fh:
|
with open(extra, "wb") as fh:
|
||||||
fh.write(b"extra file")
|
fh.write(b"extra file")
|
||||||
result, _ = run_client(source, dest, flags=["--delete-before"], port=shared_server.port)
|
result, _ = run_client(source, dest, flags=[flag], port=shared_server.port)
|
||||||
assert result.returncode == 0, \
|
assert result.returncode == 0, \
|
||||||
f"--delete-before against a refuse-delete server failed: {result.stderr[:300]}"
|
f"{flag} against a refuse-delete server failed: {result.stderr[:300]}"
|
||||||
assert os.path.exists(extra), "unauthorized delete removed an extra file"
|
assert os.path.exists(extra), "unauthorized delete removed an extra file"
|
||||||
|
|
||||||
|
|
||||||
@@ -3930,6 +3931,31 @@ class TestDeleteScope:
|
|||||||
finally:
|
finally:
|
||||||
server.stop()
|
server.stop()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
@pytest.mark.parametrize("timing", ["--delete-during", "--delete-delay"])
|
||||||
|
@pytest.mark.ci
|
||||||
|
def test_files_from_per_dir_timing_confined_to_listed_dirs(self, mt, timing):
|
||||||
|
"""The per-directory timings honor the same --files-from scope: an extra
|
||||||
|
inside a listed directory is removed, while unlisted siblings and the
|
||||||
|
receive-root extra survive."""
|
||||||
|
source, dest, received, server = self._seed(f"pd_{timing.strip('-')}_{mt}")
|
||||||
|
try:
|
||||||
|
listed = _write_rel_list(b"listed.txt\nsub/\n")
|
||||||
|
flags = ["--files-from", listed, timing] + (["--threads"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, f"{timing} delete failed: {result.stderr[:300]}"
|
||||||
|
assert not os.path.exists(os.path.join(received, "sub", "extra.txt")), \
|
||||||
|
f"{timing} did not delete the in-scope extra"
|
||||||
|
assert os.path.isfile(os.path.join(received, "sub", "x.txt"))
|
||||||
|
assert os.path.exists(os.path.join(received, "unlisted.txt")), \
|
||||||
|
f"{timing} deleted an unlisted path (data loss)"
|
||||||
|
assert os.path.exists(os.path.join(received, "other", "c.txt")), \
|
||||||
|
f"{timing} deleted an unlisted sibling directory (data loss)"
|
||||||
|
assert os.path.exists(os.path.join(received, "rootextra.txt")), \
|
||||||
|
f"{timing} deleted the receive-root extra (data loss)"
|
||||||
|
finally:
|
||||||
|
server.stop()
|
||||||
|
|
||||||
|
|
||||||
class TestDeleteExtraneousSymlinks:
|
class TestDeleteExtraneousSymlinks:
|
||||||
"""#290 (3): --delete unlinks extraneous destination symlinks (never follows
|
"""#290 (3): --delete unlinks extraneous destination symlinks (never follows
|
||||||
@@ -4017,7 +4043,8 @@ class TestDeletePolicy:
|
|||||||
|
|
||||||
@pytest.mark.parametrize("mt", [False, True])
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
@pytest.mark.parametrize("timing",
|
@pytest.mark.parametrize("timing",
|
||||||
["--delete", "--delete-before", "--delete-after", "--delete-delay"])
|
["--delete", "--delete-before", "--delete-after", "--delete-delay",
|
||||||
|
"--delete-during"])
|
||||||
def test_delete_protects_excluded_by_default_and_delete_excluded_removes(self, mt, timing):
|
def test_delete_protects_excluded_by_default_and_delete_excluded_removes(self, mt, timing):
|
||||||
"""rsync parity: with a --delete timing the destination mirror path whose
|
"""rsync parity: with a --delete timing the destination mirror path whose
|
||||||
source was excluded survives (protected by default); --delete-excluded
|
source was excluded survives (protected by default); --delete-excluded
|
||||||
@@ -4101,7 +4128,7 @@ class TestDeletePolicy:
|
|||||||
"--delete-excluded did not remove the excluded dir subtree"
|
"--delete-excluded did not remove the excluded dir subtree"
|
||||||
|
|
||||||
@pytest.mark.parametrize("mt", [False, True])
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
@pytest.mark.parametrize("timing", ["--delete", "--delete-before"])
|
@pytest.mark.parametrize("timing", ["--delete", "--delete-before", "--delete-during"])
|
||||||
def test_max_delete_exceeded_deletes_up_to_cap_and_exits_25(self, mt, timing):
|
def test_max_delete_exceeded_deletes_up_to_cap_and_exits_25(self, mt, timing):
|
||||||
"""rsync parity: --max-delete=N deletes up to N extras, skips the rest and
|
"""rsync parity: --max-delete=N deletes up to N extras, skips the rest and
|
||||||
still succeeds as a transfer, exiting 25 with a diagnostic."""
|
still succeeds as a transfer, exiting 25 with a diagnostic."""
|
||||||
|
|||||||
Reference in New Issue
Block a user