import json import unittest from h8536.model import Instruction from h8536.serial_reconstruction import ( analyze_serial_reconstruction, serial_reconstruction_comment_for_instruction, serial_reconstruction_json_payload, serial_reconstruction_metadata_for_instruction, ) def ins( address: int, mnemonic: str, operands: str = "", references: list[int] | None = None, kind: str = "normal", targets: list[int] | None = None, ) -> Instruction: return Instruction( address, b"", mnemonic, operands, kind=kind, targets=targets or [], references=references or [], ) class SerialReconstructionTest(unittest.TestCase): def test_candidate_sci1_tx_frame_length_and_checksum_pattern(self): instructions = { 0x3000: ins(0x3000, "MOV:G.B", "#H'02, @H'F858", [0xF858]), 0x3004: ins(0x3004, "MOV:G.B", "@H'F858, R0", [0xF858]), 0x3008: ins(0x3008, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]), 0x300C: ins(0x300C, "MOV:G.B", "#1, @H'F9C2", [0xF9C2]), 0x301C: ins(0x301C, "MOV:E.B", "#H'5A, R1", []), 0x3020: ins(0x3020, "XOR.B", "@H'F858, R1", [0xF858]), 0x3024: ins(0x3024, "XOR.B", "@H'F859, R1", [0xF859]), 0x3028: ins(0x3028, "XOR.B", "@H'F85A, R1", [0xF85A]), 0x302C: ins(0x302C, "XOR.B", "@H'F85B, R1", [0xF85B]), 0x3030: ins(0x3030, "XOR.B", "@H'F85C, R1", [0xF85C]), 0x3034: ins(0x3034, "MOV:G.B", "R1, @H'F85D", [0xF85D]), 0x3100: ins(0x3100, "MOV:G.B", "@H'F9C2, R2", [0xF9C2]), 0x3104: ins(0x3104, "MOV:G.B", "@(-H'07A8,R2), R0", []), 0x3108: ins(0x3108, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]), 0x310C: ins(0x310C, "ADD:Q.B", "#1, @H'F9C2", [0xF9C2]), 0x3110: ins(0x3110, "CMP:G.B", "#6, @H'F9C2", [0xF9C2]), } analysis = analyze_serial_reconstruction(instructions) self.assertEqual(len(analysis["candidates"]), 1) candidate = analysis["candidates"][0] self.assertEqual(candidate["kind"], "candidate_sci1_tx_frame") self.assertEqual(candidate["channel"], "SCI1") self.assertEqual(candidate["frame_length"], 6) self.assertEqual(candidate["buffer_start"], 0xF858) self.assertEqual(candidate["checksum_address"], 0xF85D) self.assertEqual(candidate["tx_index_address"], 0xF9C2) self.assertEqual(candidate["checksum_seed"], 0x5A) self.assertEqual( [role["name"] for role in candidate["roles"]], ["tx_frame", "tx_checksum", "tx_index"], ) self.assertEqual(candidate["roles"][0]["address"], 0xF858) self.assertEqual(candidate["roles"][1]["address"], 0xF85D) self.assertEqual(candidate["roles"][2]["address"], 0xF9C2) self.assertEqual(candidate["tx_path"]["kind"], "interrupt_driven_txi") self.assertEqual(candidate["tx_path"]["initial_tdr_write_address"], 0x3008) self.assertEqual(candidate["tx_path"]["txi_indexed_tdr_write_address"], 0x3108) self.assertIn("TDRE", candidate["tx_path"]["summary"]) self.assertEqual(candidate["confidence"], "high") self.assertEqual(candidate["confidence_score"], 0.95) self.assertEqual(candidate["missing_evidence"], []) evidence_addresses = candidate["evidence_addresses"] self.assertIn(0x3034, evidence_addresses["checksum_byte"]) self.assertIn(0x3034, evidence_addresses["xor_checksum_chain"]) self.assertIn(0x3108, evidence_addresses["tx_isr_indexed_send"]) self.assertEqual(evidence_addresses["tx_index_compare_frame_length"], [0x3110]) self.assertEqual(evidence_addresses["tx_checksum_seed"], [0x301C]) checksum_comment = serial_reconstruction_comment_for_instruction(analysis, 0x3034) self.assertIn("candidate/evidence-supported SCI1 6-byte TX frame", checksum_comment) self.assertIn("checksum H'F85D", checksum_comment) self.assertIn("confidence high", checksum_comment) metadata = serial_reconstruction_metadata_for_instruction(analysis, 0x3108) self.assertEqual(metadata[0]["candidate_id"], "sci1_tx_frame_f858_len6_candidate") self.assertEqual(metadata[0]["evidence"], "tx_isr_indexed_send") self.assertIn(0x3108, metadata[0]["evidence_addresses"]) payload = serial_reconstruction_json_payload(analysis) self.assertEqual(payload["candidates"][0]["frame_length"], 6) self.assertEqual(payload["candidates"][0]["roles"][1]["name"], "tx_checksum") json.dumps(payload) def test_candidate_timer_ram_roles_from_frt1_ocia_tick(self): instructions = { 0xBEEA: ins(0xBEEA, "BCLR.B", "#5, @FRT1_TCSR", [0xFE91]), 0xBEEE: ins(0xBEEE, "TST.B", "@H'F9C0", [0xF9C0]), 0xBEF4: ins(0xBEF4, "ADD:Q.B", "#-1, @H'F9C0", [0xF9C0]), 0xBEF8: ins(0xBEF8, "TST.B", "@H'F9C1", [0xF9C1]), 0xBEFE: ins(0xBEFE, "ADD:Q.B", "#-1, @H'F9C1", [0xF9C1]), 0xBF02: ins(0xBF02, "TST.W", "@H'F9C6", [0xF9C6]), 0xBF08: ins(0xBF08, "ADD:Q.W", "#-1, @H'F9C6", [0xF9C6]), } analysis = analyze_serial_reconstruction(instructions) self.assertEqual(analysis["candidates"], []) roles = {role["name"]: role for role in analysis["ram_roles"]} self.assertEqual(set(roles), {"post_tx_report_delay", "secondary_tx_report_delay", "periodic_report_countdown"}) self.assertEqual(roles["post_tx_report_delay"]["address"], 0xF9C0) self.assertEqual(roles["secondary_tx_report_delay"]["address"], 0xF9C1) self.assertEqual(roles["periodic_report_countdown"]["address"], 0xF9C6) self.assertEqual(roles["periodic_report_countdown"]["width_bits"], 16) self.assertIn("candidate/evidence-supported", roles["post_tx_report_delay"]["confidence"]) self.assertIn("emulator-guided timer behavior", roles["post_tx_report_delay"]["caveat"]) payload = serial_reconstruction_json_payload(analysis) self.assertEqual(payload["ram_roles"][0]["name"], "post_tx_report_delay") json.dumps(payload) decrement_comment = serial_reconstruction_comment_for_instruction(analysis, 0xBEF4) self.assertIn("RAM role post_tx_report_delay", decrement_comment) self.assertIn("FRT1 OCIA periodic tick ISR", decrement_comment) metadata = serial_reconstruction_metadata_for_instruction(analysis, 0xBF08) self.assertEqual(metadata[0]["action"], "serial_reconstruction_ram_role") self.assertEqual(metadata[0]["role_name"], "periodic_report_countdown") def test_candidate_sci1_rx_frame_length_and_checksum_validation_pattern(self): instructions = { 0x4FF0: ins(0x4FF0, "BSET.B", "#7, @H'FAA4", [0xFAA4]), 0x4FF4: ins(0x4FF4, "BCLR.B", "#5, @SCI1_SSR", [0xFEDC]), 0x4FF8: ins(0x4FF8, "BCLR.B", "#4, @SCI1_SSR", [0xFEDC]), 0x4FFC: ins(0x4FFC, "BCLR.B", "#3, @SCI1_SSR", [0xFEDC]), 0x4FFE: ins(0x4FFE, "BCLR.B", "#6, @SCI1_SSR", [0xFEDC]), 0x5000: ins(0x5000, "MOV:G.B", "@SCI1_RDR, R0", [0xFEDD]), 0x5004: ins(0x5004, "MOV:G.B", "R0, @(-H'0798,R1)", []), 0x5008: ins(0x5008, "ADD:Q.B", "#1, R1", []), 0x500C: ins(0x500C, "MOV:G.B", "R1, @H'F9C3", [0xF9C3]), 0x5010: ins(0x5010, "CMP:E", "#H'06, R1", []), 0x5014: ins(0x5014, "MOV:G.B", "#H'14, @H'F9C5", [0xF9C5]), 0x5100: ins(0x5100, "CMP:G.B", "#H'06, @H'F9C3", [0xF9C3]), 0x5104: ins(0x5104, "MOV:G.W", "@H'F868, R0", [0xF868]), 0x5108: ins(0x5108, "MOV:G.W", "R0, @H'F860", [0xF860]), 0x510C: ins(0x510C, "MOV:G.W", "@H'F86A, R0", [0xF86A]), 0x5110: ins(0x5110, "MOV:G.W", "R0, @H'F862", [0xF862]), 0x5114: ins(0x5114, "MOV:G.W", "@H'F86C, R0", [0xF86C]), 0x5118: ins(0x5118, "MOV:G.W", "R0, @H'F864", [0xF864]), 0x5120: ins(0x5120, "MOV:E.B", "#H'5A, R0", []), 0x5124: ins(0x5124, "XOR.B", "@H'F860, R0", [0xF860]), 0x5128: ins(0x5128, "XOR.B", "@H'F861, R0", [0xF861]), 0x512C: ins(0x512C, "XOR.B", "@H'F862, R0", [0xF862]), 0x5130: ins(0x5130, "XOR.B", "@H'F863, R0", [0xF863]), 0x5134: ins(0x5134, "XOR.B", "@H'F864, R0", [0xF864]), 0x5138: ins(0x5138, "CMP:G.B", "@H'F865, R0", [0xF865]), } analysis = analyze_serial_reconstruction(instructions) candidate = next(item for item in analysis["candidates"] if item["kind"] == "candidate_sci1_rx_frame") self.assertEqual(candidate["frame_length"], 6) self.assertEqual(candidate["capture_buffer_start"], 0xF868) self.assertEqual(candidate["validation_buffer_start"], 0xF860) self.assertEqual(candidate["checksum_address"], 0xF865) self.assertEqual(candidate["checksum_seed"], 0x5A) self.assertIn("no explicit header", candidate["confidence_reason"]) self.assertIn("rx_rdrf_clear_before_rdr_read", candidate["evidence_addresses"]) self.assertIn("rx_eri_falls_through_to_rxi", candidate["evidence_addresses"]) self.assertTrue(candidate["rx_error_handling"]["fallthrough_to_rx_byte_path"]) self.assertTrue(candidate["rx_error_handling"]["rdrf_clear_before_rdr_read"]) self.assertEqual(candidate["rx_error_handling"]["error_latch_address"], 0xFAA4) comment = serial_reconstruction_comment_for_instruction(analysis, 0x5138) self.assertIn("candidate/evidence-supported SCI1 6-byte RX frame", comment) self.assertIn("checksum H'F865", comment) self.assertIn("confidence high", comment) self.assertIn( "ROM clears SCI1 SSR.RDRF before reading SCI1_RDR", serial_reconstruction_comment_for_instruction(analysis, 0x4FFE), ) self.assertIn( "SCI1 ERI latches FAA4.bit7", serial_reconstruction_comment_for_instruction(analysis, 0x4FF0), ) def test_lone_tdr_write_does_not_emit_reconstruction(self): instructions = { 0x4000: ins(0x4000, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]), } analysis = analyze_serial_reconstruction(instructions) self.assertEqual(analysis["candidates"], []) self.assertEqual(serial_reconstruction_comment_for_instruction(analysis, 0x4000), "") self.assertEqual(serial_reconstruction_metadata_for_instruction(analysis, 0x4000), []) self.assertEqual(serial_reconstruction_json_payload(analysis)["candidates"], []) if __name__ == "__main__": unittest.main()