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:
167
tests/test_sim_frame.py
Normal file
167
tests/test_sim_frame.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user