Files
pm3py/tests/test_sim_mcu_bridge.py
michael 668170457e feat: migrate sim framework from worktree to master
Migrates all 43 sim modules and 23 test files (~7.8k LOC, 686 tests)
from .worktrees/sim-framework/ into pm3py/sim/. Import fixes:
- 4 files: ..protocol/..transport → ..core.protocol/..core.transport
- trace_fmt.py: pm3py.hf_15 → pm3py.trace.ndef
- test_sim_pm3medium.py: flat imports → core.*
- test_sim_trace_fmt.py: flat imports → core.*
- Added sim Cmd entries to core/protocol.py (EML_SETMEM, SIM_TABLE_*)

751 tests passing (686 sim + 65 core).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:04:55 -07:00

252 lines
7.9 KiB
Python

import pytest
from pm3py.sim.mcu_protocol import (
cobs_encode, cobs_decode,
MsgType, build_frame, parse_frame,
StreamDeframer,
)
class TestCobsCodec:
def test_encode_empty(self):
assert cobs_encode(b"") == b"\x01"
def test_encode_no_zeros(self):
assert cobs_encode(b"\x01\x02\x03") == b"\x04\x01\x02\x03"
def test_encode_single_zero(self):
assert cobs_encode(b"\x00") == b"\x01\x01"
def test_encode_zeros_in_data(self):
assert cobs_encode(b"\x01\x00\x02") == b"\x02\x01\x02\x02"
def test_decode_roundtrip(self):
for data in [b"", b"\x00", b"\x01\x02\x03", b"\x00\x00\x00",
b"hello world", bytes(range(256))]:
assert cobs_decode(cobs_encode(data)) == data
def test_decode_empty_raises(self):
with pytest.raises(ValueError):
cobs_decode(b"")
def test_decode_invalid_raises(self):
with pytest.raises(ValueError):
cobs_decode(b"\xFF")
class TestMessageFraming:
def test_build_frame_no_payload(self):
frame = build_frame(MsgType.RESET)
assert frame[-1] == 0x00
decoded = cobs_decode(frame[:-1])
assert decoded == bytes([0xF0])
def test_build_frame_with_payload(self):
frame = build_frame(MsgType.SET_ED, bytes([0x01]))
decoded = cobs_decode(frame[:-1])
assert decoded == bytes([0x20, 0x01])
def test_parse_frame_i2c_write(self):
raw = bytes([MsgType.I2C_WRITE, 0x20, 0x00, 0xDE, 0xAD])
msg_type, payload = parse_frame(raw)
assert msg_type == MsgType.I2C_WRITE
assert payload == bytes([0x20, 0x00, 0xDE, 0xAD])
def test_parse_frame_mcu_ready(self):
raw = bytes([MsgType.MCU_READY, 0x01, 0x03])
msg_type, payload = parse_frame(raw)
assert msg_type == MsgType.MCU_READY
assert payload == bytes([0x01, 0x03])
def test_parse_frame_empty_payload(self):
raw = bytes([MsgType.I2C_STOP])
msg_type, payload = parse_frame(raw)
assert msg_type == MsgType.I2C_STOP
assert payload == b""
def test_parse_frame_too_short(self):
with pytest.raises(ValueError):
parse_frame(b"")
class TestStreamDeframer:
def test_single_complete_frame(self):
df = StreamDeframer()
frame = build_frame(MsgType.MCU_READY, bytes([0x01, 0x00]))
msgs = df.feed(frame)
assert len(msgs) == 1
assert msgs[0][0] == MsgType.MCU_READY
assert msgs[0][1] == bytes([0x01, 0x00])
def test_partial_then_complete(self):
df = StreamDeframer()
frame = build_frame(MsgType.I2C_WRITE, bytes([0x20, 0x00, 0xAB]))
mid = len(frame) // 2
msgs1 = df.feed(frame[:mid])
assert len(msgs1) == 0
msgs2 = df.feed(frame[mid:])
assert len(msgs2) == 1
assert msgs2[0][0] == MsgType.I2C_WRITE
def test_multiple_frames_in_one_chunk(self):
df = StreamDeframer()
f1 = build_frame(MsgType.I2C_WRITE, bytes([0x20, 0x00]))
f2 = build_frame(MsgType.I2C_STOP)
msgs = df.feed(f1 + f2)
assert len(msgs) == 2
assert msgs[0][0] == MsgType.I2C_WRITE
assert msgs[1][0] == MsgType.I2C_STOP
def test_corrupted_frame_skipped(self):
df = StreamDeframer()
garbage = bytes([0xFF, 0xFE, 0x00])
valid = build_frame(MsgType.MCU_READY, bytes([0x01, 0x00]))
msgs = df.feed(garbage + valid)
assert len(msgs) == 1
assert msgs[0][0] == MsgType.MCU_READY
def test_empty_feed(self):
df = StreamDeframer()
msgs = df.feed(b"")
assert len(msgs) == 0
import time
from pm3py.sim.mcu_bridge import McuBridge
class MockSerial:
"""Mock serial port for testing McuBridge."""
def __init__(self):
self._rx_buf = bytearray()
self._tx_buf = bytearray()
self.is_open = True
self.timeout = 0.1
def read(self, size=1):
if not self._rx_buf:
time.sleep(0.01)
return b""
data = bytes(self._rx_buf[:size])
self._rx_buf = self._rx_buf[size:]
return data
def write(self, data):
self._tx_buf.extend(data)
return len(data)
def inject(self, data: bytes):
self._rx_buf.extend(data)
def get_sent(self) -> bytes:
data = bytes(self._tx_buf)
self._tx_buf.clear()
return data
def close(self):
self.is_open = False
class TestMcuBridge:
def test_connect_receives_mcu_ready(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
ready_events = []
bridge.on_ready = lambda ver, caps: ready_events.append((ver, caps))
bridge.start()
mock.inject(build_frame(MsgType.MCU_READY, bytes([0x01, 0x03])))
time.sleep(0.15)
bridge.stop()
assert len(ready_events) == 1
assert ready_events[0] == (0x01, 0x03)
def test_i2c_write_callback(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
writes = []
bridge.on_i2c_write = lambda addr, data: writes.append((addr, data))
bridge.start()
mock.inject(build_frame(MsgType.I2C_WRITE, bytes([0x20, 0x00, 0xDE, 0xAD])))
time.sleep(0.15)
bridge.stop()
assert len(writes) == 1
assert writes[0] == (0x2000, bytes([0xDE, 0xAD]))
def test_i2c_read_req_callback(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
reads = []
bridge.on_i2c_read_req = lambda addr, length: reads.append((addr, length))
bridge.start()
mock.inject(build_frame(MsgType.I2C_READ_REQ, bytes([0x20, 0x00, 0x04])))
time.sleep(0.15)
bridge.stop()
assert len(reads) == 1
assert reads[0] == (0x2000, 4)
def test_send_set_ed(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
bridge.start()
bridge.send_set_ed(True)
time.sleep(0.05)
bridge.stop()
sent = mock.get_sent()
assert len(sent) > 0
assert sent[-1] == 0x00
msg_type, payload = parse_frame(cobs_decode(sent[:-1]))
assert msg_type == MsgType.SET_ED
assert payload == bytes([0x01])
def test_send_set_eh_voltage(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
bridge.start()
bridge.send_set_eh_voltage(2400)
time.sleep(0.05)
bridge.stop()
sent = mock.get_sent()
assert sent[-1] == 0x00
msg_type, payload = parse_frame(cobs_decode(sent[:-1]))
assert msg_type == MsgType.SET_EH_VOLTAGE
assert payload == bytes([0x09, 0x60])
def test_send_write_sram(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
bridge.start()
bridge.send_write_sram(0x10, bytes([0xAA, 0xBB, 0xCC]))
time.sleep(0.05)
bridge.stop()
sent = mock.get_sent()
assert sent[-1] == 0x00
msg_type, payload = parse_frame(cobs_decode(sent[:-1]))
assert msg_type == MsgType.WRITE_SRAM
assert payload == bytes([0x10, 0xAA, 0xBB, 0xCC])
def test_send_i2c_read_response(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
bridge.start()
bridge.send_i2c_read_response(bytes([0x01, 0x02, 0x03, 0x04]))
time.sleep(0.05)
bridge.stop()
sent = mock.get_sent()
assert sent[-1] == 0x00
msg_type, payload = parse_frame(cobs_decode(sent[:-1]))
assert msg_type == MsgType.I2C_READ_RESPONSE
assert payload == bytes([0x01, 0x02, 0x03, 0x04])
def test_stop_is_idempotent(self):
mock = MockSerial()
bridge = McuBridge(port=mock)
bridge.start()
bridge.stop()
bridge.stop() # should not raise
class TestMcuBridgeOpen:
def test_open_creates_bridge(self):
from pm3py.sim import McuBridge as ImportedBridge
assert ImportedBridge is McuBridge
assert hasattr(McuBridge, 'open')