- Recursive scans emit a directory entry for every traversed directory that produced no transferred child, so empty source dirs (and dirs emptied by filtering) are recreated like rsync; -m prunes them, --files-from/--list-only never emit implicit dirs. - A directory entry now replaces a destination regular file (rsync removes the non-directory) instead of aborting; confined to the secure parent fd. - -R --no-implied-dirs --files-from: stop refusing a listed file whose parent is not listed; create the implied parent with default attributes (no source metadata is captured for it), matching rsync 3.4.1. - Differential gate: drop min_size/empty_dirs_recursive/dirs_plain allowlist entries (dirs_plain now hands FastSync the same trailing-slash source as rsync); add differential test for the files-from implied parent. - Docs: -d row -> Parity, tally 108/24/24. No wire change (PROTOCOL_VERSION stays 2.26.0).
47 lines
1.9 KiB
Python
47 lines
1.9 KiB
Python
"""Data-driven allowlist for the differential rsync-parity gate.
|
|
|
|
Every entry maps a case id (see ``test_differential_parity.py``) to the aspects
|
|
that are *known* to differ from ``rsync 3.4.1`` and the documented reason. A
|
|
differential mismatch in an aspect that is **not** listed here fails the gate.
|
|
|
|
Aspect keys
|
|
-----------
|
|
``tree`` destination tree differs (paths, file hashes, symlink targets,
|
|
modes, hardlink grouping)
|
|
``stdout`` normalized output for ``-i`` / ``--stats`` / ``--out-format``
|
|
``extra`` a case-specific assertion differs (basis/inode checks, ...)
|
|
``rc`` exit status differs
|
|
|
|
Burn-down
|
|
---------
|
|
If a case is listed here but now matches rsync, the gate emits a loud
|
|
``pytest`` warning naming the stale entry: delete the entry (and, when the
|
|
underlying row in ``RSYNC_COMPAT.md`` is now parity, update that row). Set
|
|
``FASTSYNC_PARITY_STRICT=1`` to turn stale entries into failures in CI.
|
|
|
|
Keep the values concise but cite the governing row so the entry can be
|
|
re-triaged when the row moves.
|
|
"""
|
|
|
|
# case id -> {aspect: "reason (ref: RSYNC_COMPAT.md ...)"}
|
|
CAVEATS = {
|
|
# --max-delete stops the extras walk part-way and exits 25 in both
|
|
# implementations; which of the remaining extras survives depends on
|
|
# deletion order, which neither tool specifies. The exit code and the
|
|
# number of survivors match (asserted implicitly by the harness's rc
|
|
# comparison and the one-for-one diff below).
|
|
"max_delete": {
|
|
"tree": "which destination extras survive a partial --max-delete abort "
|
|
"is deletion-order dependent and unspecified; rc=25 and the "
|
|
"number of survivors match rsync. ref: RSYNC_COMPAT.md "
|
|
"`--max-delete=NUM` row.",
|
|
},
|
|
}
|
|
|
|
# Accepted aspect names (guards against typos in this file).
|
|
ASPECTS = ("tree", "stdout", "extra", "rc")
|
|
|
|
|
|
def caveat_for(case_id: str) -> dict:
|
|
return CAVEATS.get(case_id, {})
|