Files
StarPilot/openpilot/starpilot/ui/tests/test_speed_source_drawer_native.py
T
2026-10-26 20:38:11 -04:00

170 lines
9.0 KiB
Python

"""Same-cycle native source readings and UI-only drawer intent boundaries."""
from unittest.mock import Mock
from openpilot.starpilot.ui.presentation import BitmapFonts
from dataclasses import replace
from types import SimpleNamespace
import unittest
import pytest
from unittest.mock import patch
import pyray as rl
from openpilot.cereal import log, messaging
from openpilot.starpilot.speed_limits.runtime import Runtime
from openpilot.starpilot.speed_limits.runtime_settings import parse
from openpilot.starpilot.ui.tests.test_slc_ui_runtime import START, replay_inputs
from openpilot.starpilot.ui.onroad_state import OnroadState, OnroadInput, SpeedLimitObservation, ObservationKind, speed_limit_from_message
from openpilot.starpilot.ui.presentation import Profile, FontRole
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
from openpilot.starpilot.ui.runtime_snapshot import display_message
from openpilot.starpilot.ui.onroad_customization import default_document, validate_document
from openpilot.starpilot.ui import clip
class DrawerNativeTest(unittest.TestCase):
def test_qualified_source_rows_roundtrip_and_do_not_change_selection(self):
sm, cp = replay_inputs()
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCPriority1': 'Dashboard', 'SLCPriority2': 'Map Data'}),
session_id='drawer-native')
out = runtime.step(sm, cp, now_ns=START).message
with log.Event.from_bytes(out.to_bytes()) as decoded:
obs = speed_limit_from_message(decoded.slcState)
rows = {row.source: row for row in obs.source_readings}
self.assertEqual(rows['dashboard'].kind, 'valid')
assert rows['dashboard'].speed_mps is not None
self.assertAlmostEqual(rows['dashboard'].speed_mps, 25)
self.assertTrue(rows['dashboard'].enabled)
self.assertTrue(rows['map'].enabled)
self.assertEqual(rows['map'].kind, 'stale')
self.assertIsNone(rows['map'].speed_mps)
self.assertFalse(rows['online'].enabled)
self.assertEqual(obs.source, 'dashboard')
self.assertEqual(decoded.slcState.sourceReadings[0].source, 'dashboard')
self.assertEqual(speed_limit_from_message(None).source_readings, ())
empty = messaging.new_message('slcState')
self.assertEqual(len(empty.slcState.sourceReadings), 0)
def test_sources_have_no_onscreen_tap_target(self):
obs = SpeedLimitObservation(ObservationKind.VALID, 'dashboard', 25, session_id='drawer', presentation_id=1)
state = OnroadState(False, False, 20, 100, obs)
doc = default_document()
doc['speedSources'] = True
state = replace(state, customization=doc)
emitted = []
touch = OnroadInput(emitted.append, Profile.LARGE)
from openpilot.starpilot.ui.large_speed_geometry import CARD_HEIGHT
card = doc['layouts']['large']['cruise_limits']
left, top = card['x'], card['y'] + CARD_HEIGHT + 22
# The sources panel follows the Galaxy layout setting only; the old toggle row is not a touch target.
touch.press(left + 12, top + 12, state)
self.assertFalse(touch.claimed)
touch.release(left + 12, top + 12, state)
pending = replace(state, speed_limit=replace(obs, pending_speed_limit_mps=30, decision_id=2))
touch.press(left + 12, top + 12, pending)
touch.release(left + 12, top + 12, pending)
self.assertEqual(emitted, [])
def test_nested_drawer_clip_restores_parent_after_exception(self):
calls = []
with patch.object(rl, 'rl_draw_render_batch_active'), patch.object(rl, 'begin_scissor_mode', side_effect=lambda *v: calls.append(v)):
with self.assertRaises(ValueError):
with clip.clipped(rl.Rectangle(264, 271, 248, 215), rl.Rectangle(30, 30, 1800, 1020)):
raise ValueError('render failed')
self.assertEqual(calls, [(264, 271, 248, 215), (30, 30, 1800, 1020)])
def test_saved_document_flag_is_optional_strict_and_preserved(self):
doc = default_document()
self.assertNotIn('speedSources', validate_document(doc))
doc['speedSources'] = True
self.assertIs(validate_document(doc)['speedSources'], True)
doc['speedSourcesCard'] = True
self.assertIs(validate_document(doc)['speedSourcesCard'], True)
for key in ('speedSources', 'speedSourcesCard'):
with self.assertRaises(ValueError):
validate_document({**doc, key: 1})
self.assertNotIn('speedAlign', validate_document(default_document())) # Left, the original look, by default.
for align in ('left', 'center', 'right'):
self.assertEqual(validate_document({**doc, 'speedAlign': align})['speedAlign'], align)
for bad in ('justify', 1, None):
with self.assertRaises(ValueError):
validate_document({**doc, 'speedAlign': bad})
def test_sources_grow_the_cruise_widget_by_one_row_or_an_attached_card(self):
from openpilot.starpilot.ui.large_speed_geometry import CARD_HEIGHT, GAP, SOURCE_ROW_HEIGHT, SOURCE_TOP, WIDTH
from openpilot.starpilot.ui.onroad_customization import customization_metadata, widget_size
doc = default_document()
self.assertEqual(widget_size(doc, 'large', 'cruise_limits'), (WIDTH, CARD_HEIGHT))
doc['speedSources'] = True
self.assertEqual(widget_size(doc, 'large', 'cruise_limits'), (WIDTH, SOURCE_TOP + SOURCE_ROW_HEIGHT))
doc['speedSourcesCard'] = True
self.assertEqual(widget_size(doc, 'large', 'cruise_limits'), (WIDTH, SOURCE_TOP + GAP + SOURCE_ROW_HEIGHT))
self.assertEqual(customization_metadata()['profiles']['large']['widgets']['cruise_limits']['sourcesExtent'],
{'inline': SOURCE_TOP - CARD_HEIGHT + SOURCE_ROW_HEIGHT, 'card': SOURCE_TOP - CARD_HEIGHT + GAP + SOURCE_ROW_HEIGHT, 'actionGap': GAP})
def test_expired_producer_and_old_drive_transport_cannot_supply_readings(self):
sm, cp = replay_inputs()
runtime = Runtime(parse({'SpeedLimitController': True}), session_id='drawer-expiry')
runtime.step(sm, cp, now_ns=START)
expired = runtime.step(sm, cp, now_ns=START + 6_000_000_000).message.slcState
rows = {str(row.source): row for row in expired.sourceReadings}
self.assertNotEqual(str(rows['dashboard'].observationKind), 'valid')
fresh = SimpleNamespace(logMonoTime={'slcState': START}, alive={'slcState': True}, valid={'slcState': True}, recv_frame={'slcState': 2})
# A dict-backed transport exposes the same freshness fields as SubMaster.
class Transport(dict):
pass
transport = Transport(slcState=expired)
for key in ('logMonoTime', 'alive', 'valid', 'recv_frame'):
setattr(transport, key, getattr(fresh, key))
self.assertIsNone(display_message(transport, 'slcState', START + 200_000_001))
self.assertIsNone(display_message(transport, 'slcState', START, after_frame=2))
self.assertEqual(speed_limit_from_message(None).source_readings, ())
def test_disabled_and_invalid_rows_have_no_value_or_ui_authority(self):
sm, cp = replay_inputs()
runtime = Runtime(parse({'ShowSpeedLimits': False}), session_id='drawer-disabled')
message = runtime.step(sm, cp, now_ns=START).message.slcState
self.assertFalse(message.enabled)
self.assertTrue(all(not row.enabled for row in message.sourceReadings))
message.observationKind = 'valid'
message.speedLimit = 25
message.sessionId = 'diagnostic'
row = message.sourceReadings[0]
row.observationKind = 'valid'
row.speedLimit = float('nan')
rows = speed_limit_from_message(message).source_readings
self.assertEqual(rows[0].kind, 'unknown')
self.assertIsNone(rows[0].speed_mps)
row.observationKind = 'unknown'
row.speedLimit = 99
self.assertIsNone(speed_limit_from_message(message).source_readings[0].speed_mps)
def test_accepted_source_is_highlighted_instead_of_selected_equal_value_source(self):
from openpilot.starpilot.ui.onroad_state import SourceReading
from openpilot.starpilot.ui.tests.test_unified_speed_native import foreground_calls
obs = SpeedLimitObservation(
ObservationKind.VALID,
'vision',
25,
accepted_source='dashboard',
source_readings=(SourceReading('dashboard', True, 'valid', 25), SourceReading('vision', True, 'valid', 25)),
)
state = OnroadState(False, False, 20, 100, obs)
fonts = SimpleNamespace(profile=Profile.LARGE, measure=lambda *args: SimpleNamespace(width=20, height=20), draw=Mock())
widget = UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts)))
widget._draw_sources(rl.Rectangle(88, 545, 320, 76), state)
drawn = [(call.args[0], call.args[1]) for call in foreground_calls(fonts.draw)]
self.assertEqual([text for text, _ in drawn], ['Dash', '56', 'Vision', '56'])
self.assertIn(('56', FontRole.MEDIUM), drawn) # The accepted source's reading is emphasized.
self.assertIn(('56', FontRole.NORMAL), drawn)
@pytest.fixture(autouse=True)
def mock_sign_graphics(monkeypatch):
monkeypatch.setattr('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.outline', Mock())
monkeypatch.setattr('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.confirmation', Mock())
for name in ('draw_rectangle_rounded_lines_ex', 'draw_circle', 'draw_ring'):
monkeypatch.setattr('openpilot.starpilot.ui.speed_limit_widget.rl.' + name, Mock())