Files
StarPilot/selfdrive/ui/mici/tests/test_widget_leaks.py
T
firestar5683 d43b7d0d3f UI
2026-04-11 23:49:51 -05:00

120 lines
4.9 KiB
Python
Executable File

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()