unit.py (85765B)
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), 22) 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 ArchiveRecoveryPolicyTests(unittest.TestCase): 514 def test_scp_execution_is_not_a_compiler_restore_gate(self): 515 archive = (ROOT / 'test/bootstrap_archive.py').read_text() 516 self.assertNotIn('scp_native_validation.py', archive) 517 self.assertIn('scp_execution_required=False', archive) 518 519 520 class SupportProjectionTests(unittest.TestCase): 521 def setUp(self): 522 from bundle_dag import merge_bundles, read 523 self.temporary = tempfile.TemporaryDirectory() 524 self.addCleanup(self.temporary.cleanup) 525 self.directory = Path(self.temporary.name) 526 self.original = self.directory / 'original.arboricx' 527 self.projected = self.directory / 'projected.arboricx' 528 leaf = ROOT / 'test/fixtures/false.arboricx' 529 other = ROOT / 'test/fixtures/true.arboricx' 530 merge_bundles(self.original, [(leaf, [(next(iter(read(leaf)[3])), 'keep.value')]), 531 (other, [(next(iter(read(other)[3])), 'drop.value')])]) 532 merge_bundles(self.projected, [(self.original, [('keep.value', 'keep.value')])], 533 prune_unreachable=True) 534 535 def test_exact_selected_exports_without_unreachable_nodes(self): 536 from project_support_fixture import verify_projection 537 self.assertEqual(verify_projection(self.original, self.projected, {'keep'}), 1) 538 539 def test_missing_export_and_changed_kernel_rejected(self): 540 from bundle_dag import merge_bundles 541 from project_support_fixture import verify_projection 542 with self.assertRaisesRegex(ValueError, 'export set'): 543 verify_projection(self.original, self.projected, {'keep', 'drop'}) 544 merge_bundles(self.projected, [(self.original, [('drop.value', 'keep.value')])], 545 prune_unreachable=True) 546 with self.assertRaisesRegex(ValueError, 'changed a kernel'): 547 verify_projection(self.original, self.projected, {'keep'}) 548 549 def test_unreachable_archived_nodes_rejected(self): 550 from bundle_dag import merge_bundles 551 from project_support_fixture import verify_projection 552 merge_bundles(self.projected, [(self.original, [('keep.value', 'keep.value')])]) 553 with self.assertRaisesRegex(ValueError, 'unreachable'): 554 verify_projection(self.original, self.projected, {'keep'}) 555 556 def test_immutable_projection_origin_fingerprints(self): 557 # The extraction ledger remains historical: current/rebuilt outputs have 558 # since been genuinely rebuilt. Never rewrite the original snapshots or 559 # pretend new compilation is an unchanged projection. Current outputs 560 # and every rebuilt receipt are authenticated by the Haskell cohort gate. 561 fixtures = ROOT / 'test/fixtures' 562 evidence = json.loads((fixtures / 'support-projection.json').read_text()) 563 self.assertEqual(evidence['format'], 'tricu.support-projection.v1') 564 for name in ['support-origin.v1', 'support-origin.arboricx', 'support-origin-compiler.arboricx']: 565 self.assertEqual(hashlib.sha256((fixtures / name).read_bytes()).hexdigest(), 566 evidence['outputs_sha256'][name], name) 567 568 def test_active_support_is_dependency_closed_without_archive(self): 569 from native_header import native_header 570 fixtures = ROOT / 'test/fixtures' 571 rows = [line.split('\t') for name in ['compiler-library.v1', 'support-library.v1'] 572 for line in (fixtures / name).read_text().splitlines() if line.startswith('module\t')] 573 providers = {row[2] for row in rows} 574 for _, _, alias, path, pin in rows: 575 self.assertNotIn('consumer', path) 576 self.assertNotIn('archive/', path) 577 from fixture_sources import snapshot_bytes 578 dependencies, _ = native_header(snapshot_bytes(fixtures, pin).decode()) 579 self.assertLessEqual(set(dependencies), providers, alias) 580 self.assertNotIn('ScpCommand', (ROOT / 'tricu.cabal').read_text()) 581 self.assertFalse((ROOT / 'src/ScpCommand.hs').exists()) 582 583 584 class SupportPublicationControls(unittest.TestCase): 585 def test_resource_limits_fail_before_build_or_publication(self): 586 import contextlib 587 import io 588 from native_library_migration import main as compile_candidates 589 from repair_support_fixture import main as publish_candidates 590 cases = [ 591 (compile_candidates, ['tool', 'host', 'generation', 'output', '--library', 'socket', '--capabilities', '0']), 592 (publish_candidates, ['tool', 'host', 'candidates', '--output-directory', 'output', '--capabilities', '0']), 593 (publish_candidates, ['tool', 'host', 'candidates', '--output-directory', 'output', '--verification-memory-mib', '65537']), 594 ] 595 for main, argv in cases: 596 with self.subTest(argv=argv), patch('sys.argv', argv), contextlib.redirect_stderr(io.StringIO()): 597 with self.assertRaises(SystemExit) as error: 598 main() 599 self.assertEqual(error.exception.code, 2) 600 601 def test_publication_cannot_overwrite_an_existing_directory(self): 602 from repair_support_fixture import main 603 with tempfile.TemporaryDirectory() as directory: 604 marker = Path(directory) / 'marker' 605 marker.write_bytes(b'keep') 606 with patch('sys.argv', ['tool', 'host', 'candidates', '--output-directory', directory]): 607 with self.assertRaises(FileExistsError): 608 main() 609 self.assertEqual(marker.read_bytes(), b'keep') 610 self.assertEqual(list(Path(directory).iterdir()), [marker]) 611 612 613 class CompilerDependencyTests(unittest.TestCase): 614 def test_current_map_is_exact_compiler_closure_without_patterns(self): 615 from bootstrap_generations import source_map 616 _, entries, required = source_map( 617 ROOT / 'bootstrap/compiler-source-map.v1', ROOT, 'source_map') 618 self.assertEqual(entries['io'][2], ['base']) 619 self.assertNotIn('patterns', entries) 620 self.assertEqual(set(entries), required) 621 622 623 class FixtureSourceTests(unittest.TestCase): 624 def setUp(self): 625 from fixture_sources import digest 626 self.temporary = tempfile.TemporaryDirectory() 627 self.addCleanup(self.temporary.cleanup) 628 self.root = Path(self.temporary.name) 629 self.fixtures, self.source = self.root / 'fixtures', self.root / 'source' 630 self.fixtures.mkdir() 631 self.source.mkdir() 632 for cohort, alias in [('compiler-library', 'compiler'), ('support-library', 'support')]: 633 source = b'!export main\nmain = t\n' + ('-- ' + alias + '\n').encode() 634 (self.source / (alias + '.tri')).write_bytes(source) 635 bundle = b'synthetic-policy-bundle-' + alias.encode() 636 (self.fixtures / (cohort + '.arboricx')).write_bytes(bundle) 637 (self.fixtures / (cohort + '.v1')).write_text( 638 'tricu.' + cohort + '-fixture.v1\nmodule\t' + alias + '\t' + alias + '\t' + alias + '.tri\t' 639 + digest('arboricx.source.v1', source) + '\nbundle\t' 640 + digest('tricu.' + cohort + '-fixture.v1', bundle) + '\n') 641 (self.source / 'bootstrap').mkdir() 642 (self.source / 'bootstrap/compiler-source-map.v1').write_text( 643 'arboricx.source-map.v1\nnative\tcompiler\tcompiler\tcompiler.tri\t-\n') 644 645 def capture(self): 646 from fixture_sources import capture 647 return capture(self.fixtures, self.source) 648 649 def test_reference_and_freshness_are_distinct(self): 650 from fixture_sources import verify, current_mismatches 651 self.capture() 652 self.assertEqual(current_mismatches(self.fixtures, self.source), []) 653 (self.source / 'compiler.tri').write_text('!import changed *\n!export main\nmain = changed\n') 654 self.assertEqual(len(verify(self.fixtures)), 2) 655 self.assertEqual(current_mismatches(self.fixtures, self.source), ['compiler.tri']) 656 657 def test_capture_cannot_bless_edited_sources(self): 658 (self.source / 'support.tri').write_text('changed') 659 with self.assertRaisesRegex(ValueError, 'does not match'): 660 self.capture() 661 self.assertFalse((self.fixtures / 'reference-source').exists()) 662 663 def test_missing_snapshot_never_falls_back_to_matching_working_source(self): 664 from fixture_sources import verify 665 with self.assertRaises(FileNotFoundError): 666 verify(self.fixtures) 667 668 def test_corrupt_snapshot_is_rejected_not_repaired(self): 669 from fixture_sources import verify 670 self.capture() 671 next((self.fixtures / 'reference-source').glob('*.tri')).write_bytes(b'corrupt') 672 with self.assertRaisesRegex(ValueError, 'digest mismatch'): 673 verify(self.fixtures) 674 with self.assertRaisesRegex(ValueError, 'existing reference'): 675 self.capture() 676 677 def test_corrupt_bundle_is_rejected(self): 678 from fixture_sources import verify 679 self.capture() 680 (self.fixtures / 'compiler-library.arboricx').write_bytes(b'corrupt') 681 with self.assertRaisesRegex(ValueError, 'bundle digest'): 682 verify(self.fixtures) 683 684 def test_unsafe_or_duplicate_metadata_is_rejected(self): 685 from fixture_sources import source_rows 686 path = self.fixtures / 'compiler-library.v1' 687 original = path.read_text() 688 for value in ['../outside', '/outside']: 689 path.write_text(original.replace('compiler.tri', value)) 690 with self.assertRaisesRegex(ValueError, 'path or digest'): 691 source_rows(self.fixtures) 692 path.write_text(original + original.splitlines()[1] + '\n') 693 with self.assertRaisesRegex(ValueError, 'duplicate'): 694 source_rows(self.fixtures) 695 696 def test_cli_reports_reference_success_but_current_failure(self): 697 import sys 698 self.capture() 699 (self.source / 'compiler.tri').write_bytes(b'edited') 700 base = [sys.executable, str(ROOT / 'test/fixture_sources.py')] 701 common = ['--fixtures', str(self.fixtures), '--source-root', str(self.source)] 702 reference = subprocess.run(base + ['verify'] + common, capture_output=True, text=True) 703 current = subprocess.run(base + ['current'] + common, capture_output=True, text=True) 704 self.assertEqual(reference.returncode, 0, reference.stderr) 705 self.assertIn('freshness NOT checked', reference.stdout) 706 self.assertEqual(current.returncode, 1) 707 self.assertIn('compiler.tri', current.stderr) 708 709 def test_map_change_is_freshness_failure(self): 710 from fixture_sources import current_mismatches 711 self.capture() 712 (self.source / 'bootstrap/compiler-source-map.v1').write_text('arboricx.source-map.v1\n') 713 self.assertEqual(current_mismatches(self.fixtures, self.source), ['bootstrap/compiler-source-map.v1']) 714 715 716 class SupportPublicationTests(unittest.TestCase): 717 def test_patterns_relocation_is_explicit_and_identity_checked(self): 718 from repair_support_fixture import relocate_patterns, updated_module_rows 719 old = ['module\thttp\thttp\tlib/http.tri\told-source'] 720 record = dict(legacy='patterns', path='lib/patterns.tri') 721 candidates = {'patterns': (record, b'!export main\nmain = t\n')} 722 with self.assertRaisesRegex(ValueError, 'membership'): 723 updated_module_rows(old, candidates) 724 relocated = relocate_patterns(old, candidates) 725 self.assertEqual(updated_module_rows(relocated, candidates), relocated) 726 self.assertEqual(old, ['module\thttp\thttp\tlib/http.tri\told-source']) 727 with self.assertRaisesRegex(ValueError, 'requires rebuilt'): 728 relocate_patterns(old, {}) 729 with self.assertRaisesRegex(ValueError, 'identity'): 730 relocate_patterns(old, {'patterns': (dict(record, path='other.tri'), b't')}) 731 with self.assertRaisesRegex(ValueError, 'already belongs'): 732 relocate_patterns(relocated, candidates) 733 734 def test_changed_source_has_a_new_authenticated_fingerprint(self): 735 from repair_support_fixture import updated_module_rows 736 old = ['module\thttp\thttp\tlib/http.tri\told-source', 737 'module\tsocket\tsocket\tlib/socket.tri\toriginal-socket'] 738 record = dict(legacy='http', path='lib/http.tri') 739 payload = b'!export f\nf x = x\n' 740 rows = updated_module_rows(old, {'http': (record, payload)}) 741 self.assertEqual(rows[0].split('\t')[-1], digest('arboricx.source.v1', payload)) 742 self.assertEqual(rows[1], old[1]) 743 self.assertEqual(old[0].split('\t')[-1], 'old-source') 744 with self.assertRaisesRegex(ValueError, 'identity'): 745 updated_module_rows(old, {'http': (dict(record, path='other.tri'), payload)}) 746 with self.assertRaisesRegex(ValueError, 'membership'): 747 updated_module_rows(old, {'unknown': (record, payload)}) 748 749 def test_candidate_replacement_is_explicit_and_complete(self): 750 from repair_support_fixture import select_candidates 751 with tempfile.TemporaryDirectory() as directory: 752 roots = [Path(directory) / str(n) for n in range(2)] 753 for root in roots: 754 root.mkdir() 755 (root / 'migration.json').write_text(json.dumps([dict(alias='http', complete=True)])) 756 with self.assertRaisesRegex(ValueError, 'requires --replace'): 757 select_candidates(roots, []) 758 self.assertEqual(select_candidates(roots, ['http'])['http'][0], roots[1]) 759 with self.assertRaisesRegex(ValueError, 'exactly'): 760 select_candidates(roots[:1], ['http']) 761 (roots[1] / 'migration.json').write_text(json.dumps([dict(alias='http', complete=False)])) 762 with self.assertRaisesRegex(ValueError, 'incomplete'): 763 select_candidates(roots, ['http']) 764 765 def test_receipt_cannot_relabel_source_compiler_or_output(self): 766 from repair_support_fixture import checked_receipt 767 source = b'f x = x\n' 768 payload = ('arboricx.build-receipt.v1\nsource\t' + digest('arboricx.source.v1', source) + 769 '\ncompiler\tcompiler\noutput\tmanifest\noptions\t' + 770 digest('arboricx.build-options.v1', b'source-module.stable.v1') + '\n').encode() 771 checked_receipt(payload, source, 'manifest', 'compiler') 772 for raw, text, manifest, compiler in [ 773 (payload, source + b'-- changed\n', 'manifest', 'compiler'), 774 (payload, source, 'wrong', 'compiler'), 775 (payload, source, 'manifest', 'wrong'), 776 (payload + b'output\tmanifest\n', source, 'manifest', 'compiler'), 777 (payload.replace(digest('arboricx.build-options.v1', b'source-module.stable.v1').encode(), 778 digest('arboricx.build-options.v1', b'source-module.reference.v1').encode()), 779 source, 'manifest', 'compiler'), 780 ]: 781 with self.subTest(manifest=manifest, compiler=compiler), self.assertRaisesRegex(ValueError, 'receipt mismatch'): 782 checked_receipt(raw, text, manifest, compiler) 783 784 def test_old_consumer_receipt_cannot_pin_an_old_support_provider(self): 785 from repair_support_fixture import check_support_dependencies 786 providers = dict(http='current-http', socket='current-socket') 787 rows = [['local', 'http', 'current-http', 'facade-source'], 788 ['pin', 'http', 'current-http', 'parseHeaders', 'tree']] 789 check_support_dependencies(rows, providers) 790 for bad in ([['local', 'http', 'old-http', 'source']], 791 [rows[0], ['pin', 'http', 'old-http', 'parseHeaders', 'tree']], 792 [rows[0], rows[0]], [['local', 'http']], [['pin', 'http']]): 793 with self.subTest(bad=bad), self.assertRaises(ValueError): 794 check_support_dependencies(bad, providers) 795 796 797 class LocalControlHarnessTests(unittest.TestCase): 798 def test_independent_case_diagnostic_and_ast_corpus(self): 799 from local_control import cases, failures, ast_cases 800 rows = cases()+failures()+ast_cases() 801 self.assertEqual(len(rows),len({row[0] for row in rows})) 802 labels = {row[0] for row in rows} 803 for label in ('case-ignored-scrutinee', 'case-unused-loop', 'case-pin-collision', 804 'rec-nested-capture', 'rec-no-mutual', 'plain-let-shadow', 805 'rec-value-before-missing', 'case-ast', 'rec-ast'): 806 self.assertIn(label,labels) 807 from http_headers import expect 808 for _,_,expected in ast_cases(): 809 with self.assertRaises(ValueError): 810 expect(expected+b'\0', expected, 'extra AST field') 811 812 def test_candidate_source_guard_rejects_old_facade_and_source_edits(self): 813 from native_library_migration import checked_candidate_source 814 import tempfile, hashlib 815 from pathlib import Path 816 with tempfile.TemporaryDirectory() as directory: 817 root = Path(directory); workspace = root/'workspace'; workspace.mkdir() 818 source = b'!export value\nvalue = 23\n' 819 (root/'library.tri').write_bytes(source) 820 staged = workspace/'source_ast.tri'; staged.write_bytes(source) 821 record = dict(path='library.tri', alias='source_ast', after_sha256=hashlib.sha256(source).hexdigest()) 822 self.assertEqual(checked_candidate_source(workspace, record, root), source) 823 staged.write_bytes(b'!export * from '+b'0'*64+b'\n') 824 with self.assertRaises(ValueError): checked_candidate_source(workspace, record, root) 825 staged.write_bytes(source); (root/'library.tri').write_bytes(source+b'-- changed\n') 826 with self.assertRaises(ValueError): checked_candidate_source(workspace, record, root) 827 828 829 class RegistryServerHarnessTests(unittest.TestCase): 830 def test_independent_oracles_and_explicit_changes(self): 831 from registry_server import pure_cases, effect_cases, demo_cases 832 rows = pure_cases() + effect_cases() + list(demo_cases()) 833 self.assertEqual(len(rows), len({row.label for row in rows})) 834 self.assertEqual(sum(c.label.startswith('hex-byte-') for c in rows), 256) 835 self.assertEqual(sum(c.label.startswith('hash-invalid-at-') for c in rows), 64) 836 self.assertEqual(sum(c.label.startswith('demo-init-failure-') for c in rows), 7) 837 for c in rows: 838 self.assertIsInstance(c.expected, bytes) 839 self.assertEqual(bool(c.change), c.previous is not None) 840 if c.previous is not None: 841 self.assertNotEqual(c.expected, c.previous) 842 843 def test_traversal_never_requests_filesystem_io(self): 844 import posixpath 845 from registry_server import effect_cases, finished, result, number, bs, DIAGNOSTIC, action, ROOT, path 846 rows = effect_cases() 847 for c in rows: 848 if c.label.startswith('get-reject-'): 849 self.assertEqual(c.expected, finished(result(False, number(400), bs(DIAGNOSTIC)))) 850 if c.label.startswith('write-reject-'): 851 self.assertEqual(c.expected, finished(result(False, bs(b'sha256Hex: '+DIAGNOSTIC)), 852 [action(28, bs(b'body'))])) 853 self.assertEqual(c.previous_export, 'putBundleWithHash') 854 self.assertFalse(posixpath.normpath(path(b'../../../escaped')).startswith(ROOT+b'/')) 855 856 def test_prior_defects_cannot_pass_current_oracles(self): 857 from registry_server import effect_cases, demo_cases 858 from http_pipeline import check_case 859 for c in effect_cases()+list(demo_cases()): 860 if c.previous is not None: 861 check_case(c, c.expected, c.previous) 862 with self.assertRaises(ValueError): 863 check_case(c, c.previous, c.previous) 864 865 def test_wrong_path_and_atomic_operation_are_detected(self): 866 from registry_server import action, bs, pair, finished, result, expect 867 output = result(True, bs(b'hash')) 868 write = action(33, pair(bs(b'/registry/objects/abc/hash.arboricx'), bs(b'data'))) 869 expected = finished(output, [write]) 870 for trace in ([], [action(22, pair(bs(b'/registry/tmp/hash.tmp'), bs(b'data')))], 871 [action(33, pair(bs(b'/outside'), bs(b'data')))], [write, write]): 872 with self.assertRaises(ValueError): 873 expect(finished(output, trace), expected, 'mutated publication') 874 875 876 class HttpPipelineHarnessTests(unittest.TestCase): 877 def test_changes_have_explicit_old_and_independent_new_results(self): 878 from http_pipeline import pure_cases, effect_cases 879 rows = pure_cases() + effect_cases() 880 self.assertEqual(len(rows), len({row.label for row in rows})) 881 changes = {row.label for row in rows if row.previous is not None} 882 self.assertEqual(changes, {'body-eof-', 'body-eof-6162', 'receive-recv-failure', 883 'handler-pure-too-large', 'handler-io-too-large', 'handler-pure-header-limit', 884 'handler-io-header-limit', 'handler-pure-body-eof', 'handler-io-body-eof', 'status-413'}) 885 for row in rows: 886 self.assertIsInstance(row.expected, bytes) 887 if row.previous is not None: 888 self.assertNotEqual(row.expected, row.previous) 889 890 def test_reference_modes_never_disable_subject_oracle(self): 891 from http_pipeline import Case, check_case 892 row = Case('changed', 'probe', (), b'\0', previous=b'\1\0') 893 check_case(row, row.expected, row.previous) 894 check_case(row, row.expected, row.expected, reference_current=True) 895 for actual, before, current in [(row.previous, row.previous, False), 896 (row.previous, row.expected, True), 897 (row.expected, row.previous, True), 898 (row.expected, row.expected, False)]: 899 with self.assertRaises(ValueError): 900 check_case(row, actual, before, current) 901 902 def test_both_router_paths_have_success_failure_and_cleanup_cases(self): 903 from http_pipeline import effect_cases 904 labels = {row.label for row in effect_cases()} 905 for flavor in ('pure', 'io'): 906 for suffix in ('get', 'post', 'head', 'bad-line', 'bad-header-head', 'bad-method', 907 'bad-version-head', 'bad-target-head', 'duplicate', 'coding-head', 908 'too-large', 'header-eof', 'header-limit', 'body-fragmented', 909 'body-recv-error', 'body-eof', 'body-send-error', 'body-close-error'): 910 self.assertIn('handler-' + flavor + '-' + suffix, labels) 911 for control in ('pure', 'extra-script', 'unscripted', 'wrong-tag', 'abort', 'bind', 'fuel'): 912 self.assertIn('driver-' + control, labels) 913 914 def test_trace_oracle_detects_missing_reordered_or_wrong_effect_payloads(self): 915 from http_pipeline import action, finished, number, byte_tree, pair, result, LEAF 916 from http_headers import expect 917 send = action(77, pair(number(7), byte_tree(b'response'))) 918 close = action(71, number(7)) 919 expected = finished(result(True, LEAF), [send, close]) 920 for trace in ([send], [close, send], [send, close, close], 921 [action(77, pair(number(7), byte_tree(b'wrong'))), close]): 922 with self.assertRaises(ValueError): 923 expect(finished(result(True, LEAF), trace), expected, 'trace mutation') 924 925 926 class HttpLengthHarnessTests(unittest.TestCase): 927 def test_length_corpus_covers_bytes_and_policies_without_running_artifacts(self): 928 from http_lengths import cases, BAD_REQUEST 929 rows = cases(include_costs=False) 930 self.assertEqual(len(rows), len({row.label for row in rows})) 931 self.assertEqual(sum(row.label.startswith('value-byte-') for row in rows), 256) 932 self.assertEqual(sum(row.label.startswith('duplicate-') for row in rows), 25) 933 for row in rows: 934 if row.label.startswith(('duplicate-', 'coding-')): 935 self.assertEqual(row.expected, BAD_REQUEST) 936 self.assertFalse(row.compare_reference) 937 self.assertEqual(len(row.arguments), 3 if row.export == 'readBody' else 1) 938 939 def test_rejections_require_complete_pure_action_not_just_a_status(self): 940 from http_lengths import cases, pair, number, BAD_REQUEST, TOO_LARGE 941 from http_headers import expect 942 rows = cases(include_costs=False) 943 by_name = {row.label: row for row in rows} 944 for row in rows: 945 if row.label.startswith('no-io-'): 946 original = by_name[row.label[len('no-io-'):]] 947 self.assertIn(original.expected, (BAD_REQUEST, TOO_LARGE)) 948 self.assertEqual(row.expected, pair(number(0), original.expected)) 949 with self.assertRaises(ValueError): 950 expect(pair(number(1), original.expected), row.expected, row.label) 951 952 def test_reference_comparisons_never_feed_large_valid_numbers_to_old_parser(self): 953 from http_lengths import cases, length_result, BAD_REQUEST, pair, number, result, byte_tree, LEAF 954 safe_results = {BAD_REQUEST, length_result(None), length_result(0), length_result(1), length_result(42), 955 pair(number(0), result(True, LEAF, byte_tree(b'body')))} 956 for row in cases(include_costs=False): 957 if row.compare_reference: 958 self.assertIn(row.expected, safe_results, row.label) 959 960 961 class HttpHeaderHarnessTests(unittest.TestCase): 962 """Cheap oracle/identity controls only; artifact execution stays opt-in.""" 963 964 def test_syntax_changes_have_explicit_reasons_and_independent_oracles(self): 965 from http_headers import cases, syntax_cases 966 old = {case.label: case for case in cases()} 967 rows = syntax_cases() 968 self.assertEqual(len(rows), len({row.label for row in rows})) 969 self.assertEqual(sum(row.label.startswith('name-byte-') for row in rows), 256) 970 self.assertEqual(sum(row.label.startswith('value-byte-') for row in rows), 256) 971 self.assertTrue(any(row.export == 'parseRequestLine' for row in rows)) 972 for row in rows: 973 if row.change: 974 self.assertIsNotNone(row.expected, row.label) 975 if row.label in old: 976 self.assertEqual(row.reference_expected, old[row.label].expected) 977 elif row.label in old: 978 self.assertEqual(row, old[row.label]) 979 980 def test_only_approved_nonbyte_cases_are_retired(self): 981 from http_headers import cases, syntax_cases, retired_case 982 historical = cases() 983 current = {case.label for case in syntax_cases()} 984 retired = {case.label for case in historical if retired_case(case)} 985 self.assertEqual(len(retired), 103) 986 self.assertEqual({case.label for case in historical} - current, retired) 987 self.assertEqual(sum(case.label.startswith('ascii-') for case in syntax_cases()), 256) 988 self.assertIn('headers-shape-5', current) 989 self.assertIn('headers-shape-6', current) 990 991 def test_changed_expectations_do_not_disable_result_or_reference_checks(self): 992 from dataclasses import replace 993 from http_headers import syntax_cases, check_case, LEAF 994 changed = next(row for row in syntax_cases() if row.label == 'bare-lf') 995 check_case(changed, changed.reference_expected, changed.expected) 996 for before, after in [(changed.reference_expected, changed.reference_expected), 997 (LEAF, changed.expected)]: 998 with self.assertRaisesRegex(ValueError, 'complete result differs'): 999 check_case(changed, before, after) 1000 with self.assertRaisesRegex(ValueError, 'independent oracle'): 1001 check_case(replace(changed, expected=None), LEAF, LEAF) 1002 unchanged = next(row for row in syntax_cases() if row.label == 'headers-shape-5') 1003 with self.assertRaisesRegex(ValueError, 'differential'): 1004 check_case(unchanged, LEAF, b'\x01\x00') 1005 with self.assertRaisesRegex(ValueError, 'independent oracle'): 1006 check_case(unchanged, None, LEAF) 1007 1008 def test_independent_tree_encodings(self): 1009 from http_headers import number, pair, tree_list, byte_tree, parse_tree_output 1010 self.assertEqual(number(0), b'\x00') 1011 self.assertEqual(number(1), b'\x02\x01\x00\x00') 1012 self.assertEqual(number(2), b'\x02\x00\x02\x01\x00\x00') 1013 self.assertEqual(pair(b'\x00', b'\x01\x00'), b'\x02\x00\x01\x00') 1014 self.assertEqual(tree_list([number(65)]), byte_tree(b'A')) 1015 self.assertEqual(parse_tree_output(' 2010\n'), b'\x02\x00\x01\x00') 1016 1017 def test_renderer_rejects_partial_or_extra_output(self): 1018 from http_headers import parse_tree_output 1019 for text in ('', '1', '20', '00', '0 error', '3', '0\n0'): 1020 with self.subTest(text=text), self.assertRaises(ValueError): 1021 parse_tree_output(text) 1022 1023 def test_corpus_is_named_and_policy_characterization_is_explicit(self): 1024 from http_headers import cases, EXPORTS 1025 rows = cases() 1026 self.assertEqual(len(rows), len({row.label for row in rows})) 1027 self.assertEqual({row.export for row in rows}, set(EXPORTS)) 1028 self.assertEqual(sum(row.label.startswith('ascii-') for row in rows), 256) 1029 for row in rows: 1030 self.assertEqual(row.expected is None, row.category == 'differential-only') 1031 by_name = {row.label: row for row in rows} 1032 for label in ('embedded-cr', 'bare-lf', 'space-name', 'eof-header', 'duplicates'): 1033 # Duplicate preservation is ordinary parser behavior; deciding how 1034 # particular headers interact belongs to a later semantic slice. 1035 expected = 'independent' if label == 'duplicates' else 'characterization' 1036 self.assertEqual(by_name[label].category, expected) 1037 1038 def test_complete_result_comparison_rejects_wrong_fields(self): 1039 from http_headers import cases, expect, headers, reply, diagnostic 1040 by_name = {row.label: row for row in cases()} 1041 wrong = [ 1042 ('ascii-065', by_name['ascii-064'].expected), 1043 ('blank', headers([], b'wrong remainder')), 1044 ('reply-missing-colon', reply(False, diagnostic(1, b'colon'), consumed=False)), 1045 ('reply-missing-colon', reply(False, diagnostic(2, b'colon'))), 1046 ] 1047 for label, value in wrong: 1048 with self.subTest(label=label), self.assertRaisesRegex(ValueError, 'complete result differs'): 1049 expect(by_name[label].expected, value, label) 1050 1051 def test_reference_integrity_and_recorded_source(self): 1052 from http_headers import reference, ROOT 1053 bundle, metadata = reference(ROOT / 'test/fixtures') 1054 self.assertTrue(bundle.is_file()) 1055 self.assertEqual(metadata['source_path'], 'lib/http.tri') 1056 1057 def test_corrupt_reference_metadata_bundle_and_source_fail(self): 1058 import shutil 1059 from http_headers import reference, ROOT 1060 fixtures = ROOT / 'test/fixtures' 1061 metadata = json.loads((fixtures / 'http-headers-reference.json').read_text()) 1062 paths = ['http-headers-reference.json', 'http-headers-reference.arboricx', 1063 'reference-source/' + metadata['source_digest'] + '.tri'] 1064 for damaged in paths: 1065 with self.subTest(damaged=damaged), tempfile.TemporaryDirectory() as directory: 1066 destination = Path(directory) 1067 for path in paths: 1068 (destination / path).parent.mkdir(parents=True, exist_ok=True) 1069 shutil.copyfile(fixtures / path, destination / path) 1070 with (destination / damaged).open('ab') as output: 1071 output.write(b'corrupt') 1072 with self.assertRaises(ValueError): 1073 reference(destination) 1074 1075 1076 class ArboricxReaderOracleTests(unittest.TestCase): 1077 def test_cases_have_unique_names_and_independent_truncation_oracles(self): 1078 from arboricx_readers import cases 1079 rows = list(cases()) 1080 self.assertEqual(len(rows), 401) 1081 self.assertEqual(len({r[1] for r in rows}), len(rows)) 1082 for module in ['common', 'nodes', 'manifest']: 1083 self.assertTrue(any(r[0] == module and '-cut-' in r[1] and r[4] is not None for r in rows)) 1084 1085 def test_binary_failure_keeps_field_start_not_consumed_prefix(self): 1086 from arboricx_readers import Reader, Failure 1087 from http_headers import result, number 1088 from stable_bundle import byte_tree 1089 r = Reader(b'xx') 1090 r.take(1) 1091 with self.assertRaises(Failure) as caught: 1092 r.take(2) 1093 self.assertEqual(caught.exception.tree, result(False, number(1), byte_tree(b'x'))) 1094 1095 1096 class SocketConvenienceOracleTests(unittest.TestCase): 1097 def test_only_post_acquisition_failures_change(self): 1098 from socket_conveniences import cases 1099 rows = list(cases()) 1100 self.assertEqual(len(rows), 14) 1101 self.assertEqual(len({r.label for r in rows}), len(rows)) 1102 changed = [r for r in rows if r.previous is not None] 1103 self.assertEqual(len(changed), 6) 1104 self.assertTrue(all('failure-close-' in r.label and r.previous != r.expected for r in changed)) 1105 1106 1107 class ParserControlOracleTests(unittest.TestCase): 1108 def test_complete_reply_and_poison_probe_inventory(self): 1109 from parser_control import cases 1110 rows = list(cases()) 1111 self.assertEqual(len(rows), 252) 1112 self.assertEqual(len({r[0] for r in rows}), len(rows)) 1113 self.assertEqual(sum('ignored-fields-' in r[0] for r in rows), 4) 1114 self.assertTrue(any(r[0] == 'unselected-failure' for r in rows)) 1115 self.assertTrue(any(r[0] == 'unselected-success' for r in rows)) 1116 1117 1118 1119 class BaseWorkloadTests(unittest.TestCase): 1120 def test_generated_worker_variants(self): 1121 from base_workloads import candidate, WORKERS, reference_source 1122 source = reference_source() 1123 for variant in ['manual', 'direct', 'patterned', 'captured']: 1124 text = candidate(source, variant) 1125 for public in WORKERS: 1126 self.assertIn(public + ' ', text) 1127 if variant != 'manual': 1128 self.assertNotRegex(text, r'\bself\b') 1129 self.assertIn('map f l = (let rec', candidate(source, 'captured')) 1130 self.assertIn('foldl f x l = (let rec', candidate(source, 'captured')) 1131 self.assertIn('reverse_ other acc = other', candidate(source, 'patterned')) 1132 1133 def test_large_rendered_result_before_rts_residency(self): 1134 from base_workloads import measurements 1135 with tempfile.TemporaryDirectory() as d: 1136 path = Path(d) / 'sample.log' 1137 rows = [dict(profile=name, wall_ns=i * 100, allocated_bytes=i * 10, 1138 gc_elapsed_ns=i, live_bytes=7) 1139 for i, name in enumerate(['load-begin', 'load-end', 1140 'invoke-begin-1', 'invoke-end-1', 1141 'observe-begin-1', 'observe-end-1'])] 1142 # Must not perform an unanchored numeric regex search over this line. 1143 path.write_text('\n'.join(json.dumps(r) for r in rows) + '\n' + 1144 '0' * 200000 + ' 9,999 bytes allocated in the heap\n' + 1145 ' 1,234 bytes maximum residency (2 samples)\n') 1146 actual = measurements(path) 1147 self.assertEqual(actual['rts_max_residency_bytes'], 1234) 1148 self.assertEqual(actual['invoke_wall_ns'], 300) 1149 self.assertEqual(actual['invoke_allocated_bytes'], 30) 1150 self.assertEqual(actual['measurement_scope'], 'invocation-through-rendered-output-v1') 1151 1152 def test_workload_independent_oracles(self): 1153 from base_workloads import workloads, encode 1154 rows = {name: (argument, expected) for name, argument, expected in workloads(1024)} 1155 data = bytes(rows['lower'][0]) 1156 self.assertEqual(bytes(rows['lower'][1]), data.lower()) 1157 self.assertEqual(rows['splitLines'][1], [list(x) for x in data.split(b'\n')]) 1158 self.assertEqual(rows['sumRows'][1], [sum(x) for x in rows['sumRows'][0]]) 1159 self.assertEqual(len(rows['prefixDigits'][1]), 20) 1160 self.assertEqual(encode([0, 1]), b'\2\0\2\2\1\0\0\0') 1161 1162 def test_summary_discards_warmup(self): 1163 from base_workloads import summarize 1164 fields = ['invoke_wall_ns', 'invoke_allocated_bytes', 'invoke_gc_elapsed_ns', 1165 'load_wall_ns', 'load_allocated_bytes', 'post_invoke_live_bytes', 1166 'rts_max_residency_bytes', 'peak_kib', 'seconds'] 1167 rows = [dict(variant='manual', workload='lower', size=64, warmup=(v == 999), 1168 **{k: v for k in fields}) for v in [999, 2, 4, 8]] 1169 result = summarize(rows)[0] 1170 self.assertEqual(result['repetitions'], 3) 1171 self.assertEqual(result['invoke_wall_ns'], dict(median=4, minimum=2, maximum=8)) 1172 1173 1174 1175 class BaseReadableOracleTests(unittest.TestCase): 1176 def test_small_bounded_inventory(self): 1177 from base_readable import cases, edges 1178 from source_order import end 1179 rows = list(edges()) 1180 self.assertEqual(len(rows), 30) 1181 self.assertEqual(len({row[0] for row in rows}), 30) 1182 for _, _, argument, expected in rows: 1183 self.assertEqual(end(argument), len(argument)) 1184 self.assertEqual(end(expected), len(expected)) 1185 for size in [64, 1024]: 1186 workloads = list(cases(size)) 1187 self.assertEqual(len(workloads), 8) 1188 for _, argument, expected in workloads: 1189 self.assertEqual(end(argument), len(argument)) 1190 self.assertEqual(end(expected), len(expected)) 1191 1192 1193 class CollectionBatchTests(unittest.TestCase): 1194 def test_oracles_are_complete_trees_with_unique_short_labels(self): 1195 from collections_batch import cases, effects 1196 from source_order import end 1197 rows = cases() + effects() 1198 self.assertEqual(len({row[0] for row in rows}), len(rows)) 1199 for label, _, argument, expected in rows: 1200 self.assertLess(len(label), 120) 1201 self.assertEqual(end(argument), len(argument)) 1202 self.assertEqual(end(expected), len(expected)) 1203 1204 def test_workspace_audit_rejects_changed_sources_and_objects(self): 1205 from collections_batch import audit_workspace 1206 with tempfile.TemporaryDirectory() as directory: 1207 workspace, store, compiler, tree, restore = self.make_workspace(Path(directory)) 1208 self.assertEqual(set(audit_workspace(workspace, store, compiler)), {'a', 'b'}) 1209 source = workspace/'a.tri' 1210 source.write_text('!export value\nvalue = t t\n') 1211 with self.assertRaisesRegex(ValueError, 'source'): 1212 audit_workspace(workspace, store, compiler) 1213 source.write_text('!export value\nvalue = t\n') 1214 (store/'objects'/tree[:3]/tree).write_bytes(b'corrupt') 1215 with self.assertRaisesRegex(AssertionError, 'object digest'): 1216 audit_workspace(workspace, store, compiler) 1217 1218 def test_workspace_audit_rejects_stale_dependency_and_export_pins(self): 1219 from collections_batch import audit_workspace 1220 with tempfile.TemporaryDirectory() as directory: 1221 workspace, store, compiler, tree, restore = self.make_workspace(Path(directory)) 1222 restore(stale_receipt=True) 1223 with self.assertRaisesRegex(AssertionError, 'stale candidate dependency'): 1224 audit_workspace(workspace, store, compiler) 1225 restore(stale_pin=True) 1226 with self.assertRaisesRegex(AssertionError, 'stale candidate export pin'): 1227 audit_workspace(workspace, store, compiler) 1228 1229 @staticmethod 1230 def make_workspace(root): 1231 workspace, store = root/'workspace', root/'store' 1232 workspace.mkdir();(store/'cache/source-build').mkdir(parents=True) 1233 compiler = 'a'*64 1234 def obj(domain, payload): 1235 pin = digest(domain, payload) 1236 path = store/'objects'/pin[:3]/pin;path.parent.mkdir(parents=True, exist_ok=True) 1237 path.write_bytes(payload);return pin 1238 tree = obj('arboricx.tree-term.v1', b'\0') 1239 sources = {'a': '!export value\nvalue = t\n', 1240 'b': '!import a *\n!export main\nmain = value\n'} 1241 manifests = {alias: obj('arboricx.module-manifest.v1', 1242 ('arboricx.module-manifest.v1\nexport\t'+name+'\tarboricx.tree-term.v1\t'+tree+'\tarboricx.abi.tree.v1\n').encode()) 1243 for alias, name in [('a', 'value'), ('b', 'main')]} 1244 for alias, source in sources.items(): 1245 (workspace/(alias+'.tri')).write_text(source) 1246 def receipt(alias, extra=''): 1247 payload = ('arboricx.build-receipt.v1\nsource\t'+digest('arboricx.source.v1', sources[alias].encode())+ 1248 '\ncompiler\t'+compiler+'\noptions\t'+digest('arboricx.build-options.v1', b'source-module.stable.v1')+ 1249 '\noutput\t'+manifests[alias]+'\n'+extra).encode() 1250 pin = obj('arboricx.build-receipt.v1', payload) 1251 (store/'cache/source-build'/alias).write_text('arboricx.source-build-cache.v1\t'+'0'*64+'\t'+manifests[alias]+'\t'+pin) 1252 return pin 1253 a_receipt = receipt('a') 1254 def restore(stale_receipt=False, stale_pin=False): 1255 receipt('b', 'local\ta\t'+manifests['a']+'\t'+('b'*64 if stale_receipt else a_receipt)+ 1256 '\npin\ta\t'+manifests['a']+'\tvalue\t'+('c'*64 if stale_pin else tree)+'\n') 1257 restore() 1258 return workspace, store, compiler, tree, restore 1259 1260 1261 class NumericBatchTests(unittest.TestCase): 1262 def test_independent_numeric_oracles_are_complete_trees(self): 1263 from numeric_batch import cases 1264 from source_order import end 1265 rows = cases() 1266 self.assertEqual(len({row[0] for row in rows}), len(rows)) 1267 for label, _, argument, expected in rows: 1268 self.assertLess(len(label), 120) 1269 self.assertEqual(end(argument), len(argument)) 1270 self.assertEqual(end(expected), len(expected)) 1271 1272 def test_zero_divisor_and_shape_oracles_are_distinct(self): 1273 from numeric_batch import cases, error, n, pair, tree_list 1274 rows = cases() 1275 checked = next(expected for _, name, argument, expected in rows 1276 if name == 'checkedBinary' and argument == pair(n(7), n(0))) 1277 self.assertTrue(checked.endswith(tree_list([error('non-zero')]*3))) 1278 self.assertNotEqual(error('non-zero'), n(0)) 1279 self.assertNotEqual(error('not a natural number'), error('non-zero')) 1280 1281 1282 class BundleBatchTests(unittest.TestCase): 1283 def test_complete_independent_oracles(self): 1284 from bundle_batch import cases 1285 from source_order import end 1286 rows = cases() 1287 self.assertEqual(len(rows), len({row[0] for row in rows})) 1288 for label, _, _, arguments, expected in rows: 1289 self.assertLess(len(label), 120) 1290 for tree in arguments + [expected]: 1291 self.assertEqual(end(tree), len(tree)) 1292 1293 def test_suffix_and_rejection_controls_remain_present(self): 1294 from bundle_batch import cases, byte_tree, n 1295 from source_order import field 1296 rows = cases() 1297 self.assertTrue(any(export == 'prefix' and field(expected, 1, 1) == byte_tree(b'TAIL') 1298 for _, _, export, _, expected in rows)) 1299 for code in [7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]: 1300 self.assertTrue(any(expected.startswith(b'\2\0\2') and field(expected, 1, 0) == n(code) 1301 for _, _, _, _, expected in rows), code) 1302 1303 1304 class ToolingBatchTests(unittest.TestCase): 1305 def test_complete_models_and_graph_ready_order(self): 1306 from tooling_batch_models import cases, graph_result, n, result 1307 from source_order import end 1308 # A newly ready earlier entry precedes the already-ready later one. 1309 self.assertEqual(graph_result([(0,[1]),(1,[]),(2,[])]), result(True,n([1,0,2]))) 1310 self.assertEqual(graph_result([(0,[1]),(1,[2]),(2,[1])]), result(False,n(1))) 1311 rows=cases() 1312 self.assertEqual(len(rows),len({row[0] for row in rows})) 1313 for _,_,argument,expected in rows: 1314 for tree in [argument,expected]:self.assertEqual(end(tree),len(tree)) 1315 1316 def test_formatter_oracles_preserve_tokens_and_line_counts(self): 1317 from tooling_batch_models import format_cases 1318 import re 1319 tokens=re.compile(r'"(?:\\.|[^"\\])*"|\|-.*?-\||--[^\r\n]*|[^\s]',re.S) 1320 for label,source,expected in format_cases(): 1321 self.assertEqual(tokens.findall(source),tokens.findall(expected),label) 1322 self.assertEqual(source.count('\n'),expected.count('\n'),label) 1323 1324 1325 class SourceFoundationOracleTests(unittest.TestCase): 1326 def test_corpus_has_complete_trees_and_unique_labels(self): 1327 from source_foundations_models import corpus 1328 from source_order import end 1329 rows = corpus(consumers=True) 1330 self.assertEqual(len(rows), len({row[0] for row in rows})) 1331 for label, _, argument, expected in rows: 1332 self.assertEqual(end(argument), len(argument), label) 1333 self.assertEqual(end(expected), len(expected), label) 1334 1335 def test_shift_preserves_bound_indices_and_opaque_trees(self): 1336 from source_foundations_models import Node, shift, substitute 1337 bound = Node(4, Node(2, 0)) 1338 free = Node(4, Node(2, 1)) 1339 opaque = Node(5, free.tree()) 1340 self.assertEqual(shift(bound, 7), bound) 1341 self.assertEqual(shift(free, 7), Node(4, Node(2, 8))) 1342 self.assertEqual(shift(opaque, 7), opaque) 1343 self.assertEqual(substitute(free, [Node(2, 3)], 5), Node(4, Node(2, 4))) 1344 self.assertEqual(shift(free, 2), shift(shift(free))) 1345 1346 1347 class CandidateResourceTests(unittest.TestCase): 1348 def test_memory_selection_reaches_heap_and_rss_guards(self): 1349 from collections_batch import ProbeRunner 1350 from unittest.mock import patch 1351 with tempfile.TemporaryDirectory() as directory: 1352 root = Path(directory) 1353 for selected in [8192, 65536]: 1354 runner = ProbeRunner(root/'tricu', root, 60, memory_mib=selected) 1355 with patch('source_order.run_guarded', return_value=('', 123)) as run: 1356 runner.run('probe', ['run'], 30) 1357 command = run.call_args.args[0] 1358 self.assertIn(f'-M{selected}m', command) 1359 self.assertEqual(run.call_args.kwargs['memory_mib'], selected) 1360 self.assertLessEqual(run.call_args.kwargs['seconds'], 30) 1361 self.assertEqual(ProbeRunner(root/'tricu', root, 60).memory_mib, 8192) 1362 1363 def test_memory_selection_rejects_out_of_policy_limits(self): 1364 from source_order import Runner 1365 for value in [-1, 0, 65537]: 1366 with self.assertRaises(ValueError): 1367 Runner(Path('tricu'), Path('.'), memory_mib=value) 1368 1369 1370 class GrammarOracleTests(unittest.TestCase): 1371 def test_authored_replies_and_lookahead_vectors_are_whole_trees(self): 1372 from source_grammar_models import authored, lookahead_rows 1373 from source_order import end 1374 cases = authored() 1375 self.assertEqual(len(cases), len({row[0] for row in cases})) 1376 for label, _, argument, expected in cases: 1377 self.assertEqual(end(argument), len(argument), label) 1378 self.assertEqual(end(expected), len(expected), label) 1379 for row in lookahead_rows(): 1380 for tree in row: 1381 self.assertEqual(end(tree), len(tree)) 1382 1383 def test_lookahead_boundaries_do_not_consume_or_guess_assignments(self): 1384 from source_grammar_models import lookahead 1385 self.assertTrue(lookahead([2, 2, 5], [1, 3, 5])[0][0]) 1386 self.assertFalse(lookahead([2, 6, 5], [1, 2, 3])[0][0]) 1387 self.assertFalse(lookahead([2, 6, 11], [1, 2, 3])[0][1]) 1388 self.assertTrue(lookahead([2, 3, 6, 4, 11], [1]*5)[0][1]) 1389 self.assertFalse(lookahead([2, 12, 11], [1]*3)[0][1]) 1390 self.assertEqual(lookahead([6, 2, 11], [1, 3, 5])[1], 1391 [True, True, True, False, True, True]) 1392 1393 1394 class ResolutionOracleTests(unittest.TestCase): 1395 def test_models_emit_complete_trees(self): 1396 from source_resolution_models import (ast_cases, scope_cases, pattern_cases, 1397 resolve_result, free, pattern_result, graph_cases, graph_model) 1398 from source_order import end 1399 for node, environment in ast_cases(): 1400 for tree in [node.tree(), resolve_result(node, environment)]: 1401 self.assertEqual(end(tree), len(tree)) 1402 for node in scope_cases(): 1403 for name in free(node): 1404 self.assertEqual(name.kind, 2) 1405 self.assertEqual(end(name.tree()), len(name.tree())) 1406 for node, binders, environment in pattern_cases(): 1407 tree = pattern_result(node, binders, environment) 1408 self.assertEqual(end(tree), len(tree)) 1409 for definitions, environment, final in graph_cases(): 1410 for tree in graph_model(definitions, environment, final): 1411 self.assertEqual(end(tree), len(tree)) 1412 1413 def test_scope_and_duplicate_priority(self): 1414 from source_resolution_models import Ast, free, resolved, graph_model 1415 from source_order import field, number 1416 name = Ast(2, 'x', 9) 1417 self.assertEqual(free(Ast(9, ('x', name, name))), [name]) 1418 self.assertEqual(free(Ast(15, ('x', name, name))), []) 1419 bound = resolved(name, [], ('y', 'x', 'x')) 1420 self.assertEqual(number(field(bound, 1, 0)), 1) 1421 definitions = [(Ast(2, 'x', 1), Ast(2, 'missing', 2)), 1422 (Ast(2, 'x', 3), Ast(1))] 1423 ordered, analyzed = graph_model(definitions, [], Ast(2, 'absent')) 1424 self.assertEqual(number(field(ordered, 1, 0, 0)), 30) 1425 self.assertEqual(number(field(analyzed, 1, 0, 0)), 20) 1426 1427 1428 class LoweringOracleTests(unittest.TestCase): 1429 def test_identity_and_constant(self): 1430 from source_lowering_models import tc_apply, I, K, n, LEAF 1431 for value in [LEAF, n(23), I]: 1432 self.assertEqual(tc_apply(I, value), value) 1433 self.assertEqual(tc_apply(tc_apply(K, value), n(99)), value) 1434 1435 def test_complete_residual_models(self): 1436 from source_lowering_models import specimens, bracket, eliminate, run_result 1437 from source_order import end 1438 terms, cores = specimens() 1439 for tree in [bracket(t).tree() for t in terms] + [eliminate(c).tree() for c in cores] + [run_result(t) for t in terms]: 1440 self.assertEqual(end(tree), len(tree)) 1441 1442 1443 class ModuleOracleTests(unittest.TestCase): 1444 def test_roundtrip_and_whole_expectations(self): 1445 from source_modules import tree_cases, decode, manifest_cases, header_cases 1446 from source_order import end 1447 from http_headers import result 1448 for tree in tree_cases(): 1449 self.assertEqual(decode(list(tree), True), result(True, tree)) 1450 for text, expected in manifest_cases() + header_cases(): 1451 self.assertEqual(end(expected), len(expected)) 1452 1453 1454 class BuildOracleTests(unittest.TestCase): 1455 def test_bundle_layout_and_missing_dependency(self): 1456 from source_building import bundle_bytes, map_cases 1457 from stable_bundle import sections 1458 from source_order import field 1459 from http_headers import LEAF 1460 with tempfile.TemporaryDirectory() as directory: 1461 path = Path(directory) / 'golden.arboricx' 1462 path.write_bytes(bundle_bytes([('x', LEAF), ('y', LEAF)])) 1463 self.assertEqual(len(sections(path)), 2) 1464 text, outcome = next((text, outcome) for text, outcome in map_cases() if '\tmissing\n' in text) 1465 self.assertEqual(field(outcome, 0), LEAF) 1466 1467 1468 class BackendDefaultTests(unittest.TestCase): 1469 def test_historical_provenance_is_not_reinterpreted(self): 1470 from generation_backend import DEFAULT_BACKEND, recorded_backend 1471 self.assertEqual(DEFAULT_BACKEND, 'stable') 1472 self.assertEqual(recorded_backend({}), 'optimized') 1473 for backend in ['stable', 'optimized']: 1474 self.assertEqual(recorded_backend({'backend': backend}), backend) 1475 with self.assertRaises(ValueError): 1476 recorded_backend({'backend': 'unknown'}) 1477 1478 def test_real_runner_parsers_default_stable_and_keep_overrides(self): 1479 # Stop at parse_args: exercise the actual CLI declarations without 1480 # reading artifacts, spawning hosts or building source in the fast gate. 1481 import argparse 1482 import runpy 1483 from unittest.mock import patch 1484 1485 class Parsed(Exception): 1486 pass 1487 1488 parse = argparse.ArgumentParser.parse_args 1489 scripts = { 1490 'bootstrap_generations.py': ['previous', 'output'], 1491 'bootstrap_generation_cli.py': ['previous'], 1492 'native_library_migration.py': ['tricu', 'generation', 'output', '--library', 'socket'], 1493 'portable_cli.py': ['tricu'], 1494 'source_equations.py': ['tricu', '--generation', 'generation'], 1495 } 1496 for script, required in scripts.items(): 1497 for override in [None, 'stable', 'optimized']: 1498 with self.subTest(script=script, override=override): 1499 captured = [] 1500 argv = required + (['--backend', override] if override else []) 1501 1502 def intercept(parser, *args, **kwargs): 1503 captured.append(parse(parser, argv)) 1504 raise Parsed() 1505 1506 with patch.object(argparse.ArgumentParser, 'parse_args', intercept): 1507 with self.assertRaises(Parsed): 1508 runpy.run_path(str(Path(__file__).parent / script), run_name='__main__') 1509 self.assertEqual(captured[0].backend, override or 'stable') 1510 1511 1512 class BootstrapKitTests(unittest.TestCase): 1513 def setUp(self): 1514 from bootstrap_inputs import SCHEMA, ROLES 1515 self.temporary = tempfile.TemporaryDirectory() 1516 self.addCleanup(self.temporary.cleanup) 1517 self.directory = Path(self.temporary.name) 1518 self.metadata = dict(schema=SCHEMA, backend='stable', artifacts={}) 1519 for role, filename in ROLES.items(): 1520 payload = (role + '-opaque-test-bytes').encode() 1521 (self.directory / filename).write_bytes(payload) 1522 self.metadata['artifacts'][role] = dict(file=filename, 1523 export='main' if role == 'adapter' else 'sourceMapBuildStable', 1524 sha256=hashlib.sha256(payload).hexdigest()) 1525 self.save() 1526 1527 def save(self): 1528 (self.directory / 'bootstrap-kit.json').write_text(json.dumps(self.metadata)) 1529 1530 def load(self, backend='stable'): 1531 from bootstrap_inputs import load_kit 1532 return load_kit(self.directory, backend) 1533 1534 def test_captures_exact_manifest_and_artifacts(self): 1535 raw, payloads, pin = self.load() 1536 self.assertEqual(raw, (self.directory / 'bootstrap-kit.json').read_bytes()) 1537 self.assertEqual(pin, hashlib.sha256(raw).hexdigest()) 1538 (self.directory / 'rebuilt-compiler.arboricx').write_bytes(b'changed') 1539 self.assertEqual(payloads['compiler'], b'compiler-opaque-test-bytes') 1540 with self.assertRaisesRegex(ValueError, 'digest mismatch'): self.load() 1541 1542 def test_backend_is_explicit(self): 1543 with self.assertRaisesRegex(ValueError, 'backend mismatch'): self.load('optimized') 1544 self.metadata.pop('backend'); self.save() 1545 with self.assertRaisesRegex(ValueError, 'backend mismatch'): self.load() 1546 1547 def test_rejects_invalid_manifest_schema(self): 1548 for schema in ('old', None, 1): 1549 with self.subTest(schema=schema): 1550 self.metadata['schema'] = schema; self.save() 1551 with self.assertRaisesRegex(ValueError, 'schema'): self.load() 1552 1553 def test_requires_both_roles_and_no_extra_role(self): 1554 adapter = self.metadata['artifacts'].pop('adapter'); self.save() 1555 with self.assertRaisesRegex(ValueError, 'exactly'): self.load() 1556 self.metadata['artifacts']['adapter'] = adapter 1557 self.metadata['artifacts']['extra'] = adapter; self.save() 1558 with self.assertRaisesRegex(ValueError, 'exactly'): self.load() 1559 1560 def test_rejects_path_traversal_and_role_swap(self): 1561 for filename in ('../elsewhere', '/tmp/elsewhere', 'io-adapter.arboricx'): 1562 with self.subTest(filename=filename): 1563 self.metadata['artifacts']['compiler']['file'] = filename; self.save() 1564 with self.assertRaisesRegex(ValueError, 'filename'): self.load() 1565 1566 def test_rejects_wrong_export(self): 1567 self.metadata['artifacts']['compiler']['export'] = 'sourceMapBuild'; self.save() 1568 with self.assertRaisesRegex(ValueError, 'export'): self.load() 1569 1570 def test_rejects_invalid_digest(self): 1571 for pin in ('a' * 63, 'A' * 64, '../x', None): 1572 with self.subTest(pin=pin): 1573 self.metadata['artifacts']['adapter']['sha256'] = pin; self.save() 1574 with self.assertRaisesRegex(ValueError, 'digest'): self.load() 1575 1576 def test_missing_or_corrupt_adapter_fails(self): 1577 path = self.directory / 'io-adapter.arboricx' 1578 path.write_bytes(b'corrupt') 1579 with self.assertRaisesRegex(ValueError, 'digest mismatch'): self.load() 1580 path.unlink() 1581 with self.assertRaises(FileNotFoundError): self.load() 1582 1583 1584 class IndexedObjectAuditTests(unittest.TestCase): 1585 def test_independent_writer_and_complete_audit(self): 1586 from native_indexed_store import indexed_bytes 1587 from executable_object import verify_payload, INDEXED, FLAT 1588 for tree in [b'\0', b'\1\0', b'\2\1\0\2\0\0']: 1589 verify_payload(FLAT, tree) 1590 data = indexed_bytes(tree) 1591 verify_payload(INDEXED, data) 1592 for at in [0, 8, 15, 31, 32, 95, 96, len(data) - 1]: 1593 changed = bytearray(data); changed[at] ^= 255 1594 with self.subTest(tree=tree, at=at): 1595 with self.assertRaises((AssertionError, ValueError)): 1596 verify_payload(INDEXED, bytes(changed)) 1597 for changed in [data[:-1], data + b'\0']: 1598 with self.assertRaises(AssertionError): verify_payload(INDEXED, changed) 1599 with self.assertRaises(AssertionError): verify_payload('wrong.kind', b'\0') 1600 1601 1602 class InspectorStagingTests(unittest.TestCase): 1603 def test_staging_is_opt_in_immutable_and_authenticates_bytes(self): 1604 from library_inspector import stage_site 1605 with tempfile.TemporaryDirectory() as temporary: 1606 root = Path(temporary) 1607 (root / 'producer').mkdir() 1608 site = root / 'site'; (site / 'bundles').mkdir(parents=True) 1609 index = site / 'packages.json' 1610 index.write_text(json.dumps(dict(schema=1, releases=[]))) 1611 payload = root / 'producer/tricu-base.arboricx' 1612 payload.write_bytes(b'fixture bytes') 1613 digest = hashlib.sha256(payload.read_bytes()).hexdigest() 1614 report = dict(complete=True, releases={'tricu-base': dict(version='0.1.0', manifest='a'*64, bundle=digest)}) 1615 with self.assertRaises(ValueError): stage_site(root, site, dict(report, complete=False)) 1616 stage_site(root, site, report) 1617 before = index.read_bytes(); stage_site(root, site, report) 1618 self.assertEqual(index.read_bytes(), before) 1619 self.assertEqual((site / 'bundles' / (digest + '.arboricx')).read_bytes(), payload.read_bytes()) 1620 payload.write_bytes(b'changed bytes') 1621 with self.assertRaises(ValueError): stage_site(root, site, report) 1622 changed = hashlib.sha256(payload.read_bytes()).hexdigest() 1623 report['releases']['tricu-base']['bundle'] = changed 1624 with self.assertRaises(ValueError): stage_site(root, site, report) 1625 self.assertEqual(index.read_bytes(), before) 1626 self.assertFalse((site / 'bundles' / (changed + '.arboricx')).exists()) 1627 report['releases']['tricu-base']['version'] = '0.2.0' 1628 stage_site(root, site, report) 1629 self.assertEqual(len(json.loads(index.read_text())['releases']), 2) 1630 self.assertTrue((site / 'bundles' / (digest + '.arboricx')).exists()) 1631 1632 1633 if __name__ == '__main__': 1634 unittest.main()