fix(stream_reader): add ASCII hex path + binary resync guard
Family-A wafers never send 0xAA 0x88 framing — _run() now routes to _run_ascii() on first byte dispatch instead of treating the whole stream as corrupt binary. Binary path gets a 16 KB give-up cap (bytes, not attempts) so a permanently-corrupt port can't stall the reader indefinitely. Raw wire bytes go to DEBUG only; the ERROR line that surfaces in the Activity Log is kept free of internal buffer content. - add family_code param to StreamReader for ASCII decode routing - add _run_binary give-up cap + DEBUG-only hex diagnostic - fix stop(): close transport on wedged thread (binary + ASCII paths) - refactor test_stream_reader: consolidate ASCII regression + resync give-up + DEBUG-vs-ERROR log split tests; drop redundant parse_line - extend test_session_controller: live-stream gate for non-X family - StreamControlPanel.qml: thread-safe stop on tab leave
This commit is contained in:
@@ -99,9 +99,10 @@ def test_leaving_live_mode_stops_stream(controller):
|
||||
def test_binary_frame_decode_bounded_to_loaded_layout(controller, monkeypatch):
|
||||
"""Regression for phantom sensor #239 polluting MIN/MAX/AVG: the wire
|
||||
payload carries a fixed-width channel array (up to 244 slots) wider than
|
||||
any single wafer family's real sensor count. Family "A" (aepwafer.yaml)
|
||||
has 48 real sensors — everything past that in the payload is an
|
||||
unpopulated hardware slot and must never reach Frame.values."""
|
||||
any single wafer family's real sensor count. Family "X" (xwafer.yaml,
|
||||
the only family live streaming is supported on) has 80 real sensors —
|
||||
everything past that in the payload is an unpopulated hardware slot and
|
||||
must never reach Frame.values."""
|
||||
fake_transport = MagicMock()
|
||||
fake_transport.read.return_value = b""
|
||||
monkeypatch.setattr(
|
||||
@@ -109,13 +110,13 @@ def test_binary_frame_decode_bounded_to_loaded_layout(controller, monkeypatch):
|
||||
lambda *a, **k: fake_transport,
|
||||
)
|
||||
|
||||
controller.startStream("COM_FAKE", "A")
|
||||
controller.startStream("COM_FAKE", "X")
|
||||
try:
|
||||
assert len(controller._sensors) == 48 # sanity: aepwafer.yaml loaded
|
||||
assert len(controller._sensors) == 80 # sanity: xwafer.yaml loaded
|
||||
parse = controller._reader._parse
|
||||
payload = bytes(244 * 2) # full-width wire payload, well beyond the real 48
|
||||
payload = bytes(244 * 2) # full-width wire payload, well beyond the real 80
|
||||
frame = parse(payload, seq=1)
|
||||
assert len(frame.values) == 48
|
||||
assert len(frame.values) == 80
|
||||
finally:
|
||||
controller.stopStream()
|
||||
|
||||
@@ -135,6 +136,62 @@ def test_live_error_surfaces_message_and_stops_stream(controller):
|
||||
assert controller._reader is None
|
||||
|
||||
|
||||
def test_start_stream_refuses_non_x_family(controller, monkeypatch):
|
||||
"""C# parity: only X-family wafer firmware supports live streaming.
|
||||
Family A produced ~10s of unrelated ASCII output, not real telemetry —
|
||||
startStream must refuse before ever opening the port."""
|
||||
open_port = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"pygui.serialcomm.serial_port.SerialPort.open_port", open_port
|
||||
)
|
||||
error_seen = MagicMock()
|
||||
controller.liveError.connect(error_seen)
|
||||
|
||||
controller.startStream("COM_FAKE", "A")
|
||||
|
||||
open_port.assert_not_called()
|
||||
error_seen.assert_called_once()
|
||||
assert "X-family" in error_seen.call_args[0][0]
|
||||
assert controller._reader is None
|
||||
|
||||
|
||||
def test_start_stream_allows_x_family(controller, monkeypatch):
|
||||
fake_transport = MagicMock()
|
||||
fake_transport.read.return_value = b""
|
||||
monkeypatch.setattr(
|
||||
"pygui.serialcomm.serial_port.SerialPort.open_port",
|
||||
lambda *a, **k: fake_transport,
|
||||
)
|
||||
error_seen = MagicMock()
|
||||
controller.liveError.connect(error_seen)
|
||||
|
||||
controller.startStream("COM_FAKE", "X")
|
||||
try:
|
||||
error_seen.assert_not_called()
|
||||
assert controller._reader is not None
|
||||
finally:
|
||||
controller.stopStream()
|
||||
|
||||
|
||||
def test_stop_stream_skips_d2s_write_when_reader_already_exited(controller):
|
||||
"""Reader's own worker thread can exit and close the transport (e.g.
|
||||
after a resync give-up) before stopStream() runs on the main thread.
|
||||
Writing D2S to that already-closed transport is a guaranteed EBADF —
|
||||
skip it instead of racing the close."""
|
||||
controller.setMode("live")
|
||||
fake_reader = MagicMock()
|
||||
fake_reader._thread = MagicMock()
|
||||
fake_reader._thread.is_alive.return_value = False
|
||||
fake_transport = MagicMock()
|
||||
fake_transport.is_open = True
|
||||
fake_reader._transport = fake_transport
|
||||
controller._reader = fake_reader
|
||||
|
||||
controller.stopStream()
|
||||
|
||||
fake_transport.write.assert_not_called()
|
||||
|
||||
|
||||
def test_compare_files_integration(controller):
|
||||
# Mock the comparisonResult signal
|
||||
mock_slot = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user