Files
pyGUI/tests/test_stream_reader.py
T

168 lines
5.8 KiB
Python

import time
from pygui.backend.models.frame import Frame
from pygui.serialcomm.stream_reader import StreamReader, _crc16
class FakeTransport:
"""Yield canned data then blocks (returns b'')."""
def __init__(self, lines):
self._data = b"".join(lines)
self._pos = 0
self.closed = False
def read(self, n=1):
time.sleep(0.001)
if self._pos >= len(self._data):
return b""
res = self._data[self._pos:self._pos+n]
self._pos += n
return res
def readline(self):
time.sleep(0.001)
if self._pos >= len(self._data):
return b""
idx = self._data.find(b"\n", self._pos)
if idx == -1:
res = self._data[self._pos:]
self._pos = len(self._data)
return res
res = self._data[self._pos:idx+1]
self._pos = idx + 1
return res
def close(self): self.closed = True
def parse_line(raw: str, seq: int) -> Frame:
parts = [float(x) for x in raw.split(",")]
return Frame(seq = seq, time = parts[0], values = parts[1:])
def test_reads_frames_and_counts_errors():
got, errors = [], []
transport = FakeTransport([b"0.0,149,148\n", b"garbage\n", b"0.5,150,149\n"])
r = StreamReader(transport, parse_line,
on_frame = got.append, on_error = lambda e: errors.append(e))
r.start()
time.sleep(0.1)
r.stop()
assert [f.values for f in got] == [[149.0, 148.0], [150.0, 149.0]]
assert r.error_count == 1
assert transport.closed
class WedgedTransport:
"""Simulates a worker thread stuck in a blocking read() that ignores
the stop event — read() never returns on its own. stop() must close
the port to unblock it rather than relying on the read loop to notice."""
def __init__(self):
self.closed = False
def read(self, n=1):
while not self.closed:
time.sleep(0.001)
raise OSError("port closed")
def close(self):
self.closed = True
def test_stop_closes_transport_even_if_worker_thread_is_wedged():
r = StreamReader(WedgedTransport(), parse_line,
on_frame=lambda f: None, on_error=lambda e: None)
r.start()
time.sleep(0.05) # let the thread enter the blocking read
r.stop()
assert r._transport.closed, (
"stop() must close the transport itself — otherwise a wedged "
"worker thread keeps the port open and a subsequent startStream() "
"opens a second competing handle to the same port"
)
def make_packet(payload: bytes, seq: int = 1) -> bytes:
"""Build a wire packet exactly as the device does: AA 88, msg type,
seq (LE), payload len (LE), payload, CRC16/MODBUS (LE) over the rest."""
body = (b"\xaa\x88\x01" + seq.to_bytes(2, "little")
+ len(payload).to_bytes(2, "little") + payload)
return body + _crc16(body).to_bytes(2, "little")
def parse_payload(payload: bytes, seq: int) -> Frame:
return Frame(seq=seq, time=0.0, values=[float(b) for b in payload])
def test_crc16_known_vector():
# CRC16/MODBUS check value for "123456789" is 0x4B37.
assert _crc16(b"123456789") == 0x4B37
def test_binary_stream_parses_crc_valid_packets():
got = []
data = make_packet(b"\x01\x02", seq=1) + make_packet(b"\x03\x04", seq=2)
r = StreamReader(FakeTransport([data]), parse_payload,
on_frame=got.append, on_error=lambda e: None)
r.start()
time.sleep(0.2)
r.stop()
assert [f.seq for f in got] == [1, 2]
assert r.error_count == 0
def test_corrupt_crc_packet_dropped_and_stream_continues():
got = []
bad = bytearray(make_packet(b"\x09\x09", seq=1))
bad[-1] ^= 0xFF # break the CRC
data = bytes(bad) + make_packet(b"\x01\x02", seq=2)
r = StreamReader(FakeTransport([data]), parse_payload,
on_frame=got.append, on_error=lambda e: None)
r.start()
time.sleep(0.2)
r.stop()
assert [f.seq for f in got] == [2]
assert r.error_count == 1
def test_bogus_giant_payload_len_does_not_stall_the_stream():
"""A false 0xAA 0x88 inside data yields a garbage payload_len (up to
64KB). Without a sanity cap the reader waits forever accumulating a
packet that doesn't exist — the 'frames stop, no error' symptom."""
got = []
# False sync + header claiming a 0xFFFF-byte payload, then a real packet.
bogus = b"\xaa\x88\x01\x00\x00\xff\xff"
data = bogus + make_packet(b"\x01\x02", seq=7)
r = StreamReader(FakeTransport([data]), parse_payload,
on_frame=got.append, on_error=lambda e: None)
r.start()
time.sleep(0.2)
r.stop()
assert [f.seq for f in got] == [7]
def test_quiet_port_between_scans_is_not_a_resync_failure():
"""Device pauses between measurement scans. Empty reads must not count
toward the resync give-up cap (C#'s findMessageStart waits forever) —
the old code died with TimeoutError after ~50 empty reads."""
errors = []
got = []
r = StreamReader(FakeTransport([make_packet(b"\x01\x02", seq=1)]),
parse_payload, on_frame=got.append, on_error=errors.append)
r.start()
time.sleep(0.5) # ~100 empty reads after the single packet drains
r.stop()
assert [f.seq for f in got] == [1]
assert errors == []
def test_stop_does_not_report_the_close_it_caused_as_an_error():
"""stop()'s own transport.close() unblocks the worker thread's read()
by making it raise — that's expected teardown, not a stream fault, and
must not reach on_error (Activity Log would otherwise show a fake
"Live stream error" on every ordinary Stop)."""
errors = []
r = StreamReader(WedgedTransport(), parse_line,
on_frame=lambda f: None, on_error=errors.append)
r.start()
time.sleep(0.05)
r.stop()
time.sleep(0.05) # let the worker thread's except-handler run
assert errors == []