tricu

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

socket_conveniences.py (4564B)


      1 #!/usr/bin/env python3
      2 """Opt-in selected-artifact socket helper effects; no live sockets or compilation.
      3 
      4 Independent complete action payloads, order, errors and remainders. The retained
      5 reference intentionally leaks on post-acquisition failures; only those cases
      6 change. Terminal abort is not an ordinary Result cleanup path.
      7 """
      8 import argparse
      9 import json
     10 from pathlib import Path
     11 from http_headers import Runner, LEAF, number, pair, result, byte_tree, tree_list, sha, expect
     12 from http_pipeline import Case, action, script, finished, invoke, check_case
     13 from bundle_dag import merge_bundles
     14 
     15 
     16 def cases():
     17     sock = number(7)
     18     create, close = action(70, LEAF), action(71, sock)
     19     addr, port = byte_tree(b'127.0.0.1'), number(9050)
     20     bound = action(72, pair(sock, pair(addr, port)))
     21     listen = action(73, pair(sock, number(128)))
     22     connect = action(75, pair(sock, pair(addr, port)))
     23     success = result(True, sock, number(23))
     24     ready = result(True, number(19), number(29))
     25     failure = result(False, byte_tree(b'denied'), byte_tree(b'opaque remainder'))
     26     cleanup_failure = result(False, byte_tree(b'close failed'), number(41))
     27     def row(label, export, args, replies, value, trace, previous=None):
     28         return Case(label, export, (tree_list(args), script(replies)), finished(value, trace), 'call', previous)
     29     for export, args, steps in [
     30         ('listenSocket', [addr,port,number(128)], [(72,bound),(73,listen)]),
     31         ('connectTo', [addr,port], [(75,connect)]),
     32     ]:
     33         yield row(export+'-acquire-failure',export,args,[(70,failure)],failure,[create])
     34         replies=[(70,success)]+[(tag,ready) for tag,_ in steps]
     35         value=result(True,sock,number(29) if export=='connectTo' else LEAF)
     36         yield row(export+'-success',export,args,replies,value,[create]+[a for _,a in steps])
     37         for index,(tag,primitive) in enumerate(steps):
     38             for label,closed in [('ok',ready),('error',cleanup_failure)]:
     39                 replies=[(70,success)]+[(t,ready) for t,_ in steps[:index]]+[(tag,failure),(71,closed)]
     40                 trace=[create]+[a for _,a in steps[:index]]+[primitive]
     41                 previous=finished(failure,trace,[(71,closed)])
     42                 yield row(f'{export}-{tag}-failure-close-{label}',export,args,replies,failure,trace+[close],previous)
     43     # New adapter itself must leave successful/non-Result values alone and
     44     # explicitly does not catch terminal aborts.
     45     for label,value in [('success',ready),('leaf',LEAF),('malformed',b'\1\0')]:
     46         yield row('adapter-'+label,'closeSocketOnError',[sock,action(0,value)],[],value,[])
     47     abort=action(2,byte_tree(b'stop'))
     48     yield Case('adapter-abort','closeSocketOnError',(tree_list([sock,abort]),script([])),finished(abort,[abort],label=b'abort'),'call')
     49 
     50 
     51 def main():
     52     p=argparse.ArgumentParser(description=__doc__)
     53     p.add_argument('tricu',type=Path)
     54     for name in ['subject','reference','driver','directory']:
     55         p.add_argument('--'+name,type=Path,required=True)
     56     a=p.parse_args();a.directory.mkdir(parents=True)
     57     runner=Runner(a.tricu,a.directory)
     58     pins={};report=dict(complete=False,cases=[],artifacts={},runtime_sha256=sha(a.tricu.read_bytes()),harness_sha256=sha(Path(__file__).read_bytes()))
     59     for name in ['subject','reference','driver']:
     60         path=getattr(a,name);local=a.directory/(name+'.arboricx');local.write_bytes(path.read_bytes())
     61         pins[name]=runner.imported(local,name)
     62         report['artifacts'][name]=sha(local.read_bytes())
     63     try:
     64         for case in cases():
     65             actual=invoke(runner,pins['subject'],case,'subject',pins['driver'])
     66             before=None if case.label.startswith('adapter-') else invoke(runner,pins['reference'],case,'reference',pins['driver'])
     67             check_case(case,actual,before)
     68             report['cases'].append(dict(label=case.label,result_sha256=sha(actual),intentional_cleanup_change=case.previous is not None))
     69         wrong=a.directory/'wrong.arboricx'
     70         merge_bundles(wrong,[(a.subject,[('connectTo','listenSocket')])],prune_unreachable=True)
     71         pin=runner.imported(wrong,'wrong')
     72         control=next(c for c in cases() if c.label=='listenSocket-success')
     73         assert invoke(runner,pin,control,'wrong',pins['driver']) != control.expected
     74         report.update(complete=True,wrong_subject_rejected=True)
     75     finally:
     76         (a.directory/'report.json').write_text(json.dumps(report,indent=2)+'\n')
     77     print(f'PASS: {len(report["cases"])} socket effect cases + wrong-subject control')
     78 
     79 
     80 if __name__=='__main__':
     81     main()