Files
pm3py/tests/test_sim_reader.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

215 lines
6.5 KiB
Python

"""Tests for pm3py.sim.reader — Reader ABC, ScriptedReader, InteractiveReader."""
import asyncio
import pytest
from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.reader import Reader, ScriptedReader, InteractiveReader, ReaderStep, StepResult
from pm3py.sim.transponder import Transponder
class EchoTransponder(Transponder):
"""Returns the received frame back as response."""
async def power_on(self) -> None:
pass
async def power_off(self) -> None:
pass
async def handle_frame(self, frame: RFFrame) -> RFFrame | None:
return frame
@property
def state(self) -> str:
return "ECHO"
class FixedTransponder(Transponder):
"""Returns a fixed response to any frame."""
def __init__(self, response: RFFrame):
super().__init__()
self._response = response
async def power_on(self) -> None:
pass
async def power_off(self) -> None:
pass
async def handle_frame(self, frame: RFFrame) -> RFFrame | None:
return self._response
@property
def state(self) -> str:
return "FIXED"
class SimpleReader(Reader):
"""Minimal reader implementation for testing the base class."""
async def run(self) -> dict:
resp = await self.transceive(RFFrame.from_hex("26"))
return {"response": resp}
class TestReaderBase:
"""Test Reader ABC transceive."""
def test_transceive_sends_and_receives(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
loop.run_until_complete(medium.attach(EchoTransponder()))
reader = SimpleReader(medium)
result = loop.run_until_complete(reader.run())
assert result["response"] is not None
assert result["response"].data == b"\x26"
def test_transceive_no_transponder_returns_none(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
reader = SimpleReader(medium)
result = loop.run_until_complete(reader.run())
assert result["response"] is None
class TestScriptedReader:
"""Test scripted reader sequence execution."""
def test_run_script_all_pass(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
response = RFFrame.from_hex("4400")
loop.run_until_complete(medium.attach(FixedTransponder(response)))
steps = [
ReaderStep(
frame=RFFrame.from_hex("26"),
description="Send REQA",
check=lambda r: r is not None and r.data == b"\x44\x00",
),
ReaderStep(
frame=RFFrame.from_hex("9320"),
description="Send ANTICOL",
check=lambda r: r is not None,
),
]
scripted = ScriptedReader(medium)
results = loop.run_until_complete(scripted.run_script(steps))
assert len(results) == 2
assert all(r.passed for r in results)
def test_run_script_check_fails(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
response = RFFrame.from_hex("4400")
loop.run_until_complete(medium.attach(FixedTransponder(response)))
steps = [
ReaderStep(
frame=RFFrame.from_hex("26"),
description="Send REQA",
check=lambda r: r is not None and r.data == b"\xFF\xFF", # wrong
),
]
scripted = ScriptedReader(medium)
results = loop.run_until_complete(scripted.run_script(steps))
assert len(results) == 1
assert not results[0].passed
def test_stop_on_fail(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
response = RFFrame.from_hex("4400")
loop.run_until_complete(medium.attach(FixedTransponder(response)))
steps = [
ReaderStep(
frame=RFFrame.from_hex("26"),
description="Fail here",
check=lambda r: False,
stop_on_fail=True,
),
ReaderStep(
frame=RFFrame.from_hex("9320"),
description="Should not run",
),
]
scripted = ScriptedReader(medium)
results = loop.run_until_complete(scripted.run_script(steps))
assert len(results) == 1 # stopped after first failure
def test_no_check_means_pass(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
loop.run_until_complete(medium.attach(FixedTransponder(RFFrame.from_hex("00"))))
steps = [
ReaderStep(frame=RFFrame.from_hex("26"), description="No check"),
]
scripted = ScriptedReader(medium)
results = loop.run_until_complete(scripted.run_script(steps))
assert results[0].passed
class TestInteractiveReader:
"""Test interactive reader step-through."""
def test_send_and_receive(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
loop.run_until_complete(medium.attach(EchoTransponder()))
interactive = InteractiveReader(medium)
resp = loop.run_until_complete(interactive.send(RFFrame.from_hex("26")))
assert resp is not None
assert resp.data == b"\x26"
def test_send_hex_convenience(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
loop.run_until_complete(medium.attach(EchoTransponder()))
interactive = InteractiveReader(medium)
resp = loop.run_until_complete(interactive.send_hex("A5"))
assert resp is not None
assert resp.data == b"\xA5"
def test_history_tracks_exchanges(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
loop.run_until_complete(medium.attach(EchoTransponder()))
interactive = InteractiveReader(medium)
loop.run_until_complete(interactive.send_hex("26"))
loop.run_until_complete(interactive.send_hex("9320"))
assert len(interactive.history) == 2
assert interactive.history[0][0].data == b"\x26"
assert interactive.history[1][0].data == b"\x93\x20"
def test_dump_trace_returns_string(self):
loop = asyncio.get_event_loop()
medium = SoftwareMedium()
loop.run_until_complete(medium.attach(EchoTransponder()))
interactive = InteractiveReader(medium)
loop.run_until_complete(interactive.send_hex("26"))
trace = interactive.dump_trace()
assert isinstance(trace, str)
assert len(trace) > 0