0
0
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:
2024-01-19 16:19:42 +01:00
parent d281d6576c
commit d909673071
12 changed files with 81 additions and 51 deletions

View File

@ -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([])

View File

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

View File

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

View File

@ -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([])

View File

@ -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

View File

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

View File

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

View File

@ -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([])

View File

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

View File

@ -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
View 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

View File

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