build: support PySide6 6.11.1, pyqtgraph 0.14 and PySide6-QtAds 5.0.0, remove force x11 on Linux

This commit is contained in:
2026-07-24 13:48:03 +02:00
committed by Jan Wyzula
parent 3c6b82dceb
commit 3ba078ea5b
16 changed files with 199 additions and 113 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+4
View File
@@ -68,6 +68,10 @@ def qapplication(qtbot, request, testable_qtimer_class): # pylint: disable=unus
# stop pyepics dispatcher for leaking tests
_dispatcher.stop()
from bec_widgets.widgets.editors.bec_console.bec_console import _bec_console_registry
_bec_console_registry.clear()
process_all_deferred_deletes(qapp)
if request.node.stash._storage.get("failed"):
print("Test failed, skipping cleanup checks")
return
+90 -84
View File
@@ -14,6 +14,33 @@ from .conftest import create_widget
# pylint: disable = redefined-outer-name
class _FakeClickEvent:
"""Minimal public-API stand-in for a pyqtgraph MouseClickEvent."""
def __init__(
self,
scene_pos: QPointF,
button: Qt.MouseButton = Qt.MouseButton.LeftButton,
double: bool = False,
):
self._scene_pos = scene_pos
self._button = button
self._double = double
self.accepted = False
def button(self):
return self._button
def scenePos(self):
return self._scene_pos
def double(self):
return self._double
def accept(self):
self.accepted = True
@pytest.fixture
def plot_widget_with_crosshair(qtbot):
widget = pg.PlotWidget()
@@ -22,6 +49,7 @@ def plot_widget_with_crosshair(qtbot):
widget.plot(x=[1, 2, 3], y=[4, 5, 6], name="Curve 1")
plot_item = widget.getPlotItem()
plot_item.vb.setRange(xRange=(0, 4), yRange=(0, 10), padding=0)
crosshair = Crosshair(plot_item=plot_item, precision=3)
yield crosshair, plot_item
@@ -38,20 +66,17 @@ def image_widget_with_crosshair(qtbot):
widget.addItem(image_item)
plot_item = widget.getPlotItem()
plot_item.vb.setRange(xRange=(0, 100), yRange=(0, 100), padding=0)
crosshair = Crosshair(plot_item=plot_item, precision=3)
yield crosshair, plot_item
def test_mouse_moved_lines(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
pos_in_view = QPointF(2, 5)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
crosshair, _ = plot_widget_with_crosshair
# Simulate mouse movement
crosshair.mouse_moved(event_mock)
crosshair.mouse_moved(manual_pos=(2, 5))
# Check that the vertical line is indeed at x=2
assert np.isclose(crosshair.v_line.pos().x(), 2)
@@ -59,7 +84,7 @@ def test_mouse_moved_lines(plot_widget_with_crosshair):
def test_mouse_moved_signals(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
crosshair, _ = plot_widget_with_crosshair
emitted_values_1D = []
@@ -68,43 +93,40 @@ def test_mouse_moved_signals(plot_widget_with_crosshair):
crosshair.coordinatesChanged1D.connect(slot)
pos_in_view = QPointF(2, 5)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
crosshair.mouse_moved(event_mock)
crosshair.mouse_moved(manual_pos=(2, 5))
# Assert the expected behavior
assert emitted_values_1D == [("Curve 1", 2, 5)]
def test_mouse_moved_signals_outside(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
crosshair, _ = plot_widget_with_crosshair
# Create a slot that will store the emitted values as tuples
emitted_values_1D = []
emitted_positions = []
def slot(coordinates):
emitted_values_1D.append(coordinates)
# Connect the signal to the custom slot
crosshair.coordinatesChanged1D.connect(slot)
crosshair.crosshairChanged.connect(emitted_positions.append)
# Simulate a mouse moved event at a specific position
pos_in_view = QPointF(22, 55)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
# Call the mouse_moved method
crosshair.mouse_moved(event_mock)
crosshair.mouse_moved(manual_pos=(2, 5))
emitted_positions.clear()
emitted_values_1D.clear()
crosshair.mouse_moved(manual_pos=(22, 55))
# Assert the expected behavior
assert emitted_values_1D == []
assert emitted_positions == []
assert np.isclose(crosshair.v_line.pos().x(), 2)
assert np.isclose(crosshair.h_line.pos().y(), 5)
def test_mouse_moved_signals_2D(image_widget_with_crosshair):
crosshair, plot_item = image_widget_with_crosshair
image_item = plot_item.items[0]
crosshair, _ = image_widget_with_crosshair
emitted_values_2D = []
@@ -113,17 +135,16 @@ def test_mouse_moved_signals_2D(image_widget_with_crosshair):
crosshair.coordinatesChanged2D.connect(slot)
pos_in_view = QPointF(21.0, 55.0)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
crosshair.mouse_moved(event_mock)
crosshair.mouse_moved(manual_pos=(21.0, 55.0))
assert emitted_values_2D == [("ImageItem", 21, 55)]
def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair):
def test_mouse_moved_signals_2D_outside_image_bounds_clamps_inside_view_range(
image_widget_with_crosshair,
):
crosshair, plot_item = image_widget_with_crosshair
plot_item.vb.setRange(xRange=(0, 300), yRange=(0, 600), padding=0)
emitted_values_2D = []
@@ -132,23 +153,34 @@ def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair):
crosshair.coordinatesChanged2D.connect(slot)
pos_in_view = QPointF(220.0, 555.0)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
crosshair.mouse_moved(manual_pos=(220.0, 555.0))
crosshair.mouse_moved(event_mock)
assert emitted_values_2D == [("ImageItem", 99, 99)]
def test_mouse_moved_signals_2D_outside_view_range_ignored(image_widget_with_crosshair):
crosshair, _ = image_widget_with_crosshair
emitted_values_2D = []
emitted_positions = []
crosshair.coordinatesChanged2D.connect(emitted_values_2D.append)
crosshair.crosshairChanged.connect(emitted_positions.append)
crosshair.mouse_moved(manual_pos=(21.0, 55.0))
emitted_positions.clear()
emitted_values_2D.clear()
crosshair.mouse_moved(manual_pos=(220.0, 555.0))
assert emitted_values_2D == []
assert emitted_positions == []
assert np.isclose(crosshair.v_line.pos().x(), 21.0)
assert np.isclose(crosshair.h_line.pos().y(), 55.0)
def test_marker_positions_after_mouse_move(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
pos_in_view = QPointF(2, 5)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
crosshair.mouse_moved(event_mock)
crosshair, _ = plot_widget_with_crosshair
crosshair.mouse_moved(manual_pos=(2, 5))
marker = crosshair.marker_moved_1d["Curve 1"]
marker_x, marker_y = marker.getData()
@@ -172,7 +204,7 @@ def test_scale_emitted_coordinates(plot_widget_with_crosshair):
def test_crosshair_changed_signal(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
crosshair, _ = plot_widget_with_crosshair
emitted_positions = []
@@ -181,11 +213,7 @@ def test_crosshair_changed_signal(plot_widget_with_crosshair):
crosshair.crosshairChanged.connect(slot)
pos_in_view = QPointF(2, 5)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
crosshair.mouse_moved(event_mock)
crosshair.mouse_moved(manual_pos=(2, 5))
x, y = emitted_positions[0]
@@ -193,33 +221,33 @@ def test_crosshair_changed_signal(plot_widget_with_crosshair):
assert np.isclose(y, 5)
def test_crosshair_clicked_signal(qtbot, plot_widget_with_crosshair):
def test_crosshair_clicked_signal(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
emitted_positions = []
emitted_view_positions = []
def slot(position):
emitted_positions.append(position)
crosshair.crosshairClicked.connect(slot)
crosshair.positionClicked.connect(emitted_view_positions.append)
x_data = 2
y_data = 5
crosshair.is_log_x = True
crosshair.is_log_y = True
plot_item.vb.setRange(xRange=(0, 1), yRange=(0, 1), padding=0)
# Map data coordinates to scene coordinates
pos_in_scene = plot_item.vb.mapViewToScene(QPointF(x_data, y_data))
# Map scene coordinates to widget coordinates
graphics_view = plot_item.vb.scene().views()[0]
qtbot.waitExposed(graphics_view)
pos_in_widget = graphics_view.mapFromScene(pos_in_scene)
# Simulate mouse click
qtbot.mouseClick(graphics_view.viewport(), Qt.LeftButton, pos=pos_in_widget)
known_view_point = QPointF(np.log10(2), np.log10(5))
pos_in_scene = plot_item.vb.mapViewToScene(known_view_point)
crosshair.mouse_clicked(_FakeClickEvent(pos_in_scene))
x, y = emitted_positions[0]
view_x, view_y = emitted_view_positions[0]
assert np.isclose(round(x, 1), 2)
assert np.isclose(round(y, 1), 5)
assert np.isclose(x, 2)
assert np.isclose(y, 5)
assert np.isclose(view_x, known_view_point.x())
assert np.isclose(view_y, known_view_point.y())
def test_update_coord_label_1D(plot_widget_with_crosshair):
@@ -388,21 +416,18 @@ def test_ignore_invisible_curves_on_move(qtbot, mocked_client):
c0 = wf.plot(x=[1, 2, 3], y=[1, 4, 9], name="Curve_0")
c1 = wf.plot(x=[1, 2, 3], y=[2, 5, 10], name="Curve_1")
wf.hook_crosshair()
wf.crosshair.plot_item.vb.setRange(xRange=(0, 4), yRange=(0, 10), padding=0)
# # Simulate a mouse move at (2,5)
pos_in_view = QPointF(2, 5)
pos_in_scene = wf.plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
# 1) Both curves visible: expect markers for both
wf.crosshair.clear_markers()
wf.crosshair.mouse_moved(event_mock)
wf.crosshair.mouse_moved(manual_pos=(2, 5))
assert set(wf.crosshair.marker_moved_1d.keys()) == {"Curve_0", "Curve_1"}
# 2) Hide Curve B and repeat: only Curve_0 should remain
c1.setVisible(False)
wf.crosshair.clear_markers()
wf.crosshair.mouse_moved(event_mock)
wf.crosshair.mouse_moved(manual_pos=(2, 5))
qtbot.wait(200)
assert set(wf.crosshair.marker_moved_1d.keys()) == {"Curve_0"}
@@ -412,25 +437,6 @@ def test_ignore_invisible_curves_on_move(qtbot, mocked_client):
###############################################
class _FakeClickEvent:
"""Minimal stand-in for a pyqtgraph MouseClickEvent for routing tests."""
def __init__(self, scene_pos, button=Qt.LeftButton, double=False):
self._scenePos = scene_pos
self._button = button
self._double = double
self.accepted = False
def button(self):
return self._button
def double(self):
return self._double
def accept(self):
self.accepted = True
def test_set_pin_1d_creates_marker_and_emits(plot_widget_with_crosshair):
crosshair, _ = plot_widget_with_crosshair
pinned = []
@@ -531,6 +531,7 @@ class TestDeviceManagerView:
# Assign the mocked client
widget.device_manager_widget.client = mocked_client
qtbot.addWidget(widget)
widget.show()
qtbot.waitExposed(widget)
yield widget
+22
View File
@@ -13,6 +13,7 @@ from bec_widgets.widgets.plots.heatmap.heatmap import (
HeatmapDeviceSignal,
_InterpolationRequest,
_StepInterpolationWorker,
_TextOnlyLegendSample,
)
# pytest: disable=unused-import
@@ -1055,6 +1056,27 @@ def test_heatmap_config_label_shows_live_or_history(heatmap_widget):
assert "Scan: 5 (history)" in labels
def test_heatmap_config_label_paints_without_error(heatmap_widget):
"""The config label rows must survive a real paint pass.
A stock ItemSample around a PlotItem raises in paint(); since PySide 6.10
override exceptions propagate out of the C++ paint loop and segfault.
"""
scan_msg = mock.MagicMock()
scan_msg.scan_number = 5
scan_msg.scan_name = "line_scan"
heatmap_widget.status_message = scan_msg
heatmap_widget._image_config.show_config_label = True
heatmap_widget.redraw_config_label()
samples = [sample for sample, _ in heatmap_widget.config_label.items]
assert samples
assert all(isinstance(sample, _TextOnlyLegendSample) for sample in samples)
pixmap = heatmap_widget.grab()
assert not pixmap.isNull()
def test_heatmap_settings_scan_index_syncs_with_widget(heatmap_widget, qtbot, scan_history_factory):
"""
The scan index combobox in the settings dialog mirrors the heatmap state:
@@ -1391,6 +1391,9 @@ def test_crosshair_moves_update_profiles_immediately(qtbot, mocked_client):
switch = bec_image_view.toolbar.components.get_action("image_switch_crosshair")
switch.actions["crosshair_roi"].action.trigger()
qtbot.wait(50)
bec_image_view.crosshair.plot_item.vb.setRange(
xRange=(0, test_data.shape[0]), yRange=(0, test_data.shape[1]), padding=0
)
# Position the crosshair lines and emit the snapped pixel (as mouse_moved does).
bec_image_view.crosshair.mouse_moved(manual_pos=(1.2, 2.2))
@@ -1410,6 +1413,9 @@ def test_live_label_intensity_updates_on_image_update(qtbot, mocked_client):
test_data = np.arange(25, dtype=float).reshape(5, 5)
bec_image_view.on_image_update_2d({"data": test_data}, {})
bec_image_view.hook_crosshair()
bec_image_view.crosshair.plot_item.vb.setRange(
xRange=(0, test_data.shape[0]), yRange=(0, test_data.shape[1]), padding=0
)
bec_image_view.crosshair.mouse_moved(manual_pos=(2.5, 3.5))
assert "Intensity: 13.000" in bec_image_view.crosshair.coord_label.toPlainText()
+1 -1
View File
@@ -80,7 +80,7 @@ def test_export_import_config(example_widget):
"value": 10,
},
"QTableWidget ()": {
"QAbstractButton ()": {},
"QAbstractButton (qt_tableview_cornerbutton)": {},
"QAbstractTableModel ()": {},
"QHeaderView ()": {
"QItemSelectionModel ()": {},