tricu

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

io_lines.py (7837B)


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