mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
170 lines
9.0 KiB
Python
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())
|