Files
FastSync/tests/integration/test_append.py
T
TapTap 79d965ac11
CI / lint (pull_request) Failing after 0s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
ci: parallelize integration suite with pytest-xdist; fast PR gate + full coverage on merge
- Add pytest-xdist to the CI Dockerfile (image -> v10).
- Worker-isolate TEST_DATA_DIR (PYTEST_XDIST_WORKER) so concurrent xdist
  workers never collide on shared-filesystem fixtures.
- Register a 'ci' marker and tag a fast representative subset of integration
  tests (basic TCP, incremental, compression, delete, basis, append).
- ci.yaml: lint + build + unit + the marked subset (-n4) on every PR; the
  full integration suite plus sanitizer/fuzz/coverage/valgrind run only on
  push to dev/main.
- Integration step runtime drops from ~860s (serial) to ~230s (-n4); the PR
  gate lands well under ~3 minutes.
2026-09-08 16:53:54 +02:00

144 lines
6.7 KiB
Python

"""--append / --append-verify tail-resume integration tests.
A shorter existing destination file is resumed by transferring only the tail:
--append sends it without verifying the retained prefix (rsync parity: a wrong
prefix is kept, so the result can differ from the source), while --append-verify
checksums the retained prefix against the source and, on a mismatch, falls back
to a clean full transfer so the result is always a byte-identical source copy.
"""
import os
import random
import shutil
import pytest
from common import (
TEST_DATA_DIR,
run_client, CountingProxy, clean_dir,
get_dest_received_dir, CLIENT_CMD,
)
REL = "sub/grow.dat"
def _grow_payload(prefix_size, added_size, seed=99):
r = random.Random(seed)
return bytes(r.randbytes(prefix_size)), bytes(r.randbytes(added_size))
class TestAppend:
def _make(self, tag):
source = os.path.join(TEST_DATA_DIR, f"append_{tag}_src")
dest = os.path.join(TEST_DATA_DIR, f"append_{tag}_dst")
clean_dir(source)
shutil.rmtree(dest, ignore_errors=True)
return source, dest
def _place(self, root, rel, data):
p = os.path.join(root, rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "wb") as fh:
fh.write(data)
return p
def _read(self, root, rel):
with open(os.path.join(root, rel), "rb") as fh:
return fh.read()
def _dest_file(self, source, dest, rel):
return os.path.join(get_dest_received_dir(dest, source), rel)
@pytest.mark.ci
def test_append_resumes_short_dest_atomically(self, shared_server):
"""A shorter dest with a MATCHING prefix is resumed; the reconstructed
file is byte-identical to the source."""
source, dest = self._make("atomic")
prefix, added = _grow_payload(1 * 1024 * 1024, 64 * 1024)
self._place(source, REL, prefix + added)
self._place(self._dest_file(source, dest, ""), REL, prefix)
result, _ = run_client(source, dest, flags=["--append"], port=shared_server.port)
assert result.returncode == 0, \
f"--append failed: {(result.stderr or result.stdout)[:400]}"
assert self._read(self._dest_file(source, dest, ""), REL) == prefix + added
def test_append_verify_matching_prefix_succeeds(self, shared_server):
source, dest = self._make("verify_ok")
prefix, added = _grow_payload(512 * 1024, 32 * 1024)
self._place(source, REL, prefix + added)
self._place(self._dest_file(source, dest, ""), REL, prefix)
result, _ = run_client(source, dest, flags=["--append-verify"], port=shared_server.port)
assert result.returncode == 0, \
f"--append-verify failed: {(result.stderr or result.stdout)[:400]}"
assert self._read(self._dest_file(source, dest, ""), REL) == prefix + added
def test_append_sends_only_tail(self, shared_server):
"""Sorted transfer moves only the tail: wire bytes stay well below the
full source size (incompressible payload, no -c)."""
source, dest = self._make("tail")
prefix, added = _grow_payload(4 * 1024 * 1024, 8 * 1024, seed=7)
full = prefix + added
self._place(source, REL, full)
self._place(self._dest_file(source, dest, ""), REL, prefix)
proxy = CountingProxy(shared_server.port)
cmd = (CLIENT_CMD + ["--source-dir", source, "--dest-dir", dest,
"--save-to-disk", "--server-port", str(proxy.port), "--append"])
result = proxy.run(cmd)
assert result.returncode == 0, \
f"--append failed: {(result.stderr or result.stdout)[:400]}"
assert self._read(self._dest_file(source, dest, ""), REL) == full
assert proxy.client_to_server < full.__len__() // 2, \
f"expected a tail-only transfer, sent {proxy.client_to_server}B for {full.__len__()}B"
def test_plain_append_wrong_prefix_is_rsync_parity(self, shared_server):
"""--append does NOT verify the retained prefix: a wrong prefix is kept,
so the result is prefix+tail (differs from the source). This is the
documented rsync-parity risk of plain --append."""
source, dest = self._make("plain_wrong")
correct_prefix, added = _grow_payload(256 * 1024, 32 * 1024, seed=1)
wrong_prefix = bytes(b ^ 0xFF for b in correct_prefix)
self._place(source, REL, correct_prefix + added)
self._place(self._dest_file(source, dest, ""), REL, wrong_prefix)
result, _ = run_client(source, dest, flags=["--append"], port=shared_server.port)
assert result.returncode == 0
assert self._read(self._dest_file(source, dest, ""), REL) == wrong_prefix + added
def test_append_verify_wrong_prefix_never_corrupts(self, shared_server):
"""--append-verify detects the retained prefix mismatch and falls back to
a full transfer, so the result is a byte-identical source copy."""
source, dest = self._make("verify_wrong")
correct_prefix, added = _grow_payload(256 * 1024, 32 * 1024, seed=2)
wrong_prefix = bytes(b ^ 0xFF for b in correct_prefix)
self._place(source, REL, correct_prefix + added)
self._place(self._dest_file(source, dest, ""), REL, wrong_prefix)
result, _ = run_client(source, dest, flags=["--append-verify"], port=shared_server.port)
assert result.returncode == 0, \
f"--append-verify mismatch fallback failed: {(result.stderr or result.stdout)[:400]}"
assert self._read(self._dest_file(source, dest, ""), REL) == correct_prefix + added
def test_append_with_inplace(self, shared_server):
source, dest = self._make("inplace")
prefix, added = _grow_payload(128 * 1024, 16 * 1024, seed=3)
self._place(source, REL, prefix + added)
self._place(self._dest_file(source, dest, ""), REL, prefix)
result, _ = run_client(source, dest, flags=["--append", "--inplace"],
port=shared_server.port)
assert result.returncode == 0, \
f"--append --inplace failed: {(result.stderr or result.stdout)[:400]}"
assert self._read(self._dest_file(source, dest, ""), REL) == prefix + added
def test_append_multithreaded(self, shared_server):
source, dest = self._make("mthread")
prefix, added = _grow_payload(512 * 1024, 32 * 1024, seed=4)
self._place(source, REL, prefix + added)
self._place(self._dest_file(source, dest, ""), REL, prefix)
result, _ = run_client(source, dest, flags=["--append", "-m"], port=shared_server.port)
assert result.returncode == 0, \
f"--append -m failed: {(result.stderr or result.stdout)[:400]}"
assert self._read(self._dest_file(source, dest, ""), REL) == prefix + added