test_portable.py (4703B)
1 #!/usr/bin/env python3 2 """Exercise a relocated release under an empty mount root with no Nix store.""" 3 import argparse 4 import json 5 from pathlib import Path 6 import shutil 7 import subprocess 8 import tempfile 9 import tarfile 10 11 12 def main(): 13 p = argparse.ArgumentParser(description=__doc__) 14 p.add_argument('--release', type=Path, required=True) 15 p.add_argument('--system', type=Path, required=True) 16 p.add_argument('--source', type=Path, required=True) 17 p.add_argument('--worker', type=Path, required=True) 18 p.add_argument('--copier', type=Path, required=True) 19 p.add_argument('--output', type=Path, required=True) 20 a = p.parse_args() 21 a.output.mkdir(parents=True, exist_ok=False) 22 with tempfile.TemporaryDirectory() as tmp: 23 tmp = Path(tmp) 24 tests = tmp/'tests' 25 tests.mkdir() 26 for name in ('test_embedding.py', 'test_scopes.py'): 27 shutil.copyfile(a.source/'ext/haskell'/name, tests/name) 28 for name in ('python_filecheck.py', 'process_rss.py'): 29 shutil.copyfile(a.source/'test'/name, tests/name) 30 (tests/'fixtures').mkdir() 31 for name in ('id.arboricx', 'append.arboricx'): 32 shutil.copyfile(a.source/'test/fixtures'/name, tests/'fixtures'/name) 33 shutil.copyfile(a.copier, tests/'copy.arboricx') 34 shutil.copytree(a.source/'demos/python-filecheck/tricu_filecheck', tests/'tricu_filecheck') 35 for path in [tests/'tricu_filecheck', *(tests/'tricu_filecheck').rglob('*')]: 36 path.chmod(0o755 if path.is_dir() else 0o644) 37 shutil.copyfile(a.worker/'worker.arboricx', tests/'tricu_filecheck/worker.arboricx') 38 pin = (a.worker/'worker.sha256').read_text().strip() 39 (tests/'tricu_filecheck/_bundle_pin.py').write_text(f'BUNDLE_SHA256 = "{pin}"\n') 40 reports = [] 41 for location in ('first', 'relocated path with spaces'): 42 release = tmp/location 43 with tarfile.open(a.release, 'r:gz') as archive: 44 archive.extractall(tmp/'unpacked', filter='data') 45 (tmp/'unpacked/tricu').rename(release) 46 mount = '/'+location 47 shutil.copyfile(a.source/'ext/haskell/portable_workload.py', tests/'run.py') 48 (release/'lib').chmod(0o755) 49 import hashlib 50 inventory = json.loads((release/'share/tricu/portable.json').read_text()) 51 for name, row in inventory['libraries'].items(): 52 assert hashlib.sha256((release/'lib'/name).read_bytes()).hexdigest() == row['sha256'] 53 work = tmp/(location+'-work') 54 work.mkdir() 55 command = ['bwrap', '--unshare-all', '--die-with-parent', '--new-session', 56 '--ro-bind', str(a.system/'python'), '/python', 57 '--ro-bind', str(a.system/'system'), '/system', 58 '--ro-bind', str(release), mount, 59 '--ro-bind', str(tests), '/tests', 60 '--bind', str(work), '/work', '--tmpfs', '/tmp', 61 '--proc', '/proc', '--dev', '/dev', '--chdir', '/work', 62 '--clearenv', '--setenv', 'HOME', '/tmp', 63 '--setenv', 'PATH', '/no-runtime-tools', 64 '--setenv', 'PYTHONHOME', '/python', 65 '--setenv', 'LD_LIBRARY_PATH', '/system/lib', 66 '--setenv', 'PYTHONDONTWRITEBYTECODE', '1', 67 '--setenv', 'PYTHONPATH', mount+'/share/tricu:/tests', 68 '/python/bin/python3', '/tests/run.py', mount] 69 result = subprocess.run(command, text=True, capture_output=True, timeout=240) 70 (a.output/(location+'.log')).write_text(result.stdout+result.stderr) 71 if result.returncode: 72 raise RuntimeError(f'exit {result.returncode}: {result.stderr or result.stdout}') 73 reports.append(json.loads((work/'report.json').read_text())) 74 # Removing a required bundled dependency must fail before initialization. 75 (release/'lib/libgmp.so.10').unlink() 76 negative = subprocess.run(command[:-2]+['-c', 'from tricu import Library; Library()'], 77 text=True, capture_output=True, timeout=30) 78 assert negative.returncode and 'libgmp.so.10' in negative.stderr, negative 79 (a.output/'report.json').write_text(json.dumps(dict( 80 schema='tricu.portable-acceptance.v1', runs=reports, 81 missing_dependency_rejected=True, 82 scope='Nix-free mount namespace; pinned relocated Python/glibc test OS; not cross-distro acceptance'), 83 indent=2)+'\n') 84 85 86 if __name__ == '__main__': 87 main()