tricu

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

project_inspection.py (8000B)


      1 #!/usr/bin/env python3
      2 """Offline, read-only project reports against independent DAG/CAS fixtures."""
      3 import argparse
      4 import hashlib
      5 from pathlib import Path
      6 import struct
      7 import subprocess
      8 import tempfile
      9 from stable_bundle import assembled, sections
     10 
     11 ROOT = Path(__file__).resolve().parent.parent
     12 ABI = 'arboricx.abi.tree.v1'
     13 KIND = 'arboricx.indexed-tree.v1'
     14 
     15 
     16 def main():
     17     p = argparse.ArgumentParser(description=__doc__)
     18     p.add_argument('tricu', type=Path)
     19     exe = p.parse_args().tricu.resolve()
     20     with tempfile.TemporaryDirectory(prefix='project-inspection-') as temp:
     21         root = Path(temp); store = root / 'cas'
     22         def put(kind, data):
     23             pin = hashlib.sha256(kind.encode() + b'\0' + data).hexdigest()
     24             path = store / 'objects' / pin[:3] / pin
     25             path.parent.mkdir(parents=True, exist_ok=True); path.write_bytes(data)
     26             return pin
     27         manifest, _ = sections(ROOT / 'test/fixtures/id.arboricx')
     28         at = 12
     29         for _ in range(9):
     30             size = struct.unpack_from('>I', manifest, at)[0]; at += 4 + size
     31         assert struct.unpack_from('>I', manifest, at)[0] == 0
     32         prefix = manifest[:at + 5]
     33         def text(value):
     34             data = value.encode(); return struct.pack('>I', len(data)) + data
     35         def graph(nodes, selected=None):
     36             selected = len(nodes) - 1 if selected is None else selected
     37             m = (prefix + struct.pack('>II', 1, selected) + text('default') + struct.pack('>I', 1)
     38                  + text('root') + struct.pack('>I', selected) + text('term') + text(ABI) + struct.pack('>II', 0, 0))
     39             records = [bytes([len(children)]) + b''.join(struct.pack('>I', c) for c in children) for children in nodes]
     40             return assembled(m, struct.pack('>Q', len(nodes)) + b''.join(struct.pack('>I', len(r)) + r for r in records))
     41         def esc(value):
     42             return value.replace('%', '%25').replace('\t', '%09').replace('\n', '%0A').replace('\r', '%0D')
     43         def module(exports):
     44             data = 'arboricx.module-manifest.v1\n'
     45             for name, obj, abi, kind in exports:
     46                 data += '\t'.join(['export', esc(name), esc(kind), obj, esc(abi)]) + '\n'
     47             return put('arboricx.module-manifest.v1', data.encode())
     48         def lock(file, pin, version):
     49             file.write_text('arboricx.project-lock.v1\nfoundation\thome\thttp://127.0.0.1:1\tbase\t' + version + '\t' + pin + '\t' + 'b'*64 + '\n')
     50         before, after = root / 'before.lock', root / 'tricu.lock'
     51         declaration = root / 'tricu.project'
     52         declaration.write_text('arboricx.project.v1\nfoundation\thome\tbase\t2\n')
     53         def snapshot():
     54             return {str(p.relative_to(root)): (p.read_bytes(), p.stat().st_mtime_ns) for p in root.rglob('*') if p.is_file()}
     55         def run(action, *args, ok=True, selected_store=store):
     56             saved = snapshot()
     57             reply = subprocess.run([str(exe), 'project', action, '--store', str(selected_store), *map(str, args),
     58                                     '+RTS', '-N1', '-M512m', '-RTS'], cwd=root, capture_output=True, text=True, timeout=30)
     59             assert (reply.returncode == 0) == ok, (reply.stdout, reply.stderr)
     60             assert snapshot() == saved, 'report mutated files'
     61             assert '\x1b' not in reply.stdout
     62             return reply.stdout
     63         def diff(ok=True, selected_store=store):
     64             return run('diff', '--before', before, '--after', after, ok=ok, selected_store=selected_store)
     65         left = put(KIND, graph([[], [0], [0, 0], [1, 2]]))
     66         right = put(KIND, graph([[], [0, 0], [0], [2, 1]]))
     67         assert left != right  # reordered physical nodes, identical ordered tree
     68         old = module([('value', left, ABI, KIND)])
     69         new = module([('value', right, ABI, KIND), ('extra', left, ABI, KIND)])
     70         lock(before, old, '1'); lock(after, new, '2')
     71         report = diff()
     72         assert 'object changed; tree identical' in report and 'export extra: added' in report
     73         assert 'Updated foundation' in report
     74         # Bind once via the real ownership protocol; reporting must not rewrite it.
     75         subprocess.run([str(exe), 'project', 'bind', '--store', str(store), '+RTS', '-N1', '-M512m', '-RTS'], cwd=root, check=True, capture_output=True, timeout=30)
     76         assert 'local module: verified' in run('explain')
     77         binding_bytes = (root / 'foundation.tri').read_bytes()
     78         (root / 'foundation.tri').write_text('edited source\n')
     79         assert 'generated binding: differs' in run('explain', ok=False)
     80         (root / 'foundation.tri').write_bytes(binding_bytes)
     81         pending = root / '.arboricx-project-bindings.pending'
     82         pending.write_text('interrupted')
     83         assert 'pending or unreadable' in run('explain', ok=False)
     84         pending.unlink()
     85         assert 'generated binding: matches lock' in run('explain')
     86         declaration.write_text(declaration.read_text().replace('base\t2', 'base\t3'))
     87         assert 'declared: home base@3' in run('explain', ok=False)
     88         # Same pin is NOT sufficient evidence when its local bytes are absent/bad.
     89         lock(before, new, '2')
     90         target = store / 'objects' / right[:3] / right
     91         saved = target.read_bytes(); target.unlink()
     92         assert 'tree unknown' in diff(ok=False) and 'missing' in diff(ok=False)
     93         target.write_bytes(saved + b'corrupt')
     94         assert 'invalid:' in diff(ok=False)
     95         target.write_bytes(saved)
     96         empty = root / 'absent-store'
     97         assert 'unknown (module evidence unavailable)' in diff(ok=False, selected_store=empty)
     98         assert not empty.exists()
     99         # Exponential logical tree and an extra unreachable record: no unfolding.
    100         nodes = [[]] + [[i-1, i-1] for i in range(1, 81)]
    101         huge = put(KIND, graph(nodes))
    102         extra = put(KIND, graph(nodes + [[0]], 80))
    103         lock(before, module([('huge', huge, ABI, KIND)]), '1')
    104         lock(after, module([('huge', extra, ABI, KIND)]), '2')
    105         assert 'object changed; tree identical' in diff()
    106         different = put(KIND, graph(nodes[:-1] + [[79]]))
    107         lock(after, module([('huge', different, ABI, KIND)]), '2')
    108         assert 'tree different' in diff()
    109         # Unsupported ABIs/kinds are explicit; no semantic equivalence inferred.
    110         lock(after, module([('huge', huge, 'future.abi', KIND)]), '2')
    111         report = diff(ok=False)
    112         assert 'ABI changed' in report and 'unsupported:' in report and 'tree unknown' in report
    113         lock(after, module([('huge', huge, ABI, 'future.kind')]), '2')
    114         assert 'kind changed' in diff(ok=False)
    115         # Malformed-but-authenticated DAG and invalid object paths fail as evidence.
    116         malformed = put(KIND, graph([[], [1]], 0))
    117         lock(after, module([('huge', malformed, ABI, KIND)]), '2')
    118         assert 'invalid:' in diff(ok=False)
    119         lock(after, module([('huge', '../outside', ABI, KIND)]), '2')
    120         assert 'invalid:' in diff(ok=False)
    121         unsafe = 'tab\tline\n%\x1b é'
    122         lock(after, module([(unsafe, left, ABI, KIND)]), '2')
    123         report = diff()
    124         assert '%09' in report and '%0a' in report and '%25' in report and '%1b' in report and '%c3%a9' in report
    125         assert 'export huge: removed' in report
    126         oversized = put('arboricx.module-manifest.v1', b'x' * 65537)
    127         lock(after, oversized, '2')
    128         assert 'inspection byte budget' in diff(ok=False)
    129         # Nonregular CAS files must not be followed or block on FIFO reads.
    130         import os
    131         path = store / 'objects' / oversized[:3] / oversized
    132         path.unlink(); os.mkfifo(path)
    133         assert 'expected regular CAS file' in diff(ok=False)
    134         path.unlink()
    135         after.write_text('arboricx.project-lock.v1\n')
    136         assert 'Removed foundation' in diff()
    137         print('PASS read-only explain/diff; independent DAG equality, exponential sharing, missing/corrupt/unsupported evidence and display escaping')
    138 
    139 
    140 if __name__ == '__main__':
    141     main()