diff --git a/bec_widgets/cli/client_utils.py b/bec_widgets/cli/client_utils.py index b90edac5..bcf11300 100644 --- a/bec_widgets/cli/client_utils.py +++ b/bec_widgets/cli/client_utils.py @@ -2,6 +2,7 @@ from __future__ import annotations import importlib import importlib.metadata as imd +import json import os import select import subprocess @@ -87,7 +88,7 @@ def _get_output(process, logger) -> None: print(f"Error reading process output: {str(e)}") -def _start_plot_process(gui_id, gui_class, config, logger=None) -> None: +def _start_plot_process(gui_id: str, gui_class: type, config: dict | str, logger=None) -> None: """ Start the plot in a new process. @@ -98,6 +99,8 @@ def _start_plot_process(gui_id, gui_class, config, logger=None) -> None: # pylint: disable=subprocess-run-check command = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__] if config: + if isinstance(config, dict): + config = json.dumps(config) command.extend(["--config", config]) env_dict = os.environ.copy() @@ -190,7 +193,7 @@ class BECGuiClientMixin: if self._process is None or self._process.poll() is not None: self._start_update_script() self._process, self._process_output_processing_thread = _start_plot_process( - self._gui_id, self.__class__, self._client._service_config.config_path + self._gui_id, self.__class__, self._client._service_config.config ) while not self.gui_is_alive(): print("Waiting for GUI to start...") diff --git a/bec_widgets/cli/server.py b/bec_widgets/cli/server.py index 4791b44e..4e3519a2 100644 --- a/bec_widgets/cli/server.py +++ b/bec_widgets/cli/server.py @@ -1,6 +1,7 @@ from __future__ import annotations import inspect +import json import signal import sys from contextlib import redirect_stderr, redirect_stdout @@ -141,10 +142,30 @@ class SimpleFileLikeFromLogOutputFunc: return +def _start_server(gui_id: str, gui_class: Union[BECFigure, BECDockArea], config: str | None = None): + if config: + try: + config = json.loads(config) + service_config = ServiceConfig(config=config) + except (json.JSONDecodeError, TypeError): + service_config = ServiceConfig(config_path=config) + else: + # if no config is provided, use the default config + service_config = ServiceConfig() + + bec_logger.configure( + service_config.redis, + QtRedisConnector, + service_name="BECWidgetsCLIServer", + service_config=service_config.service_config, + ) + server = BECWidgetsCLIServer(gui_id=gui_id, config=service_config, gui_class=gui_class) + return server + + def main(): import argparse import os - import sys from qtpy.QtCore import QSize from qtpy.QtGui import QIcon @@ -159,7 +180,7 @@ def main(): type=str, help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea", ) - parser.add_argument("--config", type=str, help="Config file") + parser.add_argument("--config", type=str, help="Config file or config string.") args = parser.parse_args() @@ -188,14 +209,7 @@ def main(): win = QMainWindow() win.setWindowTitle("BEC Widgets") - service_config = ServiceConfig(args.config) - bec_logger.configure( - service_config.redis, - QtRedisConnector, - service_name="BECWidgetsCLIServer", - service_config=service_config.service_config, - ) - server = BECWidgetsCLIServer(gui_id=args.id, config=service_config, gui_class=gui_class) + server = _start_server(args.id, gui_class, args.config) gui = server.gui win.setCentralWidget(gui) diff --git a/tests/unit_tests/test_client_utils.py b/tests/unit_tests/test_client_utils.py index bc64c4cf..ab21be6a 100644 --- a/tests/unit_tests/test_client_utils.py +++ b/tests/unit_tests/test_client_utils.py @@ -3,6 +3,7 @@ from unittest import mock import pytest from bec_widgets.cli.client import BECFigure +from bec_widgets.cli.client_utils import BECGuiClientMixin, _start_plot_process from .client_mocks import FakeDevice @@ -27,3 +28,49 @@ def test_rpc_call_accepts_device_as_input(cli_figure): fig, mock_rpc_call = cli_figure fig.plot(x_name=dev1, y_name=dev2) mock_rpc_call.assert_called_with("plot", x_name="samx", y_name="bpm4i") + + +@pytest.mark.parametrize( + "config, call_config", + [ + (None, None), + ("/path/to/config.yml", "/path/to/config.yml"), + ({"key": "value"}, '{"key": "value"}'), + ], +) +def test_client_utils_start_plot_process(config, call_config): + with mock.patch("bec_widgets.cli.client_utils.subprocess.Popen") as mock_popen: + _start_plot_process("gui_id", BECFigure, config) + command = ["bec-gui-server", "--id", "gui_id", "--gui_class", "BECFigure"] + if call_config: + command.extend(["--config", call_config]) + mock_popen.assert_called_once_with( + command, + text=True, + start_new_session=True, + stdout=mock.ANY, + stderr=mock.ANY, + env=mock.ANY, + ) + + +def test_client_utils_passes_client_config_to_server(bec_dispatcher): + """ + Test that the client config is passed to the server. This ensures that + changes to the client config (either through config files or plugins) are + reflected in the server. + """ + mixin = BECGuiClientMixin() + mixin._client = bec_dispatcher.client + mixin._gui_id = "gui_id" + mixin.gui_is_alive = mock.MagicMock() + mixin.gui_is_alive.side_effect = [True] + + with mock.patch("bec_widgets.cli.client_utils._start_plot_process") as mock_start_plot: + with mock.patch.object(mixin, "_start_update_script") as mock_start_update: + mock_start_plot.return_value = [mock.MagicMock(), mock.MagicMock()] + mixin.show() + mock_start_plot.assert_called_once_with( + "gui_id", BECGuiClientMixin, mixin._client._service_config.config + ) + mock_start_update.assert_called_once() diff --git a/tests/unit_tests/test_rpc_server.py b/tests/unit_tests/test_rpc_server.py new file mode 100644 index 00000000..66c419f8 --- /dev/null +++ b/tests/unit_tests/test_rpc_server.py @@ -0,0 +1,42 @@ +from unittest import mock + +import pytest +from bec_lib.service_config import ServiceConfig + +from bec_widgets.cli.server import _start_server +from bec_widgets.widgets.figure import BECFigure + + +@pytest.fixture +def mocked_cli_server(): + with mock.patch("bec_widgets.cli.server.BECWidgetsCLIServer") as mock_server: + with mock.patch("bec_widgets.cli.server.ServiceConfig") as mock_config: + with mock.patch("bec_lib.logger.bec_logger.configure") as mock_logger: + yield mock_server, mock_config, mock_logger + + +def test_rpc_server_start_server_without_service_config(mocked_cli_server): + """ + Test that the server is started with the correct arguments. + """ + mock_server, mock_config, _ = mocked_cli_server + + _start_server("gui_id", BECFigure, None) + mock_server.assert_called_once_with(gui_id="gui_id", config=mock_config(), gui_class=BECFigure) + + +@pytest.mark.parametrize( + "config, call_config", + [ + ("/path/to/config.yml", {"config_path": "/path/to/config.yml"}), + ({"key": "value"}, {"config": {"key": "value"}}), + ], +) +def test_rpc_server_start_server_with_service_config(mocked_cli_server, config, call_config): + """ + Test that the server is started with the correct arguments. + """ + mock_server, mock_config, _ = mocked_cli_server + config = mock_config(**call_config) + _start_server("gui_id", BECFigure, config) + mock_server.assert_called_once_with(gui_id="gui_id", config=config, gui_class=BECFigure)