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
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>
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
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
|