test: delete-policy unit and integration coverage
Unit: walker all-or-nothing bounds (exceeded -> nothing removed + distinct result; exact bound -> deletes), protected-prefix skipping, config wire round-trip for force_delete/delete_excluded/prune_empty_dirs/max_delete, CLI parse/validation for the new flags. Integration (TestDeletePolicy): default delete-excluded protection and --delete-excluded opt-out (single-thread, -m, early --delete-before, excluded-dir subtrees), --max-delete all-or-nothing over and at the limit, --force file-over-nonempty-dir replacement, --prune-empty-dirs (--dirs mode + recursion-mode parity), and --ignore-errors keeping deletion active across a genuine scan I/O error (run as an unprivileged user).
This commit is contained in:
@@ -2107,6 +2107,345 @@ class TestDeleteTiming:
|
|||||||
assert os.path.exists(extra), "unauthorized delete removed an extra file"
|
assert os.path.exists(extra), "unauthorized delete removed an extra file"
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_delete_tree(tag, entries, dest):
|
||||||
|
"""Create a source tree and seed a full mirror at `dest`, returning
|
||||||
|
(source, received_mirror)."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"delpol_{tag}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
for rel, content in entries.items():
|
||||||
|
full = os.path.join(source, rel)
|
||||||
|
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||||
|
with open(full, "wb") as fh:
|
||||||
|
fh.write(content)
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
return source, received
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeletePolicy:
|
||||||
|
"""Deletion-policy family: --delete-excluded, --max-delete, --force,
|
||||||
|
--ignore-errors and --prune-empty-dirs."""
|
||||||
|
|
||||||
|
def _write(self, path, content):
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, "wb") as fh:
|
||||||
|
fh.write(content)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
@pytest.mark.parametrize("timing", ["--delete", "--delete-before"])
|
||||||
|
def test_delete_protects_excluded_by_default_and_delete_excluded_removes(self, mt, timing):
|
||||||
|
"""rsync parity: with --delete a destination mirror path whose source was
|
||||||
|
excluded survives (protected by default); --delete-excluded opts back
|
||||||
|
into deleting it. Verified single-threaded, -m, and an early timing
|
||||||
|
run (--delete-before) where the manifest arrives before any data."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"delexcl_{timing.strip('-')}_{mt}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
entries = {
|
||||||
|
"keep.txt": b"kept\n",
|
||||||
|
"secret.log": b"secret\n",
|
||||||
|
"sub/nested.log": b"nested secret\n",
|
||||||
|
}
|
||||||
|
for rel, content in entries.items():
|
||||||
|
self._write(os.path.join(source, rel), content)
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"delexcl_{timing.strip('-')}_{mt}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||||
|
|
||||||
|
# Default: the excluded mirrors survive --delete, genuine extras die.
|
||||||
|
flags = ["--exclude", "*.log", timing] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"default delete sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert os.path.exists(os.path.join(received, "secret.log")), \
|
||||||
|
"excluded dest file was deleted under plain --delete (rsync protects it)"
|
||||||
|
assert os.path.exists(os.path.join(received, "sub", "nested.log")), \
|
||||||
|
"nested excluded dest file was deleted under plain --delete"
|
||||||
|
assert not os.path.exists(os.path.join(received, "extra.txt")), \
|
||||||
|
"genuine extra was not deleted"
|
||||||
|
|
||||||
|
# --delete-excluded: excluded mirrors are extras again and die.
|
||||||
|
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||||
|
flags = ["--exclude", "*.log", timing, "--delete-excluded"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"--delete-excluded sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert not os.path.exists(os.path.join(received, "secret.log")), \
|
||||||
|
"--delete-excluded did not remove the excluded dest file"
|
||||||
|
assert not os.path.exists(os.path.join(received, "sub", "nested.log")), \
|
||||||
|
"--delete-excluded did not remove the nested excluded dest file"
|
||||||
|
assert not os.path.exists(os.path.join(received, "extra.txt")), \
|
||||||
|
"genuine extra survived --delete-excluded"
|
||||||
|
assert _read_file(os.path.join(received, "keep.txt")) == b"kept\n"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
def test_delete_excluded_excluded_directory_subtree(self, mt):
|
||||||
|
"""A whole source directory excluded by a filter rule protects its whole
|
||||||
|
destination mirror by default; --delete-excluded removes the subtree."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"delexcldir_{mt}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
self._write(os.path.join(source, "keep.txt"), b"kept\n")
|
||||||
|
self._write(os.path.join(source, "skipdir", "a.log"), b"a\n")
|
||||||
|
self._write(os.path.join(source, "skipdir", "deep", "b.log"), b"b\n")
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"delexcldir_{mt}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
|
||||||
|
flags = ["--filter=- skipdir/", "--delete"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"default delete sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert os.path.exists(os.path.join(received, "skipdir", "a.log")), \
|
||||||
|
"excluded dir subtree was deleted under plain --delete"
|
||||||
|
assert os.path.exists(os.path.join(received, "skipdir", "deep", "b.log")), \
|
||||||
|
"nested excluded dir content was deleted under plain --delete"
|
||||||
|
|
||||||
|
flags = ["--filter=- skipdir/", "--delete", "--delete-excluded"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"--delete-excluded sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert not os.path.exists(os.path.join(received, "skipdir")), \
|
||||||
|
"--delete-excluded did not remove the excluded dir subtree"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
@pytest.mark.parametrize("timing", ["--delete", "--delete-before"])
|
||||||
|
def test_max_delete_exceeded_fails_without_deleting(self, mt, timing):
|
||||||
|
"""A run that would exceed --max-delete deletes nothing and fails."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"maxdel_{timing.strip('-')}_{mt}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
self._write(os.path.join(source, "keep.txt"), b"kept\n")
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"maxdel_{timing.strip('-')}_{mt}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
extras = []
|
||||||
|
for i in range(4):
|
||||||
|
name = f"e{i}.txt"
|
||||||
|
self._write(os.path.join(received, name), b"extra\n")
|
||||||
|
extras.append(os.path.join(received, name))
|
||||||
|
|
||||||
|
flags = ["--max-delete=2", timing] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode != 0, \
|
||||||
|
f"--max-delete=2 with 4 extras unexpectedly succeeded: {result.stderr[:300]}"
|
||||||
|
for path in extras:
|
||||||
|
assert os.path.exists(path), \
|
||||||
|
"--max-delete overrun deleted files (must be all-or-nothing)"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
def test_max_delete_not_exceeded_deletes_exactly(self, mt):
|
||||||
|
"""When the extras are at or below --max-delete the run succeeds and
|
||||||
|
removes exactly the extras."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"maxdelok_{mt}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
self._write(os.path.join(source, "keep.txt"), b"kept\n")
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"maxdelok_{mt}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
for i in range(3):
|
||||||
|
self._write(os.path.join(received, f"e{i}.txt"), b"extra\n")
|
||||||
|
flags = ["--max-delete=3", "--delete"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"--max-delete=3 with 3 extras failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
for i in range(3):
|
||||||
|
assert not os.path.exists(os.path.join(received, f"e{i}.txt")), \
|
||||||
|
f"extra e{i}.txt not deleted under --max-delete=3"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
def test_force_replaces_nonempty_dir_with_file(self, mt):
|
||||||
|
"""--force lets an incoming regular file replace a non-empty destination
|
||||||
|
directory; without it the write (and the run) fails."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"force_{mt}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
self._write(os.path.join(source, "sub", "old.txt"), b"old\n")
|
||||||
|
self._write(os.path.join(source, "keep.txt"), b"kept\n")
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"force_{mt}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
|
||||||
|
# The source path `sub` becomes a regular file (the dir is gone).
|
||||||
|
os.unlink(os.path.join(source, "sub", "old.txt"))
|
||||||
|
os.rmdir(os.path.join(source, "sub"))
|
||||||
|
self._write(os.path.join(source, "sub"), b"now a file\n")
|
||||||
|
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode != 0, \
|
||||||
|
"a file over a non-empty directory must fail without --force"
|
||||||
|
assert os.path.isdir(os.path.join(received, "sub")), \
|
||||||
|
"directory was destroyed although the run failed without --force"
|
||||||
|
assert os.path.exists(os.path.join(received, "sub", "old.txt")), \
|
||||||
|
"non-empty dir content was lost although the run failed without --force"
|
||||||
|
|
||||||
|
flags = ["--force"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"--force run failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert os.path.isfile(os.path.join(received, "sub")), \
|
||||||
|
"--force did not replace the directory with the file"
|
||||||
|
assert _read_file(os.path.join(received, "sub")) == b"now a file\n"
|
||||||
|
assert not os.path.exists(os.path.join(received, "sub", "old.txt")), \
|
||||||
|
"--force left the old directory content behind"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
def test_prune_empty_dirs_dirs_mode(self, mt):
|
||||||
|
"""--prune-empty-dirs omits an empty source directory's explicit entry in
|
||||||
|
--dirs mode (nothing is created, and an existing empty mirror is removed
|
||||||
|
by --delete). Recursive transfers never emit empty dirs, so the flag is
|
||||||
|
a no-op there (documented rsync -m parity)."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"prune_{mt}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
os.makedirs(source, exist_ok=True) # physically empty source dir
|
||||||
|
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"prune_{mt}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, flags=["--dirs"], port=server.port)
|
||||||
|
assert result.returncode == 0, f"-d seed failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
assert os.path.isdir(received), "-d should create the empty mirror dir"
|
||||||
|
assert os.listdir(received) == []
|
||||||
|
|
||||||
|
# prune-empty-dirs: the empty mirror is pruned by --delete.
|
||||||
|
flags = ["--dirs", "--prune-empty-dirs", "--delete"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"--dirs --prune-empty-dirs --delete failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert not os.path.exists(received), \
|
||||||
|
"--prune-empty-dirs did not prune the empty dir (--delete left it)"
|
||||||
|
|
||||||
|
# A fresh destination: prune-empty-dirs means the empty dir is never sent.
|
||||||
|
dest2 = os.path.join(TEST_DATA_DIR, f"prune2_{mt}_dst")
|
||||||
|
clean_dir(dest2)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
flags = ["--dirs", "--prune-empty-dirs", "-i"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest2, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"--dirs --prune-empty-dirs failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
received2 = get_dest_received_dir(dest2, source)
|
||||||
|
assert not os.path.exists(received2), \
|
||||||
|
"--prune-empty-dirs transferred the empty directory"
|
||||||
|
assert result.stdout == "", \
|
||||||
|
f"--prune-empty-dirs leaked an itemize line: {result.stdout[:200]}"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
def test_prune_empty_dirs_recursion_inherent(self, mt):
|
||||||
|
"""In recursive mode FastSync never transfers empty directories (rsync
|
||||||
|
-m parity): a truly-empty destination directory chain is removed by
|
||||||
|
--delete whether or not --prune-empty-dirs is given (the flag has no
|
||||||
|
additional effect there), while directories holding kept files survive.
|
||||||
|
A filter-excluded file's mirror is protected, so a directory that still
|
||||||
|
holds one is left intact (rsync default delete-excluded semantics)."""
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"prunerec_{mt}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
self._write(os.path.join(source, "keep.txt"), b"kept\n")
|
||||||
|
self._write(os.path.join(source, "a", "keep.log"), b"a log\n")
|
||||||
|
self._write(os.path.join(source, "b", "deep", "kept.txt"), b"deep kept\n")
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"prunerec_{mt}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
# A stray empty chain (FastSync recursion never creates such dirs, so
|
||||||
|
# this models one left by an external tool / an earlier --dirs run).
|
||||||
|
os.makedirs(os.path.join(received, "empty", "chain"))
|
||||||
|
|
||||||
|
for prune in ([], ["--prune-empty-dirs"]):
|
||||||
|
flags = prune + ["--delete"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"prune recursive sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert not os.path.exists(os.path.join(received, "empty")), \
|
||||||
|
"truly-empty dir chain was not removed by --delete"
|
||||||
|
assert os.path.exists(os.path.join(received, "b", "deep", "kept.txt")), \
|
||||||
|
"non-empty dir subtree was wrongly removed"
|
||||||
|
assert _read_file(os.path.join(received, "keep.txt")) == b"kept\n"
|
||||||
|
|
||||||
|
# An excluded file's mirror is protected: the dir that holds it stays.
|
||||||
|
flags = ["--exclude", "*.log", "--delete", "--prune-empty-dirs"] + (["-m"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 0, \
|
||||||
|
f"prune recursive sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||||
|
assert os.path.exists(os.path.join(received, "a", "keep.log")), \
|
||||||
|
"excluded file mirror was deleted under --delete (rsync protects it)"
|
||||||
|
|
||||||
|
def test_ignore_errors_keeps_deletion_active_on_scan_error(self):
|
||||||
|
"""A source I/O error (unreadable directory) aborts the run so no
|
||||||
|
deletion happens by default; --ignore-errors continues, still transfers
|
||||||
|
the readable tree and still deletes. Run as an unprivileged user so the
|
||||||
|
mode-000 directory is genuinely unreadable."""
|
||||||
|
if os.geteuid() != 0 or shutil.which("setpriv") is None:
|
||||||
|
pytest.skip("requires root + setpriv to drop privileges for the client")
|
||||||
|
tag = f"ioerr_{os.getpid()}"
|
||||||
|
source = os.path.join(TEST_DATA_DIR, f"{tag}_src")
|
||||||
|
clean_dir(source)
|
||||||
|
self._write(os.path.join(source, "top.txt"), b"top\n")
|
||||||
|
self._write(os.path.join(source, "ok", "inside.txt"), b"inside\n")
|
||||||
|
self._write(os.path.join(source, "locked", "blocked.txt"), b"blocked\n")
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, f"{tag}_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
# Seed as root (server is root too).
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
try:
|
||||||
|
os.chmod(os.path.join(source, "locked"), 0)
|
||||||
|
|
||||||
|
# Default: scan error aborts the run; nothing is deleted.
|
||||||
|
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||||
|
cmd = CLIENT_CMD + ["--source-dir", source, "--dest-dir", dest,
|
||||||
|
"--save-to-disk", "--server-port", str(server.port),
|
||||||
|
"--delete"]
|
||||||
|
result = subprocess.run(["setpriv", "--reuid=65534", "--regid=65534",
|
||||||
|
"--clear-groups"] + cmd, text=True, capture_output=True)
|
||||||
|
assert result.returncode != 0, "unreadable source dir did not fail the run"
|
||||||
|
assert os.path.exists(os.path.join(received, "extra.txt")), \
|
||||||
|
"default run deleted although the scan hit an I/O error"
|
||||||
|
|
||||||
|
# --ignore-errors: the readable tree transfers, deletion still runs.
|
||||||
|
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||||
|
cmd = CLIENT_CMD + ["--source-dir", source, "--dest-dir", dest,
|
||||||
|
"--save-to-disk", "--server-port", str(server.port),
|
||||||
|
"--delete", "--ignore-errors"]
|
||||||
|
result = subprocess.run(["setpriv", "--reuid=65534", "--regid=65534",
|
||||||
|
"--clear-groups"] + cmd, text=True, capture_output=True)
|
||||||
|
assert not os.path.exists(os.path.join(received, "extra.txt")), \
|
||||||
|
f"--ignore-errors did not keep deletion active: {result.stderr[:300]}"
|
||||||
|
assert not os.path.exists(os.path.join(received, "locked")), \
|
||||||
|
"mirror of the unreadable dir was not treated as an extra"
|
||||||
|
finally:
|
||||||
|
os.chmod(os.path.join(source, "locked"), 0o755)
|
||||||
|
|
||||||
|
|
||||||
def _pin_mtime(path, ts):
|
def _pin_mtime(path, ts):
|
||||||
os.utime(path, (ts, ts))
|
os.utime(path, (ts, ts))
|
||||||
|
|
||||||
|
|||||||
@@ -1648,6 +1648,73 @@ static void test_parse_args_files_from() {
|
|||||||
remove(list_path);
|
remove(list_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The deletion-policy family parses onto the config fields: --delete-excluded,
|
||||||
|
* --ignore-errors and --force are flags, --max-delete takes a non-negative
|
||||||
|
* number, and --prune-empty-dirs is the long-only spelling (FastSync's -m stays
|
||||||
|
* multithreading). None of them implies --delete by itself. */
|
||||||
|
static void test_parse_args_delete_policy_flags() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync",
|
||||||
|
"--delete",
|
||||||
|
"--delete-excluded",
|
||||||
|
"--max-delete=5",
|
||||||
|
"--ignore-errors",
|
||||||
|
"--force",
|
||||||
|
"--prune-empty-dirs",
|
||||||
|
"/src",
|
||||||
|
"/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 9, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_TRUE(cfg->use_delete);
|
||||||
|
EXPECT_TRUE(cfg->delete_excluded);
|
||||||
|
EXPECT_EQ_INT(cfg->max_delete, 5);
|
||||||
|
EXPECT_TRUE(cfg->ignore_errors);
|
||||||
|
EXPECT_TRUE(cfg->force_delete);
|
||||||
|
EXPECT_TRUE(cfg->prune_empty_dirs);
|
||||||
|
EXPECT_FALSE(cfg->delete_before);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
/* --max-delete accepts the separated-argument and zero forms. */
|
||||||
|
cfg = config_create();
|
||||||
|
char* argv2[] = {"fastsync", "--max-delete", "0", "/src", "/dst"};
|
||||||
|
positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_INT(cfg->max_delete, 0);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_args_delete_policy_invalid_values() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", "--max-delete=abc", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
||||||
|
config_delete(cfg);
|
||||||
|
|
||||||
|
cfg = config_create();
|
||||||
|
char* argv2[] = {"fastsync", "--max-delete=-3", "/src", "/dst"};
|
||||||
|
positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv2, positional_args, &positional_count), -1);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --max-delete without --delete is inert (it only bounds a --delete run); the
|
||||||
|
* config stays valid. */
|
||||||
|
static void test_parse_args_max_delete_inert_without_delete() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
cfg->send_directory = str_dup("/src");
|
||||||
|
cfg->receive_root_directory = str_dup("/dst");
|
||||||
|
char* argv[] = {"fastsync", "--max-delete=5", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_FALSE(cfg->use_delete);
|
||||||
|
EXPECT_EQ_INT(cfg->max_delete, 5);
|
||||||
|
EXPECT_TRUE(validate_config(cfg));
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
void test_client_cli() {
|
void test_client_cli() {
|
||||||
test_validate_config_required_paths();
|
test_validate_config_required_paths();
|
||||||
test_validate_config_incompatible_options();
|
test_validate_config_incompatible_options();
|
||||||
@@ -1740,4 +1807,7 @@ void test_client_cli() {
|
|||||||
test_parse_args_basis_dirs();
|
test_parse_args_basis_dirs();
|
||||||
test_parse_args_basis_invalid_paths();
|
test_parse_args_basis_invalid_paths();
|
||||||
test_validate_config_basis_rejects_chunk_serialization();
|
test_validate_config_basis_rejects_chunk_serialization();
|
||||||
|
test_parse_args_delete_policy_flags();
|
||||||
|
test_parse_args_delete_policy_invalid_values();
|
||||||
|
test_parse_args_max_delete_inert_without_delete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -561,6 +561,61 @@ static void test_config_delete_timing_conflict_rejected() {
|
|||||||
config_delete(c);
|
config_delete(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The deletion-policy fields that cross the wire survive a config round trip:
|
||||||
|
--force (force_delete), --delete-excluded, --prune-empty-dirs and the
|
||||||
|
--max-delete number (default -1 == no client limit). */
|
||||||
|
static void test_config_delete_policy_wire_roundtrip() {
|
||||||
|
if (is_running_under_valgrind())
|
||||||
|
return;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
bool force_delete, delete_excluded, prune_empty_dirs;
|
||||||
|
int max_delete;
|
||||||
|
} cases[] = {
|
||||||
|
{false, false, false, -1},
|
||||||
|
{true, false, false, 0},
|
||||||
|
{false, true, true, 7},
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
close(p[1]);
|
||||||
|
io_set_fds(p[0], p[0]);
|
||||||
|
Config* recv = config_receive(p[0]);
|
||||||
|
bool ok = recv != NULL;
|
||||||
|
if (ok) {
|
||||||
|
ok = recv->force_delete == cases[i].force_delete &&
|
||||||
|
recv->delete_excluded == cases[i].delete_excluded &&
|
||||||
|
recv->prune_empty_dirs == cases[i].prune_empty_dirs &&
|
||||||
|
recv->max_delete == cases[i].max_delete;
|
||||||
|
}
|
||||||
|
config_delete(recv);
|
||||||
|
close(p[0]);
|
||||||
|
_exit(ok ? 0 : 1);
|
||||||
|
} else {
|
||||||
|
close(p[0]);
|
||||||
|
io_set_fds(p[1], p[1]);
|
||||||
|
Config* send_cfg = config_create();
|
||||||
|
EXPECT_NOT_NULL(send_cfg);
|
||||||
|
send_cfg->send_directory = str_dup("/src");
|
||||||
|
send_cfg->receive_root_directory = str_dup("/dst");
|
||||||
|
send_cfg->force_delete = cases[i].force_delete;
|
||||||
|
send_cfg->delete_excluded = cases[i].delete_excluded;
|
||||||
|
send_cfg->prune_empty_dirs = cases[i].prune_empty_dirs;
|
||||||
|
send_cfg->max_delete = cases[i].max_delete;
|
||||||
|
bool sent = config_send(p[1], send_cfg);
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
close(p[1]);
|
||||||
|
config_delete(send_cfg);
|
||||||
|
EXPECT_TRUE(sent);
|
||||||
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Basis-dir lists survive the config wire: each entry's type and path must
|
/* Basis-dir lists survive the config wire: each entry's type and path must
|
||||||
round-trip unchanged. */
|
round-trip unchanged. */
|
||||||
static void test_config_basis_roundtrip() {
|
static void test_config_basis_roundtrip() {
|
||||||
@@ -712,6 +767,7 @@ void test_config() {
|
|||||||
test_config_delay_updates_reserved_backup_rejected();
|
test_config_delay_updates_reserved_backup_rejected();
|
||||||
test_config_delete_timing_wire_roundtrip();
|
test_config_delete_timing_wire_roundtrip();
|
||||||
test_config_delete_timing_conflict_rejected();
|
test_config_delete_timing_conflict_rejected();
|
||||||
|
test_config_delete_policy_wire_roundtrip();
|
||||||
test_config_basis_roundtrip();
|
test_config_basis_roundtrip();
|
||||||
test_config_basis_wire_rejects_escaping();
|
test_config_basis_wire_rejects_escaping();
|
||||||
test_config_basis_normalization();
|
test_config_basis_normalization();
|
||||||
|
|||||||
@@ -397,9 +397,8 @@ static void test_parallel_scanner_root_chunks_without_workers() {
|
|||||||
create_test_file(file1, "a");
|
create_test_file(file1, "a");
|
||||||
create_test_file(file2, "b");
|
create_test_file(file2, "b");
|
||||||
|
|
||||||
ScannerOptions options = {false, 1, NULL, 0, NULL, 0, 0,
|
ScannerOptions options = {0};
|
||||||
0, 0, 0, false, false, false, false,
|
options.chunk_size = 1;
|
||||||
false, false, NULL, NULL, false, false, false};
|
|
||||||
ParallelScanner* scanner = parallel_scanner_create_with_options(dir, &options, NULL);
|
ParallelScanner* scanner = parallel_scanner_create_with_options(dir, &options, NULL);
|
||||||
EXPECT_NOT_NULL(scanner);
|
EXPECT_NOT_NULL(scanner);
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -476,7 +476,7 @@ static Config* make_late_delete_config(const char* root) {
|
|||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int run_pending_receiver(Config* cfg, int fd, ArrayList** pending) {
|
static int run_pending_receiver(Config* cfg, int fd, DeleteManifest** pending) {
|
||||||
ReceiverSink sink = {0};
|
ReceiverSink sink = {0};
|
||||||
return receiver_process_pending(cfg, fd, &sink, pending);
|
return receiver_process_pending(cfg, fd, &sink, pending);
|
||||||
}
|
}
|
||||||
@@ -492,9 +492,10 @@ static void test_late_manifest_abort_frees_keepset() {
|
|||||||
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
||||||
EXPECT_TRUE(send_int(p[1], 1));
|
EXPECT_TRUE(send_int(p[1], 1));
|
||||||
EXPECT_TRUE(send_str(p[1], "keep.txt"));
|
EXPECT_TRUE(send_str(p[1], "keep.txt"));
|
||||||
|
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
|
||||||
EXPECT_TRUE(send_status(p[1], STATUS_ABORT));
|
EXPECT_TRUE(send_status(p[1], STATUS_ABORT));
|
||||||
|
|
||||||
ArrayList* pending = NULL;
|
DeleteManifest* pending = NULL;
|
||||||
EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1);
|
EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1);
|
||||||
EXPECT_NULL(pending);
|
EXPECT_NULL(pending);
|
||||||
|
|
||||||
@@ -514,9 +515,10 @@ static void test_late_manifest_eof_frees_keepset() {
|
|||||||
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
||||||
EXPECT_TRUE(send_int(p[1], 1));
|
EXPECT_TRUE(send_int(p[1], 1));
|
||||||
EXPECT_TRUE(send_str(p[1], "keep.txt"));
|
EXPECT_TRUE(send_str(p[1], "keep.txt"));
|
||||||
|
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
|
||||||
shutdown(p[1], SHUT_WR);
|
shutdown(p[1], SHUT_WR);
|
||||||
|
|
||||||
ArrayList* pending = NULL;
|
DeleteManifest* pending = NULL;
|
||||||
EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1);
|
EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1);
|
||||||
EXPECT_NULL(pending);
|
EXPECT_NULL(pending);
|
||||||
|
|
||||||
@@ -536,11 +538,13 @@ static void test_late_second_manifest_frees_both() {
|
|||||||
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
||||||
EXPECT_TRUE(send_int(p[1], 1));
|
EXPECT_TRUE(send_int(p[1], 1));
|
||||||
EXPECT_TRUE(send_str(p[1], "first.txt"));
|
EXPECT_TRUE(send_str(p[1], "first.txt"));
|
||||||
|
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
|
||||||
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
EXPECT_TRUE(send_status(p[1], STATUS_MANIFEST));
|
||||||
EXPECT_TRUE(send_int(p[1], 1));
|
EXPECT_TRUE(send_int(p[1], 1));
|
||||||
EXPECT_TRUE(send_str(p[1], "second.txt"));
|
EXPECT_TRUE(send_str(p[1], "second.txt"));
|
||||||
|
EXPECT_TRUE(send_int(p[1], 0)); /* protected-prefix section is empty */
|
||||||
|
|
||||||
ArrayList* pending = NULL;
|
DeleteManifest* pending = NULL;
|
||||||
EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1);
|
EXPECT_EQ_INT(run_pending_receiver(cfg, p[0], &pending), -1);
|
||||||
EXPECT_NULL(pending);
|
EXPECT_NULL(pending);
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,204 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <threads.h>
|
#include <threads.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* ---- delete-walker tests ---- */
|
||||||
|
|
||||||
|
static char* make_walk_root(const char* tag) {
|
||||||
|
char* path = malloc(256);
|
||||||
|
if (!path)
|
||||||
|
return NULL;
|
||||||
|
snprintf(path, 256, "/tmp/fastsync_walk_%s_%d", tag, (int)getpid());
|
||||||
|
rmdir(path);
|
||||||
|
if (mkdir(path, 0755) != 0) {
|
||||||
|
free(path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_file_at(const char* dir, const char* name, const char* content) {
|
||||||
|
char* path = path_cat(dir, name);
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
bool ok = fd >= 0;
|
||||||
|
if (fd >= 0) {
|
||||||
|
if (content) {
|
||||||
|
const char* p = content;
|
||||||
|
size_t remaining = strlen(content);
|
||||||
|
while (remaining > 0) {
|
||||||
|
ssize_t n = write(fd, p, remaining);
|
||||||
|
if (n <= 0) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p += n;
|
||||||
|
remaining -= (size_t)n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
free(path);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool file_exists(const char* dir, const char* name) {
|
||||||
|
char* path = path_cat(dir, name);
|
||||||
|
bool exists = path && access(path, F_OK) == 0;
|
||||||
|
free(path);
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool dir_exists(const char* dir, const char* name) {
|
||||||
|
char* path = path_cat(dir, name);
|
||||||
|
struct stat st;
|
||||||
|
bool exists = path && stat(path, &st) == 0 && S_ISDIR(st.st_mode);
|
||||||
|
free(path);
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int make_subdir(const char* root, const char* name) {
|
||||||
|
char* path = path_cat(root, name);
|
||||||
|
int rc = -1;
|
||||||
|
if (path) {
|
||||||
|
rc = mkdir(path, 0755);
|
||||||
|
free(path);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove_walk_tree(const char* path) {
|
||||||
|
DIR* dir = opendir(path);
|
||||||
|
if (!dir) {
|
||||||
|
rmdir(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const struct dirent* entry;
|
||||||
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
char* child = path_cat(path, entry->d_name);
|
||||||
|
if (child) {
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(child, &st) == 0 && S_ISDIR(st.st_mode))
|
||||||
|
remove_walk_tree(child);
|
||||||
|
else
|
||||||
|
unlink(child);
|
||||||
|
free(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
rmdir(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ArrayList* make_manifest_strings(const char* const* entries, int count) {
|
||||||
|
ArrayList* manifest = array_list_create(free);
|
||||||
|
if (!manifest)
|
||||||
|
return NULL;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
char* dup = str_dup(entries[i]);
|
||||||
|
if (!dup || !array_list_add(manifest, dup)) {
|
||||||
|
free(dup);
|
||||||
|
array_list_delete(manifest);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_walker_removes_extras_keeps_manifest_and_protected() {
|
||||||
|
char* root = make_walk_root("basic");
|
||||||
|
EXPECT_NOT_NULL(root);
|
||||||
|
EXPECT_TRUE(write_file_at(root, "a.txt", "extra"));
|
||||||
|
EXPECT_TRUE(write_file_at(root, "keep.txt", "kept"));
|
||||||
|
EXPECT_EQ_INT(make_subdir(root, "d"), 0);
|
||||||
|
EXPECT_TRUE(write_file_at(root, "d/e.txt", "extra"));
|
||||||
|
EXPECT_TRUE(write_file_at(root, "d/k.txt", "kept"));
|
||||||
|
EXPECT_EQ_INT(make_subdir(root, "prot"), 0);
|
||||||
|
EXPECT_TRUE(write_file_at(root, "prot/f.txt", "untouched"));
|
||||||
|
|
||||||
|
const char* keeps[] = {"keep.txt", "d/k.txt"};
|
||||||
|
ArrayList* manifest = make_manifest_strings(keeps, 2);
|
||||||
|
EXPECT_NOT_NULL(manifest);
|
||||||
|
DeleteSkipEntry skip = {"prot", false};
|
||||||
|
size_t deleted = 0;
|
||||||
|
DeleteWalkResult result = delete_extras_limited(root, manifest, 100000, &skip, 1, &deleted);
|
||||||
|
EXPECT_EQ_INT((int)result, (int)DELETE_WALK_OK);
|
||||||
|
EXPECT_FALSE(file_exists(root, "a.txt"));
|
||||||
|
EXPECT_TRUE(file_exists(root, "keep.txt"));
|
||||||
|
EXPECT_FALSE(file_exists(root, "d/e.txt"));
|
||||||
|
EXPECT_TRUE(file_exists(root, "d/k.txt"));
|
||||||
|
EXPECT_TRUE(dir_exists(root, "d"));
|
||||||
|
EXPECT_TRUE(file_exists(root, "prot/f.txt"));
|
||||||
|
EXPECT_TRUE(deleted >= 2);
|
||||||
|
array_list_delete(manifest);
|
||||||
|
remove_walk_tree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_walker_max_delete_exceeded_deletes_nothing() {
|
||||||
|
char* root = make_walk_root("maxdel");
|
||||||
|
EXPECT_NOT_NULL(root);
|
||||||
|
EXPECT_TRUE(write_file_at(root, "a.txt", "extra"));
|
||||||
|
EXPECT_TRUE(write_file_at(root, "b.txt", "extra"));
|
||||||
|
EXPECT_TRUE(write_file_at(root, "c.txt", "extra"));
|
||||||
|
const char* keeps[1] = {NULL};
|
||||||
|
ArrayList* manifest = make_manifest_strings(keeps, 0);
|
||||||
|
EXPECT_NOT_NULL(manifest);
|
||||||
|
size_t deleted = 999;
|
||||||
|
DeleteWalkResult result = delete_extras_limited(root, manifest, 2, NULL, 0, &deleted);
|
||||||
|
EXPECT_EQ_INT((int)result, (int)DELETE_WALK_LIMIT_EXCEEDED);
|
||||||
|
EXPECT_EQ_INT((int)deleted, 0);
|
||||||
|
EXPECT_TRUE(file_exists(root, "a.txt"));
|
||||||
|
EXPECT_TRUE(file_exists(root, "b.txt"));
|
||||||
|
EXPECT_TRUE(file_exists(root, "c.txt"));
|
||||||
|
array_list_delete(manifest);
|
||||||
|
remove_walk_tree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_walker_max_delete_exact_bound_deletes() {
|
||||||
|
char* root = make_walk_root("maxdel2");
|
||||||
|
EXPECT_NOT_NULL(root);
|
||||||
|
EXPECT_TRUE(write_file_at(root, "a.txt", "extra"));
|
||||||
|
EXPECT_TRUE(write_file_at(root, "b.txt", "extra"));
|
||||||
|
const char* keeps[1] = {NULL};
|
||||||
|
ArrayList* manifest = make_manifest_strings(keeps, 0);
|
||||||
|
EXPECT_NOT_NULL(manifest);
|
||||||
|
size_t deleted = 0;
|
||||||
|
DeleteWalkResult result = delete_extras_limited(root, manifest, 2, NULL, 0, &deleted);
|
||||||
|
EXPECT_EQ_INT((int)result, (int)DELETE_WALK_OK);
|
||||||
|
EXPECT_EQ_INT((int)deleted, 2);
|
||||||
|
EXPECT_FALSE(file_exists(root, "a.txt"));
|
||||||
|
EXPECT_FALSE(file_exists(root, "b.txt"));
|
||||||
|
array_list_delete(manifest);
|
||||||
|
remove_walk_tree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_walker_unlimited_deletes_all() {
|
||||||
|
char* root = make_walk_root("unlim");
|
||||||
|
EXPECT_NOT_NULL(root);
|
||||||
|
EXPECT_TRUE(write_file_at(root, "a.txt", "extra"));
|
||||||
|
EXPECT_TRUE(write_file_at(root, "b.txt", "extra"));
|
||||||
|
EXPECT_EQ_INT(make_subdir(root, "emptydir"), 0);
|
||||||
|
const char* keeps[1] = {NULL};
|
||||||
|
ArrayList* manifest = make_manifest_strings(keeps, 0);
|
||||||
|
EXPECT_NOT_NULL(manifest);
|
||||||
|
EXPECT_TRUE(delete_extras(root, manifest));
|
||||||
|
EXPECT_FALSE(file_exists(root, "a.txt"));
|
||||||
|
EXPECT_FALSE(file_exists(root, "b.txt"));
|
||||||
|
EXPECT_FALSE(dir_exists(root, "emptydir"));
|
||||||
|
array_list_delete(manifest);
|
||||||
|
remove_walk_tree(root);
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool eight_bit_output;
|
bool eight_bit_output;
|
||||||
@@ -24,6 +219,11 @@ static int escape_thread(void* arg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void test_shared_utils() {
|
void test_shared_utils() {
|
||||||
|
test_walker_removes_extras_keeps_manifest_and_protected();
|
||||||
|
test_walker_max_delete_exceeded_deletes_nothing();
|
||||||
|
test_walker_max_delete_exact_bound_deletes();
|
||||||
|
test_walker_unlimited_deletes_all();
|
||||||
|
|
||||||
char formatted[32];
|
char formatted[32];
|
||||||
EXPECT_TRUE(format_human_bytes(0, formatted, sizeof(formatted)));
|
EXPECT_TRUE(format_human_bytes(0, formatted, sizeof(formatted)));
|
||||||
EXPECT_EQ_STR(formatted, "0 B");
|
EXPECT_EQ_STR(formatted, "0 B");
|
||||||
|
|||||||
Reference in New Issue
Block a user