mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
refactor(tests): ensure BEC dispatcher singleton object is renewed at each test
and add a check for dangling threads
This commit is contained in:
@ -3,6 +3,7 @@ import os
|
||||
from qtpy import uic
|
||||
from qtpy.QtWidgets import QMainWindow, QApplication
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.widgets import BECMonitor
|
||||
|
||||
# some default configs for demonstration purposes
|
||||
@ -166,7 +167,7 @@ class ModularApp(QMainWindow):
|
||||
super(ModularApp, self).__init__(parent)
|
||||
|
||||
# Client and device manager from BEC
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.client = BECDispatcher().client if client is None else client
|
||||
|
||||
# Loading UI
|
||||
current_path = os.path.dirname(__file__)
|
||||
@ -187,10 +188,8 @@ class ModularApp(QMainWindow):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
|
||||
# BECclient global variables
|
||||
client = bec_dispatcher.client
|
||||
client = BECDispatcher().client
|
||||
client.start()
|
||||
|
||||
app = QApplication([])
|
||||
|
@ -10,6 +10,7 @@ from pyqtgraph import mkBrush, mkColor, mkPen
|
||||
from pyqtgraph.Qt import QtCore, uic
|
||||
|
||||
from bec_widgets.utils import Crosshair, ctrl_c
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
|
||||
# TODO implement:
|
||||
@ -239,8 +240,6 @@ class PlotApp(QWidget):
|
||||
if __name__ == "__main__":
|
||||
import yaml
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
|
||||
with open("config_noworker.yaml", "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
@ -251,6 +250,7 @@ if __name__ == "__main__":
|
||||
dap_worker = None if dap_worker == "None" else dap_worker
|
||||
|
||||
# BECclient global variables
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
|
@ -23,6 +23,7 @@ from pyqtgraph.Qt import QtWidgets
|
||||
|
||||
from bec_lib import MessageEndpoints
|
||||
from bec_widgets.utils import Crosshair, Colors
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
|
||||
# TODO implement:
|
||||
@ -92,7 +93,7 @@ class PlotApp(QWidget):
|
||||
self.error_handler = ErrorHandler(parent=self)
|
||||
|
||||
# Client and device manager from BEC
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.client = BECDispatcher().client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
|
||||
# Loading UI
|
||||
@ -692,8 +693,6 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
# from bec_widgets import ctrl_c
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
|
||||
parser = argparse.ArgumentParser(description="Plotting App")
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
@ -715,6 +714,7 @@ if __name__ == "__main__":
|
||||
exit(1)
|
||||
|
||||
# BECclient global variables
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
|
@ -13,9 +13,7 @@ from pyqtgraph import mkBrush, mkPen
|
||||
from pyqtgraph.Qt import QtCore, QtWidgets, uic
|
||||
from pyqtgraph.Qt.QtCore import pyqtSignal
|
||||
from bec_widgets.utils import Crosshair, Colors
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
|
||||
# client = bec_dispatcher.client
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
|
||||
class StreamPlot(QtWidgets.QWidget):
|
||||
@ -32,7 +30,7 @@ class StreamPlot(QtWidgets.QWidget):
|
||||
"""
|
||||
|
||||
# Client and device manager from BEC
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.client = BECDispatcher().client if client is None else client
|
||||
|
||||
super(StreamPlot, self).__init__()
|
||||
# Set style for pyqtgraph plots
|
||||
@ -314,6 +312,7 @@ if __name__ == "__main__":
|
||||
print(f"Plotting signals for: {', '.join(value.signals)}")
|
||||
|
||||
# Client from dispatcher
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
|
||||
app = QtWidgets.QApplication([])
|
||||
|
@ -90,6 +90,16 @@ class _BECDispatcher(QObject):
|
||||
consumer.start()
|
||||
return _Connection(consumer)
|
||||
|
||||
def _do_disconnect_slot(self, topic, slot):
|
||||
connection = self._connections[topic]
|
||||
connection.signal.disconnect(slot)
|
||||
connection.slots.remove(slot)
|
||||
if not connection.slots:
|
||||
print(f"{connection.consumer} is shutting down")
|
||||
connection.consumer.shutdown()
|
||||
connection.consumer.join()
|
||||
del self._connections[topic]
|
||||
|
||||
def _disconnect_slot_from_topic(self, slot: Callable, topic: str) -> None:
|
||||
"""A helper method to disconnect a slot from a specific topic.
|
||||
|
||||
@ -100,11 +110,7 @@ class _BECDispatcher(QObject):
|
||||
"""
|
||||
connection = self._connections.get(topic)
|
||||
if connection and slot in connection.slots:
|
||||
connection.signal.disconnect(slot)
|
||||
connection.slots.remove(slot)
|
||||
if not connection.slots:
|
||||
connection.consumer.shutdown()
|
||||
del self._connections[topic]
|
||||
self._do_disconnect_slot(topic, slot)
|
||||
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]) -> None:
|
||||
"""Disconnect widget's pyqt slot from pub/sub updates on a topic.
|
||||
@ -123,12 +129,7 @@ class _BECDispatcher(QObject):
|
||||
if common_topics:
|
||||
remaining_topics = set(key) - set(topics)
|
||||
# Disconnect slot from common topics
|
||||
connection.signal.disconnect(slot)
|
||||
connection.slots.remove(slot)
|
||||
if not connection.slots:
|
||||
print(f"{connection.consumer} is shutting down")
|
||||
connection.consumer.shutdown()
|
||||
del self._connections[key]
|
||||
self._do_disconnect_slot(key, slot)
|
||||
# Reconnect slot to remaining topics if any
|
||||
if remaining_topics:
|
||||
self.connect_slot(slot, list(remaining_topics), True)
|
||||
@ -139,13 +140,17 @@ class _BECDispatcher(QObject):
|
||||
for slot in list(connection.slots):
|
||||
self._disconnect_slot_from_topic(slot, key)
|
||||
|
||||
if key in self._connections and not connection.slots:
|
||||
connection.consumer.shutdown()
|
||||
del self._connections[key]
|
||||
|
||||
# variable holding the Singleton instance of BECDispatcher
|
||||
_bec_dispatcher = None
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bec-config", default=None)
|
||||
args, _ = parser.parse_known_args()
|
||||
def BECDispatcher():
|
||||
global _bec_dispatcher
|
||||
if _bec_dispatcher is None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bec-config", default=None)
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
bec_dispatcher = _BECDispatcher(args.bec_config)
|
||||
_bec_dispatcher = _BECDispatcher(args.bec_config)
|
||||
return _bec_dispatcher
|
||||
|
@ -12,7 +12,7 @@ from qtpy.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
from bec_widgets.utils import Colors, Crosshair, yaml_dialog
|
||||
from bec_widgets.validation import MonitorConfigValidator
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
# just for demonstration purposes if script run directly
|
||||
CONFIG_SCAN_MODE = {
|
||||
@ -282,6 +282,7 @@ class BECMonitor(pg.GraphicsLayoutWidget):
|
||||
|
||||
# Client and device manager from BEC
|
||||
self.plot_data = None
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.queue = self.client.queue
|
||||
@ -824,7 +825,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
else:
|
||||
config = CONFIG_SIMPLE
|
||||
|
||||
client = bec_dispatcher.client
|
||||
client = BECDispatcher().client
|
||||
client.start()
|
||||
app = QApplication(sys.argv)
|
||||
monitor = BECMonitor(
|
||||
|
@ -14,7 +14,7 @@ from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.yaml_dialog import load_yaml
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"plot_settings": {
|
||||
@ -63,6 +63,7 @@ class MotorMap(pg.GraphicsLayoutWidget):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
# Import BEC related stuff
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
|
||||
@ -172,6 +173,7 @@ class MotorMap(pg.GraphicsLayoutWidget):
|
||||
"""Connect motors to slots."""
|
||||
|
||||
# Disconnect all slots before connecting a new ones
|
||||
bec_dispatcher = BECDispatcher()
|
||||
bec_dispatcher.disconnect_all()
|
||||
|
||||
# Get list of all unique motors
|
||||
@ -541,7 +543,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
else:
|
||||
config = CONFIG_DEFAULT
|
||||
|
||||
client = bec_dispatcher.client
|
||||
client = BECDispatcher().client
|
||||
client.start()
|
||||
app = QApplication(sys.argv)
|
||||
motor_map = MotorMap(
|
||||
|
@ -22,6 +22,7 @@ from qtpy.QtWidgets import (
|
||||
|
||||
from bec_lib import MessageEndpoints
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
|
||||
class ScanArgType:
|
||||
@ -45,7 +46,7 @@ class ScanControl(QWidget):
|
||||
super().__init__(parent)
|
||||
|
||||
# Client from BEC + shortcuts to device manager and scans
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.client = BECDispatcher().client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.scans = self.client.scans
|
||||
|
||||
@ -425,10 +426,8 @@ class ScanControl(QWidget):
|
||||
|
||||
# Application example
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
|
||||
# BECclient global variables
|
||||
client = bec_dispatcher.client
|
||||
client = BECDispatcher().client
|
||||
client.start()
|
||||
|
||||
app = QApplication([])
|
||||
|
@ -6,7 +6,7 @@ from bec_lib import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property as pyqtProperty, Slot as pyqtSlot
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@ -17,7 +17,7 @@ pg.setConfigOptions(background="w", foreground="k", antialias=True)
|
||||
class BECScanPlot2D(pg.GraphicsView):
|
||||
def __init__(self, parent=None, background="default"):
|
||||
super().__init__(parent, background)
|
||||
bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
BECDispatcher().connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
|
||||
self._scanID = None
|
||||
self._scanID_lock = RLock()
|
||||
|
@ -6,7 +6,7 @@ from bec_lib import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property as pyqtProperty, Slot as pyqtSlot
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import bec_dispatcher
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@ -18,7 +18,7 @@ COLORS = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a"]
|
||||
class BECScanPlot(pg.GraphicsView):
|
||||
def __init__(self, parent=None, background="default"):
|
||||
super().__init__(parent, background)
|
||||
bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
BECDispatcher().connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
|
||||
self.view = pg.PlotItem()
|
||||
self.setCentralItem(self.view)
|
||||
@ -94,6 +94,7 @@ class BECScanPlot(pg.GraphicsView):
|
||||
|
||||
@y_channel_list.setter
|
||||
def y_channel_list(self, new_list):
|
||||
bec_dispatcher = BECDispatcher()
|
||||
# TODO: do we want to care about dap/not dap here?
|
||||
chan_removed = [chan for chan in self._y_channel_list if chan not in new_list]
|
||||
if chan_removed and chan_removed[0].startswith("dap."):
|
||||
|
32
tests/conftest.py
Normal file
32
tests/conftest.py
Normal file
@ -0,0 +1,32 @@
|
||||
import pytest
|
||||
import threading
|
||||
|
||||
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def threads_check():
|
||||
current_threads = set(
|
||||
th
|
||||
for th in threading.enumerate()
|
||||
if "loguru" not in th.name and th is not threading.main_thread()
|
||||
)
|
||||
yield
|
||||
threads_after = set(
|
||||
th
|
||||
for th in threading.enumerate()
|
||||
if "loguru" not in th.name and th is not threading.main_thread()
|
||||
)
|
||||
additional_threads = threads_after - current_threads
|
||||
assert (
|
||||
len(additional_threads) == 0
|
||||
), f"Test creates {len(additional_threads)} threads that are not cleaned: {additional_threads}"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bec_dispatcher(threads_check):
|
||||
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
|
||||
yield bec_dispatcher
|
||||
bec_dispatcher.disconnect_all()
|
||||
# reinitialize singleton for next test
|
||||
bec_dispatcher_module._bec_dispatcher = None
|
@ -5,18 +5,10 @@ import pytest
|
||||
from bec_lib.messages import ScanMessage
|
||||
from bec_lib.connector import MessageObject
|
||||
|
||||
# TODO: find a better way to mock singletons
|
||||
from bec_widgets.utils.bec_dispatcher import _BECDispatcher
|
||||
|
||||
msg = MessageObject(topic="", value=ScanMessage(point_id=0, scanID=0, data={}).dumps())
|
||||
|
||||
|
||||
@pytest.fixture(name="bec_dispatcher")
|
||||
def _bec_dispatcher():
|
||||
bec_dispatcher = _BECDispatcher()
|
||||
yield bec_dispatcher
|
||||
|
||||
|
||||
@pytest.fixture(name="consumer")
|
||||
def _consumer(bec_dispatcher):
|
||||
bec_dispatcher.client.connector.consumer = Mock()
|
||||
|
Reference in New Issue
Block a user