From 5b36673d0adf59ca17ef89bae338ca9957aeb427 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Mon, 25 May 2026 08:14:04 +1000 Subject: [PATCH] testsuite: add content and return-code assertions Several tests proved only that rsync exited cleanly (or that a file merely exists), so a no-op/short transfer would pass: protected-regular compare the dst bytes to the source after --inplace. 00-hello re-assert one/two were copied on the RSYNC_OLD_ARGS=1 env-var path (the explicit --old-args case already did). missing check the dry-run's exit status in test 1. mkpath compare transferred bytes (not just existence) and add a negative control: a transfer WITHOUT --mkpath must fail and create no intermediate path. size-filter compare each kept file's content to its source. Co-Authored-By: Claude Opus 4.7 (1M context) --- testsuite/00-hello_test.py | 5 +++++ testsuite/missing_test.py | 2 ++ testsuite/mkpath_test.py | 15 +++++++++++++-- testsuite/protected-regular_test.py | 9 ++++++++- testsuite/size-filter_test.py | 22 +++++++++++++--------- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/testsuite/00-hello_test.py b/testsuite/00-hello_test.py index 22b2e0e6..312f3539 100644 --- a/testsuite/00-hello_test.py +++ b/testsuite/00-hello_test.py @@ -97,3 +97,8 @@ try: ) finally: os.chdir(saved) + +# check=True only proves a zero exit; confirm the env-var path actually copied +# both files (as the explicit --old-args case above does). +if not (TODIR / 'one').is_file() or not (TODIR / 'two').is_file(): + test_fail("RSYNC_OLD_ARGS=1 copy of 'one two' failed") diff --git a/testsuite/missing_test.py b/testsuite/missing_test.py index 2d0a8ef9..c0a600fe 100644 --- a/testsuite/missing_test.py +++ b/testsuite/missing_test.py @@ -31,6 +31,8 @@ def run_capture(*args): out_path = TMPDIR / 'out1' proc = run_capture('-n', '-r', '--ignore-non-existing', '-vv', f'{FROMDIR}/', f'{TODIR}/') +if proc.returncode != 0: + test_fail(f"test 1 failed: dry-run errored (rc={proc.returncode})") out_path.write_text(proc.stdout) for line in proc.stdout.splitlines(): if 'not creating new' in line and 'subdir/file' in line: diff --git a/testsuite/mkpath_test.py b/testsuite/mkpath_test.py index e257d67a..f72cf986 100644 --- a/testsuite/mkpath_test.py +++ b/testsuite/mkpath_test.py @@ -4,6 +4,7 @@ # Test the rsync --mkpath option: it should create any missing intermediate # destination directories rather than erroring out. +import filecmp import os import shutil from pathlib import Path @@ -25,11 +26,20 @@ os.chdir(TMPDIR) deep_dir = Path('to/foo/bar/baz/down/deep') -def assert_file(path: Path, label: str) -> None: +def assert_file(path: Path, label: str, src: str = 'from/text') -> None: if not path.is_file(): test_fail(f"{label}: {path} not found") + if not filecmp.cmp(path, src, shallow=False): + test_fail(f"{label}: {path} content differs from {src}") +# Negative control: without --mkpath, a missing intermediate path must fail and +# create nothing -- otherwise the --mkpath successes below prove nothing. +rmtree('to/foo') +proc = run_rsync('-ai', 'from/text', str(deep_dir / 'new'), check=False) +if proc.returncode == 0 or (deep_dir / 'new').exists(): + test_fail("a transfer WITHOUT --mkpath created the missing intermediate path") + # Create several levels of dest dir (file destination — final component # is the new filename). run_rsync('-aiv', '--mkpath', 'from/text', str(deep_dir / 'new')) @@ -53,7 +63,8 @@ rmtree('to/foo') # Multiple source args (whole directory) — bare dest name. run_rsync('-aiv', '--mkpath', 'from/', str(deep_dir)) -assert_file(deep_dir / 'extra', "'extra' file in deep dir (multi-source, no trailing slash)") +assert_file(deep_dir / 'extra', "'extra' file in deep dir (multi-source, no trailing slash)", + src='from/extra') rmtree('to/foo') # Multiple source args (whole directory) — dest with trailing slash. diff --git a/testsuite/protected-regular_test.py b/testsuite/protected-regular_test.py index f3e0485f..b7d98258 100644 --- a/testsuite/protected-regular_test.py +++ b/testsuite/protected-regular_test.py @@ -12,7 +12,7 @@ import subprocess import sys from pathlib import Path -from rsyncfns import TMPDIR, run_rsync, test_skipped +from rsyncfns import TMPDIR, run_rsync, test_fail, test_skipped pr_path = Path('/proc/sys/fs/protected_regular') @@ -71,3 +71,10 @@ print(f"Contents of {workdir}:") subprocess.run(['ls', '-al', str(workdir)]) run_rsync('--inplace', str(workdir / 'src'), str(workdir / 'dst')) + +# A zero exit isn't enough: confirm --inplace actually wrote the source bytes +# into the protected destination (a no-op/short write would also exit 0). +dst_content = (workdir / 'dst').read_text() +if dst_content != "Source\n": + test_fail(f"--inplace did not write the source content into the protected " + f"dst: got {dst_content!r}") diff --git a/testsuite/size-filter_test.py b/testsuite/size-filter_test.py index ce528689..4fa3a500 100644 --- a/testsuite/size-filter_test.py +++ b/testsuite/size-filter_test.py @@ -10,7 +10,7 @@ import os from rsyncfns import ( FROMDIR, TODIR, - assert_exists, assert_not_exists, make_data_file, rmtree, run_rsync, + assert_not_exists, assert_same, make_data_file, rmtree, run_rsync, ) src = FROMDIR @@ -30,21 +30,25 @@ def seed(): # --- --max-size keeps only the small files at every level ------------------- +# Compare content (not just existence) so a kept file is proven to be the +# transferred source, not an empty/stale placeholder. seed() run_rsync('-a', '--max-size=1000', f'{src}/', f'{TODIR}/') -cur = TODIR +dcur, scur = TODIR, src 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}' + assert_same(dcur / f'small{lvl}', scur / f'small{lvl}', + label=f'--max-size kept small L{lvl}') + assert_not_exists(dcur / f'large{lvl}', label=f'--max-size dropped large L{lvl}') + dcur, scur = dcur / f'd{lvl + 1}', scur / 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 +dcur, scur = TODIR, src 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}' + assert_same(dcur / f'large{lvl}', scur / f'large{lvl}', + label=f'--min-size kept large L{lvl}') + assert_not_exists(dcur / f'small{lvl}', label=f'--min-size dropped small L{lvl}') + dcur, scur = dcur / f'd{lvl + 1}', scur / f'd{lvl + 1}' print("size-filter: --max-size / --min-size select correctly at depth")