158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
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 selector 0x003 -> H'E006", text)
|
|
self.assertIn("offset H'0124 selector 0x092 -> H'E124", text)
|
|
self.assertIn("offset H'0002 selector 0x001 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()
|