arboricx_server_inspection.py (6860B)
1 #!/usr/bin/env python3 2 """Haskell-only server alias experiment; compiles/inspects but never runs IO. 3 Usage: python3 test/arboricx_server_inspection.py result/bin/tricu --directory /tmp/server-inspection 4 The output directory must not exist. Keeps bundles, outlines and measurements. 5 """ 6 import argparse 7 import json 8 import os 9 from pathlib import Path 10 import re 11 import subprocess 12 import time 13 14 ROOT = Path(__file__).resolve().parent.parent 15 DEMO = ROOT / 'demos/arboricx-inspection' 16 17 18 def main(): 19 parser = argparse.ArgumentParser(description=__doc__) 20 parser.add_argument('tricu', type=Path) 21 parser.add_argument('--directory', type=Path, required=True) 22 args = parser.parse_args() 23 host = args.tricu.resolve() 24 output = args.directory.resolve() 25 output.mkdir(parents=True, exist_ok=False) 26 store = output / 'compiler-store' 27 28 def run(*arguments): 29 p = subprocess.run([str(host), *map(str, arguments), '+RTS', '-N1', '-M8192m', '-RTS'], 30 cwd=output, capture_output=True, text=True, timeout=930) 31 assert p.returncode == 0, (arguments, p.stdout, p.stderr) 32 return p.stdout 33 34 measurements = {} 35 36 def compile_source(label, source, *extra): 37 bundle = output / (label + '.arboricx') 38 start = time.monotonic() 39 run('arboricx', 'compile', '--file', source, '--store', store, 40 '--output', bundle, '--compile-timeout', '900', *extra) 41 measurements[label] = {'bytes': bundle.stat().st_size, 42 'compileSeconds': time.monotonic() - start} 43 return bundle 44 45 def inspect(label, bundle, name='main'): 46 report = json.loads(run('arboricx', 'inspect', '--file', bundle, '--name', name, '--as', 'io', '--json')) 47 (output / (label + '.json')).write_text(json.dumps(report, indent=2) + '\n') 48 (output / (label + '.txt')).write_text(run('arboricx', 'inspect', '--file', bundle, '--name', name, '--as', 'io')) 49 assert not report['occurrenceLimitReached'] 50 return report 51 52 baseline = compile_source('baseline', ROOT / 'tricu-apps/arboricxServer.tri') 53 plain = inspect('baseline', baseline) 54 named = compile_source('named', DEMO / 'main.tri') 55 report = inspect('named', named) 56 selected = compile_source('named-main-only', DEMO / 'main.tri', '--name', 'main') 57 assert selected.read_bytes() == baseline.read_bytes(), 'inspection exports changed the application bundle' 58 59 # Independently compare the imported indexed main objects, so identity is 60 # checked for the full named bundle too, not just compile's filtered output. 61 def main_pin(bundle): 62 cas = output / 'comparison-store' 63 reply = run('arboricx', 'import', '--file', bundle, '--store', cas) 64 pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})', reply)[1] 65 rows = (cas / 'objects' / pin[:3] / pin).read_text().splitlines()[1:] 66 exports = {r.split('\t')[1]: r.split('\t')[3] for r in rows} 67 return exports['main'] 68 69 assert main_pin(baseline) == main_pin(named), 'full bundle main tree changed' 70 expected = [ 71 'main', 'startupAction', 'initializeThenServe', 'initializeStore', 72 'ensureRootDirectory', 'createRootDirectory', 'storeRoot', 73 'afterCreateDirectory', 'afterRootDirectory', 'afterInitialization', 'finishStartup', 74 ] 75 assert len(report['occurrences']) == len(plain['occurrences']) == len(expected) 76 for before, after, alias in zip(plain['occurrences'], report['occurrences'], expected): 77 assert alias in after['aliases'], (alias, after) 78 for key in ('path', 'role', 'summary', 'status'): 79 assert before[key] == after[key], (key, before, after) 80 if after['role'] == 'resume': 81 assert after['status'] == 'opaque' 82 serve = inspect('serve-view', named, 'serveView') 83 inspect('initialize-view', named, 'initializeView') 84 assert any(o['summary'] == 'Socket' and 'createSocket' in o['aliases'] for o in serve['occurrences']) 85 assert any(o['status'] == 'opaque' and 'afterSocketResult' in o['aliases'] for o in serve['occurrences']) 86 assert not any(o['summary'] in ('BindSocket', 'Listen', 'Accept') for o in serve['occurrences']) 87 88 # Negative/measurement cohort: normal exports, with no structural projections. 89 mappings = [] 90 for row in (DEMO / 'tricu.workspace').read_text().splitlines()[1:]: 91 name, path = row.split('\t') 92 mappings.append(name + '\t' + os.path.relpath((DEMO / path).resolve(), output)) 93 (output / 'tricu.workspace').write_text('tricu.workspace.v1\n' + '\n'.join(mappings) + '\n') 94 candidates = { 95 'storeRoot': '"./store"', 96 'createRootDirectory': 'createDirectory storeRoot', 97 'ensureRootDirectory': 'Registry.ensureDir storeRoot', 98 'initializeStore': 'Registry.ensureStore storeRoot', 99 'serveRegistry': 'Registry.arboricxServer storeRoot "0.0.0.0" 8080', 100 'initializeThenServe': 'onOk_ initializeStore (_ : serveRegistry)', 101 'checkedStartup': 'onResult_ initializeThenServe abortIO pure', 102 'rawEnsureStore': 'Registry.ensureStore', 103 'rawEnsureDir': 'Registry.ensureDir', 104 'rawServer': 'Registry.arboricxServer', 105 'rawAbort': 'abortIO', 106 'rawPure': 'pure', 107 'rawOnOk': 'onOk_', 108 'rawOnResult': 'onResult_', 109 } 110 source = output / 'ordinary.tri' 111 source.write_text('!import arboricx_app as App\n!import arboricx_server as Registry\n!import io *\n!export main\n' 112 + ''.join('!export ' + name + '\n' for name in candidates) 113 + 'main = App.main\n' 114 + ''.join(name + ' = ' + value + '\n' for name, value in candidates.items())) 115 ordinary = compile_source('ordinary', source) 116 observed = inspect('ordinary', ordinary) 117 hits = {alias for o in observed['occurrences'] for alias in o['aliases']} - {'main'} 118 assert {'storeRoot', 'createRootDirectory', 'ensureRootDirectory', 'initializeStore'} <= hits 119 measurements['ordinary']['matchedCandidateExports'] = sorted(hits) 120 measurements['ordinary']['unmatchedCandidateExports'] = sorted(candidates.keys() - hits) 121 for label, view in [('baseline', plain), ('ordinary', observed), ('named', report)]: 122 measurements[label]['storedNodes'] = view['storedNodes'] 123 measurements[label]['aliasedOccurrences'] = sum(bool(o['aliases']) for o in view['occurrences']) 124 measurements[label]['occurrences'] = len(view['occurrences']) 125 measurements['mainTreeUnchanged'] = True 126 (output / 'measurements.json').write_text(json.dumps(measurements, indent=2) + '\n') 127 print(json.dumps(measurements, indent=2)) 128 print('PASS: exact main identity, named startup spine, opaque continuations, separate socket view; no application IO executed') 129 130 131 if __name__ == '__main__': 132 main()