v. vanilla 2.0

tests/test_stdlib.py

← All source files · Download this file

10,209 bytes · SHA256 10e9e93ca27ae982115f6b9d29246a6b3da60bc1325d695c13a7669a5ca81bcf

#!/usr/bin/env python3
"""Pure standard-library helpers; exact outputs, errors, aliases and budgets."""
import json, os, subprocess, sys, tempfile
from pathlib import Path
ROOT=Path(__file__).resolve().parent.parent
BIN=Path(sys.argv[1]).resolve()
CASES=[]
def q(s): return json.dumps(s,ensure_ascii=False)
def case(name,body,out='',error=None,raw=False):
    CASES.append((name,body if raw else 'function main\nvar a b c d x e i\n'+body+'\nkill main\n',out,error))
def caught(body): return 'try\n'+body+'\ncatch $e\nprint caught:\nend try\n'
case('numeric stable sort','array create $a 10 2 02 -1 +2\narray sort $b $a numeric\njoin $x $b ,\nprint $x\njoin $x $a ,\nprint : $x','-1,2,02,+2,10:10,2,02,-1,+2')
case('lexical sort alias','array create $a 10 2 A a\narray sort $a $a lexical\njoin $x $a ,\nprint $x','10,2,A,a')
case('reverse','array create $a a b c\narray reverse $b $a\narray reverse $a $a\njoin $x $b ,\nprint $x\njoin $x $a ,\nprint : $x','c,b,a:c,b,a')
case('slice','array create $a a b c d\narray slice $b $a 1 2\njoin $x $b ,\nprint $x\narray slice $a $a 4 0\narray len $x $a\nprint : $x','b,c:0')
case('find exact first','array create $a 02 2 2\narray find $x $a 2\nprint $x\narray find $x $a absent\nprint : $x','1:-1')
case('remove copy alias','array create $a a b c\narray remove $b $a 1\narray remove $a $a 0\njoin $x $b ,\nprint $x\njoin $x $a ,\nprint : $x','a,c:b,c')
case('unique first byte equality','array create $a 2 02 2 A a A\narray unique $a $a\njoin $x $a ,\nprint $x','2,02,A,a')
for op in ['reverse','unique','sort']:
    case('empty '+op,'array create $a\narray '+op+' $a $a'+(' numeric' if op=='sort' else '')+'\narray len $x $a\nprint $x','0')
for command in ['array sort $a $a numeric','array slice $a $a 2 3','array remove $a $a 3']:
    case('array atomic '+command,'array create $a 1 bad 3\n'+caught(command)+'join $x $a ,\nprint $x','caught:1,bad,3')
for sub in ['contains','startswith','endswith']:
    for hay,needle,answer in [('abc','',1),('abc','b',int(sub=='contains')),('abc','abc',1),('abc','abcd',0),('Abc','a',0),('é','é',1)]:
        case(sub+repr((hay,needle)),f'text {sub} $x {q(hay)} {q(needle)}\nprint $x',str(answer))
case('pad','text padleft $a x 6 ab\ntext padright $b x 6 ab\nprint $a : $b\ntext padleft $a $a 2 z\nprint : $a','ababax:xababa:ababax')
case('pad empty rejected','set $a keep\n'+caught('text padleft $a x 0 ""')+'print $a','caught:keep')
case('pad max','text padright $a "" 1048576 x\nlen $x $a\nprint $x','1048576')
case('pad over limit','text padleft $a x 1048577 x',error='bounds')
for n in ['NaN','inf','-inf','1e400','1e-400','0x1p0',' 2','2 ','','1,2','1x','+','1e']:
    case('invalid '+repr(n),f'number valid $x {q(n)}\nprint $x\nset $a keep\n'+caught(f'number convert $a {q(n)}')+'print $a','0caught:keep')
for n,out in [('007','7'),('+1.25e2','125'),('-.5','-0.5'),('-0','0'),('1.','1'),('5e-324','5e-324')]:
    case('convert '+n,f'number valid $x {q(n)}\nprint $x :\nnumber convert $a {q(n)}\nprint $a','1:'+out)
