test: lift diff coverage over the 80 percent gate
CI / lint (push) Skipped
CI / test (3.11) (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) Successful in 1m3s
CI / test (3.11) (pull_request) Successful in 1m25s
CI / test (3.12) (pull_request) Successful in 1m25s
CI / test (3.13) (pull_request) Successful in 1m24s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m32s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m37s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m41s
CI / test-with-coverage (pull_request) Successful in 2m4s
CI / coverage-analysis (pull_request) Successful in 3s

Promote the illumination panel's __main__ self-check to a real pytest
file so CI counts it, add busy-overlay render checks (status text
paints, video view routes through it), and cover the motor group's
incomplete-entry and below-min Enter paths. Local diff coverage from
the gui suite alone: 82 percent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 08:52:01 +02:00
co-authored by Claude Fable 5
parent 5871c959e2
commit 16e112f44a
4 changed files with 135 additions and 47 deletions
-47
View File
@@ -84,50 +84,3 @@ class IlluminationPanel(QWidget):
slider.blockSignals(True)
slider.setValue(round(val))
slider.blockSignals(False)
if __name__ == "__main__":
# ponytail: smallest check that fails if the send/readback wiring breaks
import os
from types import SimpleNamespace
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtCore import QPoint, QPointF
from PySide6.QtGui import QWheelEvent
from PySide6.QtWidgets import QApplication
app = QApplication([])
panel = IlluminationPanel()
sent = []
panel.front_light.connect(sent.append)
# wheel notch must emit (was silently discarded before)
panel.front_light_slider.setFocus()
QApplication.sendEvent(
panel.front_light_slider,
QWheelEvent(
QPointF(5, 5),
QPointF(5, 5),
QPoint(0, 0),
QPoint(0, 120),
Qt.MouseButton.RightButton,
Qt.KeyboardModifier.NoModifier,
Qt.ScrollPhase.NoScrollPhase,
False,
),
)
assert sent, "wheel adjustment must emit front_light"
# readback right after a user change must not clobber the slider
held = panel.front_light_slider.value()
status = SimpleNamespace(bl=SimpleNamespace(front_light=90.0, back_light=10.0))
panel.update_daq_status(status)
assert panel.front_light_slider.value() == held, "grace window must hold user value"
# after the grace window, readback applies without emitting a PUT
panel._last_user_change = time.monotonic() - READBACK_GRACE_S - 1
n_sent = len(sent)
panel.update_daq_status(status)
assert panel.front_light_slider.value() == 90, "readback must apply after grace"
assert len(sent) == n_sent, "readback must not echo a PUT"
print("gude")
+36
View File
@@ -0,0 +1,36 @@
"""The Axis views' busy flag is a passive status: plain shadowed text, not
the badge pill (which reads as a button). These checks fail if the text
renderer stops painting or the video view stops routing through it."""
from PySide6.QtCore import Qt
from PySide6.QtGui import QPainter, QPixmap
from aare.gui.widgets.busy_overlay import build_busy_overlay_style, draw_busy_status_text
from aare.gui.widgets.video_image import VideoGraphicsView
def _busy_style():
style = build_busy_overlay_style(is_busy=True, tell_state=None)
assert style is not None and style.text == "BEAMLINE BUSY"
return style
def test_status_text_paints_something(qapp):
pixmap = QPixmap(400, 300)
pixmap.fill(Qt.GlobalColor.black)
blank = pixmap.toImage()
painter = QPainter(pixmap)
draw_busy_status_text(painter, 400, 300, _busy_style())
painter.end()
assert pixmap.toImage() != blank, "busy text must actually paint"
def test_video_view_paints_busy_text(qtbot):
view = VideoGraphicsView()
qtbot.addWidget(view)
view.resize(400, 300)
idle = view.grab().toImage()
view.set_busy_overlay_style(_busy_style())
assert view.grab().toImage() != idle, "busy style must change the rendered view"
view.set_busy_overlay_style(None)
assert view.grab().toImage() == idle, "clearing the style must restore the view"
+80
View File
@@ -0,0 +1,80 @@
"""Send/readback wiring for the light sliders (was a __main__ self-check in
the panel; promoted here so CI counts it). Every deliberate input path must
emit, and the status-poll readback must neither clobber a fresh user change
nor echo a PUT back to the server."""
import time
from types import SimpleNamespace
import pytest
from PySide6.QtCore import QPoint, QPointF, Qt
from PySide6.QtGui import QWheelEvent
from PySide6.QtWidgets import QApplication
from aare.gui.panels.illumination_panel import READBACK_GRACE_S, IlluminationPanel
@pytest.fixture
def panel(qtbot):
panel = IlluminationPanel()
qtbot.addWidget(panel)
return panel
def _wheel_notch(slider):
# One wheel notch; sliderReleased never fires for this path, which is
# exactly the case the valueChanged wiring exists for.
QApplication.sendEvent(
slider,
QWheelEvent(
QPointF(5, 5),
QPointF(5, 5),
QPoint(0, 0),
QPoint(0, 120),
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier,
Qt.ScrollPhase.NoScrollPhase,
False,
),
)
def _status(front=90.0, back=10.0):
return SimpleNamespace(bl=SimpleNamespace(front_light=front, back_light=back))
def test_wheel_notch_emits_both_lights(panel):
front, back = [], []
panel.front_light.connect(front.append)
panel.back_light.connect(back.append)
_wheel_notch(panel.front_light_slider)
_wheel_notch(panel.back_light_slider)
assert front, "wheel adjustment must emit front_light"
assert back, "wheel adjustment must emit back_light"
def test_readback_grace_holds_user_value(panel):
_wheel_notch(panel.front_light_slider)
held = panel.front_light_slider.value()
panel.update_daq_status(_status())
assert panel.front_light_slider.value() == held, "grace window must hold user value"
def test_readback_applies_after_grace_without_echo(panel):
sent = []
panel.front_light.connect(sent.append)
panel._last_user_change = time.monotonic() - READBACK_GRACE_S - 1
panel.update_daq_status(_status())
assert panel.front_light_slider.value() == 90, "readback must apply after grace"
assert panel.back_light_slider.value() == 10
assert not sent, "readback must not echo a PUT"
def test_readback_skipped_while_sliding(panel):
panel.on_slider_pressed()
panel._last_user_change = 0.0
panel.update_daq_status(_status(front=55.0))
assert panel.front_light_slider.value() != 55
panel.on_slider_released()
panel.update_daq_status(_status(front=55.0))
assert panel.front_light_slider.value() == 55
+19
View File
@@ -3,6 +3,7 @@ targets are staged (orange), sent only by the Move button (green), and the
box returns to neutral when the motor actually arrives."""
import pytest
from PySide6.QtCore import Qt
from aare.gui.widgets.motor_move_group import MotorMoveGroup
from aare.gui.widgets.number_line_edit import NumberLineEdit
@@ -65,3 +66,21 @@ def test_over_max_is_invalid_and_never_sent(chi, qtbot):
group.button.click()
assert not sent # out-of-range target is never applied
assert box.property("movestate") == "pending"
def test_incomplete_entry_stays_pending(chi, qtbot):
_group, box = chi
_type(qtbot, box, "-") # not a number (yet): no tips, no crash
assert box.property("movestate") == "pending"
# Enter on the incomplete text must be swallowed just as quietly
qtbot.keyClick(box, Qt.Key.Key_Return)
assert box.property("movestate") == "pending"
def test_below_min_enter_hints_but_never_sends(chi, qtbot):
group, box = chi
sent = []
group.applied.connect(sent.append)
_type(qtbot, box, "-5") # below the -0.2 bottom
qtbot.keyClick(box, Qt.Key.Key_Return) # triggers the min-value tooltip path
assert not sent # Enter must never start a move