From: Andrew Tridgell Date: Sun, 31 May 2026 11:00:51 +0000 (+1000) Subject: runtests: add --rsync-bin2 / --expect-result for version-mixing tests X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=e21cdabd710881e5fe212e342e8f53274e030de5;p=thirdparty%2Frsync.git 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) --- diff --git a/runtests.py b/runtests.py index 1bdf4398..dc19d878 100755 --- a/runtests.py +++ b/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 ' + '" " 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 " " 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 ' " + 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:') diff --git a/testsuite/00-hello_test.py b/testsuite/00-hello_test.py index 312f3539..9077ab97 100644 --- a/testsuite/00-hello_test.py +++ b/testsuite/00-hello_test.py @@ -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, ) diff --git a/testsuite/alt-dest_test.py b/testsuite/alt-dest_test.py index 07389d45..d5bb37b2 100644 --- a/testsuite/alt-dest_test.py +++ b/testsuite/alt-dest_test.py @@ -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) diff --git a/testsuite/daemon_test.py b/testsuite/daemon_test.py index 4e022fa5..8d9ab4b5 100644 --- a/testsuite/daemon_test.py +++ b/testsuite/daemon_test.py @@ -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", diff --git a/testsuite/exclude_test.py b/testsuite/exclude_test.py index d4886bfc..bd528f61 100644 --- a/testsuite/exclude_test.py +++ b/testsuite/exclude_test.py @@ -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=-! */', diff --git a/testsuite/files-from_test.py b/testsuite/files-from_test.py index 855007e5..0d10d2d5 100644 --- a/testsuite/files-from_test.py +++ b/testsuite/files-from_test.py @@ -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, diff --git a/testsuite/hardlinks_test.py b/testsuite/hardlinks_test.py index 9084899d..a578d63c 100644 --- a/testsuite/hardlinks_test.py +++ b/testsuite/hardlinks_test.py @@ -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. diff --git a/testsuite/rsyncfns.py b/testsuite/rsyncfns.py index 3a3b37b1..d4c06606 100644 --- a/testsuite/rsyncfns.py +++ b/testsuite/rsyncfns.py @@ -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/' diff --git a/testsuite/ssh-basic_test.py b/testsuite/ssh-basic_test.py index dbb9957a..eb658118 100644 --- a/testsuite/ssh-basic_test.py +++ b/testsuite/ssh-basic_test.py @@ -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) diff --git a/testsuite/symlink-dirlink-basis_test.py b/testsuite/symlink-dirlink-basis_test.py index 8c52e13f..cf026815 100644 --- a/testsuite/symlink-dirlink-basis_test.py +++ b/testsuite/symlink-dirlink-basis_test.py @@ -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='')