0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

test: motor_control_compilations.py and motor_control.py tests added

This commit is contained in:
wyzula-jan
2024-02-07 01:26:35 +01:00
parent fa4ca935bb
commit bf04a4e04a
3 changed files with 659 additions and 3 deletions

View File

@ -214,7 +214,7 @@ class MotorControlPanelRelative(QWidget):
self.layout().setSizeConstraint(layout.SetFixedSize)
if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
import argparse
import sys
@ -253,7 +253,8 @@ if __name__ == "__main__":
window = MotorControlPanelRelative(client=client, config=CONFIG_DEFAULT)
else:
print("Please specify a valid variant to run. Use -h for help.")
sys.exit(1)
print("Running the full application by default.")
window = MotorControlApp(client=client, config=CONFIG_DEFAULT)
window.show()
sys.exit(app.exec())

View File

@ -219,6 +219,9 @@ class MotorControlAbsolute(MotorControlWidget):
lambda error: MotorControlErrors.display_error_message(error)
)
# Keyboard shortcuts
self._init_keyboard_shortcuts()
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""Update config dict"""
@ -577,6 +580,9 @@ class MotorCoordinateTable(MotorControlWidget):
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.table)
self.backspace_shortcut.activated.connect(self.delete_selected_row)
# Warning message for mode switch enable/disable
self.warning_message = True
@pyqtSlot(dict)
def on_config_update(self, config: dict) -> None:
"""
@ -644,7 +650,7 @@ class MotorCoordinateTable(MotorControlWidget):
"""Switch between individual and start/stop mode."""
last_selected_index = self.comboBox_mode.currentIndex()
if self.table.rowCount() > 0:
if self.table.rowCount() > 0 and self.warning_message is True:
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Critical)
msgBox.setText(

649
tests/test_motor_control.py Normal file
View File

@ -0,0 +1,649 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
from unittest.mock import patch
from bec_lib.device import Positioner
import pytest
from unittest.mock import MagicMock
from bec_widgets.widgets import (
MotorControlSelection,
MotorControlAbsolute,
MotorControlRelative,
MotorThread,
MotorCoordinateTable,
)
from bec_widgets.examples import (
MotorControlApp,
MotorControlMap,
MotorControlPanel,
MotorControlPanelAbsolute,
MotorControlPanelRelative,
)
from bec_widgets.widgets.motor_control.motor_control import MotorActions
CONFIG_DEFAULT = {
"motor_control": {
"motor_x": "samx",
"motor_y": "samy",
"step_size_x": 3,
"step_size_y": 3,
"precision": 4,
"step_x_y_same": False,
"move_with_arrows": False,
},
"plot_settings": {
"colormap": "Greys",
"scatter_size": 5,
"max_points": 1000,
"num_dim_points": 100,
"precision": 2,
"num_columns": 1,
"background_value": 25,
},
"motors": [
{
"plot_name": "Motor Map",
"x_label": "Motor X",
"y_label": "Motor Y",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "samy", "entry": "samy"}],
},
},
],
}
#######################################################
# Client and devices fixture
#######################################################
class FakeDevice:
"""Fake minimal positioner class for testing."""
def __init__(self, name, enabled=True, limits=None, read_value=1.0):
super().__init__()
self.name = name
self.enabled = enabled
self.read_value = read_value
self.limits = limits or (-100, 100) # Default limits if not provided
def read(self):
"""Simulates reading the current position of the device."""
return {self.name: {"value": self.read_value}}
def move(self, value, relative=False):
"""Simulates moving the device to a new position."""
if relative:
self.read_value += value
else:
self.read_value = value
# Respect the limits
self.read_value = max(min(self.read_value, self.limits[1]), self.limits[0])
@property
def readback(self):
return MagicMock(get=MagicMock(return_value=self.read_value))
def describe(self):
"""Describes the device."""
return {self.name: {"source": self.name, "dtype": "number", "shape": []}}
@pytest.fixture
def mocked_client():
client = MagicMock()
# Setup the fake devices
motors = {
"samx": FakeDevice("samx", limits=[-10, 10], read_value=2.0),
"samy": FakeDevice("samy", limits=[-5, 5], read_value=3.0),
"aptrx": FakeDevice("aptrx", read_value=4.0),
"aptry": FakeDevice("aptry", read_value=5.0),
}
client.device_manager.devices = MagicMock()
client.device_manager.devices.__getitem__.side_effect = lambda x: motors.get(x, FakeDevice(x))
client.device_manager.devices.enabled_devices = list(motors.values())
# Mock the scans.mv method
def mock_mv(*args, relative=False):
# Extracting motor and value pairs
for i in range(0, len(args), 2):
motor = args[i]
value = args[i + 1]
motor.move(value, relative=relative)
return MagicMock(wait=MagicMock()) # Simulate wait method of the move status object
client.scans = MagicMock(mv=mock_mv)
# Ensure isinstance check for Positioner passes
original_isinstance = isinstance
def isinstance_mock(obj, class_info):
if class_info == Positioner:
return True
return original_isinstance(obj, class_info)
with patch("builtins.isinstance", new=isinstance_mock):
yield client
#######################################################
# Motor Thread
#######################################################
@pytest.fixture
def motor_thread(mocked_client):
"""Fixture for MotorThread with a mocked client."""
return MotorThread(client=mocked_client)
def test_motor_thread_initialization(mocked_client):
motor_thread = MotorThread(client=mocked_client)
assert motor_thread.client == mocked_client
assert isinstance(motor_thread.dev, MagicMock)
def test_get_all_motors_names(mocked_client):
motor_thread = MotorThread(client=mocked_client)
motor_names = motor_thread.get_all_motors_names()
expected_names = ["samx", "samy", "aptrx", "aptry"]
assert sorted(motor_names) == sorted(expected_names)
assert all(name in motor_names for name in expected_names)
assert len(motor_names) == len(expected_names) # Ensure only these motors are returned
def test_get_coordinates(mocked_client):
motor_thread = MotorThread(client=mocked_client)
motor_x, motor_y = "samx", "samy"
x, y = motor_thread.get_coordinates(motor_x, motor_y)
assert x == mocked_client.device_manager.devices[motor_x].readback.get()
assert y == mocked_client.device_manager.devices[motor_y].readback.get()
def test_move_motor_absolute_by_run(mocked_client):
motor_thread = MotorThread(client=mocked_client)
motor_thread.motor_x = "samx"
motor_thread.motor_y = "samy"
motor_thread.target_coordinates = (5.0, -3.0)
motor_thread.action = MotorActions.MOVE_ABSOLUTE
motor_thread.run()
assert mocked_client.device_manager.devices["samx"].read_value == 5.0
assert mocked_client.device_manager.devices["samy"].read_value == -3.0
def test_move_motor_relative_by_run(mocked_client):
motor_thread = MotorThread(client=mocked_client)
motor_thread.motor = "samx"
motor_thread.value = 2.0
motor_thread.action = MotorActions.MOVE_RELATIVE
motor_thread.run()
assert mocked_client.device_manager.devices["samx"].read_value == 4.0
def test_motor_thread_move_absolute(motor_thread):
motor_x = "samx"
motor_y = "samy"
target_x = 5.0
target_y = -3.0
motor_thread.move_absolute(motor_x, motor_y, (target_x, target_y))
motor_thread.wait()
assert motor_thread.dev[motor_x].read()["samx"]["value"] == target_x
assert motor_thread.dev[motor_y].read()["samy"]["value"] == target_y
def test_motor_thread_move_relative(motor_thread):
motor_name = "samx"
move_value = 2.0
initial_value = motor_thread.dev[motor_name].read()["samx"]["value"]
motor_thread.move_relative(motor_name, move_value)
motor_thread.wait()
expected_value = initial_value + move_value
assert motor_thread.dev[motor_name].read()["samx"]["value"] == expected_value
#######################################################
# Motor Control Widgets - MotorControlSelection
#######################################################
@pytest.fixture(scope="function")
def motor_selection_widget(qtbot, mocked_client, motor_thread):
"""Fixture for creating a MotorControlSelection widget with a mocked client."""
widget = MotorControlSelection(
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget
def test_initialization_and_population(motor_selection_widget):
assert motor_selection_widget.comboBox_motor_x.count() == 4
assert motor_selection_widget.comboBox_motor_x.itemText(0) == "samx"
assert motor_selection_widget.comboBox_motor_y.itemText(1) == "samy"
assert motor_selection_widget.comboBox_motor_x.itemText(2) == "aptrx"
assert motor_selection_widget.comboBox_motor_y.itemText(3) == "aptry"
def test_selection_and_signal_emission(motor_selection_widget):
# Connect signal to a custom slot to capture the emitted values
emitted_values = []
def capture_emitted_values(motor_x, motor_y):
emitted_values.append((motor_x, motor_y))
motor_selection_widget.selected_motors_signal.connect(capture_emitted_values)
# Select motors
motor_selection_widget.comboBox_motor_x.setCurrentIndex(0) # Select 'samx'
motor_selection_widget.comboBox_motor_y.setCurrentIndex(1) # Select 'samy'
motor_selection_widget.pushButton_connecMotors.click() # Emit the signal
# Verify the emitted signal
assert emitted_values == [
("samx", "samy")
], "The emitted signal did not match the expected values"
def test_configuration_update(motor_selection_widget):
new_config = {"motor_control": {"motor_x": "samy", "motor_y": "samx"}}
motor_selection_widget.on_config_update(new_config)
assert motor_selection_widget.comboBox_motor_x.currentText() == "samy"
assert motor_selection_widget.comboBox_motor_y.currentText() == "samx"
def test_enable_motor_controls(motor_selection_widget):
motor_selection_widget.enable_motor_controls(False)
assert not motor_selection_widget.comboBox_motor_x.isEnabled()
assert not motor_selection_widget.comboBox_motor_y.isEnabled()
motor_selection_widget.enable_motor_controls(True)
assert motor_selection_widget.comboBox_motor_x.isEnabled()
assert motor_selection_widget.comboBox_motor_y.isEnabled()
#######################################################
# Motor Control Widgets - MotorControlAbsolute
#######################################################
@pytest.fixture(scope="function")
def motor_absolute_widget(qtbot, mocked_client, motor_thread):
widget = MotorControlAbsolute(
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget
def test_absolute_initialization(motor_absolute_widget):
motor_absolute_widget.change_motors("samx", "samy")
motor_absolute_widget.on_config_update(CONFIG_DEFAULT)
assert motor_absolute_widget.motor_x == "samx", "Motor X not initialized correctly"
assert motor_absolute_widget.motor_y == "samy", "Motor Y not initialized correctly"
assert motor_absolute_widget.precision == CONFIG_DEFAULT["motor_control"]["precision"]
def test_absolute_save_current_coordinates(motor_absolute_widget):
motor_absolute_widget.client.device_manager["samx"].set_value(2.0)
motor_absolute_widget.client.device_manager["samy"].set_value(3.0)
motor_absolute_widget.change_motors("samx", "samy")
emitted_coordinates = []
def capture_emit(x_y):
emitted_coordinates.append(x_y)
motor_absolute_widget.coordinates_signal.connect(capture_emit)
# Trigger saving current coordinates
motor_absolute_widget.pushButton_save.click()
# Default position of samx and samy are 2.0 and 3.0 respectively
assert emitted_coordinates == [(2.0, 3.0)]
def test_absolute_set_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.spinBox_absolute_x.setValue(5)
motor_absolute_widget.spinBox_absolute_y.setValue(10)
# Connect to the coordinates_signal to capture emitted values
emitted_values = []
def capture_coordinates(x_y):
emitted_values.append(x_y)
motor_absolute_widget.coordinates_signal.connect(capture_coordinates)
# Simulate button click for absolute movement
motor_absolute_widget.pushButton_set.click()
assert emitted_values == [(5, 10)]
def test_absolute_go_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.change_motors("samx", "samy")
motor_absolute_widget.spinBox_absolute_x.setValue(5)
motor_absolute_widget.spinBox_absolute_y.setValue(10)
with patch(
"bec_widgets.widgets.motor_control.motor_control.MotorThread.move_absolute",
new_callable=MagicMock,
) as mock_move_absolute:
motor_absolute_widget.pushButton_go_absolute.click()
mock_move_absolute.assert_called_once_with("samx", "samy", (5, 10))
def test_change_motor_absolute(motor_absolute_widget):
motor_absolute_widget.change_motors("aptrx", "aptry")
assert motor_absolute_widget.motor_x == "aptrx"
assert motor_absolute_widget.motor_y == "aptry"
motor_absolute_widget.change_motors("samx", "samy")
assert motor_absolute_widget.motor_x == "samx"
assert motor_absolute_widget.motor_y == "samy"
def test_set_precision(motor_absolute_widget):
motor_absolute_widget.on_config_update(CONFIG_DEFAULT)
motor_absolute_widget.set_precision(2)
assert motor_absolute_widget.spinBox_absolute_x.decimals() == 2
assert motor_absolute_widget.spinBox_absolute_y.decimals() == 2
#######################################################
# Motor Control Widgets - MotorControlRelative
#######################################################
@pytest.fixture(scope="function")
def motor_relative_widget(qtbot, mocked_client, motor_thread):
widget = MotorControlRelative(
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget
def test_initialization_and_config_update(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
assert motor_relative_widget.motor_x == CONFIG_DEFAULT["motor_control"]["motor_x"]
assert motor_relative_widget.motor_y == CONFIG_DEFAULT["motor_control"]["motor_y"]
assert motor_relative_widget.precision == CONFIG_DEFAULT["motor_control"]["precision"]
# Simulate a configuration update
new_config = {
"motor_control": {
"motor_x": "new_motor_x",
"motor_y": "new_motor_y",
"precision": 2,
"step_size_x": 5,
"step_size_y": 5,
"step_x_y_same": True,
"move_with_arrows": True,
}
}
motor_relative_widget.on_config_update(new_config)
assert motor_relative_widget.motor_x == "new_motor_x"
assert motor_relative_widget.motor_y == "new_motor_y"
assert motor_relative_widget.precision == 2
def test_move_motor_relative(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
# Set step sizes
motor_relative_widget.spinBox_step_x.setValue(1)
motor_relative_widget.spinBox_step_y.setValue(1)
# Mock the move_relative method
motor_relative_widget.motor_thread.move_relative = MagicMock()
# Simulate button clicks
motor_relative_widget.toolButton_right.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_x, 1
)
motor_relative_widget.toolButton_left.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_x, -1
)
motor_relative_widget.toolButton_up.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_y, 1
)
motor_relative_widget.toolButton_down.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_y, -1
)
def test_precision_update(motor_relative_widget):
# Capture emitted precision values
emitted_values = []
def capture_precision(precision):
emitted_values.append(precision)
motor_relative_widget.precision_signal.connect(capture_precision)
# Update precision
motor_relative_widget.spinBox_precision.setValue(1)
assert emitted_values == [1]
assert motor_relative_widget.spinBox_step_x.decimals() == 1
assert motor_relative_widget.spinBox_step_y.decimals() == 1
def test_sync_step_sizes(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
motor_relative_widget.checkBox_same_xy.setChecked(True)
# Change step size for X
motor_relative_widget.spinBox_step_x.setValue(2)
assert motor_relative_widget.spinBox_step_y.value() == 2
def test_change_motor_relative(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
motor_relative_widget.change_motors("aptrx", "aptry")
assert motor_relative_widget.motor_x == "aptrx"
assert motor_relative_widget.motor_y == "aptry"
#######################################################
# Motor Control Widgets - MotorCoordinateTable
#######################################################
@pytest.fixture(scope="function")
def motor_coordinate_table(qtbot, mocked_client, motor_thread):
widget = MotorCoordinateTable(
client=mocked_client, config=CONFIG_DEFAULT, motor_thread=motor_thread
)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget
def test_delete_selected_row(motor_coordinate_table):
# Add a coordinate
motor_coordinate_table.add_coordinate((1.0, 2.0))
motor_coordinate_table.add_coordinate((3.0, 4.0))
# Select the row
motor_coordinate_table.table.selectRow(0)
# Delete the selected row
motor_coordinate_table.delete_selected_row()
assert motor_coordinate_table.table.rowCount() == 1
def test_add_coordinate_and_table_update(motor_coordinate_table):
# Disable Warning message popups for test
motor_coordinate_table.warning_message = False
# Add coordinate in Individual mode
motor_coordinate_table.add_coordinate((1.0, 2.0))
assert motor_coordinate_table.table.rowCount() == 1
# Check if the coordinates match
x_item_individual = motor_coordinate_table.table.cellWidget(0, 3) # Assuming X is in column 3
y_item_individual = motor_coordinate_table.table.cellWidget(0, 4) # Assuming Y is in column 4
assert float(x_item_individual.text()) == 1.0
assert float(y_item_individual.text()) == 2.0
# Switch to Start/Stop and add coordinates
motor_coordinate_table.comboBox_mode.setCurrentIndex(1) # Switch mode
motor_coordinate_table.add_coordinate((3.0, 4.0))
motor_coordinate_table.add_coordinate((5.0, 6.0))
assert motor_coordinate_table.table.rowCount() == 1
def test_plot_coordinates_signal(motor_coordinate_table):
# Connect to the signal
def signal_emitted(coordinates, reference_tag, color):
nonlocal received
received = True
assert len(coordinates) == 1 # Assuming one coordinate was added
assert reference_tag in ["Individual", "Start", "Stop"]
assert color in ["green", "blue", "red"]
received = False
motor_coordinate_table.plot_coordinates_signal.connect(signal_emitted)
# Add a coordinate and check signal
motor_coordinate_table.add_coordinate((1.0, 2.0))
assert received
def test_move_motor_action(motor_coordinate_table):
# Add a coordinate
motor_coordinate_table.add_coordinate((1.0, 2.0))
# Mock the motor thread move_absolute function
motor_coordinate_table.motor_thread.move_absolute = MagicMock()
# Trigger the move action
move_button = motor_coordinate_table.table.cellWidget(0, 1)
move_button.click()
motor_coordinate_table.motor_thread.move_absolute.assert_called_with(
motor_coordinate_table.motor_x, motor_coordinate_table.motor_y, (1.0, 2.0)
)
def test_plot_coordinates_signal_individual(motor_coordinate_table, qtbot):
motor_coordinate_table.warning_message = False
motor_coordinate_table.set_precision(3)
motor_coordinate_table.comboBox_mode.setCurrentIndex(0)
# This list will store the signals emitted during the test
emitted_signals = []
def signal_emitted(coordinates, reference_tag, color):
emitted_signals.append((coordinates, reference_tag, color))
motor_coordinate_table.plot_coordinates_signal.connect(signal_emitted)
# Add new coordinates
motor_coordinate_table.add_coordinate((1.0, 2.0))
qtbot.wait(100)
# Verify the signals
assert len(emitted_signals) > 0, "No signals were emitted."
for coordinates, reference_tag, color in emitted_signals:
assert len(coordinates) > 0, "Coordinates list is empty."
assert reference_tag == "Individual"
assert color == "green"
assert motor_coordinate_table.table.cellWidget(0, 3).text() == "1.000"
assert motor_coordinate_table.table.cellWidget(0, 4).text() == "2.000"
#######################################################
# MotorControl examples compilations
#######################################################
@pytest.fixture(scope="function")
def motor_app(qtbot, mocked_client):
widget = MotorControlApp(config=CONFIG_DEFAULT, client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_motor_app_initialization(motor_app):
assert isinstance(motor_app, MotorControlApp)
assert motor_app.client is not None
assert motor_app.config == CONFIG_DEFAULT
@pytest.fixture(scope="function")
def motor_control_map(qtbot, mocked_client):
widget = MotorControlMap(config=CONFIG_DEFAULT, client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_motor_control_map_initialization(motor_control_map):
assert isinstance(motor_control_map, MotorControlMap)
assert motor_control_map.client is not None
assert motor_control_map.config == CONFIG_DEFAULT
@pytest.fixture(scope="function")
def motor_control_panel(qtbot, mocked_client):
widget = MotorControlPanel(config=CONFIG_DEFAULT, client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_motor_control_panel_initialization(motor_control_panel):
assert isinstance(motor_control_panel, MotorControlPanel)
assert motor_control_panel.client is not None
assert motor_control_panel.config == CONFIG_DEFAULT
@pytest.fixture(scope="function")
def motor_control_panel_absolute(qtbot, mocked_client):
widget = MotorControlPanelAbsolute(config=CONFIG_DEFAULT, client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_motor_control_panel_absolute_initialization(motor_control_panel_absolute):
assert isinstance(motor_control_panel_absolute, MotorControlPanelAbsolute)
assert motor_control_panel_absolute.client is not None
assert motor_control_panel_absolute.config == CONFIG_DEFAULT
@pytest.fixture(scope="function")
def motor_control_panel_relative(qtbot, mocked_client):
widget = MotorControlPanelRelative(config=CONFIG_DEFAULT, client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_motor_control_panel_relative_initialization(motor_control_panel_relative):
assert isinstance(motor_control_panel_relative, MotorControlPanelRelative)
assert motor_control_panel_relative.client is not None
assert motor_control_panel_relative.config == CONFIG_DEFAULT