tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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()