|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""--read-batch process substitution /dev/fd/ pipe must not crash with strict file-type checks.""" |
| 3 | + |
| 4 | +import os |
| 5 | +import shlex |
| 6 | +import shutil |
| 7 | +import subprocess |
| 8 | +import tempfile |
| 9 | +from pathlib import Path |
| 10 | + |
| 11 | +from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped |
| 12 | + |
| 13 | +# We require bash specifically because standard POSIX /bin/sh does not |
| 14 | +# guarantee support for <(...) process substitution syntax. |
| 15 | +bash = shutil.which('bash') |
| 16 | +if bash is None: |
| 17 | + test_skipped('bash is unavailable, cannot test process substitution') |
| 18 | + |
| 19 | +# Verify the host bash actually supports process substitution |
| 20 | +probe = subprocess.run( |
| 21 | + [bash, '-c', 'cat <(echo "probe")'], |
| 22 | + capture_output=True) |
| 23 | + |
| 24 | +if probe.returncode != 0: |
| 25 | + test_skipped('bash process substitution is not supported on this system') |
| 26 | + |
| 27 | +base = Path(SCRATCHDIR / 'rsync-batch-fifo') |
| 28 | +src = base / 'src' |
| 29 | +dest = base / 'dest' |
| 30 | +batch_file = base / 'update.batch' |
| 31 | +makepath(src, dest) |
| 32 | + |
| 33 | +# 1. Create dummy data |
| 34 | +(src / 'payload.txt').write_text('batch payload data\n') |
| 35 | + |
| 36 | +# 2. Generate a valid batch file so `cat` actually has a real file to read. |
| 37 | +# Note: This operation also copies the file to `dest` as a side effect. |
| 38 | +subprocess.run([*rsync_argv('-a', f'--write-batch={batch_file}'), f'{src}/', f'{dest}/'], check=True) |
| 39 | + |
| 40 | +# must wipe and recreate the destination directory so the test can |
| 41 | +# properly prove that --read-batch recreates the files from scratch. |
| 42 | +rmtree(dest) |
| 43 | +makepath(dest) |
| 44 | + |
| 45 | +# 3. Now we can test reading it via bash process substitution |
| 46 | +rsync_base_cmd = shlex.join(rsync_argv('-a')) |
| 47 | +batch_path = shlex.quote(str(batch_file)) |
| 48 | +dest_path = shlex.quote(str(dest) + '/') |
| 49 | + |
| 50 | +# Construct the bash command: rsync -a --read-batch=<(cat /path/to/batch) /dest/ |
| 51 | +bash_script = f"{rsync_base_cmd} --read-batch=<(cat {batch_path}) {dest_path}" |
| 52 | + |
| 53 | +try: |
| 54 | + proc_read = subprocess.run( |
| 55 | + [bash, '-c', bash_script], |
| 56 | + capture_output=True, |
| 57 | + text=True, |
| 58 | + timeout=10, |
| 59 | + ) |
| 60 | +except subprocess.TimeoutExpired: |
| 61 | + rmtree(base) |
| 62 | + test_fail('process substitution batch test timed out') |
| 63 | + |
| 64 | +ctx = f'rc={proc_read.returncode}, stderr={proc_read.stderr.strip()!r}' |
| 65 | + |
| 66 | +# Evaluate result against the strict S_ISREG check bug |
| 67 | +if proc_read.returncode != 0: |
| 68 | + rmtree(base) |
| 69 | + test_fail(f'rsync crashed reading batch file from pipe ({ctx})') |
| 70 | + |
| 71 | +if not (dest / 'payload.txt').is_file(): |
| 72 | + rmtree(base) |
| 73 | + test_fail(f'rsync exited successfully but payload is missing in target ({ctx})') |
| 74 | + |
| 75 | +rmtree(base) |
| 76 | +print('rsync successfully parsed batch stream via process substitution pseudo-path') |
| 77 | +raise SystemExit(0) |
0 commit comments