io_streams.py (5213B)
1 #!/usr/bin/env python3 2 """Real binary pipes/redirects, ownership errors, and bounded native residency.""" 3 import argparse 4 import concurrent.futures 5 import hashlib 6 import json 7 import os 8 from pathlib import Path 9 import subprocess 10 import time 11 from source_order import Runner 12 from process_rss import ChildRSS 13 from rts_metrics import parse_rts_stats 14 from http_headers import result, tree_list 15 from stable_bundle import byte_tree 16 17 ROOT = Path(__file__).resolve().parent.parent 18 19 def main(): 20 parser = argparse.ArgumentParser(description=__doc__) 21 parser.add_argument('tricu', type=Path) 22 parser.add_argument('--directory', type=Path, required=True) 23 args = parser.parse_args() 24 directory = args.directory.resolve() 25 directory.mkdir(parents=True, exist_ok=False) 26 runner = Runner(args.tricu, directory) 27 source = ROOT/'demos/io/streams.tri' 28 bundle = directory/'streams.arboricx' 29 runner.run('compile', ['compile', '--file', source, '--store', runner.store, '--output', bundle]) 30 pin = runner.imported(bundle, 'import') 31 checks, observations = [], [] 32 33 def command(name, stats=True): 34 return [str(runner.exe), 'arboricx', 'run', '--module', pin, '--name', name, 35 '--store', str(runner.store), '--io', '--no-result', 36 '+RTS', '-N1', '-M128m', *(['-T', '-s'] if stats else []), '-RTS'] 37 38 def invoke(label, name, body, redirected=False): 39 argv = command(name) 40 started = time.monotonic() 41 input_path, output_path = directory/(label+'.in'), directory/(label+'.out') 42 input_path.write_bytes(body) 43 with input_path.open('rb') as src, output_path.open('wb') as dst: 44 proc = subprocess.Popen(argv, stdin=src if redirected else subprocess.PIPE, 45 stdout=dst if redirected else subprocess.PIPE, stderr=subprocess.PIPE) 46 peak = None 47 try: 48 rss = ChildRSS(proc) 49 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: 50 future = pool.submit(proc.communicate, None if redirected else body, timeout=120) 51 while not future.done(): 52 current = rss.sample() 53 if current is not None: 54 peak = max(peak or 0, current) 55 if peak > 128*1024: 56 proc.kill() 57 raise AssertionError('RSS bound exceeded') 58 time.sleep(.01) 59 stdout, stderr = future.result() 60 finally: 61 if proc.poll() is None: 62 proc.kill(); proc.communicate() 63 assert proc.returncode == 0, stderr 64 if redirected: 65 stdout = output_path.read_bytes() 66 else: 67 output_path.write_bytes(stdout) 68 assert stdout == body, label 69 text = stderr.decode() 70 (directory/(label+'.log')).write_text(text) 71 stats = parse_rts_stats(text) 72 assert stats['maximum_live_bytes'] <= 16*1024*1024, stats 73 assert peak is not None, 'RSS unavailable' 74 observations.append(dict(label=label, command=argv, seconds=time.monotonic()-started, 75 peak_kib=peak, rts_process=stats, sha256=hashlib.sha256(stdout).hexdigest())) 76 checks.append(label) 77 78 for name in ['main', 'handled', 'child']: 79 for size in [0, 1, 4095, 4096, 4097, 65536, 1048576]: 80 body = (bytes(range(256))*((size+255)//256))[:size] 81 invoke(f'{name}-{size}', name, body) 82 invoke('redirected', 'main', bytes(range(256))*257, True) 83 for name, operation in [('closeDenied', 'resource/close'), ('transferDenied', 'core/spawn')]: 84 expected = result(False, tree_list([byte_tree(x.encode()) for x in 85 ['borrowed-resource', operation, '', 'none']])) 86 assert runner.invoke(name, pin, name, ['--io']) == expected, name 87 checks.append(name) 88 run = subprocess.run(command('diagnostic', False), input=b'', capture_output=True, timeout=20) 89 assert (run.returncode, run.stdout, run.stderr) == (0, b'', bytes([0,255,13,10])), run 90 checks.append('binary-stderr') 91 reader, writer = os.pipe() 92 os.close(reader) 93 try: 94 proc = subprocess.Popen(command('broken', False), stdin=subprocess.DEVNULL, 95 stdout=writer, stderr=subprocess.PIPE) 96 finally: 97 os.close(writer) 98 _, diagnostic = proc.communicate(timeout=20) 99 assert (proc.returncode, diagnostic) == (0, b'failed'), (proc.returncode, diagnostic) 100 checks.append('broken-pipe-ordinary-result') 101 paths = [source, Path(__file__).resolve(), ROOT/'lib/io.tri', 102 ROOT/'src/IO/Resources.hs', ROOT/'src/IO/Driver.hs', ROOT/'src/IO/Protocol.hs', 103 ROOT/'test/rts_metrics.py', ROOT/'bootstrap/build-stable.arboricx'] 104 (directory/'report.json').write_text(json.dumps(dict(checks=checks, observations=observations, 105 sources={str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in paths}), indent=2)+'\n') 106 print(f'PASS: {len(checks)} binary stream, lifecycle and pipe checks') 107 108 if __name__ == '__main__': 109 main()