Files
AareDAQ/tests/unit/gui/test_title_label.py
duan_jandClaude Fable 5 26fbe2ca42
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Successful in 1m2s
CI / test (3.12) (pull_request) Successful in 1m8s
CI / test (3.14) (pull_request) Successful in 1m7s
CI / test (3.13) (pull_request) Successful in 1m16s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m21s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m32s
CI / test-with-coverage (pull_request) Successful in 1m37s
CI / coverage-analysis (pull_request) Successful in 3s
fix: give deferred singleShot callbacks a receiver context
Root cause of the long-standing cross-test SystemError flake
("QPushButton returned NULL", CI failing test_title_label after an
unrelated file): TitleLabel schedules
QTimer.singleShot(0, self._apply_collapsed) at construction; when the
banner is deleted before the event loop spins (what every panel test
does), the pending timer fires into the dead C++ object and poisons
whatever test runs next. Deterministic repro added as a regression
test. The receiver-context overload singleShot(0, self, ...) auto-
cancels the callback when the widget dies. Same fix for the two
deferred main-window callbacks (dock split, floating-dock enlarge).

Note for the record: the LOCAL segfault storm this afternoon was the
dev machine's disk hitting 100% full (uv cache at 15G) — Bus errors
from mmap on a full disk, unrelated to any code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-16 13:29:07 +02:00

92 lines
3.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from PySide6.QtCore import QSettings
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QPushButton, QWidget
from aare.gui.widgets.title_label import TitleLabel
# Unique title so the test never clashes with real panel settings.
TITLE = "TitleLabelTestPanel"
KEY = f"panel_collapsed/{TITLE}"
def _remove_key():
QSettings("PSI", "AareGUI").remove(KEY)
def _build_panel(qtbot):
panel = QWidget()
qtbot.addWidget(panel)
grid = QGridLayout(panel)
title = TitleLabel(TITLE, panel, collapsible=True)
grid.addWidget(title, 0, 0, 1, 2)
direct_child = QPushButton("direct", panel)
grid.addWidget(direct_child, 1, 0)
nested = QHBoxLayout()
nested_child = QPushButton("nested", panel)
nested.addWidget(nested_child)
grid.addLayout(nested, 1, 1)
return panel, title, direct_child, nested_child
def test_starts_collapsed_by_default_and_toggle_persists(qtbot):
_remove_key()
try:
_panel, title, direct_child, nested_child = _build_panel(qtbot)
# Default is collapsed; applied deferred with a 0 ms timer (siblings
# don't exist yet at TitleLabel construction).
qtbot.waitUntil(lambda: direct_child.isHidden(), timeout=1000)
assert nested_child.isHidden()
assert not title.isHidden()
assert title.toggle_button.text() == "+"
title.toggle_collapsed()
assert not direct_child.isHidden()
assert not nested_child.isHidden()
assert title.toggle_button.text() == ""
assert QSettings("PSI", "AareGUI").value(KEY, True, type=bool) is False
title.toggle_collapsed()
assert direct_child.isHidden()
assert title.toggle_button.text() == "+"
assert QSettings("PSI", "AareGUI").value(KEY, False, type=bool) is True
finally:
_remove_key()
def test_saved_expanded_state_restored_on_construction(qtbot):
QSettings("PSI", "AareGUI").setValue(KEY, False)
try:
_panel, title, direct_child, nested_child = _build_panel(qtbot)
# A saved expanded state must override the collapsed default; give the
# (absent) deferred collapse a chance to run before asserting.
qtbot.wait(100)
assert not direct_child.isHidden()
assert not nested_child.isHidden()
assert title.toggle_button.text() == ""
finally:
_remove_key()
def test_not_collapsible_by_default(qtbot):
panel = QWidget()
qtbot.addWidget(panel)
grid = QGridLayout(panel)
title = TitleLabel("Plain", panel)
grid.addWidget(title, 0, 0)
assert not hasattr(title, "toggle_button")
def test_pending_collapse_timer_dies_with_the_banner():
"""The deferred _apply_collapsed must not fire into a deleted banner —
that was the historic "QPushButton returned NULL" / SystemError flake
poisoning whichever test ran next (singleShot without receiver context
outlives the widget)."""
from PySide6.QtWidgets import QApplication
_remove_key()
host = QWidget()
TitleLabel("DoomedBanner", host, collapsible=True, default_collapsed=True)
# Python-owned host: del cascades the C++ delete to the banner while
# its singleShot(0) is still pending.
del host
QApplication.processEvents() # must not raise into the event loop