Files
StarPilot/openpilot/starpilot/ui/tests/test_appearance_settings.py
T
2026-10-27 04:54:30 -04:00

625 lines
32 KiB
Python

"""Onroad visibility preferences through real temporary Params and both native views."""
from dataclasses import replace
import os
from pathlib import Path
import tempfile
from types import SimpleNamespace as NS
import unittest
from unittest.mock import Mock, patch
import pyray as rl
from openpilot.common.params import Params
from openpilot.starpilot import saved_document
from openpilot.starpilot.ui import appearance_compact, onroad
from openpilot.starpilot.ui.appearance_owner import AppearanceOwner
from openpilot.starpilot.ui.appearance_preferences import RETIRED_DISPLAY_KEYS, OnroadAppearance, onroad_appearance, read_visibility
from openpilot.starpilot.ui.onroad_customization import default_document
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import FeatureInput, FeatureSettingsRequest, FeatureSettingsState, row_change
from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadAlert, OnroadInput, OnroadState, SpeedLimitObservation
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
from openpilot.starpilot.ui.presentation import BitmapFonts, Profile
from openpilot.starpilot.ui.settings_state import Destination, SettingsInput, SettingsState, tile_rects
from openpilot.starpilot.ui.shell import ShellInput, ShellMode
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW, ui_fake
def _hidden_widgets():
document = default_document()
document["layouts"]["large"]["current_speed"]["enabled"] = False
for profile in ("large", "compact"):
for key in ("steering_wheel", "torque_bar"):
document["layouts"][profile][key]["enabled"] = False
return document
class AppearanceSettingsTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.params = Params(temporary.name)
storage = tempfile.TemporaryDirectory()
self.addCleanup(storage.cleanup)
self.storage = Path(storage.name)
self.parked = True
self.owner = AppearanceOwner(self.params, lambda: self.parked)
def row(self, key, profile=Profile.LARGE):
return next(row for row in self.owner.snapshot(profile).rows if row.key == key)
def test_absent_defaults_are_read_only_and_match_both_views(self):
self.assertEqual(onroad_appearance(self.params), OnroadAppearance())
self.assertEqual(tuple(row.key for row in self.owner.snapshot(Profile.LARGE).rows),
("DeveloperSidebar", "HidePersonalityAlerts", "ShowBrakeStatus", "BlindSpotMetrics", "SignalMetrics"))
self.assertEqual(tuple(row.key for row in self.owner.snapshot(Profile.COMPACT).rows),
("CameraView", "DriverCamera", "StockConfidenceBallWidget", "HidePersonalityAlerts", "ShowBrakeStatus",
"HideLeadMarker", "LeadInfo", "SignalMetrics", "BlindSpotMetrics"))
# Widget visibility, the stopped timer and Rainbow Road moved into the onroad layout.
for profile in Profile:
self.assertFalse(set(RETIRED_DISPLAY_KEYS) & {row.key for row in self.owner.snapshot(profile).rows})
for key in ("HideSpeed", "HideMaxSpeed", "HideSteeringWheel", "DriverCamera", "StoppedTimer",
"StockConfidenceBallWidget", "EnableTorqueBarWidget", "RainbowPath", "HideLeadMarker", "SignalMetrics", "BlindSpotMetrics"):
self.assertIsNone(read_visibility(self.params, key).raw)
self.assertEqual(self.params.get_type(key).name, "BOOL")
self.assertEqual(self.row("SignalMetrics").value, "Off")
self.assertEqual(self.row("BlindSpotMetrics").value, "On")
self.assertEqual((self.row("StockConfidenceBallWidget", Profile.COMPACT).label,
self.row("StockConfidenceBallWidget", Profile.COMPACT).value),
("Use StarPilot Widgets", "On"))
self.assertEqual(self.row("HideLeadMarker", Profile.COMPACT).value, "Off")
self.assertEqual(self.row("CameraView", Profile.COMPACT).value, "Standard")
self.assertEqual(self.row("DriverCamera", Profile.COMPACT).value, "Off")
self.assertIn("border highlights apply to C4 only", self.owner.snapshot(Profile.LARGE).subtitle)
def test_developer_sidebar_saved_choice_defaults_repair_and_source_guard(self):
self.assertFalse(onroad_appearance(self.params).show_developer_sidebar)
self.assertIsNone(read_visibility(self.params, "DeveloperSidebar").raw)
self.assertEqual(self.params.get_default_value("DeveloperSidebar"), False)
self.assertNotIn("DeveloperSidebar", {row.key for row in self.owner.snapshot(Profile.COMPACT).rows})
row = self.row("DeveloperSidebar")
self.assertEqual((row.label, row.value, row.default_value), ("Developer Sidebar", "Off", "Off"))
request = row_change(row)
assert request is not None
self.parked = False
self.assertFalse(self.owner.apply(request))
self.parked = True
self.assertTrue(self.owner.apply(request))
self.assertTrue(onroad_appearance(self.params).show_developer_sidebar)
self.assertFalse(self.owner.apply(request), "a stale toggle must not overwrite another edit")
request = row_change(self.row("DeveloperSidebar"))
assert request is not None
self.assertTrue(self.owner.apply(request))
self.assertFalse(onroad_appearance(self.params).show_developer_sidebar)
source = Path(self.params.get_param_path("DeveloperSidebar"))
source.write_bytes(b"bad")
self.assertFalse(onroad_appearance(self.params).show_developer_sidebar)
self.assertEqual(source.read_bytes(), b"bad")
request = row_change(self.row("DeveloperSidebar"))
assert request is not None
self.assertTrue(self.owner.apply(request))
self.assertEqual(source.read_bytes(), b"0")
def test_personality_alert_visibility_is_default_off_and_widget_independent(self):
from openpilot.starpilot.ui.onroad_customization import read_customization
before = read_customization(self.params)
self.assertFalse(onroad_appearance(self.params).hide_personality_alerts)
self.assertIsNone(self.params.get("HidePersonalityAlerts"))
self.assertEqual(self.params.get_type("HidePersonalityAlerts").name, "BOOL")
for profile in (Profile.LARGE, Profile.COMPACT):
row = self.row("HidePersonalityAlerts", profile)
self.assertEqual((row.label, row.value, row.default_value), ("Hide Personality Alerts", "Off", "Off"))
request = row_change(row)
assert request is not None
self.parked = False
self.assertFalse(self.owner.apply(request))
self.parked = True
self.assertTrue(self.owner.apply(request))
self.assertTrue(onroad_appearance(self.params).hide_personality_alerts)
request = row_change(self.row("HidePersonalityAlerts", profile))
assert request is not None
self.assertTrue(self.owner.apply(request))
self.assertFalse(onroad_appearance(self.params).hide_personality_alerts)
self.assertEqual(read_customization(self.params), before)
path = Path(self.params.get_param_path("HidePersonalityAlerts"))
path.write_bytes(b"bad")
self.assertFalse(onroad_appearance(self.params).hide_personality_alerts)
self.assertEqual(path.read_bytes(), b"bad")
def test_developer_sidebar_external_changes_refresh_without_restart(self):
from openpilot.starpilot.ui.tests.test_runtime_snapshot import RuntimeSnapshotAdapter
ui = ui_fake()
ui.params = self.params
adapter = RuntimeSnapshotAdapter(ui)
self.assertFalse(adapter.build(ShellMode.ONROAD, now_ns=NOW).onroad.appearance.show_developer_sidebar)
self.params.put_bool("DeveloperSidebar", True, block=True)
self.assertTrue(adapter.build(ShellMode.ONROAD, now_ns=NOW + 1_000_000_000).onroad.appearance.show_developer_sidebar)
self.params.put_bool("DeveloperSidebar", False, block=True)
self.assertFalse(adapter.build(ShellMode.ONROAD, now_ns=NOW + 2_000_000_000).onroad.appearance.show_developer_sidebar)
def test_compact_border_saved_controls_repair_and_parked_source_guard(self):
amber = row_change(self.row("SignalMetrics", Profile.COMPACT))
assert amber is not None
self.assertTrue(self.owner.apply(amber))
self.assertTrue(onroad_appearance(self.params).show_signal_border)
self.assertFalse(self.owner.apply(amber), "the same source cannot be confirmed twice")
red = row_change(self.row("BlindSpotMetrics", Profile.COMPACT), -1)
assert red is not None
self.assertTrue(self.owner.apply(red))
self.assertFalse(onroad_appearance(self.params).show_blindspot_border)
source = Path(self.params.get_param_path("BlindSpotMetrics"))
source.write_bytes(b"bad")
repair = row_change(self.row("BlindSpotMetrics", Profile.COMPACT))
assert repair is not None
self.assertEqual(repair.value, "On")
self.parked = False
self.assertFalse(self.owner.apply(repair))
self.assertEqual(source.read_bytes(), b"bad")
self.parked = True
self.assertTrue(self.owner.apply(repair))
self.assertEqual(source.read_bytes(), b"1")
def test_staged_camera_and_visibility_edits_preserve_competing_values(self):
actual_fsync = os.fsync
for key, concurrent in (("CameraView", b"3"), ("HideLeadMarker", b"1")):
with self.subTest(key=key):
path = Path(self.params.get_param_path(key))
request = row_change(self.row(key, Profile.COMPACT))
assert request is not None
calls = 0
def stage_fsync(fd, path=path, concurrent=concurrent):
nonlocal calls
actual_fsync(fd)
calls += 1
if calls == 1:
path.write_bytes(concurrent)
with patch.object(saved_document.os, "fsync", side_effect=stage_fsync):
self.assertFalse(self.owner.apply(request))
self.assertEqual(path.read_bytes(), concurrent)
def test_staged_camera_and_visibility_edits_require_parked_until_commit(self):
actual_fsync = os.fsync
for key in ("CameraView", "HideLeadMarker"):
with self.subTest(key=key):
self.parked = True
request = row_change(self.row(key, Profile.COMPACT))
assert request is not None
def revoke(fd):
actual_fsync(fd)
self.parked = False
with patch.object(saved_document.os, "fsync", side_effect=revoke):
self.assertFalse(self.owner.apply(request))
self.assertFalse(Path(self.params.get_param_path(key)).exists())
def test_widgets_choice_inverts_legacy_stock_ball_without_rewriting_it(self):
self.assertNotIn("StockConfidenceBallWidget", (row.key for row in self.owner.snapshot(Profile.LARGE).rows))
legacy = Path(self.params.get_param_path("StockConfidenceBallWidget"))
legacy.write_bytes(b"1")
self.assertEqual(self.row("StockConfidenceBallWidget", Profile.COMPACT).value, "Off")
request = row_change(self.row("StockConfidenceBallWidget", Profile.COMPACT))
assert request is not None
self.parked = False
self.assertFalse(self.owner.apply(request))
self.parked = True
self.assertTrue(self.owner.apply(request))
self.assertEqual(legacy.read_bytes(), b"0")
self.assertFalse(onroad_appearance(self.params).show_stock_confidence_ball)
self.assertFalse(self.owner.apply(request))
def test_lead_indicator_inverts_exact_saved_choice_and_repairs_invalid_bytes(self):
self.assertNotIn("HideLeadMarker", (row.key for row in self.owner.snapshot(Profile.LARGE).rows))
source = Path(self.params.get_param_path("HideLeadMarker"))
self.assertIsNone(read_visibility(self.params, "HideLeadMarker").raw)
self.assertFalse(onroad_appearance(self.params).show_lead_indicator)
initial = self.row("HideLeadMarker", Profile.COMPACT)
self.assertEqual((initial.value, initial.source), ("Off", None))
request = row_change(initial)
assert request is not None
self.parked = False
self.assertFalse(self.owner.apply(request))
self.parked = True
self.assertTrue(self.owner.apply(request))
self.assertEqual(source.read_bytes(), b"0")
self.assertTrue(onroad_appearance(self.params).show_lead_indicator)
self.assertFalse(self.owner.apply(request), "stale source must not overwrite")
hide = row_change(self.row("HideLeadMarker", Profile.COMPACT))
assert hide is not None
self.assertTrue(self.owner.apply(hide))
self.assertEqual(source.read_bytes(), b"1")
self.assertFalse(onroad_appearance(self.params).show_lead_indicator)
source.write_bytes(b"bad")
invalid = self.row("HideLeadMarker", Profile.COMPACT)
self.assertEqual((invalid.value, invalid.repair_value), ("Invalid saved choice", "Off"))
repair = row_change(invalid)
assert repair is not None
self.assertTrue(self.owner.apply(repair))
self.assertEqual(source.read_bytes(), b"1")
def test_manager_materializes_lead_hidden_default_without_replacing_saved_choice(self):
from openpilot.system.manager import manager
class AfterDefaults(Exception):
pass
def initialize_defaults():
with patch.object(manager, "Params", return_value=self.params), \
patch.object(manager, "prepare_manager_start"), patch.object(manager, "starpilot_storage_root", return_value=self.storage), \
patch.object(manager, "save_bootlog"), \
patch.object(manager, "get_build_metadata", return_value=NS(release_channel=False)), \
patch.object(manager.Paths, "shm_path", side_effect=AfterDefaults):
with self.assertRaises(AfterDefaults):
manager.manager_init()
source = Path(self.params.get_param_path("HideLeadMarker"))
self.assertIsNone(read_visibility(self.params, "HideLeadMarker").raw)
self.assertIs(self.params.get_default_value("HideLeadMarker"), True)
initialize_defaults()
self.assertEqual(source.read_bytes(), b"1")
self.assertFalse(onroad_appearance(self.params).show_lead_indicator)
source.write_bytes(b"0")
initialize_defaults()
self.assertEqual(source.read_bytes(), b"0")
self.assertTrue(onroad_appearance(self.params).show_lead_indicator)
def test_stock_confidence_rail_requires_fresh_source_and_road_camera(self):
from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice
view = onroad.OnroadView.__new__(onroad.OnroadView)
view.navigation = Mock()
view.background_layer = None
view.projection_viewport = None
class Fonts(BitmapFonts):
def __init__(self):
self.profile = Profile.COMPACT
view.fonts = Fonts()
view.camera_layer = None
view.pip_layer = None
view.extra_overlays = None
view.alert = Mock()
view.compact_hud = Mock()
view.compact_sidebar = Mock()
view.torque_bar = Mock()
view.stock_confidence_layer = Mock()
view.stock_confidence_reset = Mock()
view._fade = None
base = OnroadState(engaged=True, camera_available=False, speed_mps=12, cruise_kph=60,
speed_limit=SpeedLimitObservation(),
appearance=OnroadAppearance(show_stock_confidence_ball=True),
stock_confidence_source_fresh=True,
stock_confidence_source_stamp_ns=1_000_000_000,
stock_confidence_drive_frame=3)
with patch.object(onroad.clip, "begin_scissor_mode"), patch.object(onroad.clip, "end_scissor_mode"), \
patch.object(onroad.rl, "draw_rectangle_rec"), patch.object(onroad.rl, "draw_rectangle_gradient_v"), \
patch.object(onroad.rl, "draw_rectangle"), patch.object(onroad.rl, "draw_texture_ex"), \
patch.object(onroad.rl, "draw_rectangle_rounded_lines_ex"), patch.object(onroad, "render_compact_half_borders"), \
patch.object(view, "_prepare_compact_fade"), patch.object(view, "_slc_actions"):
view.render(base)
view.stock_confidence_layer.assert_called_once()
view.stock_confidence_reset.assert_called_once()
view.compact_sidebar.render.assert_not_called()
view.stock_confidence_layer.reset_mock()
view.render(replace(base, stock_confidence_source_stamp_ns=1_050_000_000))
view.stock_confidence_reset.assert_called_once()
view.render(replace(base, stock_confidence_source_stamp_ns=1_301_000_000))
self.assertEqual(view.stock_confidence_reset.call_count, 2)
for state in (replace(base, stock_confidence_source_fresh=False),
replace(base, appearance=OnroadAppearance()),
replace(base, appearance=OnroadAppearance(show_stock_confidence_ball=True,
camera_view=CameraViewChoice.NONE))):
view.render(state)
self.assertEqual(view.compact_sidebar.render.call_count, 3)
self.assertEqual(view.stock_confidence_reset.call_count, 2)
view.render(replace(base, stock_confidence_source_stamp_ns=1_351_000_000))
self.assertEqual(view.stock_confidence_reset.call_count, 3)
view.render(replace(base, stock_confidence_source_stamp_ns=1_401_000_000,
stock_confidence_drive_frame=4))
self.assertEqual(view.stock_confidence_reset.call_count, 4)
view.stock_confidence_layer.reset_mock()
for state in (replace(base, appearance=OnroadAppearance(show_stock_confidence_ball=True,
camera_view=CameraViewChoice.DRIVER)),
replace(base, reverse_driver_camera=True)):
view.render(state)
self.assertEqual(view.compact_sidebar.render.call_count, 3)
view.stock_confidence_layer.assert_not_called()
def test_native_stock_confidence_reset_clears_previous_drive_filter(self):
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
ball = ConfidenceBall.__new__(ConfidenceBall)
ball._confidence_filter = FirstOrderFilter(0.9, 0.5, 1 / 20)
view = Mock()
view._confidence_ball = ball
AugmentedRoadView.reset_stock_confidence_layer(view)
self.assertEqual(ball._confidence_filter.x, -0.5)
ball.update_filter(0.5)
self.assertGreater(ball._confidence_filter.x, -0.5)
def test_native_stock_confidence_aol_context_and_cleanup(self):
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.onroad import confidence_ball as native_ball
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import UIStatus
from openpilot.starpilot.ui.runtime_app import render_stock_confidence
ball = native_ball.ConfidenceBall.__new__(native_ball.ConfidenceBall)
ball._demo = False
ball._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / 20)
ball._render_lateral_active = False
rect = rl.Rectangle(0, 0, 536, 240)
ball._rect = rect
model = NS(meta=NS(disengagePredictions=NS(brakeDisengageProbs=[0.1], steerOverrideProbs=[0.1])))
native_state = NS(status=UIStatus.DISENGAGED, sm={"modelV2": model})
colors = []
def render(_rect):
ball._update_state()
ball._render(None)
with patch.object(native_ball, "ui_state", native_state), patch.object(ball, "render", side_effect=render), \
patch.object(native_ball, "draw_circle_gradient", side_effect=lambda *args: colors.append(args[-2:])):
ball.render_with_lateral(rect, False)
self.assertEqual(ball._confidence_filter.x, -0.5)
self.assertEqual(colors[-1][0].r, 50)
self.assertFalse(ball._render_lateral_active)
camera = Mock()
camera.render_stock_confidence_layer.side_effect = lambda _rect, *, lateral_active: ball.render_with_lateral(_rect, lateral_active)
state = OnroadState(engaged=True, camera_available=True, speed_mps=12, cruise_kph=60,
speed_limit=SpeedLimitObservation(), lateral_active=True,
stock_confidence_source_fresh=True, stock_confidence_source_stamp_ns=1,
stock_confidence_drive_frame=1)
render_stock_confidence(camera, rect, state)
camera.render_stock_confidence_layer.assert_called_once_with(rect, lateral_active=True)
self.assertGreater(ball._confidence_filter.x, -0.5)
self.assertEqual(colors[-1][0].r, 255)
self.assertFalse(ball._render_lateral_active)
native_state.status = UIStatus.OVERRIDE
ball.render_with_lateral(rect, True)
self.assertEqual((colors[-1][0].r, colors[-1][0].g, colors[-1][0].b), (255, 255, 255))
self.assertFalse(ball._render_lateral_active)
with patch.object(ball, "render", side_effect=RuntimeError("render failed")):
with self.assertRaises(RuntimeError):
ball.render_with_lateral(rect, True)
self.assertFalse(ball._render_lateral_active)
native_view = Mock()
native_view._confidence_ball = Mock()
AugmentedRoadView.render_stock_confidence_layer(native_view, rect, lateral_active=True)
native_view._confidence_ball.render_with_lateral.assert_called_once_with(rect, True)
def test_edit_repair_and_stale_source_guards(self):
request = row_change(self.row("HidePersonalityAlerts"))
assert request is not None
self.assertEqual(request.value, "On")
self.assertTrue(self.owner.apply(request))
self.assertTrue(onroad_appearance(self.params).hide_personality_alerts)
self.assertFalse(self.owner.apply(request))
corrupt_path = Path(self.params.get_param_path("SignalMetrics"))
corrupt_path.write_bytes(b"\xff")
row = self.row("SignalMetrics")
self.assertEqual(row.value, "Invalid saved choice")
self.assertEqual(corrupt_path.read_bytes(), b"\xff")
repair = row_change(row)
assert repair is not None
self.assertEqual(repair.value, "Off")
corrupt_path.write_bytes(b"2")
self.assertFalse(self.owner.apply(repair))
self.assertEqual(corrupt_path.read_bytes(), b"2")
repaired = row_change(self.row("SignalMetrics"))
assert repaired is not None
self.assertTrue(self.owner.apply(repaired))
self.assertEqual(corrupt_path.read_bytes(), b"0")
def test_parked_final_read_and_oversized_source_guards(self):
request = row_change(self.row("BlindSpotMetrics"))
assert request is not None
self.parked = False
self.assertFalse(self.owner.apply(request))
self.assertIsNone(read_visibility(self.params, "BlindSpotMetrics").raw)
self.parked = True
path = Path(self.params.get_param_path("BlindSpotMetrics"))
checks = 0
def parked_once():
nonlocal checks
checks += 1
return checks == 1
owner = AppearanceOwner(self.params, parked_once)
self.assertFalse(owner.apply(request))
self.assertIsNone(read_visibility(self.params, "BlindSpotMetrics").raw)
original = b"1" * 100_000
path.write_bytes(original)
row = self.row("BlindSpotMetrics")
self.assertFalse(row.available)
self.assertIsNone(row_change(row))
path.write_bytes(original[:-1] + b"0")
self.assertFalse(self.owner.apply(FeatureSettingsRequest(row.key, row.source, "Off")))
self.assertEqual(path.read_bytes(), original[:-1] + b"0")
path.unlink()
original_path = self.params.get_param_path
reads = 0
def get_path(key):
nonlocal reads
reads += 1
if reads == 2:
raise OSError("source unreadable at final guard")
return original_path(key)
with patch.object(self.params, "get_param_path", side_effect=get_path):
self.assertFalse(self.owner.apply(request))
self.assertIsNone(read_visibility(self.params, "BlindSpotMetrics").raw)
def test_nonregular_sources_are_unavailable_without_reading_or_repair(self):
path = Path(self.params.get_param_path("SignalMetrics"))
target = path.parent / "elsewhere"
target.write_bytes(b"1")
path.symlink_to(target)
self.assertFalse(read_visibility(self.params, "SignalMetrics").readable)
self.assertFalse(self.row("SignalMetrics").available)
path.unlink()
os.mkfifo(path)
self.assertFalse(read_visibility(self.params, "SignalMetrics").readable)
self.assertFalse(self.row("SignalMetrics").available)
self.assertTrue(path.is_fifo())
def test_large_tile_press_and_compact_visuals_refresh(self):
emitted = []
settings = SettingsState()
x, y, width, height = tile_rects(settings)[4]
touch = SettingsInput(Profile.LARGE, emitted.append)
touch.press(x + width / 2, y + height / 2, settings)
touch.release(x + width / 2, y + height / 2, settings)
self.assertEqual(emitted[0].destination.destination, Destination.APPEARANCE)
changed = []
large = FeatureInput(changed.append)
state = self.owner.snapshot(Profile.LARGE)
large.press(1980, 300, state)
large.move(1800, 300, state)
large.release(1800, 300, state)
self.assertEqual(changed, [], "the remaining rows fit on one page")
large.press(1980, 300, state)
large.release(1980, 300, state)
self.assertEqual(changed[0].row.key, "DeveloperSidebar")
class Button:
def __init__(self, label, value):
self.label, self.value, self.click = label, value, None
def set_click_callback(self, callback):
self.click = callback
class Scroller:
def __init__(self):
self._scroller = self
self.items = []
def add_widgets(self, cards):
self.items.extend(cards)
class Session:
def __init__(inner):
inner.feature_owner = FeatureSettingsOwner(self.params, lambda group: self.parked,
vehicle_fingerprint=lambda: "TEST CAR")
def appearance_snapshot(inner):
return self.owner.snapshot(Profile.COMPACT)
def pip_snapshot(inner):
return FeatureSettingsState()
def appearance_request(inner, request):
return self.owner.apply(request)
def feature_snapshot(inner, page):
return inner.feature_owner.snapshot(page, parked=self.parked, system_long=self.parked,
lateral_context=False, metric=False)
def feature_request(inner, request):
return inner.feature_owner.apply(request)
shown = []
with patch.object(appearance_compact, "BigButton", Button), patch.object(appearance_compact, "GreyBigButton", Button), \
patch.object(appearance_compact, "NavScroller", Scroller), patch.object(appearance_compact.gui_app, "push_widget", shown.append):
appearance_compact.AppearanceCompact(Session()).entry_button().click()
page = shown[0]
self.assertEqual(page.items[0].label, "visuals")
next(item for item in page.items if item.label == "hide personality alerts").click()
self.assertTrue(read_visibility(self.params, "HidePersonalityAlerts").value)
self.assertEqual(next(item for item in page.items if item.label == "hide personality alerts").value, "on")
def test_runtime_snapshot_reads_saved_choice_and_large_session_routes_back(self):
from openpilot.starpilot.ui.runtime_app import StarShellSession
from openpilot.starpilot.ui.runtime_snapshot import RuntimeSnapshotAdapter
from openpilot.starpilot.ui.feature_settings_state import FeatureUiAction
ui = ui_fake()
ui.params = self.params
adapter = RuntimeSnapshotAdapter(ui)
from openpilot.starpilot.ui import runtime_snapshot
with patch.object(runtime_snapshot, "onroad_appearance", wraps=onroad_appearance) as reads:
before = adapter.build(ShellMode.ONROAD, now_ns=NOW).onroad
adapter.build(ShellMode.ONROAD, now_ns=NOW + 500_000_000)
self.assertEqual(reads.call_count, 1)
adapter.build(ShellMode.ONROAD, now_ns=NOW + 1_000_000_000)
self.assertEqual(reads.call_count, 2)
adapter.build(ShellMode.SETTINGS, now_ns=NOW + 1_000_000_001)
adapter.build(ShellMode.ONROAD, now_ns=NOW + 1_000_000_002)
self.assertEqual(reads.call_count, 3)
self.assertEqual(before.appearance, OnroadAppearance())
session = StarShellSession.__new__(StarShellSession)
session._mode = ShellMode.SETTINGS
session.selected = Destination.APPEARANCE
session.appearance_owner = self.owner
session.adapter = adapter
session.appearance_scroll = 0
session._snapshot_cache = None
session.input = ShellInput(Profile.LARGE, lambda _: None)
session.profile = Profile.LARGE
session._unavailable = Mock()
session._appearance_ui(FeatureUiAction("change", self.row("HidePersonalityAlerts")))
self.assertTrue(adapter.build(ShellMode.ONROAD, now_ns=NOW + 1_000_000_003).onroad.appearance.hide_personality_alerts)
session._appearance_ui(FeatureUiAction("back"))
self.assertEqual(session.selected, Destination.STAR)
def test_onroad_widget_visibility_and_wheel_request_cancel(self):
base = OnroadState(engaged=True, camera_available=False, speed_mps=12, cruise_kph=60,
speed_limit=SpeedLimitObservation(), experimental_enabled=False,
experimental_available=True)
fonts = Mock(spec=BitmapFonts, profile=Profile.LARGE)
fonts.measure.return_value = NS(width=10, height=20)
fonts.vertical_ink.side_effect = lambda text, role, size: (0., size * .7)
fonts.horizontal_ink.return_value = (0., 10.)
with patch("openpilot.starpilot.ui.onroad_large_widgets.draw_control_card") as card, patch.object(rl, "draw_line_ex"):
UnifiedSpeedWidget(fonts).render(rl.Rectangle(30, 30, 1800, 1020), base)
card.assert_not_called()
from openpilot.starpilot.ui.tests.test_unified_speed_native import foreground_calls
self.assertEqual(foreground_calls(fonts.draw)[0].args[3:5], (192, 90))
for profile in (Profile.LARGE, Profile.COMPACT):
view = onroad.OnroadView.__new__(onroad.OnroadView)
view.navigation = Mock()
view.background_layer = None
view.projection_viewport = None
object.__setattr__(view, "fonts", type("Fonts", (), {"profile": profile})())
view.navigation_favorites = onroad.NavigationFavorites(view.fonts)
view.camera_layer = None
view.extra_overlays = None
view.alert = Mock()
view.torque_bar = Mock()
view.unified_speed = Mock()
view.projection_viewport = None
view.background_layer = None
view._corner_cache = Mock()
view.current_speed = Mock()
view.steering_wheel = Mock()
view.compact_hud = Mock()
view.compact_sidebar = Mock()
view._fade = None
with patch.object(onroad.clip, "begin_scissor_mode"), patch.object(onroad.clip, "end_scissor_mode"), \
patch.object(onroad.rl, "draw_rectangle_rec"), patch.object(onroad.rl, "draw_rectangle_gradient_v"), \
patch.object(onroad.rl, "draw_rectangle_lines_ex"), patch.object(onroad.rl, "draw_rectangle"), \
patch.object(onroad.rl, "draw_texture_ex"), patch.object(onroad.rl, "draw_rectangle_rounded_lines_ex"), \
patch.object(onroad, "render_corner_hint"), patch.object(view, "_prepare_compact_fade"), \
patch.object(view, "_slc_actions") as slc:
view.render(base)
view.torque_bar.render.assert_called_once()
if profile == Profile.LARGE:
view.unified_speed.render.assert_called_once()
view.current_speed.render.assert_called_once()
view.steering_wheel.render.assert_called_once()
view.torque_bar.reset_mock()
hidden = replace(base, customization=_hidden_widgets(), alert=OnroadAlert(AlertSize.NONE, "", "", True))
view.render(hidden)
view.torque_bar.render.assert_not_called()
view.alert.render.assert_called()
if profile == Profile.LARGE:
self.assertEqual(view.unified_speed.render.call_count, 2)
self.assertEqual(slc.call_count, 2)
view.current_speed.render.assert_called_once()
view.steering_wheel.render.assert_called_once()
for alert_size in (AlertSize.SMALL, AlertSize.MID, AlertSize.FULL):
view.alert.render.reset_mock()
view.render(replace(hidden, alert=OnroadAlert(alert_size, "keep warning", "", True)))
view.alert.render.assert_called_once()
actions = []
touch = OnroadInput(actions.append, Profile.LARGE)
touch.press(1600, 100, base)
touch.release(1600, 100, replace(base, customization=_hidden_widgets()))
self.assertFalse(actions)
touch.press(1600, 100, replace(base, customization=_hidden_widgets()))
touch.release(1600, 100, base)
self.assertFalse(actions)
if __name__ == "__main__":
unittest.main()