diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 72fcf077..79234a8d 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -17,8 +17,8 @@ class Widgets(str, enum.Enum): BECDock = "BECDock" BECDockArea = "BECDockArea" BECFigure = "BECFigure" + RingProgressBar = "RingProgressBar" ScanControl = "ScanControl" - SpiralProgressBar = "SpiralProgressBar" TextBox = "TextBox" VSCodeEditor = "VSCodeEditor" WebsiteWidget = "WebsiteWidget" @@ -1824,25 +1824,7 @@ class Ring(RPCBase): """ -class ScanControl(RPCBase): - @property - @rpc_call - def config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ - - @rpc_call - def get_all_rpc(self) -> "dict": - """ - Get all registered RPC objects. - """ - - -class SpiralProgressBar(RPCBase): +class RingProgressBar(RPCBase): @rpc_call def get_all_rpc(self) -> "dict": """ @@ -1874,7 +1856,7 @@ class SpiralProgressBar(RPCBase): """ @rpc_call - def update_config(self, config: "SpiralProgressBarConfig | dict"): + def update_config(self, config: "RingProgressBarConfig | dict"): """ Update the configuration of the widget. @@ -2021,6 +2003,24 @@ class SpiralProgressBar(RPCBase): """ +class ScanControl(RPCBase): + @property + @rpc_call + def config_dict(self) -> "dict": + """ + Get the configuration of the widget. + + Returns: + dict: The configuration of the widget. + """ + + @rpc_call + def get_all_rpc(self) -> "dict": + """ + Get all registered RPC objects. + """ + + class StopButton(RPCBase): @property @rpc_call diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.py b/bec_widgets/examples/jupyter_console/jupyter_console_window.py index 09c10f21..174b19f3 100644 --- a/bec_widgets/examples/jupyter_console/jupyter_console_window.py +++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.py @@ -116,7 +116,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover: self.d2 = self.dock.add_dock(name="dock_2", position="bottom") self.fig2 = self.d2.add_widget("BECFigure", row=0, col=0) self.fig2.plot(x_name="samx", y_name="bpm4i") - self.bar = self.d2.add_widget("SpiralProgressBar", row=0, col=1) + self.bar = self.d2.add_widget("RingProgressBar", row=0, col=1) self.bar.set_diameter(200) self.dock.save_state() diff --git a/bec_widgets/widgets/__init__.py b/bec_widgets/widgets/__init__.py index 00f7a11a..8b137891 100644 --- a/bec_widgets/widgets/__init__.py +++ b/bec_widgets/widgets/__init__.py @@ -1,5 +1 @@ -# from .buttons import StopButton -# from .dock import BECDock, BECDockArea -# from .figure import BECFigure, FigureConfig -# from .scan_control import ScanControl -# from .spiral_progress_bar import SpiralProgressBar + diff --git a/bec_widgets/widgets/ring_progress_bar/__init__.py b/bec_widgets/widgets/ring_progress_bar/__init__.py new file mode 100644 index 00000000..c20ea559 --- /dev/null +++ b/bec_widgets/widgets/ring_progress_bar/__init__.py @@ -0,0 +1 @@ +from .ring_progress_bar import RingProgressBar diff --git a/bec_widgets/widgets/spiral_progress_bar/ring.py b/bec_widgets/widgets/ring_progress_bar/ring.py similarity index 93% rename from bec_widgets/widgets/spiral_progress_bar/ring.py rename to bec_widgets/widgets/ring_progress_bar/ring.py index cf61425d..29be4e17 100644 --- a/bec_widgets/widgets/spiral_progress_bar/ring.py +++ b/bec_widgets/widgets/ring_progress_bar/ring.py @@ -10,12 +10,13 @@ from qtpy import QtGui from bec_widgets.utils import BECConnector, ConnectionConfig -class RingConnections(BaseModel): +class ProgressbarConnections(BaseModel): slot: Literal["on_scan_progress", "on_device_readback"] = None endpoint: EndpointInfo | str = None model_config: dict = {"validate_assignment": True} @field_validator("endpoint") + @classmethod def validate_endpoint(cls, v, values): slot = values.data["slot"] v = v.endpoint if isinstance(v, EndpointInfo) else v @@ -36,7 +37,7 @@ class RingConnections(BaseModel): return v -class RingConfig(ConnectionConfig): +class ProgressbarConfig(ConnectionConfig): value: int | float | None = Field(0, description="Value for the progress bars.") direction: int | None = Field( -1, description="Direction of the progress bars. -1 for clockwise, 1 for counter-clockwise." @@ -62,8 +63,17 @@ class RingConfig(ConnectionConfig): update_behaviour: Literal["manual", "auto"] | None = Field( "auto", description="Update behaviour for the progress bars." ) - connections: RingConnections | None = Field( - default_factory=RingConnections, description="Connections for the progress bars." + connections: ProgressbarConnections | None = Field( + default_factory=ProgressbarConnections, description="Connections for the progress bars." + ) + + +class RingConfig(ProgressbarConfig): + index: int | None = Field(0, description="Index of the progress bar. 0 is outer ring.") + start_position: int | None = Field( + 90, + description="Start position for the progress bars in degrees. Default is 90 degrees - corespons to " + "the top of the ring.", ) @@ -230,7 +240,7 @@ class Ring(BECConnector): self.bec_dispatcher.disconnect_slot( self.config.connections.slot, self.config.connections.endpoint ) - self.config.connections = RingConnections(slot=slot, endpoint=endpoint) + self.config.connections = ProgressbarConnections(slot=slot, endpoint=endpoint) self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint) def reset_connection(self): @@ -240,7 +250,7 @@ class Ring(BECConnector): self.bec_dispatcher.disconnect_slot( self.config.connections.slot, self.config.connections.endpoint ) - self.config.connections = RingConnections() + self.config.connections = ProgressbarConnections() def on_scan_progress(self, msg, meta): """ diff --git a/bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py b/bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py similarity index 97% rename from bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py rename to bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py index e1997bad..548944a9 100644 --- a/bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py +++ b/bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py @@ -11,10 +11,10 @@ from qtpy.QtCore import QSize, Slot from qtpy.QtWidgets import QSizePolicy, QWidget from bec_widgets.utils import BECConnector, Colors, ConnectionConfig, EntryValidator -from bec_widgets.widgets.spiral_progress_bar.ring import Ring, RingConfig +from bec_widgets.widgets.ring_progress_bar.ring import Ring, RingConfig -class SpiralProgressBarConfig(ConnectionConfig): +class RingProgressBarConfig(ConnectionConfig): color_map: Optional[str] = Field( "magma", description="Color scheme for the progress bars.", validate_default=True ) @@ -32,6 +32,7 @@ class SpiralProgressBarConfig(ConnectionConfig): rings: list[RingConfig] | None = Field([], description="List of ring configurations.") @field_validator("num_bars") + @classmethod def validate_num_bars(cls, v, values): min_number_of_bars = values.data.get("min_number_of_bars", None) max_number_of_bars = values.data.get("max_number_of_bars", None) @@ -43,6 +44,7 @@ class SpiralProgressBarConfig(ConnectionConfig): return v @field_validator("rings") + @classmethod def validate_rings(cls, v, values): if v is not None and v is not []: num_bars = values.data.get("num_bars", None) @@ -64,7 +66,7 @@ class SpiralProgressBarConfig(ConnectionConfig): _validate_colormap = field_validator("color_map")(Colors.validate_color_map) -class SpiralProgressBar(BECConnector, QWidget): +class RingProgressBar(BECConnector, QWidget): USER_ACCESS = [ "get_all_rpc", "rpc_id", @@ -89,17 +91,17 @@ class SpiralProgressBar(BECConnector, QWidget): def __init__( self, parent=None, - config: SpiralProgressBarConfig | dict | None = None, + config: RingProgressBarConfig | dict | None = None, client=None, gui_id: str | None = None, num_bars: int | None = None, ): if config is None: - config = SpiralProgressBarConfig(widget_class=self.__class__.__name__) + config = RingProgressBarConfig(widget_class=self.__class__.__name__) self.config = config else: if isinstance(config, dict): - config = SpiralProgressBarConfig(**config, widget_class=self.__class__.__name__) + config = RingProgressBarConfig(**config, widget_class=self.__class__.__name__) self.config = config super().__init__(client=client, config=config, gui_id=gui_id) QWidget.__init__(self, parent=None) @@ -129,7 +131,7 @@ class SpiralProgressBar(BECConnector, QWidget): def rings(self, value): self._rings = value - def update_config(self, config: SpiralProgressBarConfig | dict): + def update_config(self, config: RingProgressBarConfig | dict): """ Update the configuration of the widget. @@ -137,7 +139,7 @@ class SpiralProgressBar(BECConnector, QWidget): config(SpiralProgressBarConfig|dict): Configuration to update. """ if isinstance(config, dict): - config = SpiralProgressBarConfig(**config, widget_class=self.__class__.__name__) + config = RingProgressBarConfig(**config, widget_class=self.__class__.__name__) self.config = config self.clear_all() diff --git a/bec_widgets/widgets/spiral_progress_bar/__init__.py b/bec_widgets/widgets/spiral_progress_bar/__init__.py deleted file mode 100644 index 81ed0f36..00000000 --- a/bec_widgets/widgets/spiral_progress_bar/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .spiral_progress_bar import SpiralProgressBar diff --git a/docs/user/getting_started/quick_start.md b/docs/user/getting_started/quick_start.md index 1ed5fcb6..eeb6ea96 100644 --- a/docs/user/getting_started/quick_start.md +++ b/docs/user/getting_started/quick_start.md @@ -97,11 +97,11 @@ Note, we chain commands here which is possible since the `add_dock` and `add_wid cam_widget.set_title("Camera Image Eiger") cam_widget.set_vrange(vmin=0, vmax=100) ``` -As a final step, we can now add also a SpiralProgressBar to a new dock, and perform a grid_scan with the motors *samx* and *samy*. +As a final step, we can now add also a RingProgressBar to a new dock, and perform a grid_scan with the motors *samx* and *samy*. As you see in the example below, all docks are arranged below each other. This is the default behavior of the `add_dock` method. However, the docks can be freely arranged by drag and drop as desired by the user. We invite you to explore this by yourself following the example in the video, and build your custom GUI with BEC Widgets. ```python -prog_bar = gui.add_dock(name="prog_dock").add_widget('SpiralProgressBar') +prog_bar = gui.add_dock(name="prog_dock").add_widget('RingProgressBar') prog_bar.set_line_widths(15) scans.grid_scan(dev.samy, -2, 2, 10, dev.samx, -5, 5, 10, exp_time=0.1, relative=False) ``` diff --git a/docs/user/widgets/spiral_progress_bar.md b/docs/user/widgets/ring_progress_bar.md similarity index 66% rename from docs/user/widgets/spiral_progress_bar.md rename to docs/user/widgets/ring_progress_bar.md index 2f28b149..fd7a8dd8 100644 --- a/docs/user/widgets/spiral_progress_bar.md +++ b/docs/user/widgets/ring_progress_bar.md @@ -1,9 +1,9 @@ (user.widgets.spiral_progress_bar)= -# [Spiral Progress Bar](/api_reference/_autosummary/bec_widgets.cli.client.SpiralProgressBar) +# [Ring Progress Bar](/api_reference/_autosummary/bec_widgets.cli.client.RingProgressBar) **Purpose:** -The Spiral Progress Bar widget is a circular progress bar that can be used to visualize the progress of a task. The +The ring Progress Bar widget is a circular progress bar that can be used to visualize the progress of a task. The widget is designed to be used in applications where the progress of a task is represented as a percentage. The Spiral Progress Bar widget is a part of the BEC Widgets library and can be controlled directly using its API, or hooked up to the progress of a device readback or scan. @@ -15,22 +15,22 @@ the progress of a device readback or scan. - multiple progress rings to show different tasks in parallel. **Example of Use:** -![SpiralProgressBar](./progress_bar.gif) +![RingProgressBar](./progress_bar.gif) **Code example:** -The following code snipped demonstrates how to create a `SpiralProgressBar` using BEC Widgets within BEC. +The following code snipped demonstrates how to create a `RingProgressBar` using BEC Widgets within BEC. ```python -# adds a new dock with a spiral progress bar -progress = gui.add_dock().add_widget("SpiralProgressBar") +# adds a new dock with a ring progress bar +progress = gui.add_dock().add_widget("RingProgressBar") # customize the size of the ring progress.set_line_width(20) ``` -By default, the Spiral Progress Bar widget will display a single ring. To add more rings, use the add_ring method: +By default, the Ring Progress Bar widget will display a single ring. To add more rings, use the add_ring method: ```python -# adds a new dock with a spiral progress bar +# adds a new dock with a ring progress bar progress.add_ring() ``` @@ -42,7 +42,7 @@ progress.rings[0].set_line_width(20) # set the width of the first ring progress.rings[1].set_line_width(10) # set the width of the second ring ``` -By default, the `SpiralProgressBar` widget is set with `progress.enable_auto_update(True)`, which will automatically +By default, the `RingProgressBar` widget is set with `progress.enable_auto_update(True)`, which will automatically update the bars in the widget. To manually set updates for each progress bar, use the set_update method. Note that manually updating a ring will disable the automatic update for the whole widget: diff --git a/docs/user/widgets/widgets.md b/docs/user/widgets/widgets.md index c35212fd..9d19ed7e 100644 --- a/docs/user/widgets/widgets.md +++ b/docs/user/widgets/widgets.md @@ -9,7 +9,7 @@ hidden: false --- bec_figure/ -spiral_progress_bar/ +ring_progress_bar/ website/ buttons/ text_box/ diff --git a/tests/end-2-end/test_bec_dock_rpc_e2e.py b/tests/end-2-end/test_bec_dock_rpc_e2e.py index 87629dc4..893e8e1b 100644 --- a/tests/end-2-end/test_bec_dock_rpc_e2e.py +++ b/tests/end-2-end/test_bec_dock_rpc_e2e.py @@ -151,13 +151,13 @@ def test_dock_manipulations_e2e(rpc_server_dock): assert len(dock.temp_areas) == 0 -def test_spiral_bar(rpc_server_dock): +def test_ring_bar(rpc_server_dock): dock = BECDockArea(rpc_server_dock) d0 = dock.add_dock(name="dock_0") - bar = d0.add_widget("SpiralProgressBar") - assert bar.__class__.__name__ == "SpiralProgressBar" + bar = d0.add_widget("RingProgressBar") + assert bar.__class__.__name__ == "RingProgressBar" bar.set_number_of_bars(5) bar.set_colors_from_map("viridis") @@ -173,12 +173,12 @@ def test_spiral_bar(rpc_server_dock): assert bar_colors == expected_colors -def test_spiral_bar_scan_update(bec_client_lib, rpc_server_dock): +def test_ring_bar_scan_update(bec_client_lib, rpc_server_dock): dock = BECDockArea(rpc_server_dock) d0 = dock.add_dock("dock_0") - bar = d0.add_widget("SpiralProgressBar") + bar = d0.add_widget("RingProgressBar") client = bec_client_lib dev = client.device_manager.devices diff --git a/tests/unit_tests/test_ring_progress_bar.py b/tests/unit_tests/test_ring_progress_bar.py new file mode 100644 index 00000000..602bb62b --- /dev/null +++ b/tests/unit_tests/test_ring_progress_bar.py @@ -0,0 +1,338 @@ +# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import + +import pytest +from bec_lib.endpoints import MessageEndpoints +from pydantic import ValidationError + +from bec_widgets.utils import Colors +from bec_widgets.widgets.ring_progress_bar import RingProgressBar +from bec_widgets.widgets.ring_progress_bar.ring import ProgressbarConnections, RingConfig +from bec_widgets.widgets.ring_progress_bar.ring_progress_bar import RingProgressBarConfig + +from .client_mocks import mocked_client + + +@pytest.fixture +def ring_progress_bar(qtbot, mocked_client): + widget = RingProgressBar(client=mocked_client) + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + yield widget + widget.close() + + +def test_bar_init(ring_progress_bar): + assert ring_progress_bar is not None + assert ring_progress_bar.client is not None + assert isinstance(ring_progress_bar, RingProgressBar) + assert ring_progress_bar.config.widget_class == "RingProgressBar" + assert ring_progress_bar.config.gui_id is not None + assert ring_progress_bar.gui_id == ring_progress_bar.config.gui_id + + +def test_config_validation_num_of_bars(): + config = RingProgressBarConfig(num_bars=100, min_num_bars=1, max_num_bars=10) + + assert config.num_bars == 10 + + +def test_config_validation_num_of_ring_error(): + ring_config_0 = RingConfig(index=0) + ring_config_1 = RingConfig(index=1) + + with pytest.raises(ValidationError) as excinfo: + RingProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=1) + errors = excinfo.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "different number of configs" + assert "Length of rings configuration (2) does not match the number of bars (1)." in str( + excinfo.value + ) + + +def test_config_validation_ring_indices_wrong_order(): + ring_config_0 = RingConfig(index=2) + ring_config_1 = RingConfig(index=5) + + with pytest.raises(ValidationError) as excinfo: + RingProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2) + errors = excinfo.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "wrong indices" + assert ( + "Indices of ring configurations must be unique and in order from 0 to num_bars 2." + in str(excinfo.value) + ) + + +def test_config_validation_ring_same_indices(): + ring_config_0 = RingConfig(index=0) + ring_config_1 = RingConfig(index=0) + + with pytest.raises(ValidationError) as excinfo: + RingProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2) + errors = excinfo.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "wrong indices" + assert ( + "Indices of ring configurations must be unique and in order from 0 to num_bars 2." + in str(excinfo.value) + ) + + +def test_config_validation_invalid_colormap(): + with pytest.raises(ValueError) as excinfo: + RingProgressBarConfig(color_map="crazy_colors") + errors = excinfo.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "unsupported colormap" + assert "Colormap 'crazy_colors' not found in the current installation of pyqtgraph" in str( + excinfo.value + ) + + +def test_ring_connection_endpoint_validation(): + with pytest.raises(ValueError) as excinfo: + ProgressbarConnections(slot="on_scan_progress", endpoint="non_existing") + errors = excinfo.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "unsupported endpoint" + assert ( + "For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'." + in str(excinfo.value) + ) + + with pytest.raises(ValueError) as excinfo: + ProgressbarConnections(slot="on_device_readback", endpoint="non_existing") + errors = excinfo.value.errors() + assert len(errors) == 1 + assert errors[0]["type"] == "unsupported endpoint" + assert ( + "For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'." + in str(excinfo.value) + ) + + +def test_bar_add_number_of_bars(ring_progress_bar): + assert ring_progress_bar.config.num_bars == 1 + + ring_progress_bar.set_number_of_bars(5) + assert ring_progress_bar.config.num_bars == 5 + + ring_progress_bar.set_number_of_bars(2) + assert ring_progress_bar.config.num_bars == 2 + + +def test_add_remove_bars_individually(ring_progress_bar): + ring_progress_bar.add_ring() + ring_progress_bar.add_ring() + + assert ring_progress_bar.config.num_bars == 3 + assert len(ring_progress_bar.config.rings) == 3 + + ring_progress_bar.remove_ring(1) + assert ring_progress_bar.config.num_bars == 2 + assert len(ring_progress_bar.config.rings) == 2 + assert ring_progress_bar.rings[0].config.index == 0 + assert ring_progress_bar.rings[1].config.index == 1 + + +def test_bar_set_value(ring_progress_bar): + ring_progress_bar.set_number_of_bars(5) + + assert ring_progress_bar.config.num_bars == 5 + assert len(ring_progress_bar.config.rings) == 5 + assert len(ring_progress_bar.rings) == 5 + + ring_progress_bar.set_value([10, 20, 30, 40, 50]) + ring_values = [ring.config.value for ring in ring_progress_bar.rings] + assert ring_values == [10, 20, 30, 40, 50] + + # update just one bar + ring_progress_bar.set_value(90, 1) + ring_values = [ring.config.value for ring in ring_progress_bar.rings] + assert ring_values == [10, 90, 30, 40, 50] + + +def test_bar_set_precision(ring_progress_bar): + ring_progress_bar.set_number_of_bars(3) + + assert ring_progress_bar.config.num_bars == 3 + assert len(ring_progress_bar.config.rings) == 3 + assert len(ring_progress_bar.rings) == 3 + + ring_progress_bar.set_precision(2) + ring_precision = [ring.config.precision for ring in ring_progress_bar.rings] + assert ring_precision == [2, 2, 2] + + ring_progress_bar.set_value([10.1234, 20.1234, 30.1234]) + ring_values = [ring.config.value for ring in ring_progress_bar.rings] + assert ring_values == [10.12, 20.12, 30.12] + + ring_progress_bar.set_precision(4, 1) + ring_precision = [ring.config.precision for ring in ring_progress_bar.rings] + assert ring_precision == [2, 4, 2] + + ring_progress_bar.set_value([10.1234, 20.1234, 30.1234]) + ring_values = [ring.config.value for ring in ring_progress_bar.rings] + assert ring_values == [10.12, 20.1234, 30.12] + + +def test_set_min_max_value(ring_progress_bar): + ring_progress_bar.set_number_of_bars(2) + + ring_progress_bar.set_min_max_values(0, 10) + ring_min_values = [ring.config.min_value for ring in ring_progress_bar.rings] + ring_max_values = [ring.config.max_value for ring in ring_progress_bar.rings] + + assert ring_min_values == [0, 0] + assert ring_max_values == [10, 10] + + ring_progress_bar.set_value([5, 15]) + ring_values = [ring.config.value for ring in ring_progress_bar.rings] + assert ring_values == [5, 10] + + +def test_setup_colors_from_colormap(ring_progress_bar): + ring_progress_bar.set_number_of_bars(5) + ring_progress_bar.set_colors_from_map("viridis", "RGB") + + expected_colors = Colors.golden_angle_color("viridis", 5, "RGB") + converted_colors = [ring.color.getRgb() for ring in ring_progress_bar.rings] + ring_config_colors = [ring.config.color for ring in ring_progress_bar.rings] + + assert expected_colors == converted_colors + assert ring_config_colors == expected_colors + + +def get_colors_from_rings(rings): + converted_colors = [ring.color.getRgb() for ring in rings] + ring_config_colors = [ring.config.color for ring in rings] + return converted_colors, ring_config_colors + + +def test_set_colors_from_colormap_and_change_num_of_bars(ring_progress_bar): + ring_progress_bar.set_number_of_bars(2) + ring_progress_bar.set_colors_from_map("viridis", "RGB") + + expected_colors = Colors.golden_angle_color("viridis", 2, "RGB") + converted_colors, ring_config_colors = get_colors_from_rings(ring_progress_bar.rings) + + assert expected_colors == converted_colors + assert ring_config_colors == expected_colors + + # increase the number of bars to 6 + ring_progress_bar.set_number_of_bars(6) + expected_colors = Colors.golden_angle_color("viridis", 6, "RGB") + converted_colors, ring_config_colors = get_colors_from_rings(ring_progress_bar.rings) + + assert expected_colors == converted_colors + assert ring_config_colors == expected_colors + + # decrease the number of bars to 3 + ring_progress_bar.set_number_of_bars(3) + expected_colors = Colors.golden_angle_color("viridis", 3, "RGB") + converted_colors, ring_config_colors = get_colors_from_rings(ring_progress_bar.rings) + + assert expected_colors == converted_colors + assert ring_config_colors == expected_colors + + +def test_set_colors_directly(ring_progress_bar): + ring_progress_bar.set_number_of_bars(3) + + # setting as a list of rgb tuples + colors = [(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255)] + ring_progress_bar.set_colors_directly(colors) + converted_colors = get_colors_from_rings(ring_progress_bar.rings)[0] + + assert colors == converted_colors + + ring_progress_bar.set_colors_directly((255, 0, 0, 255), 1) + converted_colors = get_colors_from_rings(ring_progress_bar.rings)[0] + + assert converted_colors == [(255, 0, 0, 255), (255, 0, 0, 255), (0, 0, 255, 255)] + + +def test_set_line_width(ring_progress_bar): + ring_progress_bar.set_number_of_bars(3) + + ring_progress_bar.set_line_widths(5) + line_widths = [ring.config.line_width for ring in ring_progress_bar.rings] + + assert line_widths == [5, 5, 5] + + ring_progress_bar.set_line_widths([10, 20, 30]) + line_widths = [ring.config.line_width for ring in ring_progress_bar.rings] + + assert line_widths == [10, 20, 30] + + ring_progress_bar.set_line_widths(15, 1) + line_widths = [ring.config.line_width for ring in ring_progress_bar.rings] + + assert line_widths == [10, 15, 30] + + +def test_set_gap(ring_progress_bar): + ring_progress_bar.set_number_of_bars(3) + ring_progress_bar.set_gap(20) + + assert ring_progress_bar.config.gap == 20 + + +def test_auto_update(ring_progress_bar): + ring_progress_bar.enable_auto_updates(True) + + scan_queue_status_scan_progress = { + "queue": { + "primary": { + "info": [{"active_request_block": {"report_instructions": [{"scan_progress": 10}]}}] + } + } + } + meta = {} + + ring_progress_bar.on_scan_queue_status(scan_queue_status_scan_progress, meta) + + assert ring_progress_bar._auto_updates is True + assert len(ring_progress_bar._rings) == 1 + assert ring_progress_bar._rings[0].config.connections == ProgressbarConnections( + slot="on_scan_progress", endpoint=MessageEndpoints.scan_progress() + ) + + scan_queue_status_device_readback = { + "queue": { + "primary": { + "info": [ + { + "active_request_block": { + "report_instructions": [ + { + "readback": { + "devices": ["samx", "samy"], + "start": [1, 2], + "end": [10, 20], + } + } + ] + } + } + ] + } + } + } + ring_progress_bar.on_scan_queue_status(scan_queue_status_device_readback, meta) + + assert ring_progress_bar._auto_updates is True + assert len(ring_progress_bar._rings) == 2 + assert ring_progress_bar._rings[0].config.connections == ProgressbarConnections( + slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samx") + ) + assert ring_progress_bar._rings[1].config.connections == ProgressbarConnections( + slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samy") + ) + + assert ring_progress_bar._rings[0].config.min_value == 1 + assert ring_progress_bar._rings[0].config.max_value == 10 + assert ring_progress_bar._rings[1].config.min_value == 2 + assert ring_progress_bar._rings[1].config.max_value == 20 diff --git a/tests/unit_tests/test_rpc_widget_handler.py b/tests/unit_tests/test_rpc_widget_handler.py index f86d39e8..ddd8f47d 100644 --- a/tests/unit_tests/test_rpc_widget_handler.py +++ b/tests/unit_tests/test_rpc_widget_handler.py @@ -4,4 +4,4 @@ from bec_widgets.cli.rpc_wigdet_handler import RPCWidgetHandler def test_rpc_widget_handler(): handler = RPCWidgetHandler() assert "BECFigure" in handler.widget_classes - assert "SpiralProgressBar" in handler.widget_classes + assert "RingProgressBar" in handler.widget_classes diff --git a/tests/unit_tests/test_spiral_progress_bar.py b/tests/unit_tests/test_spiral_progress_bar.py deleted file mode 100644 index 11d01179..00000000 --- a/tests/unit_tests/test_spiral_progress_bar.py +++ /dev/null @@ -1,338 +0,0 @@ -# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import - -import pytest -from bec_lib.endpoints import MessageEndpoints -from pydantic import ValidationError - -from bec_widgets.utils import Colors -from bec_widgets.widgets.spiral_progress_bar import SpiralProgressBar -from bec_widgets.widgets.spiral_progress_bar.ring import RingConfig, RingConnections -from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBarConfig - -from .client_mocks import mocked_client - - -@pytest.fixture -def spiral_progress_bar(qtbot, mocked_client): - widget = SpiralProgressBar(client=mocked_client) - qtbot.addWidget(widget) - qtbot.waitExposed(widget) - yield widget - widget.close() - - -def test_bar_init(spiral_progress_bar): - assert spiral_progress_bar is not None - assert spiral_progress_bar.client is not None - assert isinstance(spiral_progress_bar, SpiralProgressBar) - assert spiral_progress_bar.config.widget_class == "SpiralProgressBar" - assert spiral_progress_bar.config.gui_id is not None - assert spiral_progress_bar.gui_id == spiral_progress_bar.config.gui_id - - -def test_config_validation_num_of_bars(): - config = SpiralProgressBarConfig(num_bars=100, min_num_bars=1, max_num_bars=10) - - assert config.num_bars == 10 - - -def test_config_validation_num_of_ring_error(): - ring_config_0 = RingConfig(index=0) - ring_config_1 = RingConfig(index=1) - - with pytest.raises(ValidationError) as excinfo: - SpiralProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=1) - errors = excinfo.value.errors() - assert len(errors) == 1 - assert errors[0]["type"] == "different number of configs" - assert "Length of rings configuration (2) does not match the number of bars (1)." in str( - excinfo.value - ) - - -def test_config_validation_ring_indices_wrong_order(): - ring_config_0 = RingConfig(index=2) - ring_config_1 = RingConfig(index=5) - - with pytest.raises(ValidationError) as excinfo: - SpiralProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2) - errors = excinfo.value.errors() - assert len(errors) == 1 - assert errors[0]["type"] == "wrong indices" - assert ( - "Indices of ring configurations must be unique and in order from 0 to num_bars 2." - in str(excinfo.value) - ) - - -def test_config_validation_ring_same_indices(): - ring_config_0 = RingConfig(index=0) - ring_config_1 = RingConfig(index=0) - - with pytest.raises(ValidationError) as excinfo: - SpiralProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2) - errors = excinfo.value.errors() - assert len(errors) == 1 - assert errors[0]["type"] == "wrong indices" - assert ( - "Indices of ring configurations must be unique and in order from 0 to num_bars 2." - in str(excinfo.value) - ) - - -def test_config_validation_invalid_colormap(): - with pytest.raises(ValueError) as excinfo: - SpiralProgressBarConfig(color_map="crazy_colors") - errors = excinfo.value.errors() - assert len(errors) == 1 - assert errors[0]["type"] == "unsupported colormap" - assert "Colormap 'crazy_colors' not found in the current installation of pyqtgraph" in str( - excinfo.value - ) - - -def test_ring_connection_endpoint_validation(): - with pytest.raises(ValueError) as excinfo: - RingConnections(slot="on_scan_progress", endpoint="non_existing") - errors = excinfo.value.errors() - assert len(errors) == 1 - assert errors[0]["type"] == "unsupported endpoint" - assert ( - "For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'." - in str(excinfo.value) - ) - - with pytest.raises(ValueError) as excinfo: - RingConnections(slot="on_device_readback", endpoint="non_existing") - errors = excinfo.value.errors() - assert len(errors) == 1 - assert errors[0]["type"] == "unsupported endpoint" - assert ( - "For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'." - in str(excinfo.value) - ) - - -def test_bar_add_number_of_bars(spiral_progress_bar): - assert spiral_progress_bar.config.num_bars == 1 - - spiral_progress_bar.set_number_of_bars(5) - assert spiral_progress_bar.config.num_bars == 5 - - spiral_progress_bar.set_number_of_bars(2) - assert spiral_progress_bar.config.num_bars == 2 - - -def test_add_remove_bars_individually(spiral_progress_bar): - spiral_progress_bar.add_ring() - spiral_progress_bar.add_ring() - - assert spiral_progress_bar.config.num_bars == 3 - assert len(spiral_progress_bar.config.rings) == 3 - - spiral_progress_bar.remove_ring(1) - assert spiral_progress_bar.config.num_bars == 2 - assert len(spiral_progress_bar.config.rings) == 2 - assert spiral_progress_bar.rings[0].config.index == 0 - assert spiral_progress_bar.rings[1].config.index == 1 - - -def test_bar_set_value(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(5) - - assert spiral_progress_bar.config.num_bars == 5 - assert len(spiral_progress_bar.config.rings) == 5 - assert len(spiral_progress_bar.rings) == 5 - - spiral_progress_bar.set_value([10, 20, 30, 40, 50]) - ring_values = [ring.config.value for ring in spiral_progress_bar.rings] - assert ring_values == [10, 20, 30, 40, 50] - - # update just one bar - spiral_progress_bar.set_value(90, 1) - ring_values = [ring.config.value for ring in spiral_progress_bar.rings] - assert ring_values == [10, 90, 30, 40, 50] - - -def test_bar_set_precision(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(3) - - assert spiral_progress_bar.config.num_bars == 3 - assert len(spiral_progress_bar.config.rings) == 3 - assert len(spiral_progress_bar.rings) == 3 - - spiral_progress_bar.set_precision(2) - ring_precision = [ring.config.precision for ring in spiral_progress_bar.rings] - assert ring_precision == [2, 2, 2] - - spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234]) - ring_values = [ring.config.value for ring in spiral_progress_bar.rings] - assert ring_values == [10.12, 20.12, 30.12] - - spiral_progress_bar.set_precision(4, 1) - ring_precision = [ring.config.precision for ring in spiral_progress_bar.rings] - assert ring_precision == [2, 4, 2] - - spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234]) - ring_values = [ring.config.value for ring in spiral_progress_bar.rings] - assert ring_values == [10.12, 20.1234, 30.12] - - -def test_set_min_max_value(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(2) - - spiral_progress_bar.set_min_max_values(0, 10) - ring_min_values = [ring.config.min_value for ring in spiral_progress_bar.rings] - ring_max_values = [ring.config.max_value for ring in spiral_progress_bar.rings] - - assert ring_min_values == [0, 0] - assert ring_max_values == [10, 10] - - spiral_progress_bar.set_value([5, 15]) - ring_values = [ring.config.value for ring in spiral_progress_bar.rings] - assert ring_values == [5, 10] - - -def test_setup_colors_from_colormap(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(5) - spiral_progress_bar.set_colors_from_map("viridis", "RGB") - - expected_colors = Colors.golden_angle_color("viridis", 5, "RGB") - converted_colors = [ring.color.getRgb() for ring in spiral_progress_bar.rings] - ring_config_colors = [ring.config.color for ring in spiral_progress_bar.rings] - - assert expected_colors == converted_colors - assert ring_config_colors == expected_colors - - -def get_colors_from_rings(rings): - converted_colors = [ring.color.getRgb() for ring in rings] - ring_config_colors = [ring.config.color for ring in rings] - return converted_colors, ring_config_colors - - -def test_set_colors_from_colormap_and_change_num_of_bars(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(2) - spiral_progress_bar.set_colors_from_map("viridis", "RGB") - - expected_colors = Colors.golden_angle_color("viridis", 2, "RGB") - converted_colors, ring_config_colors = get_colors_from_rings(spiral_progress_bar.rings) - - assert expected_colors == converted_colors - assert ring_config_colors == expected_colors - - # increase the number of bars to 6 - spiral_progress_bar.set_number_of_bars(6) - expected_colors = Colors.golden_angle_color("viridis", 6, "RGB") - converted_colors, ring_config_colors = get_colors_from_rings(spiral_progress_bar.rings) - - assert expected_colors == converted_colors - assert ring_config_colors == expected_colors - - # decrease the number of bars to 3 - spiral_progress_bar.set_number_of_bars(3) - expected_colors = Colors.golden_angle_color("viridis", 3, "RGB") - converted_colors, ring_config_colors = get_colors_from_rings(spiral_progress_bar.rings) - - assert expected_colors == converted_colors - assert ring_config_colors == expected_colors - - -def test_set_colors_directly(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(3) - - # setting as a list of rgb tuples - colors = [(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255)] - spiral_progress_bar.set_colors_directly(colors) - converted_colors = get_colors_from_rings(spiral_progress_bar.rings)[0] - - assert colors == converted_colors - - spiral_progress_bar.set_colors_directly((255, 0, 0, 255), 1) - converted_colors = get_colors_from_rings(spiral_progress_bar.rings)[0] - - assert converted_colors == [(255, 0, 0, 255), (255, 0, 0, 255), (0, 0, 255, 255)] - - -def test_set_line_width(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(3) - - spiral_progress_bar.set_line_widths(5) - line_widths = [ring.config.line_width for ring in spiral_progress_bar.rings] - - assert line_widths == [5, 5, 5] - - spiral_progress_bar.set_line_widths([10, 20, 30]) - line_widths = [ring.config.line_width for ring in spiral_progress_bar.rings] - - assert line_widths == [10, 20, 30] - - spiral_progress_bar.set_line_widths(15, 1) - line_widths = [ring.config.line_width for ring in spiral_progress_bar.rings] - - assert line_widths == [10, 15, 30] - - -def test_set_gap(spiral_progress_bar): - spiral_progress_bar.set_number_of_bars(3) - spiral_progress_bar.set_gap(20) - - assert spiral_progress_bar.config.gap == 20 - - -def test_auto_update(spiral_progress_bar): - spiral_progress_bar.enable_auto_updates(True) - - scan_queue_status_scan_progress = { - "queue": { - "primary": { - "info": [{"active_request_block": {"report_instructions": [{"scan_progress": 10}]}}] - } - } - } - meta = {} - - spiral_progress_bar.on_scan_queue_status(scan_queue_status_scan_progress, meta) - - assert spiral_progress_bar._auto_updates is True - assert len(spiral_progress_bar._rings) == 1 - assert spiral_progress_bar._rings[0].config.connections == RingConnections( - slot="on_scan_progress", endpoint=MessageEndpoints.scan_progress() - ) - - scan_queue_status_device_readback = { - "queue": { - "primary": { - "info": [ - { - "active_request_block": { - "report_instructions": [ - { - "readback": { - "devices": ["samx", "samy"], - "start": [1, 2], - "end": [10, 20], - } - } - ] - } - } - ] - } - } - } - spiral_progress_bar.on_scan_queue_status(scan_queue_status_device_readback, meta) - - assert spiral_progress_bar._auto_updates is True - assert len(spiral_progress_bar._rings) == 2 - assert spiral_progress_bar._rings[0].config.connections == RingConnections( - slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samx") - ) - assert spiral_progress_bar._rings[1].config.connections == RingConnections( - slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samy") - ) - - assert spiral_progress_bar._rings[0].config.min_value == 1 - assert spiral_progress_bar._rings[0].config.max_value == 10 - assert spiral_progress_bar._rings[1].config.min_value == 2 - assert spiral_progress_bar._rings[1].config.max_value == 20