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