mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-05 21:08:40 +02:00
fix(bec_progress_bar): replace the custom paint event progressbar with native QProgressBar
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import numpy as np
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from qtpy.QtGui import QPalette
|
||||
from qtpy.QtWidgets import QProgressBar
|
||||
|
||||
from bec_widgets.widgets.progress.bec_progressbar.bec_progressbar import (
|
||||
BECProgressBar,
|
||||
@@ -15,6 +18,14 @@ def progressbar(qtbot):
|
||||
yield widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def static_progressbar(qtbot):
|
||||
widget = BECProgressBar(enable_dynamic_stylesheet=False)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_progressbar(progressbar):
|
||||
progressbar.update()
|
||||
|
||||
@@ -23,21 +34,137 @@ def test_progressbar_set_value(qtbot, progressbar):
|
||||
progressbar.set_minimum(0)
|
||||
progressbar.set_maximum(100)
|
||||
progressbar.set_value(50)
|
||||
progressbar.paintEvent(None)
|
||||
|
||||
qtbot.waitUntil(
|
||||
lambda: np.isclose(
|
||||
progressbar._value, progressbar._user_value * progressbar._oversampling_factor
|
||||
)
|
||||
)
|
||||
assert isinstance(progressbar.progressbar, QProgressBar)
|
||||
assert progressbar._value == progressbar._user_value * progressbar._oversampling_factor
|
||||
assert progressbar.progressbar.value() == 50 * progressbar._oversampling_factor
|
||||
|
||||
|
||||
def test_progressbar_label(progressbar):
|
||||
progressbar.label_template = "Test: $value"
|
||||
progressbar.set_value(50)
|
||||
assert progressbar._get_label() == "Test: 50"
|
||||
assert progressbar.center_label.text() == "Test: 50"
|
||||
|
||||
|
||||
def test_progressbar_equal_minimum_and_maximum_does_not_raise(progressbar):
|
||||
progressbar.set_minimum(0)
|
||||
progressbar.set_maximum(0)
|
||||
progressbar.set_value(0)
|
||||
|
||||
assert progressbar._get_label() == "0 / 0 - 100 %"
|
||||
assert progressbar.progressbar.value() == progressbar.progressbar.maximum()
|
||||
|
||||
|
||||
def test_progressbar_uses_static_stylesheet_with_palette_state_color(progressbar):
|
||||
progressbar.progressbar.resize(100, 20)
|
||||
progressbar.set_value(50)
|
||||
progressbar.state = ProgressState.PAUSED
|
||||
|
||||
style_sheet = progressbar.progressbar.styleSheet()
|
||||
assert "QProgressBar::chunk" in style_sheet
|
||||
assert "background-color: palette(highlight);" in style_sheet
|
||||
assert "background-color: palette(mid);" in style_sheet
|
||||
assert "border-radius: 7px;" in style_sheet
|
||||
assert (
|
||||
progressbar.progressbar.palette().color(QPalette.ColorRole.Highlight)
|
||||
== progressbar._state_colors[ProgressState.PAUSED]
|
||||
)
|
||||
|
||||
|
||||
def test_progressbar_value_updates_do_not_rebuild_stylesheet_within_same_chunk_mode(progressbar):
|
||||
progressbar.progressbar.resize(100, 20)
|
||||
progressbar.set_value(30)
|
||||
|
||||
with mock.patch.object(
|
||||
progressbar, "_setup_style_sheet", wraps=progressbar._setup_style_sheet
|
||||
) as setup_style_sheet:
|
||||
progressbar.set_value(35)
|
||||
progressbar.set_value(42)
|
||||
progressbar.set_value(50)
|
||||
|
||||
setup_style_sheet.assert_not_called()
|
||||
|
||||
|
||||
def test_progressbar_value_updates_skip_chunk_radius_after_target_reached(progressbar):
|
||||
progressbar.progressbar.resize(100, 20)
|
||||
progressbar.set_value(30)
|
||||
assert progressbar._chunk_radius == progressbar._target_chunk_radius()
|
||||
|
||||
with mock.patch.object(
|
||||
progressbar, "_update_chunk_radius", wraps=progressbar._update_chunk_radius
|
||||
) as update_chunk_radius:
|
||||
progressbar.set_value(35)
|
||||
progressbar.set_value(42)
|
||||
progressbar.set_value(50)
|
||||
|
||||
update_chunk_radius.assert_not_called()
|
||||
|
||||
|
||||
def test_progressbar_repeated_same_maximum_does_not_reset_chunk_radius(progressbar):
|
||||
progressbar.progressbar.resize(100, 20)
|
||||
progressbar.set_maximum(100)
|
||||
progressbar.set_value(30)
|
||||
assert progressbar._chunk_radius == progressbar._target_chunk_radius()
|
||||
|
||||
with mock.patch.object(
|
||||
progressbar, "_update_chunk_radius", wraps=progressbar._update_chunk_radius
|
||||
) as update_chunk_radius:
|
||||
progressbar.set_maximum(100)
|
||||
progressbar.set_value(40)
|
||||
|
||||
update_chunk_radius.assert_not_called()
|
||||
|
||||
|
||||
def test_progressbar_can_disable_dynamic_stylesheet(static_progressbar):
|
||||
static_progressbar.progressbar.resize(100, 20)
|
||||
assert static_progressbar.enable_dynamic_stylesheet is False
|
||||
assert static_progressbar._chunk_radius == static_progressbar._target_chunk_radius()
|
||||
|
||||
with mock.patch.object(
|
||||
static_progressbar, "_setup_style_sheet", wraps=static_progressbar._setup_style_sheet
|
||||
) as setup_style_sheet:
|
||||
static_progressbar.set_value(1)
|
||||
static_progressbar.set_value(2)
|
||||
static_progressbar.set_value(3)
|
||||
|
||||
setup_style_sheet.assert_not_called()
|
||||
assert "border-radius: 7px;" in static_progressbar.progressbar.styleSheet()
|
||||
|
||||
|
||||
def test_progressbar_dynamic_stylesheet_can_be_toggled(progressbar):
|
||||
progressbar.enable_dynamic_stylesheet = False
|
||||
|
||||
assert progressbar.enable_dynamic_stylesheet is False
|
||||
assert progressbar._chunk_radius == progressbar._target_chunk_radius()
|
||||
assert "border-radius: 7px;" in progressbar.progressbar.styleSheet()
|
||||
|
||||
|
||||
def test_progressbar_rebuilds_stylesheet_until_chunk_radius_reaches_target(progressbar):
|
||||
progressbar.progressbar.resize(100, 20)
|
||||
progressbar.set_value(9)
|
||||
|
||||
with mock.patch.object(
|
||||
progressbar, "_setup_style_sheet", wraps=progressbar._setup_style_sheet
|
||||
) as setup_style_sheet:
|
||||
progressbar.set_value(12)
|
||||
progressbar.set_value(25)
|
||||
progressbar.set_value(30)
|
||||
|
||||
assert setup_style_sheet.call_count == 2
|
||||
assert "border-radius: 7px;" in progressbar.progressbar.styleSheet()
|
||||
|
||||
|
||||
def test_progressbar_resets_chunk_radius_when_value_goes_backwards(progressbar):
|
||||
progressbar.progressbar.resize(100, 20)
|
||||
progressbar.set_value(30)
|
||||
assert "border-radius: 7px;" in progressbar.progressbar.styleSheet()
|
||||
|
||||
progressbar.set_value(4)
|
||||
|
||||
assert "border-radius: 2px;" in progressbar.progressbar.styleSheet()
|
||||
|
||||
|
||||
def test_progress_state_from_bec_status():
|
||||
"""ProgressState.from_bec_status() maps BEC literals correctly."""
|
||||
mapping = {
|
||||
|
||||
@@ -117,6 +117,13 @@ def test_hidden_scan_progress_parent_blocks_children_namespace(bec_main_window):
|
||||
assert nested_progress.parent_id == hidden_progress.gui_id
|
||||
|
||||
|
||||
def test_compact_scan_progress_bar_uses_status_bar_sizing(bec_main_window):
|
||||
progressbar = bec_main_window._scan_progress_bar_simple.progressbar
|
||||
|
||||
assert progressbar.height() == bec_main_window.SCAN_PROGRESS_HEIGHT
|
||||
assert progressbar.progressbar.minimumHeight() == 0
|
||||
|
||||
|
||||
#################################################################
|
||||
# Tests for BECMainWindow Addons
|
||||
#################################################################
|
||||
|
||||
@@ -71,11 +71,33 @@ def test_progress_task_basic():
|
||||
assert task.time_elapsed == "00:00:10"
|
||||
|
||||
|
||||
def test_progress_task_elapsed_time_uses_monotonic_clock(monkeypatch):
|
||||
times = iter([100.0, 102.5])
|
||||
monkeypatch.setattr(
|
||||
"bec_widgets.widgets.progress.scan_progressbar.scan_progressbar.time.monotonic",
|
||||
lambda: next(times),
|
||||
)
|
||||
task = ProgressTask(parent=None)
|
||||
task.timer.stop()
|
||||
|
||||
task.update_elapsed_time()
|
||||
|
||||
assert task._elapsed_time == 2.5
|
||||
assert task.time_elapsed == "00:00:02"
|
||||
|
||||
|
||||
def test_scan_progressbar_initialization(scan_progressbar):
|
||||
assert isinstance(scan_progressbar, ScanProgressBar)
|
||||
assert isinstance(scan_progressbar.progressbar, BECProgressBar)
|
||||
|
||||
|
||||
def test_scan_progressbar_passes_dynamic_stylesheet_setting(qtbot, mocked_client):
|
||||
widget = ScanProgressBar(client=mocked_client, enable_dynamic_stylesheet=False)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
assert widget.progressbar.enable_dynamic_stylesheet is False
|
||||
|
||||
|
||||
def test_update_labels_content(scan_progressbar):
|
||||
"""update_labels() reflects ProgressTask time strings on the UI."""
|
||||
# fabricate a task with known timings
|
||||
@@ -148,6 +170,19 @@ def test_source_label_updates(scan_progressbar):
|
||||
assert scan_progressbar.ui.source_label.text() == "Scan 5"
|
||||
|
||||
|
||||
def test_source_label_update_logs_only_on_text_change(scan_progressbar):
|
||||
scan_progressbar.scan_number = 5
|
||||
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.progress.scan_progressbar.scan_progressbar.logger.info"
|
||||
) as mock_info:
|
||||
scan_progressbar.set_progress_source(ProgressSource.SCAN_PROGRESS)
|
||||
scan_progressbar.set_progress_source(ProgressSource.SCAN_PROGRESS)
|
||||
scan_progressbar.set_progress_source(ProgressSource.SCAN_PROGRESS)
|
||||
|
||||
mock_info.assert_called_once_with("Set progress source to Scan 5")
|
||||
|
||||
|
||||
def test_set_progress_source_connections(scan_progressbar, monkeypatch):
|
||||
""" """
|
||||
|
||||
@@ -241,7 +276,13 @@ def test_cleanup_disconnects_active_device_subscription(scan_progressbar, monkey
|
||||
monkeypatch.setattr(BECWidget, "cleanup", lambda self: None)
|
||||
|
||||
scan_progressbar.set_progress_source(ProgressSource.DEVICE_PROGRESS, device="motor1")
|
||||
ScanProgressBar.cleanup(scan_progressbar)
|
||||
with (
|
||||
mock.patch.object(scan_progressbar, "close", wraps=scan_progressbar.close) as close_mock,
|
||||
mock.patch.object(
|
||||
scan_progressbar, "deleteLater", wraps=scan_progressbar.deleteLater
|
||||
) as delete_later_mock,
|
||||
):
|
||||
ScanProgressBar.cleanup(scan_progressbar)
|
||||
|
||||
assert disconnect_calls == [
|
||||
MessageEndpoints.device_progress(device="motor1"),
|
||||
@@ -249,6 +290,21 @@ def test_cleanup_disconnects_active_device_subscription(scan_progressbar, monkey
|
||||
]
|
||||
assert scan_progressbar._progress_source is None
|
||||
assert scan_progressbar._progress_device is None
|
||||
close_mock.assert_not_called()
|
||||
delete_later_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_cleanup_stops_active_task(scan_progressbar, monkeypatch):
|
||||
monkeypatch.setattr(BECWidget, "cleanup", lambda self: None)
|
||||
scan_progressbar.task = ProgressTask(parent=scan_progressbar)
|
||||
scan_progressbar._active_scan_id = "scan-1"
|
||||
timer = scan_progressbar.task.timer
|
||||
|
||||
ScanProgressBar.cleanup(scan_progressbar)
|
||||
|
||||
assert not timer.isActive()
|
||||
assert scan_progressbar.task is None
|
||||
assert scan_progressbar._active_scan_id is None
|
||||
|
||||
|
||||
def test_progressbar_queue_update(scan_progressbar):
|
||||
@@ -265,6 +321,70 @@ def test_progressbar_queue_update(scan_progressbar):
|
||||
mock_set_source.assert_not_called()
|
||||
|
||||
|
||||
def test_progressbar_queue_update_clears_task_when_queue_is_empty(scan_progressbar):
|
||||
scan_progressbar.task = ProgressTask(parent=scan_progressbar)
|
||||
scan_progressbar._active_scan_id = "scan-1"
|
||||
timer = scan_progressbar.task.timer
|
||||
msg = messages.ScanQueueStatusMessage(
|
||||
queue={"primary": messages.ScanQueueStatus(info=[], status="RUNNING")}
|
||||
)
|
||||
|
||||
scan_progressbar.on_queue_update(
|
||||
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
|
||||
)
|
||||
|
||||
assert not timer.isActive()
|
||||
assert scan_progressbar.task is None
|
||||
assert scan_progressbar._active_scan_id is None
|
||||
|
||||
|
||||
def test_progressbar_queue_update_clears_task_when_scan_is_not_running(
|
||||
scan_progressbar, scan_message
|
||||
):
|
||||
scan_progressbar.task = ProgressTask(parent=scan_progressbar)
|
||||
scan_progressbar._active_scan_id = "scan-1"
|
||||
timer = scan_progressbar.task.timer
|
||||
request_block = messages.RequestBlock(
|
||||
msg=scan_message,
|
||||
RID="some-rid",
|
||||
scan_motors=["samx"],
|
||||
readout_priority={"monitored": ["samx"]},
|
||||
is_scan=True,
|
||||
scan_number=1,
|
||||
scan_id="scan-1",
|
||||
report_instructions=[{"scan_progress": 20}],
|
||||
)
|
||||
msg = messages.ScanQueueStatusMessage(
|
||||
metadata={},
|
||||
queue={
|
||||
"primary": messages.ScanQueueStatus(
|
||||
info=[
|
||||
messages.QueueInfoEntry(
|
||||
queue_id="queue-1",
|
||||
scan_id=["scan-1"],
|
||||
status="completed",
|
||||
active_request_block=request_block,
|
||||
is_scan=[True],
|
||||
request_blocks=[request_block],
|
||||
scan_number=[1],
|
||||
)
|
||||
],
|
||||
status="RUNNING",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
with mock.patch.object(scan_progressbar, "set_progress_source") as mock_set_source:
|
||||
scan_progressbar.on_queue_update(
|
||||
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
|
||||
)
|
||||
|
||||
assert not timer.isActive()
|
||||
assert scan_progressbar.task is None
|
||||
assert scan_progressbar._active_scan_id is None
|
||||
mock_set_source.assert_not_called()
|
||||
|
||||
|
||||
def test_progressbar_queue_update_with_scan(scan_progressbar, scan_message):
|
||||
"""
|
||||
Test that a queue update with a scan changes the progress source to SCAN_PROGRESS.
|
||||
@@ -306,6 +426,60 @@ def test_progressbar_queue_update_with_scan(scan_progressbar, scan_message):
|
||||
mock_set_source.assert_called_once_with(ProgressSource.SCAN_PROGRESS)
|
||||
|
||||
|
||||
def test_progressbar_queue_update_starts_new_task_for_new_scan(scan_progressbar, scan_message):
|
||||
started = mock.Mock()
|
||||
scan_progressbar.progress_started.connect(started)
|
||||
|
||||
def queue_msg(scan_id: str, scan_number: int):
|
||||
request_block = messages.RequestBlock(
|
||||
msg=scan_message,
|
||||
RID=f"rid-{scan_number}",
|
||||
scan_motors=["samx"],
|
||||
readout_priority={"monitored": ["samx"]},
|
||||
is_scan=True,
|
||||
scan_number=scan_number,
|
||||
scan_id=scan_id,
|
||||
report_instructions=[{"scan_progress": 20}],
|
||||
)
|
||||
return messages.ScanQueueStatusMessage(
|
||||
metadata={},
|
||||
queue={
|
||||
"primary": messages.ScanQueueStatus(
|
||||
info=[
|
||||
messages.QueueInfoEntry(
|
||||
queue_id=f"queue-{scan_number}",
|
||||
scan_id=[scan_id],
|
||||
status="RUNNING",
|
||||
active_request_block=request_block,
|
||||
is_scan=[True],
|
||||
request_blocks=[request_block],
|
||||
scan_number=[scan_number],
|
||||
)
|
||||
],
|
||||
status="RUNNING",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
first_msg = queue_msg("scan-1", 1)
|
||||
scan_progressbar.on_queue_update(
|
||||
first_msg.content, first_msg.metadata, _override_slot_params={"verify_sender": False}
|
||||
)
|
||||
first_task = scan_progressbar.task
|
||||
assert first_task is not None
|
||||
assert first_task.timer.isActive()
|
||||
|
||||
second_msg = queue_msg("scan-2", 2)
|
||||
scan_progressbar.on_queue_update(
|
||||
second_msg.content, second_msg.metadata, _override_slot_params={"verify_sender": False}
|
||||
)
|
||||
|
||||
assert started.call_count == 2
|
||||
assert not first_task.timer.isActive()
|
||||
assert scan_progressbar.task is not first_task
|
||||
assert scan_progressbar._active_scan_id == "scan-2"
|
||||
|
||||
|
||||
def test_progressbar_queue_update_with_device(scan_progressbar, scan_message):
|
||||
"""
|
||||
Test that a queue update with a device changes the progress source to DEVICE_PROGRESS.
|
||||
@@ -347,6 +521,44 @@ def test_progressbar_queue_update_with_device(scan_progressbar, scan_message):
|
||||
mock_set_source.assert_called_once_with(ProgressSource.DEVICE_PROGRESS, device="samx")
|
||||
|
||||
|
||||
def test_progressbar_queue_update_ignores_empty_device_progress(scan_progressbar, scan_message):
|
||||
request_block = messages.RequestBlock(
|
||||
msg=scan_message,
|
||||
RID="some-rid",
|
||||
scan_motors=["samx"],
|
||||
readout_priority={"monitored": ["samx"]},
|
||||
is_scan=True,
|
||||
scan_number=1,
|
||||
scan_id="e3f50794-852c-4bb1-965e-41c585ab0aa9",
|
||||
report_instructions=[{"device_progress": []}],
|
||||
)
|
||||
msg = messages.ScanQueueStatusMessage(
|
||||
metadata={},
|
||||
queue={
|
||||
"primary": messages.ScanQueueStatus(
|
||||
info=[
|
||||
messages.QueueInfoEntry(
|
||||
queue_id="40831e2c-fbd1-4432-8072-ad168a7ad964",
|
||||
scan_id=["e3f50794-852c-4bb1-965e-41c585ab0aa9"],
|
||||
status="RUNNING",
|
||||
active_request_block=request_block,
|
||||
is_scan=[True],
|
||||
request_blocks=[request_block],
|
||||
scan_number=[1],
|
||||
)
|
||||
],
|
||||
status="RUNNING",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
with mock.patch.object(scan_progressbar, "set_progress_source") as mock_set_source:
|
||||
scan_progressbar.on_queue_update(
|
||||
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
|
||||
)
|
||||
mock_set_source.assert_not_called()
|
||||
|
||||
|
||||
def test_progressbar_queue_update_with_no_scan_or_device(scan_progressbar, scan_message):
|
||||
"""
|
||||
Test that a queue update with neither scan nor device does not change the progress source.
|
||||
|
||||
Reference in New Issue
Block a user