mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-06-07 21:58:06 -04:00
runtests: add --rsync-bin2 / --expect-result for version-mixing tests
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>
This commit is contained in:
129
runtests.py
129
runtests.py
@@ -60,6 +60,11 @@ def parse_args():
|
||||
help='Per-test timeout in seconds (default: 300)')
|
||||
p.add_argument('--rsync-bin', default=None, metavar='PATH',
|
||||
help='Path to rsync binary (default: ./rsync)')
|
||||
p.add_argument('--rsync-bin2', default=None, metavar='PATH',
|
||||
help='Path to a second ("peer") rsync binary used for the '
|
||||
'daemon side and remote-shell --rsync-path. Lets the '
|
||||
'suite mix two rsync versions over the wire. Default: '
|
||||
'same as --rsync-bin (no version mixing).')
|
||||
p.add_argument('--tooldir', default=None, metavar='DIR',
|
||||
help='Tool/build directory (default: cwd)')
|
||||
p.add_argument('--srcdir', default=None, metavar='DIR',
|
||||
@@ -68,6 +73,13 @@ def parse_args():
|
||||
help='Force protocol version (adds --protocol=VER to rsync)')
|
||||
p.add_argument('--expect-skipped', default=None, metavar='LIST',
|
||||
help='Comma-separated list of expected-skipped tests')
|
||||
p.add_argument('--expect-result', default=None, metavar='FILE',
|
||||
help='Path to an expected-outcome manifest (one '
|
||||
'"<testname> <pass|skip|fail|xfail>" per line). When '
|
||||
'set, ONLY the tests listed in FILE are run, and each '
|
||||
"test's actual outcome is compared against its "
|
||||
'expected one; any mismatch (including an unexpected '
|
||||
'pass) fails the run. Used for version-mixing CI.')
|
||||
p.add_argument('--use-tcp', action='store_true',
|
||||
help='Run daemon tests against a real rsyncd bound to '
|
||||
'127.0.0.1 (non-default). The default is the secure '
|
||||
@@ -208,6 +220,44 @@ def collect_tests(suitedir, patterns):
|
||||
return tests
|
||||
|
||||
|
||||
_VALID_OUTCOMES = ('pass', 'skip', 'fail', 'xfail')
|
||||
|
||||
|
||||
def parse_expect_result(path):
|
||||
"""Parse an expected-outcome manifest into {testbase: outcome}.
|
||||
|
||||
One "<testname> <outcome>" entry per line; '#' comments and blank lines
|
||||
are ignored. outcome is one of pass|skip|fail|xfail. The set of listed
|
||||
tests doubles as the run set (see main()). Exits 2 on a malformed file.
|
||||
"""
|
||||
expect = {}
|
||||
with open(path) as f:
|
||||
for lineno, raw in enumerate(f, 1):
|
||||
line = raw.split('#', 1)[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
fields = line.split()
|
||||
if len(fields) != 2 or fields[1] not in _VALID_OUTCOMES:
|
||||
sys.stderr.write(
|
||||
f"{path}:{lineno}: expected '<testname> "
|
||||
f"<{'|'.join(_VALID_OUTCOMES)}>', got: {raw.rstrip()}\n"
|
||||
)
|
||||
sys.exit(2)
|
||||
expect[fields[0]] = fields[1]
|
||||
return expect
|
||||
|
||||
|
||||
def outcome_of(result):
|
||||
"""Map a per-test exit code to an outcome string."""
|
||||
if result == 0:
|
||||
return 'pass'
|
||||
if result == 77:
|
||||
return 'skip'
|
||||
if result == 78:
|
||||
return 'xfail'
|
||||
return 'fail'
|
||||
|
||||
|
||||
def build_rsync_cmd(rsync_bin, args, scratchbase):
|
||||
"""Build the RSYNC command string for tests."""
|
||||
parts = []
|
||||
@@ -339,6 +389,12 @@ def main():
|
||||
if rsync_bin and not os.path.isabs(rsync_bin):
|
||||
rsync_bin = os.path.abspath(rsync_bin)
|
||||
|
||||
# Optional second ("peer") binary for the daemon / remote-shell side, so a
|
||||
# run can mix two rsync versions. Defaults to rsync_bin -> no mixing.
|
||||
rsync_bin2 = args.rsync_bin2 or os.environ.get('rsync_bin2') or rsync_bin
|
||||
if rsync_bin2 and not os.path.isabs(rsync_bin2):
|
||||
rsync_bin2 = os.path.abspath(rsync_bin2)
|
||||
|
||||
suitedir = os.path.join(srcdir, 'testsuite')
|
||||
scratchbase = os.path.join(os.environ.get('scratchbase', tooldir), 'testtmp')
|
||||
os.makedirs(scratchbase, exist_ok=True)
|
||||
@@ -347,10 +403,14 @@ def main():
|
||||
tls_args = get_tls_args(os.path.join(tooldir, 'config.h'))
|
||||
setfacl_nodef = find_setfacl_nodef(scratchbase)
|
||||
rsync_cmd = build_rsync_cmd(rsync_bin, args, scratchbase)
|
||||
rsync_peer_cmd = build_rsync_cmd(rsync_bin2, args, scratchbase)
|
||||
|
||||
if not os.path.isfile(rsync_bin):
|
||||
sys.stderr.write(f"rsync_bin {rsync_bin} is not a file\n")
|
||||
sys.exit(2)
|
||||
if not os.path.isfile(rsync_bin2):
|
||||
sys.stderr.write(f"rsync_bin2 {rsync_bin2} is not a file\n")
|
||||
sys.exit(2)
|
||||
if not os.path.isdir(srcdir):
|
||||
sys.stderr.write(f"srcdir {srcdir} is not a directory\n")
|
||||
sys.exit(2)
|
||||
@@ -378,6 +438,8 @@ def main():
|
||||
print('=' * 60)
|
||||
print(f'{sys.argv[0]} running in {tooldir}')
|
||||
print(f' rsync_bin={rsync_cmd}')
|
||||
if rsync_peer_cmd != rsync_cmd:
|
||||
print(f' rsync_peer={rsync_peer_cmd}')
|
||||
print(f' srcdir={srcdir}')
|
||||
print(f' TLS_ARGS={tls_args}')
|
||||
print(f' testuser={testuser}')
|
||||
@@ -407,6 +469,7 @@ def main():
|
||||
'TOOLDIR': tooldir,
|
||||
'srcdir': srcdir,
|
||||
'RSYNC': rsync_cmd,
|
||||
'RSYNC_PEER': rsync_peer_cmd,
|
||||
'TLS_ARGS': tls_args,
|
||||
'RUNSHFLAGS': '-e',
|
||||
'scratchbase': scratchbase,
|
||||
@@ -443,6 +506,29 @@ def main():
|
||||
print(f"Excluding {before - len(tests)} test(s) matching: "
|
||||
f"{', '.join(excl)}")
|
||||
|
||||
# An expected-result manifest defines BOTH the run set (its keys) and the
|
||||
# expected per-test outcome (its values). Used for version-mixing runs.
|
||||
expect = parse_expect_result(args.expect_result) if args.expect_result else None
|
||||
if expect is not None:
|
||||
have = {_testbase(t) for t in tests}
|
||||
unknown = sorted(k for k in expect if k not in have)
|
||||
if unknown:
|
||||
sys.stderr.write(
|
||||
"runtests.py: --expect-result lists test(s) with no matching "
|
||||
f"test file (ignored): {', '.join(unknown)}\n"
|
||||
)
|
||||
tests = [t for t in tests if _testbase(t) in expect]
|
||||
full_run = False
|
||||
|
||||
def _cls(outcome):
|
||||
"""Equivalence class for outcome comparison: fail and xfail both just
|
||||
mean 'broke', so a manifest 'fail' matches an actual fail OR xfail."""
|
||||
return 'broken' if outcome in ('fail', 'xfail') else outcome
|
||||
|
||||
def mismatch(testbase, actual):
|
||||
"""True if actual outcome disagrees with the manifest expectation."""
|
||||
return expect is not None and _cls(expect[testbase]) != _cls(actual)
|
||||
|
||||
# Record test order for consistent skipped-list output
|
||||
test_order = {_testbase(t): i for i, t in enumerate(tests)}
|
||||
|
||||
@@ -450,31 +536,33 @@ def main():
|
||||
failed = 0
|
||||
skipped = 0
|
||||
skipped_list = []
|
||||
outcomes = {} # testbase -> actual outcome string ('pass'/'skip'/'fail'/'xfail')
|
||||
|
||||
def process_result(tr):
|
||||
"""Process a TestResult and update counters. Returns True if test failed."""
|
||||
"""Process a TestResult and update counters. Returns True if the test
|
||||
should count as a failure for --stop-on-fail purposes."""
|
||||
nonlocal passed, failed, skipped
|
||||
with _print_lock:
|
||||
if tr.output:
|
||||
print(tr.output)
|
||||
scratchdir = os.path.join(scratchbase, tr.testbase)
|
||||
oc = outcome_of(tr.result)
|
||||
outcomes[tr.testbase] = oc
|
||||
if tr.result == 0:
|
||||
passed += 1
|
||||
if not args.preserve_scratch and os.path.isdir(scratchdir):
|
||||
subprocess.run(['rm', '-rf', scratchdir], capture_output=True)
|
||||
return False
|
||||
elif tr.result == 77:
|
||||
skipped_list.append(tr.testbase)
|
||||
skipped += 1
|
||||
if not args.preserve_scratch and os.path.isdir(scratchdir):
|
||||
subprocess.run(['rm', '-rf', scratchdir], capture_output=True)
|
||||
return False
|
||||
elif tr.result == 78:
|
||||
failed += 1
|
||||
return True
|
||||
else:
|
||||
failed += 1
|
||||
return True
|
||||
if tr.result in (0, 77) and not args.preserve_scratch \
|
||||
and os.path.isdir(scratchdir):
|
||||
subprocess.run(['rm', '-rf', scratchdir], capture_output=True)
|
||||
# With a manifest, only a mismatch is a "failure" (an expected fail is
|
||||
# fine); without one, any non-pass/non-skip result is a failure.
|
||||
if expect is not None:
|
||||
return mismatch(tr.testbase, oc)
|
||||
return tr.result not in (0, 77)
|
||||
|
||||
if args.parallel > 1:
|
||||
# Parallel execution
|
||||
@@ -541,6 +629,25 @@ def main():
|
||||
if vg_errors > 0:
|
||||
print(f' {vg_errors} valgrind error(s) found (see logs in {scratchbase})')
|
||||
|
||||
if expect is not None:
|
||||
# Version-mixing mode: the run is judged purely on whether each test's
|
||||
# actual outcome matched its manifest expectation. An expected 'fail'
|
||||
# is fine; an UNEXPECTED pass (xpass) or any other divergence is not.
|
||||
mismatches = []
|
||||
for tb in sorted(expect, key=lambda x: test_order.get(x, 1 << 30)):
|
||||
actual = outcomes.get(tb, 'notrun')
|
||||
if actual == 'notrun' or mismatch(tb, actual):
|
||||
mismatches.append((tb, expect[tb], actual))
|
||||
if mismatches:
|
||||
print('----- expected-result mismatches:')
|
||||
for tb, want, got in mismatches:
|
||||
tag = ' (xpass)' if _cls(want) == 'broken' and got == 'pass' else ''
|
||||
print(f' {tb}: expected {want}, got {got}{tag}')
|
||||
print('-' * 60)
|
||||
exit_code = len(mismatches) + vg_errors
|
||||
print(f'overall result is {exit_code}')
|
||||
sys.exit(exit_code)
|
||||
|
||||
skipped_str = ','.join(sorted(skipped_list, key=lambda x: test_order.get(x, 0)))
|
||||
if full_run and args.expect_skipped != 'IGNORE':
|
||||
print('----- skipped results:')
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import os
|
||||
|
||||
from rsyncfns import (
|
||||
FROMDIR, RSYNC, SRCDIR, TODIR,
|
||||
FROMDIR, RSYNC, RSYNC_PEER, SRCDIR, TODIR,
|
||||
checkit, run_rsync, test_fail,
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ def append_line(line: str) -> None:
|
||||
|
||||
def copy_weird(args: list, src_host: str, dst_host: str) -> None:
|
||||
checkit(
|
||||
[*args, f'--rsync-path={RSYNC}',
|
||||
[*args, f'--rsync-path={RSYNC_PEER}',
|
||||
f'{src_host}{weird_dir}/',
|
||||
f'{dst_host}{TODIR / weird_name}'],
|
||||
FROMDIR, TODIR,
|
||||
@@ -70,7 +70,7 @@ print('test6')
|
||||
saved = os.getcwd()
|
||||
os.chdir(FROMDIR)
|
||||
try:
|
||||
run_rsync('-ai', '--old-args', f'--rsync-path={RSYNC}',
|
||||
run_rsync('-ai', '--old-args', f'--rsync-path={RSYNC_PEER}',
|
||||
'lh:one two', f'{TODIR}/')
|
||||
finally:
|
||||
os.chdir(saved)
|
||||
@@ -91,7 +91,7 @@ from rsyncfns import rsync_argv
|
||||
os.chdir(FROMDIR)
|
||||
try:
|
||||
subprocess.run(
|
||||
rsync_argv('-ai', f'--rsync-path={RSYNC}',
|
||||
rsync_argv('-ai', f'--rsync-path={RSYNC_PEER}',
|
||||
'lh:one two', f'{TODIR}/'),
|
||||
env=env, check=True,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import shutil
|
||||
import time
|
||||
|
||||
from rsyncfns import (
|
||||
CHKDIR, FROMDIR, RSYNC, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,
|
||||
CHKDIR, FROMDIR, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,
|
||||
checkit, hands_setup, rmtree, run_rsync, test_fail,
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ for maybe_inplace in ([], ['--inplace']):
|
||||
for srchost in ('', 'localhost:'):
|
||||
desthost = 'localhost:' if not srchost else ''
|
||||
rmtree(TODIR)
|
||||
checkit(['-ave', SSH, f'--rsync-path={RSYNC}', *maybe_inplace,
|
||||
checkit(['-ave', SSH, f'--rsync-path={RSYNC_PEER}', *maybe_inplace,
|
||||
f'--copy-dest={alt3dir}',
|
||||
f'{srchost}{FROMDIR}/', f'{desthost}{TODIR}/'],
|
||||
FROMDIR, TODIR)
|
||||
|
||||
@@ -10,7 +10,7 @@ import os
|
||||
import subprocess
|
||||
|
||||
from rsyncfns import (
|
||||
CHKFILE, FROMDIR, OUTFILE, RSYNC, SCRATCHDIR, SRCDIR, TODIR,
|
||||
CHKFILE, FROMDIR, OUTFILE, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TODIR,
|
||||
build_rsyncd_conf, get_rootuid, get_testuid, makepath,
|
||||
rsync_argv, run_rsync, start_test_daemon, test_fail,
|
||||
)
|
||||
@@ -78,7 +78,7 @@ def run_and_check(args, label, capture_stderr=False):
|
||||
|
||||
|
||||
# Module list via the lsh.sh stand-in.
|
||||
rsync_path = f"{RSYNC}{(' ' + ' '.join(confopt)) if confopt else ''}"
|
||||
rsync_path = f"{RSYNC_PEER}{(' ' + ' '.join(confopt)) if confopt else ''}"
|
||||
out = run_and_check(
|
||||
['-ve', SSH, f'--rsync-path={rsync_path}', 'localhost::'],
|
||||
"module list via lsh.sh",
|
||||
|
||||
@@ -15,7 +15,7 @@ import subprocess
|
||||
import sys
|
||||
|
||||
from rsyncfns import (
|
||||
CHKDIR, FROMDIR, RSYNC, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,
|
||||
CHKDIR, FROMDIR, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,
|
||||
all_plus, allspace, dots,
|
||||
checkdiff, checkit, makepath, rsync_argv, run_rsync, test_fail,
|
||||
)
|
||||
@@ -26,7 +26,7 @@ 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}']
|
||||
rpath = [f'--rsync-path={RSYNC_PEER}']
|
||||
host = 'lh:'
|
||||
else:
|
||||
rpath = []
|
||||
@@ -116,7 +116,7 @@ excl.write_text(
|
||||
# --- main checks ------------------------------------------------------------
|
||||
|
||||
# Start with a check of --prune-empty-dirs.
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'-f', '-_foo/too/', '-f', '-_foo/down/',
|
||||
'-f', '-_foo/and/', '-f', '-_new/',
|
||||
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
||||
@@ -162,7 +162,7 @@ for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('file[235-9]'):
|
||||
(up2 / 'dst-newness').touch()
|
||||
|
||||
# Un-tweak the directory times in our first (weak) exclude test.
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'--existing', '--include=*/', '--exclude=*',
|
||||
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
||||
|
||||
@@ -181,7 +181,7 @@ for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('*.junk'):
|
||||
(CHKDIR / 'bar' / 'down' / 'to' / 'home-cvs-exclude').unlink()
|
||||
(CHKDIR / 'mid' / 'one-in-one-out').unlink()
|
||||
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'--existing', '--filter=exclude,! */',
|
||||
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
||||
|
||||
@@ -205,7 +205,7 @@ 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}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'--existing', '-f', 'show .filt*', '-f', 'hide,! */', '--del',
|
||||
f'{host}{FROMDIR}/', f'{TODIR}/')
|
||||
|
||||
@@ -213,7 +213,7 @@ run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
cp_touch(TODIR / 'bar' / 'down' / 'to' / 'bar' / 'baz' / 'nodel.deep',
|
||||
CHKDIR / 'bar' / 'down' / 'to' / 'bar' / 'baz')
|
||||
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'--existing', '--filter=-! */',
|
||||
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
||||
|
||||
@@ -255,7 +255,7 @@ verify_dirs(CHKDIR, TODIR, label="dir-merge + merge-from-stdin")
|
||||
(CHKDIR / 'bar' / 'down' / 'to' / 'bar' / '.filt2').unlink()
|
||||
(CHKDIR / 'mid' / '.filt').unlink()
|
||||
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'--existing', '--include=*/', '--exclude=*',
|
||||
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
||||
|
||||
@@ -275,15 +275,15 @@ verify_dirs(CHKDIR, TODIR, label="delete-before with merge")
|
||||
"+ file3\n*.bak\n"
|
||||
)
|
||||
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
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}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'--existing', '--filter=-! */',
|
||||
f'{host}{FROMDIR}/', f'{CHKDIR}/')
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}',
|
||||
run_rsync('-av', f'--rsync-path={RSYNC_PEER}',
|
||||
'--delete-excluded', '--exclude=*',
|
||||
f'{host}{FROMDIR}/', f'{TODIR}/')
|
||||
|
||||
@@ -293,7 +293,7 @@ checkit(['-avv', *rpath, '-f', 'dir-merge,-_.excl',
|
||||
|
||||
# Combine with --relative.
|
||||
relative_opts = ['--relative', '--chmod=Du+w', '--copy-unsafe-links']
|
||||
run_rsync('-av', f'--rsync-path={RSYNC}', *relative_opts,
|
||||
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=-! */',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# files-host / src-host / dest-host placement combinations.
|
||||
|
||||
from rsyncfns import (
|
||||
CHKDIR, FROMDIR, RSYNC, SCRATCHDIR, SRCDIR, TODIR,
|
||||
CHKDIR, FROMDIR, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TODIR,
|
||||
checkit, hands_setup, rmtree, run_rsync,
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ for filehost in ('', 'localhost:'):
|
||||
|
||||
rmtree(TODIR)
|
||||
checkit(
|
||||
['-avse', SSH, f'--rsync-path={RSYNC}',
|
||||
['-avse', SSH, f'--rsync-path={RSYNC_PEER}',
|
||||
f'--files-from={filehost}{filelist}',
|
||||
f'{srchost}{SCRATCHDIR}', f'{desthost}{TODIR}/'],
|
||||
CHKDIR, TODIR,
|
||||
|
||||
@@ -12,7 +12,7 @@ import shutil
|
||||
import subprocess
|
||||
|
||||
from rsyncfns import (
|
||||
CHKDIR, FROMDIR, OUTFILE, RSYNC, SRCDIR, TODIR,
|
||||
CHKDIR, FROMDIR, OUTFILE, RSYNC, RSYNC_PEER, SRCDIR, TODIR,
|
||||
checkit, makepath, rsync_argv, test_fail, test_skipped,
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ for x in chars:
|
||||
os.link(name1, FROMDIR / 'subdir' / 'down' / 'deep' / 'new-file')
|
||||
(TODIR / 'text').unlink()
|
||||
|
||||
checkit(['-aHivve', SSH, '--debug=HLINK5', f'--rsync-path={RSYNC}',
|
||||
checkit(['-aHivve', SSH, '--debug=HLINK5', f'--rsync-path={RSYNC_PEER}',
|
||||
f'{FROMDIR}/', f'localhost:{TODIR}/'], FROMDIR, TODIR)
|
||||
|
||||
# --link-dest and --copy-dest should also keep hard-linked entries.
|
||||
|
||||
@@ -64,6 +64,15 @@ os.umask(0o022)
|
||||
os.environ['HOME'] = str(SCRATCHDIR)
|
||||
RSYNC = _required('RSYNC') # full command line, possibly with valgrind/protocol
|
||||
|
||||
# The "peer" rsync command -- used for the SERVER side of two-sided transfers
|
||||
# (the daemon process; the remote-shell --rsync-path target). The runner sets
|
||||
# RSYNC_PEER to a second binary when invoked with --rsync-bin2, letting a run
|
||||
# mix two rsync versions over the wire. When no second binary was selected,
|
||||
# RSYNC_PEER == RSYNC, so every consumer below behaves exactly as before and
|
||||
# single-binary runs are unchanged. Use .get (not _required) so a test invoked
|
||||
# by hand without the runner still works.
|
||||
RSYNC_PEER = os.environ.get('RSYNC_PEER', RSYNC)
|
||||
|
||||
# TLS_ARGS controls how the 'tls' helper formats listings (e.g. --atimes,
|
||||
# -l, -L). Tests that exercise non-default rsync features (atimes, etc.)
|
||||
# assign to rsyncfns.TLS_ARGS before calling checkit / rsync_ls_lR.
|
||||
@@ -232,7 +241,7 @@ def _stop_rsyncd(proc) -> 'None':
|
||||
pass
|
||||
|
||||
|
||||
def start_rsyncd(conf_path, port: int) -> 'subprocess.Popen':
|
||||
def start_rsyncd(conf_path, port: int, rsync_cmd: str = None) -> 'subprocess.Popen':
|
||||
"""Spawn `rsync --daemon --no-detach --address=127.0.0.1 --port=N
|
||||
--config=conf` and return the Popen handle after the port is accepting
|
||||
connections.
|
||||
@@ -245,10 +254,16 @@ def start_rsyncd(conf_path, port: int) -> 'subprocess.Popen':
|
||||
the test process doesn't strand the daemon either. The caller is expected
|
||||
to have already claim_ports()'d `port`.
|
||||
|
||||
rsync_cmd selects the binary to run as the daemon; it defaults to
|
||||
RSYNC_PEER (the peer side of a two-sided run), so ordinary daemon tests
|
||||
get current-client <-> peer-daemon. The reverse-direction test passes
|
||||
rsync_cmd=RSYNC to put the current build on the daemon side and drive with
|
||||
the old client.
|
||||
|
||||
This is only ever reached from start_test_daemon() in --use-tcp mode; the
|
||||
default (pipe) mode never starts a listening daemon.
|
||||
"""
|
||||
argv = shlex.split(RSYNC) + [
|
||||
argv = shlex.split(rsync_cmd or RSYNC_PEER) + [
|
||||
'--daemon', '--no-detach',
|
||||
'--address=127.0.0.1',
|
||||
f'--port={port}',
|
||||
@@ -282,9 +297,12 @@ def start_rsyncd(conf_path, port: int) -> 'subprocess.Popen':
|
||||
test_fail(f"rsyncd never listened on 127.0.0.1:{port}: {last_err}")
|
||||
|
||||
|
||||
def start_test_daemon(conf_path, port: int) -> str:
|
||||
def start_test_daemon(conf_path, port: int, rsync_cmd: str = None) -> str:
|
||||
"""Bring up the test daemon and return a URL prefix for client commands.
|
||||
|
||||
rsync_cmd selects the daemon-side binary (default RSYNC_PEER); pass
|
||||
rsync_cmd=RSYNC for the reverse-direction test (current daemon, old client).
|
||||
|
||||
This is the single seam every daemon test uses. The transport depends on
|
||||
the mode the runner selected:
|
||||
|
||||
@@ -302,11 +320,12 @@ def start_test_daemon(conf_path, port: int) -> str:
|
||||
Build URLs as f"{prefix}module/path". `port` is only used (and claimed)
|
||||
in --use-tcp mode.
|
||||
"""
|
||||
daemon_cmd = rsync_cmd or RSYNC_PEER
|
||||
if USE_TCP:
|
||||
claim_ports(port)
|
||||
start_rsyncd(conf_path, port)
|
||||
start_rsyncd(conf_path, port, daemon_cmd)
|
||||
return f'rsync://localhost:{port}/'
|
||||
os.environ['RSYNC_CONNECT_PROG'] = f'{RSYNC} --config={conf_path} --daemon'
|
||||
os.environ['RSYNC_CONNECT_PROG'] = f'{daemon_cmd} --config={conf_path} --daemon'
|
||||
return 'rsync://localhost/'
|
||||
|
||||
|
||||
|
||||
@@ -38,17 +38,17 @@ print(f"Using remote shell: {SSH}")
|
||||
hands_setup()
|
||||
|
||||
# RSYNC may be a multi-word command line; pass it through --rsync-path.
|
||||
from rsyncfns import RSYNC
|
||||
from rsyncfns import RSYNC, RSYNC_PEER
|
||||
|
||||
|
||||
def _basic():
|
||||
checkit(['-avH', '-e', SSH, f'--rsync-path={RSYNC}',
|
||||
checkit(['-avH', '-e', SSH, f'--rsync-path={RSYNC_PEER}',
|
||||
f'{FROMDIR}/', f'localhost:{TODIR}'], FROMDIR, TODIR)
|
||||
|
||||
|
||||
def _delete_after_rename():
|
||||
shutil.move(str(TODIR / 'text'), str(TODIR / 'ThisShouldGo'))
|
||||
checkit(['--delete', '-avH', '-e', SSH, f'--rsync-path={RSYNC}',
|
||||
checkit(['--delete', '-avH', '-e', SSH, f'--rsync-path={RSYNC_PEER}',
|
||||
f'{FROMDIR}/', f'localhost:{TODIR}'], FROMDIR, TODIR)
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import subprocess
|
||||
import time
|
||||
|
||||
from rsyncfns import (
|
||||
RSYNC, SCRATCHDIR, SRCDIR, TMPDIR,
|
||||
RSYNC, SCRATCHDIR, RSYNC_PEER, SRCDIR, TMPDIR,
|
||||
make_data_file, resolve_beneath_supported, rsync_argv, test_fail,
|
||||
test_skipped,
|
||||
)
|
||||
@@ -59,7 +59,7 @@ def push(*args, label: str) -> None:
|
||||
os.chdir(srcbase)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
rsync_argv(f'--rsync-path={RSYNC}', *args),
|
||||
rsync_argv(f'--rsync-path={RSYNC_PEER}', *args),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
)
|
||||
print(proc.stdout, end='')
|
||||
|
||||
Reference in New Issue
Block a user