Files
rsync/testsuite/inplace_test.py
Andrew Tridgell d6124a82a4 testsuite: cross-directory/temp/backup/dest coverage at depth
Fill the highest-restructure-risk gap: options that do two-directory / rename /
outside-tree work, asserted at >=3 levels deep with the aux tree kept outside
the main tree, and asserting the option's specific property rather than just
tree equality (which the ported tests already cover).

  alt-dest-deep  --link-dest hardlinks unchanged files (same inode), --copy-dest
                 copies (never links), --compare-dest omits unchanged files;
                 ref tree outside both src and dest.
  temp-dir       cross-dir temp->final rename at depth; temp dir left clean; a
                 missing --temp-dir fails (so the option is proven consulted).
  partial        --partial keeps the partial in the dest file; relative
                 --partial-dir stages per-directory at depth (pre-seed +
                 interrupt/resume); absolute --partial-dir writes the partial
                 outside the tree.
  inplace        --inplace keeps the destination inode across a delta update;
                 the default temp+rename path replaces it.
  append         --append completes truncated files tail-only; --append-verify
                 repairs a corrupted prefix (protocol >= 30).
  backup-deep    --suffix saves <name>S beside the new file; --backup-dir
                 relocates old files to a parallel deep tree outside the dest
                 and captures deletions under --delete.

All green on master and under --protocol=29/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00

72 lines
2.4 KiB
Python

#!/usr/bin/env python3
"""Coverage of --inplace at depth.
--inplace updates the destination file directly instead of writing a temp copy
and renaming it over the original, so across a delta update the destination
keeps the SAME inode. Without --inplace the receiver creates a fresh temp file
and renames it, giving the destination a NEW inode. Both behaviours hinge on
how the receiver resolves the destination directory and (for the default mode)
performs the temp->final rename, which the path restructure rewrites; verify
them on a file >=3 levels deep.
"""
import os
from rsyncfns import (
FROMDIR, TODIR,
assert_same, make_tree, rmtree, run_rsync, test_fail,
)
src = FROMDIR
deep = os.path.join('d1', 'd2', 'd3', 'f3')
def seed():
rmtree(src)
rmtree(TODIR)
make_tree(src, depth=3, data=True, data_size=200000)
def inode(path):
return os.stat(path).st_ino
def modify_deep():
# Rewrite a chunk in the middle so it is a genuine delta, not just a tail
# append. Bump mtime by a clear margin (the whole test runs inside one
# second, so a "now" touch would collide with the destination's mtime and
# the size-unchanged file would be skipped by the quick check).
p = src / deep
data = bytearray(p.read_bytes())
data[1000:1100] = bytes((b ^ 0xFF) for b in data[1000:1100])
p.write_bytes(bytes(data))
st = os.stat(p)
os.utime(p, (st.st_atime, st.st_mtime + 100))
# --- --inplace keeps the destination inode across a delta update ------------
seed()
run_rsync('-a', f'{src}/', f'{TODIR}/')
ino_before = inode(TODIR / deep)
modify_deep()
run_rsync('-a', '--inplace', '--no-whole-file', f'{src}/', f'{TODIR}/')
assert_same(TODIR / deep, src / deep, label='inplace content')
if inode(TODIR / deep) != ino_before:
test_fail("--inplace changed the destination inode at depth "
f"({ino_before} -> {inode(TODIR / deep)})")
# --- control: the default (temp+rename) path replaces the inode -------------
seed()
run_rsync('-a', f'{src}/', f'{TODIR}/')
ino_before = inode(TODIR / deep)
modify_deep()
run_rsync('-a', '--no-whole-file', f'{src}/', f'{TODIR}/')
assert_same(TODIR / deep, src / deep, label='default content')
if inode(TODIR / deep) == ino_before:
test_fail("default (non-inplace) delta update unexpectedly kept the "
"destination inode at depth -- temp+rename did not run")
print("inplace: same-inode update at depth verified; default replaces inode")