tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

effect_server.py (11410B)


      1 #!/usr/bin/env python3
      2 """Opt-in artifact-server GET slice: captured services versus scoped interpreters.
      3 
      4 nix build .#
      5 python3 test/effect_server.py result/bin/tricu --directory /tmp/effect-server --benchmark
      6 
      7 No listening socket or production source changes. Checks complete HTTP bytes,
      8 logs, Result envelopes and effect traces against independent Python expectations.
      9 Real filesystem runs use IO and independent byte/error expectations.
     10 """
     11 import argparse
     12 import hashlib
     13 import json
     14 import os
     15 from pathlib import Path
     16 import re
     17 import statistics
     18 
     19 from effect_interfaces import tree
     20 from http_headers import result
     21 from source_order import Runner, end
     22 from stable_bundle import byte_tree
     23 
     24 ROOT = Path(__file__).resolve().parent.parent
     25 HASH = 'a' * 64
     26 TARGET = '/_arboricx/bundle/hash/' + HASH
     27 BODY = b'\x00\xffportable-tree\n'
     28 
     29 
     30 def encode(value):
     31     if isinstance(value, str):
     32         return byte_tree(value.encode())
     33     if isinstance(value, (list, tuple)):
     34         return tree([encode(x) for x in value])
     35     return tree(value)
     36 
     37 
     38 def ok(value):
     39     return result(True, encode(value))
     40 
     41 
     42 def err(value):
     43     return result(False, encode(value))
     44 
     45 
     46 def response(status, body, content_type='text/plain; charset=utf-8'):
     47     if isinstance(body, str):
     48         body = body.encode()
     49     phrase = {200: 'OK', 400: 'Bad Request', 404: 'Not Found'}[status]
     50     head = (f'HTTP/1.1 {status} {phrase}\r\nContent-Type: {content_type}\r\n'
     51             f'Content-Length: {len(body)}\r\nConnection: close\r\n\r\n').encode()
     52     return byte_tree(head + body)
     53 
     54 
     55 def success(body=BODY):
     56     return response(200, body, 'application/vnd.arboricx.bundle')
     57 
     58 
     59 def event(context, hash=HASH, status=200):
     60     return [context, [hash, status]]
     61 
     62 
     63 def script(context, backend=11, hash=HASH, reply=None, status=200):
     64     return [([backend, 6, hash], ok(byte_tree(BODY)) if reply is None else reply),
     65             ([30, 7, event(context, hash, status)], ok(1))]
     66 
     67 
     68 def main():
     69     parser = argparse.ArgumentParser(description=__doc__)
     70     parser.add_argument('tricu', type=Path)
     71     parser.add_argument('--directory', type=Path, required=True)
     72     parser.add_argument('--benchmark', action='store_true')
     73     args = parser.parse_args()
     74     if os.geteuid() == 0:
     75         parser.error("OS permission-denial controls require a non-root user")
     76     directory = args.directory.resolve()
     77     directory.mkdir(parents=True, exist_ok=False)
     78     runner = Runner(args.tricu, directory)
     79     source = ROOT / 'test/effect_interfaces/server.tri'
     80     bundle = directory / 'server.arboricx'
     81     runner.run('compile', ['compile', '--file', source, '--store', runner.store,
     82                            '--output', bundle, '--compile-timeout', '270'], seconds=300)
     83     pin = runner.imported(bundle, 'import')
     84     passed = []
     85 
     86     def arguments(label, values):
     87         result = []
     88         for index, value in enumerate(values):
     89             path = directory / f'{label}-{index}.tree'
     90             path.write_bytes(encode(value))
     91             result += ['--arg-tree-file', path]
     92         return result
     93 
     94     def check(label, export, values, rows, value, *, remaining=(), trace=None):
     95         actual = runner.invoke(label, pin, export, arguments(label, [*values, rows]))
     96         wanted = encode([value, remaining, [row[0] for row in rows] if trace is None else trace])
     97         if actual != wanted:
     98             (directory / (label+'-expected.tree')).write_bytes(wanted)
     99             raise AssertionError(label)
    100         passed.append(label)
    101 
    102     for variant in 'acd':
    103         for mode, backend in [(0, 11), (1, 12), (2, 11)]:
    104             context = f'request-{mode}'
    105             check(f'{variant}-mode-{mode}', variant+'Request', [mode, context, 'GET', TARGET],
    106                   script(context, backend), ok(success()))
    107         rows = script('shared', 11) + script('shared', 12, reply=ok(byte_tree(b'alternate')))
    108         check(variant+'-dual', variant+'Dual', ['shared', TARGET], rows,
    109               ok([success(), success(b'alternate')]))
    110         for label, reply, status, body in [
    111                 ('missing', err('missing'), 404, 'Artifact unavailable\n'),
    112                 ('malformed', tree(0), 404, 'Invalid read result')]:
    113             check(variant+'-'+label, variant+'Request', [0, 'r', 'GET', TARGET],
    114                   script('r', reply=reply, status=status), ok(response(status, body)))
    115         # Production read protocol deliberately discards read Result remainder.
    116         check(variant+'-read-remainder', variant+'Request', [0, 'r', 'GET', TARGET],
    117               script('r', reply=result(True, byte_tree(BODY), byte_tree(b'unused'))), ok(success()))
    118         for index, hash in enumerate(['../secret', 'A'*64, 'a'*63, 'a'*65]):
    119             rows = [([30, 7, event('r', hash, 400)], ok(1))]
    120             check(f'{variant}-invalid-{index}', variant+'Request',
    121                   [0, 'r', 'GET', '/_arboricx/bundle/hash/'+hash], rows,
    122                   ok(response(400, 'expected 64 lowercase hexadecimal hash characters')))
    123         for method, target in [('POST', TARGET), ('GET', '/other')]:
    124             check(variant+'-not-found-'+method, variant+'Request', [0, 'r', method, target], [],
    125                   ok(response(404, 'Not found\n')))
    126         rows = script('r')
    127         rows[-1] = (rows[-1][0], err('log unavailable'))
    128         check(variant+'-log-failure', variant+'Request', [0, 'r', 'GET', TARGET], rows,
    129               err('log unavailable'))
    130         rows[-1] = (rows[-1][0], tree(42))
    131         check(variant+'-bad-log', variant+'Request', [0, 'r', 'GET', TARGET], rows,
    132               err('Invalid completion-log response'))
    133         # Repeated request contexts must not leak between sibling scopes.
    134         check(variant+'-contexts', variant+'Requests', [['first', 'second'], TARGET],
    135               script('first') + script('second'), ok([success(), success()]))
    136 
    137     root, alternate = directory / 'primary', directory / 'alternate'
    138     for path, contents in [(root, BODY), (alternate, b'alternate')]:
    139         object_path = path / 'objects' / HASH[:3] / (HASH+'.arboricx')
    140         object_path.parent.mkdir(parents=True)
    141         object_path.write_bytes(contents)
    142 
    143     def io_run(label, export, values):
    144         output = runner.run(label, ['run', '--module', pin, '--name', export,
    145             '--store', runner.store, '--io', '--format', 'ternary', '--profile',
    146             *arguments(label, values)])
    147         text = next(re.match(r'^([012]+)', line)[1] for line in output.splitlines()
    148                     if re.match(r'^[012]+', line))
    149         raw = bytes(map(int, text))
    150         return raw[:end(raw)]
    151 
    152     for variant in 'acd':
    153         for mode, expected in [(0, success()), (1, success(b'alternate')), (2, success())]:
    154             label = f'{variant}-filesystem-{mode}'
    155             actual = io_run(label, variant+'Filesystem',
    156                             [str(root), str(alternate), mode, 'real', TARGET])
    157             assert actual == encode([ok(expected), [event('real')]]), label
    158             passed.append(label)
    159         actual = io_run(variant+'-native-log-failure', variant+'FilesystemLogFailure',
    160                         [str(root), str(alternate), 0, 'failed', TARGET])
    161         assert actual == encode([err('log unavailable'), [event('failed')]])
    162         passed.append(variant+'-native-log-failure-retains-attempt')
    163         # Independent current expectations replace execution of the frozen v1
    164         # route. Missing/OS-denied reads redact diagnostics; invalid hashes never
    165         # reach the file adapter. Actual v2 registry integrity is tested separately.
    166         for label, target, status, body in [
    167                 ('success', TARGET, 200, BODY),
    168                 ('invalid', '/_arboricx/bundle/hash/../secret', 400,
    169                  'expected 64 lowercase hexadecimal hash characters'),
    170                 ('missing', '/_arboricx/bundle/hash/'+'b'*64, 404, 'Artifact unavailable\n'),
    171                 ('denied', TARGET, 404, 'Artifact unavailable\n')]:
    172             if label == 'denied':
    173                 root.chmod(0)
    174             try:
    175                 actual = io_run(f'{variant}-filesystem-check-{label}', variant+'Filesystem',
    176                                 [str(root), str(alternate), 0, 'real', target])
    177             finally:
    178                 root.chmod(0o700)
    179             hash = target.removeprefix('/_arboricx/bundle/hash/')
    180             expected = success() if status == 200 else response(status, body)
    181             assert actual == encode([ok(expected), [event('real', hash, status)]]), label
    182             passed.append(variant+'-filesystem-'+label)
    183     assert not (directory / 'secret').exists()
    184     assert (root / 'objects' / HASH[:3] / (HASH+'.arboricx')).read_bytes() == BODY
    185 
    186     benchmarks = []
    187     if args.benchmark:
    188         for backend in ['scripted', 'filesystem']:
    189             for count in [1, 20]:
    190                 contexts = list(range(count))
    191                 rows = [row for context in contexts for row in script(context)]
    192                 samples = {variant: [] for variant in 'acd'}
    193                 for repeat, order in enumerate(['acd', 'cda', 'dac']):
    194                     for variant in order:
    195                         label = f'bench-{backend}-{variant}-{count}-{repeat}'
    196                         if backend == 'scripted':
    197                             check(label, variant+'Requests', [contexts, TARGET], rows, ok([success()]*count))
    198                         else:
    199                             actual = io_run(label, variant+'RequestsIO',
    200                                 [str(root), str(alternate), contexts, TARGET])
    201                             assert actual == encode([ok([success()]*count), [event(context) for context in contexts]]), label
    202                             passed.append(label)
    203                         samples[variant].append(runner.records[-1])
    204                 for variant, records in samples.items():
    205                     benchmarks.append(dict(backend=backend, count=count, variant=variant,
    206                         seconds=statistics.median(r['invoke_seconds'] for r in records),
    207                         allocated_bytes=statistics.median(r['allocated_bytes'] for r in records)))
    208     sources = [source, ROOT / 'test/effect_interfaces/workflow.tri', Path(__file__).resolve(),
    209                ROOT / 'test/effect_interfaces/tricu.workspace', ROOT / 'lib/arboricx/server.tri',
    210                ROOT / 'lib/http.tri', ROOT / 'lib/base.tri', ROOT / 'lib/io.tri', ROOT / 'lib/io/storage.tri',
    211                ROOT / 'bootstrap/build-stable.arboricx']
    212     report = dict(passed=passed, benchmarks=benchmarks, host=str(args.tricu.resolve()),
    213                   bundle_sha256=hashlib.sha256(bundle.read_bytes()).hexdigest(),
    214                   inputs={str(path.relative_to(ROOT)): hashlib.sha256(path.read_bytes()).hexdigest()
    215                           for path in sources},
    216                   limits=['GET request slice, not sockets or full server throughput',
    217                           'completion logging added by experiment, not shipped behavior',
    218                           'filesystem benchmarks use warm budgeted local reads and explicit source-state log capture',
    219                           'all request responses fully rendered; scripted runs also render full traces, filesystem runs logs',
    220                           'no route indexing; C/D use the same linear lookup'])
    221     (directory / 'report.json').write_text(json.dumps(report, indent=2)+'\n')
    222     print(json.dumps(report, indent=2))
    223 
    224 
    225 if __name__ == '__main__':
    226     main()