73 lines
3.8 KiB
Python
73 lines
3.8 KiB
Python
"""Real CLI restarts: failed-fetch must retain valid per-instance results."""
|
|
from pathlib import Path
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
from test_public_config import make_rsync_wrapper, read_json, run_checked, write_config
|
|
|
|
|
|
def run(binary: str, evidence: Path | None = None, expect_no_cache: bool = False) -> dict:
|
|
proof = {'runs': []}
|
|
with tempfile.TemporaryDirectory(prefix='panda-failed-fetch-') as temp:
|
|
base = Path(temp)
|
|
fixture = base / 'fixture'
|
|
run_checked([sys.executable, str(Path(__file__).with_name('generate_repository.py')),
|
|
'--output', str(fixture), '--rsync-host', '127.0.0.1', '--strict-names'])
|
|
repository = base / 'repository'
|
|
shutil.copytree(fixture / 'cases/baseline-v1/repository', repository)
|
|
wrapper = base / 'rsync.py'
|
|
log = base / 'rsync.log'
|
|
make_rsync_wrapper(wrapper, fixture, log)
|
|
wrapper.write_text(wrapper.read_text().replace(str(fixture / 'cases/baseline-v1/repository'), str(repository)))
|
|
wrapper.chmod(0o755)
|
|
baseline = None
|
|
for name in ('initial', 'missing-object', 'missing-parent', 'recovered'):
|
|
if name == 'missing-object':
|
|
(repository / 'child/valid.roa').write_bytes(b'corrupted downloaded ROA')
|
|
if name == 'missing-parent':
|
|
(repository / 'root/root.mft').write_bytes(b'corrupted downloaded manifest')
|
|
if name == 'recovered':
|
|
shutil.rmtree(repository)
|
|
shutil.copytree(fixture / 'cases/baseline-v2/repository', repository)
|
|
out = base / name
|
|
config = base / 'validate.toml'
|
|
write_config(config, fixture=fixture, output=out, state=base / 'state', ccr=out / 'result.ccr',
|
|
wrapper=wrapper, mirror=base / 'mirror', sync_preference='rsync-only', rrdp_mode='auto',
|
|
validation_time='2026-06-01T00:00:00Z', resource_mode='rfc6487', scope='publication-point')
|
|
start = len(log.read_text().splitlines()) if log.exists() else 0
|
|
result = subprocess.run([binary, 'validate', '--config', str(config)], capture_output=True, text=True, timeout=120)
|
|
summary = read_json(out / 'summary.json')
|
|
expected = 2 if name == 'recovered' else (0 if expect_no_cache and name.startswith('missing') else 1)
|
|
assert summary['vrps'] == expected, (name, summary, result.stderr[-4000:])
|
|
if name.startswith('missing'):
|
|
assert summary['resultStatus'] == 'partial', summary
|
|
assert result.returncode == 3, result
|
|
else:
|
|
assert summary['resultStatus'] == 'success' and result.returncode == 0, (summary, result.stderr[-3000:])
|
|
csv = (out / 'vrps.csv').read_bytes()
|
|
if name == 'initial':
|
|
baseline = csv
|
|
elif name.startswith('missing') and not expect_no_cache:
|
|
assert baseline == csv, 'failed-fetch changed still-valid payloads'
|
|
requests = log.read_text().splitlines()[start:]
|
|
if name == 'missing-parent':
|
|
assert len(requests) == 1, ('cached subtree must not fetch children', requests)
|
|
proof['runs'].append({'name': name, 'status': summary['resultStatus'], 'vrps': summary['vrps'],
|
|
'exit': result.returncode, 'rsync_requests': requests})
|
|
if evidence:
|
|
evidence.write_text(json.dumps(proof, indent=2) + '\n')
|
|
return proof
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('binary')
|
|
parser.add_argument('--evidence', type=Path)
|
|
parser.add_argument('--expect-no-cache', action='store_true')
|
|
args = parser.parse_args()
|
|
print(json.dumps(run(args.binary, args.evidence, args.expect_no_cache)))
|