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

168 lines
5.8 KiB
Python

"""Tests for pm3py.sim.frame — RFFrame with bit-level granularity."""
import pytest
from bitarray import bitarray
from pm3py.sim.frame import RFFrame
class TestRFFrameConstruction:
"""Test RFFrame creation from various inputs."""
def test_from_bytes_creates_frame_with_correct_bits(self):
frame = RFFrame.from_bytes(b"\xA5")
assert frame.bit_count == 8
assert frame.data == b"\xA5"
def test_from_bytes_multi_byte(self):
frame = RFFrame.from_bytes(b"\x01\x02\x03")
assert frame.bit_count == 24
assert frame.data == b"\x01\x02\x03"
def test_from_bytes_empty(self):
frame = RFFrame.from_bytes(b"")
assert frame.bit_count == 0
assert frame.data == b""
def test_from_hex_creates_frame(self):
frame = RFFrame.from_hex("A5B6")
assert frame.bit_count == 16
assert frame.data == b"\xA5\xB6"
def test_from_hex_case_insensitive(self):
frame = RFFrame.from_hex("a5b6")
assert frame.data == b"\xA5\xB6"
def test_from_bits_with_partial_byte(self):
"""7-bit NVB response during anticollision."""
bits = bitarray("1010101")
frame = RFFrame(bits=bits, bit_count=7)
assert frame.bit_count == 7
# data property truncates to full bytes
assert frame.data == b""
def test_from_bits_with_full_and_partial(self):
"""12 bits: 8 full + 4 partial."""
bits = bitarray("101010110011")
frame = RFFrame(bits=bits, bit_count=12)
assert frame.bit_count == 12
assert frame.data == b"\xAB" # first 8 bits = 10101011
class TestRFFrameImmutability:
"""RFFrame should be frozen/immutable."""
def test_cannot_set_attributes(self):
frame = RFFrame.from_bytes(b"\x00")
with pytest.raises(AttributeError):
frame.bit_count = 99
def test_cannot_set_bits(self):
frame = RFFrame.from_bytes(b"\x00")
with pytest.raises(AttributeError):
frame.bits = bitarray("11111111")
class TestRFFrameOptionalFields:
"""Test optional fields: parity, crc, collision_positions, timestamp."""
def test_defaults_are_none_and_zero(self):
frame = RFFrame.from_bytes(b"\x00")
assert frame.parity is None
assert frame.crc is None
assert frame.collision_positions is None
assert frame.timestamp_us == 0
def test_with_parity(self):
parity = bitarray("1")
frame = RFFrame.from_bytes(b"\xA5", parity=parity)
assert frame.parity == parity
def test_with_crc(self):
frame = RFFrame.from_bytes(b"\xA5", crc=b"\x12\x34")
assert frame.crc == b"\x12\x34"
def test_with_collision_positions(self):
frame = RFFrame.from_bytes(b"\xA5", collision_positions=[2, 5])
assert frame.collision_positions == [2, 5]
def test_with_timestamp(self):
frame = RFFrame.from_bytes(b"\xA5", timestamp_us=12345)
assert frame.timestamp_us == 12345
def test_has_collision_property(self):
clean = RFFrame.from_bytes(b"\xA5")
assert not clean.has_collision
collided = RFFrame.from_bytes(b"\xA5", collision_positions=[3])
assert collided.has_collision
class TestRFFrameCollisionMerge:
"""Test bit-level collision merging of multiple frames."""
def test_merge_identical_frames_no_collision(self):
f1 = RFFrame.from_bytes(b"\xA5")
f2 = RFFrame.from_bytes(b"\xA5")
merged = RFFrame.merge([f1, f2])
assert merged.data == b"\xA5"
assert merged.collision_positions == []
def test_merge_different_frames_detects_collision(self):
f1 = RFFrame.from_bytes(b"\xFF") # 11111111
f2 = RFFrame.from_bytes(b"\x00") # 00000000
merged = RFFrame.merge([f1, f2])
# All 8 bit positions should have collisions
assert len(merged.collision_positions) == 8
def test_merge_partial_difference(self):
f1 = RFFrame.from_hex("A0") # 10100000
f2 = RFFrame.from_hex("A5") # 10100101
merged = RFFrame.merge([f1, f2])
# Bits 5 and 7 differ (0-indexed from MSB: positions 5 and 7)
assert merged.collision_positions is not None
assert len(merged.collision_positions) == 2
def test_merge_single_frame_returns_copy(self):
f1 = RFFrame.from_bytes(b"\xAB")
merged = RFFrame.merge([f1])
assert merged.data == b"\xAB"
assert merged.collision_positions == []
def test_merge_empty_list_returns_none(self):
result = RFFrame.merge([])
assert result is None
def test_merge_three_frames(self):
f1 = RFFrame.from_hex("FF") # 11111111
f2 = RFFrame.from_hex("FE") # 11111110
f3 = RFFrame.from_hex("FF") # 11111111
merged = RFFrame.merge([f1, f2, f3])
# Only bit 7 (LSB) has collision (1 vs 0 vs 1)
assert len(merged.collision_positions) == 1
def test_merge_different_lengths(self):
"""Shorter frame treated as not responding for extra bits."""
short = RFFrame(bits=bitarray("1010"), bit_count=4)
long = RFFrame.from_bytes(b"\xAB") # 10101011, 8 bits
merged = RFFrame.merge([short, long])
# First 4 bits: both respond, check for collisions
# Bits 4-7: only long responds, no collision
assert merged.bit_count == 8
def test_collision_bit_defaults_to_one(self):
"""Per ISO 14443-A: collision defaults to 1 (Manchester)."""
f1 = RFFrame.from_bytes(b"\x00") # 00000000
f2 = RFFrame.from_bytes(b"\xFF") # 11111111
merged = RFFrame.merge([f1, f2])
# All collision bits should be 1
assert all(merged.bits[i] == 1 for i in merged.collision_positions)
class TestRFFrameRepr:
"""Test string representation."""
def test_repr_shows_hex(self):
frame = RFFrame.from_hex("A5B6")
r = repr(frame)
assert "a5b6" in r.lower() or "A5B6" in r