CI / lint (pull_request) Successful in 1m42s
CI / parity-full (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 / parity-fast (pull_request) Successful in 19s
CI / build-and-test (pull_request) Successful in 48s
Add tests/integration/test_differential_parity.py plus a shared parity_harness.py and a data-driven parity_caveats.py allowlist. The gate runs real rsync 3.4.1 and FastSync over the same corpora, compares the destination trees (paths, hashes, symlink targets, modes, hard-link grouping) and the normalized -i/--stats/--out-format output, and fails on any difference not listed in the allowlist. Stale allowlist entries warn (or fail under FASTSYNC_PARITY_STRICT=1) so the residual list shrinks. Register parity/parity_ci markers and wire a fast PR job (parity_ci) plus a push-only full job (parity, strict) into .gitea/workflows/ci.yaml. Document the gate and the allowlist workflow in tests/integration/README.md.
81 lines
3.9 KiB
Python
81 lines
3.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 = {
|
|
# A source subtree whose only files are all filtered out (here, by
|
|
# --min-size) is left behind as an empty directory by rsync but not by
|
|
# FastSync: the recursive scanner keeps directory entries record-only, so
|
|
# `sub/deep` is only implicit through the (skipped) file. This is the
|
|
# documented recursive-empty-directory residual, not a payload/selection
|
|
# bug.
|
|
"min_size": {
|
|
"tree": "recursive transfer does not create a source directory that "
|
|
"becomes empty after --min-size filtering (FastSync directory "
|
|
"entries are record-only). ref: RSYNC_COMPAT.md `-d/--dirs` "
|
|
"row and completion-wave residual ('recursive transfers still "
|
|
"do not create empty directories').",
|
|
},
|
|
# FastSync's recursive scanner keeps directory entries record-only, so a
|
|
# plain `-d`/recursive source whose only role for a directory is that entry
|
|
# (empty dir, or a dir emptied by filtering) is not created on the
|
|
# destination. rsync creates it. `--dirs`/`--files-from`-listed
|
|
# directories DO cross as explicit entries (covered by the passing
|
|
# `empty_dirs_files_from` / `files_from` cases).
|
|
"empty_dirs_recursive": {
|
|
"tree": "recursive transfer does not create empty source directories. "
|
|
"ref: RSYNC_COMPAT.md `-d/--dirs` row and completion-wave "
|
|
"residual ('recursive transfers still do not create empty "
|
|
"directories').",
|
|
},
|
|
# A plain `-d` invocation: FastSync's `--source-dir` treats the argument as
|
|
# the directory entry itself (creates the empty source-root mirror), while
|
|
# rsync's `src/` trailing-slash form lists the immediate contents.
|
|
"dirs_plain": {
|
|
"tree": "plain -d semantics: FastSync creates the source-root directory "
|
|
"entry (its documented --dirs files-from behavior) instead of "
|
|
"rsync's one-level contents listing for a `src/` argument. "
|
|
"ref: RSYNC_COMPAT.md `-d/--dirs` row (⚠️).",
|
|
},
|
|
# --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, {})
|