mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
961 lines
56 KiB
Python
961 lines
56 KiB
Python
"""Route live large-panel requests into native owners with effects isolated."""
|
|
from unittest.mock import patch, call
|
|
|
|
|
|
import atexit
|
|
import tempfile
|
|
from types import SimpleNamespace as NS
|
|
import unittest
|
|
from unittest.mock import Mock
|
|
|
|
from openpilot.common.params import Params
|
|
|
|
|
|
_param_dir = tempfile.TemporaryDirectory(prefix="starpilot-panel-owner-test-")
|
|
atexit.register(_param_dir.cleanup)
|
|
_original_params_init = Params.__init__
|
|
with patch.object(Params, "__init__", lambda self, d="": _original_params_init(self, _param_dir.name)):
|
|
from openpilot.starpilot.ui import runtime_app
|
|
from openpilot.selfdrive.ui.layouts.settings import device as native_device
|
|
from openpilot.selfdrive.ui.layouts.settings import software as native_software
|
|
from openpilot.selfdrive.ui.layouts.settings.settings import PanelType
|
|
|
|
from openpilot.starpilot.ui.device_state import DeviceAction, DeviceRequest
|
|
from openpilot.starpilot.ui.feature_settings import FeatureSettingsView
|
|
from openpilot.starpilot.ui.presentation import BitmapFonts, Profile
|
|
from openpilot.starpilot.ui.feature_settings_state import FEATURE_ROW_HEIGHT, FEATURE_ROW_TOP, FeaturePage, FeatureUiAction
|
|
from openpilot.starpilot.ui.toggles_state import ToggleRequest, ToggleKey
|
|
from openpilot.starpilot.ui.settings_state import Destination, SettingsAction, SettingsActionKind, tile_rects
|
|
from openpilot.starpilot.ui.shell import ShellInput, ShellMode, ShellRequest, ShellView
|
|
from openpilot.starpilot.ui.software_state import SoftwareAction, SoftwareRequest
|
|
from openpilot.starpilot.ui.tests.test_runtime_snapshot import BOOT_OFFSET_NS, NOW, ui_fake
|
|
from openpilot.system.ui.lib.application import gui_app, MouseEvent, MousePos
|
|
from openpilot.starpilot.galaxy.access import AccessStatus, GalaxyAccessOwner
|
|
from openpilot.starpilot.ui.galaxy_access import GalaxyAccessFlow, connection_url
|
|
from pathlib import Path
|
|
|
|
|
|
def make_feature_view(profile: Profile):
|
|
fonts = BitmapFonts.__new__(BitmapFonts)
|
|
fonts.profile = profile
|
|
return FeatureSettingsView(fonts)
|
|
|
|
|
|
class TestRuntimePanelActions(unittest.TestCase):
|
|
def test_live_slc_action_source_accepts_system_and_stock_long_but_rejects_stale(self):
|
|
ui = ui_fake()
|
|
ui.CP = NS(carFingerprint="TOYOTA_HIGHLANDER_TSS2", openpilotLongitudinalControl=True, pcmCruise=True)
|
|
ui.sm.messages["carState"].canValid = True
|
|
ui.sm.messages["carState"].canTimeout = False
|
|
ui.sm.messages["carControl"].longActive = True
|
|
ui.sm.messages["selfdriveState"].enabled = True
|
|
ui.sm.put("controlsState", NS(longControlState="pid"))
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session.adapter = runtime_app.RuntimeSnapshotAdapter(ui)
|
|
with patch("openpilot.starpilot.ui.runtime_snapshot._clock_pair", return_value=(NOW, NOW + BOOT_OFFSET_NS, BOOT_OFFSET_NS)):
|
|
self.assertIs(session._live_slc_message(NOW), ui.sm["slcState"])
|
|
ui.CP.openpilotLongitudinalControl = False
|
|
ui.sm.messages["carControl"].longActive = False
|
|
ui.sm.messages["selfdriveState"].enabled = False
|
|
ui.sm.put("controlsState", NS(longControlState="off"))
|
|
self.assertIs(session._live_slc_message(NOW), ui.sm["slcState"])
|
|
ui.sm.logMonoTime["slcState"] = NOW - 1_000_000_000
|
|
self.assertIsNone(session._live_slc_message(NOW))
|
|
|
|
def _vehicle_session(self, sidebar_expanded=True):
|
|
from opendbc.car import gen_empty_fingerprint
|
|
from opendbc.car.gm.interface import CarInterface
|
|
from opendbc.car.gm.values import CAR
|
|
directory = tempfile.TemporaryDirectory()
|
|
self.addCleanup(directory.cleanup)
|
|
self.ui.params = Params(directory.name)
|
|
self.ui.params.put_bool("LongPitch", False, block=True)
|
|
fingerprint = gen_empty_fingerprint()
|
|
fingerprint[1][0x460] = 8
|
|
self.ui.CP = CarInterface.get_params(CAR.CHEVROLET_SUBURBAN, fingerprint, [], False, False, False)
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session.profile = runtime_app.Profile.LARGE
|
|
session.adapter = self._adapter()
|
|
session._mode, session.selected = ShellMode.SETTINGS, Destination.STAR
|
|
session.compact_y = session.compact_scroll_x = 0
|
|
session.sidebar_expanded = sidebar_expanded
|
|
session.feature_page = session.feature_root_page = FeaturePage.HUB
|
|
session.feature_scroll = 0
|
|
session._snapshot_cache = None
|
|
session._on_destination_change = Mock()
|
|
session._on_compact_destination = Mock()
|
|
session.input = ShellInput(session.profile, session._emit)
|
|
session.view = ShellView.__new__(ShellView)
|
|
session.view.features = make_feature_view(runtime_app.Profile.LARGE)
|
|
session.feature_owner = runtime_app.FeatureSettingsOwner(self.ui.params, session._feature_authority,
|
|
vehicle_fingerprint=lambda: getattr(self.ui.CP, "carFingerprint", None),
|
|
vehicle_params=lambda: self.ui.CP)
|
|
return session
|
|
|
|
def _settings_tap(self, session, x, y):
|
|
session.press(ShellMode.SETTINGS, x, y)
|
|
self.assertTrue(session.release(ShellMode.SETTINGS, x, y))
|
|
|
|
def _open_vehicle_picker(self, session, pushed):
|
|
self._settings_tap(session, 1800, 800)
|
|
self._settings_tap(session, 2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT * 1.5)
|
|
return pushed.call_args.args[0]
|
|
|
|
@staticmethod
|
|
def _option_dialog(title, options, current="", callback=None):
|
|
return NS(title=title, options=options, selection=current, callback=callback)
|
|
|
|
def test_large_manual_vehicle_selection_without_detected_vehicle_and_auto_restore(self):
|
|
from opendbc.car.hyundai.values import CAR
|
|
from openpilot.starpilot.vehicle_selection import choices, read_selection, startup_candidate
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
session = self._vehicle_session()
|
|
self.ui.CP = None
|
|
choice = next(choice for choice in choices() if choice.platform == CAR.KIA_XCEED_PHEV)
|
|
with patch("openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", side_effect=self._option_dialog), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
make = self._open_vehicle_picker(session, pushed)
|
|
self.assertIn("Auto detection", make.options)
|
|
shown = session.snapshot(ShellMode.SETTINGS).features
|
|
self.assertEqual([(row.label, row.value) for row in shown.rows],
|
|
[("Reported vehicle", "Not detected"), ("Vehicle selection", "Auto detection")])
|
|
make.selection = choice.make
|
|
make.callback(DialogResult.CONFIRM)
|
|
model = pushed.call_args.args[0]
|
|
self.assertIn(choice.label, model.options)
|
|
model.selection = choice.label
|
|
model.callback(DialogResult.CONFIRM)
|
|
self.assertEqual(read_selection(self.ui.params).platform, choice.platform)
|
|
self.assertEqual(startup_candidate(self.ui.params), choice.platform)
|
|
self.assertIsNone(self.ui.CP)
|
|
self.assertEqual(session.snapshot(ShellMode.SETTINGS).features.rows[1].value, choice.label)
|
|
self._settings_tap(session, 2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT * 1.5)
|
|
auto = pushed.call_args.args[0]
|
|
auto.selection = "Auto detection"
|
|
auto.callback(DialogResult.CONFIRM)
|
|
self.assertIsNone(startup_candidate(self.ui.params))
|
|
self.assertEqual(session.snapshot(ShellMode.SETTINGS).features.rows[1].value, "Auto detection")
|
|
|
|
def test_large_vehicle_selection_offroad_with_available_telemetry(self):
|
|
from opendbc.car.hyundai.values import CAR
|
|
from openpilot.starpilot.vehicle_selection import choices, startup_candidate
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
choice = next(choice for choice in choices() if choice.platform == CAR.KIA_XCEED_PHEV)
|
|
for evidence in ("missing", "stale", "ignition"):
|
|
with self.subTest(evidence=evidence):
|
|
self.ui.sm = ui_fake().sm
|
|
self.ui.sm["deviceState"].started = False
|
|
self.ui.sm["pandaStates"][0].ignitionLine = False
|
|
session = self._vehicle_session()
|
|
self.ui.CP = None
|
|
if evidence == "missing":
|
|
self.ui.sm.messages["pandaStates"] = []
|
|
self.ui.sm.valid["deviceState"] = self.ui.sm.valid["pandaStates"] = False
|
|
elif evidence == "stale":
|
|
self.ui.sm.logMonoTime["deviceState"] -= 2_000_000_000
|
|
self.ui.sm.logMonoTime["pandaStates"] -= 2_000_000_000
|
|
else:
|
|
self.ui.sm["pandaStates"][0].ignitionLine = True
|
|
self.assertTrue(self.ui.is_offroad())
|
|
self.assertEqual(session.confirmed_offroad(), evidence == "ignition")
|
|
with patch("openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", side_effect=self._option_dialog), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed, patch.object(session, "_unavailable") as alert:
|
|
make = self._open_vehicle_picker(session, pushed)
|
|
make.selection = choice.make
|
|
make.callback(DialogResult.CONFIRM)
|
|
model = pushed.call_args.args[0]
|
|
model.selection = choice.label
|
|
model.callback(DialogResult.CONFIRM)
|
|
alert.assert_not_called()
|
|
self.assertEqual(startup_candidate(self.ui.params), choice.platform)
|
|
self.assertIsNone(self.ui.CP)
|
|
self._settings_tap(session, 2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT * 1.5)
|
|
auto = pushed.call_args.args[0]
|
|
auto.selection = "Auto detection"
|
|
auto.callback(DialogResult.CONFIRM)
|
|
alert.assert_not_called()
|
|
self.assertIsNone(startup_candidate(self.ui.params))
|
|
|
|
def test_large_picker_lists_and_saves_every_gm_platform(self):
|
|
from opendbc.car.gm.values import CAR
|
|
from openpilot.starpilot.vehicle_selection import choices, startup_candidate
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
|
|
manufacturers = {'CHEVROLET': 'Chevrolet', 'CADILLAC': 'Cadillac', 'BUICK': 'Buick', 'GMC': 'GMC', 'HOLDEN': 'Holden'}
|
|
catalog = {choice.platform: choice for choice in choices()}
|
|
for platform in CAR:
|
|
with self.subTest(platform=platform):
|
|
session = self._vehicle_session()
|
|
self.ui.CP = None
|
|
manufacturer = manufacturers[str(platform).split('_', 1)[0]]
|
|
with patch("openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", side_effect=self._option_dialog), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
make = self._open_vehicle_picker(session, pushed)
|
|
self.assertIn(manufacturer, make.options)
|
|
self.assertNotIn("Gm", make.options)
|
|
make.selection = manufacturer
|
|
make.callback(DialogResult.CONFIRM)
|
|
model = pushed.call_args.args[0]
|
|
self.assertEqual(model.title, f"Select {manufacturer} model")
|
|
self.assertEqual(set(model.options), {choice.label for choice in catalog.values() if choice.make == manufacturer})
|
|
self.assertEqual(model.options.count(catalog[platform].label), 1)
|
|
model.selection = catalog[platform].label
|
|
model.callback(DialogResult.CONFIRM)
|
|
self.assertEqual(startup_candidate(self.ui.params), platform)
|
|
self.ui.CP = NS(carFingerprint=str(platform), notCar=False)
|
|
rows = session._vehicle_selector().rows()
|
|
self.assertEqual(rows[0].value, catalog[platform].label)
|
|
self.assertEqual(rows[1].value, catalog[platform].label)
|
|
|
|
def test_large_vehicle_picker_uses_real_dialog_stack_and_returns_to_same_panel(self):
|
|
from opendbc.car.hyundai.values import CAR
|
|
from openpilot.starpilot.vehicle_selection import choices, startup_candidate
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
|
session = self._vehicle_session()
|
|
self.ui.CP = None
|
|
page = runtime_app.StarShellPage(session, ShellMode.SETTINGS)
|
|
choice = next(choice for choice in choices() if choice.platform == CAR.KIA_XCEED_PHEV)
|
|
with patch.object(runtime_app.gui_app, "_nav_stack", [page]), \
|
|
patch("openpilot.system.ui.widgets.button.Label"), patch.object(page, "hide_event") as hidden:
|
|
self._settings_tap(session, 1800, 800)
|
|
self._settings_tap(session, 2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT * 1.5)
|
|
make = runtime_app.gui_app.get_active_widget()
|
|
self.assertIsInstance(make, MultiOptionDialog)
|
|
self.assertFalse(page.enabled)
|
|
make._on_option_clicked(choice.make)
|
|
make._set_result(DialogResult.CONFIRM)
|
|
model = runtime_app.gui_app.get_active_widget()
|
|
self.assertIsInstance(model, MultiOptionDialog)
|
|
self.assertIsNot(model, make)
|
|
model._on_option_clicked(choice.label)
|
|
model._set_result(DialogResult.CONFIRM)
|
|
self.assertEqual(startup_candidate(self.ui.params), choice.platform)
|
|
self.assertIs(runtime_app.gui_app.get_active_widget(), page)
|
|
self.assertTrue(page.enabled)
|
|
self.assertEqual(session.snapshot(ShellMode.SETTINGS).features.page, FeaturePage.VEHICLE)
|
|
hidden.assert_not_called()
|
|
|
|
def test_large_vehicle_picker_rejects_cancel_stale_source_onroad_and_old_page(self):
|
|
from opendbc.car.hyundai.values import CAR
|
|
from openpilot.starpilot.vehicle_selection import VehicleSelectionOwner, choices, read_selection
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
choice = next(choice for choice in choices() if choice.platform == CAR.KIA_XCEED_PHEV)
|
|
for failure in ("cancel", "source", "onroad", "page"):
|
|
with self.subTest(failure=failure):
|
|
session = self._vehicle_session()
|
|
self.ui.CP = None
|
|
self.ui.started = False
|
|
self.ui.sm["deviceState"].started = False
|
|
self.ui.is_offroad = lambda: not self.ui.started
|
|
with patch("openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", side_effect=self._option_dialog), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
make = self._open_vehicle_picker(session, pushed)
|
|
make.selection = choice.make
|
|
make.callback(DialogResult.CONFIRM)
|
|
model = pushed.call_args.args[0]
|
|
model.selection = choice.label
|
|
if failure == "source":
|
|
owner = VehicleSelectionOwner(self.ui.params, lambda: True)
|
|
self.assertTrue(owner.choose(None, str(CAR.KIA_CEED)).verified)
|
|
elif failure == "onroad":
|
|
self.ui.started = True
|
|
elif failure == "page":
|
|
self._settings_tap(session, 250, 160)
|
|
self._settings_tap(session, 1800, 800)
|
|
original = read_selection(self.ui.params)
|
|
model.callback(DialogResult.CANCEL if failure == "cancel" else DialogResult.CONFIRM)
|
|
self.assertEqual(read_selection(self.ui.params), original)
|
|
|
|
def test_large_vehicle_picker_repairs_invalid_selection_and_keeps_duplicate_labels(self):
|
|
from opendbc.car.hyundai.values import CAR
|
|
from openpilot.starpilot.vehicle_selection import KEY, VehicleChoice, read_selection
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
session = self._vehicle_session()
|
|
self.ui.CP = None
|
|
Path(self.ui.params.get_param_path(KEY)).write_bytes(b"invalid")
|
|
catalog = (VehicleChoice(str(CAR.KIA_CEED), "Kia", "Same model"),
|
|
VehicleChoice(str(CAR.KIA_XCEED_PHEV), "Kia", "Same model"))
|
|
with patch("openpilot.starpilot.vehicle_selection.VehicleSelectionOwner.choices", return_value=catalog), \
|
|
patch("openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", side_effect=self._option_dialog), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
make = self._open_vehicle_picker(session, pushed)
|
|
self.assertEqual(session.snapshot(ShellMode.SETTINGS).features.rows[1].value, "Needs review")
|
|
make.selection = "Auto detection"
|
|
make.callback(DialogResult.CONFIRM)
|
|
self.assertTrue(read_selection(self.ui.params).valid)
|
|
self._settings_tap(session, 2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT * 1.5)
|
|
make = pushed.call_args.args[0]
|
|
make.selection = "Kia"
|
|
make.callback(DialogResult.CONFIRM)
|
|
model = pushed.call_args.args[0]
|
|
self.assertEqual(len(model.options), 2)
|
|
model.selection = next(option for option in model.options if str(CAR.KIA_XCEED_PHEV) in option)
|
|
model.callback(DialogResult.CONFIRM)
|
|
self.assertEqual(read_selection(self.ui.params).platform, CAR.KIA_XCEED_PHEV)
|
|
|
|
def test_large_vehicle_tile_reuses_editor_and_preserves_both_back_paths(self):
|
|
for sidebar_expanded in (True, False):
|
|
with self.subTest(sidebar_expanded=sidebar_expanded):
|
|
session = self._vehicle_session(sidebar_expanded)
|
|
session.feature_scroll = 5
|
|
root = session.snapshot(ShellMode.SETTINGS)
|
|
self.assertTrue(root.settings.destination(Destination.VEHICLE).available)
|
|
x, y, width, height = tile_rects(root.settings)[5]
|
|
self._settings_tap(session, x + width / 2, y + height / 2)
|
|
shown = session.snapshot(ShellMode.SETTINGS)
|
|
self.assertEqual(shown.selected, Destination.DRIVING_CONTROLS)
|
|
self.assertEqual((shown.features.page, shown.features.title, shown.features.parent_title, shown.features.scroll),
|
|
(FeaturePage.VEHICLE, "Vehicle Settings", "StarPilot", 0))
|
|
self.assertIn("LongPitch", [row.key for row in shown.features.rows])
|
|
session._on_compact_destination.assert_not_called()
|
|
view = ShellView.__new__(ShellView)
|
|
view.profile, view.onroad, view.settings, view.features = session.profile, Mock(), Mock(), Mock()
|
|
view.render(shown)
|
|
view.features.render.assert_called_once_with(shown.features, 0.0)
|
|
view.settings.render.assert_not_called()
|
|
view.settings.render_rail.assert_called_once_with(shown.settings, selected=Destination.STAR)
|
|
self._settings_tap(session, *((250, 160) if sidebar_expanded else (70, 50)))
|
|
self.assertEqual(session.selected, Destination.STAR)
|
|
|
|
# Reopening through Driving Controls must reset the entry page.
|
|
root = session.snapshot(ShellMode.SETTINGS)
|
|
x, y, width, height = tile_rects(root.settings)[2]
|
|
self._settings_tap(session, x + width / 2, y + height / 2)
|
|
hub = session.snapshot(ShellMode.SETTINGS).features
|
|
self.assertEqual(hub.page, FeaturePage.HUB)
|
|
vehicle = next(row for row in hub.rows if row.page == FeaturePage.VEHICLE)
|
|
session._feature_ui(FeatureUiAction("open", vehicle))
|
|
shown = session.snapshot(ShellMode.SETTINGS)
|
|
self.assertEqual(shown.features.parent_title, "Driving Controls")
|
|
if sidebar_expanded:
|
|
self._settings_tap(session, 20, 581)
|
|
self._settings_tap(session, 70, 50)
|
|
self.assertEqual(session.snapshot(ShellMode.SETTINGS).features.page, FeaturePage.HUB)
|
|
self.assertEqual(session.selected, Destination.DRIVING_CONTROLS)
|
|
self._settings_tap(session, 70, 50)
|
|
self.assertEqual(session.selected, Destination.STAR)
|
|
session._on_home = Mock()
|
|
self._settings_tap(session, 70, 50)
|
|
session._on_home.assert_called_once_with()
|
|
|
|
def test_large_vehicle_control_uses_confirmation_and_cancels_after_leaving(self):
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
session = self._vehicle_session()
|
|
self._settings_tap(session, 1800, 800)
|
|
with patch("openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", side_effect=lambda *a, **kw: NS(callback=kw["callback"])), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
shown = session.snapshot(ShellMode.SETTINGS).features
|
|
index = next(index for index, row in enumerate(shown.rows) if row.key == "LongPitch")
|
|
y = FEATURE_ROW_TOP + index * FEATURE_ROW_HEIGHT + FEATURE_ROW_HEIGHT / 2
|
|
self._settings_tap(session, 2000, y)
|
|
self.assertFalse(self.ui.params.get_bool("LongPitch"))
|
|
pushed.call_args.args[0].callback(DialogResult.CONFIRM)
|
|
self.assertTrue(self.ui.params.get_bool("LongPitch"))
|
|
self._settings_tap(session, 2000, y)
|
|
confirmation = pushed.call_args.args[0].callback
|
|
self._settings_tap(session, 250, 160)
|
|
self._settings_tap(session, 1800, 800)
|
|
confirmation(DialogResult.CONFIRM)
|
|
self.assertTrue(self.ui.params.get_bool("LongPitch"))
|
|
|
|
def test_compact_vehicle_destination_keeps_its_native_route(self):
|
|
session = self._vehicle_session()
|
|
session.profile = runtime_app.Profile.COMPACT
|
|
session.drive_state = NS(snapshot=lambda: {"mode": "auto", "revision": None, "available": False,
|
|
"effective": None, "overrideAllowed": False})
|
|
destination = session.snapshot(ShellMode.SETTINGS).settings.destination(Destination.VEHICLE)
|
|
session._emit(ShellRequest("settings", SettingsAction(SettingsActionKind.REQUEST_DESTINATION, destination)))
|
|
session._on_compact_destination.assert_called_once_with(Destination.VEHICLE)
|
|
self.assertEqual(session.selected, Destination.STAR)
|
|
self.assertEqual(session.feature_page, FeaturePage.HUB)
|
|
|
|
def test_feature_details_preserve_literals_and_reject_stale_rows(self):
|
|
from dataclasses import replace
|
|
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsState, FeatureUiAction
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session._mode, session.selected = ShellMode.SETTINGS, Destination.DRIVING_CONTROLS
|
|
session.input = Mock()
|
|
row = FeatureRow("saved", "A < B & C", "75", unit="%", reason="Full explanation " * 30)
|
|
session.feature_snapshot = Mock(return_value=FeatureSettingsState(rows=(row,)))
|
|
with patch("openpilot.starpilot.ui.runtime_app.gui_app.push_widget") as push, \
|
|
patch.object(runtime_app.gui_app, "font", return_value=runtime_app.rl.Font()):
|
|
session._feature_details("features", FeatureUiAction("details", row))
|
|
elements = push.call_args.args[0]._html_renderer.elements
|
|
self.assertEqual([element.content for element in elements], [row.label, "75 %", row.reason.strip()])
|
|
self.assertTrue(all(element.font_size == 62 for element in elements))
|
|
session.input.cancel.assert_called_once()
|
|
push.reset_mock()
|
|
session._feature_details("features", FeatureUiAction("details", replace(row, value="80")))
|
|
session.selected = Destination.STAR
|
|
session._feature_details("features", FeatureUiAction("details", row))
|
|
push.assert_not_called()
|
|
|
|
def test_large_feature_pages_clamp_saved_offset_when_rows_shrink(self):
|
|
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsState
|
|
pages = ((Destination.DRIVING_CONTROLS, "features", "feature_scroll", "feature_snapshot", "_feature_ui"),
|
|
(Destination.SOUNDS, "sounds", "sounds_scroll", "sounds_snapshot", "_sounds_ui"),
|
|
(Destination.APPEARANCE, "appearance", "appearance_scroll", "appearance_snapshot", "_appearance_ui"),
|
|
(Destination.SYSTEM, "display", "display_scroll", "system_snapshot", "_display_ui"),
|
|
(Destination.DRIVING_MODEL, "models", "model_scroll", "model_snapshot", "_models_ui"))
|
|
rows = tuple(FeatureRow(str(i), str(i), "") for i in range(15))
|
|
for destination, field, offset, source, handler in pages:
|
|
with self.subTest(destination=destination):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session.profile = runtime_app.Profile.LARGE
|
|
session.sounds_edit_key = None
|
|
session.adapter = self._adapter()
|
|
session._mode, session.selected = ShellMode.SETTINGS, destination
|
|
session.compact_y = session.compact_scroll_x = 0
|
|
session.sidebar_expanded = True
|
|
session.feature_page = session.feature_root_page = FeaturePage.HUB
|
|
session._snapshot_cache = None
|
|
setattr(session, offset, 10)
|
|
state = Mock()
|
|
setattr(session, source, state)
|
|
for row_count, expected in ((15, 10), (11, 10), (8, 5)):
|
|
state.return_value = FeatureSettingsState(rows=rows[:row_count])
|
|
self.ui.sm.frame += 1
|
|
shown = getattr(session.snapshot(ShellMode.SETTINGS), field)
|
|
self.assertEqual(shown.scroll, expected)
|
|
self.assertEqual(getattr(session, offset), expected)
|
|
self.assertTrue(shown.rows[shown.scroll:shown.scroll + 5])
|
|
getattr(session, handler)(FeatureUiAction("scroll", direction=-1))
|
|
self.assertEqual(getattr(session.snapshot(ShellMode.SETTINGS), field).scroll, 0)
|
|
getattr(session, handler)(FeatureUiAction("scroll", direction=1))
|
|
self.assertEqual(getattr(session.snapshot(ShellMode.SETTINGS), field).scroll, 5)
|
|
state.return_value = FeatureSettingsState()
|
|
self.ui.sm.frame += 1
|
|
self.assertEqual(getattr(session.snapshot(ShellMode.SETTINGS), field).scroll, 0)
|
|
self.assertEqual(getattr(session, offset), 0)
|
|
if destination in (Destination.SOUNDS, Destination.SYSTEM):
|
|
setattr(session, offset, 5)
|
|
setattr(session, prefix := offset.removesuffix("_scroll") + "_edit_key", "7")
|
|
state.return_value = FeatureSettingsState(rows=(rows[7],), editor=True)
|
|
self.ui.sm.frame += 1
|
|
self.assertEqual(getattr(session.snapshot(ShellMode.SETTINGS), field).scroll, 0)
|
|
self.assertEqual(getattr(session, offset), 5)
|
|
setattr(session, prefix, None)
|
|
|
|
def test_large_feature_swipes_route_once_and_cancel_when_context_changes(self):
|
|
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsState
|
|
pages = ((Destination.DRIVING_CONTROLS, "feature", "feature_snapshot", "hub"),
|
|
(Destination.SOUNDS, "sounds", "sounds_snapshot", "sounds"),
|
|
(Destination.APPEARANCE, "appearance", "appearance_snapshot", "appearance"),
|
|
(Destination.SYSTEM, "display", "system_snapshot", "display"),
|
|
(Destination.DRIVING_MODEL, "model", "model_snapshot", "models"))
|
|
rows = tuple(FeatureRow(str(i), str(i), "", available=True, page="child") for i in range(13))
|
|
for destination, prefix, source, page in pages:
|
|
for expanded in (True, False):
|
|
with self.subTest(destination=destination, expanded=expanded):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session.profile = runtime_app.Profile.LARGE
|
|
session.sounds_edit_key = None
|
|
session.adapter = self._adapter()
|
|
session._mode, session.selected = ShellMode.SETTINGS, destination
|
|
session.compact_y = session.compact_scroll_x = 0
|
|
session.sidebar_expanded = expanded
|
|
session.feature_page = session.feature_root_page = FeaturePage.HUB
|
|
session.appearance_page = "appearance"
|
|
session._snapshot_cache = None
|
|
session.confirmed_offroad = Mock(return_value=True)
|
|
session.favorites = Mock()
|
|
session.view = ShellView.__new__(ShellView)
|
|
session.view.onroad = Mock()
|
|
for name, _, _ in runtime_app._FEATURE_SETTINGS_PANES.values():
|
|
setattr(session.view, name, make_feature_view(session.profile))
|
|
session.pip_warning = Mock()
|
|
session.settings_layer = None
|
|
session.notice = ""
|
|
offset = prefix + "_scroll"
|
|
setattr(session, offset, 0)
|
|
setattr(session, source, Mock(return_value=FeatureSettingsState(page=page, rows=rows)))
|
|
emit = Mock(wraps=session._emit)
|
|
session.input = ShellInput(session.profile, emit)
|
|
for start, end, expected in ((2000, 1700, 5), (2000, 1700, 10), (1800, 2100, 5), (1800, 2100, 0)):
|
|
before = getattr(session, offset)
|
|
session.press(ShellMode.SETTINGS, start, 200)
|
|
session.move(ShellMode.SETTINGS, end, 200)
|
|
self.assertEqual(getattr(session, offset), before)
|
|
self.assertTrue(session.release(ShellMode.SETTINGS, end, 200))
|
|
self.assertEqual(getattr(session, offset), expected)
|
|
widget = runtime_app.StarShellPage(session, ShellMode.SETTINGS)
|
|
bounds = runtime_app.rl.Rectangle(0, 0, 2160, 1080)
|
|
with (patch.object(widget, "_render"), patch.object(gui_app, "_show_touches", False),
|
|
patch("openpilot.system.ui.widgets.device", NS(awake=True)), patch.object(gui_app, "_mouse_events", [])):
|
|
widget.render(bounds)
|
|
for start, direction, duration, last_down, expected in ((2000, 1, .075, 80, 5), (1800, -1, .075, 80, 0),
|
|
(2000, 1, .5, 80, 0), (2000, 1, .075, 55, 0)):
|
|
events = [MouseEvent(MousePos(start - direction * distance, 200), 0,
|
|
index == 0, index == 3, index != 3, 1 + timestamp)
|
|
for index, (timestamp, distance) in enumerate(((0, 0), (duration / 2, last_down / 2),
|
|
(duration, last_down), (duration + .01, 80)))]
|
|
with patch.object(gui_app, "_mouse_events", events):
|
|
widget.render(bounds)
|
|
self.assertEqual(getattr(session, offset), expected)
|
|
self.assertEqual([call.args[0].action.kind for call in emit.call_args_list], ["scroll"] * 6)
|
|
changes = [("sidebar_expanded", not expanded), (offset, 5)]
|
|
if prefix in ("feature", "appearance"):
|
|
changes.append((prefix + "_page", "other"))
|
|
for attribute, changed in changes:
|
|
before = getattr(session, attribute)
|
|
session.press(ShellMode.SETTINGS, 2000, 200)
|
|
setattr(session, attribute, changed)
|
|
with patch.object(session, "snapshot") as build:
|
|
session.move(ShellMode.SETTINGS, 1700, 200)
|
|
self.assertFalse(session.release(ShellMode.SETTINGS, 1700, 200))
|
|
build.assert_not_called()
|
|
self.assertEqual(emit.call_count, 6)
|
|
setattr(session, attribute, before)
|
|
|
|
def _adapter(self, owner=None):
|
|
self._mono_now = NOW - 100_000_000
|
|
adapter = runtime_app.RuntimeSnapshotAdapter(
|
|
self.ui, owner, mono_clock=lambda: self._mono_now, boot_clock=lambda: self._mono_now + BOOT_OFFSET_NS)
|
|
self._mono_now = NOW
|
|
self.ui.sm.logMonoTime["deviceState"] = NOW - 50_000_000
|
|
self.ui.sm.logMonoTime["pandaStates"] = NOW + BOOT_OFFSET_NS - 50_000_000
|
|
self.ui.sm.recv_time["deviceState"] = (NOW - 30_000_000) / 1e9
|
|
self.ui.sm.recv_time["pandaStates"] = (NOW - 30_000_000) / 1e9
|
|
self.ui.sm.updated["deviceState"] = True
|
|
self.ui.sm.updated["pandaStates"] = True
|
|
return adapter
|
|
|
|
def setUp(self):
|
|
self.ui = ui_fake()
|
|
self.ui.started = False
|
|
self.ui.sm["deviceState"].started = False
|
|
self.ui.sm["pandaStates"][0].ignitionLine = False
|
|
self.ui.is_offroad = lambda: True
|
|
self.ui.engaged = False
|
|
self.ui.prime_state = NS(is_paired=lambda: False)
|
|
self.device = native_device.DeviceLayout.__new__(native_device.DeviceLayout)
|
|
self.device._params = Mock()
|
|
self.device._update_calib_description = Mock()
|
|
self.software = native_software.SoftwareLayout.__new__(native_software.SoftwareLayout)
|
|
object.__setattr__(self.software, "_download_btn", NS(action_item=NS(text="CHECK", enabled=True, set_enabled=Mock())))
|
|
self.software._waiting_for_updater = False
|
|
self.software._update_state = Mock()
|
|
panels = {PanelType.DEVICE: NS(instance=self.device), PanelType.SOFTWARE: NS(instance=self.software)}
|
|
self.layout = runtime_app.StarMainLayout.__new__(runtime_app.StarMainLayout)
|
|
object.__setattr__(self.layout, "_layouts", {runtime_app.MainState.SETTINGS: NS(_panels=panels)})
|
|
object.__setattr__(self.layout, "star", NS(adapter=self._adapter(), selected=Destination.DEVICE))
|
|
self.clock = patch.object(runtime_app.time, "monotonic_ns", return_value=NOW)
|
|
self.state = patch.object(runtime_app, "ui_state", self.ui)
|
|
self.clock.start()
|
|
self.state.start()
|
|
self.addCleanup(self.clock.stop)
|
|
self.addCleanup(self.state.stop)
|
|
|
|
def test_connectivity_power_uses_native_offroad_without_panda_telemetry(self):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session.adapter = self.layout.star.adapter
|
|
self.ui.sm.messages['pandaStates'] = []
|
|
self.assertFalse(session.adapter.connectivity_allowed())
|
|
self.assertTrue(session.connectivity_allowed())
|
|
self.ui.is_offroad = lambda: False
|
|
self.assertFalse(session.connectivity_allowed())
|
|
|
|
def test_large_pairing_uses_native_dialog_only_while_unpaired_and_parked(self):
|
|
with patch("openpilot.selfdrive.ui.widgets.pairing_dialog.PairingDialog", return_value="pairing") as dialog, \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
self.layout._show_pairing()
|
|
dialog.assert_called_once_with()
|
|
pushed.assert_called_once_with("pairing")
|
|
pushed.reset_mock()
|
|
self.ui.prime_state = NS(is_paired=lambda: True)
|
|
self.layout._show_pairing()
|
|
pushed.assert_not_called()
|
|
self.ui.prime_state = NS(is_paired=lambda: False)
|
|
self.ui.sm["deviceState"].started = True
|
|
self.layout._show_pairing()
|
|
pushed.assert_not_called()
|
|
|
|
def test_camera_and_calibration_use_native_dialogs_and_confirmation(self):
|
|
with patch.object(runtime_app.gui_app, "push_widget") as pushed, \
|
|
patch.object(native_device, "ConfirmDialog", side_effect=lambda *a, **k: NS(callback=k["callback"])), \
|
|
patch.object(native_device, "ui_state", self.ui), \
|
|
patch.object(native_device, "calibration_reset") as reset:
|
|
self.device._calibration_reset_pending = False
|
|
reset.request.return_value = True
|
|
with patch("openpilot.selfdrive.ui.onroad.cabin_camera_dialog.CabinCameraDialog", return_value="camera"):
|
|
self.assertTrue(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.PREVIEW_DRIVER_CAMERA)))
|
|
pushed.assert_called_once_with("camera")
|
|
self.assertTrue(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.RESET_CALIBRATION)))
|
|
dialog = pushed.call_args.args[0]
|
|
reset.request.assert_not_called()
|
|
dialog.callback(native_device.DialogResult.CANCEL)
|
|
reset.request.assert_not_called()
|
|
self.ui.sm["pandaStates"][0].ignitionLine = True
|
|
self.ui.sm["deviceState"].started = True
|
|
dialog.callback(native_device.DialogResult.CONFIRM)
|
|
reset.request.assert_not_called()
|
|
self.ui.sm["deviceState"].started = False
|
|
dialog.callback(native_device.DialogResult.CONFIRM)
|
|
reset.request.assert_called_once_with(self.device._params)
|
|
self.assertTrue(self.device._calibration_reset_pending)
|
|
|
|
def test_galaxy_never_opens_comma_pairing_or_infers_manage_from_home_pairing(self):
|
|
with patch("openpilot.selfdrive.ui.widgets.pairing_dialog.PairingDialog") as comma_dialog, \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
self.assertFalse(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.OPEN_GALAXY)))
|
|
self.ui.prime_state = NS(is_paired=lambda: True)
|
|
shown = self.layout.star.adapter.build(ShellMode.SETTINGS, Destination.DEVICE, now_ns=NOW)
|
|
self.assertTrue(shown.home.paired)
|
|
self.assertIsNone(shown.device.galaxy_paired)
|
|
self.assertNotIn(DeviceRequest.OPEN_GALAXY, shown.device.available_actions)
|
|
self.assertFalse(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.OPEN_GALAXY)))
|
|
comma_dialog.assert_not_called()
|
|
pushed.assert_not_called()
|
|
|
|
def test_galaxy_owner_runtime_allows_view_while_started(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
owner = GalaxyAccessOwner(Path(directory) / "access")
|
|
self.layout.star.adapter = self._adapter(owner)
|
|
self.layout.star.galaxy_flow = GalaxyAccessFlow(owner, self.layout._confirmed_offroad)
|
|
initial = self.layout.star.adapter.build(ShellMode.SETTINGS, Destination.DEVICE, now_ns=NOW)
|
|
self.assertIn(DeviceRequest.OPEN_GALAXY, initial.device.available_actions)
|
|
self.assertFalse(initial.device.galaxy_configured)
|
|
self.assertIsNone(initial.device.galaxy_paired)
|
|
self.assertTrue(initial.device.galaxy_local_only)
|
|
with patch("openpilot.starpilot.ui.galaxy_access.GalaxyConnectionView", return_value="connection page"), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed, \
|
|
patch("openpilot.selfdrive.ui.widgets.pairing_dialog.PairingDialog") as comma_dialog:
|
|
self.assertTrue(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.OPEN_GALAXY)))
|
|
pushed.assert_called_once_with("connection page")
|
|
pushed.reset_mock()
|
|
self.ui.started = True
|
|
self.layout.star.galaxy_flow.open_large()
|
|
pushed.assert_called_once_with("connection page")
|
|
self.assertFalse(self.layout.star.galaxy_flow.pair("password123"))
|
|
comma_dialog.assert_not_called()
|
|
|
|
def test_unavailable_credentials_do_not_block_local_galaxy(self):
|
|
owner = Mock(spec=GalaxyAccessOwner)
|
|
owner.status.return_value = NS(status=AccessStatus.UNAVAILABLE)
|
|
self.layout.star.adapter = self._adapter(owner)
|
|
self.layout.star.galaxy_flow = GalaxyAccessFlow(owner, self.layout._confirmed_offroad)
|
|
shown = self.layout.star.adapter.build(ShellMode.SETTINGS, Destination.DEVICE, now_ns=NOW)
|
|
self.assertIn(DeviceRequest.OPEN_GALAXY, shown.device.available_actions)
|
|
with patch("openpilot.starpilot.ui.galaxy_access.GalaxyConnectionView", return_value="connection page"), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
self.assertTrue(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.OPEN_GALAXY)))
|
|
pushed.assert_called_once_with("connection page")
|
|
owner.configure.assert_not_called()
|
|
owner.remove.assert_not_called()
|
|
|
|
def test_compact_galaxy_is_on_main_menu_and_device_keeps_native_rows(self):
|
|
from openpilot.selfdrive.ui.mici.layouts.settings.device import device_layout as compact_device
|
|
from openpilot.starpilot.ui import display_compact, replay_settings
|
|
replay_button = Mock()
|
|
display_entry = object()
|
|
panel = NS(_scroller=NS(add_widgets=Mock()))
|
|
layout = runtime_app.StarMiciMainLayout.__new__(runtime_app.StarMiciMainLayout)
|
|
object.__setattr__(layout, "_compact_panels", {})
|
|
galaxy = NS(open_compact=Mock())
|
|
object.__setattr__(layout, "star", NS(galaxy_flow=galaxy))
|
|
with patch.object(compact_device, "DeviceLayoutMici", return_value=panel) as built, \
|
|
patch.object(display_compact.DisplayCompact, "entry_button", return_value=display_entry), \
|
|
patch.object(replay_settings, "BigButton", return_value=replay_button) as replay_constructor, \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
layout._open_compact_destination(Destination.DEVICE)
|
|
built.assert_called_once_with()
|
|
panel._scroller.add_widgets.assert_called_once_with([display_entry, replay_button])
|
|
pushed.assert_called_once_with(panel)
|
|
layout._open_compact_destination(Destination.GALAXY)
|
|
galaxy.open_compact.assert_called_once_with()
|
|
|
|
replay_constructor.assert_called_once_with("replay", "recorded driving routes on this device")
|
|
replay_button.set_click_callback.assert_called_once()
|
|
with patch.object(replay_settings, "open_replay") as open_replay:
|
|
replay_button.set_click_callback.call_args.args[0]()
|
|
open_replay.assert_called_once_with(layout.star)
|
|
|
|
def test_compact_toggles_and_software_keep_only_native_rows(self):
|
|
from openpilot.selfdrive.ui.mici.layouts.settings import toggles, software
|
|
panels = []
|
|
layout = runtime_app.StarMiciMainLayout.__new__(runtime_app.StarMiciMainLayout)
|
|
object.__setattr__(layout, "_compact_panels", {})
|
|
object.__setattr__(layout, "star", NS())
|
|
def panel():
|
|
value = NS(_scroller=NS(add_widgets=Mock()))
|
|
panels.append(value)
|
|
return value
|
|
with patch.object(toggles, "TogglesLayoutMici", side_effect=panel), \
|
|
patch.object(software, "SoftwareLayoutMici", side_effect=panel), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
layout._open_compact_destination(Destination.TOGGLES)
|
|
layout._open_compact_destination(Destination.SOFTWARE)
|
|
self.assertEqual(len(panels), 2)
|
|
for item in panels:
|
|
item._scroller.add_widgets.assert_not_called()
|
|
self.assertEqual([call.args[0] for call in pushed.call_args_list], panels)
|
|
|
|
def test_compact_model_visuals_and_pair_use_existing_owners(self):
|
|
from openpilot.starpilot.ui import replay_settings
|
|
replay_button = Mock()
|
|
from openpilot.starpilot.ui import appearance_compact, models_compact, display_compact
|
|
from openpilot.selfdrive.ui.mici.layouts.settings.device import device_layout as compact_device
|
|
layout = runtime_app.StarMiciMainLayout.__new__(runtime_app.StarMiciMainLayout)
|
|
object.__setattr__(layout, "_compact_panels", {})
|
|
object.__setattr__(layout, "star", NS())
|
|
panel = NS(scroll_to_pairing=Mock(), _scroller=NS(add_widgets=Mock()))
|
|
with patch.object(models_compact.ModelsCompact, "open") as model_open, \
|
|
patch.object(appearance_compact.AppearanceCompact, "open") as visuals_open, \
|
|
patch.object(compact_device, "DeviceLayoutMici", return_value=panel), \
|
|
patch.object(display_compact.DisplayCompact, "entry_button", return_value="display entry"), \
|
|
patch.object(replay_settings, "BigButton", return_value=replay_button) as replay_constructor, \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
layout._open_compact_destination(Destination.DRIVING_MODEL)
|
|
layout._open_compact_destination(Destination.APPEARANCE)
|
|
layout._open_compact_destination(Destination.PAIR)
|
|
model_open.assert_called_once_with()
|
|
visuals_open.assert_called_once_with()
|
|
pushed.assert_called_once_with(panel)
|
|
panel.scroll_to_pairing.assert_called_once_with()
|
|
panel._scroller.add_widgets.assert_called_once_with(["display entry", replay_button])
|
|
|
|
replay_constructor.assert_called_once_with("replay", "recorded driving routes on this device")
|
|
replay_button.set_click_callback.assert_called_once()
|
|
with patch.object(replay_settings, "open_replay") as open_replay:
|
|
replay_button.set_click_callback.call_args.args[0]()
|
|
open_replay.assert_called_once_with(layout.star)
|
|
|
|
def test_compact_connection_can_be_viewed_while_started_but_not_mutated(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
owner = GalaxyAccessOwner(Path(directory) / "access")
|
|
self.layout.star.adapter = self._adapter(owner)
|
|
flow = GalaxyAccessFlow(owner, self.layout._confirmed_offroad)
|
|
with patch("openpilot.starpilot.ui.galaxy_access.GalaxyConnectionPage", return_value="connection page"), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
flow.open_compact()
|
|
pushed.assert_called_once_with("connection page")
|
|
pushed.reset_mock()
|
|
self.ui.started = True
|
|
flow.open_compact()
|
|
pushed.assert_called_once_with("connection page")
|
|
self.assertFalse(flow.pair("password123"))
|
|
self.ui.started = False
|
|
pushed.reset_mock()
|
|
flow.open_compact()
|
|
pushed.assert_called_once_with("connection page")
|
|
|
|
def test_galaxy_connection_url_contains_only_numeric_lan_address(self):
|
|
self.assertEqual(connection_url("192.168.1.111"), "http://192.168.1.111:8082/#/")
|
|
self.assertEqual(connection_url("10.0.0.1"), "http://10.0.0.1:8082/#/")
|
|
for address in ("", "localhost", "127.0.0.1", "0.0.0.0", "169.254.1.1", "224.0.0.1", "::1",
|
|
"192.168.1.111/?password=secret"):
|
|
with self.subTest(address=address):
|
|
self.assertIsNone(connection_url(address))
|
|
|
|
def test_stale_or_started_state_blocks_owner_and_unowned_rows(self):
|
|
with patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
self.assertFalse(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.RESET_DRIVER_MONITORING)))
|
|
self.ui.sm.logMonoTime["deviceState"] = NOW - 2_000_000_000
|
|
self.assertFalse(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.OPEN_GALAXY)))
|
|
self.ui.sm.logMonoTime["deviceState"] = NOW
|
|
self.ui.started = True
|
|
self.assertFalse(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.PREVIEW_DRIVER_CAMERA)))
|
|
pushed.assert_not_called()
|
|
|
|
def test_in_drive_toggle_uses_native_item_guard_while_device_action_stays_parked(self):
|
|
self.layout.star.selected = Destination.TOGGLES
|
|
request = ToggleRequest(ToggleKey.METRIC, True)
|
|
with patch.object(self.layout, "_confirmed_offroad", return_value=False), \
|
|
patch.object(self.layout, "_deliver_toggles", return_value=True) as deliver:
|
|
self.assertTrue(self.layout._deliver_settings_request(request))
|
|
deliver.assert_called_once_with(request)
|
|
self.layout.star.selected = Destination.DEVICE
|
|
self.assertFalse(self.layout._deliver_settings_request(DeviceAction(DeviceRequest.RESET_CALIBRATION)))
|
|
|
|
def test_runtime_snapshot_marks_only_evidenced_controls_available(self):
|
|
shown = self.layout.star.adapter.build(ShellMode.SETTINGS, Destination.SOFTWARE, now_ns=NOW)
|
|
self.assertTrue(shown.software.automatic_updates)
|
|
self.assertEqual(shown.software.available_actions, frozenset({SoftwareRequest.OPEN_UNINSTALL_CONFIRMATION}))
|
|
self.assertNotIn(DeviceRequest.RESET_DRIVER_MONITORING, shown.device.available_actions)
|
|
self.assertNotIn(DeviceRequest.OPEN_GALAXY, shown.device.available_actions)
|
|
self.assertIsNone(shown.device.galaxy_paired)
|
|
self.ui.params.values.pop("DisableUpdates", None)
|
|
self.assertIsNone(self.layout.star.adapter.build(ShellMode.SETTINGS, Destination.SOFTWARE, now_ns=NOW).software.automatic_updates)
|
|
self.ui.params.values["UpdaterState"] = "idle"
|
|
self.ui.params.values["UpdaterAvailableBranches"] = "new,old"
|
|
known = self.layout.star.adapter.build(ShellMode.SETTINGS, Destination.SOFTWARE, now_ns=NOW)
|
|
self.assertIn(SoftwareRequest.CHECK_FOR_UPDATES, known.software.available_actions)
|
|
self.assertIn(SoftwareRequest.OPEN_BRANCH_CHOOSER, known.software.available_actions)
|
|
|
|
def test_held_device_press_cancels_when_offroad_evidence_changes(self):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
self.enterContext(patch.object(session, 'drive_state', NS(snapshot=lambda: {"mode": "auto", "revision": None, "available": False,
|
|
"effective": None, "overrideAllowed": False}), create=True))
|
|
session.profile = runtime_app.Profile.LARGE
|
|
session.selected = Destination.DEVICE
|
|
session.adapter = self.layout.star.adapter
|
|
session._mode = ShellMode.SETTINGS
|
|
session._request_owner = self.layout._deliver_settings_request
|
|
session._request_emitted = False
|
|
session._snapshot_cache = None
|
|
session.input = ShellInput(session.profile, session._emit)
|
|
session.favorites = Mock()
|
|
session.view = Mock()
|
|
session._favorite_claimed = False
|
|
with patch.object(session, "snapshot", side_effect=lambda mode: session.adapter.build(mode, session.selected, now_ns=NOW)), \
|
|
patch.object(runtime_app.gui_app, "push_widget") as pushed:
|
|
session.press(ShellMode.SETTINGS, 1900, 630)
|
|
self.ui.started = True
|
|
self.assertFalse(session.release(ShellMode.SETTINGS, 1900, 630))
|
|
pushed.assert_not_called()
|
|
|
|
def test_onroad_settings_touch_uses_displayed_snapshot_without_rebuilding(self):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
self.enterContext(patch.object(session, 'drive_state', NS(snapshot=lambda: {"mode": "auto", "revision": None, "available": False,
|
|
"effective": None, "overrideAllowed": False}), create=True))
|
|
session.selected = Destination.DEVICE
|
|
session._mode = ShellMode.SETTINGS
|
|
session._request_emitted = False
|
|
session.input = Mock()
|
|
displayed = NS(selected=Destination.DEVICE, device=NS(offroad=False))
|
|
self.enterContext(patch.object(session, '_rendered_settings', (NOW, displayed), create=True))
|
|
session._rendered_settings_pipeline = (bool(self.ui.started), self.ui.started_frame)
|
|
session._settings_touch = None
|
|
with patch.object(self.ui, "is_offroad", return_value=False), \
|
|
patch.object(session, "snapshot") as build, patch.object(session, "confirmed_offroad") as parked:
|
|
session.press(ShellMode.SETTINGS, 100, 200)
|
|
session.move(ShellMode.SETTINGS, 101, 200)
|
|
self.assertFalse(session.release(ShellMode.SETTINGS, 101, 200))
|
|
build.assert_not_called()
|
|
parked.assert_not_called()
|
|
self.assertIs(session.input.press.call_args.args[-1], displayed)
|
|
self.assertIs(session.input.move.call_args.args[-1], displayed)
|
|
self.assertIs(session.input.release.call_args.args[-1], displayed)
|
|
self.assertIsNone(session._settings_touch)
|
|
|
|
def test_settings_touch_cancels_if_parked_evidence_is_lost(self):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
self.enterContext(patch.object(session, 'drive_state', NS(snapshot=lambda: {"mode": "auto", "revision": None, "available": False,
|
|
"effective": None, "overrideAllowed": False}), create=True))
|
|
session.selected = Destination.DEVICE
|
|
session._mode = ShellMode.SETTINGS
|
|
session._request_emitted = False
|
|
session.input = Mock()
|
|
session.favorites = Mock()
|
|
session.view = Mock()
|
|
session._favorite_claimed = False
|
|
self.enterContext(patch.object(session, '_rendered_settings', (NOW, NS(selected=Destination.DEVICE, device=NS(offroad=True))), create=True))
|
|
session._rendered_settings_pipeline = (bool(self.ui.started), self.ui.started_frame)
|
|
session._settings_touch = None
|
|
with patch.object(session, "confirmed_offroad", side_effect=(True, False)), \
|
|
patch.object(session, "snapshot") as build:
|
|
session.press(ShellMode.SETTINGS, 100, 200)
|
|
self.assertFalse(session.release(ShellMode.SETTINGS, 100, 200))
|
|
build.assert_not_called()
|
|
session.input.release.assert_not_called()
|
|
session.input.cancel.assert_called_once()
|
|
|
|
def test_compact_normal_pages_remain_readable_in_drive(self):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
self.enterContext(patch.object(session, 'drive_state', NS(snapshot=lambda: {"mode": "auto", "revision": None, "available": False,
|
|
"effective": None, "overrideAllowed": False}), create=True))
|
|
session.profile = runtime_app.Profile.COMPACT
|
|
session.adapter = self.layout.star.adapter
|
|
session.selected = Destination.STAR
|
|
session.compact_y = session.compact_scroll_x = 0
|
|
session.sidebar_expanded = True
|
|
session._snapshot_cache = None
|
|
self.ui.started = True
|
|
self.ui.is_offroad = lambda: False
|
|
state = session.snapshot(ShellMode.SETTINGS).settings
|
|
for destination in (Destination.NETWORK, Destination.BLUETOOTH, Destination.VEHICLE, Destination.DEVELOPER):
|
|
self.assertTrue(state.destination(destination).available, destination)
|
|
self.assertFalse(state.destination(Destination.PAIR).available)
|
|
|
|
def test_compact_network_in_drive_uses_native_network_owner(self):
|
|
layout = runtime_app.StarMiciMainLayout.__new__(runtime_app.StarMiciMainLayout)
|
|
self.enterContext(patch.object(layout, 'star', NS(connectivity_allowed=Mock(return_value=False)), create=True))
|
|
self.enterContext(patch.object(layout, '_compact_panels', {}, create=True))
|
|
page = NS()
|
|
with patch("openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout.NetworkLayoutMici", return_value=page) as native, \
|
|
patch.object(runtime_app.gui_app, "push_widget") as push:
|
|
layout._open_compact_destination(Destination.NETWORK)
|
|
layout._open_compact_destination(Destination.NETWORK)
|
|
native.assert_called_once_with()
|
|
self.assertEqual(push.call_args_list, [call(page), call(page)])
|
|
layout.star.connectivity_allowed.assert_not_called()
|
|
|
|
def test_software_check_owner_and_waiting_gate_with_mocked_final_effect(self):
|
|
self.layout.star.selected = Destination.SOFTWARE
|
|
self.ui.params.values["UpdaterState"] = "idle"
|
|
with patch.object(native_software.subprocess, "run") as effect:
|
|
self.assertTrue(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.CHECK_FOR_UPDATES)))
|
|
effect.assert_called_once()
|
|
self.assertTrue(self.software._waiting_for_updater)
|
|
self.assertFalse(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.CHECK_FOR_UPDATES)))
|
|
self.ui.params.values["UpdaterFetchAvailable"] = "1"
|
|
self.software._waiting_for_updater = False
|
|
self.assertFalse(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.CHECK_FOR_UPDATES)))
|
|
object.__setattr__(self.software._download_btn.action_item, "text", "DOWNLOAD")
|
|
self.assertTrue(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.DOWNLOAD_UPDATE)))
|
|
self.assertEqual(effect.call_count, 2)
|
|
|
|
def test_software_native_uninstall_confirmation_and_branch_owner(self):
|
|
self.layout.star.selected = Destination.SOFTWARE
|
|
self.ui.params.values.update(UpdaterState="idle", UpdaterAvailableBranches="new,old")
|
|
self.ui.params.put_bool = Mock()
|
|
self.ui.params.put = Mock()
|
|
object.__setattr__(self.software, "_branch_btn", NS(action_item=NS(set_value=Mock())))
|
|
with patch.object(native_software, "ConfirmDialog", side_effect=lambda *a, **k: NS(callback=k["callback"])), \
|
|
patch.object(native_software, "MultiOptionDialog", side_effect=lambda *a, **k: NS(callback=k["callback"], selection="new")), \
|
|
patch.object(native_software.gui_app, "push_widget") as pushed, \
|
|
patch.object(native_software.subprocess, "run") as effect, \
|
|
patch.object(native_software, "ui_state", self.ui):
|
|
self.assertTrue(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.OPEN_UNINSTALL_CONFIRMATION)))
|
|
dialog = pushed.call_args.args[0]
|
|
self.ui.params.put_bool.assert_not_called()
|
|
dialog.callback(native_software.DialogResult.CANCEL)
|
|
self.ui.params.put_bool.assert_not_called()
|
|
self.ui.sm["deviceState"].started = True
|
|
dialog.callback(native_software.DialogResult.CONFIRM)
|
|
self.ui.params.put_bool.assert_not_called()
|
|
self.ui.sm["deviceState"].started = False
|
|
dialog.callback(native_software.DialogResult.CONFIRM)
|
|
self.ui.params.put_bool.assert_called_once_with("DoUninstall", True, block=True)
|
|
self.assertTrue(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.OPEN_BRANCH_CHOOSER)))
|
|
branch_dialog = pushed.call_args.args[0]
|
|
self.ui.params.put.assert_not_called()
|
|
self.ui.sm["pandaStates"][0].ignitionLine = True
|
|
self.ui.sm["deviceState"].started = True
|
|
branch_dialog.callback(native_software.DialogResult.CONFIRM)
|
|
self.ui.params.put.assert_not_called()
|
|
self.ui.sm["deviceState"].started = False
|
|
self.assertTrue(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.OPEN_BRANCH_CHOOSER)))
|
|
branch_dialog = pushed.call_args.args[0]
|
|
branch_dialog.callback(native_software.DialogResult.CONFIRM)
|
|
self.ui.params.put.assert_called_once_with("UpdaterTargetBranch", "new", block=True)
|
|
effect.assert_called_once()
|
|
self.assertFalse(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.OPEN_ERROR_LOG)))
|
|
self.assertFalse(self.layout._deliver_settings_request(SoftwareAction(SoftwareRequest.SET_AUTOMATIC_UPDATES, False)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
|
|
|
|
def test_compact_developer_fps_is_live_onroad_but_physical_controls_keep_native_gates(tmp_path):
|
|
from openpilot.selfdrive.ui.mici.layouts.settings import developer
|
|
|
|
ui = NS(params=Params(str(tmp_path)), CP=None, is_release=False, engaged=True,
|
|
is_offroad=lambda: False, add_offroad_transition_callback=Mock())
|
|
with patch.object(developer, 'ui_state', ui), patch.object(developer, 'SshKeyFetcher'), \
|
|
patch.object(developer.gui_app, 'texture', return_value=NS(width=64, height=64)), \
|
|
patch.object(developer, 'BigButton'), patch.object(developer, 'BigToggle', side_effect=lambda *a, **k: Mock()), \
|
|
patch.object(developer, 'BigCircleParamControl', side_effect=lambda *a, **k: Mock()), \
|
|
patch.object(developer, 'BigParamControl') as debug, \
|
|
patch.object(developer.gui_app, 'set_show_touches') as touches, patch.object(developer.gui_app, 'set_show_fps') as fps:
|
|
page = developer.DeveloperLayoutMici()
|
|
assert page._adb_toggle.set_enabled.call_args.args[0]() is False
|
|
assert page._joystick_toggle.set_enabled.call_args.args[0]() is False
|
|
assert page._alpha_long_toggle.set_enabled.call_args.args[0]() is False
|
|
page._debug_mode_toggle.set_enabled.assert_not_called()
|
|
next(c for c in debug.call_args_list if c.args[0] == "ui debug mode").kwargs['toggle_callback'](True)
|
|
touches.assert_called_once_with(True)
|
|
fps.assert_called_once_with(True)
|