unit.py (100932B)
1 #!/usr/bin/env python3 2 """Fast host-side policy, provenance, RSS and native-scope controls; no builds.""" 3 4 # native_header_equivalence_check 5 import hashlib 6 7 import json 8 9 from pathlib import Path 10 11 import tempfile 12 13 import unittest 14 15 from native_header_equivalence import native_header_equivalence, digest 16 17 class HeaderEquivalenceTests(unittest.TestCase): 18 root_kind = 'unit' 19 20 def setUp(self): 21 self.temporary = tempfile.TemporaryDirectory() 22 self.addCleanup(self.temporary.cleanup) 23 root = Path(self.temporary.name) 24 self.repo, self.generation = root / 'repo', root / 'generation' 25 self.repo.mkdir(); self.generation.mkdir() 26 (self.generation / 'overlay').mkdir() 27 (self.generation / 'cas/cache/source-build').mkdir(parents=True) 28 self.map = self.repo / 'map.v1' 29 old_map = ('arboricx.source-map.v1\nunit\tbase\tbase\tbase.tri\t-\n' + 30 self.root_kind + '\tsource_map\tsource.map\troot.tri\tbase\n') 31 self.map.write_text(old_map.replace('unit\t', 'native\t').replace('reexport\t', 'native\t')) 32 (self.generation / 'source-map.v1').write_text(old_map) 33 raw = {'base': b'x = t\n', 'source_map': b'!import "base" !Local\nf x = x\n'} 34 staged = {'base': b'!export *\nx = t\n\n', 35 'source_map': (b'!import base *\n!export *\n' + 36 (b'!export * from base\n' if self.root_kind == 'reexport' else b'') + 37 b'f x = x\n\n')} 38 compiler = '1' * 64 39 provenance = dict(map_sha256=hashlib.sha256(old_map.encode()).hexdigest(), 40 sources={k: hashlib.sha256(v).hexdigest() for k, v in raw.items()}, 41 compiler_bundle_sha256=compiler) 42 (self.generation / 'inputs.json').write_text(json.dumps(provenance)) 43 (self.generation / 'status.json').write_text(json.dumps(dict(complete=True, root='source_map'))) 44 for alias, path in [('base', 'base.tri'), ('source_map', 'root.tri')]: 45 self.repo.joinpath(path).write_bytes(staged[alias]) 46 self.generation.joinpath('overlay', alias + '.tri').write_bytes(staged[alias]) 47 tree = self.put('arboricx.tree-term.v1', b'\0') 48 manifest = self.put('arboricx.module-manifest.v1', ('arboricx.module-manifest.v1\nexport\tmain\tarboricx.tree-term.v1\t' + tree + '\tarboricx.abi.tree.v1\n').encode()) 49 rows = ['source\t' + digest('arboricx.source.v1', staged[alias]), 50 'compiler\t' + compiler, 51 'options\t' + digest('arboricx.build-options.v1', b'source-module.reference.v1')] 52 receipt = self.put('arboricx.build-receipt.v1', ('arboricx.build-receipt.v1\n' + '\n'.join(rows) + '\noutput\t' + manifest + '\n').encode()) 53 key = digest('arboricx.source-build-key.v1', ('arboricx.source-build-key.v1\n' + '\n'.join(rows) + '\n').encode()) 54 self.generation.joinpath('cas/cache/source-build', alias).write_text('\t'.join(['arboricx.source-build-cache.v1', key, manifest, receipt])) 55 56 def put(self, domain, payload): 57 pin = digest(domain, payload) 58 path = self.generation / 'cas/objects' / pin[:3] / pin 59 path.parent.mkdir(parents=True, exist_ok=True); path.write_bytes(payload) 60 return pin 61 62 def check(self): 63 return native_header_equivalence(self.generation, self.repo, self.map) 64 65 def test_valid_staged_bytes(self): 66 rows, proof = self.check() 67 self.assertEqual(proof['changed'], ['base', 'source_map']) 68 self.assertEqual(len(rows), 2) 69 70 def test_changed_body(self): 71 with (self.repo / 'root.tri').open('a') as f: f.write('-- body change\n') 72 with self.assertRaisesRegex(ValueError, 'staged bytes'): self.check() 73 74 def test_changed_namespace(self): 75 p = self.repo / 'root.tri'; p.write_text(p.read_text().replace('!import base *', '!import base as B')) 76 with self.assertRaisesRegex(ValueError, 'staged bytes'): self.check() 77 78 def test_changed_exports(self): 79 p = self.repo / 'root.tri'; p.write_text(p.read_text().replace('!export *', '!export f')) 80 with self.assertRaisesRegex(ValueError, 'staged bytes|invalid native export'): self.check() 81 82 def test_changed_map_path(self): 83 (self.repo / 'other.tri').write_bytes((self.repo / 'base.tri').read_bytes()) 84 self.map.write_text(self.map.read_text().replace('base.tri', 'other.tri')) 85 with self.assertRaisesRegex(ValueError, 'edges, paths'): self.check() 86 87 def test_incomplete_generation(self): 88 (self.generation / 'status.json').write_text('{"complete":false,"root":"source_map"}') 89 with self.assertRaisesRegex(ValueError, 'completed'): self.check() 90 91 def test_overlay_receipt_corruption(self): 92 # Keep current source equal to the modified overlay: the receipt still 93 # authenticates the old compiler input and must reject this pair. 94 for p in [self.repo / 'root.tri', self.generation / 'overlay/source_map.tri']: 95 with p.open('a') as f: f.write('-- forged overlay\n') 96 with self.assertRaisesRegex(ValueError, 'receipt mismatch'): self.check() 97 98 def test_checkpoint_key_corruption(self): 99 p = self.generation / 'cas/cache/source-build/source_map' 100 parts = p.read_text().split('\t'); parts[1] = '2' * 64; p.write_text('\t'.join(parts)) 101 with self.assertRaisesRegex(ValueError, 'key mismatch'): self.check() 102 103 class ReexportHeaderEquivalenceTests(HeaderEquivalenceTests): 104 # Exercise the same fail-closed controls with the prelude's re-export policy. 105 root_kind = 'reexport' 106 107 108 # native_library_migration_check 109 import unittest 110 111 from native_library_migration import native_candidate, alias, terminal_export, reexport_candidate 112 113 class HeaderTests(unittest.TestCase): 114 def test_local_and_namespace_preserved(self): 115 body = b'-- body\nf x = N.id x\n' 116 raw = b'!import "prelude" !Local\n!import "source.core" N\n' + body 117 candidate, imports = native_candidate(raw, {'prelude': 'prelude', 'source.core': 'source_core'}) 118 self.assertEqual(candidate, b'!import prelude *\n!import source_core as N\n!export *\n'+body) 119 self.assertEqual(imports, [('prelude', '!Local'), ('source_core', 'N')]) 120 121 def test_comments_before_import_preserved(self): 122 raw = b'-- header comment\n!import "prelude" !Local\nx = t\n' 123 candidate, _ = native_candidate(raw, {'prelude': 'prelude'}) 124 self.assertTrue(candidate.endswith(b'-- header comment\nx = t\n')) 125 126 def test_explicit_transitive_dependency_is_recorded(self): 127 raw = b'!import "patterns" !Local\nmain = equal? t t\n' 128 candidate, imports = native_candidate(raw, {'patterns': 'patterns'}, [('prelude', '!Local')]) 129 self.assertEqual(imports, [('prelude', '!Local'), ('patterns', '!Local')]) 130 self.assertTrue(candidate.startswith(b'!import prelude *\n!import patterns *\n')) 131 self.assertTrue(candidate.endswith(b'main = equal? t t\n')) 132 133 def test_unknown_dependency_rejected(self): 134 with self.assertRaisesRegex(ValueError, 'unknown dependency'): 135 native_candidate(b'!import "missing" !Local\nx = t\n', {}) 136 137 def test_malformed_import_rejected(self): 138 with self.assertRaisesRegex(ValueError, 'unsupported legacy'): 139 native_candidate(b'!import "prelude"\nx = t\n', {'prelude': 'prelude'}) 140 141 def test_native_policy_not_widened(self): 142 raw = b'!import dep as D\n!export f\nf = D.f\n' 143 candidate, imports = native_candidate(raw, {'dep': 'dep'}) 144 self.assertEqual(candidate, raw) 145 self.assertEqual(imports, [('dep', 'D')]) 146 147 def test_unknown_native_dependency_rejected(self): 148 with self.assertRaisesRegex(ValueError, 'unknown native'): 149 native_candidate(b'!import dep *\n!export f\nf = t\n', {'other': 'other'}) 150 151 def test_terminal_selector_requires_explicit_exact_symbol(self): 152 raw = b'program x = x\nprogram\n\n' 153 self.assertEqual(terminal_export(raw, 'program'), b'program x = x\n\n') 154 with self.assertRaisesRegex(ValueError, 'exact bare'): 155 terminal_export(b'program x = x\nprogram t\n', 'program') 156 157 def test_terminal_selector_requires_authored_definition(self): 158 with self.assertRaisesRegex(ValueError, 'authored definition'): 159 terminal_export(b'program\n', 'program') 160 161 def test_terminal_selector_rejects_expression_names(self): 162 with self.assertRaisesRegex(ValueError, 'invalid terminal'): 163 terminal_export(b'program t\n', 'program t') 164 165 def test_reexport_directives_precede_body_comments(self): 166 raw = b'-- example\n!import "dep" !Local\n' 167 candidate, imports = native_candidate(raw, {'dep':'dep'}) 168 result = reexport_candidate(raw, candidate, imports) 169 self.assertIn(b'!export *\n!export * from dep\n-- example', result) 170 self.assertEqual(reexport_candidate(result,result,imports),result) 171 172 def test_reexport_policy_rejects_authored_definitions(self): 173 with self.assertRaisesRegex(ValueError,'only comments'): 174 reexport_candidate(b'f = t\n',b'!export *\nf = t\n',[]) 175 176 def test_reexport_policy_rejects_namespace_widening(self): 177 with self.assertRaisesRegex(ValueError,'local imports'): 178 reexport_candidate(b'-- comment\n',b'!export *\n',[('dep','N')]) 179 180 def test_flat_aliases(self): 181 self.assertEqual(alias('source.export-cache'), 'source_export_cache') 182 183 184 # process_rss_check 185 import subprocess 186 187 import unittest 188 189 from types import SimpleNamespace 190 191 from unittest.mock import patch 192 193 from process_rss import ChildRSS, status_fields 194 195 class ChildRSSTests(unittest.TestCase): 196 def monitor(self, candidates=(7, 1001)): 197 monitor = ChildRSS.__new__(ChildRSS) 198 def still_running(timeout): 199 raise subprocess.TimeoutExpired('fake-child', timeout) 200 monitor.proc = SimpleNamespace(pid=7, poll=lambda: None, wait=still_running) 201 monitor.parent, monitor.namespace_index, monitor.pid = 1000, 1, None 202 monitor.candidates = lambda: iter(candidates) 203 return monitor 204 205 def test_namespace_translation_ignores_wrong_host_pid(self): 206 def read(path): 207 if str(path) == '/proc/7/status': 208 return 'Pid: 7\nPPid: 2\nNSpid: 7\nVmRSS: 999999 kB\n' 209 return 'Pid: 1001\nPPid: 1000\nNSpid: 1001 7\nVmRSS: 512 kB\n' 210 monitor = self.monitor() 211 with patch('pathlib.Path.read_text', read): 212 self.assertEqual(monitor.sample(), 512) 213 self.assertEqual(monitor.pid, 1001) 214 215 def test_missing_rss_fails_closed(self): 216 with patch('pathlib.Path.read_text', return_value='Pid: 1001\nPPid: 1000\nNSpid: 1001 7\n'): 217 with self.assertRaisesRegex(RuntimeError, 'cannot monitor'): 218 self.monitor((1001,)).sample() 219 220 def test_inaccessible_rss_fails_closed(self): 221 with patch('pathlib.Path.read_text', side_effect=FileNotFoundError): 222 with self.assertRaisesRegex(RuntimeError, 'cannot monitor'): 223 self.monitor().sample() 224 225 def test_cached_pid_reuse_fails_closed(self): 226 monitor = self.monitor(); monitor.pid = 1001 227 with patch('pathlib.Path.read_text', return_value='Pid: 1001\nPPid: 1000\nNSpid: 1001 8\nVmRSS: 10 kB\n'): 228 with self.assertRaisesRegex(RuntimeError, 'cannot monitor'): 229 monitor.sample() 230 231 def test_exiting_leader_uses_shared_thread_memory(self): 232 from pathlib import Path 233 monitor = self.monitor((1001,)) 234 def read(path): 235 if str(path) == '/proc/1001/status': 236 return 'Pid: 1001\nPPid: 1000\nNSpid: 1001 7\n' 237 return 'Tgid: 1001\nVmRSS: 321 kB\n' 238 with patch('pathlib.Path.read_text', read), patch('pathlib.Path.glob', return_value=[Path('/proc/1001/task/1002/status')]): 239 self.assertEqual(monitor.sample(), 321) 240 241 def test_identified_running_exit_with_no_address_space(self): 242 monitor = self.monitor((1001,)) 243 def read(path): 244 if str(path).endswith('/statm'): 245 return '0 0 0 0 0 0 0\n' 246 return 'Pid: 1001\nPPid: 1000\nNSpid: 1001 7\nState: R (running)\n' 247 with patch('pathlib.Path.read_text', read), patch('pathlib.Path.glob', return_value=[]): 248 self.assertEqual(monitor.sample(), 0) 249 250 def test_identified_zombie_can_finish_kernel_cleanup(self): 251 monitor = self.monitor((1001,)) 252 with patch('pathlib.Path.read_text', return_value='Pid: 1001\nPPid: 1000\nNSpid: 1001 7\nState: Z (zombie)\n'), patch('pathlib.Path.glob', return_value=[]): 253 self.assertIsNone(monitor.sample()) 254 255 def test_exited_child_has_no_sample(self): 256 monitor = self.monitor(); monitor.proc.poll = lambda: 0 257 self.assertIsNone(monitor.sample()) 258 259 def test_address_space_disappears_just_before_reap(self): 260 monitor = self.monitor() 261 monitor.proc.wait = lambda timeout: 0 262 with patch('pathlib.Path.read_text', side_effect=FileNotFoundError): 263 self.assertIsNone(monitor.sample()) 264 265 def test_real_child_is_visible(self): 266 proc = subprocess.Popen(['sleep', '2']) 267 try: 268 monitor = ChildRSS(proc) 269 sample = monitor.sample() 270 self.assertIsNotNone(sample) 271 self.assertGreater(sample, 0) 272 self.assertTrue(monitor.matches(status_fields(__import__('pathlib').Path(f'/proc/{monitor.pid}/status').read_text()))) 273 finally: 274 proc.terminate(); proc.wait() 275 276 277 # research_session_check 278 import hashlib 279 280 from pathlib import Path 281 282 import subprocess 283 284 import tempfile 285 286 import time 287 288 import unittest 289 290 from research_session import ResearchSession, ROOT 291 292 class ResearchTests(unittest.TestCase): 293 def test_prepared_fixture_selections_never_invoke_compiler(self): 294 with tempfile.TemporaryDirectory() as directory: 295 source = Path(directory) / 'fixture.tri' 296 source.write_text('!export *\none = t\ntwo = t t\n') 297 session = ResearchSession.__new__(ResearchSession) 298 session.records = {source: {'after_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), 299 'manifest': 'manifest'}} 300 session.tricu, session.store = 'tricu', Path(directory) 301 session.object = lambda domain, pin: b'header\nexport\tone\tkind\troot1\tabi\nexport\ttwo\tkind\troot2\tabi\n' 302 calls = [] 303 session.run = lambda command, seconds: (calls.append(command) or b'', .01) 304 for name in ['one', 'two', 'one']: 305 session.compile(source, [name], Path(directory) / (name + '.arboricx')) 306 self.assertEqual(len(calls), 3) 307 self.assertTrue(all(command[1:3] == ['arboricx', 'export'] for command in calls)) 308 309 def test_changed_prepared_source_is_rejected(self): 310 with tempfile.TemporaryDirectory() as directory: 311 source = Path(directory) / 'fixture.tri'; source.write_text('changed') 312 session = ResearchSession.__new__(ResearchSession) 313 session.records = {source: {'after_sha256': '0' * 64}} 314 with self.assertRaisesRegex(ValueError, 'changed during session'): 315 session.compile(source, ['main'], Path(directory) / 'out') 316 317 def test_job_above_twenty_minutes_is_rejected(self): 318 session = ResearchSession.__new__(ResearchSession) 319 with self.assertRaisesRegex(ValueError, '1200'): 320 session.run(['never-started'], 1201) 321 322 def test_expired_whole_harness_cannot_start_another_child(self): 323 session = ResearchSession.__new__(ResearchSession) 324 session.deadline = time.monotonic() - 1 325 with self.assertRaises(subprocess.TimeoutExpired): 326 session.run(['never-started'], 1200) 327 328 def test_ir_harness_uses_original_authenticated_fixture_path(self): 329 text = (ROOT / 'test/staging_experiment.py').read_text() 330 self.assertIn("source = ROOT / 'test/stagingExperiment.tri'", text) 331 self.assertNotIn("source = root / 'fixtures.tri'", text) 332 self.assertNotIn('zig', text.lower()) 333 334 def test_conditional_fixtures_are_batched(self): 335 text = (ROOT / 'test/staged_conditional.py').read_text() 336 body = text.split(' def fixture_for(', 1)[1].split(' for entry in ', 1)[0] 337 self.assertNotIn('compile(', body) 338 self.assertIn('arboricx', body) 339 340 341 # standard_native_migration_check 342 from pathlib import Path 343 344 import unittest 345 346 ROOT = Path(__file__).resolve().parent.parent 347 348 DEFERRED_LIBRARIES = {} 349 350 LEGACY_DEMO_FIXTURES = set() 351 352 def legacy(path): 353 return any(line.startswith('!import "') for line in path.read_text().splitlines()) 354 355 class StandardMigrationTests(unittest.TestCase): 356 def test_remaining_library_imports_are_explicitly_scoped(self): 357 remaining = {str(path.relative_to(ROOT)) for path in (ROOT / 'lib').rglob('*.tri') 358 if legacy(path)} 359 self.assertEqual(remaining, set(DEFERRED_LIBRARIES)) 360 361 def test_application_demos_are_native(self): 362 demos = [*(ROOT / 'demos').glob('*.tri'), *(ROOT / 'demos/interactionTrees').glob('*.tri')] 363 self.assertEqual(len(demos), 23) 364 for path in demos: 365 with self.subTest(path=path): 366 self.assertFalse(legacy(path)) 367 self.assertTrue(any(line.startswith('!export ') for line in path.read_text().splitlines())) 368 369 def test_demo_inputs_have_no_legacy_imports(self): 370 remaining = {str(path.relative_to(ROOT)) for path in (ROOT / 'demos').rglob('*.tri') 371 if legacy(path)} 372 self.assertEqual(remaining, LEGACY_DEMO_FIXTURES) 373 374 def test_formatter_and_recursion_are_native(self): 375 for directory in ['lib/tools', 'lib/recursion']: 376 for path in (ROOT / directory).rglob('*.tri'): 377 with self.subTest(path=path): 378 self.assertFalse(legacy(path)) 379 self.assertTrue(any(line.startswith('!export ') for line in path.read_text().splitlines())) 380 381 382 class NativeHeaderTests(unittest.TestCase): 383 def test_order_and_deduplication(self): 384 from native_header import native_header 385 edges, imports = native_header("-- header\n!import math\n!export * from base\n!import math as M\n!source build_only\n!export main\nmain = t\n") 386 self.assertEqual(edges, ['math', 'base', 'build_only']) 387 self.assertEqual(imports, [('math', 'math'), ('math', 'M')]) 388 389 def test_hash_imports_and_barrels(self): 390 from native_header import native_header 391 pin = 'a' * 64 392 self.assertEqual(native_header('!export * from dep\n')[0], ['dep']) 393 self.assertEqual(native_header('!import '+pin+' as M\n!export * from '+pin+'\n')[0], []) 394 with self.assertRaises(ValueError): native_header('!import '+pin+'\n') 395 396 def test_redundant_build_only_edges(self): 397 from native_header import native_header 398 for source in ['!source dep\n!import dep\n', '!import dep\n!source dep\n', 399 '!source dep\n!export * from dep\n', '!source dep\n!source dep\n']: 400 with self.subTest(source=source), self.assertRaisesRegex(ValueError, 'redundant'): 401 native_header(source) 402 403 def test_old_native_syntax_and_recovery_spelling_rejected(self): 404 from native_header import native_header 405 for source in ['!use dep *\n', '!import "dep" !Local\n']: 406 with self.subTest(source=source), self.assertRaises(ValueError): native_header(source) 407 408 409 class GuardedJobTests(unittest.TestCase): 410 def test_output_and_nonzero_exit(self): 411 import subprocess 412 import sys 413 from process_rss import run_guarded 414 output, peak = run_guarded([sys.executable, '-c', 'print("transport-ok")'], 415 memory_mib=64, seconds=5) 416 self.assertEqual(output.strip(), 'transport-ok') 417 self.assertGreaterEqual(peak, 0) 418 with self.assertRaises(subprocess.CalledProcessError) as failure: 419 run_guarded([sys.executable, '-c', 'print("diagnostic"); raise SystemExit(3)'], 420 memory_mib=64, seconds=5) 421 self.assertIn('diagnostic', failure.exception.output) 422 423 def test_time_and_memory_guards(self): 424 import sys 425 from process_rss import run_guarded 426 with self.assertRaisesRegex(RuntimeError, 'wall-time guard'): 427 run_guarded([sys.executable, '-c', 'import time; time.sleep(10)'], 428 memory_mib=64, seconds=0.05) 429 with self.assertRaisesRegex(RuntimeError, 'RSS guard'): 430 run_guarded([sys.executable, '-c', 'import time; data=bytearray(8*1024*1024); time.sleep(10)'], 431 memory_mib=1, seconds=5) 432 433 434 class BootstrapAcceptanceTests(unittest.TestCase): 435 def setUp(self): 436 from bootstrap_generations import source_map, snapshot 437 from bootstrap_acceptance import ARTIFACTS, sha 438 self.temporary = tempfile.TemporaryDirectory() 439 self.addCleanup(self.temporary.cleanup) 440 self.root = Path(self.temporary.name) 441 self.source = self.root / 'source' 442 self.source.mkdir() 443 (self.source / 'root.tri').write_text('!export main\nmain = t\n') 444 self.runtime = self.root / 'host' 445 self.runtime.write_bytes(b'host-identity') 446 self.first, self.second = self.root / 'first', self.root / 'second' 447 for index, run in enumerate((self.first, self.second)): 448 (run / 'cas/cache/source-build').mkdir(parents=True) 449 (run / 'source-map.v1').write_text('arboricx.source-map.v1\nnative\tsource_map\troot\troot.tri\t-\n') 450 (run / 'cas/cache/source-build/source_map').write_text('version\tkey\tidentical-manifest\treceipt') 451 (run / 'compiler.arboricx').write_bytes(b'bridge' if index == 0 else b'rebuilt-compiler.arboricx') 452 (run / 'io-adapter.arboricx').write_bytes(b'adapter') 453 for name in ARTIFACTS: 454 (run / name).write_bytes(name.encode()) 455 text, entries, _ = source_map(run / 'source-map.v1', self.source, 'source_map') 456 inputs = dict(source_root=str(self.source), root='source_map', backend='stable', fresh_store=True, 457 runtime_sha256=sha(self.runtime), adapter_sha256=sha(run / 'io-adapter.arboricx'), 458 compiler_bundle_sha256=sha(run / 'compiler.arboricx'), **snapshot(text, entries, self.source)) 459 (run / 'inputs.json').write_text(json.dumps(inputs)) 460 (run / 'status.json').write_text(json.dumps(dict(complete=True, **inputs, 461 artifacts={name: dict(sha256=sha(run / name)) for name in ARTIFACTS}))) 462 463 def verify(self): 464 from bootstrap_acceptance import verify 465 from unittest.mock import patch 466 # Synthetic outputs test gate policy only; real CAS auditing is separate. 467 with patch('bootstrap_acceptance.run_guarded', return_value=('synthetic audit', 0)): 468 return verify(self.first, self.second, self.runtime) 469 470 def test_accepts_matching_policy_records(self): 471 self.assertTrue(self.verify()['accepted']) 472 473 def test_rejects_wrong_compiler_and_artifact(self): 474 from bootstrap_acceptance import sha 475 path = self.second / 'compiler.arboricx' 476 path.write_bytes(b'unrelated-worker') 477 for filename in ('inputs.json', 'status.json'): 478 record_path = self.second / filename 479 record = json.loads(record_path.read_text()) 480 record['compiler_bundle_sha256'] = sha(path) 481 record_path.write_text(json.dumps(record)) 482 with self.assertRaisesRegex(ValueError, 'did not execute'): 483 self.verify() 484 path.write_bytes(b'rebuilt-compiler.arboricx') 485 (self.first / 'build-stable.arboricx').write_bytes(b'corrupt') 486 with self.assertRaisesRegex(ValueError, 'artifact identity'): 487 self.verify() 488 489 def test_rejects_seeded_or_failed_runs(self): 490 for field, value, diagnostic in [('fresh_store', False, 'fresh-store'), 491 ('complete', False, 'completed stable')]: 492 path = self.second / 'status.json' 493 original = path.read_bytes() 494 data = json.loads(original) 495 data[field] = value 496 path.write_text(json.dumps(data)) 497 with self.assertRaisesRegex(ValueError, diagnostic): 498 self.verify() 499 path.write_bytes(original) 500 501 def test_rejects_changed_source_and_actual_manifests(self): 502 path = self.source / 'root.tri' 503 original = path.read_bytes() 504 path.write_bytes(original + b'-- changed\n') 505 with self.assertRaisesRegex(ValueError, 'fingerprint'): 506 self.verify() 507 path.write_bytes(original) 508 (self.second / 'cas/cache/source-build/source_map').write_text('version\tkey\tdifferent-manifest\treceipt') 509 with self.assertRaisesRegex(ValueError, 'manifest reproduction'): 510 self.verify() 511 512 513 class SourceMappedKitTests(unittest.TestCase): 514 def test_projection_keeps_authored_sources_and_explicit_framing(self): 515 from prepare_compiler_kit import project 516 _, sources, worker, adapter = project(ROOT/'bootstrap/compiler-source-map.v1', ROOT, 2, 'stable') 517 self.assertEqual(sources['compiler_io'], (ROOT/'lib/source/io.tri').read_bytes()) 518 self.assertIn('sourceMapBuildStable = M.sourceMapBuildStable', worker) 519 self.assertIn('!import io as IO', adapter) 520 self.assertEqual(sources['source_object'], (ROOT/'lib/source/object.tri').read_bytes()) 521 with self.assertRaisesRegex(ValueError, 'requires IO protocol 2'): 522 project(ROOT/'bootstrap/compiler-source-map.v1', ROOT, 1, 'stable') 523 self.assertEqual(sources['io'], (ROOT/'lib/io.tri').read_bytes()) 524 self.assertIn('!import compiler_io *', sources['source_object'].decode()) 525 _, _, worker, adapter = project(ROOT/'bootstrap/compiler-source-map.v1', ROOT, 2, 'optimized') 526 self.assertIn('sourceMapBuild = M.sourceMapBuild', worker) 527 self.assertIn('!import io as IO', adapter) 528 529 530 class ArchiveRecoveryPolicyTests(unittest.TestCase): 531 def test_recovery_binds_produced_version_and_adapter(self): 532 from bootstrap_archive import recovery_profile 533 import hashlib 534 with tempfile.TemporaryDirectory() as directory: 535 root = Path(directory); adapter = root/'adapter' 536 adapter.write_bytes(b'v2') 537 pin = hashlib.sha256(b'v2').hexdigest() 538 (root/'produced-io-adapter.arboricx').write_bytes(b'v2') 539 inputs = dict(execution_io_version=1, produced_io_version=2, 540 produced_adapter_sha256=pin, adapter_sha256='0'*64) 541 self.assertEqual(recovery_profile(root, inputs, adapter, True), 2) 542 with self.assertRaisesRegex(ValueError, 'accepted pair'): 543 recovery_profile(root, inputs, adapter, False) 544 adapter.write_bytes(b'wrong') 545 with self.assertRaisesRegex(ValueError, 'produced adapter'): 546 recovery_profile(root, inputs, adapter, True) 547 adapter.write_bytes(b'v2') 548 (root/'produced-io-adapter.arboricx').write_bytes(b'corrupt') 549 with self.assertRaisesRegex(ValueError, 'identity mismatch'): 550 recovery_profile(root, inputs, adapter, True) 551 del inputs['execution_io_version'] 552 with self.assertRaisesRegex(ValueError, 'incomplete'): 553 recovery_profile(root, inputs, adapter, True) 554 self.assertEqual(recovery_profile(root, {}, adapter, False), 1) 555 with self.assertRaisesRegex(ValueError, 'accepted output'): 556 recovery_profile(root, {'adapter_sha256':'0'*64}, adapter, True) 557 558 def test_scp_execution_is_not_a_compiler_restore_gate(self): 559 archive = (ROOT / 'test/bootstrap_archive.py').read_text() 560 self.assertNotIn('scp_native_validation.py', archive) 561 self.assertIn('scp_execution_required=False', archive) 562 563 564 class SupportProjectionTests(unittest.TestCase): 565 def setUp(self): 566 from bundle_dag import merge_bundles, read 567 self.temporary = tempfile.TemporaryDirectory() 568 self.addCleanup(self.temporary.cleanup) 569 self.directory = Path(self.temporary.name) 570 self.original = self.directory / 'original.arboricx' 571 self.projected = self.directory / 'projected.arboricx' 572 leaf = ROOT / 'test/fixtures/false.arboricx' 573 other = ROOT / 'test/fixtures/true.arboricx' 574 merge_bundles(self.original, [(leaf, [(next(iter(read(leaf)[3])), 'keep.value')]), 575 (other, [(next(iter(read(other)[3])), 'drop.value')])]) 576 merge_bundles(self.projected, [(self.original, [('keep.value', 'keep.value')])], 577 prune_unreachable=True) 578 579 def test_exact_selected_exports_without_unreachable_nodes(self): 580 from project_support_fixture import verify_projection 581 self.assertEqual(verify_projection(self.original, self.projected, {'keep'}), 1) 582 583 def test_missing_export_and_changed_kernel_rejected(self): 584 from bundle_dag import merge_bundles 585 from project_support_fixture import verify_projection 586 with self.assertRaisesRegex(ValueError, 'export set'): 587 verify_projection(self.original, self.projected, {'keep', 'drop'}) 588 merge_bundles(self.projected, [(self.original, [('drop.value', 'keep.value')])], 589 prune_unreachable=True) 590 with self.assertRaisesRegex(ValueError, 'changed a kernel'): 591 verify_projection(self.original, self.projected, {'keep'}) 592 593 def test_unreachable_archived_nodes_rejected(self): 594 from bundle_dag import merge_bundles 595 from project_support_fixture import verify_projection 596 merge_bundles(self.projected, [(self.original, [('keep.value', 'keep.value')])]) 597 with self.assertRaisesRegex(ValueError, 'unreachable'): 598 verify_projection(self.original, self.projected, {'keep'}) 599 600 def test_immutable_projection_origin_fingerprints(self): 601 # The extraction ledger remains historical: current/rebuilt outputs have 602 # since been genuinely rebuilt. Never rewrite the original snapshots or 603 # pretend new compilation is an unchanged projection. Current outputs 604 # and every rebuilt receipt are authenticated by the Haskell cohort gate. 605 fixtures = ROOT / 'test/fixtures' 606 evidence = json.loads((fixtures / 'support-projection.json').read_text()) 607 self.assertEqual(evidence['format'], 'tricu.support-projection.v1') 608 for name in ['support-origin.v1', 'support-origin.arboricx', 'support-origin-compiler.arboricx']: 609 self.assertEqual(hashlib.sha256((fixtures / name).read_bytes()).hexdigest(), 610 evidence['outputs_sha256'][name], name) 611 612 def test_active_support_is_dependency_closed_without_archive(self): 613 from native_header import native_header 614 fixtures = ROOT / 'test/fixtures' 615 rows = [line.split('\t') for name in ['compiler-library.v1', 'support-library.v1'] 616 for line in (fixtures / name).read_text().splitlines() if line.startswith('module\t')] 617 providers = {row[2] for row in rows} 618 for _, _, alias, path, pin in rows: 619 self.assertNotIn('consumer', path) 620 self.assertNotIn('archive/', path) 621 from fixture_sources import snapshot_bytes 622 dependencies, _ = native_header(snapshot_bytes(fixtures, pin).decode()) 623 self.assertLessEqual(set(dependencies), providers, alias) 624 self.assertNotIn('ScpCommand', (ROOT / 'tricu.cabal').read_text()) 625 self.assertFalse((ROOT / 'src/ScpCommand.hs').exists()) 626 627 628 class LibraryPublicationAliases(unittest.TestCase): 629 def test_authored_aliases_are_not_filename_guesses(self): 630 import importlib.util 631 spec = importlib.util.spec_from_file_location('prepare_libraries', ROOT/'ext/demo-site/prepare-libraries.py') 632 module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) 633 rows = module.library_sources("tricu.workspace.v1\nhandler_upload\tlib/handlers/upload.tri\nhandler_testing\tlib/handlers/testing.tri\n") 634 self.assertEqual(next(alias for alias,path,_ in rows if path=='lib/handlers/upload.tri'), 'handler_upload') 635 self.assertEqual(next(alias for alias,path,_ in rows if path=='lib/handlers/testing.tri'), 'handler_testing') 636 self.assertEqual({path for _,path,_ in rows}, {str(path.relative_to(ROOT)) for path in (ROOT/'lib').rglob('*.tri')}) 637 for text in ['bad', 'tricu.workspace.v1\na\tx\na\ty', 'tricu.workspace.v1\na\tx\nb\tx', 'tricu.workspace.v1\na\t../x']: 638 with self.assertRaises(ValueError): module.workspace_aliases(text) 639 640 641 class SupportPublicationControls(unittest.TestCase): 642 def test_resource_limits_fail_before_build_or_publication(self): 643 import contextlib 644 import io 645 from native_library_migration import main as compile_candidates 646 from repair_support_fixture import main as publish_candidates 647 cases = [ 648 (compile_candidates, ['tool', 'host', 'generation', 'output', '--library', 'socket', '--capabilities', '0']), 649 (publish_candidates, ['tool', 'host', 'candidates', '--output-directory', 'output', '--capabilities', '0']), 650 (publish_candidates, ['tool', 'host', 'candidates', '--output-directory', 'output', '--verification-memory-mib', '65537']), 651 ] 652 for main, argv in cases: 653 with self.subTest(argv=argv), patch('sys.argv', argv), contextlib.redirect_stderr(io.StringIO()): 654 with self.assertRaises(SystemExit) as error: 655 main() 656 self.assertEqual(error.exception.code, 2) 657 658 def test_publication_cannot_overwrite_an_existing_directory(self): 659 from repair_support_fixture import main 660 with tempfile.TemporaryDirectory() as directory: 661 marker = Path(directory) / 'marker' 662 marker.write_bytes(b'keep') 663 with patch('sys.argv', ['tool', 'host', 'candidates', '--output-directory', directory]): 664 with self.assertRaises(FileExistsError): 665 main() 666 self.assertEqual(marker.read_bytes(), b'keep') 667 self.assertEqual(list(Path(directory).iterdir()), [marker]) 668 669 670 class CompilerDependencyTests(unittest.TestCase): 671 def test_current_map_is_exact_compiler_closure_without_patterns(self): 672 from bootstrap_generations import source_map 673 _, entries, required = source_map( 674 ROOT / 'bootstrap/compiler-source-map.v1', ROOT, 'source_map') 675 self.assertEqual(entries['compiler_io'][2], ['base', 'io', 'io_storage', 'io_text']) 676 self.assertEqual(entries['io'][1], 'lib/io.tri') 677 self.assertNotIn('patterns', entries) 678 self.assertEqual(set(entries), required) 679 680 681 class FixtureSourceTests(unittest.TestCase): 682 def setUp(self): 683 from fixture_sources import digest 684 self.temporary = tempfile.TemporaryDirectory() 685 self.addCleanup(self.temporary.cleanup) 686 self.root = Path(self.temporary.name) 687 self.fixtures, self.source = self.root / 'fixtures', self.root / 'source' 688 self.fixtures.mkdir() 689 self.source.mkdir() 690 for cohort, alias in [('compiler-library', 'compiler'), ('support-library', 'support')]: 691 source = b'!export main\nmain = t\n' + ('-- ' + alias + '\n').encode() 692 (self.source / (alias + '.tri')).write_bytes(source) 693 bundle = b'synthetic-policy-bundle-' + alias.encode() 694 (self.fixtures / (cohort + '.arboricx')).write_bytes(bundle) 695 (self.fixtures / (cohort + '.v1')).write_text( 696 'tricu.' + cohort + '-fixture.v1\nmodule\t' + alias + '\t' + alias + '\t' + alias + '.tri\t' 697 + digest('arboricx.source.v1', source) + '\nbundle\t' 698 + digest('tricu.' + cohort + '-fixture.v1', bundle) + '\n') 699 (self.source / 'bootstrap').mkdir() 700 (self.source / 'bootstrap/compiler-source-map.v1').write_text( 701 'arboricx.source-map.v1\nnative\tcompiler\tcompiler\tcompiler.tri\t-\n') 702 703 def capture(self): 704 from fixture_sources import capture 705 return capture(self.fixtures, self.source) 706 707 def test_reference_and_freshness_are_distinct(self): 708 from fixture_sources import verify, current_mismatches 709 self.capture() 710 self.assertEqual(current_mismatches(self.fixtures, self.source), []) 711 (self.source / 'compiler.tri').write_text('!import changed *\n!export main\nmain = changed\n') 712 self.assertEqual(len(verify(self.fixtures)), 2) 713 self.assertEqual(current_mismatches(self.fixtures, self.source), ['compiler.tri']) 714 715 def test_materialize_authenticates_reference_without_blessing_worktree(self): 716 from fixture_sources import materialize, current_mismatches 717 self.capture() 718 (self.source / 'support.tri').write_bytes(b'changed work') 719 target = self.root / 'reference' 720 materialize(self.fixtures, target) 721 self.assertEqual(current_mismatches(self.fixtures, target), []) 722 self.assertEqual(current_mismatches(self.fixtures, self.source), ['support.tri']) 723 with self.assertRaises(FileExistsError): materialize(self.fixtures, target) 724 next((self.fixtures / 'reference-source').glob('*.tri')).write_bytes(b'corrupt') 725 with self.assertRaises(ValueError): materialize(self.fixtures, self.root / 'bad') 726 self.assertFalse((self.root / 'bad').exists()) 727 728 def test_capture_cannot_bless_edited_sources(self): 729 (self.source / 'support.tri').write_text('changed') 730 with self.assertRaisesRegex(ValueError, 'does not match'): 731 self.capture() 732 self.assertFalse((self.fixtures / 'reference-source').exists()) 733 734 def test_missing_snapshot_never_falls_back_to_matching_working_source(self): 735 from fixture_sources import verify 736 with self.assertRaises(FileNotFoundError): 737 verify(self.fixtures) 738 739 def test_corrupt_snapshot_is_rejected_not_repaired(self): 740 from fixture_sources import verify 741 self.capture() 742 next((self.fixtures / 'reference-source').glob('*.tri')).write_bytes(b'corrupt') 743 with self.assertRaisesRegex(ValueError, 'digest mismatch'): 744 verify(self.fixtures) 745 with self.assertRaisesRegex(ValueError, 'existing reference'): 746 self.capture() 747 748 def test_corrupt_bundle_is_rejected(self): 749 from fixture_sources import verify 750 self.capture() 751 (self.fixtures / 'compiler-library.arboricx').write_bytes(b'corrupt') 752 with self.assertRaisesRegex(ValueError, 'bundle digest'): 753 verify(self.fixtures) 754 755 def test_unsafe_or_duplicate_metadata_is_rejected(self): 756 from fixture_sources import source_rows 757 path = self.fixtures / 'compiler-library.v1' 758 original = path.read_text() 759 for value in ['../outside', '/outside']: 760 path.write_text(original.replace('compiler.tri', value)) 761 with self.assertRaisesRegex(ValueError, 'path or digest'): 762 source_rows(self.fixtures) 763 path.write_text(original + original.splitlines()[1] + '\n') 764 with self.assertRaisesRegex(ValueError, 'duplicate'): 765 source_rows(self.fixtures) 766 767 def test_cli_reports_reference_success_but_current_failure(self): 768 import sys 769 self.capture() 770 (self.source / 'compiler.tri').write_bytes(b'edited') 771 base = [sys.executable, str(ROOT / 'test/fixture_sources.py')] 772 common = ['--fixtures', str(self.fixtures), '--source-root', str(self.source)] 773 reference = subprocess.run(base + ['verify'] + common, capture_output=True, text=True) 774 current = subprocess.run(base + ['current'] + common, capture_output=True, text=True) 775 self.assertEqual(reference.returncode, 0, reference.stderr) 776 self.assertIn('freshness NOT checked', reference.stdout) 777 self.assertEqual(current.returncode, 1) 778 self.assertIn('compiler.tri', current.stderr) 779 780 def test_map_change_is_freshness_failure(self): 781 from fixture_sources import current_mismatches 782 self.capture() 783 (self.source / 'bootstrap/compiler-source-map.v1').write_text('arboricx.source-map.v1\n') 784 self.assertEqual(current_mismatches(self.fixtures, self.source), ['bootstrap/compiler-source-map.v1']) 785 786 787 class SupportPublicationTests(unittest.TestCase): 788 def test_patterns_relocation_is_explicit_and_identity_checked(self): 789 from repair_support_fixture import relocate_patterns, updated_module_rows 790 old = ['module\thttp\thttp\tlib/http.tri\told-source'] 791 record = dict(legacy='patterns', path='lib/patterns.tri') 792 candidates = {'patterns': (record, b'!export main\nmain = t\n')} 793 with self.assertRaisesRegex(ValueError, 'membership'): 794 updated_module_rows(old, candidates) 795 relocated = relocate_patterns(old, candidates) 796 self.assertEqual(updated_module_rows(relocated, candidates), relocated) 797 self.assertEqual(old, ['module\thttp\thttp\tlib/http.tri\told-source']) 798 with self.assertRaisesRegex(ValueError, 'requires rebuilt'): 799 relocate_patterns(old, {}) 800 with self.assertRaisesRegex(ValueError, 'identity'): 801 relocate_patterns(old, {'patterns': (dict(record, path='other.tri'), b't')}) 802 with self.assertRaisesRegex(ValueError, 'already belongs'): 803 relocate_patterns(relocated, candidates) 804 805 def test_changed_source_has_a_new_authenticated_fingerprint(self): 806 from repair_support_fixture import updated_module_rows 807 old = ['module\thttp\thttp\tlib/http.tri\told-source', 808 'module\tsocket\tsocket\tlib/socket.tri\toriginal-socket'] 809 record = dict(legacy='http', path='lib/http.tri') 810 payload = b'!export f\nf x = x\n' 811 rows = updated_module_rows(old, {'http': (record, payload)}) 812 self.assertEqual(rows[0].split('\t')[-1], digest('arboricx.source.v1', payload)) 813 self.assertEqual(rows[1], old[1]) 814 self.assertEqual(old[0].split('\t')[-1], 'old-source') 815 with self.assertRaisesRegex(ValueError, 'identity'): 816 updated_module_rows(old, {'http': (dict(record, path='other.tri'), payload)}) 817 with self.assertRaisesRegex(ValueError, 'membership'): 818 updated_module_rows(old, {'unknown': (record, payload)}) 819 820 def test_candidate_replacement_is_explicit_and_complete(self): 821 from repair_support_fixture import select_candidates 822 with tempfile.TemporaryDirectory() as directory: 823 roots = [Path(directory) / str(n) for n in range(2)] 824 for root in roots: 825 root.mkdir() 826 (root / 'migration.json').write_text(json.dumps([dict(alias='http', complete=True)])) 827 with self.assertRaisesRegex(ValueError, 'requires --replace'): 828 select_candidates(roots, []) 829 self.assertEqual(select_candidates(roots, ['http'])['http'][0], roots[1]) 830 with self.assertRaisesRegex(ValueError, 'exactly'): 831 select_candidates(roots[:1], ['http']) 832 (roots[1] / 'migration.json').write_text(json.dumps([dict(alias='http', complete=False)])) 833 with self.assertRaisesRegex(ValueError, 'incomplete'): 834 select_candidates(roots, ['http']) 835 836 def test_receipt_cannot_relabel_source_compiler_or_output(self): 837 from repair_support_fixture import checked_receipt 838 source = b'f x = x\n' 839 payload = ('arboricx.build-receipt.v1\nsource\t' + digest('arboricx.source.v1', source) + 840 '\ncompiler\tcompiler\noutput\tmanifest\noptions\t' + 841 digest('arboricx.build-options.v1', b'source-module.stable.v1') + '\n').encode() 842 checked_receipt(payload, source, 'manifest', 'compiler') 843 for raw, text, manifest, compiler in [ 844 (payload, source + b'-- changed\n', 'manifest', 'compiler'), 845 (payload, source, 'wrong', 'compiler'), 846 (payload, source, 'manifest', 'wrong'), 847 (payload + b'output\tmanifest\n', source, 'manifest', 'compiler'), 848 (payload.replace(digest('arboricx.build-options.v1', b'source-module.stable.v1').encode(), 849 digest('arboricx.build-options.v1', b'source-module.reference.v1').encode()), 850 source, 'manifest', 'compiler'), 851 ]: 852 with self.subTest(manifest=manifest, compiler=compiler), self.assertRaisesRegex(ValueError, 'receipt mismatch'): 853 checked_receipt(raw, text, manifest, compiler) 854 855 def test_old_consumer_receipt_cannot_pin_an_old_support_provider(self): 856 from repair_support_fixture import check_support_dependencies 857 providers = dict(http='current-http', socket='current-socket') 858 rows = [['local', 'http', 'current-http', 'facade-source'], 859 ['pin', 'http', 'current-http', 'parseHeaders', 'tree']] 860 check_support_dependencies(rows, providers) 861 for bad in ([['local', 'http', 'old-http', 'source']], 862 [rows[0], ['pin', 'http', 'old-http', 'parseHeaders', 'tree']], 863 [rows[0], rows[0]], [['local', 'http']], [['pin', 'http']]): 864 with self.subTest(bad=bad), self.assertRaises(ValueError): 865 check_support_dependencies(bad, providers) 866 867 868 class LocalControlHarnessTests(unittest.TestCase): 869 def test_independent_case_diagnostic_and_ast_corpus(self): 870 from local_control import cases, failures, ast_cases 871 rows = cases()+failures()+ast_cases() 872 self.assertEqual(len(rows),len({row[0] for row in rows})) 873 labels = {row[0] for row in rows} 874 for label in ('case-ignored-scrutinee', 'case-unused-loop', 'case-pin-collision', 875 'rec-nested-capture', 'rec-no-mutual', 'plain-let-shadow', 876 'rec-value-before-missing', 'case-ast', 'rec-ast'): 877 self.assertIn(label,labels) 878 from http_headers import expect 879 for _,_,expected in ast_cases(): 880 with self.assertRaises(ValueError): 881 expect(expected+b'\0', expected, 'extra AST field') 882 883 def test_explicit_source_dependencies_cannot_override_or_escape(self): 884 from native_library_migration import dependency_sources, check_source_dependencies 885 import tempfile, json 886 from pathlib import Path 887 with tempfile.TemporaryDirectory() as directory: 888 root = Path(directory); (root/'extra.tri').write_text('!export main\nmain = 42\n') 889 rows = dependency_sources(root, ['extra=extra.tri'], {'base'}) 890 workspace = root/'workspace'; workspace.mkdir() 891 (workspace/'extra.tri').write_bytes((root/'extra.tri').read_bytes()) 892 (root/'selection.json').write_text(json.dumps(dict(source_dependencies=rows))) 893 check_source_dependencies(root, root) 894 for entries in [['base=extra.tri'], ['extra=extra.tri']*2, ['extra=../escape'], ['extra=/absolute'], ['!bad=extra.tri'], ['extra']]: 895 with self.assertRaises(ValueError): dependency_sources(root, entries, {'base'}) 896 (root/'outside.tri').symlink_to('/etc/passwd') 897 with self.assertRaises(ValueError): dependency_sources(root, ['extra=outside.tri'], set()) 898 (root/'extra.tri').write_text('!export main\nmain = 43\n') 899 with self.assertRaises(ValueError): check_source_dependencies(root, root) 900 901 def test_candidate_source_guard_rejects_old_facade_and_source_edits(self): 902 from native_library_migration import checked_candidate_source 903 import tempfile, hashlib 904 from pathlib import Path 905 with tempfile.TemporaryDirectory() as directory: 906 root = Path(directory); workspace = root/'workspace'; workspace.mkdir() 907 source = b'!export value\nvalue = 23\n' 908 (root/'library.tri').write_bytes(source) 909 staged = workspace/'source_ast.tri'; staged.write_bytes(source) 910 record = dict(path='library.tri', alias='source_ast', after_sha256=hashlib.sha256(source).hexdigest()) 911 self.assertEqual(checked_candidate_source(workspace, record, root), source) 912 staged.write_bytes(b'!export * from '+b'0'*64+b'\n') 913 with self.assertRaises(ValueError): checked_candidate_source(workspace, record, root) 914 staged.write_bytes(source); (root/'library.tri').write_bytes(source+b'-- changed\n') 915 with self.assertRaises(ValueError): checked_candidate_source(workspace, record, root) 916 917 918 class RegistryServerHarnessTests(unittest.TestCase): 919 def test_independent_oracles_and_explicit_changes(self): 920 from registry_server import pure_cases, effect_cases, demo_cases 921 rows = pure_cases() + effect_cases() + list(demo_cases()) 922 self.assertEqual(len(rows), len({row.label for row in rows})) 923 self.assertEqual(sum(c.label.startswith('hex-byte-') for c in rows), 256) 924 self.assertEqual(sum(c.label.startswith('hash-invalid-at-') for c in rows), 64) 925 self.assertEqual(sum(c.label.startswith('demo-init-failure-') for c in rows), 7) 926 for c in rows: 927 self.assertIsInstance(c.expected, bytes) 928 self.assertEqual(bool(c.change), c.previous is not None) 929 if c.previous is not None: 930 self.assertNotEqual(c.expected, c.previous) 931 932 def test_traversal_never_requests_filesystem_io(self): 933 import posixpath 934 from registry_server import effect_cases, finished, result, number, bs, DIAGNOSTIC, action, ROOT, path 935 rows = effect_cases() 936 for c in rows: 937 if c.label.startswith('get-reject-'): 938 self.assertEqual(c.expected, finished(result(False, number(400), bs(DIAGNOSTIC)))) 939 if c.label.startswith('write-reject-'): 940 self.assertEqual(c.expected, finished(result(False, bs(b'sha256Hex: '+DIAGNOSTIC)), 941 [action(28, bs(b'body'))])) 942 self.assertEqual(c.previous_export, 'putBundleWithHash') 943 self.assertFalse(posixpath.normpath(path(b'../../../escaped')).startswith(ROOT+b'/')) 944 945 def test_prior_defects_cannot_pass_current_oracles(self): 946 from registry_server import effect_cases, demo_cases 947 from http_pipeline import check_case 948 for c in effect_cases()+list(demo_cases()): 949 if c.previous is not None: 950 check_case(c, c.expected, c.previous) 951 with self.assertRaises(ValueError): 952 check_case(c, c.previous, c.previous) 953 954 def test_wrong_path_and_atomic_operation_are_detected(self): 955 from registry_server import action, bs, pair, finished, result, expect 956 output = result(True, bs(b'hash')) 957 write = action(33, pair(bs(b'/registry/objects/abc/hash.arboricx'), bs(b'data'))) 958 expected = finished(output, [write]) 959 for trace in ([], [action(22, pair(bs(b'/registry/tmp/hash.tmp'), bs(b'data')))], 960 [action(33, pair(bs(b'/outside'), bs(b'data')))], [write, write]): 961 with self.assertRaises(ValueError): 962 expect(finished(output, trace), expected, 'mutated publication') 963 964 965 class HttpPipelineHarnessTests(unittest.TestCase): 966 def test_changes_have_explicit_old_and_independent_new_results(self): 967 from http_pipeline import pure_cases, effect_cases 968 rows = pure_cases() + effect_cases() 969 self.assertEqual(len(rows), len({row.label for row in rows})) 970 changes = {row.label for row in rows if row.previous is not None} 971 self.assertEqual(changes, {'body-eof-', 'body-eof-6162', 'receive-recv-failure', 972 'handler-pure-too-large', 'handler-io-too-large', 'handler-pure-header-limit', 973 'handler-io-header-limit', 'handler-pure-body-eof', 'handler-io-body-eof', 'status-413'}) 974 for row in rows: 975 self.assertIsInstance(row.expected, bytes) 976 if row.previous is not None: 977 self.assertNotEqual(row.expected, row.previous) 978 979 def test_reference_modes_never_disable_subject_oracle(self): 980 from http_pipeline import Case, check_case 981 row = Case('changed', 'probe', (), b'\0', previous=b'\1\0') 982 check_case(row, row.expected, row.previous) 983 check_case(row, row.expected, row.expected, reference_current=True) 984 for actual, before, current in [(row.previous, row.previous, False), 985 (row.previous, row.expected, True), 986 (row.expected, row.previous, True), 987 (row.expected, row.expected, False)]: 988 with self.assertRaises(ValueError): 989 check_case(row, actual, before, current) 990 991 def test_both_router_paths_have_success_failure_and_cleanup_cases(self): 992 from http_pipeline import effect_cases 993 labels = {row.label for row in effect_cases()} 994 for flavor in ('pure', 'io'): 995 for suffix in ('get', 'post', 'head', 'bad-line', 'bad-header-head', 'bad-method', 996 'bad-version-head', 'bad-target-head', 'duplicate', 'coding-head', 997 'too-large', 'header-eof', 'header-limit', 'body-fragmented', 998 'body-recv-error', 'body-eof', 'body-send-error', 'body-close-error'): 999 self.assertIn('handler-' + flavor + '-' + suffix, labels) 1000 for control in ('pure', 'extra-script', 'unscripted', 'wrong-tag', 'abort', 'bind', 'fuel'): 1001 self.assertIn('driver-' + control, labels) 1002 1003 def test_trace_oracle_detects_missing_reordered_or_wrong_effect_payloads(self): 1004 from http_pipeline import action, finished, number, byte_tree, pair, result, LEAF 1005 from http_headers import expect 1006 send = action(77, pair(number(7), byte_tree(b'response'))) 1007 close = action(71, number(7)) 1008 expected = finished(result(True, LEAF), [send, close]) 1009 for trace in ([send], [close, send], [send, close, close], 1010 [action(77, pair(number(7), byte_tree(b'wrong'))), close]): 1011 with self.assertRaises(ValueError): 1012 expect(finished(result(True, LEAF), trace), expected, 'trace mutation') 1013 1014 1015 class HttpLengthHarnessTests(unittest.TestCase): 1016 def test_length_corpus_covers_bytes_and_policies_without_running_artifacts(self): 1017 from http_lengths import cases, BAD_REQUEST 1018 rows = cases(include_costs=False) 1019 self.assertEqual(len(rows), len({row.label for row in rows})) 1020 self.assertEqual(sum(row.label.startswith('value-byte-') for row in rows), 256) 1021 self.assertEqual(sum(row.label.startswith('duplicate-') for row in rows), 25) 1022 for row in rows: 1023 if row.label.startswith(('duplicate-', 'coding-')): 1024 self.assertEqual(row.expected, BAD_REQUEST) 1025 self.assertFalse(row.compare_reference) 1026 self.assertEqual(len(row.arguments), 3 if row.export == 'readBody' else 1) 1027 1028 def test_rejections_require_complete_pure_action_not_just_a_status(self): 1029 from http_lengths import cases, pair, number, BAD_REQUEST, TOO_LARGE 1030 from http_headers import expect 1031 rows = cases(include_costs=False) 1032 by_name = {row.label: row for row in rows} 1033 for row in rows: 1034 if row.label.startswith('no-io-'): 1035 original = by_name[row.label[len('no-io-'):]] 1036 self.assertIn(original.expected, (BAD_REQUEST, TOO_LARGE)) 1037 self.assertEqual(row.expected, pair(number(0), original.expected)) 1038 with self.assertRaises(ValueError): 1039 expect(pair(number(1), original.expected), row.expected, row.label) 1040 1041 def test_reference_comparisons_never_feed_large_valid_numbers_to_old_parser(self): 1042 from http_lengths import cases, length_result, BAD_REQUEST, pair, number, result, byte_tree, LEAF 1043 safe_results = {BAD_REQUEST, length_result(None), length_result(0), length_result(1), length_result(42), 1044 pair(number(0), result(True, LEAF, byte_tree(b'body')))} 1045 for row in cases(include_costs=False): 1046 if row.compare_reference: 1047 self.assertIn(row.expected, safe_results, row.label) 1048 1049 1050 class HttpHeaderHarnessTests(unittest.TestCase): 1051 """Cheap oracle/identity controls only; artifact execution stays opt-in.""" 1052 1053 def test_syntax_changes_have_explicit_reasons_and_independent_oracles(self): 1054 from http_headers import cases, syntax_cases 1055 old = {case.label: case for case in cases()} 1056 rows = syntax_cases() 1057 self.assertEqual(len(rows), len({row.label for row in rows})) 1058 self.assertEqual(sum(row.label.startswith('name-byte-') for row in rows), 256) 1059 self.assertEqual(sum(row.label.startswith('value-byte-') for row in rows), 256) 1060 self.assertTrue(any(row.export == 'parseRequestLine' for row in rows)) 1061 for row in rows: 1062 if row.change: 1063 self.assertIsNotNone(row.expected, row.label) 1064 if row.label in old: 1065 self.assertEqual(row.reference_expected, old[row.label].expected) 1066 elif row.label in old: 1067 self.assertEqual(row, old[row.label]) 1068 1069 def test_only_approved_nonbyte_cases_are_retired(self): 1070 from http_headers import cases, syntax_cases, retired_case 1071 historical = cases() 1072 current = {case.label for case in syntax_cases()} 1073 retired = {case.label for case in historical if retired_case(case)} 1074 self.assertEqual(len(retired), 103) 1075 self.assertEqual({case.label for case in historical} - current, retired) 1076 self.assertEqual(sum(case.label.startswith('ascii-') for case in syntax_cases()), 256) 1077 self.assertIn('headers-shape-5', current) 1078 self.assertIn('headers-shape-6', current) 1079 1080 def test_changed_expectations_do_not_disable_result_or_reference_checks(self): 1081 from dataclasses import replace 1082 from http_headers import syntax_cases, check_case, LEAF 1083 changed = next(row for row in syntax_cases() if row.label == 'bare-lf') 1084 check_case(changed, changed.reference_expected, changed.expected) 1085 for before, after in [(changed.reference_expected, changed.reference_expected), 1086 (LEAF, changed.expected)]: 1087 with self.assertRaisesRegex(ValueError, 'complete result differs'): 1088 check_case(changed, before, after) 1089 with self.assertRaisesRegex(ValueError, 'independent oracle'): 1090 check_case(replace(changed, expected=None), LEAF, LEAF) 1091 unchanged = next(row for row in syntax_cases() if row.label == 'headers-shape-5') 1092 with self.assertRaisesRegex(ValueError, 'differential'): 1093 check_case(unchanged, LEAF, b'\x01\x00') 1094 with self.assertRaisesRegex(ValueError, 'independent oracle'): 1095 check_case(unchanged, None, LEAF) 1096 1097 def test_independent_tree_encodings(self): 1098 from http_headers import number, pair, tree_list, byte_tree, parse_tree_output 1099 self.assertEqual(number(0), b'\x00') 1100 self.assertEqual(number(1), b'\x02\x01\x00\x00') 1101 self.assertEqual(number(2), b'\x02\x00\x02\x01\x00\x00') 1102 self.assertEqual(pair(b'\x00', b'\x01\x00'), b'\x02\x00\x01\x00') 1103 self.assertEqual(tree_list([number(65)]), byte_tree(b'A')) 1104 self.assertEqual(parse_tree_output(' 2010\n'), b'\x02\x00\x01\x00') 1105 1106 def test_renderer_rejects_partial_or_extra_output(self): 1107 from http_headers import parse_tree_output 1108 for text in ('', '1', '20', '00', '0 error', '3', '0\n0'): 1109 with self.subTest(text=text), self.assertRaises(ValueError): 1110 parse_tree_output(text) 1111 1112 def test_corpus_is_named_and_policy_characterization_is_explicit(self): 1113 from http_headers import cases, EXPORTS 1114 rows = cases() 1115 self.assertEqual(len(rows), len({row.label for row in rows})) 1116 self.assertEqual({row.export for row in rows}, set(EXPORTS)) 1117 self.assertEqual(sum(row.label.startswith('ascii-') for row in rows), 256) 1118 for row in rows: 1119 self.assertEqual(row.expected is None, row.category == 'differential-only') 1120 by_name = {row.label: row for row in rows} 1121 for label in ('embedded-cr', 'bare-lf', 'space-name', 'eof-header', 'duplicates'): 1122 # Duplicate preservation is ordinary parser behavior; deciding how 1123 # particular headers interact belongs to a later semantic slice. 1124 expected = 'independent' if label == 'duplicates' else 'characterization' 1125 self.assertEqual(by_name[label].category, expected) 1126 1127 def test_complete_result_comparison_rejects_wrong_fields(self): 1128 from http_headers import cases, expect, headers, reply, diagnostic 1129 by_name = {row.label: row for row in cases()} 1130 wrong = [ 1131 ('ascii-065', by_name['ascii-064'].expected), 1132 ('blank', headers([], b'wrong remainder')), 1133 ('reply-missing-colon', reply(False, diagnostic(1, b'colon'), consumed=False)), 1134 ('reply-missing-colon', reply(False, diagnostic(2, b'colon'))), 1135 ] 1136 for label, value in wrong: 1137 with self.subTest(label=label), self.assertRaisesRegex(ValueError, 'complete result differs'): 1138 expect(by_name[label].expected, value, label) 1139 1140 def test_reference_integrity_and_recorded_source(self): 1141 from http_headers import reference, ROOT 1142 bundle, metadata = reference(ROOT / 'test/fixtures') 1143 self.assertTrue(bundle.is_file()) 1144 self.assertEqual(metadata['source_path'], 'lib/http.tri') 1145 1146 def test_corrupt_reference_metadata_bundle_and_source_fail(self): 1147 import shutil 1148 from http_headers import reference, ROOT 1149 fixtures = ROOT / 'test/fixtures' 1150 metadata = json.loads((fixtures / 'http-headers-reference.json').read_text()) 1151 paths = ['http-headers-reference.json', 'http-headers-reference.arboricx', 1152 'reference-source/' + metadata['source_digest'] + '.tri'] 1153 for damaged in paths: 1154 with self.subTest(damaged=damaged), tempfile.TemporaryDirectory() as directory: 1155 destination = Path(directory) 1156 for path in paths: 1157 (destination / path).parent.mkdir(parents=True, exist_ok=True) 1158 shutil.copyfile(fixtures / path, destination / path) 1159 with (destination / damaged).open('ab') as output: 1160 output.write(b'corrupt') 1161 with self.assertRaises(ValueError): 1162 reference(destination) 1163 1164 1165 class ArboricxReaderOracleTests(unittest.TestCase): 1166 def test_cases_have_unique_names_and_independent_truncation_oracles(self): 1167 from arboricx_readers import cases 1168 rows = list(cases()) 1169 self.assertEqual(len(rows), 401) 1170 self.assertEqual(len({r[1] for r in rows}), len(rows)) 1171 for module in ['common', 'nodes', 'manifest']: 1172 self.assertTrue(any(r[0] == module and '-cut-' in r[1] and r[4] is not None for r in rows)) 1173 1174 def test_binary_failure_keeps_field_start_not_consumed_prefix(self): 1175 from arboricx_readers import Reader, Failure 1176 from http_headers import result, number 1177 from stable_bundle import byte_tree 1178 r = Reader(b'xx') 1179 r.take(1) 1180 with self.assertRaises(Failure) as caught: 1181 r.take(2) 1182 self.assertEqual(caught.exception.tree, result(False, number(1), byte_tree(b'x'))) 1183 1184 1185 class SocketConvenienceOracleTests(unittest.TestCase): 1186 def test_only_post_acquisition_failures_change(self): 1187 from socket_conveniences import cases 1188 rows = list(cases()) 1189 self.assertEqual(len(rows), 14) 1190 self.assertEqual(len({r.label for r in rows}), len(rows)) 1191 changed = [r for r in rows if r.previous is not None] 1192 self.assertEqual(len(changed), 6) 1193 self.assertTrue(all('failure-close-' in r.label and r.previous != r.expected for r in changed)) 1194 1195 1196 class ParserControlOracleTests(unittest.TestCase): 1197 def test_complete_reply_and_poison_probe_inventory(self): 1198 from parser_control import cases 1199 rows = list(cases()) 1200 self.assertEqual(len(rows), 252) 1201 self.assertEqual(len({r[0] for r in rows}), len(rows)) 1202 self.assertEqual(sum('ignored-fields-' in r[0] for r in rows), 4) 1203 self.assertTrue(any(r[0] == 'unselected-failure' for r in rows)) 1204 self.assertTrue(any(r[0] == 'unselected-success' for r in rows)) 1205 1206 1207 1208 class BaseWorkloadTests(unittest.TestCase): 1209 def test_generated_worker_variants(self): 1210 from base_workloads import candidate, WORKERS, reference_source 1211 source = reference_source() 1212 for variant in ['manual', 'direct', 'patterned', 'captured']: 1213 text = candidate(source, variant) 1214 for public in WORKERS: 1215 self.assertIn(public + ' ', text) 1216 if variant != 'manual': 1217 self.assertNotRegex(text, r'\bself\b') 1218 self.assertIn('map f l = (let rec', candidate(source, 'captured')) 1219 self.assertIn('foldl f x l = (let rec', candidate(source, 'captured')) 1220 self.assertIn('reverse_ other acc = other', candidate(source, 'patterned')) 1221 1222 def test_large_rendered_result_before_rts_residency(self): 1223 from base_workloads import measurements 1224 with tempfile.TemporaryDirectory() as d: 1225 path = Path(d) / 'sample.log' 1226 rows = [dict(profile=name, wall_ns=i * 100, allocated_bytes=i * 10, 1227 gc_elapsed_ns=i, live_bytes=7) 1228 for i, name in enumerate(['load-begin', 'load-end', 1229 'invoke-begin-1', 'invoke-end-1', 1230 'observe-begin-1', 'observe-end-1'])] 1231 # Must not perform an unanchored numeric regex search over this line. 1232 path.write_text('\n'.join(json.dumps(r) for r in rows) + '\n' + 1233 '0' * 200000 + ' 9,999 bytes allocated in the heap\n' + 1234 ' 1,234 bytes maximum residency (2 samples)\n') 1235 actual = measurements(path) 1236 self.assertEqual(actual['rts_max_residency_bytes'], 1234) 1237 self.assertEqual(actual['invoke_wall_ns'], 300) 1238 self.assertEqual(actual['invoke_allocated_bytes'], 30) 1239 self.assertEqual(actual['measurement_scope'], 'invocation-through-rendered-output-v1') 1240 1241 def test_workload_independent_oracles(self): 1242 from base_workloads import workloads, encode 1243 rows = {name: (argument, expected) for name, argument, expected in workloads(1024)} 1244 data = bytes(rows['lower'][0]) 1245 self.assertEqual(bytes(rows['lower'][1]), data.lower()) 1246 self.assertEqual(rows['splitLines'][1], [list(x) for x in data.split(b'\n')]) 1247 self.assertEqual(rows['sumRows'][1], [sum(x) for x in rows['sumRows'][0]]) 1248 self.assertEqual(len(rows['prefixDigits'][1]), 20) 1249 self.assertEqual(encode([0, 1]), b'\2\0\2\2\1\0\0\0') 1250 1251 def test_summary_discards_warmup(self): 1252 from base_workloads import summarize 1253 fields = ['invoke_wall_ns', 'invoke_allocated_bytes', 'invoke_gc_elapsed_ns', 1254 'load_wall_ns', 'load_allocated_bytes', 'post_invoke_live_bytes', 1255 'rts_max_residency_bytes', 'peak_kib', 'seconds'] 1256 rows = [dict(variant='manual', workload='lower', size=64, warmup=(v == 999), 1257 **{k: v for k in fields}) for v in [999, 2, 4, 8]] 1258 result = summarize(rows)[0] 1259 self.assertEqual(result['repetitions'], 3) 1260 self.assertEqual(result['invoke_wall_ns'], dict(median=4, minimum=2, maximum=8)) 1261 1262 1263 1264 class BaseReadableOracleTests(unittest.TestCase): 1265 def test_small_bounded_inventory(self): 1266 from base_readable import cases, edges 1267 from source_order import end 1268 rows = list(edges()) 1269 self.assertEqual(len(rows), 30) 1270 self.assertEqual(len({row[0] for row in rows}), 30) 1271 for _, _, argument, expected in rows: 1272 self.assertEqual(end(argument), len(argument)) 1273 self.assertEqual(end(expected), len(expected)) 1274 for size in [64, 1024]: 1275 workloads = list(cases(size)) 1276 self.assertEqual(len(workloads), 8) 1277 for _, argument, expected in workloads: 1278 self.assertEqual(end(argument), len(argument)) 1279 self.assertEqual(end(expected), len(expected)) 1280 1281 1282 class CollectionBatchTests(unittest.TestCase): 1283 def test_oracles_are_complete_trees_with_unique_short_labels(self): 1284 from collections_batch import cases, effects 1285 from source_order import end 1286 rows = cases() + effects() 1287 self.assertEqual(len({row[0] for row in rows}), len(rows)) 1288 for label, _, argument, expected in rows: 1289 self.assertLess(len(label), 120) 1290 self.assertEqual(end(argument), len(argument)) 1291 self.assertEqual(end(expected), len(expected)) 1292 1293 def test_workspace_audit_rejects_changed_sources_and_objects(self): 1294 from collections_batch import audit_workspace 1295 with tempfile.TemporaryDirectory() as directory: 1296 workspace, store, compiler, tree, restore = self.make_workspace(Path(directory)) 1297 self.assertEqual(set(audit_workspace(workspace, store, compiler)), {'a', 'b'}) 1298 source = workspace/'a.tri' 1299 source.write_text('!export value\nvalue = t t\n') 1300 with self.assertRaisesRegex(ValueError, 'source'): 1301 audit_workspace(workspace, store, compiler) 1302 source.write_text('!export value\nvalue = t\n') 1303 (store/'objects'/tree[:3]/tree).write_bytes(b'corrupt') 1304 with self.assertRaisesRegex(AssertionError, 'object digest'): 1305 audit_workspace(workspace, store, compiler) 1306 1307 def test_workspace_audit_rejects_stale_dependency_and_export_pins(self): 1308 from collections_batch import audit_workspace 1309 with tempfile.TemporaryDirectory() as directory: 1310 workspace, store, compiler, tree, restore = self.make_workspace(Path(directory)) 1311 restore(stale_receipt=True) 1312 with self.assertRaisesRegex(AssertionError, 'stale candidate dependency'): 1313 audit_workspace(workspace, store, compiler) 1314 restore(stale_pin=True) 1315 with self.assertRaisesRegex(AssertionError, 'stale candidate export pin'): 1316 audit_workspace(workspace, store, compiler) 1317 1318 @staticmethod 1319 def make_workspace(root): 1320 workspace, store = root/'workspace', root/'store' 1321 workspace.mkdir();(store/'cache/source-build').mkdir(parents=True) 1322 compiler = 'a'*64 1323 def obj(domain, payload): 1324 pin = digest(domain, payload) 1325 path = store/'objects'/pin[:3]/pin;path.parent.mkdir(parents=True, exist_ok=True) 1326 path.write_bytes(payload);return pin 1327 tree = obj('arboricx.tree-term.v1', b'\0') 1328 sources = {'a': '!export value\nvalue = t\n', 1329 'b': '!import a *\n!export main\nmain = value\n'} 1330 manifests = {alias: obj('arboricx.module-manifest.v1', 1331 ('arboricx.module-manifest.v1\nexport\t'+name+'\tarboricx.tree-term.v1\t'+tree+'\tarboricx.abi.tree.v1\n').encode()) 1332 for alias, name in [('a', 'value'), ('b', 'main')]} 1333 for alias, source in sources.items(): 1334 (workspace/(alias+'.tri')).write_text(source) 1335 def receipt(alias, extra=''): 1336 payload = ('arboricx.build-receipt.v1\nsource\t'+digest('arboricx.source.v1', sources[alias].encode())+ 1337 '\ncompiler\t'+compiler+'\noptions\t'+digest('arboricx.build-options.v1', b'source-module.stable.v1')+ 1338 '\noutput\t'+manifests[alias]+'\n'+extra).encode() 1339 pin = obj('arboricx.build-receipt.v1', payload) 1340 (store/'cache/source-build'/alias).write_text('arboricx.source-build-cache.v1\t'+'0'*64+'\t'+manifests[alias]+'\t'+pin) 1341 return pin 1342 a_receipt = receipt('a') 1343 def restore(stale_receipt=False, stale_pin=False): 1344 receipt('b', 'local\ta\t'+manifests['a']+'\t'+('b'*64 if stale_receipt else a_receipt)+ 1345 '\npin\ta\t'+manifests['a']+'\tvalue\t'+('c'*64 if stale_pin else tree)+'\n') 1346 restore() 1347 return workspace, store, compiler, tree, restore 1348 1349 1350 class NumericBatchTests(unittest.TestCase): 1351 def test_independent_numeric_oracles_are_complete_trees(self): 1352 from numeric_batch import cases 1353 from source_order import end 1354 rows = cases() 1355 self.assertEqual(len({row[0] for row in rows}), len(rows)) 1356 for label, _, argument, expected in rows: 1357 self.assertLess(len(label), 120) 1358 self.assertEqual(end(argument), len(argument)) 1359 self.assertEqual(end(expected), len(expected)) 1360 1361 def test_zero_divisor_and_shape_oracles_are_distinct(self): 1362 from numeric_batch import cases, error, n, pair, tree_list 1363 rows = cases() 1364 checked = next(expected for _, name, argument, expected in rows 1365 if name == 'checkedBinary' and argument == pair(n(7), n(0))) 1366 self.assertTrue(checked.endswith(tree_list([error('non-zero')]*3))) 1367 self.assertNotEqual(error('non-zero'), n(0)) 1368 self.assertNotEqual(error('not a natural number'), error('non-zero')) 1369 1370 1371 class BundleBatchTests(unittest.TestCase): 1372 def test_complete_independent_oracles(self): 1373 from bundle_batch import cases 1374 from source_order import end 1375 rows = cases() 1376 self.assertEqual(len(rows), len({row[0] for row in rows})) 1377 for label, _, _, arguments, expected in rows: 1378 self.assertLess(len(label), 120) 1379 for tree in arguments + [expected]: 1380 self.assertEqual(end(tree), len(tree)) 1381 1382 def test_suffix_and_rejection_controls_remain_present(self): 1383 from bundle_batch import cases, byte_tree, n 1384 from source_order import field 1385 rows = cases() 1386 self.assertTrue(any(export == 'prefix' and field(expected, 1, 1) == byte_tree(b'TAIL') 1387 for _, _, export, _, expected in rows)) 1388 for code in [7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]: 1389 self.assertTrue(any(expected.startswith(b'\2\0\2') and field(expected, 1, 0) == n(code) 1390 for _, _, _, _, expected in rows), code) 1391 1392 1393 class ToolingBatchTests(unittest.TestCase): 1394 def test_complete_models_and_graph_ready_order(self): 1395 from tooling_batch_models import cases, graph_result, n, result 1396 from source_order import end 1397 # A newly ready earlier entry precedes the already-ready later one. 1398 self.assertEqual(graph_result([(0,[1]),(1,[]),(2,[])]), result(True,n([1,0,2]))) 1399 self.assertEqual(graph_result([(0,[1]),(1,[2]),(2,[1])]), result(False,n(1))) 1400 rows=cases() 1401 self.assertEqual(len(rows),len({row[0] for row in rows})) 1402 for _,_,argument,expected in rows: 1403 for tree in [argument,expected]:self.assertEqual(end(tree),len(tree)) 1404 1405 def test_formatter_oracles_preserve_tokens_and_line_counts(self): 1406 from tooling_batch_models import format_cases 1407 import re 1408 tokens=re.compile(r'"(?:\\.|[^"\\])*"|\|-.*?-\||--[^\r\n]*|[^\s]',re.S) 1409 for label,source,expected in format_cases(): 1410 self.assertEqual(tokens.findall(source),tokens.findall(expected),label) 1411 self.assertEqual(source.count('\n'),expected.count('\n'),label) 1412 1413 1414 class SourceFoundationOracleTests(unittest.TestCase): 1415 def test_corpus_has_complete_trees_and_unique_labels(self): 1416 from source_foundations_models import corpus 1417 from source_order import end 1418 rows = corpus(consumers=True) 1419 self.assertEqual(len(rows), len({row[0] for row in rows})) 1420 for label, _, argument, expected in rows: 1421 self.assertEqual(end(argument), len(argument), label) 1422 self.assertEqual(end(expected), len(expected), label) 1423 1424 def test_shift_preserves_bound_indices_and_opaque_trees(self): 1425 from source_foundations_models import Node, shift, substitute 1426 bound = Node(4, Node(2, 0)) 1427 free = Node(4, Node(2, 1)) 1428 opaque = Node(5, free.tree()) 1429 self.assertEqual(shift(bound, 7), bound) 1430 self.assertEqual(shift(free, 7), Node(4, Node(2, 8))) 1431 self.assertEqual(shift(opaque, 7), opaque) 1432 self.assertEqual(substitute(free, [Node(2, 3)], 5), Node(4, Node(2, 4))) 1433 self.assertEqual(shift(free, 2), shift(shift(free))) 1434 1435 1436 class CandidateResourceTests(unittest.TestCase): 1437 def test_memory_selection_reaches_heap_and_rss_guards(self): 1438 from collections_batch import ProbeRunner 1439 from unittest.mock import patch 1440 with tempfile.TemporaryDirectory() as directory: 1441 root = Path(directory) 1442 for selected in [8192, 65536]: 1443 runner = ProbeRunner(root/'tricu', root, 60, memory_mib=selected) 1444 with patch('source_order.run_guarded', return_value=('', 123)) as run: 1445 runner.run('probe', ['run'], 30) 1446 command = run.call_args.args[0] 1447 self.assertIn(f'-M{selected}m', command) 1448 self.assertEqual(run.call_args.kwargs['memory_mib'], selected) 1449 self.assertLessEqual(run.call_args.kwargs['seconds'], 30) 1450 self.assertEqual(ProbeRunner(root/'tricu', root, 60).memory_mib, 8192) 1451 1452 def test_memory_selection_rejects_out_of_policy_limits(self): 1453 from source_order import Runner 1454 for value in [-1, 0, 65537]: 1455 with self.assertRaises(ValueError): 1456 Runner(Path('tricu'), Path('.'), memory_mib=value) 1457 1458 1459 class GrammarOracleTests(unittest.TestCase): 1460 def test_authored_replies_and_lookahead_vectors_are_whole_trees(self): 1461 from source_grammar_models import authored, lookahead_rows 1462 from source_order import end 1463 cases = authored() 1464 self.assertEqual(len(cases), len({row[0] for row in cases})) 1465 for label, _, argument, expected in cases: 1466 self.assertEqual(end(argument), len(argument), label) 1467 self.assertEqual(end(expected), len(expected), label) 1468 for row in lookahead_rows(): 1469 for tree in row: 1470 self.assertEqual(end(tree), len(tree)) 1471 1472 def test_layout_oracles_are_complete_and_independent(self): 1473 from source_layout_cases import semantic_cases, parser_cases 1474 from source_order import end 1475 values, replies = semantic_cases(), parser_cases() 1476 self.assertEqual(len(values + replies), len({row[0] for row in values + replies})) 1477 for label, _, value in values: 1478 self.assertEqual(end(value), len(value), label) 1479 for label, _, _, expected in replies: 1480 self.assertEqual(end(expected), len(expected), label) 1481 aligned = next(row[3] for row in replies if row[0] == 'ast-layout-2') 1482 deeper = next(row[3] for row in replies if row[0] == 'ast-layout-4') 1483 self.assertNotEqual(aligned, deeper) # equal structure, different spans 1484 self.assertTrue(any(row[0] == 'unindented-body' for row in replies)) 1485 self.assertTrue(any(row[0] == 'nested-do-indent' for row in replies)) 1486 1487 def test_lookahead_boundaries_do_not_consume_or_guess_assignments(self): 1488 from source_grammar_models import lookahead 1489 self.assertTrue(lookahead([2, 2, 5], [1, 3, 5])[0][0]) 1490 self.assertFalse(lookahead([2, 6, 5], [1, 2, 3])[0][0]) 1491 self.assertFalse(lookahead([2, 6, 11], [1, 2, 3])[0][1]) 1492 self.assertTrue(lookahead([2, 3, 6, 4, 11], [1]*5)[0][1]) 1493 self.assertFalse(lookahead([2, 12, 11], [1]*3)[0][1]) 1494 self.assertEqual(lookahead([6, 2, 11], [1, 3, 5])[1], 1495 [True, True, True, False, True, True]) 1496 1497 1498 class ResolutionOracleTests(unittest.TestCase): 1499 def test_models_emit_complete_trees(self): 1500 from source_resolution_models import (ast_cases, scope_cases, pattern_cases, 1501 resolve_result, free, pattern_result, graph_cases, graph_model) 1502 from source_order import end 1503 for node, environment in ast_cases(): 1504 for tree in [node.tree(), resolve_result(node, environment)]: 1505 self.assertEqual(end(tree), len(tree)) 1506 for node in scope_cases(): 1507 for name in free(node): 1508 self.assertEqual(name.kind, 2) 1509 self.assertEqual(end(name.tree()), len(name.tree())) 1510 for node, binders, environment in pattern_cases(): 1511 tree = pattern_result(node, binders, environment) 1512 self.assertEqual(end(tree), len(tree)) 1513 for definitions, environment, final in graph_cases(): 1514 for tree in graph_model(definitions, environment, final): 1515 self.assertEqual(end(tree), len(tree)) 1516 1517 def test_scope_and_duplicate_priority(self): 1518 from source_resolution_models import Ast, free, resolved, graph_model 1519 from source_order import field, number 1520 name = Ast(2, 'x', 9) 1521 self.assertEqual(free(Ast(9, ('x', name, name))), [name]) 1522 self.assertEqual(free(Ast(15, ('x', name, name))), []) 1523 bound = resolved(name, [], ('y', 'x', 'x')) 1524 self.assertEqual(number(field(bound, 1, 0)), 1) 1525 definitions = [(Ast(2, 'x', 1), Ast(2, 'missing', 2)), 1526 (Ast(2, 'x', 3), Ast(1))] 1527 ordered, analyzed = graph_model(definitions, [], Ast(2, 'absent')) 1528 self.assertEqual(number(field(ordered, 1, 0, 0)), 30) 1529 self.assertEqual(number(field(analyzed, 1, 0, 0)), 20) 1530 1531 1532 class LoweringOracleTests(unittest.TestCase): 1533 def test_identity_and_constant(self): 1534 from source_lowering_models import tc_apply, I, K, n, LEAF 1535 for value in [LEAF, n(23), I]: 1536 self.assertEqual(tc_apply(I, value), value) 1537 self.assertEqual(tc_apply(tc_apply(K, value), n(99)), value) 1538 1539 def test_complete_residual_models(self): 1540 from source_lowering_models import specimens, bracket, eliminate, run_result 1541 from source_order import end 1542 terms, cores = specimens() 1543 for tree in [bracket(t).tree() for t in terms] + [eliminate(c).tree() for c in cores] + [run_result(t) for t in terms]: 1544 self.assertEqual(end(tree), len(tree)) 1545 1546 1547 class ModuleOracleTests(unittest.TestCase): 1548 def test_roundtrip_and_whole_expectations(self): 1549 from source_modules import tree_cases, decode, manifest_cases, header_cases 1550 from source_order import end 1551 from http_headers import result 1552 for tree in tree_cases(): 1553 self.assertEqual(decode(list(tree), True), result(True, tree)) 1554 for text, expected in manifest_cases() + header_cases(): 1555 self.assertEqual(end(expected), len(expected)) 1556 1557 1558 class BuildOracleTests(unittest.TestCase): 1559 def test_bundle_layout_and_missing_dependency(self): 1560 from source_building import bundle_bytes, map_cases 1561 from stable_bundle import sections 1562 from source_order import field 1563 from http_headers import LEAF 1564 with tempfile.TemporaryDirectory() as directory: 1565 path = Path(directory) / 'golden.arboricx' 1566 path.write_bytes(bundle_bytes([('x', LEAF), ('y', LEAF)])) 1567 self.assertEqual(len(sections(path)), 2) 1568 text, outcome = next((text, outcome) for text, outcome in map_cases() if '\tmissing\n' in text) 1569 self.assertEqual(field(outcome, 0), LEAF) 1570 1571 1572 class BackendDefaultTests(unittest.TestCase): 1573 def test_historical_provenance_is_not_reinterpreted(self): 1574 from generation_backend import DEFAULT_BACKEND, recorded_backend 1575 self.assertEqual(DEFAULT_BACKEND, 'stable') 1576 self.assertEqual(recorded_backend({}), 'optimized') 1577 for backend in ['stable', 'optimized']: 1578 self.assertEqual(recorded_backend({'backend': backend}), backend) 1579 with self.assertRaises(ValueError): 1580 recorded_backend({'backend': 'unknown'}) 1581 1582 def test_real_runner_parsers_default_stable_and_keep_overrides(self): 1583 # Stop at parse_args: exercise the actual CLI declarations without 1584 # reading artifacts, spawning hosts or building source in the fast gate. 1585 import argparse 1586 import runpy 1587 from unittest.mock import patch 1588 1589 class Parsed(Exception): 1590 pass 1591 1592 parse = argparse.ArgumentParser.parse_args 1593 scripts = { 1594 'bootstrap_generations.py': ['previous', 'output'], 1595 'bootstrap_generation_cli.py': ['previous'], 1596 'native_library_migration.py': ['tricu', 'generation', 'output', '--library', 'socket'], 1597 'portable_cli.py': ['tricu'], 1598 'source_equations.py': ['tricu', '--generation', 'generation'], 1599 } 1600 for script, required in scripts.items(): 1601 for override in [None, 'stable', 'optimized']: 1602 with self.subTest(script=script, override=override): 1603 captured = [] 1604 argv = required + (['--backend', override] if override else []) 1605 1606 def intercept(parser, *args, **kwargs): 1607 captured.append(parse(parser, argv)) 1608 raise Parsed() 1609 1610 with patch.object(argparse.ArgumentParser, 'parse_args', intercept): 1611 with self.assertRaises(Parsed): 1612 runpy.run_path(str(Path(__file__).parent / script), run_name='__main__') 1613 self.assertEqual(captured[0].backend, override or 'stable') 1614 1615 1616 class HistoricalLibraryReferenceTests(unittest.TestCase): 1617 def test_historical_paths_are_labelled_without_source_substitution(self): 1618 import importlib.util 1619 spec = importlib.util.spec_from_file_location('site_reference', ROOT/'ext/demo-site/reference.py') 1620 module = importlib.util.module_from_spec(spec) 1621 spec.loader.exec_module(module) 1622 source, note = module.source_reference(ROOT, 'lib/io-v2.tri') 1623 self.assertIsNone(source) 1624 self.assertIn('Historical source path', note) 1625 source, note = module.source_reference(ROOT, 'lib/io.tri') 1626 self.assertEqual(source, ROOT/'lib/io.tri') 1627 self.assertIn('Source SHA-256', note) 1628 for path in ['lib/missing-current.tri', '../outside.tri', '/etc/passwd']: 1629 with self.assertRaises(ValueError): 1630 module.source_reference(ROOT, path) 1631 1632 1633 class CheckpointProgressTests(unittest.TestCase): 1634 def test_private_and_unselected_files_are_never_read_as_checkpoints(self): 1635 from bootstrap_generations import published_checkpoints 1636 with tempfile.TemporaryDirectory() as temporary: 1637 root = Path(temporary) 1638 cache = root / 'cas/cache/source-build' 1639 cache.mkdir(parents=True) 1640 (cache/'app').write_bytes(b'committed') 1641 # Directories would fail read_bytes if the watcher touched them. 1642 (cache/'473-17.tricu-stage-').mkdir() 1643 (cache/'unselected').mkdir() 1644 self.assertEqual(dict(published_checkpoints(root, ['app', 'not-yet-published'])), 1645 {'app': b'committed'}) 1646 1647 1648 class PublishedInventoryTests(unittest.TestCase): 1649 def test_only_reserved_unpublished_stage_names_are_excluded(self): 1650 from shared_library_generation import published_objects 1651 with tempfile.TemporaryDirectory() as temporary: 1652 root = Path(temporary) 1653 shard = root / 'objects' / 'aaa' 1654 shard.mkdir(parents=True) 1655 (shard / ('a' * 64)).write_bytes(b'committed') 1656 (shard / '.tricu-stage-interrupted').write_bytes(b'partial') 1657 (shard / '473-17.tricu-stage-').write_bytes(b'GHC temporary naming') 1658 self.assertEqual(published_objects(root), {'a' * 64: b'committed'}) 1659 (shard / ('a' * 63 + 'b')).write_bytes(b'extra committed object') 1660 self.assertEqual(len(published_objects(root)), 2) 1661 (shard / 'unexpected').write_bytes(b'not a reserved stage') 1662 with self.assertRaises(AssertionError): 1663 published_objects(root) 1664 1665 1666 class RuntimeRetirementTests(unittest.TestCase): 1667 def test_generation_parser_rejects_retired_runtime_controls_before_output(self): 1668 import runpy 1669 import sys 1670 from contextlib import redirect_stderr 1671 from io import StringIO 1672 from unittest.mock import patch 1673 with tempfile.TemporaryDirectory() as temporary: 1674 output = Path(temporary) / 'not-created' 1675 for options in [('--execution-host', 'zig'), ('--zig-library', '/missing'), 1676 ('--zig-gc-policy', '1', '1')]: 1677 with self.subTest(options=options), patch.object(sys, 'argv', 1678 ['bootstrap_generations.py', 'previous', str(output), *options]): 1679 with redirect_stderr(StringIO()), self.assertRaises(SystemExit) as error: 1680 runpy.run_path(str(Path(__file__).parent / 'bootstrap_generations.py'), run_name='__main__') 1681 self.assertEqual(error.exception.code, 2) 1682 self.assertFalse(output.exists()) 1683 1684 1685 class BootstrapKitTests(unittest.TestCase): 1686 def setUp(self): 1687 from bootstrap_inputs import SCHEMA, ROLES 1688 self.temporary = tempfile.TemporaryDirectory() 1689 self.addCleanup(self.temporary.cleanup) 1690 self.directory = Path(self.temporary.name) 1691 self.metadata = dict(schema=SCHEMA, backend='stable', artifacts={}) 1692 for role, filename in ROLES.items(): 1693 payload = (role + '-opaque-test-bytes').encode() 1694 (self.directory / filename).write_bytes(payload) 1695 self.metadata['artifacts'][role] = dict(file=filename, 1696 export='main' if role == 'adapter' else 'sourceMapBuildStable', 1697 sha256=hashlib.sha256(payload).hexdigest()) 1698 self.save() 1699 1700 def save(self): 1701 (self.directory / 'bootstrap-kit.json').write_text(json.dumps(self.metadata)) 1702 1703 def load(self, backend='stable'): 1704 from bootstrap_inputs import load_kit 1705 return load_kit(self.directory, backend) 1706 1707 def test_captures_exact_manifest_and_artifacts(self): 1708 raw, payloads, pin = self.load() 1709 self.assertEqual(raw, (self.directory / 'bootstrap-kit.json').read_bytes()) 1710 self.assertEqual(pin, hashlib.sha256(raw).hexdigest()) 1711 (self.directory / 'rebuilt-compiler.arboricx').write_bytes(b'changed') 1712 self.assertEqual(payloads['compiler'], b'compiler-opaque-test-bytes') 1713 with self.assertRaisesRegex(ValueError, 'digest mismatch'): self.load() 1714 1715 def test_backend_is_explicit(self): 1716 with self.assertRaisesRegex(ValueError, 'backend mismatch'): self.load('optimized') 1717 self.metadata.pop('backend'); self.save() 1718 with self.assertRaisesRegex(ValueError, 'backend mismatch'): self.load() 1719 1720 def test_rejects_invalid_manifest_schema(self): 1721 for schema in ('old', None, 1): 1722 with self.subTest(schema=schema): 1723 self.metadata['schema'] = schema; self.save() 1724 with self.assertRaisesRegex(ValueError, 'schema'): self.load() 1725 1726 def test_requires_both_roles_and_no_extra_role(self): 1727 adapter = self.metadata['artifacts'].pop('adapter'); self.save() 1728 with self.assertRaisesRegex(ValueError, 'exactly'): self.load() 1729 self.metadata['artifacts']['adapter'] = adapter 1730 self.metadata['artifacts']['extra'] = adapter; self.save() 1731 with self.assertRaisesRegex(ValueError, 'exactly'): self.load() 1732 1733 def test_rejects_path_traversal_and_role_swap(self): 1734 for filename in ('../elsewhere', '/tmp/elsewhere', 'io-adapter.arboricx'): 1735 with self.subTest(filename=filename): 1736 self.metadata['artifacts']['compiler']['file'] = filename; self.save() 1737 with self.assertRaisesRegex(ValueError, 'filename'): self.load() 1738 1739 def test_rejects_wrong_export(self): 1740 self.metadata['artifacts']['compiler']['export'] = 'sourceMapBuild'; self.save() 1741 with self.assertRaisesRegex(ValueError, 'export'): self.load() 1742 1743 def test_rejects_invalid_digest(self): 1744 for pin in ('a' * 63, 'A' * 64, '../x', None): 1745 with self.subTest(pin=pin): 1746 self.metadata['artifacts']['adapter']['sha256'] = pin; self.save() 1747 with self.assertRaisesRegex(ValueError, 'digest'): self.load() 1748 1749 def test_explicit_versioned_kit(self): 1750 self.metadata.update(schema='tricu.bootstrap-kit.v2', io_version=2); self.save() 1751 self.load() 1752 for bad in [True, '2', 0, 3, None]: 1753 self.metadata['io_version'] = bad; self.save() 1754 with self.assertRaises(ValueError): self.load() 1755 self.metadata.pop('io_version'); self.save() 1756 with self.assertRaises(ValueError): self.load() 1757 self.metadata.update(schema='tricu.bootstrap-kit.v1', io_version=2); self.save() 1758 with self.assertRaises(ValueError): self.load() 1759 1760 def test_missing_or_corrupt_adapter_fails(self): 1761 path = self.directory / 'io-adapter.arboricx' 1762 path.write_bytes(b'corrupt') 1763 with self.assertRaisesRegex(ValueError, 'digest mismatch'): self.load() 1764 path.unlink() 1765 with self.assertRaises(FileNotFoundError): self.load() 1766 1767 1768 class GenerationIOTests(unittest.TestCase): 1769 def test_strict_versions_and_historical_default(self): 1770 from generation_io import versions, version 1771 self.assertEqual(versions({}), (1, 1)) 1772 for bad in [True, False, 0, 3, '2', None, 2.0]: 1773 with self.subTest(bad=bad), self.assertRaises(ValueError): version(bad) 1774 for incomplete in [{'execution_io_version': 2}, {'produced_adapter_sha256': 'a'*64}]: 1775 with self.assertRaises(ValueError): versions(incomplete) 1776 with self.assertRaises(ValueError): versions(dict(execution_io_version=2, 1777 produced_io_version=2, produced_adapter_sha256='A'*64)) 1778 1779 def test_host_and_authority_are_explicit(self): 1780 from generation_io import execution_flags, compiler_flags 1781 self.assertEqual(compiler_flags(1), []) 1782 self.assertEqual(compiler_flags(2), ['--toolchain-io-version', '2']) 1783 self.assertEqual(execution_flags(1), ['--unsafe-io']) 1784 with self.assertRaises(ValueError): execution_flags(1, 'haskell-ffi') 1785 self.assertEqual(execution_flags(2), []) 1786 self.assertEqual(execution_flags(2, 'haskell-ffi'), []) 1787 for host in ['browser', 'zig', 'unknown']: 1788 with self.assertRaises(ValueError): execution_flags(2, host) 1789 1790 def test_invocation_grants_and_canonical_cli_selection(self): 1791 from generation_io import cli_version, check_invocation 1792 import argparse 1793 v2 = dict(execution_io_version=2, produced_io_version=2, produced_adapter_sha256='a'*64) 1794 check_invocation(v2, ['tricu', 'arboricx', 'run', '--io']) 1795 check_invocation({}, ['tricu', '--unsafe-io']) 1796 for flag in ['--unsafe-io', '--allow-read=/tmp', '--allow-write', '--allow-read-all', '--allow-write-all']: 1797 with self.assertRaises(ValueError): check_invocation(v2, ['tricu', flag]) 1798 with self.assertRaises(ValueError): check_invocation({}, ['tricu', '--io']) 1799 for text in ['01', '+2', '2.0', ' 2', '3', '-1']: 1800 with self.assertRaises(argparse.ArgumentTypeError): cli_version(text) 1801 self.assertEqual(cli_version('2'), 2) 1802 1803 def test_transition_chain_not_identical_execution_framing(self): 1804 from generation_io import check_pair 1805 first = dict(adapter_sha256='a'*64, execution_io_version=1, 1806 produced_io_version=2, produced_adapter_sha256='b'*64) 1807 second = dict(adapter_sha256='b'*64, execution_io_version=2, 1808 produced_io_version=2, produced_adapter_sha256='b'*64) 1809 check_pair(first, second) 1810 for key, value in [('execution_io_version', 1), ('produced_io_version', 1), 1811 ('adapter_sha256', 'a'*64), ('produced_adapter_sha256', 'c'*64)]: 1812 with self.subTest(key=key), self.assertRaises(ValueError): 1813 check_pair(first, {**second, key: value}) 1814 check_pair(dict(adapter_sha256='a'*64), dict(adapter_sha256='a'*64)) 1815 1816 def test_produced_adapter_is_authenticated_not_input_adapter(self): 1817 from generation_io import produced_adapter 1818 with tempfile.TemporaryDirectory() as directory: 1819 root = Path(directory) 1820 (root/'io-adapter.arboricx').write_bytes(b'v1') 1821 (root/'produced-io-adapter.arboricx').write_bytes(b'v2') 1822 old = dict(adapter_sha256=hashlib.sha256(b'v1').hexdigest()) 1823 new = dict(**old, execution_io_version=1, produced_io_version=2, 1824 produced_adapter_sha256=hashlib.sha256(b'v2').hexdigest()) 1825 self.assertEqual(produced_adapter(root, old), b'v1') 1826 self.assertEqual(produced_adapter(root, new), b'v2') 1827 (root/'produced-io-adapter.arboricx').write_bytes(b'v1') 1828 with self.assertRaises(ValueError): produced_adapter(root, new) 1829 1830 1831 class IndexedObjectAuditTests(unittest.TestCase): 1832 def test_independent_writer_and_complete_audit(self): 1833 from native_indexed_store import indexed_bytes 1834 from executable_object import verify_payload, INDEXED, FLAT 1835 for tree in [b'\0', b'\1\0', b'\2\1\0\2\0\0']: 1836 verify_payload(FLAT, tree) 1837 data = indexed_bytes(tree) 1838 verify_payload(INDEXED, data) 1839 for at in [0, 8, 15, 31, 32, 95, 96, len(data) - 1]: 1840 changed = bytearray(data); changed[at] ^= 255 1841 with self.subTest(tree=tree, at=at): 1842 with self.assertRaises((AssertionError, ValueError)): 1843 verify_payload(INDEXED, bytes(changed)) 1844 for changed in [data[:-1], data + b'\0']: 1845 with self.assertRaises(AssertionError): verify_payload(INDEXED, changed) 1846 with self.assertRaises(AssertionError): verify_payload('wrong.kind', b'\0') 1847 1848 1849 class InspectorStagingTests(unittest.TestCase): 1850 def test_staging_is_opt_in_immutable_and_authenticates_bytes(self): 1851 from library_inspector import stage_site 1852 with tempfile.TemporaryDirectory() as temporary: 1853 root = Path(temporary) 1854 (root / 'producer').mkdir() 1855 site = root / 'site'; (site / 'bundles').mkdir(parents=True) 1856 index = site / 'packages.json' 1857 index.write_text(json.dumps(dict(schema=1, releases=[]))) 1858 payload = root / 'producer/tricu-base.arboricx' 1859 payload.write_bytes(b'fixture bytes') 1860 digest = hashlib.sha256(payload.read_bytes()).hexdigest() 1861 report = dict(complete=True, releases={'tricu-base': dict(version='0.1.0', manifest='a'*64, bundle=digest)}) 1862 with self.assertRaises(ValueError): stage_site(root, site, dict(report, complete=False)) 1863 for version in (0, 3, True, "2"): 1864 with self.assertRaises(ValueError): stage_site(root, site, dict(report, io_version=version)) 1865 self.assertEqual(list((site / "bundles").iterdir()), []) 1866 stage_site(root, site, report) 1867 before = index.read_bytes(); stage_site(root, site, dict(report, io_version=2)) 1868 self.assertEqual(index.read_bytes(), before) 1869 self.assertEqual((site / 'bundles' / (digest + '.arboricx')).read_bytes(), payload.read_bytes()) 1870 payload.write_bytes(b'changed bytes') 1871 with self.assertRaises(ValueError): stage_site(root, site, report) 1872 changed = hashlib.sha256(payload.read_bytes()).hexdigest() 1873 report['releases']['tricu-base']['bundle'] = changed 1874 with self.assertRaises(ValueError): stage_site(root, site, report) 1875 self.assertEqual(index.read_bytes(), before) 1876 self.assertFalse((site / 'bundles' / (changed + '.arboricx')).exists()) 1877 report['releases']['tricu-base']['version'] = '0.2.0' 1878 stage_site(root, site, report) 1879 self.assertEqual(len(json.loads(index.read_text())['releases']), 2) 1880 self.assertTrue((site / 'bundles' / (digest + '.arboricx')).exists()) 1881 1882 1883 if __name__ == '__main__': 1884 unittest.main()