tricu

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

digest_hex.py (5354B)


      1 #!/usr/bin/env python3
      2 """Artifact-only guarded digest regression; no source compilation or generations.
      3 
      4 python3 test/digest_hex.py result/bin/tricu --subject SOURCE_OBJECT.arboricx
      5 The subject can also be the checked compiler-library fixture. The old worker is
      6 an immutable accepted-generation golden, never derived from the candidate.
      7 """
      8 import argparse
      9 import hashlib
     10 import json
     11 from pathlib import Path
     12 import subprocess
     13 import sys
     14 import tempfile
     15 import time
     16 
     17 sys.dont_write_bytecode = True
     18 ROOT = Path(__file__).resolve().parent.parent
     19 
     20 
     21 def number(value, zeros=0):
     22     payload = bytearray()
     23     while value:
     24         payload.extend(b'\x02\x01\x00' if value & 1 else b'\x02\x00')
     25         value >>= 1
     26     return bytes(payload) + b'\x02\x00' * zeros + b'\x00'
     27 
     28 
     29 def tree_list(values, tail=b'\x00'):
     30     return b''.join(b'\x02' + value for value in values) + tail
     31 
     32 
     33 def cases():
     34     rows = [(f'byte-{n}', tree_list([number(n)]), 48 <= n <= 57 or 97 <= n <= 102)
     35             for n in range(256)]
     36     rows += [(f'wide-{n}', tree_list([number(n)]), False) for n in (256,257,511,512,65535)]
     37     rows += [(f'noncanonical-{n}-{z}', tree_list([number(n,z)]), None)
     38              for n in (0,1,47,48,49,57,58,96,97,102,103,255) for z in (1,2)]
     39     malformed = (b'\x01\x00', b'\x01\x01\x00', b'\x02\x02\x00\x00\x00',
     40                  b'\x02\x01\x01\x00\x00', b'\x02\x00\x01\x00')
     41     rows += [(f'malformed-{i}', tree_list([n]), None) for i,n in enumerate(malformed)]
     42     shapes = (b'\x00', b'\x01\x00', b'\x01\x01\x00', tree_list([number(97)],b'\x01\x00'),
     43               tree_list([number(103)],b'\x01\x00'))
     44     rows += [(f'list-shape-{i}', n, None) for i,n in enumerate(shapes)]
     45     strings = (b'0123456789abcdef', b'a'*64, b'0'*64, b'f'*64, b'0123456789abcdef'*4,
     46                b'A'+b'a'*63, b'a'*63+b'G', b'a'*31+b'\x00'+b'a'*32, b'a'*31+b'\xff'+b'a'*32)
     47     rows += [(f'digest-{i}',tree_list([number(n) for n in text]),
     48               all(n in b'0123456789abcdef' for n in text)) for i,text in enumerate(strings)]
     49     return rows
     50 
     51 
     52 def main():
     53     p=argparse.ArgumentParser(description=__doc__)
     54     p.add_argument('tricu',type=Path)
     55     p.add_argument('--subject',type=Path,required=True)
     56     p.add_argument('--directory',type=Path,required=True,help='fresh private store/report directory')
     57     a=p.parse_args();root=a.directory.resolve();root.mkdir(parents=True,exist_ok=False)
     58     subject=a.subject.resolve();exe=str(a.tricu.resolve());store=root/'store';start=time.monotonic()
     59     from bootstrap_archive import select_exports
     60     from stable_bundle import sections
     61     # Select just two roots, not a full compiler-library import.
     62     names=('sourceObjectHashHex','sourceObjectHashValid')
     63     manifest,_=sections(subject)
     64     prefix='source_object.' if b'source_object.sourceObjectHashHex' in manifest else ''
     65     selected=root/'subject.arboricx'
     66     select_exports(subject,selected,[(prefix+names[0],'worker'),(prefix+names[1],'valid')])
     67     golden=ROOT/'test/fixtures/hash-hex-reference.arboricx'
     68     expected='b5419c577b5a3e8a4458261bfa6934a43cce40eb86e24326aa9a33d6d0552e81'
     69     assert hashlib.sha256(b'tricu.hash-hex-reference.v1\0'+golden.read_bytes()).hexdigest()==expected
     70     def run(args):
     71         assert time.monotonic()-start<120,'whole regression batch exceeded 120s'
     72         reply=subprocess.run([exe,'arboricx',*map(str,args),'+RTS','-N1','-M256m','-RTS'],
     73                              capture_output=True,text=True,timeout=2)
     74         assert reply.returncode==0,(args,reply.stdout,reply.stderr)
     75         return reply.stdout.strip()
     76     import re
     77     def imported(path):
     78         reply=run(['import','--file',path,'--store',store,'--indexed'])
     79         return re.search(r'Created module alias .* -> ([0-9a-f]{64})',reply)[1]
     80     old=imported(golden);new=imported(selected);input=root/'input.tree';records=[]
     81     def worker(module,payload):
     82         input.write_bytes(payload)
     83         return run(['run','--store',store,'--module',old,'--name','fix',
     84                     '--arg-export',module+':worker','--arg-tree-file',input,'--format','ternary'])
     85     for label,payload,oracle in cases():
     86         before=worker(old,payload)
     87         input.write_bytes(payload)
     88         after=run(['run','--store',store,'--module',new,'--name','worker',
     89                    '--arg-tree-file',input,'--format','ternary'])
     90         assert after==before,(label,before,after)
     91         if oracle is not None:assert after==('10' if oracle else '0'),(label,after,oracle)
     92         records.append(dict(label=label,tree_sha256=hashlib.sha256(payload).hexdigest(),result=after))
     93     for size in (0,1,63,64,65):
     94         input.write_bytes(tree_list([number(97)]*size))
     95         actual=run(['run','--store',store,'--module',new,'--name','valid','--arg-tree-file',input,'--format','ternary'])
     96         assert actual==('10' if size==64 else '0'),(size,actual)
     97     report=dict(complete=True,seconds=time.monotonic()-start,subject_sha256=hashlib.sha256(subject.read_bytes()).hexdigest(),
     98                 runtime_sha256=hashlib.sha256(Path(exe).read_bytes()).hexdigest(),cases=records,
     99                 worker_cases=len(records),public_length_cases=5,old_reference_digest=expected)
    100     (root/'report.json').write_text(json.dumps(report,indent=2)+'\n')
    101     print(f'PASS: {len(records)} worker cases + 5 public length cases; {report["seconds"]:.2f}s')
    102 
    103 
    104 if __name__=='__main__':
    105     main()