Files
rsync/testsuite/prune-empty-dirs_test.py
Andrew Tridgell 0d546ee3b4 testsuite: structure / recursion / link coverage at depth
Cover the structure and link options at >=3 levels and across directories,
asserting each option's specific effect:

  links            -l keeps a symlink, -L dereferences it, -k follows a
                   directory symlink -- all on a symlink several levels deep.
  dirs             -d copies the top layer (file + empty dir) without recursing.
  prune-empty-dirs -m drops empty chains and chains emptied by an exclude,
                   keeps populated ones.
  hardlinks-deep   -H preserves a hard link whose names live in different
                   directories at depth; without -H they become separate inodes.
  delete-deep      --delete removes a deep extraneous file/subtree; the four
                   delete-timing variants agree; --max-delete caps deletions;
                   --existing / --ignore-existing select/skip correctly.
  relative-implied -R mirrors an implied directory's mode at depth;
                   --no-implied-dirs does not (proto 30+).

Green on master and under --protocol=29/30 (the --no-implied-dirs sub-case is
gated to protocol >= 30, where multi-component sender paths are accepted).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00

46 lines
1.6 KiB
Python

#!/usr/bin/env python3
"""Coverage of -m / --prune-empty-dirs at depth.
--prune-empty-dirs drops directory chains that would end up empty at the
destination -- both chains that are empty in the source and chains that become
empty because a filter excluded their only files. Populated chains are kept.
"""
from rsyncfns import (
FROMDIR, TODIR,
assert_not_exists, assert_same, makepath, rmtree, run_rsync,
)
src = FROMDIR
def reseed():
rmtree(src)
rmtree(TODIR)
# --- a deep empty chain is pruned; a deep populated chain is kept ------------
reseed()
makepath(src / 'empty' / 'e1' / 'e2', src / 'full' / 'd1' / 'd2')
(src / 'full' / 'd1' / 'd2' / 'file').write_text("data\n")
run_rsync('-a', '-m', f'{src}/', f'{TODIR}/')
assert_not_exists(TODIR / 'empty', label='-m pruned an empty chain')
assert_same(TODIR / 'full' / 'd1' / 'd2' / 'file',
src / 'full' / 'd1' / 'd2' / 'file', label='-m kept populated chain')
# --- a chain emptied by an exclude filter is also pruned --------------------
reseed()
makepath(src / 'mixed' / 'sub', src / 'onlylogs' / 'sub')
(src / 'mixed' / 'sub' / 'keep.txt').write_text("k\n")
(src / 'mixed' / 'sub' / 'drop.log').write_text("d\n")
(src / 'onlylogs' / 'sub' / 'a.log').write_text("a\n")
run_rsync('-a', '-m', '--exclude=*.log', f'{src}/', f'{TODIR}/')
assert_same(TODIR / 'mixed' / 'sub' / 'keep.txt',
src / 'mixed' / 'sub' / 'keep.txt', label='-m kept non-empty dir')
assert_not_exists(TODIR / 'onlylogs',
label='-m pruned a dir emptied by an exclude')
print("prune-empty-dirs: empty and filter-emptied chains pruned, populated kept")