mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 11:11:49 +02:00
fix: segfaults from logpanel
- set parent of dialog components - try/except and log for redis callback - pass in ServiceStatusMixin
This commit is contained in:
@ -52,7 +52,7 @@ from bec_widgets.widgets.utility.logpanel._util import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from qtpy.QtCore import pyqtBoundSignal # type: ignore
|
from PySide6.QtCore import SignalInstance
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@ -74,14 +74,14 @@ class BecLogsQueue:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
conn: ConnectorBase,
|
conn: ConnectorBase,
|
||||||
new_message_signal: pyqtBoundSignal,
|
new_message_signal: SignalInstance,
|
||||||
maxlen: int = 1000,
|
maxlen: int = 1000,
|
||||||
line_formatter: LineFormatter = noop_format,
|
line_formatter: LineFormatter = noop_format,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._timestamp_start: QDateTime | None = None
|
self._timestamp_start: QDateTime | None = None
|
||||||
self._timestamp_end: QDateTime | None = None
|
self._timestamp_end: QDateTime | None = None
|
||||||
self._conn = conn
|
self._conn = conn
|
||||||
self._new_message_signal: pyqtBoundSignal | None = new_message_signal
|
self._new_message_signal: SignalInstance | None = new_message_signal
|
||||||
self._max_length = maxlen
|
self._max_length = maxlen
|
||||||
self._data: deque[LogMessage] = deque([], self._max_length)
|
self._data: deque[LogMessage] = deque([], self._max_length)
|
||||||
self._display_queue: deque[str] = deque([], self._max_length)
|
self._display_queue: deque[str] = deque([], self._max_length)
|
||||||
@ -93,15 +93,18 @@ class BecLogsQueue:
|
|||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
self._conn.unregister([MessageEndpoints.log()], None, self._process_incoming_log_msg)
|
self._conn.unregister([MessageEndpoints.log()], None, self._process_incoming_log_msg)
|
||||||
self._new_message_signal = None
|
self._new_message_signal.disconnect()
|
||||||
|
|
||||||
def _process_incoming_log_msg(self, msg: dict):
|
def _process_incoming_log_msg(self, msg: dict):
|
||||||
_msg: LogMessage = msg["data"]
|
try:
|
||||||
self._data.append(_msg)
|
_msg: LogMessage = msg["data"]
|
||||||
if self.filter is None or self.filter(_msg):
|
self._data.append(_msg)
|
||||||
self._display_queue.append(self._line_formatter(_msg))
|
if self.filter is None or self.filter(_msg):
|
||||||
if self._new_message_signal:
|
self._display_queue.append(self._line_formatter(_msg))
|
||||||
self._new_message_signal.emit()
|
if self._new_message_signal:
|
||||||
|
self._new_message_signal.emit()
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Error in LogPanel incoming message callback!")
|
||||||
|
|
||||||
def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
|
def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
|
||||||
self._line_formatter: LineFormatter = line_formatter
|
self._line_formatter: LineFormatter = line_formatter
|
||||||
@ -266,20 +269,21 @@ class LogPanelToolbar(QWidget):
|
|||||||
self._dt_dialog = QDialog(self)
|
self._dt_dialog = QDialog(self)
|
||||||
self._dt_dialog.setWindowTitle("Time range selection")
|
self._dt_dialog.setWindowTitle("Time range selection")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
self._dt_dialog.setLayout(layout)
|
||||||
|
|
||||||
label_start = QLabel()
|
label_start = QLabel(parent=self._dt_dialog)
|
||||||
label_end = QLabel()
|
label_end = QLabel(parent=self._dt_dialog)
|
||||||
|
|
||||||
def date_button_set(selection_type: Literal["start", "end"], label: QLabel):
|
def date_button_set(selection_type: Literal["start", "end"], label: QLabel):
|
||||||
dt = self._current_ts(selection_type)
|
dt = self._current_ts(selection_type)
|
||||||
_layout = QHBoxLayout()
|
_layout = QHBoxLayout()
|
||||||
layout.addLayout(_layout)
|
layout.addLayout(_layout)
|
||||||
date_button = QPushButton(f"Time {selection_type}")
|
date_button = QPushButton(f"Time {selection_type}", parent=self._dt_dialog)
|
||||||
_layout.addWidget(date_button)
|
_layout.addWidget(date_button)
|
||||||
label.setText(dt.toString() if dt else "not selected")
|
label.setText(dt.toString() if dt else "not selected")
|
||||||
_layout.addWidget(label)
|
_layout.addWidget(label)
|
||||||
date_button.clicked.connect(partial(self._open_cal_dialog, selection_type, label))
|
date_button.clicked.connect(partial(self._open_cal_dialog, selection_type, label))
|
||||||
date_clear_button = QPushButton("clear")
|
date_clear_button = QPushButton("clear", parent=self._dt_dialog)
|
||||||
date_clear_button.clicked.connect(
|
date_clear_button.clicked.connect(
|
||||||
lambda: (
|
lambda: (
|
||||||
partial(self._update_time, selection_type)(None),
|
partial(self._update_time, selection_type)(None),
|
||||||
@ -291,12 +295,12 @@ class LogPanelToolbar(QWidget):
|
|||||||
for v in [("start", label_start), ("end", label_end)]:
|
for v in [("start", label_start), ("end", label_end)]:
|
||||||
date_button_set(*v)
|
date_button_set(*v)
|
||||||
|
|
||||||
close_button = QPushButton("Close")
|
close_button = QPushButton("Close", parent=self._dt_dialog)
|
||||||
close_button.clicked.connect(self._dt_dialog.accept)
|
close_button.clicked.connect(self._dt_dialog.accept)
|
||||||
layout.addWidget(close_button)
|
layout.addWidget(close_button)
|
||||||
self._dt_dialog.setLayout(layout)
|
|
||||||
self._dt_dialog.exec()
|
self._dt_dialog.exec()
|
||||||
self._dt_dialog = None
|
self._dt_dialog.deleteLater()
|
||||||
|
|
||||||
def _open_cal_dialog(self, selection_type: Literal["start", "end"], label: QLabel):
|
def _open_cal_dialog(self, selection_type: Literal["start", "end"], label: QLabel):
|
||||||
"""Open dialog window for timestamp filter selection"""
|
"""Open dialog window for timestamp filter selection"""
|
||||||
@ -309,18 +313,19 @@ class LogPanelToolbar(QWidget):
|
|||||||
self._cal_dialog = QDialog(self)
|
self._cal_dialog = QDialog(self)
|
||||||
self._cal_dialog.setWindowTitle(f"Select time range {selection_type}")
|
self._cal_dialog.setWindowTitle(f"Select time range {selection_type}")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
cal = QDateTimeEdit()
|
self._cal_dialog.setLayout(layout)
|
||||||
|
cal = QDateTimeEdit(parent=self._cal_dialog)
|
||||||
cal.setCalendarPopup(True)
|
cal.setCalendarPopup(True)
|
||||||
cal.setDateTime(dt)
|
cal.setDateTime(dt)
|
||||||
cal.setDisplayFormat("yyyy-MM-dd HH:mm:ss.zzz")
|
cal.setDisplayFormat("yyyy-MM-dd HH:mm:ss.zzz")
|
||||||
cal.dateTimeChanged.connect(partial(self._update_time, selection_type))
|
cal.dateTimeChanged.connect(partial(self._update_time, selection_type))
|
||||||
layout.addWidget(cal)
|
layout.addWidget(cal)
|
||||||
close_button = QPushButton("Close")
|
close_button = QPushButton("Close", parent=self._cal_dialog)
|
||||||
close_button.clicked.connect(self._cal_dialog.accept)
|
close_button.clicked.connect(self._cal_dialog.accept)
|
||||||
layout.addWidget(close_button)
|
layout.addWidget(close_button)
|
||||||
self._cal_dialog.setLayout(layout)
|
|
||||||
self._cal_dialog.exec()
|
self._cal_dialog.exec()
|
||||||
self._cal_dialog = None
|
self._cal_dialog.deleteLater()
|
||||||
|
|
||||||
def _update_time(self, selection_type: Literal["start", "end"], dt: QDateTime | None):
|
def _update_time(self, selection_type: Literal["start", "end"], dt: QDateTime | None):
|
||||||
if selection_type == "start":
|
if selection_type == "start":
|
||||||
@ -344,7 +349,9 @@ class LogPanelToolbar(QWidget):
|
|||||||
self._svc_dialog = QDialog(self)
|
self._svc_dialog = QDialog(self)
|
||||||
self._svc_dialog.setWindowTitle(f"Select services to show logs from")
|
self._svc_dialog.setWindowTitle(f"Select services to show logs from")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
service_cb_grid = QGridLayout(parent=self)
|
self._svc_dialog.setLayout(layout)
|
||||||
|
|
||||||
|
service_cb_grid = QGridLayout(parent=self._svc_dialog)
|
||||||
layout.addLayout(service_cb_grid)
|
layout.addLayout(service_cb_grid)
|
||||||
|
|
||||||
def check_box(name: str, checked: Qt.CheckState):
|
def check_box(name: str, checked: Qt.CheckState):
|
||||||
@ -356,18 +363,18 @@ class LogPanelToolbar(QWidget):
|
|||||||
self.services_selected.emit(self._services_selected)
|
self.services_selected.emit(self._services_selected)
|
||||||
|
|
||||||
for i, svc in enumerate(self._unique_service_names):
|
for i, svc in enumerate(self._unique_service_names):
|
||||||
service_cb_grid.addWidget(QLabel(svc), i, 0)
|
service_cb_grid.addWidget(QLabel(svc, parent=self._svc_dialog), i, 0)
|
||||||
cb = QCheckBox()
|
cb = QCheckBox(parent=self._svc_dialog)
|
||||||
cb.setChecked(svc in self._services_selected)
|
cb.setChecked(svc in self._services_selected)
|
||||||
cb.checkStateChanged.connect(partial(check_box, svc))
|
cb.checkStateChanged.connect(partial(check_box, svc))
|
||||||
service_cb_grid.addWidget(cb, i, 1)
|
service_cb_grid.addWidget(cb, i, 1)
|
||||||
|
|
||||||
close_button = QPushButton("Close")
|
close_button = QPushButton("Close", parent=self._svc_dialog)
|
||||||
close_button.clicked.connect(self._svc_dialog.accept)
|
close_button.clicked.connect(self._svc_dialog.accept)
|
||||||
layout.addWidget(close_button)
|
layout.addWidget(close_button)
|
||||||
self._svc_dialog.setLayout(layout)
|
|
||||||
self._svc_dialog.exec()
|
self._svc_dialog.exec()
|
||||||
self._svc_dialog = None
|
self._svc_dialog.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
class LogPanel(TextBox):
|
class LogPanel(TextBox):
|
||||||
@ -377,11 +384,17 @@ class LogPanel(TextBox):
|
|||||||
_new_messages = Signal()
|
_new_messages = Signal()
|
||||||
service_list_update = Signal(dict, set)
|
service_list_update = Signal(dict, set)
|
||||||
|
|
||||||
def __init__(self, parent=None, client: BECClient | None = None, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
client: BECClient | None = None,
|
||||||
|
service_status: BECServiceStatusMixin | None = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""Initialize the LogPanel widget."""
|
"""Initialize the LogPanel widget."""
|
||||||
super().__init__(parent=parent, client=client, **kwargs)
|
super().__init__(parent=parent, client=client, **kwargs)
|
||||||
self._update_colors()
|
self._update_colors()
|
||||||
self._service_status = BECServiceStatusMixin(self, client=self.client) # type: ignore
|
self._service_status = service_status or BECServiceStatusMixin(self, client=self.client) # type: ignore
|
||||||
self._log_manager = BecLogsQueue(
|
self._log_manager = BecLogsQueue(
|
||||||
self.client.connector, # type: ignore
|
self.client.connector, # type: ignore
|
||||||
new_message_signal=self._new_messages,
|
new_message_signal=self._new_messages,
|
||||||
@ -500,7 +513,6 @@ class LogPanel(TextBox):
|
|||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self._log_manager.disconnect()
|
self._log_manager.disconnect()
|
||||||
self._new_messages.disconnect(self._on_append)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
@ -15,14 +15,15 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bec_ipython_client>=2.21.4, <=4.0", # needed for jupyter console
|
"bec_ipython_client>=2.21.4, <=4.0", # needed for jupyter console
|
||||||
"bec_lib>=2.21.4, <=4.0",
|
"bec_lib>=2.21.4, <=4.0",
|
||||||
|
"bec_qthemes~=0.7, >=0.7",
|
||||||
"black~=24.0", # needed for bw-generate-cli
|
"black~=24.0", # needed for bw-generate-cli
|
||||||
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
||||||
"pydantic~=2.0",
|
"pydantic~=2.0",
|
||||||
"pyqtgraph~=0.13",
|
"pyqtgraph~=0.13",
|
||||||
"bec_qthemes~=0.7, >=0.7",
|
"PySide6==6.7.2",
|
||||||
|
"pyte", # needed for vt100 console
|
||||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||||
"qtpy~=2.4",
|
"qtpy~=2.4",
|
||||||
"pyte", # needed for vt100 console
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -37,7 +38,6 @@ dev = [
|
|||||||
"pytest-xvfb~=3.0",
|
"pytest-xvfb~=3.0",
|
||||||
"pytest~=8.0",
|
"pytest~=8.0",
|
||||||
]
|
]
|
||||||
pyside6 = ["PySide6==6.7.2"]
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Bug Tracker" = "https://gitlab.psi.ch/bec/bec_widgets/issues"
|
"Bug Tracker" = "https://gitlab.psi.ch/bec/bec_widgets/issues"
|
||||||
|
@ -5,8 +5,13 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from bec_lib.messages import LogMessage
|
from bec_lib.messages import LogMessage
|
||||||
|
from qtpy.QtCore import QDateTime, Qt, Signal # type: ignore
|
||||||
|
|
||||||
from bec_widgets.widgets.utility.logpanel._util import replace_escapes, simple_color_format
|
from bec_widgets.widgets.utility.logpanel._util import (
|
||||||
|
log_time,
|
||||||
|
replace_escapes,
|
||||||
|
simple_color_format,
|
||||||
|
)
|
||||||
from bec_widgets.widgets.utility.logpanel.logpanel import DEFAULT_LOG_COLORS, LogPanel
|
from bec_widgets.widgets.utility.logpanel.logpanel import DEFAULT_LOG_COLORS, LogPanel
|
||||||
|
|
||||||
from .client_mocks import mocked_client
|
from .client_mocks import mocked_client
|
||||||
@ -19,7 +24,7 @@ TEST_LOG_MESSAGES = [
|
|||||||
log_type="debug",
|
log_type="debug",
|
||||||
log_msg={
|
log_msg={
|
||||||
"text": "datetime | debug | test log message",
|
"text": "datetime | debug | test log message",
|
||||||
"record": {},
|
"record": {"time": {"timestamp": 123456789.000}},
|
||||||
"service_name": "ScanServer",
|
"service_name": "ScanServer",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -28,7 +33,7 @@ TEST_LOG_MESSAGES = [
|
|||||||
log_type="info",
|
log_type="info",
|
||||||
log_msg={
|
log_msg={
|
||||||
"text": "datetime | info | test log message",
|
"text": "datetime | info | test log message",
|
||||||
"record": {},
|
"record": {"time": {"timestamp": 123456789.007}},
|
||||||
"service_name": "ScanServer",
|
"service_name": "ScanServer",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -37,7 +42,7 @@ TEST_LOG_MESSAGES = [
|
|||||||
log_type="success",
|
log_type="success",
|
||||||
log_msg={
|
log_msg={
|
||||||
"text": "datetime | success | test log message",
|
"text": "datetime | success | test log message",
|
||||||
"record": {},
|
"record": {"time": {"timestamp": 123456789.012}},
|
||||||
"service_name": "ScanServer",
|
"service_name": "ScanServer",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -53,7 +58,7 @@ def raw_queue():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def log_panel(qtbot, mocked_client: MagicMock):
|
def log_panel(qtbot, mocked_client: MagicMock):
|
||||||
widget = LogPanel(client=mocked_client)
|
widget = LogPanel(client=mocked_client, service_status=MagicMock())
|
||||||
qtbot.addWidget(widget)
|
qtbot.addWidget(widget)
|
||||||
qtbot.waitExposed(widget)
|
qtbot.waitExposed(widget)
|
||||||
yield widget
|
yield widget
|
||||||
@ -115,3 +120,14 @@ def test_clear_button(log_panel: LogPanel):
|
|||||||
log_panel._log_manager._data = deque(TEST_LOG_MESSAGES)
|
log_panel._log_manager._data = deque(TEST_LOG_MESSAGES)
|
||||||
log_panel.toolbar.clear_button.click()
|
log_panel.toolbar.clear_button.click()
|
||||||
assert log_panel._log_manager._data == deque([])
|
assert log_panel._log_manager._data == deque([])
|
||||||
|
|
||||||
|
|
||||||
|
def test_timestamp_filter(log_panel: LogPanel):
|
||||||
|
log_panel._log_manager._timestamp_start = QDateTime(1973, 11, 29, 21, 33, 9, 5, 1)
|
||||||
|
pytest.approx(log_panel._log_manager._timestamp_start.toMSecsSinceEpoch() / 1000, 123456789.005)
|
||||||
|
log_panel._log_manager._timestamp_end = QDateTime(1973, 11, 29, 21, 33, 9, 10, 1)
|
||||||
|
pytest.approx(log_panel._log_manager._timestamp_end.toMSecsSinceEpoch() / 1000, 123456789.010)
|
||||||
|
filter_ = log_panel._log_manager._create_timestamp_filter()
|
||||||
|
assert not filter_(TEST_LOG_MESSAGES[0])
|
||||||
|
assert filter_(TEST_LOG_MESSAGES[1])
|
||||||
|
assert not filter_(TEST_LOG_MESSAGES[2])
|
||||||
|
Reference in New Issue
Block a user