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
- 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.
144 lines
6.7 KiB
Python
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 |