test(delete): cover per-dir timings for files-from scope, max-delete, excluded protection, refuse-delete, type conflicts
This commit is contained in:
@@ -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:
|
||||
"""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 not os.path.exists(extra), "--delete-delay did not remove the extra"
|
||||
|
||||
def test_early_flag_respected_when_server_refuses_delete(self, shared_server):
|
||||
"""With an --allow-delete-less server the client's early timing still
|
||||
completes (no deadlock on the pre-delete ack) and simply never deletes,
|
||||
exactly like the plain server policy."""
|
||||
@pytest.mark.parametrize("flag", ["--delete-before", "--delete-during", "--delete-delay"])
|
||||
def test_early_flag_respected_when_server_refuses_delete(self, flag, shared_server):
|
||||
"""With an --allow-delete-less server the client's timing still completes
|
||||
(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")
|
||||
dest = os.path.join(TEST_DATA_DIR, "deltiming_refused_dst")
|
||||
clean_dir(dest)
|
||||
@@ -3833,9 +3834,9 @@ class TestDeleteTiming:
|
||||
extra = os.path.join(received, "extra.txt")
|
||||
with open(extra, "wb") as fh:
|
||||
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, \
|
||||
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"
|
||||
|
||||
|
||||
@@ -3930,6 +3931,31 @@ class TestDeleteScope:
|
||||
finally:
|
||||
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:
|
||||
"""#290 (3): --delete unlinks extraneous destination symlinks (never follows
|
||||
@@ -4017,7 +4043,8 @@ class TestDeletePolicy:
|
||||
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
@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):
|
||||
"""rsync parity: with a --delete timing the destination mirror path whose
|
||||
source was excluded survives (protected by default); --delete-excluded
|
||||
@@ -4101,7 +4128,7 @@ class TestDeletePolicy:
|
||||
"--delete-excluded did not remove the excluded dir subtree"
|
||||
|
||||
@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):
|
||||
"""rsync parity: --max-delete=N deletes up to N extras, skips the rest and
|
||||
still succeeds as a transfer, exiting 25 with a diagnostic."""
|
||||
|
||||
Reference in New Issue
Block a user