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