CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Failing after 35s
CI / test (3.12) (pull_request) Canceled after 36s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Canceled after 21s
CI / test (3.13) (pull_request) Canceled after 31s
CI / test (3.14) (pull_request) Canceled after 31s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Canceled after 26s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Canceled after 26s
CI / test-with-coverage (pull_request) Canceled after 21s
CI / coverage-analysis (pull_request) Canceled after 0s
CI's diff-coverage gate sat at 29% because the ZMQ fixes live in code no test reached: the subscriber's socket setup, _emit_image/notify_frame_ displayed, _recv_latest and the run() frame path, plus the main window's frame slot. The subscriber now runs against a real inproc socket (the drain and the in-flight cap are the fix, so a mock would prove nothing); run() is driven with a stubbed _recv_latest that hands over one JPEG and then stops. The busy overlay gets a check that cooling is the only blue busy state. The subscriber fixture takes qapp so no QImage is built before the QApplication exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
121 lines
3.7 KiB
Python
121 lines
3.7 KiB
Python
"""Checks the binary-frame sniff in _try_parse_json: JPEG frames (0xff lead
|
|
byte) must be rejected without attempting utf-8 decode (previously raised
|
|
UnicodeDecodeError on every frame)."""
|
|
|
|
from typing import cast
|
|
|
|
from aare.gui.threads.prediction_subscriber import PredictionSubscriber
|
|
|
|
|
|
def _parse(part: bytes):
|
|
# Called unbound: _try_parse_json touches no instance state, so no
|
|
# QThread/zmq construction is needed. cast keeps basedpyright happy
|
|
# about the stand-in self.
|
|
return PredictionSubscriber._try_parse_json(cast(PredictionSubscriber, object()), part)
|
|
|
|
|
|
def test_binary_jpeg_frame_is_not_json():
|
|
assert _parse(b"\xff\xd8\xff\xe0somejpegbytes") is None
|
|
|
|
|
|
def test_json_dict_is_parsed():
|
|
assert _parse(b'{"encoding": "jpeg"}') == {"encoding": "jpeg"}
|
|
|
|
|
|
def test_json_non_dict_is_rejected():
|
|
assert _parse(b"[1, 2]") is None
|
|
assert _parse(b"") is None
|
|
|
|
|
|
# --- Frame pacing and the multipart-safe conflate -------------------------
|
|
# Real socket on an inproc endpoint: the drain and the in-flight cap are the
|
|
# fix, so they run against libzmq rather than a mock.
|
|
|
|
import time
|
|
|
|
import cv2
|
|
import numpy as np
|
|
import pytest
|
|
import zmq
|
|
from PySide6.QtGui import QImage
|
|
|
|
|
|
@pytest.fixture
|
|
def sub(qapp):
|
|
s = PredictionSubscriber("inproc://samcam-test")
|
|
yield s
|
|
# run() nulls both on its own cleanup; only tear down what is left.
|
|
if s._sock is not None:
|
|
s._sock.close(0)
|
|
if s._ctx is not None:
|
|
s._ctx.term()
|
|
|
|
|
|
def test_emit_image_caps_frames_in_flight(sub):
|
|
image = QImage(4, 4, QImage.Format.Format_RGB888)
|
|
for _ in range(sub.MAX_FRAMES_IN_FLIGHT + 1):
|
|
sub._emit_image(image)
|
|
assert sub._frames_in_flight == sub.MAX_FRAMES_IN_FLIGHT
|
|
assert sub._dropped == 1
|
|
|
|
for _ in range(sub.MAX_FRAMES_IN_FLIGHT + 1):
|
|
sub.notify_frame_displayed()
|
|
assert sub._frames_in_flight == 0 # floors at zero, never negative
|
|
|
|
|
|
def test_recv_latest_keeps_only_the_newest_message(sub):
|
|
pub = sub._ctx.socket(zmq.PUB)
|
|
pub.bind("inproc://samcam-test")
|
|
try:
|
|
# Slow joiner: publish probes until the subscription has propagated.
|
|
deadline = time.monotonic() + 5
|
|
while True:
|
|
pub.send_multipart([b"probe"])
|
|
try:
|
|
sub._sock.recv_multipart(zmq.NOBLOCK)
|
|
break
|
|
except zmq.Again:
|
|
assert time.monotonic() < deadline, "subscription never propagated"
|
|
time.sleep(0.01)
|
|
while True: # flush any extra probes
|
|
try:
|
|
sub._sock.recv_multipart(zmq.NOBLOCK)
|
|
except zmq.Again:
|
|
break
|
|
sub._dropped = 0
|
|
|
|
for payload in (b"old", b"stale", b"newest"):
|
|
pub.send_multipart([b"hdr", payload])
|
|
assert sub._recv_latest() == [b"hdr", b"newest"]
|
|
assert sub._dropped == 2
|
|
finally:
|
|
pub.close(0)
|
|
|
|
|
|
def test_run_decodes_one_frame_and_reports_the_drops(sub, qtbot):
|
|
ok, jpeg = cv2.imencode(".jpg", np.zeros((4, 6, 3), dtype=np.uint8))
|
|
assert ok
|
|
frame = [b'{"encoding": "jpeg"}', jpeg.tobytes()]
|
|
calls = []
|
|
|
|
def one_frame_then_stop():
|
|
calls.append(1)
|
|
if len(calls) == 1:
|
|
return frame
|
|
sub.running = False
|
|
raise zmq.Again
|
|
|
|
sub._recv_latest = one_frame_then_stop # type: ignore[method-assign]
|
|
sub._fps_window_start = time.perf_counter() - 10 # force the fps report
|
|
sub._dropped = 3
|
|
images = []
|
|
sub.image.connect(images.append)
|
|
|
|
sub.run()
|
|
|
|
assert len(images) == 1 and images[0].width() == 6 and images[0].height() == 4
|
|
assert sub._frames_in_flight == 1
|
|
assert sub._dropped == 0 # reported alongside the fps, then reset
|
|
assert sub._camera_available
|
|
assert sub._sock is None and sub._ctx is None # run() cleaned up
|