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>
215 lines
6.5 KiB
Python
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
|