io_v2_lines.py (7843B)
1 #!/usr/bin/env python3 2 """Independent LF/UTF-8/limit oracles, fragmented pipes and interactive prompts.""" 3 import argparse 4 import json 5 import subprocess 6 import selectors 7 import time 8 from pathlib import Path 9 from source_order import Runner 10 from http_headers import number, result, tree_list, parse_tree_output 11 from io_v2_text import text, problem 12 ROOT=Path(__file__).resolve().parent.parent 13 14 def event(s,ending): return tree_list([text('line'),text(s),text(ending)]) 15 def lines(data): 16 parts=data.split(b'\n') 17 return [event(p.decode('utf8'),'lf') for p in parts[:-1]] + ([event(parts[-1].decode('utf8'),'eof')] if parts[-1] else []) 18 19 def main(): 20 p=argparse.ArgumentParser(description=__doc__);p.add_argument('tricu',type=Path);p.add_argument('--directory',type=Path,required=True) 21 a=p.parse_args();d=a.directory.resolve();d.mkdir(parents=True,exist_ok=False);runner=Runner(a.tricu,d);checks=[] 22 def compile(label,path): 23 bundle=d/(label+'.arboricx');runner.run(label+'-compile',['compile','--file',path,'--store',runner.store,'--output',bundle,'--compile-timeout','600'],seconds=660) 24 return runner.imported(bundle,label+'-import') 25 pin=compile('lines',ROOT/'demos/io-v2/lines.tri') 26 def invoke(label,name,values): 27 args=['--io'] 28 for i,value in enumerate(values): 29 f=d/f'{label}-{i}.tree';f.write_bytes(value);args+=['--arg-tree-file',f] 30 return runner.invoke(label,pin,name,args) 31 def collect(label,data,bound,expected=None): 32 path=d/(label+'.input');path.write_bytes(data) 33 actual=invoke(label,'collect',[text(str(path)),number(bound)]) 34 assert actual==(expected if expected is not None else result(True,tree_list(lines(data)))),label 35 checks.append(label) 36 for i,data in enumerate([b'',b'\n',b'\n\n',b'a',b'a\n',b'a\nb',b'a\n\n',b'a\r\nb\rc\n\0', '\ufeffλ😀e\u0301\n'.encode(),b'a'*4092+'😀'.encode()+b'\nnext\n',b'a'*4093+b'\n',b'a'*8190+b'\nend']): 37 collect(f'valid-{i}',data,max(map(len,data.split(b'\n')))) 38 for carry in [1,2,3]: 39 data=b'a'*(4093-carry)+'😀'.encode()+b'\nnext\n' 40 collect(f'carry-{carry}',data,4100) 41 collect('zero-empty-lines',b'\n\n',0) 42 over=problem('invalid-bound','text/read-line','line exceeds byte limit') 43 collect('overlong',b'ab\n',1,result(False,tree_list([b'\0',tree_list([text('read-line'),number(2),over])]))) 44 invalid=problem('invalid-data','utf8/decode','invalid UTF-8 leading byte at byte 0') 45 collect('later-invalid',b'ok\n\xff\n',20,result(False,tree_list([tree_list([event('ok','lf')]),tree_list([text('read-line'),number(1),invalid])]))) 46 partial=problem('invalid-data','utf8/decode','incomplete UTF-8 sequence at byte 0') 47 collect('incomplete-before-lf',b'\xe2\n',20,result(False,tree_list([b'\0',tree_list([text('read-line'),number(1),partial])]))) 48 collect('incomplete-at-eof',b'\xe2',20,result(False,tree_list([b'\0',tree_list([text('read-line'),number(1),partial])]))) 49 # Returning a first line never validates unrequested later buffered text. 50 path=d/'later-invalid.input' 51 assert invoke('first-only','first',[text(str(path)),number(20)])==result(True,event('ok','lf'));checks.append('first-only') 52 stale=problem('stale-handle','stream/read','') 53 assert invoke('closed-cursor','closed',[text(str(path)),number(20)])==result(False,tree_list([text('read-line'),number(0),stale])) 54 badlimit=problem('invalid-bound','text/limit','invalid text byte limit') 55 assert invoke('invalid-bound-before-read','closed',[text(str(path)),number(9223372036854775808)])==result(False,badlimit) 56 checks+=['closed-cursor','invalid-bound-before-read'] 57 def command(module,name,values=(),no_result=False): 58 cmd=[str(runner.exe),'arboricx','run','--module',module,'--name',name,'--store',str(runner.store),'--io','--format','ternary'] 59 for i,value in enumerate(values): 60 f=d/f'pipe-{name}-{i}.tree';f.write_bytes(value);cmd+=['--arg-tree-file',str(f)] 61 if no_result:cmd+=['--no-result'] 62 return cmd 63 data='A😀λ\n\ufeff\r\nlast'.encode() 64 for split in range(len(data)+1): 65 proc=subprocess.Popen(command(pin,'stdinLines',[number(100)]),stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 66 try: 67 proc.stdin.write(data[:split]);proc.stdin.flush();time.sleep(.02) 68 proc.stdin.write(data[split:]);proc.stdin.close();proc.stdin=None 69 out,err=proc.communicate(timeout=20) 70 assert proc.returncode==0,(split,err) 71 assert parse_tree_output(out.decode())==result(True,tree_list(lines(data))),split 72 finally: 73 if proc.poll() is None:proc.kill();proc.wait() 74 checks.append('every-pipe-split') 75 proc=subprocess.Popen(command(pin,'cancelled'),stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 76 try: 77 # Do not close stdin: cancellation, rather than EOF, must release the read. 78 proc.wait(timeout=10);out=proc.stdout.read();err=proc.stderr.read() 79 assert proc.returncode==0,err 80 assert parse_tree_output(out.decode())==tree_list([text('cancelled'),text('line-stop'),b'\0']) 81 finally: 82 if proc.poll() is None:proc.kill();proc.wait() 83 proc.stdin.close();proc.stdout.close();proc.stderr.close() 84 checks.append('cancel-blocked-input') 85 demo=compile('getLineAsync',ROOT/'demos/interactionTrees/getLineAsync.tri') 86 prompt=b'Please enter your first name: \n(this printed before you typed anything)\n(this second line also printed before you typed anything)\n' 87 for label,payload,wanted in [('name','λ😀\n'.encode(),'Hello, λ😀!\n'.encode()),('empty',b'\n',b'Hello, !\n'),('eof',b'',b'End of input.\n')]: 88 proc=subprocess.Popen(command(demo,'main',no_result=True),stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 89 try: 90 collected=b'';deadline=time.monotonic()+10 91 with selectors.DefaultSelector() as sel: 92 sel.register(proc.stdout,selectors.EVENT_READ) 93 while len(collected)<len(prompt): 94 assert time.monotonic()<deadline,'prompt blocked on input' 95 if sel.select(.1): collected+=__import__('os').read(proc.stdout.fileno(),4096) 96 assert collected==prompt,collected 97 out,err=proc.communicate(payload,timeout=20) 98 assert proc.returncode==0 and out==wanted,(label,out,err) 99 finally: 100 if proc.poll() is None:proc.kill();proc.wait() 101 checks.append('interactive-'+label) 102 for label,wanted in [('greet',b'Hello, tricu\n'),('forkAwait',b'working: job1\nworking: job2\nGot job1-result and job2-result\n')]: 103 demo=compile(label,ROOT/f'demos/interactionTrees/{label}.tri') 104 run=subprocess.run(command(demo,'main',no_result=True),capture_output=True,timeout=30) 105 assert run.returncode==0 and run.stdout==wanted,(label,run.stdout,run.stderr) 106 checks.append(label) 107 with open('/dev/full','wb') as full: 108 failed=subprocess.run(command(demo,'main',no_result=True),stdout=full,stderr=subprocess.PIPE,timeout=20) 109 assert failed.returncode!=0,(label,failed.stderr) 110 checks.append(label+'-failed-output') 111 for count in [1024,16384]: 112 path=d/f'churn-{count}.input';path.write_bytes(b'x\n'*count) 113 assert invoke(f'churn-{count}','count',[text(str(path)),number(1)])==number(count) 114 row=runner.records[-1] 115 assert row['peak_kib']<128*1024 and row['rts_process']['maximum_live_bytes']<16*1024*1024,row 116 checks.append(f'churn-{count}') 117 (d/'report.json').write_text(json.dumps(dict(passed=checks,records=runner.records),indent=2)+'\n') 118 print(f'PASS {len(checks)} line/interactive controls (including every pipe split)') 119 if __name__=='__main__':main()