mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
203 lines
9.6 KiB
Python
203 lines
9.6 KiB
Python
from dataclasses import replace
|
|
from types import SimpleNamespace as NS
|
|
from typing import cast
|
|
from unittest.mock import Mock
|
|
|
|
import pyray as rl
|
|
import pytest
|
|
|
|
from openpilot.selfdrive.ui.mici.onroad import long_indicator as native
|
|
from openpilot.starpilot.ui import onroad_personality as indicator
|
|
from openpilot.starpilot.ui.onroad_customization import default_document
|
|
from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadAlert, OnroadState, SpeedLimitObservation
|
|
from openpilot.starpilot.ui.presentation import BitmapFonts, Profile
|
|
from openpilot.starpilot.ui.traffic_status import TrafficDisplay
|
|
|
|
|
|
def road(**changes):
|
|
return replace(OnroadState(True, False, 15, 80, SpeedLimitObservation(),
|
|
longitudinal_active=True, lead_detected=True), **changes)
|
|
|
|
|
|
@pytest.fixture
|
|
def display(monkeypatch):
|
|
monkeypatch.setattr(indicator.gui_app, 'texture', lambda path: NS(path=path, width=200, height=200))
|
|
monkeypatch.setattr(rl, 'get_time', lambda: 1.)
|
|
fonts = cast(BitmapFonts, NS(profile=Profile.COMPACT, measure=Mock(return_value=NS(width=20)), draw=Mock()))
|
|
widget = indicator.PersonalityWidget(fonts)
|
|
widget._draw_centered = Mock()
|
|
return widget
|
|
|
|
|
|
def settle(widget, state):
|
|
for _ in range(90):
|
|
widget.render(state)
|
|
return widget._draw_centered.call_args_list[-8:]
|
|
|
|
|
|
@pytest.mark.parametrize('profile', list(Profile))
|
|
@pytest.mark.parametrize('personality', range(3))
|
|
def test_same_selected_bars_and_lead_on_both_profiles(display, profile, personality):
|
|
display.fonts.profile = profile
|
|
draws = settle(display, road(personality=personality))
|
|
assert draws[1].args[3] > .999
|
|
assert all(draws[2 + 2*i].args[3] == pytest.approx(.9 if i <= personality else .35, abs=.001) for i in range(3))
|
|
for has_lead, active, white, green in ((False, True, .35, 0), (True, False, 1, 0), (True, True, 0, 1)):
|
|
draws = settle(display, road(lead_detected=has_lead, longitudinal_active=active))
|
|
assert draws[0].args[3] == pytest.approx(white, abs=.001)
|
|
assert draws[1].args[3] == pytest.approx(green, abs=.001)
|
|
|
|
|
|
@pytest.mark.parametrize('traffic', (False, True))
|
|
def test_art_scales_uniformly_without_clipping_or_downsized_source_textures(display, monkeypatch, traffic):
|
|
def draw_all_art_opaque(art, rect, y, alpha, *args, **kwargs):
|
|
return indicator.PersonalityWidget._draw_centered(art, rect, y, 1., *args, **kwargs)
|
|
display._draw_centered = draw_all_art_opaque
|
|
draw = Mock()
|
|
monkeypatch.setattr(rl, 'draw_texture_pro', draw)
|
|
state = road(traffic_mode=traffic, traffic_display=TrafficDisplay('active', 'TRAFFIC') if traffic else None)
|
|
bounds = []
|
|
for profile in (Profile.COMPACT, Profile.LARGE):
|
|
display.fonts.profile = profile
|
|
draw.reset_mock()
|
|
display.render(state)
|
|
x, y, width, height = (476, 160, 60, 80) if profile == Profile.COMPACT else (88, 804, 150, 200)
|
|
destinations = [call.args[2] for call in draw.call_args_list]
|
|
assert len(destinations) == 8
|
|
assert all(x <= r.x < r.x + r.width <= x + width and y <= r.y < r.y + r.height <= y + height for r in destinations)
|
|
bounds.append([(r.x-x, r.y-y, r.width, r.height) for r in destinations])
|
|
assert all(call.args[1].width == 200 for call in draw.call_args_list)
|
|
for compact, large in zip(*bounds, strict=True):
|
|
assert large == pytest.approx(tuple(value * 2.5 for value in compact), abs=.0001)
|
|
|
|
|
|
def test_selection_highlight_waits_for_alert_and_then_expires(display, monkeypatch):
|
|
settle(display, road(personality=1))
|
|
monkeypatch.setattr(rl, 'get_time', lambda: 1.1)
|
|
assert settle(display, road(personality=2))[7].args[3] > .999
|
|
monkeypatch.setattr(rl, 'get_time', lambda: 2.)
|
|
settle(display, road(personality=2, alert=OnroadAlert(AlertSize.MID)))
|
|
monkeypatch.setattr(rl, 'get_time', lambda: 4.)
|
|
assert settle(display, road(personality=2))[7].args[3] > .999
|
|
monkeypatch.setattr(rl, 'get_time', lambda: 4.6)
|
|
assert settle(display, road(personality=2))[7].args[3] < .001
|
|
|
|
|
|
def test_gas_override_blinks_only_selected_bars_and_clears_on_disengagement(display, monkeypatch):
|
|
monkeypatch.setattr(rl, 'get_time', lambda: 1.1)
|
|
state = road(personality=1, gas_pressed_override=True)
|
|
draws = settle(display, state)
|
|
assert draws[2].args[3] == pytest.approx(.35, abs=.001)
|
|
assert draws[4].args[3] == pytest.approx(.35, abs=.001)
|
|
assert draws[6].args[3] == pytest.approx(.35, abs=.001)
|
|
display.render(replace(state, longitudinal_active=False))
|
|
draws = display._draw_centered.call_args_list[-8:]
|
|
assert draws[1].args[3] == 0
|
|
assert draws[2].args[3] == pytest.approx(.9, abs=.001)
|
|
monkeypatch.setattr(rl, 'get_time', lambda: 1.2)
|
|
assert settle(display, state)[2].args[3] == pytest.approx(.9, abs=.001)
|
|
|
|
|
|
@pytest.mark.parametrize('profile', list(Profile))
|
|
def test_traffic_label_and_accent_restore_selected_personality(display, profile):
|
|
display.fonts.profile = profile
|
|
state = road(personality=2, traffic_mode=True, traffic_display=TrafficDisplay('active', 'TRAFFIC'))
|
|
draws = settle(display, state)
|
|
assert draws[2].args[4] == (200, 32, 48)
|
|
assert draws[4].args[3] == pytest.approx(.35, abs=.001)
|
|
assert all(draws[i].args[3] < .001 for i in (3, 5, 7))
|
|
assert display.fonts.draw.call_args.args[0] == 'TRF'
|
|
for status, label in (('paused', 'PAUSE'), ('off', 'OFF')):
|
|
draws = settle(display, replace(state, traffic_mode=False, traffic_display=TrafficDisplay(status, '')))
|
|
assert all(draws[i].args[3] == pytest.approx(.9, abs=.001) for i in (2, 4, 6))
|
|
assert display.fonts.draw.call_args.args[0] == label
|
|
|
|
|
|
def test_large_default_sits_left_of_driver_monitor_and_custom_positions_stay_put(display):
|
|
display.fonts.profile = Profile.LARGE
|
|
state = road(right_hand_drive=True)
|
|
display.render(state)
|
|
assert display._draw_centered.call_args.args[1].x == 1280
|
|
document = default_document()
|
|
document['layouts']['large']['following_distance'].update(x=600, y=300)
|
|
display.render(replace(state, customization=document))
|
|
rect = display._draw_centered.call_args.args[1]
|
|
assert (rect.x, rect.y) == (600, 300)
|
|
|
|
|
|
@pytest.mark.parametrize('profile', list(Profile))
|
|
def test_hidden_widget_and_driver_camera_do_not_paint(display, profile):
|
|
from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice
|
|
display.fonts.profile = profile
|
|
document = default_document()
|
|
document['layouts'][profile]['following_distance']['enabled'] = False
|
|
display.render(road(customization=document))
|
|
state = road()
|
|
display.render(replace(state, appearance=replace(state.appearance, camera_view=CameraViewChoice.DRIVER)))
|
|
display._draw_centered.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize('missing', ['longitudinal', 'engagement', 'drive'])
|
|
def test_native_recovery_indicator_keeps_its_original_availability_guard(monkeypatch, missing):
|
|
class Messages:
|
|
recv_frame = {'selfdriveState': 12}
|
|
def __getitem__(self, key):
|
|
return NS(enabled=missing != 'engagement')
|
|
sm = Messages()
|
|
ui = NS(sm=sm, started_frame=13 if missing == 'drive' else 10, has_longitudinal_control=missing != 'longitudinal')
|
|
monkeypatch.setattr(native, 'ui_state', ui)
|
|
widget = native.LongIndicator()
|
|
widget.indicator = Mock()
|
|
widget._render(rl.Rectangle(0, 0, 476, 240))
|
|
widget.indicator.draw.assert_not_called()
|
|
widget.indicator.reset.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize('profile', list(Profile))
|
|
@pytest.mark.parametrize('ordered', (False, True))
|
|
def test_shared_widget_is_in_both_layer_orders(display, profile, ordered):
|
|
from openpilot.starpilot.ui.onroad import OnroadView
|
|
from openpilot.starpilot.ui.onroad_compact_widgets import MiciSidebarWidgets
|
|
from openpilot.starpilot.ui.onroad_customization import DEFAULT_WIDGET_ORDER
|
|
display.fonts.profile = profile
|
|
view = OnroadView.__new__(OnroadView)
|
|
view.fonts, view.personality, view.projection_viewport = display.fonts, display, None
|
|
view.compact_sidebar = MiciSidebarWidgets(display.fonts)
|
|
view.compact_sidebar.personality = display
|
|
document = default_document()
|
|
if ordered:
|
|
document['widgetOrder'] = {profile: list(reversed(DEFAULT_WIDGET_ORDER[profile]))}
|
|
view._ordered_widgets(rl.Rectangle(0, 0, *profile.size), road(customization=document), only_widget='following_distance')
|
|
assert display._draw_centered.call_count == 8
|
|
|
|
|
|
@pytest.mark.parametrize('profile', list(Profile))
|
|
@pytest.mark.parametrize('moved', (False, True))
|
|
def test_routine_lane_guidance_keeps_widget_and_urgent_alerts_hide_it(display, profile, moved):
|
|
display.fonts.profile = profile
|
|
document = default_document()
|
|
if moved:
|
|
document['layouts'][profile]['following_distance'].update(x=260, y=20)
|
|
for kind, user_prompt, visible in (('preLaneChangeLeft', False, True), ('laneChange', False, True), ('laneChangeBlocked', True, False)):
|
|
display._draw_centered.reset_mock()
|
|
settle(display, road(customization=document, alert=OnroadAlert(AlertSize.SMALL, alert_type=kind + '/warning', user_prompt=user_prompt)))
|
|
draws = display._draw_centered.call_args_list
|
|
if profile == Profile.COMPACT and moved and not visible:
|
|
assert not draws
|
|
else:
|
|
assert (draws[-7].args[3] > .99) if visible else (draws[-7].args[3] < .001)
|
|
|
|
|
|
@pytest.mark.parametrize(('alpha', 'visible_alpha'), ((0., None), (.0001, None), (.0019, None),
|
|
(.002, 1), (1/255, 1), (.5, 128), (1., 255)))
|
|
def test_invisible_art_avoids_gpu_submission_without_losing_faint_visible_art(display, monkeypatch, alpha, visible_alpha):
|
|
display._prepare()
|
|
draw = Mock()
|
|
monkeypatch.setattr(rl, 'draw_texture_pro', draw)
|
|
indicator.PersonalityWidget._draw_centered(display._lead_car[0], rl.Rectangle(476, 160, 60, 80), 100, alpha)
|
|
if visible_alpha is None:
|
|
draw.assert_not_called()
|
|
else:
|
|
draw.assert_called_once()
|
|
assert draw.call_args.args[-1].a == visible_alpha
|