"""Tests for SniffSession live streaming mode.""" import struct import time import pytest from pm3py.core.protocol import ( Cmd, SNIFF_FLAG_STREAMING, SNIFF_FLAG_BUTTON_TOGGLE, SNIFF_STATE_STARTED, SNIFF_STATE_PAUSED, SNIFF_STATE_RESUMED, SNIFF_STATE_STOPPED, RESP_PREAMBLE_MAGIC, RESP_POSTAMBLE_NOCRC, ) from pm3py.core.transport import encode_ng_frame from pm3py.sniff.session import SniffSession # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- class FakeSerial: """Mock serial port that feeds pre-built response frames.""" def __init__(self, frames: list[bytes] | None = None): self._buf = bytearray() if frames: for f in frames: self._buf.extend(f) self._pos = 0 self._closed = False self._written: list[bytes] = [] @property def in_waiting(self): return len(self._buf) - self._pos def read(self, size=1): if self._closed or self._pos >= len(self._buf): time.sleep(0.01) return b"" end = min(self._pos + size, len(self._buf)) data = bytes(self._buf[self._pos:end]) self._pos = end return data def write(self, data): self._written.append(bytes(data)) return len(data) def reset_input_buffer(self): pass def reset_output_buffer(self): pass def close(self): self._closed = True def feed(self, data: bytes): """Append more data to the read buffer.""" self._buf.extend(data) def _make_resp_frame(cmd: int, status: int, data: bytes) -> bytes: """Build a raw NG response frame (USB, no CRC).""" payload_len = len(data) preamble = struct.pack("= 1 frame = ser._written[0] # Decode the command frame: magic(4) + length_ng(2) + cmd(2) + payload + postamble(2) magic, length_ng, cmd = struct.unpack_from("= 1 frame = ser._written[0] payload = frame[8:-2] idle_timeout, flags = struct.unpack_from(" stream -> pause -> resume -> more -> stopped.""" frames = [ _make_status_frame(SNIFF_STATE_STARTED), # Reader command _make_sniff_stream_frame(0, 0, 100, 1000, b"\x26\x01\x00"), # Tag response _make_sniff_stream_frame(0, 1, 200, 1500, b"\x00\x0F\xAA\xBB"), # Another reader command _make_sniff_stream_frame(0, 0, 100, 3000, b"\x20\x01"), _make_status_frame(SNIFF_STATE_PAUSED, count=3), _make_status_frame(SNIFF_STATE_RESUMED), # More frames after resume _make_sniff_stream_frame(0, 0, 100, 5000, b"\x26\x01\x00"), _make_sniff_stream_frame(0, 1, 200, 5500, b"\x00\x0F\xCC\xDD"), _make_status_frame(SNIFF_STATE_STOPPED, count=5), ] ser = FakeSerial(frames) session = SniffSession(ser) session._formatter = None session._active = True session._state = "starting" session._reader_thread = __import__("threading").Thread( target=session._stream_reader, daemon=True) session._reader_thread.start() time.sleep(0.2) session._reader_thread.join(timeout=1.0) entries = session.entries assert len(entries) == 5 # Verify timestamps are monotonically increasing timestamps = [e["timestamp"] for e in entries] assert timestamps == sorted(timestamps) assert timestamps == [1000, 1500, 3000, 5000, 5500] # Verify directions alternate for first pair assert entries[0]["direction"] == 0 # reader assert entries[1]["direction"] == 1 # tag # Final state assert session.state == "stopped" assert session._active is False def test_close_cleans_up(self): """close() stops session and closes serial.""" ser = FakeSerial() session = SniffSession(ser) session._active = True session._reader_thread = None session.close() assert session._active is False assert session.state == "stopped" assert ser._closed is True def test_entries_returns_copy(self): """entries property returns a copy, not the internal list.""" ser = FakeSerial() session = SniffSession(ser) session._entries.append({"direction": 0, "data": b"\x26"}) entries = session.entries entries.append({"direction": 1, "data": b"\x00"}) assert len(session.entries) == 1 # internal list unchanged