mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-06-08 06:05:57 -04:00
Let the suite run with two rsync binaries so the current build can be
tested against the actual old code of a previous release, rather than
only forcing the current binary to speak an old protocol (check29/30).
--rsync-bin2 PATH exports RSYNC_PEER, the binary used for the SERVER
side of two-sided transfers (the daemon process and
the remote-shell --rsync-path target). Defaults to
RSYNC, so single-binary runs are byte-for-byte
unchanged.
--expect-result F the manifest's listed tests ARE the run set; each
test's actual outcome (pass/skip/fail/xfail) is
compared to its expected one and any mismatch --
including an unexpected pass (xpass) -- fails the
run. --expect-skipped and the default exit logic
are untouched.
rsyncfns gains the RSYNC_PEER global and launches the daemon with it
(start_rsyncd / start_test_daemon, the latter with an optional rsync_cmd
override used by the reverse-direction test); the remote-shell tests
pass --rsync-path={RSYNC_PEER}. All no-ops when no peer is selected.
Direction is fixed: the current binary always drives (only it
understands the new test scripts); the old binary is only ever the
server/daemon side.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
322 lines
12 KiB
Python
322 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
# Python rewrite of testsuite/exclude.test (and, via a Makefile-built
|
|
# symlink, of exclude-lsh.test).
|
|
#
|
|
# Test rsync's exclude / include / filter rules, including the more
|
|
# obscure wildcard cases, per-directory filter files, CVS-style
|
|
# exclusions, --prune-empty-dirs, --delete-during / --delete-before /
|
|
# --delete-excluded, and rule-restricted filter files.
|
|
#
|
|
# The lsh.sh "remote shell" variant runs every transfer through the
|
|
# local rsync-over-ssh stand-in -- detected via sys.argv[0].
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
from rsyncfns import (
|
|
CHKDIR, FROMDIR, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,
|
|
all_plus, allspace, dots,
|
|
checkdiff, checkit, makepath, rsync_argv, run_rsync, test_fail,
|
|
)
|
|
|
|
|
|
os.environ['CVSIGNORE'] = '*.junk'
|
|
|
|
script_name = os.path.basename(sys.argv[0] if sys.argv[0] else __file__)
|
|
if 'lsh' in script_name:
|
|
os.environ['RSYNC_RSH'] = str(SRCDIR / 'support' / 'lsh.sh')
|
|
rpath = [f'--rsync-path={RSYNC_PEER}']
|
|
host = 'lh:'
|
|
else:
|
|
rpath = []
|
|
host = ''
|
|
|
|
|
|
# Build the from/ tree.
|
|
makepath(
|
|
FROMDIR / 'foo/down/to/you',
|
|
FROMDIR / 'foo/sub',
|
|
FROMDIR / 'bar/down/to/foo/too',
|
|
FROMDIR / 'bar/down/to/bar/baz',
|
|
FROMDIR / 'mid/for/foo/and/that/is/who',
|
|
FROMDIR / 'new/keep/this',
|
|
FROMDIR / 'new/lose/this',
|
|
)
|
|
|
|
(FROMDIR / '.filt').write_text(
|
|
"exclude down\n"
|
|
": .filt-temp\n"
|
|
"clear\n"
|
|
"- .filt\n"
|
|
"- *.bak\n"
|
|
"- *.old\n"
|
|
)
|
|
(FROMDIR / 'foo' / 'file1').write_text("filtered-1\n")
|
|
(FROMDIR / 'foo' / 'file2').write_text("removed\n")
|
|
(FROMDIR / 'foo' / 'file2.old').write_text("cvsout\n")
|
|
(FROMDIR / 'foo' / '.filt').write_text("include .filt\n- /file1\n")
|
|
(FROMDIR / 'foo' / 'sub' / 'file1').write_text("not-filtered-1\n")
|
|
|
|
(FROMDIR / 'bar' / '.filt').write_text(
|
|
"- home-cvs-exclude\n"
|
|
"dir-merge .filt2\n"
|
|
"+ to\n"
|
|
)
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'home-cvs-exclude').write_text("cvsout\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / '.filt2').write_text("- .filt2\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / '.filt2').write_text("+ *.junk\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file1').write_text("keeper\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file1.bak').write_text("cvsout\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file3').write_text("gone\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file4').write_text("lost\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / '+ file3').write_text("weird\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file4.junk').write_text("cvsout-but-filtin\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'to').write_text("smashed\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'bar' / '.filt2').write_text("- *.deep\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'bar' / 'baz' / 'file5.deep').write_text("filtout\n")
|
|
|
|
# This one should be ineffectual.
|
|
(FROMDIR / 'mid' / '.filt2').write_text("- extra\n")
|
|
(FROMDIR / 'mid' / 'one-in-one-out').write_text("cvsout\n")
|
|
(FROMDIR / 'mid' / '.cvsignore').write_text("one-in-one-out\n")
|
|
(FROMDIR / 'mid' / 'one-for-all').write_text("cvsin\n")
|
|
(FROMDIR / 'mid' / '.filt').write_text(":C\n")
|
|
(FROMDIR / 'mid' / 'for' / 'one-in-one-out').write_text("cvsin\n")
|
|
(FROMDIR / 'mid' / 'for' / 'foo' / 'extra').write_text("expunged\n")
|
|
(FROMDIR / 'mid' / 'for' / 'foo' / 'keep').write_text("retained\n")
|
|
|
|
|
|
# Setup our test exclude/include files.
|
|
excl = SCRATCHDIR / 'exclude-from'
|
|
excl.write_text(
|
|
"!\n"
|
|
"# If the second line of these two lines does anything, it's a bug.\n"
|
|
"+ **/bar\n"
|
|
"- /bar\n"
|
|
"# This should match against the whole path, not just the name.\n"
|
|
"+ foo**too\n"
|
|
"# These should float at the end of the path.\n"
|
|
"+ foo/s?b/\n"
|
|
"- foo/*/\n"
|
|
"# Test how /** differs from /***\n"
|
|
"- new/keep/**\n"
|
|
"- new/lose/***\n"
|
|
"# Test some normal excludes. Competing lines are paired.\n"
|
|
"+ t[o]/\n"
|
|
"- to\n"
|
|
"+ file4\n"
|
|
"- file[2-9]\n"
|
|
"- /mid/for/foo/extra\n"
|
|
)
|
|
|
|
(SCRATCHDIR / '.cvsignore').write_text("home-cvs-exclude\n")
|
|
|
|
|
|
# --- main checks ------------------------------------------------------------
|
|
|
|
# Start with a check of --prune-empty-dirs.
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'-f', '-_foo/too/', '-f', '-_foo/down/',
|
|
'-f', '-_foo/and/', '-f', '-_new/',
|
|
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
|
|
|
checkit(['-av', *rpath, '--prune-empty-dirs',
|
|
f'{host}{FROMDIR}/', f'{TODIR}/'], CHKDIR, TODIR)
|
|
|
|
import shutil
|
|
shutil.rmtree(TODIR, ignore_errors=True)
|
|
|
|
# Add a directory symlink.
|
|
os.symlink('too', FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'sym')
|
|
|
|
# Pre-build an --update test pair.
|
|
up1 = SCRATCHDIR / 'up1'
|
|
up2 = SCRATCHDIR / 'up2'
|
|
up1.mkdir()
|
|
up2.mkdir()
|
|
(up1 / 'dst-newness').touch()
|
|
(up2 / 'src-newness').touch()
|
|
(up1 / 'same-newness').touch()
|
|
(up2 / 'same-newness').touch()
|
|
(up1 / 'extra-src').touch()
|
|
(up2 / 'extra-dest').touch()
|
|
|
|
# Build CHKDIR mirroring source (everything), then remove the entries we
|
|
# expect to be excluded.
|
|
checkit(['-avv', *rpath, f'{host}{FROMDIR}/', f'{CHKDIR}/'], FROMDIR, CHKDIR)
|
|
import time
|
|
time.sleep(1)
|
|
shutil.rmtree(CHKDIR / 'foo' / 'down', ignore_errors=True)
|
|
shutil.rmtree(CHKDIR / 'mid' / 'for' / 'foo' / 'and', ignore_errors=True)
|
|
shutil.rmtree(CHKDIR / 'new' / 'keep' / 'this', ignore_errors=True)
|
|
shutil.rmtree(CHKDIR / 'new' / 'lose', ignore_errors=True)
|
|
for f in (CHKDIR / 'foo').glob('file[235-9]'):
|
|
f.unlink()
|
|
(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / 'to').unlink()
|
|
for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('file[235-9]'):
|
|
f.unlink()
|
|
(CHKDIR / 'mid' / 'for' / 'foo' / 'extra').unlink()
|
|
|
|
(up1 / 'src-newness').touch()
|
|
(up2 / 'dst-newness').touch()
|
|
|
|
# Un-tweak the directory times in our first (weak) exclude test.
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--existing', '--include=*/', '--exclude=*',
|
|
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
|
|
|
# Test that rsync excludes the same files.
|
|
checkit(['-avv', *rpath, f'--exclude-from={excl}',
|
|
'--delete-during', f'{host}{FROMDIR}/', f'{TODIR}/'],
|
|
CHKDIR, TODIR)
|
|
|
|
# Modify the chk dir by removing cvs-ignored files and tweaking dir times.
|
|
for f in (CHKDIR / 'foo').glob('*.old'):
|
|
f.unlink()
|
|
for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('*.bak'):
|
|
f.unlink()
|
|
for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('*.junk'):
|
|
f.unlink()
|
|
(CHKDIR / 'bar' / 'down' / 'to' / 'home-cvs-exclude').unlink()
|
|
(CHKDIR / 'mid' / 'one-in-one-out').unlink()
|
|
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--existing', '--filter=exclude,! */',
|
|
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
|
|
|
# Now test --cvs-exclude + --delete-excluded.
|
|
# -C order differs between push/pull, so use -f :C / -f -C explicitly.
|
|
checkit(['-avv', *rpath, f'--filter=merge {excl}',
|
|
'-f:C', '-f-C', '--delete-excluded', '--delete-during',
|
|
f'{host}{FROMDIR}/', f'{TODIR}/'],
|
|
CHKDIR, TODIR)
|
|
|
|
# Modify the chk dir for the merge-exclude test.
|
|
(CHKDIR / 'foo' / 'file1').unlink()
|
|
for f in (CHKDIR / 'bar' / 'down' / 'to' / 'bar' / 'baz').glob('*.deep'):
|
|
f.unlink()
|
|
for src in (FROMDIR / 'bar' / 'down' / 'to' / 'foo').glob('*.junk'):
|
|
cp_touch_dst = CHKDIR / 'bar' / 'down' / 'to' / 'foo'
|
|
cp_touch_dst.mkdir(exist_ok=True)
|
|
from rsyncfns import cp_touch
|
|
cp_touch(src, cp_touch_dst)
|
|
from rsyncfns import cp_touch
|
|
cp_touch(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'to',
|
|
CHKDIR / 'bar' / 'down' / 'to' / 'foo')
|
|
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--existing', '-f', 'show .filt*', '-f', 'hide,! */', '--del',
|
|
f'{host}{FROMDIR}/', f'{TODIR}/')
|
|
|
|
(TODIR / 'bar' / 'down' / 'to' / 'bar' / 'baz' / 'nodel.deep').write_text("retained\n")
|
|
cp_touch(TODIR / 'bar' / 'down' / 'to' / 'bar' / 'baz' / 'nodel.deep',
|
|
CHKDIR / 'bar' / 'down' / 'to' / 'bar' / 'baz')
|
|
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--existing', '--filter=-! */',
|
|
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
|
|
|
|
|
# Test merge-exclude file. The shell test piped excl-minus-bangs into
|
|
# rsync via stdin; here we materialise the filtered file and merge it.
|
|
filtered_excl = SCRATCHDIR / 'exclude-from-filtered'
|
|
filtered_excl.write_text(
|
|
'\n'.join(ln for ln in excl.read_text().splitlines() if '!' not in ln)
|
|
+ '\n'
|
|
)
|
|
|
|
|
|
def run_with_stdin_filter(args, label="merge"):
|
|
"""Run rsync with `args`, feeding `filtered_excl` content on stdin
|
|
(which `merge_-` in the filter list picks up). checkit-equivalent
|
|
that also re-uses CHKDIR/TODIR for the listing comparison."""
|
|
print(f"Running: rsync {' '.join(args)}")
|
|
with open(filtered_excl, 'rb') as inp:
|
|
proc = subprocess.run(rsync_argv(*args), stdin=inp)
|
|
if proc.returncode != 0:
|
|
test_fail(f"{label}: rsync exited {proc.returncode}")
|
|
|
|
|
|
run_with_stdin_filter(
|
|
['-avv', *rpath, '-f', 'dir-merge_.filt', '-f', 'merge_-',
|
|
'--delete-during', f'{host}{FROMDIR}/', f'{TODIR}/'],
|
|
"dir-merge .filt + merge from stdin",
|
|
)
|
|
from rsyncfns import verify_dirs
|
|
verify_dirs(CHKDIR, TODIR, label="dir-merge + merge-from-stdin")
|
|
|
|
|
|
# Remove the files that will be deleted.
|
|
(CHKDIR / '.filt').unlink()
|
|
(CHKDIR / 'bar' / '.filt').unlink()
|
|
(CHKDIR / 'bar' / 'down' / 'to' / '.filt2').unlink()
|
|
(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / '.filt2').unlink()
|
|
(CHKDIR / 'bar' / 'down' / 'to' / 'bar' / '.filt2').unlink()
|
|
(CHKDIR / 'mid' / '.filt').unlink()
|
|
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--existing', '--include=*/', '--exclude=*',
|
|
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
|
|
|
# Run the prior command with --delete-before and side-specific rules.
|
|
run_with_stdin_filter(
|
|
['-avv', *rpath, '-f', ':s_.filt', '-f', '.s_-',
|
|
'-f', 'P_nodel.deep',
|
|
'--delete-before', f'{host}{FROMDIR}/', f'{TODIR}/'],
|
|
"delete-before with merge",
|
|
)
|
|
verify_dirs(CHKDIR, TODIR, label="delete-before with merge")
|
|
|
|
|
|
# Rule-restricted filter files.
|
|
(FROMDIR / 'bar' / 'down' / '.excl').write_text("file3\n")
|
|
(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / '.excl').write_text(
|
|
"+ file3\n*.bak\n"
|
|
)
|
|
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--del', f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
|
(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / 'file1.bak').unlink()
|
|
(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / 'file3').unlink()
|
|
(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / '+ file3').unlink()
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--existing', '--filter=-! */',
|
|
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
|
'--delete-excluded', '--exclude=*',
|
|
f'{host}{FROMDIR}/', f'{TODIR}/')
|
|
|
|
checkit(['-avv', *rpath, '-f', 'dir-merge,-_.excl',
|
|
f'{host}{FROMDIR}/', f'{TODIR}/'], CHKDIR, TODIR)
|
|
|
|
|
|
# Combine with --relative.
|
|
relative_opts = ['--relative', '--chmod=Du+w', '--copy-unsafe-links']
|
|
run_rsync('-av', f'--rsync-path={RSYNC_PEER}', *relative_opts,
|
|
f'{host}{FROMDIR}/foo', f'{CHKDIR}/')
|
|
shutil.rmtree(str(CHKDIR) + str(FROMDIR) + '/foo/down', ignore_errors=True)
|
|
run_rsync('-av', *relative_opts, '--existing', '--filter=-! */',
|
|
f'{FROMDIR}/foo', f'{CHKDIR}/')
|
|
|
|
checkit(['-avv', *rpath, *relative_opts,
|
|
f'--exclude={FROMDIR}/foo/down',
|
|
f'{host}{FROMDIR}/foo', str(TODIR)],
|
|
str(CHKDIR) + str(FROMDIR) + '/foo',
|
|
str(TODIR) + str(FROMDIR) + '/foo')
|
|
|
|
|
|
# --update test.
|
|
checkdiff(
|
|
['-aiiO', *rpath, '--update', '--info=skip',
|
|
f'{host}{SCRATCHDIR}/up1/', f'{SCRATCHDIR}/up2/'],
|
|
"dst-newness is newer\n"
|
|
f">f{all_plus} extra-src\n"
|
|
f".f{allspace} same-newness\n"
|
|
f">f..t.{dots} src-newness\n",
|
|
filter=lambda txt: '\n'.join(
|
|
ln for ln in txt.splitlines()
|
|
if not ln.startswith('.d' + allspace)
|
|
) + ('\n' if txt else ''),
|
|
)
|