206 lines
10 KiB
Python
206 lines
10 KiB
Python
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(0x4046, "TST.B @H'F9C4", references=[0xF9C4]),
|
|
ins(0x404A, "BNE loc_4058", targets=[0x4058]),
|
|
ins(0x404C, "BTST.B #7, @H'FAA5", references=[0xFAA5]),
|
|
ins(0x4050, "BEQ loc_4059", targets=[0x4059]),
|
|
ins(0x4052, "TST.B @H'F9C3", references=[0xF9C3]),
|
|
ins(0x4056, "BEQ loc_4059", targets=[0x4059]),
|
|
ins(0x4058, "RTS"),
|
|
ins(0x4059, "MOV:G.B @H'F9B0, R2", references=[0xF9B0]),
|
|
ins(0x405F, "CMP:G.B @H'F9B5, R2", references=[0xF9B5]),
|
|
ins(0x4063, "BNE loc_4074", targets=[0x4074]),
|
|
ins(0x4067, "MOV:G.W #H'00, @(-H'0790,R2)"),
|
|
ins(0x406C, "ADD:Q.B #1, @H'F9B0", references=[0xF9B0]),
|
|
ins(0x4070, "BCLR.B #7, @H'F9B0", references=[0xF9B0]),
|
|
ins(0x40E0, "MOV:G.B #H'14, @H'F9C4", references=[0xF9C4]),
|
|
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(0xBA31, "MOV:G.B #H'07, @H'F9C4", references=[0xF9C4]),
|
|
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]),
|
|
ins(0xBEEA, "BCLR.B #5, @FRT1_TCSR"),
|
|
ins(0xBEEE, "TST.B @H'F9C0", references=[0xF9C0]),
|
|
ins(0xBEF2, "BEQ loc_BEF8", targets=[0xBEF8]),
|
|
ins(0xBEF4, "ADD:Q.B #-1, @H'F9C0", references=[0xF9C0]),
|
|
ins(0xBEF8, "TST.B @H'F9C1", references=[0xF9C1]),
|
|
ins(0xBEFC, "BEQ loc_BF02", targets=[0xBF02]),
|
|
ins(0xBEFE, "ADD:Q.B #-1, @H'F9C1", references=[0xF9C1]),
|
|
ins(0xBF02, "TST.W @H'F9C6", references=[0xF9C6]),
|
|
ins(0xBF06, "BEQ loc_BF0C", targets=[0xBF0C]),
|
|
ins(0xBF08, "ADD:Q.W #-1, @H'F9C6", references=[0xF9C6]),
|
|
ins(0xBF23, "BCLR.B #5, @FRT2_TCSR"),
|
|
ins(0xBF27, "TST.B @H'F9C4", references=[0xF9C4]),
|
|
ins(0xBF2D, "ADD:Q.B #-1, @H'F9C4", references=[0xF9C4]),
|
|
]
|
|
return {
|
|
"vectors": [
|
|
{"address": 0x0062, "name": "frt1_ocia", "target": 0xBEEA, "target_label": "vec_frt1_ocia_BEEA"},
|
|
{"address": 0x006A, "name": "frt2_ocia", "target": 0xBF23, "target_label": "vec_frt2_ocia_BF23"},
|
|
],
|
|
"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_json_analysis_includes_frt1_tick_timer_roles(self):
|
|
analysis = analyze_serial_gate(fixture_payload())
|
|
tick = analysis["evidence"]["timer_tick_evidence"]
|
|
|
|
self.assertTrue(tick["present"])
|
|
self.assertEqual(tick["vector_address_hex"], "H'0062")
|
|
self.assertEqual(tick["handler_address_hex"], "H'BEEA")
|
|
self.assertIn("FRT1_TCSR.OCFA", tick["summary"])
|
|
roles = {role["address"]: role for role in tick["candidate_timer_roles"]}
|
|
self.assertIn("post-TX/report delay", roles[0xF9C0]["role"])
|
|
self.assertIn("secondary delay", roles[0xF9C1]["role"])
|
|
self.assertIn("periodic report/heartbeat", roles[0xF9C6]["role"])
|
|
|
|
def test_json_analysis_includes_frt2_idle_heartbeat_gate(self):
|
|
analysis = analyze_serial_gate(fixture_payload())
|
|
gate = analysis["evidence"]["idle_heartbeat_gate_loc_4046"]
|
|
|
|
self.assertTrue(gate["present"])
|
|
self.assertEqual(gate["timer"]["handler_address_hex"], "H'BF23")
|
|
self.assertEqual(gate["post_tx_reload_value_hex"], "H'07")
|
|
self.assertIn("0.7s", gate["summary"])
|
|
roles = {role["address"]: role for role in gate["candidate_timer_roles"]}
|
|
self.assertIn("heartbeat", roles[0xF9C4]["role"])
|
|
|
|
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[0xF9C1]["read_write_count"], 1)
|
|
self.assertGreaterEqual(accesses[0xF9C4]["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_text_report_mentions_frt1_tick_timer_roles(self):
|
|
text = format_text_report(analyze_serial_gate(fixture_payload()))
|
|
|
|
self.assertIn("FRT1 OCIA periodic tick countdowns: present", text)
|
|
self.assertIn("H'BEEA: BCLR.B #5, @FRT1_TCSR", text)
|
|
self.assertIn("H'F9C0: candidate post-TX/report delay countdown", text)
|
|
self.assertIn("H'F9C1: candidate secondary delay countdown", text)
|
|
self.assertIn("H'F9C6: candidate periodic report/heartbeat countdown", text)
|
|
|
|
def test_text_report_mentions_frt2_idle_heartbeat_gate(self):
|
|
text = format_text_report(analyze_serial_gate(fixture_payload()))
|
|
|
|
self.assertIn("loc_4046 idle heartbeat/report gate: present", text)
|
|
self.assertIn("FRT2 OCIA", text)
|
|
self.assertIn("H'F9C4: candidate idle heartbeat/report gate countdown", text)
|
|
self.assertIn("observed period ~= 700ms", 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()
|