From b61f57c2e0aab7350e4ea45aa2aa3b577a6264a0 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Mon, 5 Oct 2026 16:47:22 +0200 Subject: [PATCH] test(icons): guard against the material icon segfault in cyclic garbage Runs the device manager's icon caches through cyclic garbage collection in a child process. It segfaults (SIGSEGV in the QIcon destructor) with bec_qthemes releases that lack the icon engine ownership fix, and passes once bec_widgets requires a fixed bec_qthemes. --- .../test_material_icon_cycle_exit_crash.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/unit_tests/test_material_icon_cycle_exit_crash.py diff --git a/tests/unit_tests/test_material_icon_cycle_exit_crash.py b/tests/unit_tests/test_material_icon_cycle_exit_crash.py new file mode 100644 index 00000000..81465137 --- /dev/null +++ b/tests/unit_tests/test_material_icon_cycle_exit_crash.py @@ -0,0 +1,62 @@ +""" +F18: segfault in the QIcon destructor when material icons held by a widget are collected as +cyclic garbage (seen at interpreter exit after DeviceManagerWidget or BECMainApp tests). + +The crash happens in a child process so that it turns into a failing test instead of killing +pytest. The child uses the real icon dictionaries of the device manager components, which are +built as dict literals (icons first, dict afterwards) - the order that makes the garbage +collector clear the icon engine before its QIcon. +""" + +import os +import subprocess +import sys +import textwrap + +import pytest + +_SCRIPT = textwrap.dedent(""" + import faulthandler + import gc + import os + + faulthandler.enable(all_threads=False) + os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + + from qtpy.QtWidgets import QApplication + + app = QApplication([]) + + from bec_widgets.utils.colors import get_accent_colors + from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import ( + get_validation_icons, + ) + + + class IconOwner: + '''Stands in for a widget that caches icons and ends up in a reference cycle.''' + + + # Icons first, owner afterwards: the collector then visits every icon engine before the + # QIcon and the dicts that hold it, as it did for the DeviceTable/OphydValidation icon caches + # at interpreter exit. + icons = get_validation_icons(get_accent_colors(), (18, 18)) + owner = IconOwner() + owner._icons = icons + owner.cycle = owner + del owner, icons + gc.collect() + print("collected", flush=True) + """) + + +@pytest.mark.timeout(180) +def test_material_icons_in_cyclic_garbage_do_not_crash(): + """Collecting a cycle that holds material QIcons must not segfault.""" + env = dict(os.environ, QT_QPA_PLATFORM="offscreen") + proc = subprocess.run( + [sys.executable, "-c", _SCRIPT], capture_output=True, text=True, timeout=150, env=env + ) + assert ( + proc.returncode == 0 and "collected" in proc.stdout + ), f"child exited with {proc.returncode}\n{proc.stdout[-2000:]}\n{proc.stderr[:4000]}"