mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-10-10 02:51:15 -04:00
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>
131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Coverage of --partial / --partial-dir at depth and across directory
|
|
boundaries.
|
|
|
|
--partial keeps a partially transferred file so a later run can resume it.
|
|
--partial-dir=DIR keeps the partial in DIR instead of the destination file:
|
|
a RELATIVE dir is created in (and removed from) each destination file's own
|
|
directory; an ABSOLUTE dir is a reserved location that holds partials by
|
|
basename. All of this is parent- and cross-directory path resolution -- what
|
|
the resolver restructure rewrites -- so exercise it on a file several levels
|
|
deep, with the absolute partial-dir kept OUTSIDE the destination tree.
|
|
|
|
Note: a *delta* resume from an absolute partial-dir currently fails whole-file
|
|
verification on master (it re-puts the partial and never converges). This test
|
|
therefore only asserts the cross-directory WRITE of the partial for that case
|
|
and completes it with --whole-file, which is the clearly-correct baseline.
|
|
"""
|
|
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import time
|
|
|
|
from rsyncfns import (
|
|
FROMDIR, SCRATCHDIR, TODIR,
|
|
assert_same, make_data_file, makepath, rmtree, rsync_argv, run_rsync,
|
|
test_fail,
|
|
)
|
|
|
|
src = FROMDIR
|
|
deepdir = os.path.join('d1', 'd2', 'd3')
|
|
deep = os.path.join(deepdir, 'f3')
|
|
FULL = 12_000_000
|
|
|
|
|
|
def seed_big():
|
|
rmtree(src)
|
|
rmtree(TODIR)
|
|
makepath(src / deepdir)
|
|
make_data_file(src / deep, FULL)
|
|
|
|
|
|
def is_prefix(partial) -> bool:
|
|
pb = partial.read_bytes()
|
|
return 0 < len(pb) < FULL and (src / deep).read_bytes()[:len(pb)] == pb
|
|
|
|
|
|
def interrupt_transfer(extra_args, partial_path):
|
|
"""Start a deliberately slow transfer, SIGTERM it once the receiver's
|
|
in-progress temp (.f3.XXXXXX) has some data, and wait for `partial_path`
|
|
(where this mode keeps the partial) to materialise.
|
|
|
|
The bandwidth limit is low so the multi-second transfer cannot finish
|
|
before we interrupt it -- important under a loaded parallel run (-j16),
|
|
where the polling loop can lag by seconds. We then poll for the partial,
|
|
since rsync moves it into place from its exit_cleanup handler."""
|
|
proc = subprocess.Popen(
|
|
rsync_argv('-a', '--no-whole-file', '--bwlimit=400', *extra_args,
|
|
f'{src}/', f'{TODIR}/'),
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
tdir = TODIR / deepdir
|
|
caught = False
|
|
deadline = time.monotonic() + 30
|
|
while time.monotonic() < deadline:
|
|
if proc.poll() is not None:
|
|
break # exited before we caught it
|
|
if tdir.is_dir():
|
|
temps = [p for p in tdir.glob('.f3.*')
|
|
if p.is_file() and p.stat().st_size > 0]
|
|
if temps:
|
|
caught = True
|
|
break
|
|
time.sleep(0.02)
|
|
proc.send_signal(signal.SIGTERM)
|
|
proc.wait()
|
|
if not caught:
|
|
test_fail("never caught an in-progress temp (transfer finished too "
|
|
"fast to interrupt)")
|
|
# rsync moves the partial into place from exit_cleanup; give it a moment.
|
|
pdeadline = time.monotonic() + 5
|
|
while time.monotonic() < pdeadline:
|
|
if partial_path.is_file() and partial_path.stat().st_size > 0:
|
|
return
|
|
time.sleep(0.05)
|
|
|
|
|
|
# --- 1. --partial (no dir): partial kept in the dest file itself, at depth --
|
|
seed_big()
|
|
interrupt_transfer(['--partial'], TODIR / deep)
|
|
if not (TODIR / deep).is_file() or not is_prefix(TODIR / deep):
|
|
test_fail("--partial did not leave a valid partial in the dest file")
|
|
run_rsync('-a', '--partial', '--no-whole-file', f'{src}/', f'{TODIR}/')
|
|
assert_same(TODIR / deep, src / deep, label='--partial resume')
|
|
|
|
# --- 2. relative --partial-dir at depth: deterministic clean pre-seed -------
|
|
rmtree(src)
|
|
rmtree(TODIR)
|
|
makepath(src / deepdir, TODIR / deepdir / '.rsync-partial')
|
|
make_data_file(src / deep, 1_000_000)
|
|
full = (src / deep).read_bytes()
|
|
(TODIR / deepdir / '.rsync-partial' / 'f3').write_bytes(full[:400_000])
|
|
run_rsync('-a', '--partial-dir=.rsync-partial', '--no-whole-file',
|
|
f'{src}/', f'{TODIR}/')
|
|
assert_same(TODIR / deep, src / deep, label='rel partial-dir preseed')
|
|
if (TODIR / deepdir / '.rsync-partial').exists():
|
|
test_fail("relative --partial-dir not removed after the partial was used")
|
|
|
|
# --- 3. relative --partial-dir at depth: interrupt then resume -------------
|
|
seed_big()
|
|
part = TODIR / deepdir / '.rsync-partial' / 'f3'
|
|
interrupt_transfer(['--partial-dir=.rsync-partial'], part)
|
|
if not part.is_file() or not is_prefix(part):
|
|
test_fail("relative --partial-dir did not keep a valid partial at depth")
|
|
run_rsync('-a', '--partial-dir=.rsync-partial', '--no-whole-file',
|
|
f'{src}/', f'{TODIR}/')
|
|
assert_same(TODIR / deep, src / deep, label='rel partial-dir resume')
|
|
|
|
# --- 4. absolute --partial-dir OUTSIDE the tree (cross-dir): interrupt -----
|
|
ext = SCRATCHDIR / 'partials' # sibling of from/ and to/ -- outside both
|
|
rmtree(ext)
|
|
ext.mkdir()
|
|
seed_big()
|
|
interrupt_transfer([f'--partial-dir={ext}'], ext / 'f3')
|
|
if not (ext / 'f3').is_file() or not is_prefix(ext / 'f3'):
|
|
test_fail("absolute --partial-dir did not write the partial to the "
|
|
"outside-tree dir")
|
|
run_rsync('-a', f'--partial-dir={ext}', '--whole-file', f'{src}/', f'{TODIR}/')
|
|
assert_same(TODIR / deep, src / deep, label='abs partial-dir resume')
|
|
|
|
print("partial: --partial + relative/absolute --partial-dir verified at depth")
|