More decompiling work
This commit is contained in:
145
tests/test_protocol_capture.py
Normal file
145
tests/test_protocol_capture.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import io
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from h8536.protocol_capture import analyze_capture_text, format_text_report, main, parse_capture_text
|
||||
|
||||
|
||||
class ProtocolCaptureTest(unittest.TestCase):
|
||||
def test_parses_timestamped_capture_chunks(self):
|
||||
chunks = parse_capture_text("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n")
|
||||
|
||||
self.assertEqual(len(chunks), 1)
|
||||
self.assertEqual(chunks[0].timestamp_ms, 57975502)
|
||||
self.assertEqual(chunks[0].analyzer_direction, "rx")
|
||||
self.assertEqual(chunks[0].device_direction, "tx")
|
||||
self.assertEqual(chunks[0].bytes, (0x00, 0x00, 0x15, 0x80, 0x00, 0xCF))
|
||||
|
||||
def test_recombines_user_split_rx_chunks_into_valid_call_frame(self):
|
||||
analysis = analyze_capture_text(
|
||||
"16:06:15.502 RX 003 bytes 00 00 15\n"
|
||||
"16:06:15.506 RX 003 bytes 80 00 CF\n"
|
||||
)
|
||||
|
||||
self.assertEqual(analysis["frame_count"], 1)
|
||||
frame = analysis["frames"][0]
|
||||
self.assertEqual(frame["source_chunk_indexes"], [0, 1])
|
||||
self.assertEqual(frame["analyzer_direction"], "rx")
|
||||
self.assertEqual(frame["device_direction"], "tx")
|
||||
self.assertEqual(frame["report_candidate"]["index"], 0x15)
|
||||
self.assertEqual(frame["report_candidate"]["value"], 0x8000)
|
||||
self.assertEqual(
|
||||
frame["report_candidate"]["observed_candidate"]["name_candidate"],
|
||||
"call_button_candidate",
|
||||
)
|
||||
self.assertEqual(frame["report_candidate"]["observed_candidate"]["state_candidate"], "active")
|
||||
|
||||
def test_recombines_split_chunks_with_multiple_frames_and_labels_known_reports(self):
|
||||
analysis = analyze_capture_text(
|
||||
"16:06:15.500 RX 004 bytes 00 00 00 00\n"
|
||||
"16:06:15.502 RX 005 bytes 80 DA 00 00 07\n"
|
||||
"16:06:15.504 RX 003 bytes 80 00 DD\n"
|
||||
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
|
||||
)
|
||||
|
||||
self.assertEqual(analysis["frame_count"], 3)
|
||||
names = [
|
||||
frame["report_candidate"]["observed_candidate"]["name_candidate"]
|
||||
for frame in analysis["frames"]
|
||||
]
|
||||
self.assertEqual(
|
||||
names,
|
||||
[
|
||||
"heartbeat_alive_candidate",
|
||||
"cam_power_button_candidate",
|
||||
"heartbeat_alive_candidate",
|
||||
],
|
||||
)
|
||||
self.assertEqual(analysis["repeated_group_count"], 1)
|
||||
group = analysis["repeated_groups"][0]
|
||||
self.assertEqual(group["count"], 2)
|
||||
self.assertEqual(group["cadence_ms"]["samples"], [100])
|
||||
|
||||
def test_text_report_mentions_split_label_and_cadence(self):
|
||||
report = format_text_report(
|
||||
analyze_capture_text(
|
||||
"16:06:15.502 RX 003 bytes 00 00 15\n"
|
||||
"16:06:15.506 RX 003 bytes 80 00 CF\n"
|
||||
"16:06:15.602 RX 006 bytes 00 00 15 80 00 CF\n"
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIn("call_button_candidate state=active", report)
|
||||
self.assertIn("checksum=ok split", report)
|
||||
self.assertIn("cadence=avg=100.0ms", report)
|
||||
|
||||
def test_gate_session_hints_summarize_reports_transitions_and_heartbeat_interruptions(self):
|
||||
analysis = analyze_capture_text(
|
||||
"16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n"
|
||||
"16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n"
|
||||
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
|
||||
"16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n"
|
||||
"16:06:15.700 RX 006 bytes 00 00 00 00 80 DA\n"
|
||||
)
|
||||
|
||||
hints = analysis["gate_session_hints"]
|
||||
self.assertEqual(
|
||||
hints["observed_autonomous_report_names"],
|
||||
["call_button_candidate", "heartbeat_alive_candidate"],
|
||||
)
|
||||
reports = {item["name_candidate"]: item for item in hints["observed_reports"]}
|
||||
self.assertEqual(reports["heartbeat_alive_candidate"]["count"], 3)
|
||||
self.assertEqual(reports["heartbeat_alive_candidate"]["first_timestamp"], "16:06:15.500")
|
||||
self.assertEqual(reports["heartbeat_alive_candidate"]["last_timestamp"], "16:06:15.700")
|
||||
self.assertEqual(reports["call_button_candidate"]["states"], ["active", "inactive"])
|
||||
|
||||
self.assertEqual(hints["heartbeat_cadence_ms"]["samples"], [100, 100])
|
||||
self.assertEqual(hints["heartbeat_cadence_ms"]["average"], 100.0)
|
||||
self.assertEqual(len(hints["heartbeat_interruptions"]), 2)
|
||||
self.assertEqual(
|
||||
hints["heartbeat_interruptions"][0]["interrupted_by"][0]["name_candidate"],
|
||||
"call_button_candidate",
|
||||
)
|
||||
|
||||
self.assertEqual(len(hints["active_inactive_transitions"]), 1)
|
||||
transition = hints["active_inactive_transitions"][0]
|
||||
self.assertEqual(transition["index_hex"], "0x0015")
|
||||
self.assertEqual(transition["from_state"], "active")
|
||||
self.assertEqual(transition["to_state"], "inactive")
|
||||
self.assertEqual(hints["evidence_scope"], "capture_side_observation_only")
|
||||
self.assertIn("host/session gating", hints["caveat"])
|
||||
|
||||
def test_text_report_mentions_gate_session_hints_and_caveat(self):
|
||||
report = format_text_report(
|
||||
analyze_capture_text(
|
||||
"16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n"
|
||||
"16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n"
|
||||
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
|
||||
"16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n"
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"observed autonomous report candidates: call_button_candidate, heartbeat_alive_candidate",
|
||||
report,
|
||||
)
|
||||
self.assertIn("heartbeat cadence count=2 cadence=avg=100.0ms", report)
|
||||
self.assertIn("transition index=0x0015 active->inactive", report)
|
||||
self.assertIn("heartbeat gap 16:06:15.500..16:06:15.600", report)
|
||||
self.assertIn("caveat: Missing autonomous reports", report)
|
||||
|
||||
def test_cli_json_output(self):
|
||||
output = io.StringIO()
|
||||
rc = main(
|
||||
["--json", "-"],
|
||||
stdin=io.StringIO("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n"),
|
||||
stdout=output,
|
||||
)
|
||||
|
||||
self.assertEqual(rc, 0)
|
||||
payload = json.loads(output.getvalue())
|
||||
self.assertEqual(payload["frames"][0]["report_candidate"]["index"], 0x15)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
154
tests/test_protocol_trace.py
Normal file
154
tests/test_protocol_trace.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from h8536.protocol_trace import (
|
||||
checksum_for,
|
||||
decode_trace,
|
||||
format_text_report,
|
||||
main,
|
||||
parse_byte_text,
|
||||
)
|
||||
|
||||
|
||||
def frame(prefix: list[int]) -> list[int]:
|
||||
return prefix + [checksum_for(prefix)]
|
||||
|
||||
|
||||
class ProtocolTraceTest(unittest.TestCase):
|
||||
def test_decodes_six_byte_frame_fields_and_checksum(self):
|
||||
decoded = decode_trace(frame([0x08, 0x85, 0x34, 0x12, 0xAB]), direction="rx")
|
||||
only = decoded["frames"][0]
|
||||
|
||||
self.assertTrue(only["checksum"]["valid"])
|
||||
self.assertEqual(only["direction"], "rx")
|
||||
self.assertEqual(only["command"]["value"], 0)
|
||||
self.assertEqual(only["index"]["byte1_low3"], 5)
|
||||
self.assertEqual(only["index"]["combined"], 0x0534)
|
||||
self.assertEqual(only["payload_value"]["word_be"], 0x12AB)
|
||||
self.assertEqual(only["payload_value"]["word_le"], 0xAB12)
|
||||
|
||||
def test_reports_bad_checksum_and_trailing_bytes(self):
|
||||
decoded = decode_trace([0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x99])
|
||||
only = decoded["frames"][0]
|
||||
|
||||
self.assertFalse(only["checksum"]["valid"])
|
||||
self.assertEqual(only["checksum"]["expected"], checksum_for([1, 2, 3, 4, 5]))
|
||||
self.assertEqual(decoded["trailing_bytes"], ["0x99"])
|
||||
|
||||
def test_loads_semantic_command_names_from_decompiler_json(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "rom.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"serial_semantics": {
|
||||
"protocol_semantics": [
|
||||
{
|
||||
"command_effects": [
|
||||
{
|
||||
"command_value": 1,
|
||||
"name_candidate": "read_value",
|
||||
}
|
||||
],
|
||||
"response_schema": [],
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
decoded = decode_trace(frame([0x09, 0, 1, 2, 3]), semantics_path=path)
|
||||
|
||||
self.assertTrue(decoded["semantics"]["loaded"])
|
||||
self.assertEqual(decoded["frames"][0]["command"]["name_candidate"], "read_value")
|
||||
|
||||
def test_marks_rx_cmd_7_as_retransmit_or_error_candidate_with_previous_frame(self):
|
||||
prior = frame([0x01, 0, 0, 0, 0])
|
||||
retry = frame([0x07, 0, 0, 0, 0])
|
||||
|
||||
decoded = decode_trace(prior + retry, direction="rx")
|
||||
annotation = decoded["frames"][1]["stateful_annotations"][0]
|
||||
|
||||
self.assertEqual(annotation["kind"], "retransmit_or_error_candidate")
|
||||
self.assertEqual(annotation["previous_valid_same_direction"]["frame_index"], 0)
|
||||
|
||||
def test_decodes_observed_tx_frames_as_reports(self):
|
||||
samples = [
|
||||
([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA], 0x0000, 0x0080, "heartbeat_alive_candidate", None),
|
||||
([0x00, 0x00, 0x15, 0x80, 0x00, 0xCF], 0x0015, 0x8000, "call_button_candidate", "active"),
|
||||
([0x00, 0x00, 0x15, 0x00, 0x00, 0x4F], 0x0015, 0x0000, "call_button_candidate", "inactive"),
|
||||
([0x00, 0x00, 0x07, 0x80, 0x00, 0xDD], 0x0007, 0x8000, "cam_power_button_candidate", "active"),
|
||||
]
|
||||
|
||||
decoded = decode_trace([byte for sample, *_ in samples for byte in sample], direction="tx")
|
||||
|
||||
for actual, (_, index, value, name, state) in zip(decoded["frames"], samples):
|
||||
self.assertTrue(actual["checksum"]["valid"])
|
||||
self.assertFalse(actual["command"]["applicable"])
|
||||
self.assertIsNone(actual["command"]["name_candidate"])
|
||||
self.assertEqual(actual["response_schema_candidates"], [])
|
||||
self.assertEqual(actual["stateful_annotations"], [])
|
||||
self.assertEqual(actual["report"]["index"], index)
|
||||
self.assertEqual(actual["report"]["value"], value)
|
||||
self.assertEqual(actual["report"]["observed_candidate"]["name_candidate"], name)
|
||||
self.assertEqual(actual["report"]["observed_candidate"].get("state_candidate"), state)
|
||||
|
||||
def test_tx_text_report_does_not_render_cmd_or_set_value_acked(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "rom.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"serial_semantics": {
|
||||
"command_effects": [
|
||||
{
|
||||
"command_value": 0,
|
||||
"name_candidate": "set_value_acked",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
decoded = decode_trace(
|
||||
[0x00, 0x00, 0x15, 0x80, 0x00, 0xCF],
|
||||
direction="tx",
|
||||
semantics_path=path,
|
||||
)
|
||||
|
||||
text = format_text_report(decoded)
|
||||
|
||||
self.assertIn("report_index=0x0015", text)
|
||||
self.assertIn("observed_candidate=call_button_candidate", text)
|
||||
self.assertNotIn("cmd=", text)
|
||||
self.assertNotIn("set_value_acked", text)
|
||||
|
||||
def test_auto_direction_uses_rx_tx_prefixes(self):
|
||||
events = parse_byte_text(
|
||||
"rx: 01 00 00 00 00 5B\n"
|
||||
"tx: 04 00 00 00 00 5E\n"
|
||||
)
|
||||
|
||||
decoded = decode_trace(events, direction="auto")
|
||||
|
||||
self.assertEqual(decoded["frames"][0]["direction"], "rx")
|
||||
self.assertEqual(decoded["frames"][1]["direction"], "tx")
|
||||
|
||||
def test_cli_json_output(self):
|
||||
output = io.StringIO()
|
||||
rc = main(["--json", "01", "00", "00", "00", "00", "5B"], stdout=output)
|
||||
|
||||
self.assertEqual(rc, 0)
|
||||
payload = json.loads(output.getvalue())
|
||||
self.assertEqual(payload["frames"][0]["command"]["value"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
130
tests/test_serial_gate.py
Normal file
130
tests/test_serial_gate.py
Normal file
@@ -0,0 +1,130 @@
|
||||
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()
|
||||
@@ -270,6 +270,80 @@ class SerialPseudocodeTest(unittest.TestCase):
|
||||
},
|
||||
"evidence_addresses_hex": ["H'BBD6", "H'BE29"],
|
||||
},
|
||||
"gate_queue_model": {
|
||||
"predicates": [
|
||||
{
|
||||
"name": "main_loop_may_enter_report_builder",
|
||||
"condition_candidate": "FAA2 == 0 && F9C0 == 0 && ((FAA5.bit7 == 0) || (F9C3 == 0))",
|
||||
"summary": "loc_3FD3 gates loc_BAF2.",
|
||||
},
|
||||
{
|
||||
"name": "queue_has_pending_report",
|
||||
"condition_candidate": "F9B5 != F9B0",
|
||||
"summary": "Queue non-empty path stages through BB43/BA26.",
|
||||
},
|
||||
{
|
||||
"name": "periodic_resend_may_fire",
|
||||
"condition_candidate": "(FAA5 & FAA3 & 0x80) != 0 && F9C6 == 0 && F9C8 != 0",
|
||||
"summary": "BE9E/BED5 resend gate.",
|
||||
},
|
||||
],
|
||||
"session_effects": [
|
||||
{
|
||||
"name": "rx_completion_sets_session_timer",
|
||||
"summary": "RX completion sets F9C5.",
|
||||
},
|
||||
{
|
||||
"name": "session_timeout_clears_gate_and_queue",
|
||||
"summary": "loc_3FEF clears F9B5/F9B0 and clears or sets FAA5.",
|
||||
},
|
||||
{
|
||||
"name": "host_ack_can_advance_queue",
|
||||
"summary": "Commands 0x05/0x06 can ack or advance F9B5.",
|
||||
"command_values_hex": ["H'05", "H'06"],
|
||||
},
|
||||
],
|
||||
"caveat": "Many panel controls may require host/session traffic before reporting; observed autonomous call/cam-power indexes are runtime/capture overlays, not ROM constants.",
|
||||
"evidence_addresses_hex": ["H'3FD3", "H'3FEB", "H'BAF2", "H'BB43", "H'BE9E", "H'BED5"],
|
||||
},
|
||||
"tx_report_model": {
|
||||
"entry_label": "loc_BB43",
|
||||
"value_source_candidate": "current_value_table_candidate",
|
||||
"observed_capture_overlay_candidates": [
|
||||
{
|
||||
"name_candidate": "heartbeat_or_idle_report_candidate",
|
||||
"observed_frames_hex": ["00 00 00 00 80 DA"],
|
||||
},
|
||||
{
|
||||
"name_candidate": "call_button_report_candidate",
|
||||
"observed_frames_hex": ["00 00 15 80 00 CF", "00 00 15 00 00 4F"],
|
||||
},
|
||||
{
|
||||
"name_candidate": "camera_power_report_candidate",
|
||||
"observed_frames_hex": ["00 00 07 80 00 DD"],
|
||||
},
|
||||
],
|
||||
"observed_autonomous_output_caveat": "Observed autonomous output is limited to heartbeat/call/cam-power; other controls may require host/device requests first.",
|
||||
"evidence_addresses_hex": ["H'BB20", "H'BB43"],
|
||||
},
|
||||
"periodic_resend_model": {
|
||||
"period_timer": {
|
||||
"reload_value_hex": "H'01F4",
|
||||
"summary": "Candidate periodic report/heartbeat timer reload.",
|
||||
},
|
||||
"resend_countdown": {
|
||||
"reload_value_hex": "H'14",
|
||||
"summary": "Candidate periodic resend countdown.",
|
||||
},
|
||||
"pending_mask": {
|
||||
"mask_hex": "H'80",
|
||||
"summary": "Candidate autonomous report pending mask.",
|
||||
},
|
||||
"resend_path": {
|
||||
"summary": "Candidate periodic resend path feeding TX staging.",
|
||||
},
|
||||
"evidence_addresses_hex": ["H'BE90", "H'BED5"],
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -287,6 +361,19 @@ class SerialPseudocodeTest(unittest.TestCase):
|
||||
self.assertIn("serial_session_flags_candidate H'FAA2: reads 1, writes 2; bits 7", text)
|
||||
self.assertIn("retry/error model candidate:", text)
|
||||
self.assertIn("checksum path: 0x5A-seeded XOR over RX[0..4] differs from RX[5] -> loc_BE29", text)
|
||||
self.assertIn("gate/queue state machine candidate:", text)
|
||||
self.assertIn("main_loop_may_enter_report_builder: FAA2 == 0 && F9C0 == 0", text)
|
||||
self.assertIn("queue_has_pending_report: F9B5 != F9B0", text)
|
||||
self.assertIn("host_ack_can_advance_queue: Commands 0x05/0x06 can ack or advance F9B5", text)
|
||||
self.assertIn("static bool sci1_candidate_main_report_gate_open(void)", text)
|
||||
self.assertIn("static bool sci1_candidate_report_queue_nonempty(void)", text)
|
||||
self.assertIn("static bool sci1_candidate_periodic_resend_gate_open(void)", text)
|
||||
self.assertIn("TX/autonomous report model candidate:", text)
|
||||
self.assertIn("loc_BB43 -> loc_BA26: bytes 0..2 encode candidate logical index/report id; bytes 3..4 come from current_value_table_candidate", text)
|
||||
self.assertIn("heartbeat_or_idle_report_candidate: 00 00 00 00 80 DA", text)
|
||||
self.assertIn("heartbeat/periodic resend candidate:", text)
|
||||
self.assertIn("F9C6 reload H'01F4", text)
|
||||
self.assertIn("BED5 resend path", text)
|
||||
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
|
||||
|
||||
def test_tx_only_option_omits_rx_functions(self):
|
||||
|
||||
@@ -103,8 +103,20 @@ def planned_semantics_payload() -> dict:
|
||||
instruction(0xBF14, "BEQ", "loc_C100", targets=[0xC100]),
|
||||
instruction(0xBF18, "CMP:E.B", "#H'06, R0"),
|
||||
instruction(0xBF1C, "BEQ", "loc_C600", targets=[0xC600]),
|
||||
instruction(0xBF1E, "CMP:E.B", "#H'05, R0"),
|
||||
instruction(0xBF1F, "BEQ", "loc_C500", targets=[0xC500]),
|
||||
instruction(0xBF20, "CMP:E.B", "#H'07, R0"),
|
||||
instruction(0xBF24, "BEQ", "loc_C700", targets=[0xC700]),
|
||||
instruction(0x3FD3, "TST.B", "@H'FAA2", [0xFAA2]),
|
||||
instruction(0x3FD9, "BTST.B", "#7, @H'FAA5", [0xFAA5]),
|
||||
instruction(0x3FDF, "TST.B", "@H'F9C3", [0xF9C3]),
|
||||
instruction(0x3FE5, "TST.B", "@H'F9C0", [0xF9C0]),
|
||||
instruction(0x3FEB, "BSR", "loc_BAF2", targets=[0xBAF2]),
|
||||
instruction(0x3FEF, "TST.B", "@H'F9C5", [0xF9C5]),
|
||||
instruction(0x3FF5, "CLR.B", "@H'F9B5", [0xF9B5]),
|
||||
instruction(0x3FF9, "CLR.B", "@H'F9B0", [0xF9B0]),
|
||||
instruction(0x3FFD, "BCLR.B", "#7, @H'FAA5", [0xFAA5]),
|
||||
instruction(0x4007, "BSET.B", "#7, @H'FAA5", [0xFAA5]),
|
||||
instruction(0xC000, "MOV:G.B", "@H'F861, R1", [0xF861]),
|
||||
instruction(0xC004, "MOV:G.B", "@H'F862, R2", [0xF862]),
|
||||
instruction(0xC008, "BSR", "loc_622B", targets=[0x622B]),
|
||||
@@ -130,6 +142,9 @@ def planned_semantics_payload() -> dict:
|
||||
instruction(0xC124, "MOV:G.W", "R3, @H'F853", [0xF853]),
|
||||
instruction(0xC128, "MOV:G.B", "@H'F9B5, R6", [0xF9B5]),
|
||||
instruction(0xC12C, "BSR", "loc_BA26", targets=[0xBA26]),
|
||||
instruction(0xC500, "ADD:Q.B", "#1, @H'F9B5", [0xF9B5]),
|
||||
instruction(0xC504, "BCLR.B", "#7, @H'F9B5", [0xF9B5]),
|
||||
instruction(0xC508, "BCLR.B", "#7, @H'FAA3", [0xFAA3]),
|
||||
instruction(0xC600, "MOV:G.B", "@H'F861, R1", [0xF861]),
|
||||
instruction(0xC604, "MOV:G.B", "@H'F862, R2", [0xF862]),
|
||||
instruction(0xC608, "BSR", "loc_622B", targets=[0x622B]),
|
||||
@@ -143,6 +158,25 @@ def planned_semantics_payload() -> dict:
|
||||
instruction(0xC70C, "BSR", "loc_BA26", targets=[0xBA26]),
|
||||
instruction(0xC800, "CMP:E.B", "@H'F865, R7", [0xF865]),
|
||||
instruction(0xC804, "BNE", "loc_C700", targets=[0xC700]),
|
||||
instruction(0xBB20, "MOV:G.B", "#H'00, @H'F850", [0xF850]),
|
||||
instruction(0xBB24, "MOV:G.B", "#H'00, @H'F851", [0xF851]),
|
||||
instruction(0xBB28, "MOV:G.B", "#H'15, @H'F852", [0xF852]),
|
||||
instruction(0xBB2C, "MOV:G.W", "@(-H'1800,R4), R0"),
|
||||
instruction(0xBB30, "MOV:G.W", "R0, @H'F853", [0xF853]),
|
||||
instruction(0xBB43, "BSR", "loc_BA26", targets=[0xBA26]),
|
||||
instruction(0xBE90, "MOV:G.W", "#H'01F4, @H'F9C6", [0xF9C6]),
|
||||
instruction(0xBE94, "MOV:G.B", "#H'14, @H'F9C8", [0xF9C8]),
|
||||
instruction(0xBE98, "MOV:G.B", "#H'80, @H'FAA3", [0xFAA3]),
|
||||
instruction(0xBE9C, "MOV:G.B", "#H'01, @H'FAA2", [0xFAA2]),
|
||||
instruction(0xBEA0, "MOV:G.B", "@H'F9B5, R1", [0xF9B5]),
|
||||
instruction(0xBEA4, "MOV:G.B", "#H'01, @H'F9C0", [0xF9C0]),
|
||||
instruction(0xBE9E, "MOV:G.B", "@H'FAA5, R0", [0xFAA5]),
|
||||
instruction(0xBEA5, "AND.B", "@H'FAA3, R0", [0xFAA3]),
|
||||
instruction(0xBEB5, "TST.W", "@H'F9C6", [0xF9C6]),
|
||||
instruction(0xBEBB, "TST.B", "@H'F9C8", [0xF9C8]),
|
||||
instruction(0xBED5, "MOV:G.B", "@H'F858, R0", [0xF858]),
|
||||
instruction(0xBED9, "MOV:G.B", "R0, @H'F850", [0xF850]),
|
||||
instruction(0xBEE0, "BSR", "loc_BA26", targets=[0xBA26]),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -296,6 +330,53 @@ class SerialSemanticsTest(unittest.TestCase):
|
||||
self.assertIn("0x07", retry_text)
|
||||
self.assertIn("checksum_error_response", retry_text)
|
||||
|
||||
def test_tx_report_model_separates_autonomous_reports_from_rx_commands(self):
|
||||
semantics = only_semantics(self, planned_semantics_payload())
|
||||
|
||||
report = semantics["tx_report_model"]
|
||||
report_text = semantic_text(report)
|
||||
|
||||
self.assertEqual(report["direction"], "device_to_host_autonomous_report_candidate")
|
||||
self.assertIn("bb43", report_text)
|
||||
self.assertIn("ba26", report_text)
|
||||
self.assertIn("bytes 0..2", report_text)
|
||||
self.assertIn("current_value_table", report_text)
|
||||
self.assertIn("00 00 15 80 00 cf", report_text)
|
||||
self.assertIn("host/device request", report_text)
|
||||
|
||||
def test_periodic_resend_model_marks_heartbeat_constants(self):
|
||||
semantics = only_semantics(self, planned_semantics_payload())
|
||||
|
||||
periodic = semantics["periodic_resend_model"]
|
||||
periodic_text = semantic_text(periodic)
|
||||
|
||||
self.assertIn("f9c6", periodic_text)
|
||||
self.assertIn("01f4", periodic_text)
|
||||
self.assertIn("f9c8", periodic_text)
|
||||
self.assertIn("14", periodic_text)
|
||||
self.assertIn("faa3", periodic_text)
|
||||
self.assertIn("80", periodic_text)
|
||||
self.assertIn("bed5", periodic_text)
|
||||
|
||||
def test_gate_queue_model_surfaces_autonomous_tx_state_machine(self):
|
||||
semantics = only_semantics(self, planned_semantics_payload())
|
||||
|
||||
gate = semantics["gate_queue_model"]
|
||||
gate_text = semantic_text(gate)
|
||||
|
||||
self.assertIn("3fd3", gate_text)
|
||||
self.assertIn("baf2", gate_text)
|
||||
self.assertIn("faa2 == 0", gate_text)
|
||||
self.assertIn("f9c0 == 0", gate_text)
|
||||
self.assertIn("f9b5 != f9b0", gate_text)
|
||||
self.assertIn("bb43", gate_text)
|
||||
self.assertIn("be9e", gate_text)
|
||||
self.assertIn("bed5", gate_text)
|
||||
self.assertIn("f9c5", gate_text)
|
||||
self.assertIn("commands 0x05", gate_text)
|
||||
self.assertIn("0x06", gate_text)
|
||||
self.assertIn("not rom constants", gate_text)
|
||||
|
||||
def test_missing_serial_reconstruction_candidates_emit_no_protocol_semantics(self):
|
||||
payload = {
|
||||
"serial_reconstruction": {"candidates": []},
|
||||
|
||||
157
tests/test_table_xrefs.py
Normal file
157
tests/test_table_xrefs.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from h8536.table_xrefs import analyze_table_xrefs, generate_table_xref_report, write_table_xrefs
|
||||
|
||||
|
||||
def reference(address: int) -> dict:
|
||||
return {"address": address}
|
||||
|
||||
|
||||
def instruction(
|
||||
address: int,
|
||||
mnemonic: str,
|
||||
operands: str = "",
|
||||
references: list[int] | None = None,
|
||||
text: str | None = None,
|
||||
) -> dict:
|
||||
return {
|
||||
"address": address,
|
||||
"mnemonic": mnemonic,
|
||||
"operands": operands,
|
||||
"text": text or f"{mnemonic} {operands}".strip(),
|
||||
"references": [reference(item) for item in (references or [])],
|
||||
"targets": [],
|
||||
}
|
||||
|
||||
|
||||
def payload() -> dict:
|
||||
return {
|
||||
"call_graph": {
|
||||
"nodes": [
|
||||
{"start": 0xC000, "end": 0xC0FF, "label": "loc_C000"},
|
||||
{"start": 0xD000, "end": 0xD0FF, "label": "loc_D000"},
|
||||
],
|
||||
},
|
||||
"serial_semantics": {
|
||||
"table_map_candidates": [
|
||||
{
|
||||
"kind": "logical_table_map_candidate",
|
||||
"name_candidate": "primary_value_table_candidate",
|
||||
"confidence": "candidate-medium",
|
||||
"accesses": [{"instruction_address": 0xC004}],
|
||||
}
|
||||
],
|
||||
},
|
||||
"lcd_text": {
|
||||
"strings": [
|
||||
{
|
||||
"address": 0x77F4,
|
||||
"text": "COMM LINK ITEM-1",
|
||||
"trimmed": "COMM LINK ITEM-1",
|
||||
"confidence": "high",
|
||||
"xrefs": [
|
||||
{
|
||||
"address": 0x7804,
|
||||
"following_bsr": {"address": 0x7807, "target": 0x5A91},
|
||||
}
|
||||
],
|
||||
"xref_count": 1,
|
||||
}
|
||||
],
|
||||
},
|
||||
"lcd_driver": {
|
||||
"routines": [
|
||||
{
|
||||
"start": 0x3F40,
|
||||
"end": 0x3F74,
|
||||
"role_hint": "lcd_wait_and_transfer",
|
||||
"roles": ["lcd_data_write"],
|
||||
}
|
||||
],
|
||||
},
|
||||
"instructions": [
|
||||
instruction(0xC000, "MOV:G.W", "#H'0006, R3"),
|
||||
instruction(0xC004, "MOV:G.W", "@(-H'2000,R3), R0"),
|
||||
instruction(0xC008, "MOV:G.W", "R1, @(-H'1800,R4)"),
|
||||
instruction(0xD000, "MOV:G.W", "@H'F900, R2", [0xF900]),
|
||||
instruction(0xD004, "BSET.B", "#7, @(-H'1400,R5)"),
|
||||
instruction(0xD008, "CMP:G.W", "@H'E124, R0", [0xE124]),
|
||||
instruction(0xD00C, "MOV:G.W", "R3, @H'F922", [0xF922]),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class TableXrefsTest(unittest.TestCase):
|
||||
def test_reports_logical_direct_static_and_dynamic_accesses(self):
|
||||
analysis = analyze_table_xrefs(payload())
|
||||
tables = {table["name"]: table for table in analysis["tables"]}
|
||||
|
||||
primary = tables["primary_value_table_candidate"]
|
||||
self.assertEqual(primary["access_count"], 3)
|
||||
self.assertEqual(primary["read_count"], 3)
|
||||
self.assertEqual(primary["static_offsets"], [0, 6, 0x124])
|
||||
static_access = primary["accesses"][0]
|
||||
self.assertEqual(static_access["index"], 6)
|
||||
self.assertEqual(static_access["logical_address"], 0xE006)
|
||||
self.assertEqual(static_access["function_label"], "loc_C000")
|
||||
self.assertEqual(static_access["semantic_candidates"][0]["confidence"], "candidate-medium")
|
||||
self.assertEqual(primary["accesses"][2]["kind"], "direct_logical_address_access")
|
||||
self.assertEqual(primary["accesses"][2]["logical_address"], 0xE124)
|
||||
|
||||
current = tables["current_value_table_candidate"]
|
||||
self.assertEqual(current["access_count"], 2)
|
||||
self.assertEqual(current["dynamic_index_count"], 1)
|
||||
self.assertEqual(current["accesses"][0]["index"], "dynamic")
|
||||
self.assertEqual(current["accesses"][0]["index_register"], "R4")
|
||||
self.assertEqual(current["accesses"][1]["direct_address"], 0xF922)
|
||||
self.assertEqual(current["accesses"][1]["offset"], 2)
|
||||
self.assertEqual(current["write_count"], 2)
|
||||
|
||||
flags = tables["flag_table_candidate"]
|
||||
self.assertEqual(flags["write_count"], 1)
|
||||
self.assertEqual(flags["dynamic_index_count"], 1)
|
||||
|
||||
lcd_terms = {
|
||||
item["term"]: item
|
||||
for item in analysis["lcd_correlation"]["term_hits"]
|
||||
}
|
||||
self.assertEqual(lcd_terms["CONNECT"]["hit_count"], 0)
|
||||
self.assertEqual(lcd_terms["COMM LINK"]["hit_count"], 1)
|
||||
self.assertEqual(
|
||||
analysis["lcd_correlation"]["display_builder_targets"][0]["target"],
|
||||
0x5A91,
|
||||
)
|
||||
|
||||
def test_text_report_names_dynamic_registers_and_functions(self):
|
||||
text = generate_table_xref_report(payload(), source_name="sample.json")
|
||||
|
||||
self.assertIn("Table/Index Cross-Reference Report for sample.json", text)
|
||||
self.assertIn("primary_value_table_candidate H'E000", text)
|
||||
self.assertIn("offset H'0006 -> H'E006", text)
|
||||
self.assertIn("offset H'0124 -> H'E124", text)
|
||||
self.assertIn("offset H'0002 at H'F922", text)
|
||||
self.assertIn("index dynamic via R4", text)
|
||||
self.assertIn("term 'CONNECT': no LCD/text candidate hits", text)
|
||||
self.assertIn("term 'COMM LINK': 1 candidate", text)
|
||||
self.assertIn("display builder xrefs: H'5A91:1", text)
|
||||
self.assertIn("loc_C000", text)
|
||||
|
||||
def test_write_json_output(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
input_path = Path(tmp) / "rom.json"
|
||||
output_path = Path(tmp) / "xrefs.json"
|
||||
input_path.write_text(json.dumps(payload()), encoding="utf-8")
|
||||
|
||||
write_table_xrefs(input_path, output_path, as_json=True)
|
||||
|
||||
written = json.loads(output_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(written["kind"], "table_xrefs")
|
||||
self.assertEqual(written["summary"]["access_count"], 6)
|
||||
self.assertEqual(written["source"], str(input_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user