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>
This commit is contained in:
251
tests/test_sim_mcu_bridge.py
Normal file
251
tests/test_sim_mcu_bridge.py
Normal file
@@ -0,0 +1,251 @@
|
||||
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')
|
||||
Reference in New Issue
Block a user