mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-04 16:02:51 +01:00
603 lines
17 KiB
Python
603 lines
17 KiB
Python
# pylint: disable=missing-function-docstring, missing-module-docstring
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from qtpy.QtGui import QColor
|
|
|
|
from bec_widgets.tests.utils import FakeDevice
|
|
from bec_widgets.widgets.progress.ring_progress_bar.ring import ProgressbarConfig, Ring
|
|
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
|
|
RingProgressContainerWidget,
|
|
)
|
|
|
|
from .client_mocks import mocked_client
|
|
|
|
|
|
@pytest.fixture
|
|
def ring_container(qtbot, mocked_client):
|
|
container = RingProgressContainerWidget()
|
|
qtbot.addWidget(container)
|
|
yield container
|
|
|
|
|
|
@pytest.fixture
|
|
def ring_widget(qtbot, ring_container, mocked_client):
|
|
ring = Ring(parent=ring_container, client=mocked_client)
|
|
qtbot.addWidget(ring)
|
|
qtbot.waitExposed(ring)
|
|
yield ring
|
|
|
|
|
|
@pytest.fixture
|
|
def ring_widget_with_device(ring_widget):
|
|
mock_device = FakeDevice(name="samx")
|
|
ring_widget.bec_dispatcher.client.device_manager.devices["samx"] = mock_device
|
|
yield ring_widget
|
|
|
|
|
|
def test_ring_initialization(ring_widget):
|
|
assert ring_widget is not None
|
|
assert isinstance(ring_widget.config, ProgressbarConfig)
|
|
assert ring_widget.config.mode == "manual"
|
|
assert ring_widget.config.value == 0
|
|
assert ring_widget.registered_slot is None
|
|
|
|
|
|
def test_ring_has_default_config_values(ring_widget):
|
|
assert ring_widget.config.direction == -1
|
|
assert ring_widget.config.line_width == 20
|
|
assert ring_widget.config.start_position == 90
|
|
assert ring_widget.config.min_value == 0
|
|
assert ring_widget.config.max_value == 100
|
|
assert ring_widget.config.precision == 3
|
|
|
|
|
|
###################################
|
|
# set_update method tests
|
|
###################################
|
|
|
|
|
|
def test_set_update_to_manual(ring_widget):
|
|
# Start in manual mode
|
|
assert ring_widget.config.mode == "manual"
|
|
|
|
# Set to manual again (should return early)
|
|
ring_widget.set_update("manual")
|
|
assert ring_widget.config.mode == "manual"
|
|
assert ring_widget.registered_slot is None
|
|
|
|
|
|
def test_set_update_to_scan(ring_widget):
|
|
# Mock the dispatcher to avoid actual connections
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
|
|
# Set to scan mode
|
|
ring_widget.set_update("scan")
|
|
|
|
assert ring_widget.config.mode == "scan"
|
|
# Verify that connect_slot was called
|
|
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
|
call_args = ring_widget.bec_dispatcher.connect_slot.call_args
|
|
assert call_args[0][0] == ring_widget.on_scan_progress
|
|
assert "scan_progress" in str(call_args[0][1])
|
|
|
|
|
|
def test_set_update_from_scan_to_manual(ring_widget):
|
|
# Mock the dispatcher
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
|
|
|
# Set to scan mode first
|
|
ring_widget.set_update("scan")
|
|
assert ring_widget.config.mode == "scan"
|
|
|
|
# Now switch back to manual
|
|
ring_widget.set_update("manual")
|
|
|
|
assert ring_widget.config.mode == "manual"
|
|
assert ring_widget.registered_slot is None
|
|
|
|
|
|
def test_set_update_to_device(ring_widget_with_device):
|
|
ring_widget = ring_widget_with_device
|
|
# Mock the dispatcher
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
|
|
# Set to device mode
|
|
test_device = "samx"
|
|
ring_widget.set_update("device", device=test_device)
|
|
|
|
assert ring_widget.config.mode == "device"
|
|
assert ring_widget.config.device == test_device
|
|
assert ring_widget.config.signal == "samx"
|
|
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
|
|
|
|
|
def test_set_update_from_device_to_manual(ring_widget_with_device):
|
|
ring_widget = ring_widget_with_device
|
|
# Mock the dispatcher
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
|
|
|
# Set to device mode first
|
|
ring_widget.set_update("device", device="samx")
|
|
assert ring_widget.config.mode == "device"
|
|
|
|
# Switch to manual
|
|
ring_widget.set_update("manual")
|
|
|
|
assert ring_widget.config.mode == "manual"
|
|
assert ring_widget.registered_slot is None
|
|
|
|
|
|
def test_set_update_scan_to_device(ring_widget_with_device):
|
|
ring_widget = ring_widget_with_device
|
|
# Mock the dispatcher
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
|
|
|
# Set to scan mode first
|
|
ring_widget.set_update("scan")
|
|
assert ring_widget.config.mode == "scan"
|
|
|
|
# Switch to device mode
|
|
ring_widget.set_update("device", device="samx")
|
|
|
|
assert ring_widget.config.mode == "device"
|
|
assert ring_widget.config.device == "samx"
|
|
|
|
|
|
def test_set_update_device_to_scan(ring_widget_with_device):
|
|
ring_widget = ring_widget_with_device
|
|
# Mock the dispatcher
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
|
|
|
# Set to device mode first
|
|
ring_widget.set_update("device", device="samx")
|
|
assert ring_widget.config.mode == "device"
|
|
|
|
# Switch to scan mode
|
|
ring_widget.set_update("scan")
|
|
|
|
assert ring_widget.config.mode == "scan"
|
|
|
|
|
|
def test_set_update_same_device_resubscribes(ring_widget_with_device):
|
|
ring_widget = ring_widget_with_device
|
|
# Mock the dispatcher
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
|
|
|
# Set to device mode
|
|
test_device = "samx"
|
|
ring_widget.set_update("device", device=test_device)
|
|
|
|
# Reset mocks
|
|
ring_widget.bec_dispatcher.connect_slot.reset_mock()
|
|
ring_widget.bec_dispatcher.disconnect_slot.reset_mock()
|
|
|
|
# Set to same device mode again (should resubscribe)
|
|
ring_widget.set_update("device", device=test_device)
|
|
|
|
# Should disconnect and reconnect
|
|
ring_widget.bec_dispatcher.disconnect_slot.assert_called_once()
|
|
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
|
|
|
|
|
def test_set_update_invalid_mode(ring_widget):
|
|
with pytest.raises(ValueError) as excinfo:
|
|
ring_widget.set_update("invalid_mode")
|
|
|
|
assert "Unsupported mode: invalid_mode" in str(excinfo.value)
|
|
|
|
|
|
###################################
|
|
# Value and property tests
|
|
###################################
|
|
|
|
|
|
def test_set_value(ring_widget):
|
|
ring_widget.set_value(42.5)
|
|
assert ring_widget.config.value == 42.5
|
|
|
|
|
|
def test_set_value_with_min_max_clamping(ring_widget):
|
|
ring_widget.set_min_max_values(0, 100)
|
|
|
|
# Set value above max
|
|
ring_widget.set_value(150)
|
|
assert ring_widget.config.value == 100
|
|
|
|
# Set value below min
|
|
ring_widget.set_value(-10)
|
|
assert ring_widget.config.value == 0
|
|
|
|
|
|
def test_set_precision(ring_widget):
|
|
ring_widget.set_precision(2)
|
|
assert ring_widget.config.precision == 2
|
|
|
|
ring_widget.set_value(10.12345)
|
|
assert ring_widget.config.value == 10.12
|
|
|
|
|
|
def test_set_min_max_values(ring_widget):
|
|
ring_widget.set_min_max_values(10, 90)
|
|
|
|
assert ring_widget.config.min_value == 10
|
|
assert ring_widget.config.max_value == 90
|
|
|
|
|
|
def test_set_line_width(ring_widget):
|
|
ring_widget.set_line_width(25)
|
|
assert ring_widget.config.line_width == 25
|
|
|
|
|
|
def test_set_start_angle(ring_widget):
|
|
ring_widget.set_start_angle(180)
|
|
assert ring_widget.config.start_position == 180
|
|
|
|
|
|
###################################
|
|
# Color management tests
|
|
###################################
|
|
|
|
|
|
def test_set_color(ring_widget):
|
|
test_color = (255, 128, 64, 255)
|
|
ring_widget.set_color(test_color)
|
|
|
|
# Color is stored as hex string internally
|
|
assert ring_widget.color.getRgb() == test_color
|
|
|
|
|
|
def test_set_color_with_link_colors_updates_background(ring_widget):
|
|
# Enable color linking
|
|
ring_widget.config.link_colors = True
|
|
|
|
# Store original background
|
|
original_bg = ring_widget.background_color.getRgb()
|
|
|
|
test_color = (255, 100, 50, 255)
|
|
ring_widget.set_color(test_color)
|
|
|
|
# Background should be derived using subtle_background_color
|
|
bg_color = ring_widget.background_color
|
|
# Background should have changed
|
|
assert bg_color.getRgb() != original_bg
|
|
# Background should be different from the main color
|
|
assert bg_color.getRgb() != test_color
|
|
|
|
|
|
def test_set_background_when_colors_unlinked(ring_widget):
|
|
# Disable color linking
|
|
ring_widget.config.link_colors = False
|
|
|
|
test_bg = (100, 100, 100, 128)
|
|
ring_widget.set_background(test_bg)
|
|
|
|
assert ring_widget.background_color.getRgb() == test_bg
|
|
|
|
|
|
def test_set_background_when_colors_linked_does_nothing(ring_widget):
|
|
# Enable color linking
|
|
ring_widget.config.link_colors = True
|
|
|
|
original_bg = ring_widget.background_color.getRgb()
|
|
test_bg = (100, 100, 100, 128)
|
|
|
|
ring_widget.set_background(test_bg)
|
|
|
|
# Background should not change when colors are linked
|
|
assert ring_widget.background_color.getRgb() == original_bg
|
|
|
|
|
|
def test_color_link_derives_background(ring_widget):
|
|
ring_widget.config.link_colors = True
|
|
|
|
bright_color = QColor(255, 255, 0, 255) # Bright yellow
|
|
original_bg = ring_widget.background_color.getRgb()
|
|
|
|
ring_widget.set_color(bright_color.getRgb())
|
|
|
|
# Get the derived background color
|
|
bg_color = ring_widget.background_color
|
|
|
|
# Background should have changed
|
|
assert bg_color.getRgb() != original_bg
|
|
# Background should be a subtle blend, not the same as the main color
|
|
assert bg_color.getRgb() != bright_color.getRgb()
|
|
|
|
|
|
def test_convert_color_from_tuple(ring_widget):
|
|
color_tuple = (200, 150, 100, 255)
|
|
qcolor = ring_widget.convert_color(color_tuple)
|
|
|
|
assert isinstance(qcolor, QColor)
|
|
assert qcolor.getRgb() == color_tuple
|
|
|
|
|
|
def test_convert_color_from_hex_string(ring_widget):
|
|
hex_color = "#FF8040FF"
|
|
qcolor = ring_widget.convert_color(hex_color)
|
|
|
|
assert isinstance(qcolor, QColor)
|
|
assert qcolor.isValid()
|
|
|
|
|
|
###################################
|
|
# Gap property tests
|
|
###################################
|
|
|
|
|
|
def test_gap_property(ring_widget):
|
|
ring_widget.gap = 15
|
|
assert ring_widget.gap == 15
|
|
|
|
|
|
###################################
|
|
# Config validation tests
|
|
###################################
|
|
|
|
|
|
def test_config_default_values():
|
|
config = ProgressbarConfig()
|
|
|
|
assert config.value == 0
|
|
assert config.direction == -1
|
|
assert config.line_width == 20
|
|
assert config.start_position == 90
|
|
assert config.min_value == 0
|
|
assert config.max_value == 100
|
|
assert config.precision == 3
|
|
assert config.mode == "manual"
|
|
assert config.link_colors is True
|
|
|
|
|
|
def test_config_with_custom_values():
|
|
config = ProgressbarConfig(
|
|
value=50, direction=1, line_width=20, min_value=10, max_value=90, precision=2, mode="scan"
|
|
)
|
|
|
|
assert config.value == 50
|
|
assert config.direction == 1
|
|
assert config.line_width == 20
|
|
assert config.min_value == 10
|
|
assert config.max_value == 90
|
|
assert config.precision == 2
|
|
assert config.mode == "scan"
|
|
|
|
|
|
###################################
|
|
# set_direction tests
|
|
###################################
|
|
|
|
|
|
def test_set_direction_clockwise(ring_widget):
|
|
ring_widget.set_direction(-1)
|
|
assert ring_widget.config.direction == -1
|
|
|
|
|
|
def test_set_direction_counter_clockwise(ring_widget):
|
|
ring_widget.set_direction(1)
|
|
assert ring_widget.config.direction == 1
|
|
|
|
|
|
###################################
|
|
# _update_device_connection tests
|
|
###################################
|
|
|
|
|
|
def test_update_device_connection_with_progress_signal(ring_widget_with_device):
|
|
ring_widget = ring_widget_with_device
|
|
samx = ring_widget.bec_dispatcher.client.device_manager.devices.samx
|
|
samx._info["signals"]["progress"] = {
|
|
"obj_name": "samx_progress",
|
|
"component_name": "progress",
|
|
"signal_class": "ProgressSignal",
|
|
"kind_str": "hinted",
|
|
}
|
|
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
|
|
ring_widget._update_device_connection("samx", "progress")
|
|
|
|
# Should connect to device_progress endpoint
|
|
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
|
call_args = ring_widget.bec_dispatcher.connect_slot.call_args
|
|
assert call_args[0][0] == ring_widget.on_device_progress
|
|
|
|
|
|
def test_update_device_connection_with_hinted_signal(ring_widget):
|
|
mock_device = FakeDevice(name="samx")
|
|
mock_device._info = {
|
|
"signals": {
|
|
"samx": {"obj_name": "samx", "signal_class": "SomeOtherSignal", "kind_str": "hinted"}
|
|
}
|
|
}
|
|
|
|
ring_widget.bec_dispatcher.client.device_manager.devices["samx"] = mock_device
|
|
|
|
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
|
|
|
ring_widget._update_device_connection("samx", "samx")
|
|
|
|
# Should connect to device_readback endpoint
|
|
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
|
call_args = ring_widget.bec_dispatcher.connect_slot.call_args
|
|
assert call_args[0][0] == ring_widget.on_device_readback
|
|
|
|
|
|
def test_update_device_connection_no_device_manager(ring_widget):
|
|
ring_widget.bec_dispatcher.client.device_manager = None
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
ring_widget._update_device_connection("samx", "signal")
|
|
assert "Device manager is not available" in str(excinfo.value)
|
|
|
|
|
|
def test_update_device_connection_device_not_found(ring_widget):
|
|
mock_device = FakeDevice(name="samx")
|
|
ring_widget.bec_dispatcher.client.device_manager.devices["samx"] = mock_device
|
|
|
|
# Should return without raising an error
|
|
ring_widget._update_device_connection("nonexistent", "signal")
|
|
|
|
|
|
###################################
|
|
# on_scan_progress tests
|
|
###################################
|
|
|
|
|
|
def test_on_scan_progress_updates_value(ring_widget):
|
|
msg = {"value": 42, "max_value": 100}
|
|
meta = {"RID": "test_rid_123"}
|
|
|
|
ring_widget.on_scan_progress(msg, meta)
|
|
|
|
assert ring_widget.config.value == 42
|
|
|
|
|
|
def test_on_scan_progress_updates_min_max_on_new_rid(ring_widget):
|
|
msg = {"value": 50, "max_value": 200}
|
|
meta = {"RID": "new_rid"}
|
|
|
|
ring_widget.RID = "old_rid"
|
|
ring_widget.on_scan_progress(msg, meta)
|
|
|
|
assert ring_widget.config.min_value == 0
|
|
assert ring_widget.config.max_value == 200
|
|
assert ring_widget.config.value == 50
|
|
|
|
|
|
def test_on_scan_progress_same_rid_no_min_max_update(ring_widget):
|
|
msg = {"value": 75, "max_value": 300}
|
|
meta = {"RID": "same_rid"}
|
|
|
|
ring_widget.RID = "same_rid"
|
|
ring_widget.set_min_max_values(0, 100)
|
|
|
|
ring_widget.on_scan_progress(msg, meta)
|
|
|
|
# Max value should not be updated when RID is the same
|
|
assert ring_widget.config.max_value == 100
|
|
assert ring_widget.config.value == 75
|
|
|
|
|
|
###################################
|
|
# on_device_readback tests
|
|
###################################
|
|
|
|
|
|
def test_on_device_readback_updates_value(ring_widget):
|
|
ring_widget.config.device = "samx"
|
|
ring_widget.config.signal = "readback"
|
|
|
|
msg = {"signals": {"readback": {"value": 12.34}}}
|
|
meta = {}
|
|
|
|
ring_widget.on_device_readback(msg, meta)
|
|
|
|
assert ring_widget.config.value == 12.34
|
|
|
|
|
|
def test_on_device_readback_uses_device_name_when_no_signal(ring_widget):
|
|
ring_widget.config.device = "samy"
|
|
ring_widget.config.signal = None
|
|
|
|
msg = {"signals": {"samy": {"value": 56.78}}}
|
|
meta = {}
|
|
|
|
ring_widget.on_device_readback(msg, meta)
|
|
|
|
assert ring_widget.config.value == 56.78
|
|
|
|
|
|
def test_on_device_readback_no_device_returns_early(ring_widget):
|
|
ring_widget.config.device = None
|
|
|
|
msg = {"signals": {"samx": {"value": 99.99}}}
|
|
meta = {}
|
|
|
|
initial_value = ring_widget.config.value
|
|
ring_widget.on_device_readback(msg, meta)
|
|
|
|
# Value should not change
|
|
assert ring_widget.config.value == initial_value
|
|
|
|
|
|
def test_on_device_readback_missing_signal_data(ring_widget):
|
|
ring_widget.config.device = "samx"
|
|
ring_widget.config.signal = "missing_signal"
|
|
|
|
msg = {"signals": {"other_signal": {"value": 11.11}}}
|
|
meta = {}
|
|
|
|
initial_value = ring_widget.config.value
|
|
ring_widget.on_device_readback(msg, meta)
|
|
|
|
# Value should not change when signal is missing
|
|
assert ring_widget.config.value == initial_value
|
|
|
|
|
|
###################################
|
|
# on_device_progress tests
|
|
###################################
|
|
|
|
|
|
def test_on_device_progress_updates_value_and_max(ring_widget):
|
|
ring_widget.config.device = "samx"
|
|
|
|
msg = {"value": 30, "max_value": 150, "done": False}
|
|
meta = {}
|
|
|
|
ring_widget.on_device_progress(msg, meta)
|
|
|
|
assert ring_widget.config.value == 30
|
|
assert ring_widget.config.max_value == 150
|
|
|
|
|
|
def test_on_device_progress_done_sets_to_max(ring_widget):
|
|
ring_widget.config.device = "samx"
|
|
|
|
msg = {"value": 80, "max_value": 100, "done": True}
|
|
meta = {}
|
|
|
|
ring_widget.on_device_progress(msg, meta)
|
|
|
|
# When done is True, value should be set to max_value
|
|
assert ring_widget.config.value == 100
|
|
assert ring_widget.config.max_value == 100
|
|
|
|
|
|
def test_on_device_progress_no_device_returns_early(ring_widget):
|
|
ring_widget.config.device = None
|
|
|
|
msg = {"value": 50, "max_value": 100, "done": False}
|
|
meta = {}
|
|
|
|
initial_value = ring_widget.config.value
|
|
initial_max = ring_widget.config.max_value
|
|
|
|
ring_widget.on_device_progress(msg, meta)
|
|
|
|
# Nothing should change
|
|
assert ring_widget.config.value == initial_value
|
|
assert ring_widget.config.max_value == initial_max
|
|
|
|
|
|
def test_on_device_progress_default_values(ring_widget):
|
|
ring_widget.config.device = "samx"
|
|
|
|
# Message without value and max_value
|
|
msg = {}
|
|
meta = {}
|
|
|
|
ring_widget.on_device_progress(msg, meta)
|
|
|
|
# Should use defaults: value=0, max_value=100
|
|
assert ring_widget.config.value == 0
|
|
assert ring_widget.config.max_value == 100
|