From 56e74a0e7da72d18e89bc30d1896dbf9ef97cd6b Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Thu, 20 Jun 2024 16:21:39 +0200 Subject: [PATCH] test(scan_control): tests added --- tests/unit_tests/test_scan_control.py | 357 +++++++++++++----- .../unit_tests/test_scan_control_group_box.py | 160 ++++++++ 2 files changed, 425 insertions(+), 92 deletions(-) create mode 100644 tests/unit_tests/test_scan_control_group_box.py diff --git a/tests/unit_tests/test_scan_control.py b/tests/unit_tests/test_scan_control.py index 6813b3c4..1a436abb 100644 --- a/tests/unit_tests/test_scan_control.py +++ b/tests/unit_tests/test_scan_control.py @@ -2,18 +2,222 @@ from unittest.mock import MagicMock import pytest -from qtpy.QtWidgets import QLineEdit +from bec_lib.messages import AvailableResourceMessage from bec_widgets.utils.widget_io import WidgetIO -from bec_widgets.widgets import ScanControl -from tests.unit_tests.test_msgs.available_scans_message import available_scans_message +from bec_widgets.widgets.scan_control import ScanControl from .client_mocks import mocked_client +available_scans_message = AvailableResourceMessage( + resource={ + "line_scan": { + "class": "LineScan", + "base_class": "ScanBase", + "arg_input": {"device": "device", "start": "float", "stop": "float"}, + "gui_config": { + "scan_class_name": "LineScan", + "arg_group": { + "name": "Scan Arguments", + "bundle": 3, + "arg_inputs": {"device": "device", "start": "float", "stop": "float"}, + "inputs": [ + { + "arg": True, + "name": "device", + "type": "device", + "display_name": "Device", + "tooltip": None, + "default": None, + "expert": False, + }, + { + "arg": True, + "name": "start", + "type": "float", + "display_name": "Start", + "tooltip": None, + "default": None, + "expert": False, + }, + { + "arg": True, + "name": "stop", + "type": "float", + "display_name": "Stop", + "tooltip": None, + "default": None, + "expert": False, + }, + ], + "min": 1, + "max": None, + }, + "kwarg_groups": [ + { + "name": "Movement Parameters", + "inputs": [ + { + "arg": False, + "name": "steps", + "type": "int", + "display_name": "Steps", + "tooltip": "Number of steps", + "default": None, + "expert": False, + }, + { + "arg": False, + "name": "relative", + "type": "bool", + "display_name": "Relative", + "tooltip": "If True, the start and end positions are relative to the current position", + "default": False, + "expert": False, + }, + ], + }, + { + "name": "Acquisition Parameters", + "inputs": [ + { + "arg": False, + "name": "exp_time", + "type": "float", + "display_name": "Exp Time", + "tooltip": "Exposure time in s", + "default": 0, + "expert": False, + }, + { + "arg": False, + "name": "burst_at_each_point", + "type": "int", + "display_name": "Burst At Each Point", + "tooltip": "Number of acquisition per point", + "default": 1, + "expert": False, + }, + ], + }, + ], + }, + "required_kwargs": ["steps", "relative"], + "arg_bundle_size": {"bundle": 3, "min": 1, "max": None}, + }, + "grid_scan": { + "class": "Scan", + "base_class": "ScanBase", + "arg_input": {"device": "device", "start": "float", "stop": "float", "steps": "int"}, + "gui_config": { + "scan_class_name": "Scan", + "arg_group": { + "name": "Scan Arguments", + "bundle": 4, + "arg_inputs": { + "device": "device", + "start": "float", + "stop": "float", + "steps": "int", + }, + "inputs": [ + { + "arg": True, + "name": "device", + "type": "device", + "display_name": "Device", + "tooltip": None, + "default": None, + "expert": False, + }, + { + "arg": True, + "name": "start", + "type": "float", + "display_name": "Start", + "tooltip": None, + "default": None, + "expert": False, + }, + { + "arg": True, + "name": "stop", + "type": "float", + "display_name": "Stop", + "tooltip": None, + "default": None, + "expert": False, + }, + { + "arg": True, + "name": "steps", + "type": "int", + "display_name": "Steps", + "tooltip": None, + "default": None, + "expert": False, + }, + ], + "min": 2, + "max": None, + }, + "kwarg_groups": [ + { + "name": "Scan Parameters", + "inputs": [ + { + "arg": False, + "name": "exp_time", + "type": "float", + "display_name": "Exp Time", + "tooltip": "Exposure time in seconds", + "default": 0, + "expert": False, + }, + { + "arg": False, + "name": "settling_time", + "type": "float", + "display_name": "Settling Time", + "tooltip": "Settling time in seconds", + "default": 0, + "expert": False, + }, + { + "arg": False, + "name": "burst_at_each_point", + "type": "int", + "display_name": "Burst At Each Point", + "tooltip": "Number of exposures at each point", + "default": 1, + "expert": False, + }, + { + "arg": False, + "name": "relative", + "type": "bool", + "display_name": "Relative", + "tooltip": "If True, the motors will be moved relative to their current position", + "default": False, + "expert": False, + }, + ], + } + ], + }, + "required_kwargs": ["relative"], + "arg_bundle_size": {"bundle": 4, "min": 2, "max": None}, + }, + "not_supported_scan_class": {"base_class": "NotSupportedScanClass"}, + } +) + + @pytest.fixture(scope="function") def scan_control(qtbot, mocked_client): # , mock_dev): + mocked_client.connector.set("scans/available_scans", available_scans_message) widget = ScanControl(client=mocked_client) qtbot.addWidget(widget) qtbot.waitExposed(widget) @@ -21,81 +225,67 @@ def scan_control(qtbot, mocked_client): # , mock_dev): def test_populate_scans(scan_control, mocked_client): - # The comboBox should be populated with all scan from the message right after initialization - expected_scans = available_scans_message.resource.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 = available_scans_message.resource[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" + expected_scans = ["line_scan", "grid_scan"] + items = [ + scan_control.comboBox_scan_selection.itemText(i) + for i in range(scan_control.comboBox_scan_selection.count()) ] - # 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) + assert scan_control.comboBox_scan_selection.count() == 2 + assert sorted(items) == sorted(expected_scans) @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 +def test_on_scan_selected(scan_control, scan_name): expected_scan_info = available_scans_message.resource[scan_name] + scan_control.comboBox_scan_selection.setCurrentText(scan_name) - # Select a scan from the comboBox + # Check arg_box labels and widgets + for index, (arg_key, arg_value) in enumerate(expected_scan_info["arg_input"].items()): + label = scan_control.arg_box.layout.itemAtPosition(0, index).widget() + assert label.text().lower() == arg_key + + for row in range(1, expected_scan_info["arg_bundle_size"]["min"] + 1): + widget = scan_control.arg_box.layout.itemAtPosition(row, index).widget() + assert widget is not None # Confirm that a widget exists + expected_widget_type = scan_control.arg_box.WIDGET_HANDLER.get(arg_value, None) + assert isinstance(widget, expected_widget_type) # Confirm the widget type matches + + # Check kwargs boxes + kwargs_group = [param for param in expected_scan_info["gui_config"]["kwarg_groups"]] + print(kwargs_group) + + for kwarg_box, kwarg_group in zip(scan_control.kwarg_boxes, kwargs_group): + assert kwarg_box.title() == kwarg_group["name"] + for index, kwarg_info in enumerate(kwarg_group["inputs"]): + label = kwarg_box.layout.itemAtPosition(0, index).widget() + assert label.text() == kwarg_info["display_name"] + widget = kwarg_box.layout.itemAtPosition(1, index).widget() + expected_widget_type = kwarg_box.WIDGET_HANDLER.get(kwarg_info["type"], None) + assert isinstance(widget, expected_widget_type) + + +@pytest.mark.parametrize("scan_name", ["line_scan", "grid_scan"]) +def test_add_remove_bundle(scan_control, scan_name, qtbot): + expected_scan_info = available_scans_message.resource[scan_name] scan_control.comboBox_scan_selection.setCurrentText(scan_name) # Initial number of args row - initial_num_of_rows = scan_control.args_table.rowCount() + initial_num_of_rows = scan_control.arg_box.count_arg_rows() - # Check initial row count of args table - assert scan_control.args_table.rowCount() == expected_scan_info["arg_bundle_size"]["min"] + assert initial_num_of_rows == 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"] + scan_control.button_add_bundle.click() + scan_control.button_add_bundle.click() - # 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 + assert scan_control.arg_box.count_arg_rows() == initial_num_of_rows + 2 # Remove one bundle - scan_control.pushButton_remove_bundle.click() + scan_control.button_remove_bundle.click() + qtbot.wait(200) - # 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 + assert scan_control.arg_box.count_arg_rows() == initial_num_of_rows + 1 def test_run_line_scan_with_parameters(scan_control, mocked_client): @@ -103,32 +293,21 @@ def test_run_line_scan_with_parameters(scan_control, mocked_client): 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]) - + for kwarg_box in scan_control.kwarg_boxes: + for widget in kwarg_box.widgets: + for key, value in kwargs.items(): + if widget.arg_name == key: + WidgetIO.set_value(widget, value) + break # 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]) + for widget in scan_control.arg_box.widgets: + for key, value in args.items(): + if widget.arg_name == key: + WidgetIO.set_value(widget, value) + break # Mock the scan function mocked_scan_function = MagicMock() @@ -141,13 +320,7 @@ def test_run_line_scan_with_parameters(scan_control, mocked_client): 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_device = mocked_client.device_manager.devices.samx 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." + assert called_args == tuple(expected_args_list) + assert called_kwargs == kwargs diff --git a/tests/unit_tests/test_scan_control_group_box.py b/tests/unit_tests/test_scan_control_group_box.py new file mode 100644 index 00000000..4a2d0ea0 --- /dev/null +++ b/tests/unit_tests/test_scan_control_group_box.py @@ -0,0 +1,160 @@ +# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring +import pytest + +from bec_widgets.utils.widget_io import WidgetIO +from bec_widgets.widgets.scan_control.scan_group_box import ScanGroupBox + + +def test_kwarg_box(qtbot): + group_input = { + "name": "Kwarg Test", + "inputs": [ + # Test float + { + "arg": False, + "name": "exp_time", + "type": "float", + "display_name": "Exp Time", + "tooltip": "Exposure time in seconds", + "default": 0, + "expert": False, + }, + # Test int + { + "arg": False, + "name": "num_points", + "type": "int", + "display_name": "Num Points", + "tooltip": "Number of points", + "default": 1, + "expert": False, + }, + # Test bool + { + "arg": False, + "name": "relative", + "type": "bool", + "display_name": "Relative", + "tooltip": "If True, the motors will be moved relative to their current position", + "default": False, + "expert": False, + }, + # Test str + { + "arg": False, + "name": "scan_type", + "type": "str", + "display_name": "Scan Type", + "tooltip": "Type of scan", + "default": "line", + "expert": False, + }, + ], + } + + kwarg_box = ScanGroupBox(box_type="kwargs", config=group_input) + assert kwarg_box is not None + assert kwarg_box.box_type == "kwargs" + assert kwarg_box.config == group_input + assert kwarg_box.title() == "Kwarg Test" + + # Labels + assert kwarg_box.layout.itemAtPosition(0, 0).widget().text() == "Exp Time" + assert kwarg_box.layout.itemAtPosition(0, 1).widget().text() == "Num Points" + assert kwarg_box.layout.itemAtPosition(0, 2).widget().text() == "Relative" + assert kwarg_box.layout.itemAtPosition(0, 3).widget().text() == "Scan Type" + + # Widget 0 + assert kwarg_box.widgets[0].__class__.__name__ == "ScanDoubleSpinBox" + assert kwarg_box.widgets[0].arg_name == "exp_time" + assert WidgetIO.get_value(kwarg_box.widgets[0]) == 0 + assert kwarg_box.widgets[0].toolTip() == "Exposure time in seconds" + + # Widget 1 + assert kwarg_box.widgets[1].__class__.__name__ == "ScanSpinBox" + assert kwarg_box.widgets[1].arg_name == "num_points" + assert WidgetIO.get_value(kwarg_box.widgets[1]) == 1 + assert kwarg_box.widgets[1].toolTip() == "Number of points" + + # Widget 2 + assert kwarg_box.widgets[2].__class__.__name__ == "ScanCheckBox" + assert kwarg_box.widgets[2].arg_name == "relative" + assert WidgetIO.get_value(kwarg_box.widgets[2]) == False + assert ( + kwarg_box.widgets[2].toolTip() + == "If True, the motors will be moved relative to their current position" + ) + + # Widget 3 + assert kwarg_box.widgets[3].__class__.__name__ == "ScanLineEdit" + assert kwarg_box.widgets[3].arg_name == "scan_type" + assert WidgetIO.get_value(kwarg_box.widgets[3]) == "line" + assert kwarg_box.widgets[3].toolTip() == "Type of scan" + + parameters = kwarg_box.get_parameters() + assert parameters == {"exp_time": 0, "num_points": 1, "relative": False, "scan_type": "line"} + + +def test_arg_box(qtbot): + group_input = { + "name": "Arg Test", + "inputs": [ + # Test device + { + "arg": True, + "name": "device", + "type": "str", + "display_name": "Device", + "tooltip": "Device to scan", + "default": "samx", + "expert": False, + }, + # Test float + { + "arg": True, + "name": "start", + "type": "float", + "display_name": "Start", + "tooltip": "Start position", + "default": 0, + "expert": False, + }, + # Test int + { + "arg": True, + "name": "stop", + "type": "int", + "display_name": "Stop", + "tooltip": "Stop position", + "default": 1, + "expert": False, + }, + ], + } + + arg_box = ScanGroupBox(box_type="args", config=group_input) + assert arg_box is not None + assert arg_box.box_type == "args" + assert arg_box.config == group_input + assert arg_box.title() == "Arg Test" + + # Labels + assert arg_box.layout.itemAtPosition(0, 0).widget().text() == "Device" + assert arg_box.layout.itemAtPosition(0, 1).widget().text() == "Start" + assert arg_box.layout.itemAtPosition(0, 2).widget().text() == "Stop" + + # Widget 0 + assert arg_box.widgets[0].__class__.__name__ == "ScanLineEdit" + assert arg_box.widgets[0].arg_name == "device" + assert WidgetIO.get_value(arg_box.widgets[0]) == "samx" + assert arg_box.widgets[0].toolTip() == "Device to scan" + + # Widget 1 + assert arg_box.widgets[1].__class__.__name__ == "ScanDoubleSpinBox" + assert arg_box.widgets[1].arg_name == "start" + assert WidgetIO.get_value(arg_box.widgets[1]) == 0 + assert arg_box.widgets[1].toolTip() == "Start position" + + # Widget 2 + assert arg_box.widgets[2].__class__.__name__ == "ScanSpinBox" + assert arg_box.widgets[2].arg_name