From cf7d878e7fab6de3c2940049aa4fbe5c680fd9bc Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Fri, 22 May 2026 14:41:17 +1000 Subject: [PATCH 1/2] testsuite: generate predictable fixture files instead of reading /etc, /bin, / The Python rewrite of the suite carried over the shell habit of populating the test tree by capturing "ls -l /etc" / "ls -l /bin" (falling back to "ls /"): hands_setup() built etc-ltr-list / bin-lt-list that way, and longdir_test.py did the same for its leaf files. That ties the fixtures to the host filesystem layout -- those directories are absent or unreadable on Android/Termux and other minimal environments, where "ls /" fails outright -- and the captured content was never reproducible from run to run. Add a deterministic make_text_file() helper to rsyncfns.py and use it for hands_setup()'s two fixture files and longdir's leaf files. The names etc-ltr-list / bin-lt-list are unchanged (chmod, chmod-temp-dir and alt-dest reference them by name); only the content source changes, so the fixtures are now self-contained and identical on every platform. This also drops longdir_test.py's date(1) and ls(1) subprocess calls. Co-Authored-By: Claude Opus 4.7 (1M context) --- testsuite/longdir_test.py | 17 +++++------------ testsuite/rsyncfns.py | 40 ++++++++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/testsuite/longdir_test.py b/testsuite/longdir_test.py index ead1fa4d1..4676c3f1a 100644 --- a/testsuite/longdir_test.py +++ b/testsuite/longdir_test.py @@ -6,10 +6,8 @@ # 175-char directory names, drop a couple of files in the leaf, and # verify that --delete -avH still produces an identical destination. -import os -import subprocess - -from rsyncfns import FROMDIR, TODIR, checkit, hands_setup, test_skipped +from rsyncfns import (FROMDIR, TODIR, checkit, hands_setup, make_text_file, + test_skipped) hands_setup() @@ -29,13 +27,8 @@ except OSError: test_skipped("unable to create files in long directory") -# Drop some recognisably-varied content into the two leaf files. -(longdir / '1').write_text(subprocess.check_output(['date'], text=True)) - -listdir = '/etc' if os.access('/etc', os.R_OK) else '/' -out = subprocess.run(['ls', '-la', listdir], capture_output=True, text=True) -# ls exits 1 if it can't stat some entries (e.g. permission-denied files in -# /etc); the shell test silently accepts that. We do the same. -(longdir / '2').write_text(out.stdout) +# Drop predictable, self-contained content into the two leaf files. +make_text_file(longdir / '1', 50) +make_text_file(longdir / '2', 100) checkit(['--delete', '-avH', f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR) diff --git a/testsuite/rsyncfns.py b/testsuite/rsyncfns.py index 5b42654e5..371c2c251 100644 --- a/testsuite/rsyncfns.py +++ b/testsuite/rsyncfns.py @@ -410,6 +410,26 @@ def make_data_file(path, size: int) -> 'None': f.write(bytes(out)) +def make_text_file(path, lines: int = 100) -> 'None': + """Write a predictable, self-contained text file of `lines` lines. + + This replaces the old habit of capturing `ls -l /etc` / `ls -l /bin` + (falling back to `ls /`) into the test tree. Those tied the fixtures + to the host filesystem layout: the directories are absent or + unreadable on Android/Termux and other minimal environments, where + `ls /` fails outright, and the captured content was never + reproducible. The output here is deterministic and depends on nothing + outside the suite, so every platform builds the identical fixture. + """ + content = ''.join( + "line %06d the quick brown fox jumps over the lazy dog %d %d\n" + % (i, (i * 31) % 97, (i * 131) % 89) + for i in range(1, lines + 1) + ) + with open(str(path), 'w') as f: + f.write(content) + + def get_testuid() -> int: return os.getuid() @@ -677,11 +697,12 @@ def build_symlinks() -> 'None': def hands_setup() -> 'None': - """Equivalent of rsync.fns hands_setup: populate FROMDIR with a varied - tree of files and directories for the canonical 'hands' transfer test. + """Populate FROMDIR with a varied tree of files and directories for the + canonical 'hands' transfer test. - Recreates the shell behavior bit-for-bit so the tls listings match - across the shell and Python halves of the suite during the transition. + All content is generated from within the suite (srcdir contents plus + make_text_file output) so the fixture is self-contained and + reproducible on every platform. """ rmtree(FROMDIR) rmtree(TODIR) @@ -717,15 +738,12 @@ def hands_setup() -> 'None': (FROMDIR / 'dir' / 'subdir').mkdir(exist_ok=True) (FROMDIR / 'dir' / 'subdir' / 'foobar.baz').write_text("some data\n") (FROMDIR / 'dir' / 'subdir' / 'subsubdir').mkdir(exist_ok=True) - - src_listdir = '/etc' if os.access('/etc', os.R_OK) else '/' - out = subprocess.run(['ls', '-ltr', src_listdir], capture_output=True, text=True) - (FROMDIR / 'dir' / 'subdir' / 'subsubdir' / 'etc-ltr-list').write_text(out.stdout) + # Predictable, self-contained fixture files (the names etc-ltr-list / + # bin-lt-list are kept because other tests reference them by name). + make_text_file(FROMDIR / 'dir' / 'subdir' / 'subsubdir' / 'etc-ltr-list', 120) (FROMDIR / 'dir' / 'subdir' / 'subsubdir2').mkdir(exist_ok=True) - src_listdir = '/bin' if os.access('/bin', os.R_OK) else '/' - out = subprocess.run(['ls', '-lt', src_listdir], capture_output=True, text=True) - (FROMDIR / 'dir' / 'subdir' / 'subsubdir2' / 'bin-lt-list').write_text(out.stdout) + make_text_file(FROMDIR / 'dir' / 'subdir' / 'subsubdir2' / 'bin-lt-list', 200) # --- listing / verification ------------------------------------------------ From f09f256cc65e500de4ddb8ac80cb74d8b7505dd4 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Fri, 22 May 2026 14:52:46 +1000 Subject: [PATCH 2/2] testsuite: read xattrs natively instead of shelling out to getfattr xattr_set() sets attributes with the native os.setxattr(), but xattr_dump() read them back by running "getfattr -d". That asymmetry breaks "make check" on any system where rsync is built with xattr support (libattr headers present) but the attr package's CLI tools are not installed -- common on Android/Termux and minimal CI images: setting succeeds via os.setxattr, then xattr_dump's getfattr raises FileNotFoundError, which crashes the test (reported FAIL) instead of running or skipping it. That's why "make check" was failing here on xattrs / xattrs-hlink. Read the xattrs natively with os.listxattr()/os.getxattr() on Linux, symmetric with xattr_set(), so the suite needs no external getfattr; the output still mimics "getfattr -d" and only has to be self-consistent between the source and destination dumps. Cygwin keeps the CLI path (Python there lacks os.*xattr). make check now passes with no attr package installed. Co-Authored-By: Claude Opus 4.7 (1M context) --- testsuite/rsyncfns.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/testsuite/rsyncfns.py b/testsuite/rsyncfns.py index 371c2c251..f151fd5e1 100644 --- a/testsuite/rsyncfns.py +++ b/testsuite/rsyncfns.py @@ -620,7 +620,29 @@ def xattr_dump(*paths) -> str: for comparing a source tree against its rsync'd copy. The format only needs to be self-consistent on a given OS (we never compare across OSes), mirroring the per-OS xls() in the old xattrs.test.""" - if _SYSTEM == 'Linux' or _CYGWIN: + if _SYSTEM == 'Linux': + # Read xattrs natively (symmetric with the os.setxattr used in + # xattr_set) so the suite needs no external getfattr. The attr + # package's CLI tools are frequently absent -- on Android/Termux + # and minimal CI images -- even when the filesystem itself supports + # user xattrs, in which case shelling out to getfattr would crash + # the test instead of exercising it. The output mimics "getfattr + # -d": a "# file:" header then sorted name="value" lines, files + # with no user xattrs omitted. + out = [] + for p in paths: + sp = str(p) + names = sorted(n for n in os.listxattr(sp) if n.startswith('user.')) + if not names: + continue + out.append(f'# file: {sp}\n') + for n in names: + v = os.getxattr(sp, n).decode('utf-8', 'surrogateescape') + out.append(f'{n}="{v}"\n') + out.append('\n') + return ''.join(out) + if _CYGWIN: + # Python on Cygwin lacks os.*xattr, so use the CLI there. return subprocess.check_output( ['getfattr', '-d', *(str(p) for p in paths)], text=True) if _SYSTEM == 'Darwin':