import io import json import tempfile import unittest from pathlib import Path from h8536.rx_branch_trace import analyze_rx_branch_trace, format_text_report, main def ins(address: int, text: str | None = None) -> dict[str, object]: return { "address": address, "text": text or f"INS_{address:04X}", "mnemonic": (text or "NOP").split()[0], "operands": "", "kind": "normal", "targets": [], "references": [], } def fixture_payload() -> dict[str, object]: addresses = { 0x3FEF, 0x3FF3, 0x3FF5, 0x3FF9, 0x3FFD, 0x4001, 0x4003, 0x4007, 0xBB57, 0xBB5B, 0xBB5F, 0xBB63, 0xBB67, 0xBB6D, 0xBB71, 0xBB75, 0xBB77, 0xBB7D, 0xBB82, 0xBB84, 0xBB88, 0xBB8A, 0xBB90, 0xBB96, 0xBB9A, 0xBB9C, 0xBB9E, 0xBBA3, 0xBBAB, 0xBBB0, 0xBBB3, 0xBBCB, 0xBBCF, 0xBBD3, 0xBBD6, 0xBBD8, 0xBBDC, 0xBBE0, 0xBBE4, 0xBBE8, 0xBBEC, 0xBBF0, 0xBBF3, 0xBBF7, 0xBBFD, 0xBC01, 0xBC08, 0xBC0C, 0xBC0F, 0xBC13, 0xBC15, 0xBC19, 0xBC1D, 0xBC20, 0xBC24, 0xBC29, 0xBC2E, 0xBC33, 0xBC37, 0xBC3A, 0xBC3C, 0xBC3E, 0xBC42, 0xBC45, 0xBC4A, 0xBC4F, 0xBC54, 0xBC5C, 0xBC60, 0xBC63, 0xBC67, 0xBC69, 0xBC75, 0xBC79, 0xBC82, 0xBC86, 0xBCB0, 0xBCCD, 0xBCD0, 0xBCD7, 0xBCE0, 0xBCE8, 0xBCEC, 0xBCF0, 0xBCF6, 0xBCFA, 0xBCFD, 0xBD04, 0xBD08, 0xBD0B, 0xBD0E, 0xBD1A, 0xBD1E, 0xBD22, 0xBD26, 0xBD35, 0xBD64, 0xBD67, 0xBD6D, 0xBD75, 0xBD79, 0xBD80, 0xBD85, 0xBD94, 0xBD9A, 0xBDB5, 0xBDBF, 0xBDC2, 0xBDC8, 0xBDD0, 0xBDD4, 0xBDDB, 0xBDE5, 0xBDE9, 0xBDED, 0xBDF3, 0xBDFB, 0xBDFF, 0xBE05, 0xBE09, 0xBE0D, 0xBE11, 0xBE15, 0xBE19, 0xBE1D, 0xBE22, 0xBE27, 0xBE29, 0xBE2D, 0xBE31, 0xBE33, 0xBE37, 0xBE3C, 0xBE3E, 0xBE43, 0xBE47, 0xBE4D, 0xBE52, 0xBE5A, 0xBE62, 0xBE6A, 0xBE70, 0xBE78, 0xBE80, 0xBE82, 0xBE84, 0xBE88, 0xBE91, 0xBE95, 0xBE99, 0xBE9D, 0xBE9E, 0xBEA5, 0xBEA9, 0xBEAD, 0xBEAF, 0xBEB5, 0xBEBB, 0xBEBF, 0xBEC1, 0xBEC5, 0xBECB, 0xBED1, 0xBED5, 0xBEE4, } return {"instructions": [ins(address) for address in sorted(addresses)]} class RxBranchTraceTest(unittest.TestCase): def test_analyzes_dispatch_split_and_commands(self): analysis = analyze_rx_branch_trace(fixture_payload()) self.assertEqual(analysis["summary"]["confidence"], "high") self.assertEqual(analysis["frame_model"]["checksum_seed"], 0x5A) self.assertTrue(analysis["stages"][2]["present"]) self.assertIn("FAA2 != 0", analysis["stages"][2]["summary"]) commands = {command["command"]: command for command in analysis["commands"]} self.assertIn("continuation path only", commands[0x04]["availability"]) self.assertIn("selector zero is special", "\n".join(commands[0x04]["side_effects"])) self.assertIn("selectors 0x006C", "\n".join(commands[0x05]["side_effects"])) self.assertIn("previous finalized TX frame", commands[0x07]["summary"]) def test_text_report_mentions_bench_implications(self): text = format_text_report(analyze_rx_branch_trace(fixture_payload())) self.assertIn("H8/536 SCI1 RX Branch Trace", text) self.assertIn("cmd 0x04 continuation_set_value_candidate", text) self.assertIn("standalone command 4 frame from idle should not hit BD0E", text) self.assertIn("Command 5 is not a generic always-live ACK", text) self.assertIn("Selector Decode", text) self.assertIn("TXI/RXI race and continuation collapse", text) self.assertIn("RX-to-TX Feedback Loops", text) def test_cli_writes_json_output(self): with tempfile.TemporaryDirectory() as tmp: input_path = Path(tmp) / "rom.json" output_path = Path(tmp) / "rx.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"], "rx_branch_trace") self.assertIn("downstream_traces", payload) self.assertIn("feedback_loops", payload) if __name__ == "__main__": unittest.main()