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..f151fd5e1 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() @@ -600,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': @@ -677,11 +719,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 +760,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 ------------------------------------------------