import io import json import tempfile import unittest from pathlib import Path from h8536.ccu_seed_hints import ( analyze_ccu_seed_hints, checksum, encode_host_frame, frame_hex, main, selector_bytes, write_ccu_seed_hints, ) def reference(address: int) -> dict: return {"address": address} def instruction(address: int, mnemonic: str, operands: str = "", refs: list[int] | None = None) -> dict: return { "address": address, "mnemonic": mnemonic, "operands": operands, "text": f"{mnemonic} {operands}".strip(), "references": [reference(item) for item in (refs or [])], "targets": [], } def payload() -> dict: return { "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'E1EC", [0xE1EC]), ], "call_graph": { "nodes": [ {"start": 0xC000, "end": 0xC0FF, "label": "loc_C000"}, ], }, "indirect_flow": { "sites": [ { "table": { "base": 0x28A6, "entries": [ {"index": 0, "entry_address": 0x28A6, "target": 0x2CB9, "target_label": "loc_2CB9"}, {"index": 1, "entry_address": 0x28A8, "target": 0x1234, "target_label": "loc_1234"}, {"index": 2, "entry_address": 0x28AA, "target": 0x1234, "target_label": "loc_1234"}, ], }, } ], }, } class CcuSeedHintsTest(unittest.TestCase): def test_selector_encoding_matches_loc_622b_ranges(self): self.assertEqual(selector_bytes(0x000), (0x00, 0x00)) self.assertEqual(selector_bytes(0x07F), (0x00, 0x7F)) self.assertEqual(selector_bytes(0x080), (0x01, 0x00)) self.assertEqual(selector_bytes(0x17F), (0x01, 0xFF)) self.assertEqual(selector_bytes(0x180), (0x02, 0x00)) self.assertEqual(selector_bytes(0x1FF), (0x02, 0x7F)) def test_frame_encoding_uses_xor_seed(self): frame = encode_host_frame(0x00, 0x000, 0x8080) self.assertEqual(frame, [0x00, 0x00, 0x00, 0x80, 0x80, 0x5A]) self.assertEqual(checksum(frame[:5]), frame[5]) self.assertEqual(frame_hex(frame), "00 00 00 80 80 5A") def test_analysis_emits_seed_plan_and_selector_reasons(self): analysis = analyze_ccu_seed_hints(payload(), rom_path=None) by_selector = { int(item["selector"]): item for item in analysis["selector_candidates"] } self.assertEqual(analysis["kind"], "ccu_seed_hints") self.assertIn(0x000, by_selector) self.assertIn(0x0F6, by_selector) self.assertIn("00 00 00 80 80 5A", [step["frame"] for step in analysis["seed_plan"]["steps"]]) self.assertIn("01 01 76 00 00 2C", [step["readback_frame"] for step in analysis["seed_plan"]["steps"]]) def test_write_json_output(self): with tempfile.TemporaryDirectory() as tmp: input_path = Path(tmp) / "rom.json" output_path = Path(tmp) / "hints.json" input_path.write_text(json.dumps(payload()), encoding="utf-8") write_ccu_seed_hints(input_path, output_path, rom_path=None, as_json=True) written = json.loads(output_path.read_text(encoding="utf-8")) self.assertEqual(written["kind"], "ccu_seed_hints") self.assertGreaterEqual(written["summary"]["candidate_count"], 1) def test_cli_writes_text_report(self): with tempfile.TemporaryDirectory() as tmp: input_path = Path(tmp) / "rom.json" output_path = Path(tmp) / "hints.txt" input_path.write_text(json.dumps(payload()), encoding="utf-8") stdout = io.StringIO() rc = main([str(input_path), "--rom", str(Path(tmp) / "missing.bin"), "--out", str(output_path)], stdout=stdout) self.assertEqual(rc, 0) self.assertIn("wrote", stdout.getvalue()) self.assertIn("Candidate Fake-CCU Seed Plan", output_path.read_text(encoding="utf-8")) if __name__ == "__main__": unittest.main()