import io import json import tempfile import unittest from pathlib import Path from h8536.serial_gate import analyze_serial_gate, format_text_report, main def ins( address: int, text: str, mnemonic: str | None = None, operands: str = "", references: list[int] | None = None, targets: list[int] | None = None, ) -> dict[str, object]: refs = [{"address": ref, "symbol": f"ram_{ref:04X}", "region": "on_chip_ram", "kind": "ram"} for ref in references or []] return { "address": address, "text": text, "mnemonic": mnemonic or text.split()[0], "operands": operands or (text.split(" ", 1)[1] if " " in text else ""), "kind": "call" if text.startswith("BSR") else "normal", "targets": targets or [], "references": refs, } def fixture_payload() -> dict[str, object]: rows = [ ins(0x3FD3, "TST.B @H'FAA2", references=[0xFAA2]), ins(0x3FD7, "BNE loc_3FEE", targets=[0x3FEE]), ins(0x3FD9, "BTST.B #7, @H'FAA5", references=[0xFAA5]), ins(0x3FDD, "BEQ loc_3FE5", targets=[0x3FE5]), ins(0x3FDF, "TST.B @H'F9C3", references=[0xF9C3]), ins(0x3FE3, "BNE loc_3FEE", targets=[0x3FEE]), ins(0x3FE5, "TST.B @H'F9C0", references=[0xF9C0]), ins(0x3FE9, "BNE loc_3FEE", targets=[0x3FEE]), ins(0x3FEB, "BSR loc_BAF2", targets=[0xBAF2]), ins(0x3FEF, "TST.B @H'F9C5", references=[0xF9C5]), ins(0x3FF5, "CLR.B @H'F9B5", references=[0xF9B5]), ins(0x3FF9, "CLR.B @H'F9B0", references=[0xF9B0]), ins(0xBAF2, "MOV:G.B @H'F9B5, R1", references=[0xF9B5]), ins(0xBAF8, "CMP:G.B @H'F9B0, R1", references=[0xF9B0]), ins(0xBAFC, "BNE loc_BB00", targets=[0xBB00]), ins(0xBAFE, "BRA loc_BB56", targets=[0xBB56]), ins(0xBB00, "BSET.B #3, @H'FAA2", references=[0xFAA2]), ins(0xBB08, "MOV:G.W @(-H'0790,R0), R0"), ins(0xBB1C, "MOV:G.B R1, @H'F850", references=[0xF850]), ins(0xBB20, "MOV:G.B R5, @H'F852", references=[0xF852]), ins(0xBB2B, "MOV:G.B R5, @H'F851", references=[0xF851]), ins(0xBB39, "MOV:G.B R4, @H'F854", references=[0xF854]), ins(0xBB3F, "MOV:G.B R4, @H'F853", references=[0xF853]), ins(0xBB43, "BSR loc_BA26", targets=[0xBA26]), ins(0xBB46, "MOV:G.W #H'01F4, @H'F9C6", references=[0xF9C6]), ins(0xBB4C, "MOV:G.B #H'14, @H'F9C8", references=[0xF9C8]), ins(0xBB51, "MOV:G.B #H'80, @H'FAA3", references=[0xFAA3]), ins(0xBBCB, "CLR.B @H'F9C3", references=[0xF9C3]), ins(0xBC0F, "TST.B @H'FAA2", references=[0xFAA2]), ins(0xBC15, "BSET.B #7, @H'FAA2", references=[0xFAA2]), ins(0xBD6D, "ADD:Q.B #1, @H'F9B5", references=[0xF9B5]), ins(0xBD71, "BCLR.B #7, @H'F9B5", references=[0xF9B5]), ins(0xBD75, "CLR.B @H'FAA3", references=[0xFAA3]), ins(0xBD79, "CLR.B @H'FAA2", references=[0xFAA2]), ins(0xBE9E, "MOV:G.B @H'FAA5, R0", references=[0xFAA5]), ins(0xBEA5, "AND.B @H'FAA3, R0", references=[0xFAA3]), ins(0xBEA9, "MOV:G.B R0, @H'FAA3", references=[0xFAA3]), ins(0xBEAF, "CLR.B @H'FAA2", references=[0xFAA2]), ins(0xBEB5, "TST.W @H'F9C6", references=[0xF9C6]), ins(0xBEBB, "TST.B @H'F9C8", references=[0xF9C8]), ins(0xBEC5, "MOV:G.W #H'01F4, @H'F9C6", references=[0xF9C6]), ins(0xBECB, "BTST.B #7, @H'FAA3", references=[0xFAA3]), ins(0xBED1, "CLR.B @H'F9C3", references=[0xF9C3]), ins(0xBED5, "BSR loc_BA26", targets=[0xBA26]), ] return { "call_graph": {"nodes": [{"start": 0x3FD3, "label": "loc_3FD3"}, {"start": 0xBAF2, "label": "loc_BAF2"}]}, "instructions": rows, } class SerialGateTest(unittest.TestCase): def test_reconstructs_requested_gate_evidence(self): analysis = analyze_serial_gate(fixture_payload()) self.assertEqual(analysis["summary"]["confidence"], "high") self.assertTrue(analysis["evidence"]["scheduler_gate_loc_3FD3"]["present"]) self.assertIn("FAA2 == 0", analysis["evidence"]["scheduler_gate_loc_3FD3"]["summary"]) self.assertTrue(analysis["evidence"]["queue_send_gate_loc_BAF2"]["present"]) self.assertEqual(analysis["evidence"]["queue_send_gate_loc_BAF2"]["queue_table_candidate"]["base_address_hex"], "H'F870") self.assertEqual(analysis["evidence"]["queue_send_gate_loc_BAF2"]["send_call_address_hex"], "H'BB43") self.assertTrue(analysis["evidence"]["resend_gate_path"]["present"]) self.assertEqual(analysis["evidence"]["resend_gate_path"]["resend_call_address_hex"], "H'BED5") self.assertTrue(analysis["evidence"]["rx_session_maintenance"]["present"]) self.assertTrue(any("0x0007" in caveat and "0x0015" in caveat for caveat in analysis["caveats"])) def test_summarizes_key_state_readers_and_writers(self): analysis = analyze_serial_gate(fixture_payload()) accesses = {entry["address"]: entry for entry in analysis["state_accesses"]} self.assertGreaterEqual(accesses[0xF9B5]["read_count"], 1) self.assertGreaterEqual(accesses[0xF9B5]["read_write_count"], 1) self.assertGreaterEqual(accesses[0xFAA3]["write_count"], 1) self.assertIn("sample_accesses", accesses[0xFAA2]) def test_text_report_mentions_caveat_and_gate(self): text = format_text_report(analyze_serial_gate(fixture_payload())) self.assertIn("loc_3FD3 gate into loc_BAF2", text) self.assertIn("capture overlays/runtime queue entries", text) self.assertIn("H'F9B5", 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) / "gate.json" input_path.write_text(json.dumps(fixture_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()) payload = json.loads(output_path.read_text(encoding="utf-8")) self.assertEqual(payload["kind"], "serial_gate") if __name__ == "__main__": unittest.main()