Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion bec_widgets/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pylint: skip-file
from unittest.mock import MagicMock

from bec_lib.config_helper import ConfigHelper
from bec_lib.device import Device as BECDevice
from bec_lib.device import Positioner as BECPositioner
from bec_lib.device import ReadoutPriority
Expand Down Expand Up @@ -219,7 +220,9 @@ def __init__(self, name, enabled=True):


class DMMock:
def __init__(self):
def __init__(self, *args, **kwargs):
self._service = args[0]
self.config_helper = ConfigHelper(self._service.connector, self._service._service_name)
self.devices = DeviceContainer()
self.enabled_devices = [device for device in self.devices if device.enabled]

Expand Down Expand Up @@ -273,6 +276,10 @@ def _get_redis_device_config(self) -> list[dict]:
configs.append(device._config)
return configs

def initialize(*_): ...

def shutdown(self): ...


DEVICES = [
FakePositioner("samx", limits=[-10, 10], read_value=2.0),
Expand Down
11 changes: 5 additions & 6 deletions bec_widgets/utils/bec_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,16 @@ def __init__(self, client=None, config: str | ServiceConfig | None = None, gui_i
self._registered_slots: DefaultDict[Hashable, QtThreadSafeCallback] = (
collections.defaultdict()
)
self.client = client

if self.client is None:
if config is not None:
if not isinstance(config, ServiceConfig):
# config is supposed to be a path
config = ServiceConfig(config)
if client is None:
if config is not None and not isinstance(config, ServiceConfig):
# config is supposed to be a path
config = ServiceConfig(config)
self.client = BECClient(
config=config, connector_cls=QtRedisConnector, name="BECWidgets"
)
else:
self.client = client
if self.client.started:
# have to reinitialize client to use proper connector
logger.info("Shutting down BECClient to switch to QtRedisConnector")
Expand Down
6 changes: 3 additions & 3 deletions bec_widgets/widgets/containers/explorer/script_tree_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,20 @@ def __init__(self, parent=None):
layout.setSpacing(0)

# Create tree view
self.tree = QTreeView()
self.tree = QTreeView(parent=self)
self.tree.setHeaderHidden(True)
self.tree.setRootIsDecorated(True)

# Enable mouse tracking for hover effects
self.tree.setMouseTracking(True)

# Create file system model
self.model = QFileSystemModel()
self.model = QFileSystemModel(parent=self)
self.model.setNameFilters(["*.py"])
self.model.setNameFilterDisables(False)

# Create proxy model to filter out underscore directories
self.proxy_model = QSortFilterProxyModel()
self.proxy_model = QSortFilterProxyModel(parent=self)
self.proxy_model.setFilterRegularExpression(QRegularExpression("^[^_].*"))
self.proxy_model.setSourceModel(self.model)
self.tree.setModel(self.proxy_model)
Expand Down
47 changes: 8 additions & 39 deletions tests/unit_tests/client_mocks.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,18 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
from math import inf
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, PropertyMock, patch

import fakeredis
import pytest
from bec_lib.bec_service import messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.redis_connector import RedisConnector
from bec_lib.scan_history import ScanHistory

from bec_widgets.tests.utils import DEVICES, DMMock, FakePositioner, Positioner


def fake_redis_server(host, port, **kwargs):
redis = fakeredis.FakeRedis()
return redis
from bec_widgets.tests.utils import FakePositioner, Positioner


@pytest.fixture(scope="function")
def mocked_client(bec_dispatcher):
connector = RedisConnector("localhost:1", redis_cls=fake_redis_server)
# Create a MagicMock object
client = MagicMock() # TODO change to real BECClient

# Shutdown the original client
bec_dispatcher.client.shutdown()
# Mock the connector attribute
bec_dispatcher.client = client

# Mock the device_manager.devices attribute
client.connector = connector
client.device_manager = DMMock()
client.device_manager.add_devices(DEVICES)

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())

client.scans = MagicMock(mv=mock_mv)

# Ensure isinstance check for Positioner passes
original_isinstance = isinstance
Expand All @@ -52,8 +23,8 @@ def isinstance_mock(obj, class_info):
return original_isinstance(obj, class_info)

with patch("builtins.isinstance", new=isinstance_mock):
yield client
connector.shutdown() # TODO change to real BECClient
yield bec_dispatcher.client
bec_dispatcher.client.connector.shutdown()


##################################################
Expand Down Expand Up @@ -190,17 +161,16 @@ def mocked_client_with_dap(mocked_client, dap_plugin_message):
name="LmfitService1D", status=1, info={}
),
}
client = mocked_client
client.service_status = dap_services
client.connector.set(
type(mocked_client).service_status = PropertyMock(return_value=dap_services)
mocked_client.connector.set(
topic=MessageEndpoints.dap_available_plugins("dap"), msg=dap_plugin_message
)

# Patch the client's DAP attribute so that the available models include "GaussianModel"
patched_models = {"GaussianModel": {}, "LorentzModel": {}, "SineModel": {}}
client.dap._available_dap_plugins = patched_models
mocked_client.dap._available_dap_plugins = patched_models

yield client
yield mocked_client


class DummyData:
Expand Down Expand Up @@ -233,7 +203,6 @@ def create_dummy_scan_item():
"readout_priority": {"monitored": ["bpm4i"], "async": ["async_device"]},
}
}
dummy_scan.status_message = MagicMock()
dummy_scan.status_message.info = {
"readout_priority": {"monitored": ["bpm4i"], "async": ["async_device"]},
"scan_report_devices": ["samx"],
Expand Down
52 changes: 47 additions & 5 deletions tests/unit_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import json
import time
from unittest import mock
from unittest.mock import patch

import fakeredis
import h5py
import numpy as np
import pytest
from bec_lib import messages
from bec_lib import messages, service_config
from bec_lib.bec_service import messages
from bec_lib.client import BECClient
from bec_lib.messages import _StoredDataInfo
from bec_qthemes import apply_theme
from bec_qthemes._theme import Theme
from ophyd._pyepics_shim import _dispatcher
from pytestqt.exceptions import TimeoutError as QtBotTimeoutError
from qtpy.QtCore import QEvent, QEventLoop
from qtpy.QtWidgets import QApplication, QMessageBox

from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.tests.utils import DEVICES, DMMock
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
from bec_widgets.utils import error_popups
from bec_widgets.utils.bec_dispatcher import QtRedisConnector

# Patch to set default RAISE_ERROR_DEFAULT to True for tests
# This means that by default, error popups will raise exceptions during tests
Expand All @@ -38,15 +47,20 @@ def process_all_deferred_deletes(qapp):
def qapplication(qtbot, request, testable_qtimer_class): # pylint: disable=unused-argument
qapp = QApplication.instance()
process_all_deferred_deletes(qapp)
apply_theme("light")
qapp.processEvents()

if (
not hasattr(qapp, "theme")
or not isinstance(qapp.theme, Theme)
or qapp.theme.theme != "light"
):
apply_theme("light")
qapp.processEvents()

yield

# if the test failed, we don't want to check for open widgets as
# it simply pollutes the output
# stop pyepics dispatcher for leaking tests
from ophyd._pyepics_shim import _dispatcher

_dispatcher.stop()
if request.node.stash._storage.get("failed"):
Expand All @@ -71,9 +85,37 @@ def rpc_register():
RPCRegister.reset_singleton()


_REDIS_CONN: QtRedisConnector | None = None


def global_mock_qt_redis_connector(*_, **__):
global _REDIS_CONN
if _REDIS_CONN is None:
_REDIS_CONN = QtRedisConnector(bootstrap="localhost:1", redis_cls=fakeredis.FakeRedis)
return _REDIS_CONN


def mock_client(*_, **__):
with (
patch("bec_lib.client.DeviceManagerBase", DMMock),
patch("bec_lib.client.DAPPlugins"),
patch("bec_lib.client.Scans"),
patch("bec_lib.client.ScanManager"),
patch("bec_lib.bec_service.BECAccess"),
):
client = BECClient(
config=service_config.ServiceConfig(config={"redis": {"host": "localhost", "port": 1}}),
connector_cls=global_mock_qt_redis_connector,
)
client.start()
client.device_manager.add_devices(DEVICES)
return client


@pytest.fixture(autouse=True)
def bec_dispatcher(threads_check): # pylint: disable=unused-argument
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
with mock.patch.object(bec_dispatcher_module, "BECClient", mock_client):
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
yield bec_dispatcher
bec_dispatcher.disconnect_all()
# clean BEC client
Expand Down
3 changes: 3 additions & 0 deletions tests/unit_tests/test_abort_button.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import

from unittest.mock import MagicMock

import pytest

from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
Expand All @@ -10,6 +12,7 @@
@pytest.fixture
def abort_button(qtbot, mocked_client):
widget = AbortButton(client=mocked_client)
widget.queue = MagicMock()
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
Expand Down
37 changes: 36 additions & 1 deletion tests/unit_tests/test_bec_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,45 @@
from unittest import mock

import pytest
from bec_lib import service_config
from bec_lib.messages import ScanMessage
from bec_lib.serialization import MsgpackSerialization

from bec_widgets.utils.bec_dispatcher import QtRedisConnector, QtThreadSafeCallback
from bec_widgets.utils.bec_dispatcher import BECDispatcher, QtRedisConnector, QtThreadSafeCallback


def test_init_handles_client_and_config_arg():
# Client passed
self_mock = mock.MagicMock(_initialized=False)
with mock.patch.object(BECDispatcher, "start_cli_server"):
BECDispatcher.__init__(self_mock, client=mock.MagicMock(name="test_client"))
assert "test_client" in repr(self_mock.client)

# No client, service config object
self_mock.reset_mock()
self_mock._initialized = False
with (
mock.patch.object(BECDispatcher, "start_cli_server"),
mock.patch("bec_widgets.utils.bec_dispatcher.BECClient") as client_cls,
):
config = service_config.ServiceConfig()
BECDispatcher.__init__(self_mock, client=None, config=config)
client_cls.assert_called_with(
config=config, connector_cls=QtRedisConnector, name="BECWidgets"
)

# No client, service config string
self_mock.reset_mock()
self_mock._initialized = False
with (
mock.patch.object(BECDispatcher, "start_cli_server"),
mock.patch("bec_widgets.utils.bec_dispatcher.BECClient"),
mock.patch("bec_widgets.utils.bec_dispatcher.ServiceConfig") as svc_cfg,
mock.patch("bec_widgets.utils.bec_dispatcher.isinstance", return_value=False),
):
config = service_config.ServiceConfig()
BECDispatcher.__init__(self_mock, client=None, config="test_str")
svc_cfg.assert_called_with("test_str")


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion tests/unit_tests/test_device_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def test_signal_display(mocked_client, qtbot):

def test_signal_display_no_device(mocked_client, qtbot):
device_mock = mock.MagicMock()
mocked_client.client.device_manager.devices = {"test_device_1": device_mock}
mocked_client.device_manager.devices = {"test_device_1": device_mock}
signal_display = SignalDisplay(client=mocked_client, device="test_device_2")
qtbot.addWidget(signal_display)
assert (
Expand Down
10 changes: 5 additions & 5 deletions tests/unit_tests/test_device_signal_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,12 @@ def test_signal_lineedit(device_signal_line_edit):


def test_device_signal_input_base_cleanup(qtbot, mocked_client):
with mock.patch.object(mocked_client.callbacks, "remove"):
widget = DeviceInputWidget(client=mocked_client)
widget.close()
widget.deleteLater()

widget = DeviceInputWidget(client=mocked_client)
widget.close()
widget.deleteLater()

mocked_client.callbacks.remove.assert_called_once_with(widget._device_update_register)
mocked_client.callbacks.remove.assert_called_once_with(widget._device_update_register)


def test_signal_combobox_get_signal_name_with_item_data(qtbot, device_signal_combobox):
Expand Down
10 changes: 6 additions & 4 deletions tests/unit_tests/test_scatter_waveform.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import MagicMock, patch

import numpy as np

Expand Down Expand Up @@ -53,14 +53,16 @@ def test_scatter_waveform_update_with_scan_history(qtbot, mocked_client, monkeyp
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)

dummy_scan = create_dummy_scan_item()
mocked_client.history = MagicMock()
# .get_by_scan_id() typically returns historical data, but we abuse it here
# to return mock live data
mocked_client.history.get_by_scan_id.return_value = dummy_scan
mocked_client.history.__getitem__.return_value = dummy_scan

swf.plot("samx", "samy", "bpm4i", label="test_curve")
swf.update_with_scan_history(scan_id="dummy")
qtbot.wait(500)

assert swf.scan_item == dummy_scan
qtbot.waitUntil(lambda: swf.scan_item == dummy_scan, timeout=500)
qtbot.wait(200)

x_data, y_data = swf.main_curve.getData()
np.testing.assert_array_equal(x_data, [10, 20, 30])
Expand Down
4 changes: 2 additions & 2 deletions tests/unit_tests/test_utils_plot_indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@pytest.fixture
def plot_widget_with_arrow_item(qtbot, mocked_client):
widget = Waveform(client=mocked_client())
widget = Waveform(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)

Expand All @@ -17,7 +17,7 @@ def plot_widget_with_arrow_item(qtbot, mocked_client):

@pytest.fixture
def plot_widget_with_tick_item(qtbot, mocked_client):
widget = Waveform(client=mocked_client())
widget = Waveform(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)

Expand Down
Loading
Loading