mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-15 14:51:20 -04:00
An LD_PRELOAD hook refuses linkat() for a symlink source only, so the arm is reachable on a filesystem that hard-links symlinks perfectly well. Three controls keep it from proving less than it looks: - the regular file in the same transfer must still be hard-linked, or "it fell back" would also be satisfied by --link-dest having been abandoned; - the itemised run must emit exactly one "cL... sym -> some-target" line. A plain -a run cannot see a duplicated itemisation, which is how that defect reached an HFS+ target before this was added; - EPERM and ENOSYS must fall back too, since errno does not separate "cannot" from "may not". itemize picked its expected change-type letter from the build capability, which is the wrong question -- the link happens on whichever filesystem holds the test data. Ask that one too, and drop the XFAIL the old mismatch needed. The hook is Linux-only, so the test joins the macOS and Cygwin skip lists, which are required to be sorted.
254 lines
9.1 KiB
Python
254 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
# Python rewrite of testsuite/itemize.test.
|
|
#
|
|
# Test the output of various copy commands to ensure itemized output
|
|
# (-i, -ii) and double-verbose output (-vv) match the canonical
|
|
# representations across whole-file and delta paths.
|
|
|
|
import os
|
|
import shutil
|
|
|
|
from rsyncfns import (
|
|
CHKFILE, FROMDIR, RSYNC, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,
|
|
all_plus, allspace, dots,
|
|
checkdiff, cp_p, makepath, run_rsync, v_filt, hardlink_symlinks_supported,
|
|
)
|
|
|
|
|
|
to2dir = TMPDIR / 'to2'
|
|
|
|
makepath(FROMDIR / 'foo', FROMDIR / 'bar' / 'baz')
|
|
cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config1')
|
|
cp_p(SRCDIR / 'config.sub', FROMDIR / 'foo' / 'config2')
|
|
cp_p(SRCDIR / 'rsync.h', FROMDIR / 'bar' / 'baz' / 'rsync')
|
|
os.chmod(FROMDIR / 'foo' / 'config1', 0o600)
|
|
os.chmod(FROMDIR / 'foo' / 'config2', 0o600)
|
|
os.chmod(FROMDIR / 'bar' / 'baz' / 'rsync', 0o600)
|
|
|
|
old_umask = os.umask(0)
|
|
try:
|
|
os.symlink('../bar/baz/rsync', FROMDIR / 'foo' / 'sym')
|
|
finally:
|
|
os.umask(old_umask)
|
|
|
|
os.link(FROMDIR / 'foo' / 'config1', FROMDIR / 'foo' / 'extra')
|
|
if to2dir.is_file():
|
|
to2dir.unlink()
|
|
|
|
|
|
# Detect what this rsync build supports.
|
|
vv = run_rsync('-VV', check=True, capture_output=True).stdout
|
|
hardlink_symlinks = '"hardlink_symlinks": true' in vv
|
|
symtimes_supported = '"symtimes": true' in vv
|
|
# The only itemisation difference between a platform that can hard-link a
|
|
# symlink and one that cannot is the change-type letter where the symlink
|
|
# itself is transferred: hL when it is hard-linked, cL when it is copied.
|
|
# The attribute field, the "is uptodate" wording and the --copy-dest lines
|
|
# are identical either way -- measured on macOS 10.13, the one platform that
|
|
# lacks the capability (linkat(AT_FDCWD, sym, ..., 0) is EOPNOTSUPP there).
|
|
#
|
|
# The two answers can disagree, so both are asked. The build capability is
|
|
# decided by configure on whatever filesystem the source tree sat on, while the
|
|
# link happens on whichever filesystem holds the test data: macOS builds on APFS
|
|
# (yes) and can write to HFS+ (ENOTSUP). rsync falls back to a copy when the
|
|
# filesystem refuses, so the letter follows the filesystem.
|
|
L = ('hL' if hardlink_symlinks and hardlink_symlinks_supported(SCRATCHDIR)
|
|
else 'cL')
|
|
|
|
if 'protocol=2' in RSYNC:
|
|
T = '.T'
|
|
elif symtimes_supported:
|
|
T = '.t'
|
|
else:
|
|
T = '.T'
|
|
|
|
|
|
# First check: -iplr basic itemize on a fresh transfer.
|
|
checkdiff(['-iplr', f'{FROMDIR}/', f'{TODIR}/'],
|
|
f"created directory {TODIR}\n"
|
|
f"cd{all_plus} ./\n"
|
|
f"cd{all_plus} bar/\n"
|
|
f"cd{all_plus} bar/baz/\n"
|
|
f">f{all_plus} bar/baz/rsync\n"
|
|
f"cd{all_plus} foo/\n"
|
|
f">f{all_plus} foo/config1\n"
|
|
f">f{all_plus} foo/config2\n"
|
|
f">f{all_plus} foo/extra\n"
|
|
f"cL{all_plus} foo/sym -> ../bar/baz/rsync\n")
|
|
|
|
# Touch dir times so subsequent itemize diffs don't pick up dir-time noise.
|
|
run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))
|
|
|
|
# Permute one file's content + mode; expect a content/mode itemize.
|
|
cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config2')
|
|
os.chmod(FROMDIR / 'foo' / 'config2', 0o601)
|
|
|
|
checkdiff(['-iplrH', f'{FROMDIR}/', f'{TODIR}/'],
|
|
f">f..T.{dots} bar/baz/rsync\n"
|
|
f">f..T.{dots} foo/config1\n"
|
|
f">f.sTp{dots} foo/config2\n"
|
|
f"hf..T.{dots} foo/extra => foo/config1\n")
|
|
|
|
# Re-touch dirs, permute config2 again and replace the symlink target.
|
|
run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))
|
|
cp_p(SRCDIR / 'config.sub', FROMDIR / 'foo' / 'config2')
|
|
import time
|
|
time.sleep(1) # to provoke a directory mtime change below
|
|
(TODIR / 'foo' / 'sym').unlink()
|
|
old_umask = os.umask(0)
|
|
try:
|
|
os.symlink('../bar/baz', TODIR / 'foo' / 'sym')
|
|
finally:
|
|
os.umask(old_umask)
|
|
os.chmod(FROMDIR / 'foo' / 'config2', 0o600)
|
|
os.chmod(TODIR / 'bar' / 'baz' / 'rsync', 0o777)
|
|
|
|
checkdiff(['-iplrtc', f'{FROMDIR}/', f'{TODIR}/'],
|
|
f".f..tp{dots} bar/baz/rsync\n"
|
|
f".d..t.{dots} foo/\n"
|
|
f".f..t.{dots} foo/config1\n"
|
|
f">fcstp{dots} foo/config2\n"
|
|
f"cLc{T}.{dots} foo/sym -> ../bar/baz/rsync\n")
|
|
|
|
# Re-permute config2, leaving the others untouched; lack of -t is for
|
|
# the unchanged-hard-link stress test.
|
|
cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config2')
|
|
os.chmod(FROMDIR / 'foo' / 'config2', 0o600)
|
|
|
|
checkdiff(['-vvplrH', f'{FROMDIR}/', f'{TODIR}/'],
|
|
"bar/baz/rsync is uptodate\n"
|
|
"foo/config1 is uptodate\n"
|
|
"foo/extra is uptodate\n"
|
|
"foo/sym is uptodate\n"
|
|
"foo/config2\n",
|
|
filter=v_filt)
|
|
|
|
# Touch a mode change on one dest file then run -ii to see "no change".
|
|
os.chmod(TODIR / 'bar' / 'baz' / 'rsync', 0o747)
|
|
run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))
|
|
|
|
checkdiff(['-ivvplrtH', f'{FROMDIR}/', f'{TODIR}/'],
|
|
f".d{allspace} ./\n"
|
|
f".d{allspace} bar/\n"
|
|
f".d{allspace} bar/baz/\n"
|
|
f".f...p{dots} bar/baz/rsync\n"
|
|
f".d{allspace} foo/\n"
|
|
f".f{allspace} foo/config1\n"
|
|
f">f..t.{dots} foo/config2\n"
|
|
f"hf{allspace} foo/extra\n"
|
|
f".L{allspace} foo/sym -> ../bar/baz/rsync\n",
|
|
filter=v_filt)
|
|
|
|
# Permute one perm and re-touch a file; expect just those two itemizes.
|
|
os.chmod(TODIR / 'foo' / 'config1', 0o757)
|
|
(TODIR / 'foo' / 'config2').touch()
|
|
checkdiff(['-vplrtH', f'{FROMDIR}/', f'{TODIR}/'],
|
|
"foo/config2\n",
|
|
filter=v_filt)
|
|
|
|
os.chmod(TODIR / 'foo' / 'config1', 0o757)
|
|
(TODIR / 'foo' / 'config2').touch()
|
|
checkdiff(['-iplrtH', f'{FROMDIR}/', f'{TODIR}/'],
|
|
f".f...p{dots} foo/config1\n"
|
|
f">f..t.{dots} foo/config2\n")
|
|
|
|
|
|
# --copy-dest variants.
|
|
checkdiff(['-ivvplrtH', '--copy-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
|
|
f"cd{allspace} ./\n"
|
|
f"cd{allspace} bar/\n"
|
|
f"cd{allspace} bar/baz/\n"
|
|
f"cf{allspace} bar/baz/rsync\n"
|
|
f"cd{allspace} foo/\n"
|
|
f"cf{allspace} foo/config1\n"
|
|
f"cf{allspace} foo/config2\n"
|
|
f"hf{allspace} foo/extra => foo/config1\n"
|
|
f"cL{allspace} foo/sym -> ../bar/baz/rsync\n",
|
|
filter=v_filt)
|
|
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-iplrtH', '--copy-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
|
|
f"created directory {to2dir}\n"
|
|
f"hf{allspace} foo/extra => foo/config1\n")
|
|
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-vvplrtH', f'--copy-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
|
|
"./ is uptodate\n"
|
|
"bar/ is uptodate\n"
|
|
"bar/baz/ is uptodate\n"
|
|
"bar/baz/rsync is uptodate\n"
|
|
"foo/ is uptodate\n"
|
|
"foo/config1 is uptodate\n"
|
|
"foo/config2 is uptodate\n"
|
|
f"foo/sym is uptodate\n"
|
|
"foo/extra => foo/config1\n",
|
|
filter=v_filt)
|
|
|
|
|
|
# --link-dest variants.
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-ivvplrtH', f'--link-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
|
|
f"cd{allspace} ./\n"
|
|
f"cd{allspace} bar/\n"
|
|
f"cd{allspace} bar/baz/\n"
|
|
f"hf{allspace} bar/baz/rsync\n"
|
|
f"cd{allspace} foo/\n"
|
|
f"hf{allspace} foo/config1\n"
|
|
f"hf{allspace} foo/config2\n"
|
|
f"hf{allspace} foo/extra => foo/config1\n"
|
|
f"{L}{allspace} foo/sym -> ../bar/baz/rsync\n",
|
|
filter=v_filt)
|
|
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-iplrtH', '--dry-run', '--link-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
|
|
f"created directory {to2dir}\n")
|
|
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-iplrtH', '--link-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],
|
|
f"created directory {to2dir}\n")
|
|
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-vvplrtH', f'--link-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
|
|
"./ is uptodate\n"
|
|
"bar/ is uptodate\n"
|
|
"bar/baz/ is uptodate\n"
|
|
"bar/baz/rsync is uptodate\n"
|
|
"foo/ is uptodate\n"
|
|
"foo/config1 is uptodate\n"
|
|
"foo/config2 is uptodate\n"
|
|
"foo/extra is uptodate\n"
|
|
f"foo/sym is uptodate\n",
|
|
filter=v_filt)
|
|
|
|
|
|
# --compare-dest variants.
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-ivvplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
|
|
f"cd{allspace} ./\n"
|
|
f"cd{allspace} bar/\n"
|
|
f"cd{allspace} bar/baz/\n"
|
|
f".f{allspace} bar/baz/rsync\n"
|
|
f"cd{allspace} foo/\n"
|
|
f".f{allspace} foo/config1\n"
|
|
f".f{allspace} foo/config2\n"
|
|
f".f{allspace} foo/extra\n"
|
|
f".L{allspace} foo/sym -> ../bar/baz/rsync\n",
|
|
filter=v_filt)
|
|
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-iplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
|
|
f"created directory {to2dir}\n")
|
|
|
|
shutil.rmtree(to2dir, ignore_errors=True)
|
|
checkdiff(['-vvplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],
|
|
"./ is uptodate\n"
|
|
"bar/ is uptodate\n"
|
|
"bar/baz/ is uptodate\n"
|
|
"bar/baz/rsync is uptodate\n"
|
|
"foo/ is uptodate\n"
|
|
"foo/config1 is uptodate\n"
|
|
"foo/config2 is uptodate\n"
|
|
"foo/extra is uptodate\n"
|
|
f"foo/sym is uptodate\n",
|
|
filter=v_filt)
|