io_v2_protocol.py (6814B)
1 #!/usr/bin/env python3 2 """Independent source oracles for v2 discovery, inspection and unsupported calls.""" 3 import argparse 4 import hashlib 5 import json 6 from pathlib import Path 7 from source_order import Runner 8 from stable_bundle import byte_tree 9 from http_headers import tree_list, number, result 10 11 ROOT = Path(__file__).resolve().parent.parent 12 13 def text(value): 14 return byte_tree(value.encode()) 15 16 def strings(values): 17 return tree_list([text(v) for v in values]) 18 19 def expected_capabilities(): 20 def row(key, value): 21 return tree_list([text(key), value]) 22 groups = [('runtime', ['capabilities', 'inspect']), ('standard', ['input', 'output', 'error']), ('file', ['open-reader', 'open-writer', 'open-appender', 'open-update', 'seek', 'position', 'size', 'resize', 'sync']), 23 ('path', ['stat', 'lstat', 'remove-file', 'rename']), ('directory', ['create', 'remove', 'open', 'next', 'sync']), ('tcp', ['listen', 'connect', 'accept', 'local-address', 'peer-address', 'shutdown-write']), ('resolver', ['resolve']), ('indexed-tree', ['read', 'hash', 'write', 'verify']), ('utf8', ['encode', 'decode']), ('stream', ['read', 'write']), ('resource', ['close']), 24 ('stage', ['open', 'seal', 'publish']), ('digest', ['open', 'update', 'finish']), ('clock', ['monotonic', 'sleep']), ('scheduler', ['yield']), ('process', ['spawn', 'working-directory', 'environment-variable'])] 25 return tree_list([ 26 row('schema', number(1)), row('io-version', number(2)), 27 row('backend', text('haskell-native')), row('authority', text('os-process')), 28 row('core', strings(['pure', 'bind', 'fail', 'scope', 'current-scope', 'spawn', 'join', 'discard', 'cancel', 'cancelled'])), 29 row('groups', tree_list([tree_list([text(g), number(1), strings(ops)]) for g, ops in groups])), 30 row('limits', tree_list([row('chunk-bytes', number(4096)), row('transfer-handles', number(256)), row('file-offset-max', number(9223372036854775807)), row('directory-name-scalars', number(4096)), row('tcp-backlog', number(4096)), row('resolver-host-characters', number(253)), row('resolver-addresses', number(64)), row('indexed-sections', number(64)), row('indexed-manifest-bytes', number(65536)), row('utf8-encode-scalars', number(1024)), row('sleep-milliseconds-max', number(9223372036854775807)), row('process-arguments', number(256)), row('process-environment-entries', number(256)), row('process-text-scalars', number(4096)), row('process-total-scalars', number(65536)), row('process-grace-milliseconds-max', number(9223372036854775807))])), 31 row('tcp-address-families', strings(['ipv4', 'ipv6'])), 32 row('digest-algorithms', strings(['sha256'])), 33 row('restrictions', strings(['action-boundary-cancellation', 'no-hard-deadlines', 34 'stage-same-filesystem', 'stage-no-copy-fallback', 'stage-private-namespace', 'publish-new-requires-hard-links', 'no-crash-recovery', 'borrowed-standard-streams', 'standard-stream-leases-per-run', 'file-single-cursor-lease', 'append-no-position-or-resize', 'file-host-handle-locking', 'directory-unsorted-non-snapshot', 'filesystem-host-encoding', 'directory-failure-poisons-cursor', 'tcp-numeric-addresses', 'tcp-ipv6-only-listeners', 'tcp-direction-leases', 'tcp-failure-poisons-direction', 'resolver-host-order-and-buffering', 'resolver-may-delay-cancellation', 'indexed-authenticated-snapshot', 'indexed-caller-frame-and-disk-limit', 'indexed-dag-resident', 'indexed-writer-preflight', 'indexed-budget-not-heap-quota', 'sync-host-filesystem-guarantees', 'utf8-strict-scalars', 'clock-host-monotonic-word64', 'sleep-may-run-late', 'yield-no-ordering-guarantee', 'process-direct-child-only', 'process-normal-exit-joins', 'process-explicit-pipe-draining']))]) 35 36 def main(): 37 parser = argparse.ArgumentParser(description=__doc__) 38 parser.add_argument('tricu', type=Path) 39 parser.add_argument('--directory', type=Path, required=True) 40 args = parser.parse_args() 41 directory = args.directory.resolve() 42 directory.mkdir(parents=True, exist_ok=False) 43 runner = Runner(args.tricu, directory) 44 bundle = directory/'protocol.arboricx' 45 source = ROOT/'test/io-v2/protocol.tri' 46 runner.run('compile', ['compile', '--file', source, '--store', runner.store, '--output', bundle]) 47 pin = runner.imported(bundle, 'import') 48 victim = directory/'must-remain' 49 victim.write_bytes(b'not truncated\x00\xff') 50 path = directory/'path.tree' 51 path.write_bytes(byte_tree(str(victim).encode())) 52 seed = directory/'seed.tree' 53 seed.write_bytes(number(7)) 54 cases = { 55 'capabilities': (result(True, expected_capabilities()), []), 56 'handledCapabilities': (result(True, expected_capabilities()), []), 57 'inspectWriter': (result(True, tree_list([text('facility'), text('file'), number(1), 58 text('open-writer'), text('available')])), [path]), 59 'inspectLazyBind': (result(True, strings(['core', 'bind'])), [seed]), 60 'inspectLazyScope': (result(True, strings(['core', 'scope'])), [seed]), 61 'inspectFuture': (result(True, tree_list([text('facility'), text('future-tcp'), number(1), 62 text('connect'), text('unsupported')])), [seed]), 63 'unsupported': (result(False, strings(['unsupported', 'future-tcp/connect', 'group revision 1', 'none'])), [seed])} 64 for name, (expected, inputs) in cases.items(): 65 argv = ['--io'] 66 for arg in inputs: 67 argv += ['--arg-tree-file', arg] 68 assert runner.invoke(name, pin, name, argv) == expected, name 69 assert victim.read_bytes() == b'not truncated\x00\xff' 70 for name in ['invalidHeader', 'retired']: 71 try: 72 runner.invoke(name, pin, name, ['--io']) 73 except Exception as error: 74 assert 'IO v2 task failed' in str(getattr(error, 'output', error)), error 75 else: 76 raise AssertionError('malformed protocol executed: ' + name) 77 observations = runner.records 78 for row in observations: 79 stats = row['rts_process'] 80 assert all(k in stats for k in ['copied_bytes', 'mutator_cpu_seconds', 'gc_cpu_seconds', 81 'productivity_cpu_percent']), row 82 paths = [source, ROOT/'lib/io-v2.tri', ROOT/'src/IOV2/Protocol.hs', ROOT/'src/IOV2/Driver.hs', 83 ROOT/'test/source_order.py', ROOT/'test/rts_metrics.py', Path(__file__).resolve()] 84 report = {'checks': list(cases)+['invalidHeader', 'retired'], 'observations': observations, 85 'sources': {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in paths}} 86 (directory/'report.json').write_text(json.dumps(report, indent=2)+'\n') 87 print('PASS: 9 independent capability/inspection/protocol checks') 88 89 if __name__ == '__main__': 90 main()