From 0d4fb1bc890573c930daa6630bf9b15e447549b8 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sun, 24 May 2026 13:36:32 +1000 Subject: [PATCH] testsuite: cover more path/file-operation code (syscall.c, util1.c, delete.c) Target previously-uncovered functions in the path/file-operation files the resolver restructure touches, confirmed hit under coverage: preallocate --preallocate (syscall.c do_fallocate) and sparse hole-punching via --preallocate --sparse and --inplace --sparse (do_punch_hole), on a file several levels deep. fuzzy-basis --fuzzy basis selection with similar-named candidates and no exact match, so the generator scores them (util1.c fuzzy_distance). delete-deep add a --backup --delete case so removing an extraneous backup-suffixed file consults delete.c is_backup_file. preallocate probes --preallocate support up front and skips where it is unavailable: macOS, the *BSDs and Solaris build without fallocate/posix_fallocate (and FALLOC_FL_PUNCH_HOLE is Linux-only), and reject the option outright. It runs on Linux and Cygwin. fuzzy-basis and delete-deep are plain local transfers with no skips. All green on master and under --protocol=29/30. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/macos-build.yml | 2 +- testsuite/delete-deep_test.py | 28 ++++++++++-- testsuite/fuzzy-basis_test.py | 37 +++++++++++++++ testsuite/preallocate_test.py | 75 +++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 testsuite/fuzzy-basis_test.py create mode 100644 testsuite/preallocate_test.py diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index a87c276e..6af741fd 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -44,7 +44,7 @@ jobs: # chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on macOS # (rsyncfns.py drives xattrs via the `xattr` command), verified on a # real macOS host, so they're no longer in the skip set. - run: sudo RSYNC_EXPECT_SKIPPED=acls-default,acls-depth,chmod-temp-dir,daemon-access-ip,daemon-chroot-acl,dir-sgid,open-noatime,protected-regular,proxy-response-line-too-long,simd-checksum,sparse make check + run: sudo RSYNC_EXPECT_SKIPPED=acls-default,acls-depth,chmod-temp-dir,daemon-access-ip,daemon-chroot-acl,dir-sgid,open-noatime,preallocate,protected-regular,proxy-response-line-too-long,simd-checksum,sparse make check - name: check (TCP daemon transport) # Second run with daemon tests over a real loopback rsyncd; the default # 'make check' above uses the secure stdio-pipe transport. diff --git a/testsuite/delete-deep_test.py b/testsuite/delete-deep_test.py index 3525c2e3..b8e01587 100644 --- a/testsuite/delete-deep_test.py +++ b/testsuite/delete-deep_test.py @@ -12,8 +12,8 @@ import os from rsyncfns import ( FROMDIR, TODIR, - assert_not_exists, assert_same, make_tree, makepath, rmtree, run_rsync, - test_fail, walk_files, + assert_exists, assert_not_exists, assert_same, make_tree, makepath, rmtree, + run_rsync, test_fail, walk_files, ) src = FROMDIR @@ -82,4 +82,26 @@ if (TODIR / 'd1' / 'f1').read_text() != "KEEP THIS\n": test_fail("--ignore-existing overwrote an existing deep file") assert_same(TODIR / 'f0', src / 'f0', label='--ignore-existing created new file') -print("delete-deep: delete family, max-delete, existing/ignore-existing at depth") +# --- --backup --delete consults is_backup_file ------------------------------- +# Under --backup with no --backup-dir, an extraneous file is backed up to +# ~ before removal, but a name that already ends in the backup suffix is +# unlinked directly rather than re-backed-up to ~~ (delete.c +# is_backup_file). rsync auto-protects *~ from deletion, so without an explicit +# "risk" rule the suffixed file is never even a deletion candidate and that +# branch is unreachable; the R rule un-protects it so the branch actually runs. +rels = seed_src() +fresh_dest() +d = TODIR / 'd1' / 'd2' +(d / 'plain').write_text("extraneous\n") # normal -> backed up to plain~ +(d / 'stale~').write_text("already a backup\n") # suffixed -> unlinked, no stale~~ +run_rsync('-a', '-b', '--delete', '--filter=R *~', f'{src}/', f'{TODIR}/') +assert_not_exists(d / 'plain', label='--backup --delete removed extraneous') +assert_exists(d / 'plain~', label='--backup backed up the extraneous file') +assert_not_exists(d / 'stale~', label='--backup --delete removed suffixed orphan') +assert_not_exists(d / 'stale~~', + label='is_backup_file: already-suffixed file unlinked, not re-backed-up') +for rel in rels: + assert_same(TODIR / rel, src / rel, label=f'--backup --delete kept {rel}') + +print("delete-deep: delete family, max-delete, existing/ignore-existing, " + "backup-delete at depth") diff --git a/testsuite/fuzzy-basis_test.py b/testsuite/fuzzy-basis_test.py new file mode 100644 index 00000000..77889439 --- /dev/null +++ b/testsuite/fuzzy-basis_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Coverage of --fuzzy basis selection scoring (util1.c fuzzy_distance). + +When the destination has no exact match for a source file, --fuzzy makes the +generator score the same-directory candidates by name similarity (fuzzy_distance) +and use the closest as the delta basis. Set this up at depth with several +similar-named candidates so the scorer actually runs. +""" + +import os + +from rsyncfns import ( + FROMDIR, TODIR, + assert_same, make_data_file, makepath, rmtree, run_rsync, +) + +src = FROMDIR +deepdir = os.path.join('d1', 'd2') +newfile = os.path.join(deepdir, 'archive-v2.tar') + +rmtree(src) +rmtree(TODIR) +makepath(src / deepdir, TODIR / deepdir) + +make_data_file(src / newfile, 300_000) +base = (src / newfile).read_bytes() + +# Destination has NO 'archive-v2.tar', but several similar-named candidates that +# are mostly identical to it -- so fuzzy must score them by name distance. +(TODIR / deepdir / 'archive-v1.tar').write_bytes(base[:280_000] + b'older tail data') +(TODIR / deepdir / 'archive-old.tar').write_bytes(base[:200_000]) +(TODIR / deepdir / 'unrelated.dat').write_bytes(b'nothing alike' * 1000) + +run_rsync('-a', '--fuzzy', '--no-whole-file', f'{src}/', f'{TODIR}/') +assert_same(TODIR / newfile, src / newfile, label='fuzzy result') + +print("fuzzy-basis: --fuzzy candidate scoring (fuzzy_distance) verified at depth") diff --git a/testsuite/preallocate_test.py b/testsuite/preallocate_test.py new file mode 100644 index 00000000..4e2da536 --- /dev/null +++ b/testsuite/preallocate_test.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Coverage of the file-allocation syscalls in syscall.c at depth: +do_fallocate (--preallocate) and do_punch_hole (sparse writes). + +These are receiver-side file operations the resolver restructure also touches. +Where the filesystem lacks fallocate/punch-hole the calls warn and the transfer +still completes, so the content assertions hold regardless; the coverage is +gained wherever the kernel supports them. +""" + +import os + +from rsyncfns import ( + FROMDIR, TODIR, + assert_same, make_data_file, makepath, rmtree, run_rsync, test_skipped, +) + +src = FROMDIR +deep = os.path.join('d1', 'd2', 'd3', 'f') + +# --preallocate needs fallocate/posix_fallocate, and do_punch_hole needs +# FALLOC_FL_PUNCH_HOLE -- both Linux (and Cygwin) features. macOS, the *BSDs and +# Solaris build without preallocation and reject the option outright ("prealloc- +# ation is not supported"), so probe once with a trivial transfer and skip the +# whole test where it's unavailable. +rmtree(src) +rmtree(TODIR) +makepath(src) +(src / 'probe').write_text("x\n") +if run_rsync('-a', '--preallocate', f'{src}/', f'{TODIR}/', + check=False, capture_output=True).returncode != 0: + test_skipped("--preallocate not supported on this platform") + + +def seed_plain(size=1_000_000): + rmtree(src) + rmtree(TODIR) + makepath(src / 'd1' / 'd2' / 'd3') + make_data_file(src / deep, size) + + +def seed_holey(head=4096, hole=2 * 1024 * 1024, tail=4096): + rmtree(src) + rmtree(TODIR) + makepath(src / 'd1' / 'd2' / 'd3') + with open(src / deep, 'wb') as f: + f.write(os.urandom(head)) + f.write(b'\0' * hole) # a real zero run for the sparse writer + f.write(os.urandom(tail)) + + +# --- --preallocate: do_fallocate on the receiver ---------------------------- +seed_plain() +run_rsync('-a', '--preallocate', f'{src}/', f'{TODIR}/') +assert_same(TODIR / deep, src / deep, label='--preallocate content') + +# --- --preallocate --sparse on a holey file: do_fallocate + do_punch_hole --- +seed_holey() +run_rsync('-a', '--preallocate', '--sparse', f'{src}/', f'{TODIR}/') +assert_same(TODIR / deep, src / deep, label='--preallocate --sparse content') + +# --- --inplace --sparse update that introduces a zero run: do_punch_hole ---- +# (sparse_end's updating_basis_or_equiv branch punches the hole in place.) +seed_plain() +run_rsync('-a', f'{src}/', f'{TODIR}/') # dest starts fully populated +data = bytearray((src / deep).read_bytes()) +data[200_000:800_000] = b'\0' * 600_000 # same size, new zero run +(src / deep).write_bytes(bytes(data)) +st = os.stat(src / deep) +os.utime(src / deep, (st.st_atime, st.st_mtime + 100)) # force a delta update +run_rsync('-a', '--inplace', '--sparse', '--no-whole-file', f'{src}/', f'{TODIR}/') +assert_same(TODIR / deep, src / deep, label='--inplace --sparse content') + +print("preallocate: --preallocate (do_fallocate) + sparse hole-punching " + "(do_punch_hole) verified at depth")