diff --git a/tests/test_motor_map.py b/tests/test_motor_map.py new file mode 100644 index 00000000..1af9b875 --- /dev/null +++ b/tests/test_motor_map.py @@ -0,0 +1,241 @@ +import pytest +from unittest.mock import MagicMock + +from bec_widgets.widgets import MotorMap + +CONFIG_DEFAULT = { + "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"}], + }, + }, + { + "plot_name": "Motor Map 2 ", + "x_label": "Motor X", + "y_label": "Motor Y", + "signals": { + "x": [{"name": "aptrx", "entry": "aptrx"}], + "y": [{"name": "aptry", "entry": "aptry"}], + }, + }, + ], +} + +CONFIG_ONE_DEVICE = { + "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"}], + }, + }, + ], +} + + +class FakeDevice: + """Fake minimal positioner class for testing.""" + + def __init__(self, name, enabled=True, limits=None, read_value=1.0): + self.name = name + self.enabled = enabled + self.signals = {self.name: {"value": 1.0}} + self.description = {self.name: {"source": self.name}} + self.limits = limits if limits is not None else [0, 0] + self.read_value = read_value + + def set_read_value(self, value): + self.read_value = value + + def read(self): + return {self.name: {"value": self.read_value}} + + def set_limits(self, limits): + self.limits = limits + + def __contains__(self, item): + return item == self.name + + @property + def _hints(self): + return [self.name] + + def set_value(self, fake_value: float = 1.0) -> None: + """ + Setup fake value for device readout + Args: + fake_value(float): Desired fake value + """ + self.signals[self.name]["value"] = fake_value + + def describe(self) -> dict: + """ + Get the description of the device + Returns: + dict: Description of the device + """ + return self.description + + +@pytest.fixture +def mocked_client(): + client = MagicMock() + + # Mocking specific motors with their limits + 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)) + + return client + + +@pytest.fixture(scope="function") +def motor_map(qtbot, mocked_client): + widget = MotorMap(client=mocked_client) + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + yield widget + + +def test_motor_limits_initialization(motor_map): + # Example test to check if motor limits are correctly initialized + expected_limits = { + "samx": [-10, 10], + "samy": [-5, 5], + } + for motor_name, expected_limit in expected_limits.items(): + actual_limit = motor_map._get_motor_limit(motor_name) + assert actual_limit == expected_limit + + +def test_motor_initial_position(motor_map): + motor_map.precision = 2 + # Example test to check if motor initial positions are correctly initialized + expected_positions = { + ("samx", "samx"): 2.0, + ("samy", "samy"): 3.0, + ("aptrx", "aptrx"): 4.0, + ("aptry", "aptry"): 5.0, + } + for (motor_name, entry), expected_position in expected_positions.items(): + actual_position = motor_map._get_motor_init_position(motor_name, entry) + assert actual_position == expected_position + + +@pytest.mark.parametrize( + "config, number_of_plots", + [ + (CONFIG_DEFAULT, 2), + (CONFIG_ONE_DEVICE, 1), + ], +) +def test_initialization(motor_map, config, number_of_plots): + config_load = config + motor_map.on_config_update(config_load) + assert isinstance(motor_map, MotorMap) + assert motor_map.client is not None + assert motor_map.config == config_load + assert len(motor_map.plot_data) == number_of_plots + + +def test_motor_movement_updates_position_and_database(motor_map): + motor_map.on_config_update(CONFIG_DEFAULT) + + # Initial positions + initial_position_samx = 2.0 + initial_position_samy = 3.0 + + # Set initial positions in the mocked database + motor_map.database["samx"]["samx"] = [initial_position_samx] + motor_map.database["samy"]["samy"] = [initial_position_samy] + + # Simulate motor movement for 'samx' only + new_position_samx = 4.0 + motor_map.on_device_readback({"signals": {"samx": {"value": new_position_samx}}}) + + # Verify database update for 'samx' + assert motor_map.database["samx"]["samx"] == [ + initial_position_samx, + new_position_samx, + ] + + # Verify 'samy' retains its last known position + assert motor_map.database["samy"]["samy"] == [ + initial_position_samy, + initial_position_samy, + ] + + +def test_scatter_plot_rendering(motor_map): + motor_map.on_config_update(CONFIG_DEFAULT) + # Set initial positions + initial_position_samx = 2.0 + initial_position_samy = 3.0 + motor_map.database["samx"]["samx"] = [initial_position_samx] + motor_map.database["samy"]["samy"] = [initial_position_samy] + + # Simulate motor movement for 'samx' only + new_position_samx = 4.0 + motor_map.on_device_readback({"signals": {"samx": {"value": new_position_samx}}}) + motor_map._update_plots() + + # Get the scatter plot item + plot_name = "Motor Map" # Update as per your actual plot name + scatter_plot_item = motor_map.curves_data[plot_name]["pos"] + + # Check the scatter plot item properties + assert len(scatter_plot_item.data) > 0, "Scatter plot data is empty" + x_data = scatter_plot_item.data["x"] + y_data = scatter_plot_item.data["y"] + assert x_data[-1] == new_position_samx, "Scatter plot X data not updated correctly" + assert ( + y_data[-1] == initial_position_samy + ), "Scatter plot Y data should retain last known position" + + +def test_plot_visualization_consistency(motor_map): + motor_map.on_config_update(CONFIG_DEFAULT) + # Simulate updating the plot with new data + motor_map.on_device_readback({"signals": {"samx": {"value": 5}}}) + motor_map.on_device_readback({"signals": {"samy": {"value": 9}}}) + motor_map._update_plots() + + plot_name = "Motor Map" + scatter_plot_item = motor_map.curves_data[plot_name]["pos"] + + # Check if the scatter plot reflects the new data correctly + assert ( + scatter_plot_item.data["x"][-1] == 5 and scatter_plot_item.data["y"][-1] == 9 + ), "Plot not updated correctly with new data"