From 832a438b24bbb6be6f0d5e6e14a51a0d4e8c65b4 Mon Sep 17 00:00:00 2001 From: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com> Date: Thu, 9 Nov 2023 21:46:31 +0100 Subject: [PATCH] test: tests for scan_control --- bec_widgets/widgets/monitor/config_dialog.py | 2 +- bec_widgets/widgets/monitor/monitor.py | 2 +- tests/test_msgs/msg_dict.pkl | Bin 0 -> 25067 bytes tests/test_scan_control.py | 197 +++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 tests/test_msgs/msg_dict.pkl create mode 100644 tests/test_scan_control.py diff --git a/bec_widgets/widgets/monitor/config_dialog.py b/bec_widgets/widgets/monitor/config_dialog.py index 889769e2..9adf414e 100644 --- a/bec_widgets/widgets/monitor/config_dialog.py +++ b/bec_widgets/widgets/monitor/config_dialog.py @@ -503,7 +503,7 @@ class ConfigDialog(QWidget, Ui_Form): self.close() -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover app = QApplication([]) main_app = ConfigDialog() main_app.show() diff --git a/bec_widgets/widgets/monitor/monitor.py b/bec_widgets/widgets/monitor/monitor.py index fd21cd97..6797629d 100644 --- a/bec_widgets/widgets/monitor/monitor.py +++ b/bec_widgets/widgets/monitor/monitor.py @@ -384,7 +384,7 @@ class BECMonitor(pg.GraphicsLayoutWidget): self.update_signal.emit() -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import sys from bec_widgets.bec_dispatcher import bec_dispatcher diff --git a/tests/test_msgs/msg_dict.pkl b/tests/test_msgs/msg_dict.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d7adca853719704bdee2e95c348db6b55470f86d GIT binary patch literal 25067 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 U=b4{J2kws)HQVXn+m(3ge{LESAOHXW literal 0 HcmV?d00001 diff --git a/tests/test_scan_control.py b/tests/test_scan_control.py new file mode 100644 index 00000000..0cb95f46 --- /dev/null +++ b/tests/test_scan_control.py @@ -0,0 +1,197 @@ +import os +import pickle +from unittest.mock import MagicMock + +import msgpack +import pytest +from PyQt5.QtWidgets import QLineEdit + +from bec_widgets.widgets import ScanControl +from bec_widgets.qt_utils.widget_io import WidgetIO + + +# TODO there has to be a better way to mock messages than this, in this case I just took the msg from bec +def load_test_msg(msg_name): + """Helper function to load msg from pickle file.""" + msg_path = os.path.join(os.path.dirname(__file__), "test_msgs", f"{msg_name}.pkl") + with open(msg_path, "rb") as f: + msg = pickle.load(f) + return msg + + +packed_message = load_test_msg("msg_dict")["available_scans"] + + +class FakePositioner: + """Fake minimal positioner class for testing.""" + + def __init__(self, name, enabled=True): + self.name = name + self.enabled = enabled + + def __contains__(self, item): + return item == self.name + + +def get_mocked_device(device_name): + """Helper function to mock the devices""" + if device_name == "samx": + return FakePositioner(name="samx", enabled=True) + + +@pytest.fixture(scope="function") +def mocked_client(): + # Create a MagicMock object + client = MagicMock() + + # Mock the producer.get method to return the packed message + client.producer.get.return_value = packed_message + + # # Mock the device_manager.devices attribute to return a mock object for samx + client.device_manager.devices = MagicMock() + client.device_manager.devices.__contains__.side_effect = lambda x: x == "samx" + client.device_manager.devices.samx = get_mocked_device("samx") + + return client + + +@pytest.fixture(scope="function") +def scan_control(qtbot, mocked_client): # , mock_dev): + widget = ScanControl(client=mocked_client) + # widget.dev.samx = MagicMock() + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + yield widget + + +def test_populate_scans(scan_control, mocked_client): + # The comboBox should be populated with all scan from the message right after initialization + expected_scans = msgpack.loads(packed_message).keys() + assert scan_control.comboBox_scan_selection.count() == len(expected_scans) + for scan in expected_scans: # Each scan should be in the comboBox + assert scan_control.comboBox_scan_selection.findText(scan) != -1 + + +@pytest.mark.parametrize( + "scan_name", ["line_scan", "grid_scan"] +) # TODO now only for line_scan and grid_scan, later for all loaded scans +def test_on_scan_selected(scan_control, scan_name): + # Expected scan info from the message signature + expected_scan_info = msgpack.loads(packed_message)[scan_name] + + # Select a scan from the comboBox + scan_control.comboBox_scan_selection.setCurrentText(scan_name) + + # Check labels and widgets in args table + for index, (arg_key, arg_value) in enumerate(expected_scan_info["arg_input"].items()): + label = scan_control.args_table.horizontalHeaderItem(index) + assert label.text().lower() == arg_key # labes + + for row in range(expected_scan_info["arg_bundle_size"]["min"]): + widget = scan_control.args_table.cellWidget(row, index) + assert widget is not None # Confirm that a widget exists + expected_widget_type = scan_control.WIDGET_HANDLER.get(arg_value, None) + assert isinstance(widget, expected_widget_type) # Confirm the widget type matches + + # kwargs + kwargs_from_signature = [ + param for param in expected_scan_info["signature"] if param["kind"] == "KEYWORD_ONLY" + ] + + # Check labels and widgets in kwargs grid layout + for index, kwarg_info in enumerate(kwargs_from_signature): + label_widget = scan_control.kwargs_layout.itemAtPosition(1, index).widget() + assert label_widget.text() == kwarg_info["name"].capitalize() + widget = scan_control.kwargs_layout.itemAtPosition(2, index).widget() + expected_widget_type = scan_control.WIDGET_HANDLER.get(kwarg_info["annotation"], QLineEdit) + assert isinstance(widget, expected_widget_type) + + +@pytest.mark.parametrize("scan_name", ["line_scan", "grid_scan"]) +def test_add_remove_bundle(scan_control, scan_name): + # Expected scan info from the message signature + expected_scan_info = msgpack.loads(packed_message)[scan_name] + + # Select a scan from the comboBox + scan_control.comboBox_scan_selection.setCurrentText(scan_name) + + # Initial number of args row + initial_num_of_rows = scan_control.args_table.rowCount() + + # Check initial row count of args table + assert scan_control.args_table.rowCount() == expected_scan_info["arg_bundle_size"]["min"] + + # Try to remove default number of args row + scan_control.pushButton_remove_bundle.click() + assert scan_control.args_table.rowCount() == expected_scan_info["arg_bundle_size"]["min"] + + # Try to add two bundles + scan_control.pushButton_add_bundle.click() + scan_control.pushButton_add_bundle.click() + + # check the case where no max number of args are defined + # TODO do check also for the case where max number of args are defined + if expected_scan_info["arg_bundle_size"]["max"] is None: + assert scan_control.args_table.rowCount() == initial_num_of_rows + 2 + + # Remove one bundle + scan_control.pushButton_remove_bundle.click() + + # check the case where no max number of args are defined + if expected_scan_info["arg_bundle_size"]["max"] is None: + assert scan_control.args_table.rowCount() == initial_num_of_rows + 1 + + +def test_run_line_scan_with_parameters(scan_control, mocked_client): + scan_name = "line_scan" + kwargs = {"exp_time": 0.1, "steps": 10, "relative": True, "burst_at_each_point": 1} + args = {"device": "samx", "start": -5, "stop": 5} + + # Select a scan from the comboBox + scan_control.comboBox_scan_selection.setCurrentText(scan_name) + + # Set kwargs in the UI + for label_index in range( + scan_control.kwargs_layout.rowCount() + 1 + ): # from some reason rowCount() returns 1 less than the actual number of rows + label_item = scan_control.kwargs_layout.itemAtPosition(1, label_index) + if label_item: + label_widget = label_item.widget() + kwarg_key = WidgetIO.get_value(label_widget).lower() + if kwarg_key in kwargs: + widget_item = scan_control.kwargs_layout.itemAtPosition(2, label_index) + if widget_item: + widget = widget_item.widget() + WidgetIO.set_value(widget, kwargs[kwarg_key]) + + # Set args in the UI + for col_index in range(scan_control.args_table.columnCount()): + header_item = scan_control.args_table.horizontalHeaderItem(col_index) + if header_item: + arg_key = header_item.text().lower() + if arg_key in args: + for row_index in range(scan_control.args_table.rowCount()): + widget = scan_control.args_table.cellWidget(row_index, col_index) + WidgetIO.set_value(widget, args[arg_key]) + + # Mock the scan function + mocked_scan_function = MagicMock() + setattr(mocked_client.scans, scan_name, mocked_scan_function) + + # Run the scan + scan_control.button_run_scan.click() + + # Retrieve the actual arguments passed to the mock + called_args, called_kwargs = mocked_scan_function.call_args + + # Check if the scan function was called correctly + expected_device = ( + mocked_client.device_manager.devices.samx + ) # This is the FakePositioner instance + expected_args_list = [expected_device, args["start"], args["stop"]] + assert called_args == tuple( + expected_args_list + ), "The positional arguments passed to the scan function do not match expected values." + assert ( + called_kwargs == kwargs + ), "The keyword arguments passed to the scan function do not match expected values."