testsuite: filtering coverage at depth

Assert exactly which entries are/aren't transferred, deep in the tree:

  filter-depth      --exclude/--include precedence on files at every level, and
                    a -F per-directory .rsync-filter loaded from a deep dir that
                    applies to that subtree only (not above it).
  cvs-exclude       -C built-in cruft patterns (*.o, *~) at every level plus a
                    deep per-directory .cvsignore scoped to its subtree.
  size-filter       --max-size / --min-size select the right files all the way
                    down.
  files-from-depth  --files-from selects only the listed deep paths (implied
                    parents created); --from0 NUL-delimited; --exclude-from /
                    --include-from filter at depth.

(--existing / --ignore-existing are covered in delete-deep_test.py.)
Green on master and under --protocol=29/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrew Tridgell
2026-05-24 07:55:45 +10:00
parent 273b9f265f
commit 922681e140
4 changed files with 235 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Coverage of -C / --cvs-exclude at depth.
-C ignores the usual CVS cruft (object files, core, editor backups, VCS dirs,
...) and also honours a per-directory .cvsignore. Verify both the built-in
patterns and a deep .cvsignore on a >=3-level tree.
"""
from rsyncfns import (
FROMDIR, TODIR,
assert_exists, assert_not_exists, makepath, rmtree, run_rsync,
)
src = FROMDIR
rmtree(src)
rmtree(TODIR)
makepath(src / 'd1' / 'd2' / 'd3')
# A real file plus default-CVS-cruft at every level.
cur = src
for lvl in range(4):
(cur / f'real{lvl}.c').write_text('code\n')
(cur / f'obj{lvl}.o').write_text('obj\n') # *.o is built-in cruft
(cur / f'back{lvl}~').write_text('backup\n') # *~ is built-in cruft
cur = cur / f'd{lvl + 1}'
# A per-directory .cvsignore deep in the tree adds "*.junk" for that subtree.
(src / 'd1' / 'd2' / '.cvsignore').write_text('*.junk\n')
(src / 'd1' / 'd2' / 'local.junk').write_text('j\n')
(src / 'top.junk').write_text('j\n') # not covered by that .cvsignore
run_rsync('-aC', f'{src}/', f'{TODIR}/')
cur = TODIR
for lvl in range(4):
assert_exists(cur / f'real{lvl}.c', label=f'-C kept real L{lvl}')
assert_not_exists(cur / f'obj{lvl}.o', label=f'-C dropped *.o L{lvl}')
assert_not_exists(cur / f'back{lvl}~', label=f'-C dropped *~ L{lvl}')
cur = cur / f'd{lvl + 1}'
# .cvsignore is scoped to its directory subtree.
assert_not_exists(TODIR / 'd1' / 'd2' / 'local.junk',
label='-C deep .cvsignore applied')
assert_exists(TODIR / 'top.junk', label='-C deep .cvsignore not applied above')
print("cvs-exclude: built-in patterns + deep .cvsignore honoured at depth")

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Coverage of --files-from, -0/--from0, --exclude-from, --include-from at depth.
--files-from selects exactly the listed source-relative paths (creating their
implied parent dirs); --from0 makes the list NUL-delimited; --exclude-from /
--include-from read filter patterns from a file. All resolve names several
levels deep.
"""
from rsyncfns import (
FROMDIR, SCRATCHDIR, TODIR,
assert_exists, assert_not_exists, assert_same, make_tree, rmtree,
run_rsync,
)
src = FROMDIR
listed = ['d1/f1', 'd1/d2/d3/f3']
unlisted = ['f0', 'd1/d2/f2']
def seed():
rmtree(src)
rmtree(TODIR)
make_tree(src, depth=3)
# --- --files-from selects only the listed deep paths ------------------------
seed()
lf = SCRATCHDIR / 'files.lst'
lf.write_text('\n'.join(listed) + '\n')
run_rsync('-a', f'--files-from={lf}', f'{src}/', f'{TODIR}/')
for rel in listed:
assert_same(TODIR / rel, src / rel, label=f'--files-from {rel}')
for rel in unlisted:
assert_not_exists(TODIR / rel, label=f'--files-from excluded {rel}')
# --- --from0: the same list, NUL-delimited ----------------------------------
rmtree(TODIR)
lf0 = SCRATCHDIR / 'files0.lst'
lf0.write_bytes(b'\0'.join(p.encode() for p in listed) + b'\0')
run_rsync('-a', '--from0', f'--files-from={lf0}', f'{src}/', f'{TODIR}/')
for rel in listed:
assert_same(TODIR / rel, src / rel, label=f'--from0 {rel}')
for rel in unlisted:
assert_not_exists(TODIR / rel, label=f'--from0 excluded {rel}')
# --- --exclude-from drops matching files at depth ---------------------------
seed()
(src / 'a.skip').write_text('s\n')
(src / 'd1' / 'd2' / 'a.skip').write_text('s\n')
ef = SCRATCHDIR / 'excl.lst'
ef.write_text('*.skip\n')
run_rsync('-a', f'--exclude-from={ef}', f'{src}/', f'{TODIR}/')
assert_not_exists(TODIR / 'a.skip', label='--exclude-from top')
assert_not_exists(TODIR / 'd1' / 'd2' / 'a.skip', label='--exclude-from deep')
assert_same(TODIR / 'd1' / 'd2' / 'f2', src / 'd1' / 'd2' / 'f2',
label='--exclude-from kept others')
# --- --include-from keeps only matching files at depth ----------------------
seed()
(src / 'd1' / 'd2' / 'k.keepme').write_text('k\n')
inc = SCRATCHDIR / 'inc.lst'
inc.write_text('*/\n*.keepme\n')
run_rsync('-a', f'--include-from={inc}', '--exclude=*', f'{src}/', f'{TODIR}/')
assert_exists(TODIR / 'd1' / 'd2' / 'k.keepme', label='--include-from kept')
assert_not_exists(TODIR / 'd1' / 'd2' / 'f2', label='--include-from excluded rest')
print("files-from-depth: --files-from/--from0/--exclude-from/--include-from at depth")

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Coverage of --exclude / --include / --filter / -F at depth.
The interesting case for the resolver restructure is a per-directory merge file
(-F reads .rsync-filter from each directory as it descends): the rule set is
loaded from a file several levels deep and must apply to that directory and
below, but not above. Also check plain --exclude / --include precedence on
files spread through the tree.
"""
import os
from rsyncfns import (
FROMDIR, TODIR,
assert_exists, assert_not_exists, assert_same, makepath, rmtree, run_rsync,
)
src = FROMDIR
def seed_ext():
"""A tree with a .log and a .txt at every level."""
rmtree(src)
rmtree(TODIR)
cur = src
for lvl in range(4):
cur.mkdir(parents=True, exist_ok=True)
(cur / f'keep{lvl}.txt').write_text(f'txt {lvl}\n')
(cur / f'drop{lvl}.log').write_text(f'log {lvl}\n')
cur = cur / f'd{lvl + 1}'
# --- --exclude drops matching files at every level --------------------------
seed_ext()
run_rsync('-a', '--exclude=*.log', f'{src}/', f'{TODIR}/')
cur = TODIR
for lvl in range(4):
assert_exists(cur / f'keep{lvl}.txt', label=f'--exclude kept txt L{lvl}')
assert_not_exists(cur / f'drop{lvl}.log', label=f'--exclude dropped log L{lvl}')
cur = cur / f'd{lvl + 1}'
# --- --include before --exclude='*' keeps only .txt at every level ----------
seed_ext()
run_rsync('-a', '--include=*/', '--include=*.txt', '--exclude=*',
f'{src}/', f'{TODIR}/')
cur = TODIR
for lvl in range(4):
assert_exists(cur / f'keep{lvl}.txt', label=f'--include txt L{lvl}')
assert_not_exists(cur / f'drop{lvl}.log', label=f'--include excluded log L{lvl}')
cur = cur / f'd{lvl + 1}'
# --- -F per-directory merge file loaded from a deep directory ---------------
# .rsync-filter at d1/d2 excludes "secret*" for d1/d2 and below only.
rmtree(src)
rmtree(TODIR)
makepath(src / 'd1' / 'd2' / 'd3')
for rel in ('secret.top', 'd1/secret.mid', 'd1/d2/secret.deep',
'd1/d2/d3/secret.deeper'):
(src / rel).write_text('x\n')
(src / 'd1' / 'd2' / '.rsync-filter').write_text('- secret*\n')
run_rsync('-aF', f'{src}/', f'{TODIR}/')
# Above the merge file: not affected.
assert_exists(TODIR / 'secret.top', label='-F above merge dir')
assert_exists(TODIR / 'd1' / 'secret.mid', label='-F above merge dir')
# At and below the merge file: excluded.
assert_not_exists(TODIR / 'd1' / 'd2' / 'secret.deep', label='-F at merge dir')
assert_not_exists(TODIR / 'd1' / 'd2' / 'd3' / 'secret.deeper',
label='-F below merge dir')
print("filter-depth: --exclude/--include precedence and -F per-dir merge at depth")

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Coverage of --max-size / --min-size at depth.
A small and a large file at every level; --max-size must transfer only the
small ones and --min-size only the large ones, the selection holding all the
way down the tree.
"""
import os
from rsyncfns import (
FROMDIR, TODIR,
assert_exists, assert_not_exists, make_data_file, rmtree, run_rsync,
)
src = FROMDIR
SMALL = 500
LARGE = 5000
def seed():
rmtree(src)
rmtree(TODIR)
cur = src
for lvl in range(4):
cur.mkdir(parents=True, exist_ok=True)
make_data_file(cur / f'small{lvl}', SMALL)
make_data_file(cur / f'large{lvl}', LARGE)
cur = cur / f'd{lvl + 1}'
# --- --max-size keeps only the small files at every level -------------------
seed()
run_rsync('-a', '--max-size=1000', f'{src}/', f'{TODIR}/')
cur = TODIR
for lvl in range(4):
assert_exists(cur / f'small{lvl}', label=f'--max-size kept small L{lvl}')
assert_not_exists(cur / f'large{lvl}', label=f'--max-size dropped large L{lvl}')
cur = cur / f'd{lvl + 1}'
# --- --min-size keeps only the large files at every level -------------------
seed()
run_rsync('-a', '--min-size=1000', f'{src}/', f'{TODIR}/')
cur = TODIR
for lvl in range(4):
assert_exists(cur / f'large{lvl}', label=f'--min-size kept large L{lvl}')
assert_not_exists(cur / f'small{lvl}', label=f'--min-size dropped small L{lvl}')
cur = cur / f'd{lvl + 1}'
print("size-filter: --max-size / --min-size select correctly at depth")