mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-09 08:12:15 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55f5959926 | |||
| 5861b4a538 | |||
| 2d2c773ce5 | |||
| 0a3185e785 | |||
| be657616e2 |
@@ -133,8 +133,4 @@ if __name__ == "__main__":
|
|||||||
exclusive=True,
|
exclusive=True,
|
||||||
)
|
)
|
||||||
_app.show()
|
_app.show()
|
||||||
# developer_view.show()
|
|
||||||
# developer_view.setWindowTitle("Developer View")
|
|
||||||
# developer_view.resize(1920, 1080)
|
|
||||||
# developer_view.set_stretch(horizontal=[1, 3, 2], vertical=[5, 5]) #can be set during runtime
|
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import re
|
|||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
from bec_lib.messages import ProcedureRequestMessage
|
||||||
from bec_lib.script_executor import upload_script
|
from bec_lib.script_executor import upload_script
|
||||||
from bec_qthemes import material_icon
|
from bec_qthemes import material_icon
|
||||||
from qtpy.QtGui import QKeySequence, QShortcut
|
from qtpy.QtGui import QKeySequence, QShortcut # type: ignore
|
||||||
from qtpy.QtWidgets import QTextEdit
|
from qtpy.QtWidgets import QTextEdit
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
@@ -16,6 +17,7 @@ from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
|||||||
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
|
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
|
||||||
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
|
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
|
||||||
from bec_widgets.widgets.containers.qt_ads import CDockWidget
|
from bec_widgets.widgets.containers.qt_ads import CDockWidget
|
||||||
|
from bec_widgets.widgets.control.procedure_control.procedure_panel import ProcedurePanel
|
||||||
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
|
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
|
||||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||||
from bec_widgets.widgets.editors.web_console.web_console import BECShell, WebConsole
|
from bec_widgets.widgets.editors.web_console.web_console import BECShell, WebConsole
|
||||||
@@ -125,6 +127,9 @@ class DeveloperWidget(DockAreaWidget):
|
|||||||
self._current_script_id: str | None = None
|
self._current_script_id: str | None = None
|
||||||
self.script_editor_tab = None
|
self.script_editor_tab = None
|
||||||
|
|
||||||
|
self.procedures = ProcedurePanel(self)
|
||||||
|
self.procedures.setObjectName("Procedure Control")
|
||||||
|
|
||||||
self._initialize_layout()
|
self._initialize_layout()
|
||||||
|
|
||||||
# Connect editor signals
|
# Connect editor signals
|
||||||
@@ -183,24 +188,16 @@ class DeveloperWidget(DockAreaWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Plotting area on the right with signature help tabbed alongside
|
# Plotting area on the right with signature help tabbed alongside
|
||||||
self.plotting_ads_dock = self.new(
|
_r_panel = {
|
||||||
self.plotting_ads,
|
"closable": False,
|
||||||
where="right",
|
"floatable": False,
|
||||||
closable=False,
|
"movable": False,
|
||||||
floatable=False,
|
"return_dock": True,
|
||||||
movable=False,
|
"title_buttons": {"float": True},
|
||||||
return_dock=True,
|
}
|
||||||
title_buttons={"float": True},
|
self.plotting_dock = self.new(self.plotting_ads, where="right", **_r_panel)
|
||||||
)
|
self.signature_dock = self.new(self.signature_help, **_r_panel, tab_with=self.plotting_dock)
|
||||||
self.signature_dock = self.new(
|
self.procedure_dock = self.new(self.procedures, **_r_panel, tab_with=self.plotting_dock)
|
||||||
self.signature_help,
|
|
||||||
closable=False,
|
|
||||||
floatable=False,
|
|
||||||
movable=False,
|
|
||||||
tab_with=self.plotting_ads_dock,
|
|
||||||
return_dock=True,
|
|
||||||
title_buttons={"float": False, "close": False},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.set_layout_ratios(horizontal=[2, 5, 3], vertical=[7, 3])
|
self.set_layout_ratios(horizontal=[2, 5, 3], vertical=[7, 3])
|
||||||
|
|
||||||
@@ -233,6 +230,16 @@ class DeveloperWidget(DockAreaWidget):
|
|||||||
run_action.action.triggered.connect(self.on_execute)
|
run_action.action.triggered.connect(self.on_execute)
|
||||||
self.toolbar.components.add_safe("run", run_action)
|
self.toolbar.components.add_safe("run", run_action)
|
||||||
|
|
||||||
|
submit_action = MaterialIconAction(
|
||||||
|
icon_name="animated_images",
|
||||||
|
tooltip="Run current file as a BEC procedure",
|
||||||
|
label_text="Run on server",
|
||||||
|
filled=True,
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
submit_action.action.triggered.connect(self.on_submit_procedure)
|
||||||
|
self.toolbar.components.add_safe("run_proc", submit_action)
|
||||||
|
|
||||||
stop_action = MaterialIconAction(
|
stop_action = MaterialIconAction(
|
||||||
icon_name="stop",
|
icon_name="stop",
|
||||||
tooltip="Stop current execution",
|
tooltip="Stop current execution",
|
||||||
@@ -246,6 +253,7 @@ class DeveloperWidget(DockAreaWidget):
|
|||||||
execution_bundle = ToolbarBundle("execution", self.toolbar.components)
|
execution_bundle = ToolbarBundle("execution", self.toolbar.components)
|
||||||
execution_bundle.add_action("run")
|
execution_bundle.add_action("run")
|
||||||
execution_bundle.add_action("stop")
|
execution_bundle.add_action("stop")
|
||||||
|
execution_bundle.add_action("run_proc")
|
||||||
self.toolbar.add_bundle(execution_bundle)
|
self.toolbar.add_bundle(execution_bundle)
|
||||||
|
|
||||||
vim_action = MaterialIconAction(
|
vim_action = MaterialIconAction(
|
||||||
@@ -305,24 +313,41 @@ class DeveloperWidget(DockAreaWidget):
|
|||||||
self.toolbar.components.get_action("save").action.setEnabled(enabled)
|
self.toolbar.components.get_action("save").action.setEnabled(enabled)
|
||||||
self.toolbar.components.get_action("save_as").action.setEnabled(enabled)
|
self.toolbar.components.get_action("save_as").action.setEnabled(enabled)
|
||||||
|
|
||||||
@SafeSlot()
|
def _try_upload(self) -> str | None:
|
||||||
def on_execute(self):
|
|
||||||
"""Upload and run the currently focused script in the Monaco editor."""
|
|
||||||
self.script_editor_tab = self.monaco.last_focused_editor
|
self.script_editor_tab = self.monaco.last_focused_editor
|
||||||
if not self.script_editor_tab:
|
if not self.script_editor_tab:
|
||||||
return
|
return None
|
||||||
widget = self.script_editor_tab.widget()
|
if not isinstance(widget := self.script_editor_tab.widget(), MonacoWidget):
|
||||||
if not isinstance(widget, MonacoWidget):
|
return None
|
||||||
return
|
|
||||||
if widget.modified:
|
if widget.modified:
|
||||||
# Save the file before execution if there are unsaved changes
|
# Save the file before execution if there are unsaved changes
|
||||||
self.monaco.save_file()
|
self.monaco.save_file()
|
||||||
if widget.modified:
|
if widget.modified:
|
||||||
# If still modified, user likely cancelled save dialog
|
# If still modified, user likely cancelled save dialog
|
||||||
return
|
return None
|
||||||
self.current_script_id = upload_script(self.client.connector, widget.get_text())
|
return upload_script(self.client.connector, widget.get_text())
|
||||||
self.console.write(f'bec._run_script("{self.current_script_id}")')
|
|
||||||
|
@SafeSlot()
|
||||||
|
def on_execute(self):
|
||||||
|
"""Upload and run the currently focused script in the Monaco editor."""
|
||||||
print(f"Uploaded script with ID: {self.current_script_id}")
|
print(f"Uploaded script with ID: {self.current_script_id}")
|
||||||
|
if (script_id := self._try_upload()) is not None:
|
||||||
|
self.current_script_id = script_id
|
||||||
|
self.console.write(f'bec._run_script("{self.current_script_id}")')
|
||||||
|
print(f"Uploaded script with ID: {self.current_script_id}")
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def on_submit_procedure(self):
|
||||||
|
"""Upload and run the currently focused script in the Monaco editor as a procedure."""
|
||||||
|
if (script_id := self._try_upload()) is not None:
|
||||||
|
self.current_script_id = script_id
|
||||||
|
print(f"Uploaded script with ID: {self.current_script_id}")
|
||||||
|
self.client.connector.xadd(
|
||||||
|
MessageEndpoints.procedure_request(),
|
||||||
|
ProcedureRequestMessage(
|
||||||
|
identifier="run_script", args_kwargs=((self.current_script_id,), {})
|
||||||
|
).model_dump(),
|
||||||
|
)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
|
|||||||
@@ -4787,6 +4787,188 @@ class PositionerGroup(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ProcedurePanel(RPCBase):
|
||||||
|
@rpc_call
|
||||||
|
def new(
|
||||||
|
self,
|
||||||
|
widget: "QWidget | str",
|
||||||
|
*,
|
||||||
|
closable: "bool" = True,
|
||||||
|
floatable: "bool" = True,
|
||||||
|
movable: "bool" = True,
|
||||||
|
start_floating: "bool" = False,
|
||||||
|
floating_state: "Mapping[str, object] | None" = None,
|
||||||
|
where: "Literal['left', 'right', 'top', 'bottom'] | None" = None,
|
||||||
|
on_close: "Callable[[CDockWidget, QWidget], None] | None" = None,
|
||||||
|
tab_with: "CDockWidget | QWidget | str | None" = None,
|
||||||
|
relative_to: "CDockWidget | QWidget | str | None" = None,
|
||||||
|
return_dock: "bool" = False,
|
||||||
|
show_title_bar: "bool | None" = None,
|
||||||
|
title_buttons: "Mapping[str, bool] | Sequence[str] | str | None" = None,
|
||||||
|
show_settings_action: "bool | None" = False,
|
||||||
|
promote_central: "bool" = False,
|
||||||
|
dock_icon: "QIcon | None" = None,
|
||||||
|
apply_widget_icon: "bool" = True,
|
||||||
|
object_name: "str | None" = None,
|
||||||
|
**widget_kwargs,
|
||||||
|
) -> "QWidget | CDockWidget | BECWidget":
|
||||||
|
"""
|
||||||
|
Create a new widget (or reuse an instance) and add it as a dock.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget(QWidget | str): Instance or registered widget type string.
|
||||||
|
closable(bool): Whether the dock is closable.
|
||||||
|
floatable(bool): Whether the dock is floatable.
|
||||||
|
movable(bool): Whether the dock is movable.
|
||||||
|
start_floating(bool): Whether to start the dock floating.
|
||||||
|
floating_state(Mapping | None): Optional floating geometry metadata to apply when floating.
|
||||||
|
where(Literal["left", "right", "top", "bottom"] | None): Dock placement hint relative to the dock area (ignored when
|
||||||
|
``relative_to`` is provided without an explicit value).
|
||||||
|
on_close(Callable[[CDockWidget, QWidget], None] | None): Optional custom close handler accepting (dock, widget).
|
||||||
|
tab_with(CDockWidget | QWidget | str | None): Existing dock (or widget/name) to tab the new dock alongside.
|
||||||
|
relative_to(CDockWidget | QWidget | str | None): Existing dock (or widget/name) used as the positional anchor.
|
||||||
|
When supplied and ``where`` is ``None``, the new dock inherits the
|
||||||
|
anchor's current dock area.
|
||||||
|
return_dock(bool): When True, return the created dock instead of the widget.
|
||||||
|
show_title_bar(bool | None): Explicitly show or hide the dock area's title bar.
|
||||||
|
title_buttons(Mapping[str, bool] | Sequence[str] | str | None): Mapping or iterable describing which title bar buttons should
|
||||||
|
remain visible. Provide a mapping of button names (``"float"``,
|
||||||
|
``"close"``, ``"menu"``, ``"auto_hide"``, ``"minimize"``) to booleans,
|
||||||
|
or a sequence of button names to hide.
|
||||||
|
show_settings_action(bool | None): Control whether a dock settings/property action should
|
||||||
|
be installed. Defaults to ``False`` for the basic dock area; subclasses
|
||||||
|
such as `AdvancedDockArea` override the default to ``True``.
|
||||||
|
promote_central(bool): When True, promote the created dock to be the dock manager's
|
||||||
|
central widget (useful for editor stacks or other root content).
|
||||||
|
dock_icon(QIcon | None): Optional icon applied to the dock via ``CDockWidget.setIcon``.
|
||||||
|
Provide a `QIcon` (e.g. from ``material_icon``). When ``None`` (default),
|
||||||
|
the widget's ``ICON_NAME`` attribute is used when available.
|
||||||
|
apply_widget_icon(bool): When False, skip automatically resolving the icon from
|
||||||
|
the widget's ``ICON_NAME`` (useful for callers who want no icon and do not pass one explicitly).
|
||||||
|
object_name(str | None): Optional object name to assign to the created widget.
|
||||||
|
**widget_kwargs: Additional keyword arguments passed to the widget constructor
|
||||||
|
when creating by type name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The widget instance by default, or the created `CDockWidget` when `return_dock` is True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def dock_map(self) -> "dict[str, CDockWidget]":
|
||||||
|
"""
|
||||||
|
Return the dock widgets map as dictionary with names as keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def dock_list(self) -> "list[CDockWidget]":
|
||||||
|
"""
|
||||||
|
Return the list of dock widgets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def widget_map(self, bec_widgets_only: "bool" = True) -> "dict[str, QWidget]":
|
||||||
|
"""
|
||||||
|
Return a dictionary mapping widget names to their corresponding widgets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bec_widgets_only(bool): If True, only include widgets that are BECConnector instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def widget_list(self, bec_widgets_only: "bool" = True) -> "list[QWidget]":
|
||||||
|
"""
|
||||||
|
Return a list of widgets contained in the dock area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bec_widgets_only(bool): If True, only include widgets that are BECConnector instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def attach_all(self):
|
||||||
|
"""
|
||||||
|
Re-attach floating docks back into the dock manager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def delete_all(self):
|
||||||
|
"""
|
||||||
|
Delete all docks and their associated widgets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def delete(self, object_name: "str") -> "bool":
|
||||||
|
"""
|
||||||
|
Remove a widget from the dock area by its object name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
object_name: The object name of the widget to remove.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the widget was found and removed, False otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no widget with the given object name is found.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> dock_area.delete("my_widget")
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_layout_ratios(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
horizontal: "Sequence[float] | Mapping[int | str, float] | None" = None,
|
||||||
|
vertical: "Sequence[float] | Mapping[int | str, float] | None" = None,
|
||||||
|
splitter_overrides: "Mapping[int | str | Sequence[int], Sequence[float] | Mapping[int | str, float]] | None" = None,
|
||||||
|
) -> "None":
|
||||||
|
"""
|
||||||
|
Adjust splitter ratios in the dock layout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
horizontal: Weights applied to every horizontal splitter encountered.
|
||||||
|
vertical: Weights applied to every vertical splitter encountered.
|
||||||
|
splitter_overrides: Optional overrides targeting specific splitters identified
|
||||||
|
by their index path (e.g. ``{0: [1, 2], (1, 0): [3, 5]}``). Paths are zero-based
|
||||||
|
indices following the splitter hierarchy, starting from the root splitter.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
To build three columns with custom per-column ratios::
|
||||||
|
|
||||||
|
area.set_layout_ratios(
|
||||||
|
horizontal=[1, 2, 1], # column widths
|
||||||
|
splitter_overrides={
|
||||||
|
0: [1, 2], # column 0 (two rows)
|
||||||
|
1: [3, 2, 1], # column 1 (three rows)
|
||||||
|
2: [1], # column 2 (single row)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def describe_layout(self) -> "list[dict[str, Any]]":
|
||||||
|
"""
|
||||||
|
Return metadata describing splitter paths, orientations, and contained docks.
|
||||||
|
|
||||||
|
Useful for determining the keys to use in `set_layout_ratios(splitter_overrides=...)`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def print_layout_structure(self) -> "None":
|
||||||
|
"""
|
||||||
|
Pretty-print the current splitter paths to stdout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_central_dock(self, dock: "CDockWidget | QWidget | str") -> "None":
|
||||||
|
"""
|
||||||
|
Promote an existing dock to be the dock manager's central widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dock(CDockWidget | QWidget | str): Dock reference to promote.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RectangularROI(RPCBase):
|
class RectangularROI(RPCBase):
|
||||||
"""Defines a rectangular Region of Interest (ROI) with additional functionality."""
|
"""Defines a rectangular Region of Interest (ROI) with additional functionality."""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
import operator
|
||||||
|
from functools import partial, reduce
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.messages import (
|
||||||
|
ProcedureExecutionMessage,
|
||||||
|
ProcedureQNotifMessage,
|
||||||
|
ProcedureRequestMessage,
|
||||||
|
)
|
||||||
|
from bec_lib.procedures.helper import FrontendProcedureHelper
|
||||||
|
from bec_qthemes._icon.material_icons import material_icon
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from qtpy.QtCore import QSize, Qt, Signal
|
||||||
|
from qtpy.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QPushButton,
|
||||||
|
QToolButton,
|
||||||
|
QTreeWidget,
|
||||||
|
QTreeWidgetItem,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
_icon = partial(material_icon, size=(20, 20), convert_to_pixmap=False, filled=False)
|
||||||
|
|
||||||
|
_ActionTypes = Literal["abort", "delete", "resubmit"]
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseConfig(BaseModel):
|
||||||
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
actions: set[_ActionTypes]
|
||||||
|
child_actions: set[_ActionTypes]
|
||||||
|
actions_column: int = 3
|
||||||
|
params_column: int = 2
|
||||||
|
helper: FrontendProcedureHelper
|
||||||
|
tree: QTreeWidget
|
||||||
|
active_queue: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class _QueueConfig(BaseModel):
|
||||||
|
queue: str
|
||||||
|
base: _BaseConfig
|
||||||
|
msgs: list[ProcedureExecutionMessage]
|
||||||
|
|
||||||
|
|
||||||
|
class _ItemConfig(BaseModel):
|
||||||
|
base: _BaseConfig
|
||||||
|
msg: ProcedureExecutionMessage
|
||||||
|
|
||||||
|
|
||||||
|
class _ActionItem(QTreeWidgetItem):
|
||||||
|
ABORT_BUTTON_COLOR = DELETE_BUTTON_COLOR = "#CC181E"
|
||||||
|
RESUBMIT_BUTTON_COLOR = "#2266BB"
|
||||||
|
ACTION_TYPE: Literal["parent", "child"] = "child"
|
||||||
|
|
||||||
|
def __init__(self, parent, strings: list[str], config: _BaseConfig):
|
||||||
|
super().__init__(parent, strings)
|
||||||
|
self._tree = config.tree
|
||||||
|
self._config = config
|
||||||
|
self._init_actions()
|
||||||
|
|
||||||
|
def _init_actions(self):
|
||||||
|
"""Create the actions widget in the given column."""
|
||||||
|
self.actions_widget = QWidget()
|
||||||
|
actions_layout = QHBoxLayout(self.actions_widget)
|
||||||
|
actions_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
actions_layout.setSpacing(0)
|
||||||
|
|
||||||
|
def button(icon, color, slot, tooltip):
|
||||||
|
button = QToolButton(self.actions_widget)
|
||||||
|
setattr(self, icon, button)
|
||||||
|
icon = _icon(icon, color=color)
|
||||||
|
button.setIcon(icon)
|
||||||
|
button.clicked.connect(slot)
|
||||||
|
actions_layout.addWidget(button)
|
||||||
|
button.setToolTip(tooltip)
|
||||||
|
|
||||||
|
actions = (
|
||||||
|
self._config.actions if self.ACTION_TYPE == "parent" else self._config.child_actions
|
||||||
|
)
|
||||||
|
if "abort" in actions:
|
||||||
|
button("cancel_presentation", self.ABORT_BUTTON_COLOR, self._abort_self, "abort")
|
||||||
|
if "delete" in actions:
|
||||||
|
button("delete", self.DELETE_BUTTON_COLOR, self._delete_self, "delete")
|
||||||
|
if "resubmit" in actions:
|
||||||
|
button("autorenew", self.RESUBMIT_BUTTON_COLOR, self._resubmit_self, "resubmit")
|
||||||
|
|
||||||
|
self._tree.setItemWidget(self, self._config.actions_column, self.actions_widget)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def _abort_self(self): ...
|
||||||
|
@SafeSlot()
|
||||||
|
def _delete_self(self): ...
|
||||||
|
@SafeSlot()
|
||||||
|
def _resubmit_self(self): ...
|
||||||
|
|
||||||
|
|
||||||
|
class JobItem(_ActionItem):
|
||||||
|
def __init__(self, parent, strings: list[str], config: _ItemConfig):
|
||||||
|
super().__init__(parent, strings, config.base)
|
||||||
|
self._msg = config.msg
|
||||||
|
self._init_params_display()
|
||||||
|
|
||||||
|
def queue(self):
|
||||||
|
return self._msg.queue
|
||||||
|
|
||||||
|
def _init_params_display(self):
|
||||||
|
self.setText(self._config.params_column, self._short_params_text())
|
||||||
|
self.setToolTip(self._config.params_column, self._long_params_html())
|
||||||
|
|
||||||
|
def _short_params_text(self):
|
||||||
|
a, k = self._msg.args_kwargs
|
||||||
|
args = f"{a}, " if a else ""
|
||||||
|
kwargs = f"{k}".strip("{}") if k else ""
|
||||||
|
return args + kwargs
|
||||||
|
|
||||||
|
def _long_params_html(self):
|
||||||
|
a, k = self._msg.args_kwargs
|
||||||
|
args = "<b>Positional arguments:</b><br>" + ", ".join(str(arg) for arg in a) if a else ""
|
||||||
|
kwargs = (
|
||||||
|
reduce(
|
||||||
|
operator.add,
|
||||||
|
(f" {k}: {v}<br>" for k, v in k.items()),
|
||||||
|
"<b>Keyword arguments:</b><br>",
|
||||||
|
)
|
||||||
|
if k
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
return args + kwargs
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def _abort_self(self):
|
||||||
|
self._config.helper.request.abort_execution(self._msg.execution_id)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def _delete_self(self):
|
||||||
|
self._config.helper.request.clear_unhandled_execution(self._msg.execution_id)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def _resubmit_self(self):
|
||||||
|
self._config.helper.request.clear_unhandled_execution(self._msg.execution_id)
|
||||||
|
self._config.helper.request.procedure(
|
||||||
|
identifier=self._msg.identifier,
|
||||||
|
queue=self._msg.queue,
|
||||||
|
args_kwargs=self._msg.args_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QueueItem(_ActionItem):
|
||||||
|
ACTION_TYPE = "parent"
|
||||||
|
|
||||||
|
def __init__(self, parent, strings: list[str], config: _QueueConfig):
|
||||||
|
super().__init__(parent, strings, config.base)
|
||||||
|
self._queue = config.queue
|
||||||
|
self.update(config.msgs)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
for i in reversed(range(self.childCount())):
|
||||||
|
self.removeChild(self.child(i))
|
||||||
|
|
||||||
|
def update(self, msgs: list[ProcedureExecutionMessage]):
|
||||||
|
if self._config.active_queue:
|
||||||
|
active = self._config.helper.get.running_procedures()
|
||||||
|
for msg in active:
|
||||||
|
if msg.queue == self._queue:
|
||||||
|
JobItem(
|
||||||
|
self, [msg.identifier, "RUNNING"], _ItemConfig(base=self._config, msg=msg)
|
||||||
|
)
|
||||||
|
for msg in msgs:
|
||||||
|
JobItem(
|
||||||
|
self,
|
||||||
|
[msg.identifier, "PENDING" if self._config.active_queue else "ABORTED"],
|
||||||
|
_ItemConfig(base=self._config, msg=msg),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queue(self):
|
||||||
|
return self._queue
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def _abort_self(self):
|
||||||
|
self._config.helper.request.abort_queue(self._queue)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def _delete_self(self):
|
||||||
|
self._config.helper.request.clear_unhandled_queue(self._queue)
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryItem(QTreeWidgetItem):
|
||||||
|
def __init__(self, parent, strings: list[str], config: _BaseConfig):
|
||||||
|
super().__init__(parent, strings)
|
||||||
|
self._queues: dict[str, QueueItem] = {}
|
||||||
|
self._tree: QTreeWidget = parent
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
def update(self, queue: str, msgs: list[ProcedureExecutionMessage]):
|
||||||
|
if (queue_item := self._queues.get(queue)) is not None:
|
||||||
|
queue_item.clear()
|
||||||
|
queue_item.update(msgs)
|
||||||
|
if queue_item.childCount() == 0:
|
||||||
|
self.removeChild(queue_item)
|
||||||
|
del self._queues[queue]
|
||||||
|
elif msgs:
|
||||||
|
self._queues[queue] = QueueItem(
|
||||||
|
self, [queue], _QueueConfig(base=self._config, queue=queue, msgs=msgs)
|
||||||
|
)
|
||||||
|
self._queues[queue].setExpanded(True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProcedureControl(BECWidget, QWidget):
|
||||||
|
|
||||||
|
RPC = False
|
||||||
|
|
||||||
|
queue_selected = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, client=None, config=None, gui_id: str | None = None, **kwargs):
|
||||||
|
config = config or ConnectionConfig()
|
||||||
|
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
|
||||||
|
self._conn = self.bec_dispatcher.client.connector
|
||||||
|
self._helper = FrontendProcedureHelper(self._conn)
|
||||||
|
self._setup_ui()
|
||||||
|
self.bec_dispatcher.connect_slot(self._update, MessageEndpoints.procedure_queue_notif())
|
||||||
|
self._init_queues()
|
||||||
|
self._content.itemSelectionChanged.connect(self.on_selection_changed)
|
||||||
|
|
||||||
|
def on_selection_changed(self):
|
||||||
|
selected_items = self._content.selectedItems()
|
||||||
|
if len(selected_items) != 1:
|
||||||
|
self.queue_selected.emit("")
|
||||||
|
return
|
||||||
|
if isinstance((item := selected_items[0]), (QueueItem, JobItem)):
|
||||||
|
self.queue_selected.emit(item.queue())
|
||||||
|
return
|
||||||
|
self.queue_selected.emit("")
|
||||||
|
|
||||||
|
@SafeSlot(ProcedureQNotifMessage, dict)
|
||||||
|
def _update(self, msg: dict | ProcedureQNotifMessage, _):
|
||||||
|
msg = ProcedureQNotifMessage.model_validate(msg)
|
||||||
|
if msg.queue_type == "execution":
|
||||||
|
cat_to_update = self._active_queues
|
||||||
|
read_queue = self._helper.get.exec_queue
|
||||||
|
else:
|
||||||
|
cat_to_update = self._unhandled_queues
|
||||||
|
read_queue = self._helper.get.unhandled_queue
|
||||||
|
cat_to_update.update(msg.queue_name, read_queue(msg.queue_name))
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
self.setLayout(self._layout)
|
||||||
|
|
||||||
|
self._content = QTreeWidget()
|
||||||
|
self._content.setAlternatingRowColors(True)
|
||||||
|
self._content.setHeaderLabels(["name", "status", "params", "actions"])
|
||||||
|
self._layout.addWidget(self._content)
|
||||||
|
|
||||||
|
config = partial(_BaseConfig, helper=self._helper, tree=self._content, actions_column=3)
|
||||||
|
|
||||||
|
self._active_queues = CategoryItem(
|
||||||
|
self._content,
|
||||||
|
["active queues"],
|
||||||
|
config(actions={"abort"}, child_actions={"abort"}, active_queue=True),
|
||||||
|
)
|
||||||
|
self._content.addTopLevelItem(self._active_queues)
|
||||||
|
self._active_queues.setExpanded(True)
|
||||||
|
|
||||||
|
self._unhandled_queues = CategoryItem(
|
||||||
|
self._content,
|
||||||
|
["unhandled queues"],
|
||||||
|
config(actions={"delete"}, child_actions={"delete", "resubmit"}),
|
||||||
|
)
|
||||||
|
self._content.addTopLevelItem(self._unhandled_queues)
|
||||||
|
self._active_queues.setExpanded(True)
|
||||||
|
|
||||||
|
def _init_queues(self):
|
||||||
|
for queue in self._helper.get.active_and_pending_queue_names():
|
||||||
|
self._active_queues.update(queue, self._helper.get.exec_queue(queue))
|
||||||
|
for queue in self._helper.get.queue_names("unhandled"):
|
||||||
|
self._unhandled_queues.update(queue, self._helper.get.unhandled_queue(queue))
|
||||||
|
|
||||||
|
|
||||||
|
class ProcedureSubmissionOptionsDialog(QDialog):
|
||||||
|
"""
|
||||||
|
Dialog to customize procedure options
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None, client=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Procedure execution options")
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def sizeHint(self) -> QSize:
|
||||||
|
return QSize(600, 800)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
"""Setup the dialog UI with ScanControl widget and buttons."""
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Create the scan control widget
|
||||||
|
|
||||||
|
# Create dialog buttons
|
||||||
|
button_box = QDialogButtonBox(Qt.Orientation.Horizontal, self)
|
||||||
|
|
||||||
|
# Create custom buttons with appropriate text
|
||||||
|
insert_button = QPushButton("Insert")
|
||||||
|
cancel_button = QPushButton("Cancel")
|
||||||
|
|
||||||
|
button_box.addButton(insert_button, QDialogButtonBox.ButtonRole.AcceptRole)
|
||||||
|
button_box.addButton(cancel_button, QDialogButtonBox.ButtonRole.RejectRole)
|
||||||
|
|
||||||
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
# Connect button signals
|
||||||
|
button_box.accepted.connect(self.accept)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Override accept to generate code before closing."""
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
widget = ProcedureControl()
|
||||||
|
widget.setFixedWidth(800)
|
||||||
|
widget.setFixedHeight(800)
|
||||||
|
widget.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.procedures.helper import FrontendProcedureHelper
|
||||||
|
from bec_qthemes import material_icon
|
||||||
|
from qtpy.QtGui import QFont
|
||||||
|
from qtpy.QtWidgets import (
|
||||||
|
QComboBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QSizePolicy,
|
||||||
|
QTextEdit,
|
||||||
|
QToolButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class ProcedureLogs(BECWidget, QWidget):
|
||||||
|
|
||||||
|
RPC = False
|
||||||
|
|
||||||
|
def __init__(self, parent=None, client=None, config=None, gui_id: str | None = None, **kwargs):
|
||||||
|
config = config or ConnectionConfig()
|
||||||
|
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
|
||||||
|
self._conn = self.bec_dispatcher.client.connector
|
||||||
|
self._queue: str | None = None
|
||||||
|
self._helper = FrontendProcedureHelper(self._conn)
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def _update_selection_box(self):
|
||||||
|
self._selection_box.clear()
|
||||||
|
self._available_streams = self._helper.get.log_queue_names()
|
||||||
|
self._selection_box.addItems(self._available_streams)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
self.setLayout(self._layout)
|
||||||
|
self._setup_tools()
|
||||||
|
self._setup_display()
|
||||||
|
|
||||||
|
def _setup_tools(self):
|
||||||
|
self.tools = QWidget(self)
|
||||||
|
self._tools_layout = QHBoxLayout()
|
||||||
|
self._tools_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.tools.setLayout(self._tools_layout)
|
||||||
|
self._selection_box = QComboBox()
|
||||||
|
self._update_selection_box()
|
||||||
|
self._selection_box.setCurrentIndex(-1)
|
||||||
|
self._selection_box.currentTextChanged.connect(self.set_queue)
|
||||||
|
self._refresh_button = QToolButton()
|
||||||
|
self._refresh_button.setIcon(material_icon("refresh", convert_to_pixmap=False))
|
||||||
|
self._tools_layout.addWidget(QLabel("Select logs stream: "))
|
||||||
|
self._tools_layout.addWidget(self._selection_box)
|
||||||
|
self._tools_layout.addWidget(self._refresh_button)
|
||||||
|
self._refresh_button.clicked.connect(self._update_selection_box)
|
||||||
|
self._layout.addWidget(self.tools)
|
||||||
|
|
||||||
|
def _setup_display(self):
|
||||||
|
self.widget = QTextEdit(lineWrapMode=QTextEdit.LineWrapMode.NoWrap, readOnly=True)
|
||||||
|
font = QFont("Courier New")
|
||||||
|
font.setStyleHint(QFont.StyleHint.Monospace)
|
||||||
|
self.widget.setFont(font)
|
||||||
|
self.widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
|
self._layout.addWidget(self.widget)
|
||||||
|
|
||||||
|
@SafeSlot(dict, dict)
|
||||||
|
def _update(self, msg, _):
|
||||||
|
self.widget.append(msg.get("data").strip())
|
||||||
|
|
||||||
|
def _init_content(self):
|
||||||
|
self.widget.setText("")
|
||||||
|
if self._queue is None:
|
||||||
|
return
|
||||||
|
if msgs := self._conn.xread(MessageEndpoints.procedure_logs(self._queue), from_start=True):
|
||||||
|
self.widget.append("\n".join(msg.get("data").data.strip() for msg in msgs))
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def clear_selection_box(self, *_, **__):
|
||||||
|
self._selection_box.setCurrentIndex(-1)
|
||||||
|
|
||||||
|
@SafeSlot(None)
|
||||||
|
@SafeSlot(str)
|
||||||
|
def set_queue(self, queue: str | None):
|
||||||
|
if queue == "":
|
||||||
|
return
|
||||||
|
self.queue = queue
|
||||||
|
self._selection_box.setCurrentIndex(-1)
|
||||||
|
|
||||||
|
@SafeProperty(str)
|
||||||
|
def queue(self) -> str | None:
|
||||||
|
return self._queue
|
||||||
|
|
||||||
|
@queue.setter
|
||||||
|
def queue(self, queue: str | None) -> None:
|
||||||
|
if self._queue == queue:
|
||||||
|
return
|
||||||
|
if self._queue is not None:
|
||||||
|
self.bec_dispatcher.disconnect_slot(
|
||||||
|
self._update, MessageEndpoints.procedure_logs(self._queue)
|
||||||
|
)
|
||||||
|
self._queue = queue or None
|
||||||
|
if self._queue is not None:
|
||||||
|
self.bec_dispatcher.connect_slot(
|
||||||
|
self._update, MessageEndpoints.procedure_logs(self._queue)
|
||||||
|
)
|
||||||
|
self._init_content()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
widget = ProcedureLogs()
|
||||||
|
widget.queue = "primary"
|
||||||
|
widget.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
|
||||||
|
from bec_widgets.widgets.control.procedure_control.procedure_control import ProcedureControl
|
||||||
|
from bec_widgets.widgets.control.procedure_control.procedure_logs import ProcedureLogs
|
||||||
|
|
||||||
|
|
||||||
|
class ProcedurePanel(DockAreaWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent=None, **kwargs):
|
||||||
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
self.procedure_control = ProcedureControl(parent=self)
|
||||||
|
self.procedure_control.setObjectName("Procedure Queue Control")
|
||||||
|
self.procedure_logs = ProcedureLogs(parent=self)
|
||||||
|
self.procedure_logs.setObjectName("Procedure Logs")
|
||||||
|
|
||||||
|
_dock_kwargs = {
|
||||||
|
"closable": False,
|
||||||
|
"movable": False,
|
||||||
|
"floatable": False,
|
||||||
|
"title_buttons": {"float": False, "close": False, "menu": False},
|
||||||
|
}
|
||||||
|
self.new(self.procedure_control, **_dock_kwargs)
|
||||||
|
self.new(self.procedure_logs, where="bottom", **_dock_kwargs)
|
||||||
|
|
||||||
|
self.procedure_control.queue_selected.connect(self.procedure_logs.set_queue)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
widget = ProcedurePanel()
|
||||||
|
widget.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
@@ -6,10 +6,10 @@ import fakeredis
|
|||||||
import pytest
|
import pytest
|
||||||
from bec_lib.bec_service import messages
|
from bec_lib.bec_service import messages
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_lib.redis_connector import RedisConnector
|
|
||||||
from bec_lib.scan_history import ScanHistory
|
from bec_lib.scan_history import ScanHistory
|
||||||
|
|
||||||
from bec_widgets.tests.utils import DEVICES, DMMock, FakePositioner, Positioner
|
from bec_widgets.tests.utils import DEVICES, DMMock, FakePositioner, Positioner
|
||||||
|
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
||||||
|
|
||||||
|
|
||||||
def fake_redis_server(host, port, **kwargs):
|
def fake_redis_server(host, port, **kwargs):
|
||||||
@@ -19,7 +19,7 @@ def fake_redis_server(host, port, **kwargs):
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def mocked_client(bec_dispatcher):
|
def mocked_client(bec_dispatcher):
|
||||||
connector = RedisConnector("localhost:1", redis_cls=fake_redis_server)
|
connector = QtRedisConnector("localhost:1", redis_cls=fake_redis_server)
|
||||||
# Create a MagicMock object
|
# Create a MagicMock object
|
||||||
client = MagicMock() # TODO change to real BECClient
|
client = MagicMock() # TODO change to real BECClient
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import h5py
|
import h5py
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
from time import sleep
|
||||||
|
from typing import Callable
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from bec_lib.messages import ProcedureExecutionMessage, ProcedureRequestMessage
|
||||||
|
from bec_lib.procedures.helper import BackendProcedureHelper
|
||||||
|
from bec_server.procedures.manager import ProcedureManager
|
||||||
|
from bec_server.procedures.procedure_registry import register
|
||||||
|
from bec_server.procedures.worker_base import ProcedureWorker
|
||||||
|
|
||||||
|
from bec_widgets.widgets.control.procedure_control.procedure_control import (
|
||||||
|
ProcedureControl,
|
||||||
|
QueueItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .client_mocks import mocked_client
|
||||||
|
|
||||||
|
|
||||||
|
class MockWorker(ProcedureWorker):
|
||||||
|
def _kill_process(self): ...
|
||||||
|
|
||||||
|
def _run_task(self, item):
|
||||||
|
sleep(0.1)
|
||||||
|
|
||||||
|
def _setup_execution_environment(self): ...
|
||||||
|
|
||||||
|
def abort(self): ...
|
||||||
|
|
||||||
|
def abort_execution(self, execution_id: str): ...
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def register_test_proc():
|
||||||
|
register("test", lambda: None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def proc_ctrl_w_helper(qtbot, mocked_client: MagicMock):
|
||||||
|
proc_ctrl = ProcedureControl(client=mocked_client)
|
||||||
|
qtbot.addWidget(proc_ctrl)
|
||||||
|
with patch(
|
||||||
|
"bec_server.procedures.manager.RedisConnector", lambda _: proc_ctrl.client.connector
|
||||||
|
):
|
||||||
|
manager = ProcedureManager(MagicMock(), MockWorker)
|
||||||
|
yield proc_ctrl, BackendProcedureHelper(proc_ctrl.client.connector)
|
||||||
|
manager.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def req_msg():
|
||||||
|
return ProcedureRequestMessage(identifier="test")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def exec_msg():
|
||||||
|
return lambda id: ProcedureExecutionMessage(identifier="test", queue="test", execution_id=id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_proc(qtbot, proc_ctrl_w_helper: tuple[ProcedureControl, BackendProcedureHelper]):
|
||||||
|
proc_ctrl, helper = proc_ctrl_w_helper
|
||||||
|
assert proc_ctrl._active_queues.childCount() == 0
|
||||||
|
helper.request.procedure("test")
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._active_queues.childCount() != 0, timeout=500)
|
||||||
|
|
||||||
|
|
||||||
|
def test_abort(qtbot, proc_ctrl_w_helper: tuple[ProcedureControl, BackendProcedureHelper]):
|
||||||
|
proc_ctrl, helper = proc_ctrl_w_helper
|
||||||
|
assert proc_ctrl._active_queues.childCount() == 0
|
||||||
|
helper.request.procedure("test")
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._active_queues.childCount() != 0, timeout=500)
|
||||||
|
|
||||||
|
assert proc_ctrl._unhandled_queues.childCount() == 0
|
||||||
|
queue: QueueItem = proc_ctrl._active_queues.child(0)
|
||||||
|
queue.child(0).actions_widget.layout().itemAt(0).widget().click()
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._active_queues.childCount() == 0, timeout=500)
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._unhandled_queues.childCount() != 0, timeout=500)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete(
|
||||||
|
qtbot,
|
||||||
|
proc_ctrl_w_helper: tuple[ProcedureControl, BackendProcedureHelper],
|
||||||
|
exec_msg: Callable[[str], ProcedureExecutionMessage],
|
||||||
|
):
|
||||||
|
proc_ctrl, helper = proc_ctrl_w_helper
|
||||||
|
[helper.push.unhandled("test", exec_msg("abcd")) for _ in range(3)]
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._unhandled_queues.childCount() != 0, timeout=500)
|
||||||
|
queue: QueueItem = proc_ctrl._unhandled_queues.child(0)
|
||||||
|
assert queue.childCount() == 3
|
||||||
|
queue.actions_widget.layout().itemAt(0).widget().click()
|
||||||
|
qtbot.waitUntil(lambda: helper.get.unhandled_queue("test") == [], timeout=500)
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._unhandled_queues.childCount() == 0, timeout=500)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resubmit(
|
||||||
|
qtbot,
|
||||||
|
proc_ctrl_w_helper: tuple[ProcedureControl, BackendProcedureHelper],
|
||||||
|
exec_msg: Callable[[str], ProcedureExecutionMessage],
|
||||||
|
):
|
||||||
|
proc_ctrl, helper = proc_ctrl_w_helper
|
||||||
|
[helper.push.unhandled("test", exec_msg(f"abcd{i}")) for i in range(3)]
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._unhandled_queues.childCount() != 0, timeout=500)
|
||||||
|
queue: QueueItem = proc_ctrl._unhandled_queues.child(0)
|
||||||
|
assert queue.childCount() == 3
|
||||||
|
assert proc_ctrl._active_queues.childCount() == 0
|
||||||
|
queue.child(0).actions_widget.layout().itemAt(1).widget().click()
|
||||||
|
qtbot.waitUntil(lambda: len(helper.get.unhandled_queue("test")) == 2, timeout=500)
|
||||||
|
qtbot.waitUntil(lambda: proc_ctrl._active_queues.childCount() != 0, timeout=500)
|
||||||
Reference in New Issue
Block a user