test: review follow-up coverage for --fuzzy
CI / lint (pull_request) Failing after 37s
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 / lint (pull_request) Failing after 37s
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
Unit (CLI): --fuzzy --no-incremental (either order) stays a valid plain-mode config -- no forced handshake, no delta implication; --fuzzy --no-delta stays covered. Integration (TestFuzzy): - worthless basis: a sibling that passes the name+size gates but shares no blocks makes the sender reply STATUS_NEXT; the whole file is consumed inside the delta handshake with byte-exact output (no protocol desync). - non-displacement: an existing exact-path destination file INSIDE the delta size bounds (same size, different content, older mtime) is used as the delta basis instead of a byte-identical similar sibling (whole-file wire cost). - asymmetric bases: basis larger than source (prefix reuse) and basis smaller than source (appended tail as literals) both reconstruct byte-exactly with a small delta. - --no-fuzzy end-to-end equals the no-flag whole-file behavior. CountingProxy closes its listener socket (fd hygiene).
This commit is contained in:
@@ -106,6 +106,7 @@ class CountingProxy:
|
||||
server_sock = socket.create_connection(("127.0.0.1", self.target_port),
|
||||
timeout=10)
|
||||
except OSError:
|
||||
self._listener.close()
|
||||
return
|
||||
c2s, s2c = [0], [0]
|
||||
a = threading.Thread(target=self._pump, args=(client_sock, server_sock, c2s))
|
||||
@@ -116,6 +117,7 @@ class CountingProxy:
|
||||
b.join()
|
||||
self.client_to_server = c2s[0]
|
||||
self.server_to_client = s2c[0]
|
||||
self._listener.close()
|
||||
|
||||
thread = threading.Thread(target=serve)
|
||||
thread.start()
|
||||
|
||||
@@ -2513,6 +2513,8 @@ class TestFuzzy:
|
||||
|
||||
OLD_NAME = "report-2025.dat"
|
||||
NEW_NAME = "report-2026.dat"
|
||||
TS = 1577836800 # 2020-01-01, used to pin stale destination mtimes
|
||||
SIZE = 2 * 1024 * 1024
|
||||
|
||||
def _client_via_proxy(self, source, dest, flags, proxy):
|
||||
cmd = (CLIENT_CMD + ["--source-dir", source, "--dest-dir", dest,
|
||||
@@ -2721,3 +2723,112 @@ class TestFuzzy:
|
||||
assert _read_file(os.path.join(received, self.NEW_NAME)) == new_bytes
|
||||
assert _read_file(os.path.join(received, self.OLD_NAME)) == old_bytes
|
||||
|
||||
@staticmethod
|
||||
def _rand_bytes(size, seed):
|
||||
return random.Random(seed).randbytes(size)
|
||||
|
||||
def _replace_source_file(self, source, old_name, new_name, new_bytes):
|
||||
"""Remove old_name from source and add new_name with new_bytes."""
|
||||
os.unlink(os.path.join(source, old_name))
|
||||
with open(os.path.join(source, new_name), "wb") as fh:
|
||||
fh.write(new_bytes)
|
||||
|
||||
def test_worthless_fuzzy_basis_falls_back_inside_handshake(self, shared_server):
|
||||
# The sibling passes the name AND size gates but shares no blocks with
|
||||
# the incoming file, so the sender's delta is not worthwhile: it replies
|
||||
# STATUS_NEXT and the receiver consumes the WHOLE file inside the delta
|
||||
# handshake. This proves a bad fuzzy basis cannot desync the protocol
|
||||
# or corrupt the result.
|
||||
source, dest = self._prepare("worthless")
|
||||
basis = self._rand_bytes(self.SIZE, 424242)
|
||||
target = self._rand_bytes(self.SIZE, 777777)
|
||||
self._seed_dest(source, dest, {self.OLD_NAME: basis}, shared_server.port)
|
||||
self._replace_source_file(source, self.OLD_NAME, self.NEW_NAME, target)
|
||||
result, proxy = self._run_measured(source, dest, ["--fuzzy"], shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fuzzy worthless-basis run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert _read_file(os.path.join(received, self.NEW_NAME)) == target, \
|
||||
"whole-file fallback after a worthless fuzzy basis is not byte-exact"
|
||||
assert proxy.client_to_server > self.SIZE // 2, \
|
||||
"a worthless basis should have made the sender fall back to the whole file"
|
||||
|
||||
def test_existing_dest_file_preferred_over_fuzzy_sibling(self, shared_server):
|
||||
# Non-displacement: the destination holds a file at the exact path that
|
||||
# is inside the delta size bounds (same size, different content, older
|
||||
# mtime). FastSync must delta against THAT file -- even though it
|
||||
# shares nothing with the source -- and must NOT reuse a similar-named
|
||||
# sibling that is byte-identical to the source.
|
||||
source, dest = self._prepare("nondisp")
|
||||
sibling = self._rand_bytes(self.SIZE, 111) # will equal the incoming file
|
||||
stale = self._rand_bytes(self.SIZE, 333) # worthless exact-path file
|
||||
self._seed_dest(source, dest,
|
||||
{self.OLD_NAME: sibling, self.NEW_NAME: stale},
|
||||
shared_server.port)
|
||||
# Force the exact-path destination file's mtime into the past so the
|
||||
# quick check deterministically decides to transfer it.
|
||||
os.utime(os.path.join(get_dest_received_dir(dest, source), self.NEW_NAME),
|
||||
(self.TS, self.TS))
|
||||
os.unlink(os.path.join(source, self.OLD_NAME))
|
||||
with open(os.path.join(source, self.NEW_NAME), "wb") as fh:
|
||||
fh.write(sibling)
|
||||
result, proxy = self._run_measured(source, dest, ["--fuzzy"], shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fuzzy non-displacement run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert _read_file(os.path.join(received, self.NEW_NAME)) == sibling
|
||||
assert proxy.client_to_server > self.SIZE // 2, \
|
||||
"the exact-path destination file must be the delta basis, not the fuzzy sibling"
|
||||
|
||||
def test_fuzzy_basis_larger_than_source(self, shared_server):
|
||||
# The similar sibling is LARGER than the incoming file (within the delta
|
||||
# engine's 10x ratio); the new file is an exact prefix of the basis, so
|
||||
# every block matches and only a tiny delta travels.
|
||||
source, dest = self._prepare("largerbasis")
|
||||
big = self._rand_bytes(1536 * 1024, 1)
|
||||
prefix = big[:1024 * 1024]
|
||||
self._seed_dest(source, dest, {self.OLD_NAME: big}, shared_server.port)
|
||||
self._replace_source_file(source, self.OLD_NAME, self.NEW_NAME, prefix)
|
||||
result, proxy = self._run_measured(source, dest, ["--fuzzy"], shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fuzzy larger-basis run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert _read_file(os.path.join(received, self.NEW_NAME)) == prefix, \
|
||||
"shrunken file reconstructed from a larger fuzzy basis is not byte-exact"
|
||||
assert proxy.client_to_server < len(prefix) // 4, \
|
||||
"larger fuzzy basis should have carried most of the file as block matches"
|
||||
|
||||
def test_fuzzy_basis_smaller_than_source(self, shared_server):
|
||||
# The similar sibling is SMALLER than the incoming file; the new file
|
||||
# appends data past the basis, so the appended tail travels as literals
|
||||
# while the shared prefix is block-matched.
|
||||
source, dest = self._prepare("smallerbasis")
|
||||
base = self._rand_bytes(self.SIZE, 2)
|
||||
tail = self._rand_bytes(64 * 1024, 3)
|
||||
new_bytes = base + tail
|
||||
self._seed_dest(source, dest, {self.OLD_NAME: base}, shared_server.port)
|
||||
self._replace_source_file(source, self.OLD_NAME, self.NEW_NAME, new_bytes)
|
||||
result, proxy = self._run_measured(source, dest, ["--fuzzy"], shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fuzzy smaller-basis run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert _read_file(os.path.join(received, self.NEW_NAME)) == new_bytes, \
|
||||
"grown file reconstructed from a smaller fuzzy basis is not byte-exact"
|
||||
assert proxy.client_to_server < len(new_bytes) // 4, \
|
||||
"smaller fuzzy basis should have block-matched the shared prefix"
|
||||
|
||||
def test_no_fuzzy_end_to_end_equals_no_flag(self, shared_server):
|
||||
# --no-fuzzy must not enable anything: a run with it behaves exactly
|
||||
# like a run without it (whole-file transfer, byte-exact output).
|
||||
source, dest = self._prepare("nofuzzye2e")
|
||||
old_bytes, new_bytes = _random_payloads()
|
||||
self._seed_dest(source, dest, {self.OLD_NAME: old_bytes}, shared_server.port)
|
||||
self._replace_source_file(source, self.OLD_NAME, self.NEW_NAME, new_bytes)
|
||||
result, proxy = self._run_measured(source, dest, ["--no-fuzzy"], shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--no-fuzzy run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert _read_file(os.path.join(received, self.NEW_NAME)) == new_bytes
|
||||
assert proxy.client_to_server > len(new_bytes) // 2, \
|
||||
"--no-fuzzy should leave the default whole-file behavior intact"
|
||||
|
||||
|
||||
@@ -1246,6 +1246,32 @@ static void test_parse_args_fuzzy_respects_no_delta() {
|
||||
}
|
||||
}
|
||||
|
||||
/* An explicit --no-incremental is respected by the --fuzzy implication in
|
||||
* either argument order (unlike the basis-dir options, --fuzzy does not force
|
||||
* the incremental handshake back on). Because delta needs the handshake, the
|
||||
* delta implication is suppressed too, so the run is a plain (default-mode)
|
||||
* transfer rather than an invalid "--delta requires --incremental" config. */
|
||||
static void test_parse_args_fuzzy_respects_no_incremental() {
|
||||
static const char* const combos[][2] = {
|
||||
{"--fuzzy", "--no-incremental"},
|
||||
{"--no-incremental", "--fuzzy"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(combos) / sizeof(combos[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", (char*)combos[i][0], (char*)combos[i][1], "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->fuzzy);
|
||||
EXPECT_FALSE(cfg->use_incremental);
|
||||
EXPECT_FALSE(cfg->use_delta);
|
||||
cfg->send_directory = str_dup("/src");
|
||||
cfg->receive_root_directory = str_dup("/dst");
|
||||
EXPECT_TRUE(validate_config(cfg));
|
||||
config_delete(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
/* --fuzzy requires the delta machinery, which the chunk-serialization (-s) and
|
||||
* sendfile (-f) modes reject -- mirroring the --delta constraint checks. */
|
||||
static void test_validate_config_fuzzy_incompatible_modes() {
|
||||
@@ -1831,6 +1857,7 @@ void test_client_cli() {
|
||||
test_parse_args_fuzzy_negation();
|
||||
test_parse_args_fuzzy_with_whole_file();
|
||||
test_parse_args_fuzzy_respects_no_delta();
|
||||
test_parse_args_fuzzy_respects_no_incremental();
|
||||
test_validate_config_fuzzy_incompatible_modes();
|
||||
test_parse_args_one_file_system();
|
||||
test_parse_args_compression_aliases();
|
||||
|
||||
Reference in New Issue
Block a user