import pyray as rl rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) import gc import weakref import pytest from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget # mici dialogs from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal # tici dialogs from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.system.ui.widgets.html_render import HtmlModal from openpilot.system.ui.widgets.keyboard import Keyboard # FIXME: known small leaks not worth worrying about at the moment KNOWN_LEAKS = { "openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog.DriverCameraView", "openpilot.selfdrive.ui.mici.layouts.onboarding.TermsPage", "openpilot.selfdrive.ui.mici.layouts.onboarding.TrainingGuide", "openpilot.selfdrive.ui.mici.layouts.onboarding.DeclinePage", "openpilot.selfdrive.ui.mici.layouts.onboarding.OnboardingWindow", "openpilot.selfdrive.ui.onroad.driver_state.DriverStateRenderer", "openpilot.selfdrive.ui.onroad.driver_camera_dialog.DriverCameraDialog", "openpilot.selfdrive.ui.layouts.onboarding.TermsPage", "openpilot.selfdrive.ui.layouts.onboarding.DeclinePage", "openpilot.selfdrive.ui.layouts.onboarding.OnboardingWindow", "openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", "openpilot.system.ui.widgets.label.Label", "openpilot.system.ui.widgets.button.Button", "openpilot.system.ui.widgets.html_render.HtmlRenderer", "openpilot.system.ui.widgets.nav_widget.NavBar", "openpilot.selfdrive.ui.mici.layouts.settings.device.MiciFccModal", "openpilot.system.ui.widgets.inputbox.InputBox", "openpilot.system.ui.widgets.scroller_tici.Scroller", "openpilot.system.ui.widgets.label.UnifiedLabel", "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialog", "openpilot.system.ui.widgets.keyboard.Keyboard", "openpilot.system.ui.widgets.slider.BigSlider", "openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog", "openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", } def get_child_widgets(widget: Widget) -> list[Widget]: children = [] for val in widget.__dict__.values(): items = val if isinstance(val, (list, tuple)) else (val,) children.extend(w for w in items if isinstance(w, Widget)) return children @pytest.mark.skip(reason="segfaults") def test_dialogs_do_not_leak(): gui_app.init_window("ref-test") leaked_widgets = set() for ctor in ( # mici MiciDriverCameraDialog, MiciPairingDialog, lambda: MiciTrainingGuide(lambda: None), lambda: MiciOnboardingWindow(lambda: None), lambda: BigDialog("test", "test"), lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None), lambda: BigInputDialog("test"), lambda: MiciFccModal(text="test"), # tici TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, lambda: ConfirmDialog("test", "ok"), lambda: MultiOptionDialog("test", ["a", "b"]), lambda: HtmlModal(text="test"), ): widget = ctor() all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]] del widget for ref in all_refs: if ref() is not None: obj = ref() name = f"{type(obj).__module__}.{type(obj).__qualname__}" leaked_widgets.add(name) print(f"\n=== Widget {name} alive after del") print(" Referrers:") for r in gc.get_referrers(obj): if r is obj: continue if hasattr(r, '__self__') and r.__self__ is not obj: print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}") elif hasattr(r, '__func__'): print(f" method: {r.__name__}") else: print(f" {type(r).__module__}.{type(r).__qualname__}") del obj gui_app.close() unexpected = leaked_widgets - KNOWN_LEAKS assert not unexpected, f"New leaked widgets: {unexpected}" fixed = KNOWN_LEAKS - leaked_widgets assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}" if __name__ == "__main__": test_dialogs_do_not_leak()