- cli: remove UB in --bwlimit scaling (range-check the double product before casting, drop atoi for the +/-1 form) and add huge/boundary unit tests - test: widen the CI throttle wall-clock band to [1.5, 4.5]s with a 2s cross-tolerance so a loaded runner cannot flake it - log: drop the unused LOG_INFO_BACKUP bit; --info=backup is accepted-but- silent like the other rsync-only categories - client_send: remove the duplicate delete_display_path forward declaration - utils: add non-allocating utils_strip_transfer_root and use it from scanner_note_nonreg and delete_display_path (was duplicated logic) - scanner: lstat() instead of stat() when re-reading an empty dir's metadata - file: drop the no-op else-if and the redundant ELOOP arm in file_ensure_directory_secure (symlinks are refused anyway) - format/stats: document literal_data as whole-file accurate (delta upper bound) instead of claiming literal bytes sent - docs: refresh stale protocol 2.26.0 labels to 2.27.0
Integration tests
The integration suite drives the built build/server and build/client
against local corpora. Unit tests live in tests/ (the custom C framework);
the Python suite here covers the full transfer pipeline, transports, features,
and rsync parity.
Running
# Full suite (excludes privilege-dependent tests on CI runners)
python3 -m pytest tests/integration/ -n 4 --dist=load -m "not setpriv"
# Fast PR subset only
python3 -m pytest tests/integration/ -n 4 --dist=load -m ci
The tests expect build/server and build/client (configure/build with CMake
first); common.py derives BUILD_DIR from the repository root.
Differential rsync-parity gate
test_differential_parity.py runs the same transfer with real
rsync 3.4.1 and with FastSync over separate destinations, then compares:
- the destination trees — relative paths, file content hashes, symlink targets, modes (where the case is about perms), and hard-link grouping;
- the normalized stdout for output-oriented flags (
-i,--out-format=...,--stats), after stripping volatile fields (timings, rates, wire byte counts) and directory-only itemize lines that FastSync's recursive scanner documents as absent.
FastSync mirrors the absolute source path under its receive root (see
get_dest_received_dir); the harness normalizes that layout (and the
-R/--files-from layouts) before comparing.
# Fast subset that guards the ✅ surface on pull requests
python3 -m pytest tests/integration/test_differential_parity.py -n 4 --dist=load -m parity_ci
# Full set (all ✅ cases plus the documented ⚠️/❌ residuals)
python3 -m pytest tests/integration/test_differential_parity.py -n 4 --dist=load -m parity
The suite skips cleanly when rsync is not installed.
Allowlist (parity_caveats.py)
parity_caveats.py is the single data-driven allowlist of known differences.
Each entry maps a case id to the aspects that may differ (tree, stdout,
extra, rc) and cites the governing row in RSYNC_COMPAT.md:
CAVEATS = {
"min_size": {
"tree": "recursive transfer does not create a source directory that "
"becomes empty after --min-size filtering. ref: RSYNC_COMPAT.md "
"`-d/--dirs` row and completion-wave residual.",
},
}
A differential mismatch in an aspect that is not listed fails the gate with a readable tree/stdout diff.
If a case is allowlisted but now matches rsync, the gate emits a loud warning
naming the stale entry — that is the parity burn-down signal. Run with
FASTSYNC_PARITY_STRICT=1 to make stale entries fail instead (the full CI
parity job sets this). To add a residual:
- Reproduce it with
-m parityand read the failure's tree/stdout diff. - Confirm it is a documented
⚠️/❌residual (or get the✅row reclassified) and cite the row. - Add the case id and aspect(s) to
CAVEATS, keeping the reason concise.
Do not allowlist an undocumented divergence from a ✅ row — fix it or get the
row reclassified first.