docs(parity): --fuzzy name heuristic matches rsync; reclassify to caveat
Probe shows the candidate choice is observable only as --stats bandwidth counters (tree/exit always identical). FastSync already uses rsync's fuzzy_distance/find_filename_suffix name heuristic; the residual is the narrower delta eligibility window (>=16 KiB, <=10x) vs rsync's wider one. Row -> caveat; tally 116/14/27.
This commit is contained in:
@@ -229,6 +229,18 @@ def corpus_iconv(root: str) -> None:
|
||||
os.utime(full, (_SRC_MTIME, _SRC_MTIME))
|
||||
|
||||
|
||||
# Payload for the --fuzzy basis corpus: large enough for the delta engine's
|
||||
# 16 KiB minimum and with repeated content so a coinciding basis yields a
|
||||
# non-zero (and identical) Matched data count in both tools.
|
||||
FUZZY_PAYLOAD = (b"the quick brown fox jumps over the lazy dog\n" * 2000)[:65536]
|
||||
|
||||
|
||||
def corpus_fuzzy(root: str) -> None:
|
||||
"""A named regular file; the `fuzzy` seed adds the similar-suffix sibling."""
|
||||
clean_dir(root)
|
||||
_write(os.path.join(root, "report_v2.txt"), FUZZY_PAYLOAD)
|
||||
|
||||
|
||||
CORPORA: Dict[str, Callable[[str], None]] = {
|
||||
"basic": corpus_basic,
|
||||
"unicode": corpus_unicode,
|
||||
@@ -240,6 +252,7 @@ CORPORA: Dict[str, Callable[[str], None]] = {
|
||||
"multidir": corpus_multidir,
|
||||
"relative": corpus_relative,
|
||||
"iconv": corpus_iconv,
|
||||
"fuzzy": corpus_fuzzy,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -133,6 +133,16 @@ def seed_max_delete(_src, rroot, froot):
|
||||
_mk(os.path.join(root, "extra2.txt"), b"e2\n", _OLD_MTIME)
|
||||
|
||||
|
||||
def fuzzy_basis_seed(_src, rroot, froot):
|
||||
"""Seed a same-suffix sibling whose name is one edit from the source and
|
||||
whose content matches it, with a DIFFERENT mtime so rsync's exact
|
||||
size+mtime pass cannot fire: both tools must select it via the
|
||||
name-distance pass. Where the two tools' basis choices coincide the
|
||||
block-level results are identical when the block size is pinned."""
|
||||
for root in (rroot, froot):
|
||||
_mk(os.path.join(root, "report_v1.txt"), H.FUZZY_PAYLOAD, _OLD_MTIME)
|
||||
|
||||
|
||||
def max_delete_count_check(_src, rroot, froot, _rs, _fs):
|
||||
"""The exact survivor set is order-dependent; the count must still match."""
|
||||
r = H.snapshot(rroot)
|
||||
@@ -208,6 +218,18 @@ _CASES = [
|
||||
H.Case("chmod", "basic", ["-a", "--chmod=Fu+rwx"], compare_modes=True,
|
||||
ci=True, ref="--chmod"),
|
||||
|
||||
# --- delta / similar-file basis (--fuzzy) -----------------------------
|
||||
# Basis choices coincide here (same-suffix sibling, name distance one edit,
|
||||
# content identical); with the block size pinned both tools report the same
|
||||
# Matched/Literal/transferred counters. The residual (FastSync's narrower
|
||||
# delta size window) is covered by TestFuzzy in test_parity_quickwins.py.
|
||||
H.Case("fuzzy_basis", "fuzzy",
|
||||
["-a", "--no-whole-file", "--fuzzy", "--stats", "-B8192"],
|
||||
fastsync_flags=["-a", "--incremental", "--delta", "--fuzzy",
|
||||
"--stats", "--delta-block=8192"],
|
||||
seed=fuzzy_basis_seed, stdout=H.STDOUT_STATS, ci=True,
|
||||
ref="-y/--fuzzy similar-file basis"),
|
||||
|
||||
# --- deletion ---------------------------------------------------------
|
||||
H.Case("delete", "basic", ["-a", "--delete"], seed=seed_extras,
|
||||
server_args=DELETE, ci=True, ref="--delete"),
|
||||
|
||||
@@ -847,6 +847,92 @@ class TestVerifyAndFlip:
|
||||
"--verify-basis must reject the same-size/different-content basis"
|
||||
|
||||
|
||||
def _stat_bytes(output, key):
|
||||
"""Parse a --stats byte counter (e.g. ``Matched data: 65,536 bytes``)."""
|
||||
for line in output.splitlines():
|
||||
if line.startswith(key + ":"):
|
||||
raw = line.split(":", 1)[1].strip().split()[0]
|
||||
return int(raw.replace(",", ""))
|
||||
return None
|
||||
|
||||
|
||||
class TestFuzzy:
|
||||
"""Track 5b: `-y`/`--fuzzy` is an internal bandwidth optimization with a
|
||||
byte-exact result. FastSync ports rsync 3.4.1's weighted-Levenshtein name
|
||||
heuristic, so where both delta engines admit the candidate the tools pick
|
||||
the same basis (the ``fuzzy_basis`` differential asserts the tree and the
|
||||
Matched/Literal counters match with the block size pinned). The residual is
|
||||
candidate ELIGIBILITY: FastSync's delta size gate (both files >= 16 KiB and
|
||||
a <= 10x size ratio) is narrower than rsync's, which empirically uses a
|
||||
fuzzy basis well beyond 10x and below 16 KiB. These tests pin the window
|
||||
boundary and prove the byte-exact fallback on both sides of it."""
|
||||
|
||||
_BASE = b"the quick brown fox jumps over the lazy dog\n" * 4000
|
||||
|
||||
def _src(self, tag):
|
||||
source = os.path.join(TEST_DATA_DIR, f"fz_{tag}_src")
|
||||
clean_dir(source)
|
||||
return source
|
||||
|
||||
def _dst(self, tag):
|
||||
d = os.path.join(TEST_DATA_DIR, f"fz_{tag}_dst")
|
||||
clean_dir(d)
|
||||
return d
|
||||
|
||||
def _run_both(self, shared_server, source, dest, rdst, payload, sibling,
|
||||
rs_extra=(), fs_extra=()):
|
||||
with open(os.path.join(source, "report_v2.txt"), "wb") as fh:
|
||||
fh.write(payload)
|
||||
for root in (rdst, get_dest_received_dir(dest, source)):
|
||||
os.makedirs(root, exist_ok=True)
|
||||
with open(os.path.join(root, "report_v1.txt"), "wb") as fh:
|
||||
fh.write(sibling)
|
||||
rs = _rsync(["-a", "--no-whole-file", "--fuzzy", "--stats"] +
|
||||
list(rs_extra) + [source + "/", rdst + "/"])
|
||||
assert rs.returncode == 0, rs.stderr[:300]
|
||||
result, _ = run_client(
|
||||
source, dest,
|
||||
flags=["-a", "--incremental", "--delta", "--fuzzy", "--stats"] +
|
||||
list(fs_extra),
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, result.stderr[:300]
|
||||
_assert_same_tree(rdst, get_dest_received_dir(dest, source), "(--fuzzy)")
|
||||
return rs, result
|
||||
|
||||
@requires_rsync
|
||||
def test_fuzzy_above_size_window_declines_but_tree_exact(self, shared_server):
|
||||
"""A sibling >10x the source is used by rsync but declined by FastSync's
|
||||
delta size-ratio gate; both destinations stay byte-identical."""
|
||||
n = 65536
|
||||
payload = (self._BASE * ((n // len(self._BASE)) + 1))[:n]
|
||||
sibling = (self._BASE * 200)[: n * 20]
|
||||
source, dest, rdst = (self._src("big"), self._dst("big"),
|
||||
self._dst("big_r"))
|
||||
rs, result = self._run_both(shared_server, source, dest, rdst,
|
||||
payload, sibling)
|
||||
assert _stat_bytes(rs.stdout, "Matched data") > 0, \
|
||||
"rsync should still use a >10x fuzzy basis"
|
||||
assert _stat_bytes(result.stdout, "Matched data") == 0, \
|
||||
"FastSync's 10x delta size-ratio gate must decline the oversized basis"
|
||||
assert _stat_bytes(result.stdout, "Literal data") == n
|
||||
|
||||
@requires_rsync
|
||||
def test_fuzzy_below_delta_minimum_declines_but_tree_exact(self, shared_server):
|
||||
"""A sibling below the 16 KiB delta minimum is used by rsync but never
|
||||
enters FastSync's delta/fuzzy path; both trees stay byte-identical."""
|
||||
n = 8192
|
||||
payload = (self._BASE * ((n // len(self._BASE)) + 1))[:n]
|
||||
source, dest, rdst = (self._src("small"), self._dst("small"),
|
||||
self._dst("small_r"))
|
||||
rs, result = self._run_both(shared_server, source, dest, rdst,
|
||||
payload, payload)
|
||||
assert _stat_bytes(rs.stdout, "Matched data") > 0, \
|
||||
"rsync applies --fuzzy below 16 KiB"
|
||||
assert _stat_bytes(result.stdout, "Matched data") == 0, \
|
||||
"FastSync's 16 KiB delta minimum must bypass the fuzzy basis"
|
||||
assert _stat_bytes(result.stdout, "Literal data") == n
|
||||
|
||||
|
||||
class TestIgnoreExistingShortCircuit:
|
||||
"""#9: --ignore-existing is decided by the receiver during the per-file
|
||||
check, before the sender streams any payload. A large destination file that
|
||||
|
||||
Reference in New Issue
Block a user