mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 21:13:41 +08:00
80fbb4f6db
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Daniel Koepping <elkoled@gmail.com> Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com> Co-authored-by: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: stef <19478336+stefpi@users.noreply.github.com> Co-authored-by: whoisdomi <dcantu83@gmail.com> Co-authored-by: Intelli <contact@intelli.software> Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
282 lines
15 KiB
Python
282 lines
15 KiB
Python
"""Replay large-panel input through real widgets with graphics and effects isolated."""
|
|
from unittest.mock import patch
|
|
|
|
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
import tempfile
|
|
from types import SimpleNamespace as NS
|
|
import unittest
|
|
from unittest.mock import Mock
|
|
|
|
import pyray as rl
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.selfdrive.ui.layouts.settings import device, software
|
|
from openpilot.selfdrive.ui.layouts.settings.settings import PanelType
|
|
from openpilot.starpilot.software.preferences import automatic_downloads
|
|
from openpilot.starpilot.ui import runtime_app
|
|
from openpilot.starpilot.ui.device_state import DeviceState
|
|
from openpilot.starpilot.ui.presentation import Profile
|
|
from openpilot.starpilot.ui.preview_home import reference_state
|
|
from openpilot.starpilot.ui.onroad_state import OnroadState, SpeedLimitObservation
|
|
from openpilot.starpilot.ui.settings_state import Destination, SettingsState
|
|
from openpilot.starpilot.ui.shell import ShellInput, ShellMode, ShellSnapshot
|
|
from openpilot.starpilot.ui.software_state import SoftwareState
|
|
from openpilot.system.ui.lib.application import gui_app, MouseEvent, MousePos
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
from openpilot.system.ui.widgets import list_view
|
|
from openpilot.system.ui.widgets.label import Label
|
|
|
|
|
|
class NativeSettingsTests(unittest.TestCase):
|
|
def setUp(self):
|
|
directory = tempfile.TemporaryDirectory()
|
|
self.addCleanup(directory.cleanup)
|
|
self.params = Params(directory.name)
|
|
self.params.put('UpdaterState', 'idle', block=True)
|
|
self.params.put('UpdaterAvailableBranches', 'SecretGoodStarPilot,Dom', block=True)
|
|
self.params.put('GitBranch', 'SecretGoodStarPilot', block=True)
|
|
self.offroad = True
|
|
self.ui = NS(params=self.params, is_offroad=lambda: self.offroad, is_onroad=lambda: not self.offroad,
|
|
prime_state=NS(is_paired=lambda: False), engaged=False, started=False, started_frame=0,
|
|
add_offroad_transition_callback=Mock())
|
|
self.enterContext(patch.object(device, 'ui_state', self.ui))
|
|
self.enterContext(patch.object(software, 'ui_state', self.ui))
|
|
self.enterContext(patch.object(runtime_app, 'ui_state', self.ui))
|
|
self.enterContext(patch.object(device, 'Params', return_value=self.params))
|
|
self.enterContext(patch.object(gui_app, 'font', return_value=rl.Font()))
|
|
self.enterContext(patch.object(gui_app, '_mouse_events', []))
|
|
self.enterContext(patch.object(gui_app, '_show_touches', False))
|
|
self.pushed = self.enterContext(patch.object(gui_app, 'push_widget'))
|
|
self.enterContext(patch.object(list_view, 'measure_text_cached', return_value=rl.Vector2(100, 50)))
|
|
self.enterContext(patch.object(list_view, 'gui_label'))
|
|
self.enterContext(patch.object(list_view, 'HtmlRenderer', side_effect=lambda *a, **kw: Mock(elements=[])))
|
|
self.enterContext(patch.object(Label, '_render'))
|
|
self.enterContext(patch('openpilot.system.ui.widgets.device', NS(awake=True)))
|
|
for name in ('draw_rectangle_rounded', 'draw_circle', 'draw_line', 'draw_text_ex', 'begin_scissor_mode', 'end_scissor_mode'):
|
|
self.enterContext(patch.object(rl, name))
|
|
self.enterContext(patch.object(rl, 'get_mouse_wheel_move', return_value=0))
|
|
self.enterContext(patch.object(rl, 'get_frame_time', return_value=1 / 60))
|
|
self.software = software.SoftwareLayout()
|
|
self.device = device.DeviceLayout()
|
|
self.bounds = rl.Rectangle(550, 25, 1560, 1030)
|
|
self.snapshot = ShellSnapshot(ShellMode.SETTINGS, reference_state(), SettingsState(),
|
|
OnroadState(False, False, None, None, SpeedLimitObservation()),
|
|
device=DeviceState(offroad=False, available_actions=frozenset()),
|
|
software=SoftwareState(available_actions=frozenset()))
|
|
|
|
def event(self, panel, x, y, *, pressed=False, down=False, released=False, t=1):
|
|
gui_app._mouse_events = [MouseEvent(MousePos(x, y), 0, pressed, released, down, t)]
|
|
panel.render(self.bounds)
|
|
gui_app._mouse_events = []
|
|
|
|
def click(self, panel, item):
|
|
panel.render(self.bounds)
|
|
rect = item.get_right_item_rect(item.rect)
|
|
point = rect.x + rect.width - 100, item.rect.y + 85
|
|
self.event(panel, *point, pressed=True, down=True)
|
|
self.event(panel, *point, released=True, t=1.1)
|
|
|
|
def test_desk_buttons_and_scrolling_use_native_widgets(self):
|
|
with patch.object(software.subprocess, 'run') as signal:
|
|
self.click(self.software, self.software._download_btn)
|
|
signal.assert_any_call("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
|
self.assertEqual(sum(call.args == ("pkill -SIGUSR1 -f openpilot.system.updated.updated",) for call in signal.call_args_list), 1)
|
|
self.software._waiting_for_updater = False
|
|
with patch.object(software, 'MultiOptionDialog', return_value='branch chooser'):
|
|
self.click(self.software, self.software._branch_btn)
|
|
self.pushed.assert_called_once_with('branch chooser')
|
|
camera = next(item for item in self.device._scroller._items if item.title == 'Cabin Camera')
|
|
with patch.object(device, 'CabinCameraDialog', return_value='camera'):
|
|
self.click(self.device, camera)
|
|
self.pushed.assert_called_with('camera')
|
|
for panel in (self.software, self.device):
|
|
with self.subTest(panel=type(panel).__name__):
|
|
panel.show_event()
|
|
panel.render(self.bounds)
|
|
first = next(item for item in panel._scroller._items if item.is_visible)
|
|
before = first.rect.y
|
|
self.event(panel, 1000, 800, pressed=True, down=True)
|
|
self.event(panel, 1000, 770, down=True, t=1.1)
|
|
self.event(panel, 1000, 650, down=True, t=1.2)
|
|
self.assertLess(first.rect.y, before)
|
|
self.assertFalse(panel._scroller.scroll_panel.is_touch_valid())
|
|
panel.hide_event()
|
|
panel.show_event()
|
|
self.assertEqual(panel._scroller.scroll_panel.offset, 0)
|
|
|
|
def test_restored_controls_and_confirmation_recheck(self):
|
|
self.click(self.software, self.software._auto_updates_toggle)
|
|
self.assertFalse(automatic_downloads(self.params))
|
|
self.assertFalse(self.params.get_bool('DisableUpdates'))
|
|
with patch.object(software.Path, 'read_text', return_value='example failure'), patch.object(software, 'ConfirmDialog') as dialog:
|
|
self.software._on_error_log()
|
|
self.assertEqual(dialog.call_args.args[0], 'example failure')
|
|
with patch.object(device, 'ConfirmDialog') as dialog:
|
|
self.device._reset_driver_monitoring_prompt()
|
|
confirm = dialog.call_args.kwargs['callback']
|
|
self.params.put_bool('IsRhdDetected', True, block=True)
|
|
self.offroad = False
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertTrue(self.params.get_bool('IsRhdDetected'))
|
|
self.offroad = True
|
|
confirm(DialogResult.CANCEL)
|
|
self.assertTrue(self.params.get_bool('IsRhdDetected'))
|
|
self.params.put_bool('IsDriverViewEnabled', True, block=True)
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertTrue(self.params.get_bool('IsRhdDetected'))
|
|
self.params.put_bool('IsDriverViewEnabled', False, block=True)
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertFalse(Path(self.params.get_param_path('IsRhdDetected')).exists())
|
|
self.assertTrue(self.params.get_bool('OnroadCycleRequested'))
|
|
|
|
def test_software_effects_recheck_native_offroad_state(self):
|
|
with patch.object(software, 'ConfirmDialog') as dialog:
|
|
self.software._on_uninstall()
|
|
uninstall = dialog.call_args.kwargs['callback']
|
|
with patch.object(software, 'MultiOptionDialog') as dialog:
|
|
self.software._on_select_branch()
|
|
branch = dialog.call_args.kwargs['callback']
|
|
self.enterContext(patch.object(self.software._branch_dialog, 'selection', 'Dom', create=True))
|
|
self.params.put_bool('UpdateAvailable', True, block=True)
|
|
self.offroad = False
|
|
with patch.object(software.subprocess, 'run') as signal:
|
|
self.software._on_download_update()
|
|
self.software._on_install_update()
|
|
self.software._on_auto_updates_toggle(False)
|
|
uninstall(DialogResult.CONFIRM)
|
|
branch(DialogResult.CONFIRM)
|
|
signal.assert_not_called()
|
|
self.assertFalse(self.params.get_bool('DoReboot'))
|
|
self.assertFalse(self.params.get_bool('DoUninstall'))
|
|
self.assertFalse(self.params.get('UpdaterTargetBranch'))
|
|
self.assertTrue(automatic_downloads(self.params))
|
|
|
|
def test_native_leaf_input_cannot_also_emit_shell_requests(self):
|
|
requests = []
|
|
handler = ShellInput(Profile.LARGE, requests.append, native_panels=(Destination.DEVICE, Destination.SOFTWARE))
|
|
for destination in (Destination.DEVICE, Destination.SOFTWARE):
|
|
snapshot = replace(self.snapshot, selected=destination)
|
|
handler.press(1980, 650, 1, snapshot)
|
|
handler.move(1985, 650, 1.1, snapshot)
|
|
handler.release(1985, 650, 1.2, snapshot)
|
|
self.assertEqual(requests, [])
|
|
handler.press(250, 160, 2, snapshot)
|
|
handler.release(250, 160, 2.1, snapshot)
|
|
self.assertEqual(requests[0].source, 'settings')
|
|
|
|
def test_session_mounts_native_panels_inside_shared_rail(self):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session.slc_actions = None
|
|
session.profile = Profile.LARGE
|
|
session.view = Mock()
|
|
session.favorites = Mock()
|
|
session.pip_warning = Mock()
|
|
session.pip_renderer = Mock()
|
|
session.notice = ''
|
|
session.network_layer = None
|
|
with patch.object(runtime_app, 'placed_at'), patch.object(software.subprocess, 'run') as signal, \
|
|
patch.object(device, 'CabinCameraDialog', return_value='camera'):
|
|
for destination, panel in ((Destination.DEVICE, self.device), (Destination.SOFTWARE, self.software)):
|
|
with self.subTest(destination=destination):
|
|
session.snapshot = Mock(return_value=replace(self.snapshot, selected=destination))
|
|
session.settings_layer = Mock(side_effect=lambda dest, rect, panel=panel: panel.render(rect))
|
|
session.render(ShellMode.SETTINGS, rl.Rectangle(0, 0, 2160, 1080))
|
|
session.settings_layer.assert_called_once()
|
|
self.assertEqual(session.view.render.call_args.args[0].selected, Destination.NETWORK)
|
|
self.assertEqual(panel.rect.width, 1560)
|
|
# Replay the page too: missing parked evidence must not swallow native desk touches.
|
|
session.selected = destination
|
|
emitted = Mock()
|
|
session.input = ShellInput(Profile.LARGE, emitted, native_panels=runtime_app.NATIVE_SETTINGS_PANELS)
|
|
page = runtime_app.StarShellPage(session, ShellMode.SETTINGS)
|
|
item = (self.software._download_btn if destination == Destination.SOFTWARE else
|
|
next(item for item in self.device._scroller._items if item.title == 'Cabin Camera'))
|
|
rect = item.get_right_item_rect(item.rect)
|
|
point = MousePos(rect.x + rect.width - 100, item.rect.y + 85)
|
|
for pressed, released in ((True, False), (False, True)):
|
|
gui_app._mouse_events = [MouseEvent(point, 0, pressed, released, pressed, 1)]
|
|
page.render(rl.Rectangle(0, 0, 2160, 1080))
|
|
gui_app._mouse_events = []
|
|
emitted.assert_not_called()
|
|
signal.assert_any_call("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
|
self.assertEqual(sum(call.args == ("pkill -SIGUSR1 -f openpilot.system.updated.updated",) for call in signal.call_args_list), 1)
|
|
self.pushed.assert_called_once_with('camera')
|
|
|
|
def test_layout_reuses_native_panels_and_preserves_galaxy_entry(self):
|
|
toggles = NS(_scroller=NS(_items=[NS(title=str(index)) for index in range(9)]))
|
|
panels = {PanelType.DEVICE: NS(instance=self.device), PanelType.SOFTWARE: NS(instance=self.software),
|
|
PanelType.DEVELOPER: NS(instance=Mock()), PanelType.NETWORK: NS(instance=Mock()), PanelType.TOGGLES: NS(instance=toggles)}
|
|
session = Mock()
|
|
session.fonts.profile = Profile.LARGE
|
|
|
|
def parent_init(layout):
|
|
layout._layouts = {runtime_app.MainState.SETTINGS: NS(_panels=panels), runtime_app.MainState.ONROAD: Mock()}
|
|
|
|
with patch.object(runtime_app.MainLayout, '__init__', parent_init), \
|
|
patch.object(runtime_app, 'validate_runtime_fonts'), patch.object(runtime_app, 'validate_runtime_assets'), \
|
|
patch.object(runtime_app, 'StarShellSession', return_value=session):
|
|
layout = runtime_app.StarMainLayout()
|
|
self.assertIs(layout._large_panels[Destination.DEVICE].panel, self.device)
|
|
self.assertIs(layout._large_panels[Destination.SOFTWARE].panel, self.software)
|
|
self.assertIs(layout._large_panels[Destination.TOGGLES].panel, toggles)
|
|
self.assertEqual(toggles._scroller._items[2].title, 'Safe Mode')
|
|
self.assertEqual(toggles._scroller._items[7].title, 'Right Hand Driving')
|
|
self.assertEqual(self.device._scroller._items[0].title, 'Galaxy')
|
|
self.assertNotIn(self.device._power_off_btn, self.device._scroller._items)
|
|
self.assertEqual(session.system_power_action, layout._system_power_action)
|
|
galaxy = next(item for item in self.device._scroller._items if item.title == 'Galaxy')
|
|
galaxy.callback()
|
|
session.galaxy_flow.open_large.assert_called_once_with()
|
|
|
|
def test_system_power_uses_native_confirmation_and_rechecks_before_effects(self):
|
|
put_bool = self.params.put_bool
|
|
self.enterContext(patch.object(self.params, 'put_bool', side_effect=lambda key, value: put_bool(key, value, block=True)))
|
|
layout = runtime_app.StarMainLayout.__new__(runtime_app.StarMainLayout)
|
|
layout._current_mode = runtime_app.MainState.SETTINGS
|
|
layout.star = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
layout.star._mode = ShellMode.SETTINGS
|
|
layout.star.selected = Destination.SYSTEM
|
|
layout._panel_owner = Mock(return_value=self.device)
|
|
self.enterContext(patch.object(layout, '_confirmed_offroad', side_effect=lambda: self.offroad))
|
|
for index, key in enumerate(("DoReboot", "DoShutdown")):
|
|
with self.subTest(key=key), patch.object(device, 'ConfirmDialog') as dialog:
|
|
layout._system_power_action(index)
|
|
confirm = dialog.call_args.kwargs['callback']
|
|
confirm(DialogResult.CANCEL)
|
|
self.assertFalse(self.params.get_bool(key))
|
|
self.ui.engaged = True
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertFalse(self.params.get_bool(key))
|
|
self.ui.engaged = False
|
|
layout.star.selected = Destination.STAR
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertFalse(self.params.get_bool(key))
|
|
layout.star.selected = Destination.SYSTEM
|
|
layout.star.display_page = "display"
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertFalse(self.params.get_bool(key))
|
|
layout.star.display_page = "system"
|
|
layout._current_mode = runtime_app.MainState.HOME
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertFalse(self.params.get_bool(key))
|
|
layout._current_mode = runtime_app.MainState.SETTINGS
|
|
if index == 1:
|
|
self.offroad = False
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertFalse(self.params.get_bool(key))
|
|
self.offroad = True
|
|
confirm(DialogResult.CONFIRM)
|
|
self.assertTrue(self.params.get_bool(key))
|
|
with patch.object(device, 'ConfirmDialog') as dialog:
|
|
self.offroad = False
|
|
layout._system_power_action(1)
|
|
layout._system_power_action(-1)
|
|
layout._system_power_action(2)
|
|
dialog.assert_not_called()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|