arboricx_server_inspection.py (6924B)
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 = ['main', 'startupAction', 'startupBody'] 71 assert len(report['occurrences']) == len(plain['occurrences']) == len(expected) 72 for before, after, alias in zip(plain['occurrences'], report['occurrences'], expected): 73 assert alias in after['aliases'], (alias, after) 74 for key in ('path', 'role', 'summary', 'status'): 75 assert before[key] == after[key], (key, before, after) 76 assert [o['summary'] for o in report['occurrences']] == [ 77 'tricuIO', 'Scope', 'scope callback (not applied)'] 78 assert report['occurrences'][-1]['status'] == 'opaque' 79 create = inspect('create-view', named, 'createView') 80 assert [o['summary'] for o in create['occurrences']] == [ 81 'tricuIO', 'Facility directory@1/create', 82 'arguments (not validated; backend availability not inferred)'] 83 assert 'createRootDirectory' in create['occurrences'][1]['aliases'] 84 assert 'storeRoot' in create['occurrences'][2]['aliases'] 85 assert create['occurrences'][2]['status'] == 'opaque' 86 initialize = inspect('initialize-view', named, 'initializeView') 87 assert any(o['summary'] == 'Facility directory@1/create' for o in initialize['occurrences']) 88 assert all(o['status'] == 'opaque' for o in initialize['occurrences'] if o['role'] == 'resume') 89 assert not (output / 'store').exists(), 'inspection executed store initialization' 90 91 # Negative/measurement cohort: normal exports, with no structural projections. 92 mappings = [] 93 for row in (DEMO / 'tricu.workspace').read_text().splitlines()[1:]: 94 name, path = row.split('\t') 95 mappings.append(name + '\t' + os.path.relpath((DEMO / path).resolve(), output)) 96 (output / 'tricu.workspace').write_text('tricu.workspace.v1\n' + '\n'.join(mappings) + '\n') 97 candidates = { 98 'storeRoot': '"./store"', 99 'createRootDirectory': 'IO.createDirectory storeRoot', 100 'initializeStore': 'H.runIO (Artifacts.prepareFilesystem storeRoot)', 101 'rawPrepare': 'Artifacts.prepareFilesystem', 102 'rawListen': 'IO.listenTCP', 103 'rawScope': 'IO.scope', 104 'rawPure': 'IO.pure', 105 } 106 source = output / 'ordinary.tri' 107 source.write_text('!import arboricx_app as App\n!import handler_artifacts as Artifacts\n' 108 '!import handlers as H\n!import io as IO\n!export main\n' 109 + ''.join('!export ' + name + '\n' for name in candidates) 110 + 'main = App.main\n' 111 + ''.join(name + ' = ' + value + '\n' for name, value in candidates.items())) 112 ordinary = compile_source('ordinary', source) 113 observed = inspect('ordinary', ordinary) 114 hits = {alias for o in observed['occurrences'] for alias in o['aliases']} - {'main'} 115 assert not hits, ('scope callback must remain opaque', hits) 116 assert main_pin(ordinary) == main_pin(baseline), 'ordinary aliases changed main' 117 assert not (output / 'store').exists(), 'ordinary inspection ran IO' 118 measurements['ordinary']['matchedCandidateExports'] = sorted(hits) 119 measurements['ordinary']['unmatchedCandidateExports'] = sorted(candidates.keys() - hits) 120 for label, view in [('baseline', plain), ('ordinary', observed), ('named', report)]: 121 measurements[label]['storedNodes'] = view['storedNodes'] 122 measurements[label]['aliasedOccurrences'] = sum(bool(o['aliases']) for o in view['occurrences']) 123 measurements[label]['occurrences'] = len(view['occurrences']) 124 measurements['mainTreeUnchanged'] = True 125 (output / 'measurements.json').write_text(json.dumps(measurements, indent=2) + '\n') 126 print(json.dumps(measurements, indent=2)) 127 print('PASS: exact main identity, named v2 scope, opaque callbacks/continuations, separate initialization views; no application IO executed') 128 129 130 if __name__ == '__main__': 131 main()