io_v2_zig_files.py (8286B)
1 #!/usr/bin/env python3 2 """Private Zig regular-file host: actual source, independent bytes and trees.""" 3 import argparse 4 import hashlib 5 import json 6 import subprocess 7 from pathlib import Path 8 from source_order import Runner, field 9 from http_headers import result, tree_list, number 10 from stable_bundle import byte_tree 11 from process_rss import run_guarded 12 13 ROOT = Path(__file__).resolve().parent.parent 14 15 def main(): 16 p = argparse.ArgumentParser(description=__doc__) 17 p.add_argument('tricu', type=Path) 18 p.add_argument('probe', type=Path) 19 p.add_argument('--directory', type=Path, required=True) 20 args = p.parse_args() 21 d = args.directory.resolve(); d.mkdir(parents=True, exist_ok=False) 22 r = Runner(args.tricu, d) 23 bundle = d/'copy.arboricx' 24 r.run('compile', ['compile', '--file', ROOT/'demos/io-v2/copy.tri', '--store', r.store, '--output', bundle]) 25 pin = r.imported(bundle, 'import') 26 checks = [] 27 def invoke(name, source, target, label): 28 command = [str(args.probe.resolve()), str(bundle), name, 'text:'+str(source), 'text:'+str(target)] 29 output, peak = run_guarded(command, memory_mib=128, seconds=180) 30 (d/(label+'.log')).write_text(output) 31 assert output.endswith('\n') and set(output.strip()) <= set('012'), (label, output) 32 return bytes(map(int, output.strip())), peak 33 for name in ('copy', 'handledCopy', 'borrowedCopy', 'transferredCopy', 'joinedOnExitCopy', 'handledChildCopy', 'cancelledCopy'): 34 for size in (0, 1, 4095, 4096, 4097, 65536, 1048576): 35 label = f'{name}-{size}' 36 source, target = d/(label+'.in'), d/(label+'.out') 37 body = (bytes(range(256))*((size+255)//256))[:size] 38 source.write_bytes(body) 39 expected = result(True, b'\0') 40 if name == 'handledCopy': expected = result(True, expected) 41 if name == 'cancelledCopy': expected = tree_list([byte_tree(b'cancelled'), byte_tree(b'requested'), b'\0']) 42 actual, peak = invoke(name, source, target, label) 43 assert actual == expected, (label, actual) 44 assert target.read_bytes() == (body[:4096] if name == 'cancelledCopy' else body), label 45 # Actual Haskell comparison on each composition at the boundary; the 46 # complete corpus is independently checked by io_v2_files.py too. 47 if size == 4097: 48 argv = ['--io'] 49 for i, path in enumerate((source, d/(label+'.haskell'))): 50 argument = d/f'{label}-{i}.tree'; argument.write_bytes(tree_list([number(ord(c)) for c in str(path)])) 51 argv += ['--arg-tree-file', argument] 52 assert r.invoke('haskell-'+label, pin, name, argv) == expected 53 assert (d/(label+'.haskell')).read_bytes() == target.read_bytes() 54 checks.append(dict(label=label, peak_kib=peak, sha256=hashlib.sha256(target.read_bytes()).hexdigest())) 55 source, target = d/'source-é😀', d/'target-é😀' 56 source.write_bytes(b'\x00\xffunicode-path') 57 actual, peak = invoke('copy', source, target, 'unicode-path') 58 assert actual == result(True, b'\0') and target.read_bytes() == source.read_bytes() 59 flags = ['--io'] 60 target.unlink() 61 for i, path in enumerate((source, target)): 62 arg = d/f'unicode-{i}.tree'; arg.write_bytes(tree_list([number(ord(c)) for c in str(path)])) 63 flags += ['--arg-tree-file', arg] 64 assert r.invoke('unicode-path-haskell', pin, 'copy', flags) == result(True, b'\0') 65 assert target.read_bytes() == source.read_bytes() 66 checks.append(dict(label='unicode-path', peak_kib=peak)) 67 for case in ('missing-source', 'missing-parent', 'fifo', 'directory', 'device'): 68 source, target = d/(case+'.in'), d/(case+'.out') 69 source.write_bytes(b'keep') 70 category = b'not-found'; operation = b'file/open-reader' 71 if case == 'missing-source': source = d/'does-not-exist' 72 elif case == 'missing-parent': target = d/'missing-parent'/'out'; operation = b'file/open-writer' 73 elif case == 'fifo': 74 import os 75 source.unlink(); os.mkfifo(source); category = b'unsupported' 76 elif case == 'directory': source = d; category = b'unsupported' 77 elif case == 'device': source = Path('/dev/null'); category = b'unsupported' 78 actual, peak = invoke('copy', source, target, case) 79 assert field(actual, 0) == b'\0', case 80 problem = field(actual, 1, 0) 81 assert field(problem, 0) == byte_tree(category), case 82 assert field(problem, 1, 0) == byte_tree(operation), case 83 assert not target.exists(), case 84 checks.append(dict(label=case, peak_kib=peak)) 85 controls = d/'controls.arboricx' 86 r.run('compile-controls', ['compile', '--file', ROOT/'test/io-v2/zig-files.tri', '--store', r.store, '--output', controls]) 87 control_pin = r.imported(controls, 'import-controls') 88 def problem(category, operation, detail=b''): 89 return result(False, tree_list([byte_tree(category), byte_tree(operation), byte_tree(detail), byte_tree(b'none')])) 90 data = bytes([2])+byte_tree(b'data')+byte_tree(b'bc') 91 eof = bytes([2])+byte_tree(b'eof')+b'\0' 92 control_cases = { 93 'positioned': (b'', tree_list([data, number(3), number(10), eof]), b'a'), 94 'appended': (b'k', tree_list([number(3), problem(b'unsupported', b'file/seek', b'operation unavailable in this file mode')]), b'kxy'), 95 'emptyWrite': (b'previous', problem(b'invalid-bound', b'stream/write'), b''), 96 'closed': (b'keep', problem(b'stale-handle', b'resource/close'), b'keep'), 97 'readerWrite': (b'keep', problem(b'wrong-kind', b'stream/write'), b'keep'), 98 } 99 for name, (before, expected, after) in control_cases.items(): 100 for backend in ('zig', 'haskell'): 101 target = d/(name+'-'+backend+'.file'); target.write_bytes(before) 102 if backend == 'zig': 103 output, peak = run_guarded([str(args.probe.resolve()), str(controls), name, 'text:'+str(target)], memory_mib=128, seconds=60) 104 actual = bytes(map(int, output.strip())) 105 else: 106 argument = d/(name+'.tree'); argument.write_bytes(tree_list([number(ord(c)) for c in str(target)])) 107 actual = r.invoke(name, control_pin, name, ['--io', '--arg-tree-file', argument]) 108 assert actual == expected, (name, backend, actual, expected) 109 assert target.read_bytes() == after, (name, backend) 110 checks.append(dict(label=name)) 111 for backend in ('zig', 'haskell'): 112 target = d/('failed-'+backend) 113 if backend == 'zig': 114 process = subprocess.run([str(args.probe.resolve()), str(controls), 'failedAfterWrite', 'text:'+str(target)], capture_output=True, timeout=60) 115 assert process.returncode == 1 and b'not forced' in process.stderr 116 else: 117 argument = d/'failed.tree'; argument.write_bytes(tree_list([number(ord(c)) for c in str(target)])) 118 try: r.invoke('failedAfterWrite', control_pin, 'failedAfterWrite', ['--io', '--arg-tree-file', argument]) 119 except subprocess.CalledProcessError as error: assert 'IO v2 task failed' in error.output 120 else: raise AssertionError('failedAfterWrite succeeded') 121 assert target.read_bytes() == b'*' 122 checks.append(dict(label='failed-after-write-no-rollback')) 123 output, _ = run_guarded([str(args.probe.resolve()), str(controls), 'unknownWithPoison', '0'], memory_mib=128, seconds=10) 124 assert bytes(map(int, output.strip())) == problem(b'unsupported', b'file/open-reader', b'group revision 99') 125 checks.append(dict(label='unknown-revision-no-demand')) 126 paths = [ROOT/'test/io-v2/zig-files.tri', ROOT/'demos/io-v2/copy.tri', ROOT/'ext/zig/src/io_v2_files.zig', ROOT/'ext/zig/src/io_v2_file_host.zig', ROOT/'ext/zig/src/io_v2_core.zig', args.probe.resolve()] 127 (d/'report.json').write_text(json.dumps(dict(checks=checks, sources={str(p):hashlib.sha256(p.read_bytes()).hexdigest() for p in paths}, haskell=r.records, scope='private-regular-file-host-no-public-v2'), indent=2)+'\n') 128 print(f'PASS {len(checks)} private file-host source/byte controls; 14 Haskell boundary/mode/failure comparisons') 129 if __name__ == '__main__': main()