for n,p,out in [('2.675',2,'2.67'),('2.5',0,'2'),('3.5',0,'4'),('-0.001',2,'0.00'),('1.2',4,'1.2000'),('1e20',2,'100000000000000000000.00'),('1',100,'1.'+'0'*100)]:
    case('format '+n+':'+str(p),f'number format $a {n} {p}\nprint $a',out)
for p in ['-1','101','1.5','NaN']:
    case('places '+p,'set $a keep\n'+caught('number format $a 1 '+p)+'print $a','caught:keep')
case('json mutate types','json parse $a "[]"\njson parse $b 2\njson append $a $b\njson append $a 2\njson insert $a 1 true\njson remove $a 0\njson stringify $x $a\nprint $x','["true","2"]')
case('json self append','json parse $a "[1]"\njson append $a $a\njson insert $a 0 $a\njson stringify $x $a\nprint $x','[[1,[1]],1,[1]]')
case('json length membership','json parse $a "[1,1.0,true,null,{},[]]"\njson length $x $a\nprint $x\njson parse $b 1\njson contains $x $a $b\nprint : $x\njson contains $x $a 1\nprint : $x\njson parse $a "{}"\njson length $a $a\nprint : $a','6:1:0:0')
for policy,out in [('left','{"a":{"x":1},"b":2,"c":3}'),('right','{"a":{"y":2},"b":2,"c":3}')]:
    case('merge '+policy,'json parse $a '+q('{"a":{"x":1},"b":2}')+'\njson parse $b '+q('{"a":{"y":2},"c":3}')+f'\njson merge $a $a $b {policy}\njson stringify $x $a\nprint $x',out)
case('merge error atomic','json parse $a '+q('{"a":1}')+'\njson parse $b '+q('{"a":2}')+'\n'+caught('json merge $a $a $b error')+'json stringify $x $a\nprint $x','caught:{"a":1}')
case('merge disjoint error policy','json parse $a "{}"\njson parse $b '+q('{"a":1}')+'\njson merge $a $a $b error\njson stringify $x $a\nprint $x','{"a":1}')
for cmd in ['json insert $a 2 x','json remove $a 1','json remove $a -1']:
    case('json atomic '+cmd,'json parse $a "[1]"\n'+caught(cmd)+'json stringify $x $a\nprint $x','caught:[1]')
case('json snapshot named call','function mutate items\njson append $items x\nreturnvalue $items\nkill mutate\nfunction main\nvar a b x\njson parse $a "[]"\ncall $b mutate $a\njson stringify $x $a\nprint $x\njson stringify $x $b\nprint : $x\nkill main\n','[]:["x"]',raw=True)
for cmd in ['array sort $a $a auto','array unique $a','array slice $a $a 0','text contains $a x','text unknown $a x x','number format $a 1','number unknown $a 1','json merge $a $a $a auto','json append $a']:
    case('dead validation '+cmd,'print BAD\nif 0 == 1\n'+cmd+'\nend if',error='error:')
for name in ['text','number']:
    case('reserved function '+name,f'function {name}\nkill {name}\nfunction main\nkill main\n',error='reserved function',raw=True)
    case('reserved parameter '+name,f'function f {name}\nkill f\nfunction main\nkill main\n',error='reserved',raw=True)
