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:
211
tests/test_sim_dual_session.py
Normal file
211
tests/test_sim_dual_session.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""Tests for DualInterfaceSession coordinator."""
|
||||
import time
|
||||
import pytest
|
||||
from pm3py.sim.dual_session import DualInterfaceSession
|
||||
from pm3py.sim.mcu_bridge import McuBridge
|
||||
from pm3py.sim.mcu_protocol import MsgType, build_frame, cobs_decode, parse_frame
|
||||
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
||||
|
||||
|
||||
class MockSimSession:
|
||||
"""Mock SimSession for testing — no real PM3 hardware."""
|
||||
def __init__(self):
|
||||
self.on_field_strength = None
|
||||
self._started = False
|
||||
self._stopped = False
|
||||
self._tag = None
|
||||
self._trace = False
|
||||
|
||||
def start_15693(self, tag, trace=False):
|
||||
self._started = True
|
||||
self._tag = tag
|
||||
self._trace = trace
|
||||
|
||||
def stop(self):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
class MockSerial:
|
||||
"""Mock serial port for 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
|
||||
|
||||
|
||||
def _decode_sent(mock_serial) -> list[tuple[int, bytes]]:
|
||||
"""Decode all COBS-framed messages sent to mock serial."""
|
||||
raw = mock_serial.get_sent()
|
||||
messages = []
|
||||
while raw:
|
||||
zero_idx = raw.find(b"\x00")
|
||||
if zero_idx == -1:
|
||||
break
|
||||
frame = raw[:zero_idx]
|
||||
raw = raw[zero_idx + 1:]
|
||||
if frame:
|
||||
decoded = cobs_decode(frame)
|
||||
messages.append(parse_frame(decoded))
|
||||
return messages
|
||||
|
||||
|
||||
class TestDualInterfaceSession:
|
||||
def _make_session(self):
|
||||
"""Create a DualInterfaceSession with mocks."""
|
||||
mock_serial = MockSerial()
|
||||
sim = MockSimSession()
|
||||
mcu = McuBridge(port=mock_serial)
|
||||
tag = Ntag5LinkTag()
|
||||
tag.sram_enable = True
|
||||
tag.arbiter_mode = 2 # PASSTHROUGH
|
||||
session = DualInterfaceSession(sim, mcu, tag)
|
||||
return session, sim, mcu, mock_serial, tag
|
||||
|
||||
def test_start_wires_callbacks(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
|
||||
assert sim._started
|
||||
assert mcu.on_i2c_write is not None
|
||||
assert mcu.on_i2c_read_req is not None
|
||||
assert tag.ed_pin_callback is not None
|
||||
assert sim.on_field_strength is not None
|
||||
|
||||
session.stop()
|
||||
|
||||
def test_start_pushes_initial_sram(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
tag._sram[0:4] = b"\xDE\xAD\xBE\xEF"
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
session.stop()
|
||||
|
||||
msgs = _decode_sent(mock_serial)
|
||||
# Should have WRITE_SRAM with full 256-byte SRAM
|
||||
sram_msgs = [m for m in msgs if m[0] == MsgType.WRITE_SRAM]
|
||||
assert len(sram_msgs) == 1
|
||||
assert sram_msgs[0][1][0] == 0x00 # offset 0
|
||||
assert sram_msgs[0][1][1:5] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
def test_i2c_write_updates_tag_sram(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
tag.config_pt_transfer_dir = False # I2C→NFC
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
|
||||
# Simulate MCU pushing an I2C write event
|
||||
mock_serial.inject(build_frame(MsgType.I2C_WRITE,
|
||||
bytes([0x20, 0x00, 0xCA, 0xFE])))
|
||||
time.sleep(0.15)
|
||||
session.stop()
|
||||
|
||||
# Tag SRAM should have the data
|
||||
assert tag._sram[0:2] == b"\xCA\xFE"
|
||||
|
||||
def test_i2c_read_req_responds(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
# Preload SRAM
|
||||
tag._sram[0:4] = b"\x01\x02\x03\x04"
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
|
||||
# Clear initial WRITE_SRAM from get_sent
|
||||
mock_serial.get_sent()
|
||||
|
||||
# Simulate MCU requesting I2C read
|
||||
mock_serial.inject(build_frame(MsgType.I2C_READ_REQ,
|
||||
bytes([0x20, 0x00, 0x04])))
|
||||
time.sleep(0.15)
|
||||
session.stop()
|
||||
|
||||
msgs = _decode_sent(mock_serial)
|
||||
resp_msgs = [m for m in msgs if m[0] == MsgType.I2C_READ_RESPONSE]
|
||||
assert len(resp_msgs) == 1
|
||||
assert resp_msgs[0][1] == b"\x01\x02\x03\x04"
|
||||
|
||||
def test_ed_pin_relayed_to_mcu(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
mock_serial.get_sent() # clear initial WRITE_SRAM
|
||||
|
||||
# Trigger ED on tag
|
||||
tag.assert_ed()
|
||||
time.sleep(0.05)
|
||||
session.stop()
|
||||
|
||||
msgs = _decode_sent(mock_serial)
|
||||
ed_msgs = [m for m in msgs if m[0] == MsgType.SET_ED]
|
||||
assert len(ed_msgs) == 1
|
||||
assert ed_msgs[0][1] == bytes([0x01])
|
||||
|
||||
def test_field_strength_above_threshold_activates_eh(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
session.eh_threshold = 500
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
mock_serial.get_sent() # clear initial
|
||||
|
||||
# Simulate field strength above threshold
|
||||
sim.on_field_strength(600)
|
||||
time.sleep(0.05)
|
||||
session.stop()
|
||||
|
||||
msgs = _decode_sent(mock_serial)
|
||||
eh_msgs = [m for m in msgs if m[0] == MsgType.SET_EH]
|
||||
assert len(eh_msgs) == 1
|
||||
assert eh_msgs[0][1] == bytes([0x01])
|
||||
assert tag.nfc_field_ok
|
||||
|
||||
def test_field_strength_below_threshold_deactivates_eh(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
session.eh_threshold = 500
|
||||
session._eh_active = True # pretend already active
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
mock_serial.get_sent()
|
||||
|
||||
sim.on_field_strength(300)
|
||||
time.sleep(0.05)
|
||||
session.stop()
|
||||
|
||||
msgs = _decode_sent(mock_serial)
|
||||
eh_msgs = [m for m in msgs if m[0] == MsgType.SET_EH]
|
||||
assert len(eh_msgs) == 1
|
||||
assert eh_msgs[0][1] == bytes([0x00])
|
||||
|
||||
def test_stop_unhooks_callbacks(self):
|
||||
session, sim, mcu, mock_serial, tag = self._make_session()
|
||||
session.start()
|
||||
time.sleep(0.05)
|
||||
session.stop()
|
||||
|
||||
assert mcu.on_i2c_write is None
|
||||
assert mcu.on_i2c_read_req is None
|
||||
assert tag.ed_pin_callback is None
|
||||
assert sim.on_field_strength is None
|
||||
Reference in New Issue
Block a user