import io import json import tempfile import unittest from pathlib import Path from h8536.report_source_trace import analyze_report_sources, format_text_report, main, write_report_sources def ins( address: int, mnemonic: str, operands: str = "", *, text: str | None = None, targets: list[int] | None = None, block: int | None = None, ) -> dict[str, object]: row: dict[str, object] = { "address": address, "mnemonic": mnemonic, "operands": operands, "text": text or f"{mnemonic} {operands}".strip(), "kind": "call" if mnemonic in {"BSR", "JSR"} else "normal", "targets": targets or [], "references": [], } if block is not None: row["dataflow"] = {"block": block} return row def payload() -> dict[str, object]: return { "call_graph": { "nodes": [ {"start": 0x1000, "end": 0x10FF, "label": "loc_1000"}, {"start": 0x2000, "end": 0x20FF, "label": "loc_2000"}, {"start": 0x3000, "end": 0x30FF, "label": "loc_3000"}, ], }, "instructions": [ ins(0x1000, "MOV:E.B", "#H'80, R2", block=0x1000), ins(0x1002, "MOV:I.W", "#H'0007, R3", block=0x1000), ins(0x1005, "BSR", "loc_3E54", targets=[0x3E54], block=0x1000), ins(0x2000, "MOV:I.W", "#H'0012, R5", block=0x2000), ins(0x2003, "CMP:G.W", "@(-H'2000,R5), R1", block=0x2000), ins(0x2007, "MOV:E.B", "#H'80, R2", block=0x2000), ins(0x2009, "MOV:G.W", "R5, R3", block=0x2000), ins(0x200B, "BSR", "loc_3E54", targets=[0x3E54], block=0x2000), ins(0x3000, "MOV:E.B", "#H'00, R2", block=0x3000), ins(0x3002, "MOV:I.W", "#H'0007, R3", block=0x3000), ins(0x3005, "BSR", "loc_3E54", targets=[0x3E54], block=0x3000), ], } class ReportSourceTraceTest(unittest.TestCase): def test_finds_direct_static_report_index_0007(self): analysis = analyze_report_sources(payload()) self.assertEqual(analysis["summary"]["direct_call_count"], 3) self.assertEqual(analysis["summary"]["direct_static_hit_count"], 1) hit = analysis["calls"][0] self.assertTrue(hit["can_directly_enqueue_report_index"]) self.assertEqual(hit["r2"]["bit7"], True) self.assertEqual(hit["r3"]["classification"], "constant") self.assertEqual(hit["r3"]["value"], 0x0007) self.assertIn("Direct static enqueue source", hit["assessment"]) def test_classifies_table_context_and_clear_gate(self): analysis = analyze_report_sources(payload()) dynamic = analysis["calls"][1] gated_off = analysis["calls"][2] self.assertEqual(dynamic["r3"]["classification"], "constant") self.assertEqual(dynamic["r3"]["value"], 0x0012) self.assertEqual(dynamic["table_hints"][0]["table"], "primary_value_table_candidate") self.assertFalse(gated_off["can_directly_enqueue_report_index"]) self.assertEqual(gated_off["r2"]["bit7"], False) self.assertIn("would not enqueue", gated_off["assessment"]) def test_text_report_mentions_conclusion_and_caveats(self): text = format_text_report(analyze_report_sources(payload())) self.assertIn("loc_3E54 Report Source Trace", text) self.assertIn("Direct static 0x0007 hits: 1", text) self.assertIn("Indirect dispatch", text) self.assertIn("R3 evidence", text) def test_cli_json_output_and_out_file(self): with tempfile.TemporaryDirectory() as tmp: input_path = Path(tmp) / "rom.json" output_path = Path(tmp) / "sources.json" input_path.write_text(json.dumps(payload()), encoding="utf-8") stdout = io.StringIO() rc = main(["--json", "--out", str(output_path), str(input_path)], stdout=stdout) self.assertEqual(rc, 0) self.assertIn("wrote", stdout.getvalue()) written = json.loads(output_path.read_text(encoding="utf-8")) self.assertEqual(written["kind"], "report_source_trace") self.assertEqual(written["summary"]["direct_static_hit_count"], 1) def test_write_text_output(self): with tempfile.TemporaryDirectory() as tmp: input_path = Path(tmp) / "rom.json" output_path = Path(tmp) / "sources.txt" input_path.write_text(json.dumps(payload()), encoding="utf-8") analysis = write_report_sources(input_path, output_path) self.assertEqual(analysis["kind"], "report_source_trace") self.assertIn("Report Source Trace", output_path.read_text(encoding="utf-8")) def test_real_rom_smoke_when_present(self): path = Path("build/rom_decompiled.json") if not path.exists(): self.skipTest("build/rom_decompiled.json is not present") payload_real = json.loads(path.read_text(encoding="utf-8")) analysis = analyze_report_sources(payload_real) self.assertEqual(analysis["kind"], "report_source_trace") self.assertGreaterEqual(analysis["summary"]["direct_call_count"], 1) self.assertIn("0x0007", analysis["summary"]["conclusion"]) for call in analysis["calls"]: self.assertIn("address_hex", call) self.assertIn("r2", call) self.assertIn("r3", call) if __name__ == "__main__": unittest.main()