case('local text number remain legal','var text number\nset $text ok\nset $number yes\nprint $text : $number','ok:yes')
case('JSON storage limit','json parse $a "[0]"\nfor $i 0 20\njson append $a $a\nend for',error='storage limit')
case('frame budget','text padleft $a x 700000 x\ntext padleft $b x 700000 x\ntext padleft $c x 700000 x',error='frame data limit')
case('array item limit preserved','array create $a\nfor $i 0 16385\narray push $a x\nend for',error='item limit')
# More adversarial coverage: type boundaries, aliasing, byte behavior and caches.
case('JSON empty insert and last remove','json parse $a "[]"\njson insert $a 0 x\njson remove $a 0\njson stringify $x $a\nprint $x','[]')
case('JSON deep member equality','json parse $a '+q('[{"x":[true,null]},1]')+'\njson parse $b '+q('{"x":[true,null]}')+'\njson contains $a $a $b\nprint $a','1')
case('JSON bool differs from number','json parse $a "[1]"\njson parse $b true\njson contains $x $a $b\nprint $x','0')
case('merge all aliases','json parse $a '+q('{"x":1}')+'\njson merge $a $a $a right\njson stringify $a $a\nprint $a','{"x":1}')
case('array find alias','array create $a a b\narray find $a $a b\nprint $a','1')
case('text aliases and cache invalidation','math $a 999\ntext startswith $a 123 1\nmath $x $a+1\nprint $x\ntext padright $a $a 3 $a\nprint : $a','2:111')
case('number alias cache invalidation','math $a 12\nnumber format $a $a 2\nprint $a\nmath $x $a+1\nprint : $x\nnumber convert $a $a\nprint : $a','12.00:13:12')
case('binary NUL text','json parse $a '+q('"a\\u0000b"')+'\njson text $a $a\ntext contains $x $a b\nprint $x\ntext padleft $a $a 4 x\nlen $x $a\nprint : $x','1:4')
case('UTF8 byte width','text padright $a é 3 x\nlen $x $a\nprint $x : $a','3:éx')
for cmd in ['json append $a x','json insert $a 0 x','json remove $a 0','json length $x $a','json contains $x $a x','json merge $x $a $a left']:
    case('JSON rejects scalar '+cmd,'json parse $a 1\nset $x keep\n'+caught(cmd)+'json text $b $a\nprint $b : $x','caught:1:keep')
for cmd in ['array sort $x $a numeric','array reverse $x $a','array slice $x $a 0 0','array find $x $a a','array remove $x $a 0','array unique $x $a']:
    case('array type rejection '+cmd,'set $a scalar\nset $x keep\n'+caught(cmd)+'print $x','caught:keep')
for cmd in ['array reverse $missing $a','json append $missing x','number valid $missing 1','text contains $missing x x']:
    case('undeclared '+cmd,'array create $a\n'+cmd,error='undefined variable')
for n in ['NaN','Infinity','1e400','--2','++2','+-2','1e-400']:
    case('sort rejects '+n,'array create $a 1 '+n+'\nset $x keep\n'+caught('array sort $x $a numeric')+'print $x','caught:keep')
case('JSON empty remove bounds','json parse $a "[]"\njson remove $a 0',error='bounds')
# Each iteration adds two singleton layers without exponential copying.
case('JSON depth fatal','json parse $a "[]"\nfor $i 0 34\njson parse $b "[]"\njson append $b $a\njson parse $a "[]"\njson append $a $b\nend for',error='depth limit')
example=ROOT/'examples/stdlib.v'
if example.exists():
    case('shipped example',example.read_text(), 'Lowest distinct prices: 1, 2.50\nIndex of 1 after reverse: 1\nAfter removal: 1\nContains numeric price: 1\nItem count: 1\nMerged options: {"currency":"USD","places":3}\nInvoice prefix: 1\nInvoice suffix: 1\nInvoice contains: 1\nPadded identifier: 00042\nPadded currency: USD..\nValid number: 1\nNormalized number: 2.5\nBinary64 rounded (not exact decimal): 2.67\n',raw=True)
rows=[]
with tempfile.TemporaryDirectory(prefix='stdlib-') as tmp:
    for name,source,out,error in CASES:
        path=Path(tmp)/'case.v'; path.write_text(source)
        try:
            p=subprocess.run([str(BIN),'run',str(path)],capture_output=True,text=True,timeout=30)
            ok=(p.returncode!=0 and error in p.stderr and p.stdout=='') if error else (p.returncode==0 and p.stdout==out and not p.stderr)
            row=dict(name=name,passed=ok,exit_code=p.returncode)
            if not ok: row.update(stdout=p.stdout,stderr=p.stderr,expected=error or out)
        except Exception as exc: row=dict(name=name,passed=False,error=str(exc))
        rows.append(row); print(('PASS ' if row['passed'] else 'FAIL ')+name,flush=True)
        if not row['passed']: print(json.dumps(row),flush=True)
summary=dict(binary=str(BIN),total=len(rows),passed=sum(r['passed'] for r in rows),cases=rows)
if os.environ.get('STDLIB_REPORT'): Path(os.environ['STDLIB_REPORT']).write_text(json.dumps(summary,indent=2)+'\n')
print(json.dumps({k:v for k,v in summary.items() if k!='cases'}))
sys.exit(summary['passed']!=summary['total'])