Files
StarPilot/openpilot/starpilot/ui/tests/test_onroad_customization.py
T

1077 lines
55 KiB
Python

from unittest.mock import Mock, call as mock_call
from pathlib import Path
from openpilot.starpilot.ui.presentation import BitmapFonts
import json
from dataclasses import replace
from types import SimpleNamespace as NS
from unittest.mock import patch
import pyray as rl
import pytest
from openpilot.starpilot.ui.appearance_preferences import OnroadAppearance
from openpilot.starpilot.ui.onroad_customization import (
CLOCK_WIDGET, DEFAULT_WIDGET_ORDER, MAX_BYTES, STOPPED_TIMER_WIDGET, customization_metadata, default_document, offset, read_customization,
validate_document, decode_document, widget_palette, rgba, widget_size)
from openpilot.starpilot.ui.onroad_compact_widgets import CompactHudRenderer
from openpilot.starpilot.ui.onroad_large_widgets import CurrentSpeedHud, UnifiedSpeedWidget, SteeringWheelWidget
from openpilot.starpilot.ui.onroad_state import (
AlertSize, ObservationKind, OnroadAlert, OnroadInput, OnroadRequest, OnroadState, SpeedLimitObservation, slc_controls)
from openpilot.starpilot.ui.presentation import Profile
from openpilot.starpilot.ui.tests.test_unified_speed_native import foreground_calls
@pytest.fixture(autouse=True)
def drawing_guard(monkeypatch):
monkeypatch.setattr(rl, 'draw_line_ex', Mock())
monkeypatch.setattr('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.arrow', Mock())
monkeypatch.setattr('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.outline', Mock())
monkeypatch.setattr('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.circle', Mock())
monkeypatch.setattr('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.confirmation', Mock())
def road(document=None, **changes):
changes.setdefault('slc_prompt_open', True)
return replace(OnroadState(True, True, 15.0, 80.0, SpeedLimitObservation(),
appearance=OnroadAppearance(show_developer_sidebar=True), viewport_width=1860),
customization=document or default_document(), **changes)
def test_defaults_and_metadata_are_independent_and_valid():
document = default_document()
assert validate_document(document) == document
for profile, data in customization_metadata()["profiles"].items():
for key, widget in data["widgets"].items():
assert document["layouts"][profile][key] == {
**widget["default"], **({"size": widget["width"]} if key == "steering_wheel" else {})}
assert offset(document, profile, key) == (0, 0)
document["layouts"]["large"]["current_speed"]["x"] = 50
assert default_document()["layouts"]["large"]["current_speed"]["x"] == 640
@pytest.mark.parametrize('version', (10, 11))
@pytest.mark.parametrize('separate', (False, True))
@pytest.mark.parametrize('lower', (False, True))
def test_source_attachment_saves_real_bounded_positions_without_changing_input(version, separate, lower):
import copy
from openpilot.starpilot.ui.onroad_customization import placement
document = default_document()
document.update(version=version, speedSources=True, speedSourcesCard=separate)
if lower:
document['layouts']['large']['cruise_limits']['y'] = 690
document['layouts']['large']['speed_limit_actions']['y'] = 882
before = copy.deepcopy(document)
saved = validate_document(document)
card, actions = (saved['layouts']['large'][key] for key in ('cruise_limits', 'speed_limit_actions'))
assert document == before and validate_document(saved) == saved
assert placement(saved, 'large', 'speed_limit_actions') == actions
assert actions['y'] == card['y'] + widget_size(saved, 'large', 'cruise_limits')[1] + 8
assert actions['y'] + widget_size(saved, 'large', 'speed_limit_actions')[1] <= 1050
assert actions['y'] == (367 if separate else 359)
def test_v7_speed_layout_migration_is_bounded_non_mutating_and_idempotent():
import copy
from openpilot.starpilot.ui.onroad_customization import SPEED_LAYOUT_V7
document = default_document()
document['version'] = 7
for profile, widgets in SPEED_LAYOUT_V7.items():
for key, (_, _, x, y) in widgets.items():
document['layouts'][profile][key].update(x=x, y=y)
document['layouts']['large']['cem_gauge'].update(x=480, y=99)
document['layouts']['large']['cruise_limits']['x'] = 1510
document['layouts']['large']['speed_limit_actions'].update(x=1510, y=900)
document['layouts']['compact']['max_speed']['y'] = 60
document['layouts']['compact']['speed_limit'].update(x=330, y=20)
document['layouts']['large']['current_speed'].update(x=700, y=100, enabled=False)
document['widgetColors']['large']['current_speed'] = {'text': '#12345678'}
document['speedSources'] = True
original = copy.deepcopy(document)
migrated = validate_document(document)
assert document == original
assert migrated['version'] == 12 and validate_document(migrated) == migrated
assert migrated['layouts']['large']['cruise_limits']['x'] == 1510
assert migrated['layouts']['large']['speed_limit_actions'] == {'x': 1510, 'y': 882, 'enabled': True}
assert migrated['layouts']['large']['current_speed'] == original['layouts']['large']['current_speed']
assert migrated['layouts']['large']['cem_gauge'] == {'x': 56, 'y': 536, 'enabled': True}
assert migrated['layouts']['compact']['speed_limit']['x'] == 330
assert migrated['layouts']['compact']['max_speed']['y'] == 60
assert migrated['widgetColors'] == original['widgetColors'] and migrated['speedSources']
document['layouts']['large']['cruise_limits']['x'] = 2000
with pytest.raises(ValueError, match='outside'):
validate_document(document)
@pytest.mark.parametrize('custom', (False, True))
def test_v8_restores_only_compact_speed_defaults_and_preserves_customizations(custom):
import copy
from openpilot.starpilot.ui.onroad_customization import COMPACT_SPEED_LAYOUT_V8
document = default_document()
document['version'] = 8
for key, (_, _, x, y) in COMPACT_SPEED_LAYOUT_V8.items():
document['layouts']['compact'][key].update(x=x, y=y)
if custom:
document['layouts']['compact']['speed_limit'].update(x=100, y=40, enabled=False)
document['layouts']['compact']['max_speed'].update(x=50, y=0)
document['widgetColors']['compact']['max_speed'] = {'text': '#12345678'}
document['layouts']['large']['cruise_limits'].update(x=1100, y=180)
document['widgetOrder'] = {profile: list(order) for profile, order in DEFAULT_WIDGET_ORDER.items()}
original = copy.deepcopy(document)
migrated = validate_document(document)
assert document == original
assert migrated['version'] == 12 and validate_document(migrated) == migrated
assert migrated['layouts']['large'] == document['layouts']['large']
assert migrated['widgetColors'] == document['widgetColors']
assert migrated['widgetOrder'] == document['widgetOrder']
expected = document['layouts']['compact']['speed_limit'] if custom else {'x': 330, 'y': 20, 'enabled': True}
assert migrated['layouts']['compact']['speed_limit'] == expected
assert migrated['layouts']['compact']['max_speed']['x'] == 0
assert migrated['layouts']['compact']['speed_limit_actions'] == {'x': 146, 'y': 44, 'enabled': True}
def test_v8_compact_speed_widgets_at_old_edges_stay_in_bounds_after_upgrade():
from openpilot.starpilot.ui.onroad_customization import COMPACT_SPEED_LAYOUT_V8
document = default_document()
document['version'] = 8
for key, (_, _, x, y) in COMPACT_SPEED_LAYOUT_V8.items():
document['layouts']['compact'][key].update(x=x, y=y)
document['layouts']['compact']['max_speed'].update(x=330, y=126)
document['layouts']['compact']['speed_limit'].update(x=212, y=158)
migrated = validate_document(document)
assert migrated['layouts']['compact']['max_speed'] == {'x': 0, 'y': 0, 'enabled': True}
assert migrated['layouts']['compact']['speed_limit'] == {'x': 330, 'y': 20, 'enabled': True}
assert validate_document(migrated) == migrated
def test_v8_actionless_layout_preserves_customizations_when_adding_compact_actions():
import copy
from openpilot.starpilot.ui.onroad_customization import COMPACT_SPEED_LAYOUT_V8, RAIL_WIDGETS
document = default_document()
document['version'] = 8
for key, (_, _, x, y) in COMPACT_SPEED_LAYOUT_V8.items():
document['layouts']['compact'][key].update(x=x, y=y)
large_actions = document['layouts']['large'].pop('speed_limit_actions')
for key in ('speed_limit_actions', *RAIL_WIDGETS):
del document['layouts']['compact'][key]
document['layouts']['large']['current_speed']['x'] = 700
document['layouts']['compact']['speed_limit'].update(x=100, y=40)
document['palette']['text'] = '#ABCD1234'
document['widgetColors']['compact']['max_speed'] = {'text': '#12345678'}
original = copy.deepcopy(document)
migrated = validate_document(document)
assert document == original
assert migrated['version'] == 12 and validate_document(migrated) == migrated
assert migrated['layouts']['large'] == {**original['layouts']['large'], 'speed_limit_actions': large_actions}
assert migrated['layouts']['compact']['speed_limit'] == default_document()['layouts']['compact']['speed_limit']
assert migrated['layouts']['compact']['speed_limit_actions'] == {'x': 146, 'y': 44, 'enabled': True}
assert migrated['palette'] == original['palette']
assert migrated['widgetColors'] == original['widgetColors']
def test_clock_is_an_opt_in_widget_and_migrates_saved_layout_and_layer_order():
document = default_document()
metadata = customization_metadata()['profiles']
for profile in ('large', 'compact'):
assert metadata[profile]['widgets'][CLOCK_WIDGET]['kind'] == CLOCK_WIDGET
assert document['layouts'][profile][CLOCK_WIDGET]['enabled'] is False
document['layouts'][profile].pop(CLOCK_WIDGET)
document['widgetOrder'] = {
profile: [key for key in DEFAULT_WIDGET_ORDER[profile] if key != CLOCK_WIDGET]
for profile in ('large', 'compact')
}
migrated = validate_document(document)
for profile in ('large', 'compact'):
assert migrated['layouts'][profile][CLOCK_WIDGET] == metadata[profile]['widgets'][CLOCK_WIDGET]['default']
assert migrated['widgetOrder'][profile][-1] == CLOCK_WIDGET
def test_clock_format_preference_defaults_migrates_and_validates():
document = default_document()
assert document['clock24Hour'] is False
document['clock24Hour'] = True
assert validate_document(document)['clock24Hour'] is True
document.pop('clock24Hour')
assert validate_document(document)['clock24Hour'] is False
document['clock24Hour'] = 1
with pytest.raises(ValueError, match='clock format'):
validate_document(document)
@pytest.mark.parametrize("widget,x,y", [("max_speed", 174, 25), ("steering_wheel", 174, 137)])
def test_protected_compact_actions_reject_actual_overlap(widget, x, y):
document = default_document()
document["layouts"]["compact"][widget].update(x=x, y=y)
with pytest.raises(ValueError, match="protected"):
validate_document(document)
@pytest.mark.parametrize("widget,x,y", [("speed_limit", 330, 24), ("max_speed", 0, 0), ("steering_wheel", 78, 174)])
def test_protected_zone_edge_touch_is_valid(widget, x, y):
document = default_document()
document["layouts"]["compact"][widget].update(x=x, y=y)
assert validate_document(document) == document
zones = customization_metadata()["profiles"]["compact"]["reservedZones"]
assert zones == []
assert customization_metadata()["profiles"]["compact"]["protectedWidget"] == "speed_limit_actions"
@pytest.mark.parametrize("change", [
lambda d: d.update(version=True),
lambda d: d.update(extra=0),
lambda d: d["palette"].update(text="#FFFFFF"),
lambda d: d["palette"].update(text="#GGFFFFFF"),
lambda d: d["layouts"]["large"].update(unknown={}),
lambda d: d["layouts"]["compact"]["max_speed"].update(x=float("nan")),
lambda d: d["layouts"]["compact"]["max_speed"].update(y=float("inf")),
lambda d: d["layouts"]["large"]["current_speed"].update(x=True),
lambda d: d["layouts"]["large"]["current_speed"].update(enabled=1),
lambda d: d["layouts"]["large"]["steering_wheel"].update(x=1800),
lambda d: d["layouts"]["compact"]["speed_limit"].update(y=200),
])
def test_invalid_contract_rejected(change):
document = default_document()
change(document)
with pytest.raises(ValueError):
validate_document(document)
@pytest.mark.parametrize("raw", [None, b"{", b"\xff", b"x" * (MAX_BYTES + 1), b'{"version":1,"version":1}', b"[" * 1200 + b"]" * 1200])
def test_saved_invalid_or_absent_uses_defaults(raw):
with patch("openpilot.starpilot.ui.onroad_customization.read_saved", return_value=(raw, True)):
assert read_customization(Mock()) == default_document()
def test_mode_widget_defaults_off_without_resetting_saved_layouts():
key = 'driving_mode_descriptions'
document = default_document()
document['layouts']['large']['current_speed'].update(x=700, y=100, enabled=False)
document['layouts']['compact']['speed_limit_actions']['y'] = 44
for layout in document['layouts'].values():
assert layout.pop(key)['enabled'] is False
migrated = validate_document(document)
for profile, layout in document['layouts'].items():
assert {key: value for key, value in migrated['layouts'][profile].items()
if key != 'driving_mode_descriptions'} == layout
assert migrated['layouts'][profile][key]['enabled'] is False
migrated['layouts']['large'][key].update(enabled=True, x=300, y=600)
assert validate_document(migrated) == migrated
migrated['layouts']['compact'][key]['enabled'] = True
migrated['layouts']['compact'][key]['y'] = 130
with pytest.raises(ValueError, match='protected speed limit actions'):
validate_document(migrated)
def test_saved_valid_layouts_survive_independently():
document = default_document()
document["layouts"]["large"]["current_speed"].update(x=100, y=120)
document["layouts"]["compact"]["max_speed"].update(x=0, y=20)
with patch("openpilot.starpilot.ui.onroad_customization.read_saved", return_value=(json.dumps(document).encode(), True)):
assert read_customization(Mock()) == document
def test_v3_layout_migrates_wheel_size_without_changing_saved_placement_or_color():
document = default_document()
document["version"] = 3
del document["layouts"]["large"]["cem_gauge"] # Version 3 predates the CEM gauge.
document["palette"]["text"] = "#ABCDEF80"
document["layouts"]["large"]["steering_wheel"].update(x=1300, y=100, enabled=False)
document["layouts"]["compact"]["steering_wheel"].update(x=90, y=130)
for profile in ("large", "compact"):
del document["layouts"][profile]["steering_wheel"]["size"]
migrated = validate_document(document)
assert migrated["version"] == 12
assert migrated["palette"] == document["palette"]
assert migrated["layouts"]["large"]["steering_wheel"] == {"x": 1300, "y": 100, "enabled": False, "size": 192}
assert migrated["layouts"]["compact"]["steering_wheel"] == {"x": 90, "y": 130, "enabled": True, "size": 50}
@pytest.mark.parametrize("removed", [("driver_monitor", "torque_bar"), ("torque_bar",), ()])
def test_pre_action_saved_layout_migrates_without_losing_positions_or_palette(removed):
document = default_document()
document["version"] = 1
del document["layouts"]["large"]["cem_gauge"] # Version 1 predates the CEM gauge.
del document["widgetColors"]
del document["roadColors"]
document["palette"]["cardFill"] = "#12345678"
document["layouts"]["large"]["current_speed"].update(x=700, y=120, enabled=False)
document["layouts"]["large"]["cruise_limits"].update(x=288, y=175, enabled=False)
document["layouts"]["compact"]["speed_limit"].update(x=200, y=20)
for key in ("model_confidence", "conditional_mode", "following_distance"):
del document["layouts"]["compact"][key]
for layout in document["layouts"].values():
del layout["speed_limit_actions"]
del layout["steering_wheel"]["size"]
for key in removed:
del layout[key]
migrated = validate_document(document)
assert migrated["palette"] == document["palette"]
for profile in ("large", "compact"):
for key, position in document["layouts"][profile].items():
if profile == "compact" and key == "speed_limit":
assert migrated["layouts"][profile][key] == default_document()["layouts"][profile][key]
continue
assert migrated["layouts"][profile][key] == {
**position, **({"size": customization_metadata()["profiles"][profile]["widgets"][key]["width"]}
if key == "steering_wheel" else {})}
assert migrated["layouts"]["large"]["speed_limit_actions"] == {"x": 312, "y": 367, "enabled": False}
assert migrated["layouts"]["compact"]["speed_limit_actions"] == default_document()["layouts"]["compact"]["speed_limit_actions"]
def test_malformed_pre_action_cruise_placement_is_rejected():
document = default_document()
for key in ("model_confidence", "conditional_mode", "following_distance"):
del document["layouts"]["compact"][key]
for layout in document["layouts"].values():
del layout["speed_limit_actions"]
document["layouts"]["large"]["cruise_limits"]["x"] = "moved"
with pytest.raises(ValueError, match="Invalid widget placement"):
validate_document(document)
def test_duplicate_key_cannot_apply_otherwise_valid_placement():
document = default_document()
document["layouts"]["large"]["current_speed"]["x"] = 100
raw = json.dumps(document).replace('"version": 12', '"version": 11, "version": 11').encode()
with patch("openpilot.starpilot.ui.onroad_customization.read_saved", return_value=(raw, True)):
assert read_customization(Mock()) == default_document()
@pytest.mark.parametrize('sources', [False, True])
def test_v9_large_sign_migration_preserves_customizations_and_clamps_bottom_edge(sources):
from copy import deepcopy
old = default_document()
old['version'] = 9
old['speedSources'] = sources
old['layouts']['large']['cruise_limits'].update(y=918)
old['layouts']['large']['speed_limit_actions'].update(x=250, y=400)
before = deepcopy(old)
updated = validate_document(old)
assert old == before
assert updated['version'] == 12
assert updated['layouts']['large']['cruise_limits']['y'] == default_document()['layouts']['large']['cruise_limits']['y']
assert updated['layouts']['large']['speed_limit_actions'] == before['layouts']['large']['speed_limit_actions']
assert updated['layouts']['compact'] == before['layouts']['compact']
assert validate_document(updated) == updated
def test_v9_moves_only_actions_paired_with_the_old_large_speed_group():
old = default_document()
old['version'] = 9
old['layouts']['large']['cruise_limits'].update(x=188, y=95)
old['layouts']['large']['speed_limit_actions'].update(x=200, y=268)
updated = validate_document(old)
assert updated['layouts']['large']['speed_limit_actions'] == {'x': 212, 'y': 287, 'enabled': True}
assert validate_document(updated) == updated
def test_wheel_hit_edges_match_default_draw_bounds():
emit = Mock()
inputs = OnroadInput(emit)
state = road(experimental_available=True)
for x in (1588, 1775, 1780):
inputs.press(x, 100, state)
inputs.release(x, 100, state)
assert emit.call_count == 3
emit.reset_mock()
for x in (1580, 1781):
inputs.press(x, 100, state)
inputs.release(x, 100, state)
emit.assert_not_called()
def test_resized_wheel_draw_and_touch_share_large_bounds():
document = default_document()
document["layouts"]["large"]["steering_wheel"].update(x=1300, y=100, size=240)
state = road(document, experimental_available=True)
assert validate_document(document) == document
wheel = SteeringWheelWidget(Path('/unused'))
wheel._texture = Mock()
with patch.object(rl, "draw_circle") as circle, patch.object(rl, "draw_texture_pro") as texture:
wheel.render(rl.Rectangle(30, 30, 1800, 1020), state)
assert circle.call_args.args[:3] == (1420, 220, 120)
rect = texture.call_args.args[2]
assert (rect.x, rect.y, rect.width, rect.height) == (1330, 130, 180, 180)
emit = Mock()
inputs = OnroadInput(emit)
for x, y in ((1300, 100), (1539, 339)):
inputs.press(x, y, state)
inputs.release(x, y, state)
assert emit.call_count == 2
inputs.press(1541, 339, state)
inputs.release(1541, 339, state)
assert emit.call_count == 2
def test_compact_resize_rejects_protected_overlap_and_other_widget_sizes():
document = default_document()
document["layouts"]["compact"]["steering_wheel"].update(size=70, y=52)
assert validate_document(document) == document
assert widget_size(document, "compact", "steering_wheel") == (70, 70)
document["layouts"]["compact"]["steering_wheel"].update(x=150, y=150)
with pytest.raises(ValueError, match="protected"):
validate_document(document)
document = default_document()
document["layouts"]["compact"]["speed_limit_actions"]["size"] = 70
with pytest.raises(ValueError, match="placement"):
validate_document(document)
@pytest.mark.parametrize("changed_token", ["new-settings", "new-drive", "new-mode", ""])
def test_wheel_press_cancels_when_action_token_changes(changed_token):
emit = Mock()
inputs = OnroadInput(emit)
state = road(experimental_available=True, experimental_action_token="displayed-action")
inputs.press(1600, 100, state)
assert inputs.claimed
changed = replace(state, experimental_action_token=changed_token)
inputs.move(1600, 100, changed)
assert not inputs.claimed
inputs.release(1600, 100, state)
emit.assert_not_called()
inputs.press(1600, 100, state)
inputs.release(1600, 100, changed)
emit.assert_not_called()
@pytest.mark.parametrize("enabled,token", [(False, ""), (True, "stable-action")])
def test_moved_wheel_emits_captured_action_token_and_effective_bool(enabled, token):
document = default_document()
document["layouts"]["large"]["steering_wheel"].update(x=1300, y=100)
state = road(document, experimental_available=True, experimental_enabled=enabled, experimental_action_token=token)
emit = Mock()
inputs = OnroadInput(emit)
inputs.press(1310, 110, state)
inputs.release(1310, 110, replace(state))
emit.assert_called_once_with(OnroadRequest("set_experimental", not enabled, token))
def test_large_draw_origins_and_wheel_input_move_together():
document = default_document()
document["layouts"]["large"]["cruise_limits"].update(x=188, y=95)
document["layouts"]["large"]["current_speed"].update(x=680, y=70)
document["layouts"]["large"]["steering_wheel"].update(x=1300, y=100)
state = road(document, experimental_available=True)
fonts = NS(profile=Profile.LARGE, measure=lambda *a, **k: NS(width=10, height=20), vertical_ink=lambda text, role, size: (0., size * .7),
horizontal_ink=lambda text, role, size: (0., 10.), draw=Mock())
content = rl.Rectangle(30, 30, 1800, 1020)
with patch("openpilot.starpilot.ui.onroad_large_widgets.draw_control_card") as card, patch.object(rl, "draw_line_ex"):
UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
card.assert_not_called()
assert foreground_calls(fonts.draw)[0].args[3:5] == (292, 110)
CurrentSpeedHud(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
assert fonts.draw.call_args_list[-2].args[3:5] == (965, 112)
wheel = SteeringWheelWidget(Path('/unused'))
wheel._texture = Mock()
with patch.object(rl, "draw_circle") as circle, patch.object(rl, "draw_texture_pro"):
wheel.render(content, state)
assert circle.call_args.args[:3] == (1396, 196, 96)
emit = Mock()
inputs = OnroadInput(emit)
inputs.press(1310, 110, state)
inputs.release(1310, 110, state)
emit.assert_called_once()
emit.reset_mock()
inputs.press(1600, 80, state)
inputs.release(1600, 80, state)
emit.assert_not_called()
document["layouts"]["large"]["steering_wheel"]["enabled"] = False
inputs.press(1310, 110, state)
inputs.release(1310, 110, state)
emit.assert_not_called()
def test_speed_limit_actions_move_independently_of_cruise_card_and_keep_touch_priority():
document = default_document()
document["layouts"]["large"]["cruise_limits"].update(x=288, y=175)
document["layouts"]["large"]["speed_limit_actions"].update(x=388, y=600)
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
pending_speed_limit_mps=None, session_id="drive", decision_id=1,
presentation_id=2, action_enabled=True)
state = road(document, speed_limit=observation, longitudinal_active=True, slc_system_long_available=True)
controls = slc_controls(Profile.LARGE, state)
assert controls[0].bounds == (388, 600, 696, 768)
assert slc_controls(Profile.COMPACT, state)[0].bounds == (146, 44, 330, 164)
emit = Mock()
inputs = OnroadInput(emit)
from openpilot.starpilot.ui.tests.test_slc_ui_actions import tap_sign
tap_sign(inputs, state)
for x, y in ((400, 650), (300, 400)):
inputs.press(x, y, state)
inputs.release(x, y, state)
assert emit.call_count == 1
assert not slc_controls(Profile.LARGE, replace(state, alert=OnroadAlert(size=AlertSize.FULL)))
document["layouts"]["large"]["cruise_limits"]["enabled"] = False
assert slc_controls(Profile.LARGE, state) == controls
emit.reset_mock()
inputs.press(300, 400, state)
inputs.release(300, 400, state)
emit.assert_not_called()
document["layouts"]["large"]["speed_limit_actions"]["enabled"] = False
assert not slc_controls(Profile.LARGE, state)
def test_moved_compact_actions_relocate_hitboxes_without_weakening_state_gate():
document = default_document()
document["layouts"]["compact"]["speed_limit_actions"].update(x=150, y=44)
document["layouts"]["compact"]["speed_limit"]["enabled"] = False
assert validate_document(document) == document
document["layouts"]["compact"]["speed_limit_actions"].update(x=160, y=44)
document = validate_document(document)
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
pending_speed_limit_mps=None, session_id="drive", decision_id=1,
presentation_id=2, action_enabled=True)
state = road(document, speed_limit=observation, longitudinal_active=True, slc_system_long_available=True)
assert slc_controls(Profile.COMPACT, state)[0].bounds == (160, 44, 344, 164)
emit = Mock()
inputs = OnroadInput(emit, Profile.COMPACT)
inputs.press(200, 80, state)
inputs.release(200, 80, state)
assert emit.call_count == 1
inputs.press(480, 200, state)
inputs.release(480, 200, state)
assert emit.call_count == 1
assert not slc_controls(Profile.COMPACT, replace(state, alert=OnroadAlert(size=AlertSize.FULL)))
def test_frozen_default_draw_geometry_and_neutral_colors():
state = road()
fonts = NS(profile=Profile.LARGE, measure=lambda *a, **k: NS(width=10, height=20), vertical_ink=lambda text, role, size: (0., size * .7),
horizontal_ink=lambda text, role, size: (0., 10.), draw=Mock())
content = rl.Rectangle(30, 30, 1800, 1020)
with patch("openpilot.starpilot.ui.onroad_large_widgets.draw_control_card") as card, patch.object(rl, "draw_line_ex"):
UnifiedSpeedWidget(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
card.assert_not_called()
assert fonts.draw.call_args_list[0].args[0] == 'SET'
assert foreground_calls(fonts.draw)[0].args[3:5] == (192, 90)
CurrentSpeedHud(Mock(spec=BitmapFonts, **vars(fonts))).render(content, state)
assert fonts.draw.call_args_list[-2].args[3:5] == (925, 72)
assert fonts.draw.call_args_list[-1].args[3:5] == pytest.approx((925, 215.2))
wheel = SteeringWheelWidget(Path('/unused'))
wheel._texture = Mock()
with patch.object(rl, "draw_circle") as circle, patch.object(rl, "draw_texture_pro"):
wheel.render(content, state)
assert circle.call_args.args[:3] == (1684, 171, 96)
@pytest.mark.parametrize("speed", [65, 105])
def test_compact_us_sign_keeps_camera_visible_at_saved_position(speed):
document = default_document()
document["layouts"]["compact"]["speed_limit"].update(x=200, y=30)
state = road(document, metric=True, speed_limit=SpeedLimitObservation(
kind=ObservationKind.VALID, speed_limit_mps=speed / 3.6))
state = replace(state, appearance=replace(state.appearance, show_speed_limit_sign=True))
fonts = NS(profile=Profile.COMPACT, measure=lambda *a, **k: NS(width=10, height=20), vertical_ink=lambda text, role, size: (0., size * .7),
horizontal_ink=lambda text, role, size: (0., 10.), draw=Mock())
hud = CompactHudRenderer(Mock(spec=BitmapFonts, **vars(fonts)), Mock())
with patch.object(rl, "draw_rectangle_rounded") as fill, \
patch('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.outline') as outline:
hud._speed_limit_sign(state)
fill.assert_not_called()
outline.assert_called_once()
assert outline.call_args.args[:2] == (259, 96)
assert [call.args[0] for call in foreground_calls(fonts.draw)] == ['SPEED', str(speed)]
assert foreground_calls(fonts.draw)[0].args[3:5] == (254, 50)
def test_compact_drag_keeps_animation_state_and_separate_origins():
document = default_document()
document["layouts"]["compact"]["max_speed"].update(x=100, y=10)
document["layouts"]["compact"]["steering_wheel"].update(x=90, y=150)
fonts = NS(profile=Profile.COMPACT, draw=Mock(), vertical_ink=lambda text, role, size: (0, size * .7), measure=lambda text, _role, size: NS(
width=len(text) * size * .5, height=size * Profile.COMPACT.font_scale))
hud = CompactHudRenderer(Mock(spec=BitmapFonts, **vars(fonts)), Mock())
hud._wheel = Mock()
state = road(document, lateral_active=True, longitudinal_active=True)
with patch.object(hud, "_speed_limit_sign"), patch.object(rl, "draw_circle_gradient"), patch.object(rl, "draw_texture_pro") as wheel:
hud.render(state)
hud.render(state)
assert foreground_calls(fonts.draw)[-1].args[3:5] == (125, 119)
rect = wheel.call_args.args[2]
assert (rect.x, rect.y) == (115, 175)
alpha = hud._set_speed_alpha.x
document["layouts"]["compact"]["max_speed"]["enabled"] = False
document["layouts"]["compact"]["steering_wheel"]["enabled"] = False
fonts.draw.reset_mock()
wheel.reset_mock()
hud.render(state)
assert hud._set_speed_alpha.x > alpha
fonts.draw.assert_not_called()
wheel.assert_not_called()
def test_saved_six_widget_layout_adds_only_default_rail_positions():
document = default_document()
for key in ("model_confidence", "conditional_mode", "following_distance"):
del document["layouts"]["compact"][key]
document["layouts"]["compact"]["speed_limit_actions"].update(x=162, enabled=False)
document["palette"]["text"] = "#ABCD1234"
migrated = validate_document(document)
assert migrated["palette"] == document["palette"]
for profile, layout in document["layouts"].items():
for key, value in layout.items():
assert migrated["layouts"][profile][key] == value
assert migrated["layouts"]["compact"]["conditional_mode"] == {"x": 476, "y": 80, "enabled": True}
assert "conditional_mode" not in document["layouts"]["compact"]
def test_movable_rail_keeps_limits_and_does_not_cover_active_alerts():
from openpilot.starpilot.ui.onroad_compact_widgets import MiciSidebarWidgets
document = default_document()
document["layouts"]["compact"]["model_confidence"].update(x=200, y=20, enabled=False)
document["layouts"]["compact"]["conditional_mode"].update(x=330, y=25)
assert validate_document(document) == document
widget = MiciSidebarWidgets(Mock())
widget._confidence_ball, widget._conditional, widget._personality = Mock(), Mock(), Mock()
with patch.object(rl, "draw_rectangle"):
widget.render(rl.Rectangle(0, 0, 536, 240), road(document))
widget._confidence_ball.assert_not_called()
assert tuple(getattr(widget._conditional.call_args.args[0], name) for name in ("x", "y", "width", "height")) == (330, 25, 60, 80)
assert tuple(getattr(widget._personality.call_args.args[0], name) for name in ("x", "y", "width", "height")) == (476, 160, 60, 80)
widget._conditional.reset_mock()
widget.render(rl.Rectangle(0, 0, 536, 240), road(document, alert=OnroadAlert(size=AlertSize.FULL)))
widget._conditional.assert_not_called()
document["layouts"]["compact"]["conditional_mode"].update(x=200, y=107)
with pytest.raises(ValueError, match="protected"):
validate_document(document)
document["layouts"]["compact"]["conditional_mode"].update(x=477, y=0)
with pytest.raises(ValueError, match="bounds"):
validate_document(document)
def test_v1_palette_positions_and_visibility_migrate_without_mutating_source():
import copy
old = default_document()
old['version'] = 1
del old['layouts']['large']['cem_gauge'] # Version 1 predates the CEM gauge.
del old['widgetColors']
del old['roadColors']
old['palette'].update(cardFill='#12345678', text='#AABBCCDD')
old['layouts']['large']['current_speed'].update(x=600.5, y=100.25, enabled=False)
old['layouts']['compact']['following_distance'].update(x=400, y=20)
for layout in old['layouts'].values():
del layout['steering_wheel']['size']
original = copy.deepcopy(old)
result = decode_document(json.dumps(old).encode())
assert old == original
assert result['version'] == 12
assert result['palette'] == old['palette']
for profile, layout in old['layouts'].items():
for key, position in layout.items():
assert result['layouts'][profile][key] == {
**position, **({'size': customization_metadata()['profiles'][profile]['widgets'][key]['width']}
if key == 'steering_wheel' else {})}
assert result['widgetColors'] == {'large': {'cruise_limits': {'cardFill': '#12345678', 'cardBorder': '#C4CDD0B4'}}, 'compact': {}}
assert rgba(result, 'text', 'large', 'current_speed') == (170, 187, 204, 221)
assert rgba(result, 'cardFill', 'compact', 'following_distance') == (0, 0, 0, 0)
@pytest.mark.parametrize('colors', [
{'large': {}}, {'large': {}, 'compact': [], 'extra': {}},
{'large': {'unknown': {}}, 'compact': {}},
{'large': {'current_speed': {'cardFill': '#FFFFFFFF'}}, 'compact': {}},
{'large': {}, 'compact': {'torque_bar': {'text': '#FFFFFFFF'}}},
{'large': {'current_speed': {'text': 123}}, 'compact': {}},
{'large': {'current_speed': {'text': '#FFFFFF'}}, 'compact': {}},
])
def test_unsupported_widget_colors_are_rejected(colors):
document = default_document()
document['widgetColors'] = colors
with pytest.raises(ValueError):
validate_document(document)
def test_widget_color_isolation_and_native_text_resolution():
document = default_document()
document['palette']['text'] = '#10203040'
document['widgetColors']['large']['current_speed'] = {'text': '#aabbccdd'}
document['widgetColors']['compact']['following_distance'] = {'cardFill': '#12345678', 'text': '#ABCDEF80'}
validated = validate_document(document)
assert rgba(validated, 'text', 'large', 'current_speed') == (170, 187, 204, 221)
assert rgba(validated, 'text', 'compact', 'max_speed') == (16, 32, 48, 64)
assert rgba(validated, 'text', 'large', 'cruise_limits') == (16, 32, 48, 64)
assert widget_palette(validated, 'compact', 'conditional_mode')['cardFill'] == '#00000000'
fonts = NS(measure=lambda *a, **k: NS(width=10, height=20), vertical_ink=lambda text, role, size: (0., size * .7),
horizontal_ink=lambda text, role, size: (0., 10.), draw=Mock())
CurrentSpeedHud(Mock(spec=BitmapFonts, **vars(fonts))).render(rl.Rectangle(30, 30, 1800, 1020), road(validated))
actual = fonts.draw.call_args_list[0].args[-1]
assert (actual.r, actual.g, actual.b, actual.a) == (170, 187, 204, 221)
def test_unsaved_color_changes_replace_values_and_restore_palette_fallback():
document = default_document()
for profile, widget in (("large", "current_speed"), ("compact", "max_speed")):
assert rgba(document, "text", profile, widget) == (255, 255, 255, 255)
document["palette"]["text"] = "#10203040"
assert rgba(document, "text", profile, widget) == (16, 32, 48, 64)
document["widgetColors"][profile][widget] = {"text": "#aabbccdd"}
assert rgba(document, "text", profile, widget) == (170, 187, 204, 221)
document["widgetColors"][profile][widget]["text"] = "#01020304"
assert rgba(document, "text", profile, widget) == (1, 2, 3, 4)
del document["widgetColors"][profile][widget]
assert rgba(document, "text", profile, widget) == (16, 32, 48, 64)
document["palette"]["text"] = "#FFFFFFFF"
def test_transparent_widget_frames_add_no_default_draw_calls():
from openpilot.starpilot.ui.onroad_widget_style import draw_widget_frame
document = default_document()
with patch.object(rl, 'draw_rectangle_rounded') as fill, patch.object(rl, 'draw_rectangle_rounded_lines_ex') as border:
for profile, widgets in (('compact', ('driver_monitor', 'model_confidence', 'conditional_mode', 'following_distance')),
('large', ('driver_monitor',))):
for widget in widgets:
draw_widget_frame(rl.Rectangle(10, 20, 60, 80), document, profile, widget)
fill.assert_not_called()
border.assert_not_called()
document['widgetColors']['compact']['conditional_mode'] = {'cardFill': '#12345678', 'cardBorder': '#ABCDEF90'}
draw_widget_frame(rl.Rectangle(200, 80, 60, 80), document, 'compact', 'conditional_mode')
color = fill.call_args.args[-1]
assert (color.r, color.g, color.b, color.a) == (18, 52, 86, 120)
color = border.call_args.args[-1]
assert (color.r, color.g, color.b, color.a) == (171, 205, 239, 144)
def test_colored_slc_controls_retain_requests_and_touch_bounds(monkeypatch):
from openpilot.starpilot.ui.onroad import OnroadView
document = default_document()
state = road(document, slc_system_long_available=True, longitudinal_active=True,
speed_limit=SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
pending_speed_limit_mps=None, decision_id=1, presentation_id=2,
session_id='session', action_enabled=True))
# Compare the actual owner-produced controls before and after presentation edits.
before = slc_controls(Profile.COMPACT, state)
document['widgetColors']['compact']['speed_limit_actions'] = {
'cardFill': '#12345678', 'cardBorder': '#ABCDEF90', 'text': '#FEDCBAFF'}
assert slc_controls(Profile.COMPACT, state) == before
assert before
fonts = NS(profile=Profile.COMPACT, measure=lambda *a, **k: NS(width=10, height=20), vertical_ink=lambda text, role, size: (0., size * .7),
horizontal_ink=lambda text, role, size: (0., 10.), draw=Mock())
view = OnroadView.__new__(OnroadView)
monkeypatch.setattr(view, 'fonts', fonts, raising=False)
with patch.object(rl, 'draw_rectangle_rounded') as fill, patch.object(rl, 'draw_rectangle_rounded_lines_ex'):
view._slc_actions(state)
fill.assert_not_called()
color = fonts.draw.call_args.args[-1]
assert (color.r, color.g, color.b, color.a) == (254, 220, 186, 255)
def test_driver_frame_matches_native_preview_and_keeps_face_renderer_separate(monkeypatch):
from openpilot.starpilot.ui.layout_preview_renderer import _DriverMonitorArt
from openpilot.starpilot.ui.onroad_dm import DriverMonitorLayer
document = default_document()
document['layouts']['compact']['driver_monitor'].update(x=90, y=40)
document['widgetColors']['compact']['driver_monitor'] = {'cardFill': '#AABBCC80'}
state = road(document)
live = DriverMonitorLayer.__new__(DriverMonitorLayer)
live.profile = Profile.COMPACT
matrix = Mock()
monkeypatch.setattr(live, 'renderer', NS(set_should_draw=Mock(), set_position=Mock(), _rect=object(),
_fade_filter=NS(update=Mock()), _render=matrix.live_render), raising=False)
preview = _DriverMonitorArt.__new__(_DriverMonitorArt)
preview.profile = Profile.COMPACT
monkeypatch.setattr(preview, 'renderer', NS(_render=matrix.preview_render), raising=False)
with patch('openpilot.starpilot.ui.onroad_dm.draw_widget_frame') as live_frame, \
patch('openpilot.starpilot.ui.layout_preview_renderer.draw_widget_frame') as preview_frame, \
patch.multiple(rl, rl_push_matrix=matrix.push, rl_translatef=matrix.translate,
rl_scalef=matrix.scale, rl_pop_matrix=matrix.pop):
live.render(state, monitor=None, driver=None, fresh=False, onroad=True)
preview.render(rl.Rectangle(0, 0, 536, 240), state)
assert live_frame.call_args.args[1:] == preview_frame.call_args.args[1:]
live_rect, preview_rect = live_frame.call_args.args[0], preview_frame.call_args.args[0]
assert tuple(getattr(live_rect, key) for key in ('x', 'y', 'width', 'height')) == \
tuple(getattr(preview_rect, key) for key in ('x', 'y', 'width', 'height')) == (90, 40, 60, 60)
live.renderer._fade_filter.update.assert_called_once_with(0.35)
live.renderer._render.assert_called_once()
preview.renderer._render.assert_called_once()
assert matrix.mock_calls == [
mock_call.push(), mock_call.translate(90, 40, 0), mock_call.scale(1, 1, 1), mock_call.translate(-90, -40, 0),
mock_call.live_render(live.renderer._rect), mock_call.pop(),
mock_call.push(), mock_call.translate(90, 40, 0), mock_call.scale(1, 1, 1), mock_call.translate(-90, -40, 0),
mock_call.preview_render(preview.renderer._rect), mock_call.pop(),
]
@pytest.mark.parametrize("enabled", [True, False])
def test_intentional_small_sign_action_overlap_roundtrips_and_large_is_independent(enabled):
from openpilot.starpilot.ui.onroad_state import compact_sign_obscured_by_actions
document = default_document()
large = json.loads(json.dumps(document["layouts"]["large"]))
document["layouts"]["compact"]["speed_limit"].update(x=330, y=20, enabled=enabled)
assert decode_document(json.dumps(validate_document(document))) == document
document["layouts"]["compact"]["speed_limit_actions"].update(x=150, y=44)
assert validate_document(document) == document
assert document["layouts"]["large"] == large
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
pending_speed_limit_mps=None, session_id="drive", decision_id=1,
presentation_id=2, action_enabled=True)
state = road(document, speed_limit=observation, longitudinal_active=True, slc_system_long_available=True)
assert compact_sign_obscured_by_actions(state)
# Recording a driver choice is still available without software longitudinal control.
assert compact_sign_obscured_by_actions(replace(state, longitudinal_active=False, slc_system_long_available=False))
for change in ({"alert": OnroadAlert(size=AlertSize.FULL)},
{"speed_limit": replace(observation, action_enabled=False)},
{"speed_limit": replace(observation, pending_speed_limit_mps=None, accepted_speed_limit_mps=20)}):
assert not compact_sign_obscured_by_actions(replace(state, **change))
def test_saved_small_actions_near_top_have_no_heading_and_keep_labels_visible(monkeypatch):
from openpilot.starpilot.ui.onroad import OnroadView
document = default_document()
document["layouts"]["compact"]["speed_limit"]["enabled"] = False
document["layouts"]["compact"]["speed_limit_actions"].update(x=16, y=4)
document["layouts"]["compact"]["max_speed"].update(x=314, y=74)
document["layouts"]["compact"]["driver_monitor"].update(x=216, y=60)
assert decode_document(json.dumps(document)) == document
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
pending_speed_limit_mps=None, session_id="drive", decision_id=1,
presentation_id=2, action_enabled=True)
state = road(document, speed_limit=observation, longitudinal_active=True, slc_system_long_available=True)
view = OnroadView.__new__(OnroadView)
fonts = NS(profile=Profile.COMPACT, measure=lambda *a, **k: NS(width=10, height=20),
vertical_ink=lambda text, role, size: (0., size * .7),
horizontal_ink=lambda text, role, size: (0., 10.), draw=Mock())
monkeypatch.setattr(view, 'fonts', fonts, raising=False)
with patch.object(rl, "draw_rectangle_rounded"), patch.object(rl, "draw_rectangle_rounded_lines_ex"):
view._slc_actions(state)
assert [call.args[0] for call in foreground_calls(view.fonts.draw)] == ['Use 45']
assert foreground_calls(view.fonts.draw)[0].args[4] >= 0
assert slc_controls(Profile.COMPACT, state)[0].bounds == (16, 4, 200, 124)
def test_overlapping_recovery_sign_restores_after_adoption_and_chooser_keeps_exact_input():
from openpilot.starpilot.ui.onroad_state import SlcActionKind
from openpilot.starpilot.ui.tests.test_slc_ui_actions import tap_sign, tap_control
document = default_document()
document['layouts']['compact']['speed_limit_actions'].update(x=150, y=44)
observation = SpeedLimitObservation(kind=ObservationKind.VALID, speed_limit_mps=20,
pending_speed_limit_mps=15, session_id='drive', decision_id=1,
presentation_id=2, action_enabled=True)
state = road(document, speed_limit=observation, longitudinal_active=True, slc_system_long_available=True)
fonts = NS(profile=Profile.COMPACT, measure=lambda *a, **k: NS(width=10, height=20), vertical_ink=lambda text, role, size: (0., size*.7),
horizontal_ink=lambda text, role, size: (0., 10.), draw=Mock())
hud = CompactHudRenderer(Mock(spec=BitmapFonts, **vars(fonts)), Mock())
recovery = replace(state, speed_limit=replace(observation, pending_speed_limit_mps=None))
hud._speed_limit_sign(recovery)
fonts.draw.assert_not_called()
with patch.object(hud._speed_limit_widget.shapes, 'outline'):
hud._speed_limit_sign(replace(recovery, speed_limit=replace(recovery.speed_limit, accepted_speed_limit_mps=20)))
assert fonts.draw.called
emit = Mock()
inputs = OnroadInput(emit, Profile.COMPACT)
for kind in (SlcActionKind.ACCEPT, SlcActionKind.REJECT):
emit.reset_mock()
tap_sign(inputs, state)
tap_control(inputs, state, kind)
inputs.release(238, 73, state)
emit.assert_called_once()
assert emit.call_args.args[0].kind == kind
@pytest.mark.parametrize('profile', ['large', 'compact'])
def test_pip_circles_are_independent_widgets_and_migrate_old_camera_pair(profile):
import copy
document = default_document()
metadata = customization_metadata()['profiles'][profile]['widgets']
assert 'vasm' not in metadata
for key in ('pip_left', 'pip_right'):
assert metadata[key]['kind'] == 'pip_camera'
assert metadata[key]['width'] == metadata[key]['height']
right = copy.deepcopy(document['layouts'][profile]['pip_right'])
document['layouts'][profile]['pip_left']['enabled'] = False
assert validate_document(document)['layouts'][profile]['pip_right'] == right
legacy = copy.deepcopy(document)
for name, layout in legacy['layouts'].items():
layout.pop('pip_left')
layout.pop('pip_right')
layout['vasm'] = {'x': 1200 if name == 'large' else 300, 'y': 650 if name == 'large' else 30, 'enabled': False}
migrated = validate_document(legacy)
for name, layout in migrated['layouts'].items():
assert 'vasm' not in layout
assert not layout['pip_left']['enabled'] and not layout['pip_right']['enabled']
assert layout['pip_left']['x'] <= layout['pip_right']['x']
assert layout['current_speed' if name == 'large' else 'max_speed'] == document['layouts'][name]['current_speed' if name == 'large' else 'max_speed']
assert validate_document(migrated) == migrated
for layout in legacy['layouts'].values():
layout.pop('vasm')
assert validate_document(legacy)['layouts'][profile]['pip_left']['enabled']
def test_stopped_timer_widget_is_optional_off_and_can_yield_to_slc_when_on():
older = default_document()
del older["layouts"]["compact"][STOPPED_TIMER_WIDGET]
older["widgetOrder"] = {"compact": [key for key in DEFAULT_WIDGET_ORDER["compact"] if key != STOPPED_TIMER_WIDGET]}
upgraded = validate_document(older)
assert upgraded["layouts"]["compact"][STOPPED_TIMER_WIDGET] == {"x": 18, "y": 48, "enabled": False}
assert upgraded["widgetOrder"]["compact"][-1] == STOPPED_TIMER_WIDGET
assert upgraded["layouts"]["large"][STOPPED_TIMER_WIDGET]["enabled"] is False
# The transient timer yields at render time when SLC needs its area.
parked = default_document()
parked["layouts"]["compact"][STOPPED_TIMER_WIDGET].update(y=120)
validate_document(parked)
parked["layouts"]["compact"][STOPPED_TIMER_WIDGET]["enabled"] = True
assert validate_document(parked) == parked
def test_show_max_speed_is_an_optional_boolean():
document = default_document()
assert "showMaxSpeed" not in validate_document(document)
document["showMaxSpeed"] = False
assert validate_document(document)["showMaxSpeed"] is False
document["showMaxSpeed"] = 0
with pytest.raises(ValueError):
validate_document(document)
def test_stopped_timer_draws_inside_its_moved_widget():
from openpilot.starpilot.ui import onroad
document = default_document()
view = onroad.OnroadView.__new__(onroad.OnroadView)
view.fonts = Mock(spec=BitmapFonts)
view.fonts.profile = Profile.COMPACT
view.fonts.measure.return_value = NS(width=100, height=20)
state = road(document)
view._stopped_timer(state, 75)
view.fonts.draw.assert_not_called()
document["layouts"]["compact"][STOPPED_TIMER_WIDGET]["enabled"] = True
view._stopped_timer(state, 75)
default = [call.args[3:5] for call in view.fonts.draw.call_args_list[1::2]]
assert default == [((476 - 100) / 2, 101 - 10), ((476 - 100) / 2, 149 - 10)]
document["layouts"]["compact"][STOPPED_TIMER_WIDGET].update(x=0, y=0)
view.fonts.draw.reset_mock()
view._stopped_timer(state, 75)
assert [call.args[3:5] for call in view.fonts.draw.call_args_list[1::2]] == [(170, 53 - 10), (170, 101 - 10)]
def test_projection_timer_renders_at_saved_coordinates_without_native_timer_enabled():
from openpilot.starpilot.ui import onroad
from openpilot.starpilot.system.starport.projection_layout import default_layout_for_viewport, projection_customization
layout = default_layout_for_viewport((2880, 1080))
layout['widgets'][STOPPED_TIMER_WIDGET].update(enabled=True, x=450, y=600)
document = projection_customization(layout, default_document())
view = onroad.OnroadView.__new__(onroad.OnroadView)
view.fonts = Mock(spec=BitmapFonts)
view.fonts.profile = Profile.LARGE
view.fonts.measure.return_value = NS(width=100, height=20)
view._stopped_timer(road(document), 134)
text_calls = view.fonts.draw.call_args_list[1::2]
assert [call.args[0] for call in text_calls] == ['2 minutes', '14 seconds']
assert [call.args[3:5] for call in text_calls] == [(730, 669.5), (730, 741.5)]
view.fonts.draw.reset_mock()
document['layouts']['large'][STOPPED_TIMER_WIDGET]['enabled'] = False
view._stopped_timer(road(document), 134)
view.fonts.draw.assert_not_called()
def test_large_navigation_widgets_migrate_without_changing_existing_layout_or_order():
import copy
keys = ("nav_card", "nav_map", STOPPED_TIMER_WIDGET)
older = default_document()
older["layouts"]["large"]["current_speed"].update(x=700, enabled=False)
older["widgetOrder"] = {"large": [key for key in reversed(DEFAULT_WIDGET_ORDER["large"]) if key not in keys]}
for key in keys:
del older["layouts"]["large"][key]
original = copy.deepcopy(older)
upgraded = validate_document(older)
assert older == original
assert upgraded["layouts"]["large"]["current_speed"] == older["layouts"]["large"]["current_speed"]
assert upgraded["widgetOrder"]["large"][:len(older["widgetOrder"]["large"])] == older["widgetOrder"]["large"]
assert set(upgraded["widgetOrder"]["large"]) == set(upgraded["layouts"]["large"])
for key in keys:
assert upgraded["layouts"]["large"][key] == default_document()["layouts"]["large"][key]
assert upgraded["layouts"]["large"][key]["enabled"] is (key == "nav_card")
upgraded["layouts"]["large"]["nav_map"].update(x=100, y=300, width=900, height=500, opacity=35, enabled=True)
upgraded["layouts"]["large"]["nav_card"].update(x=800, y=500, enabled=True)
assert decode_document(json.dumps(upgraded)) == upgraded
assert widget_size(upgraded, "large", "nav_map") == (900, 500)
@pytest.mark.parametrize("change", [{"width": 279}, {"height": 199}, {"opacity": 14}, {"opacity": 101},
{"opacity": True}, {"width": 560.5}, {"width": 900}, {"x": float("nan")}])
def test_native_map_rejects_invalid_size_opacity_and_offscreen_placements(change):
document = default_document()
document["layouts"]["large"]["nav_map"].update(change)
with pytest.raises(ValueError):
validate_document(document)
def test_big_widgets_can_use_the_space_a_hidden_sidebar_adds():
from openpilot.starpilot.ui.onroad_customization import ANCHOR_SHIFT, PROFILES
widgets = PROFILES["large"]["widgets"]
document = default_document()
speed = document["layouts"]["large"]["current_speed"]
assert widgets["current_speed"]["bounds"]["x"] == 30 - 150 and widgets["steering_wheel"]["bounds"]["x"] == 30 - 300
assert all(widget["expandable"] for widget in widgets.values())
speed["x"] = 30 - 150
assert validate_document(document)["layouts"]["large"]["current_speed"]["x"] == -120
speed["x"] = 30 - 151
with pytest.raises(ValueError):
validate_document(document)
# Every widget reaches the right edge of the full screen, and not past it.
for key in widgets:
document = default_document()
document["layouts"]["large"]["speed_limit_actions"]["enabled"] = False
placed, width = document["layouts"]["large"][key], widget_size(document, "large", key)[0]
placed["x"] = 2130 - width - ANCHOR_SHIFT.get(key, 0) * 300
validate_document(document)
placed["x"] += 1
with pytest.raises(ValueError):
validate_document(document)
@pytest.mark.parametrize("right,bottom", [(False, False), (False, True), (True, False), (True, True)])
def test_every_large_widget_reaches_each_corner_and_fits_when_sidebar_returns(right, bottom):
from openpilot.starpilot.ui.onroad_customization import ANCHOR_SHIFT, PROFILES, fit_to_road
for key in PROFILES["large"]["widgets"]:
document = default_document()
document["layouts"]["large"]["speed_limit_actions"]["enabled"] = False
width, height = widget_size(document, "large", key)
shift = ANCHOR_SHIFT.get(key, 0) * 300
x, y = (2130 - width if right else 30) - shift, 1050 - height if bottom else 30
document["layouts"]["large"][key].update(x=x, y=y)
assert validate_document(document)["layouts"]["large"][key]["x"] == x
assert fit_to_road(document, 2160) is document
fitted = fit_to_road(document, 1860)["layouts"]["large"][key]
assert 30 <= fitted["x"] <= 1830 - width
assert fitted["y"] == y
assert document["layouts"]["large"][key]["x"] == x
def test_fit_to_road_pulls_widgets_in_only_where_the_road_is_narrower():
from openpilot.starpilot.ui.onroad_customization import fit_to_road
document = default_document()
large = document["layouts"]["large"]
large["stopped_timer"]["x"] = 2130 - 660
large["steering_wheel"]["x"] = -270
large["current_speed"]["x"] = -120
assert fit_to_road(document, 2160) is document
fitted = fit_to_road(document, 1860)["layouts"]["large"]
assert fitted["stopped_timer"]["x"] == 1830 - 660
assert fitted["steering_wheel"]["x"] == 30
assert fitted["current_speed"]["x"] == 30
assert fitted["cruise_limits"] is large["cruise_limits"]
assert large["stopped_timer"]["x"] == 2130 - 660
assert fit_to_road(default_document(), 1860)["layouts"]["large"] == default_document()["layouts"]["large"]
def test_jetlink_status_is_an_opt_in_big_widget_with_background_opacity():
document = default_document()
placed = document["layouts"]["large"]["jetlink_status"]
assert placed["enabled"] is False and placed["opacity"] == 85
assert "jetlink_status" not in document["layouts"]["compact"]
placed.update(enabled=True, x=100, y=300, opacity=40)
assert validate_document(document)["layouts"]["large"]["jetlink_status"]["opacity"] == 40
older = default_document()
del older["layouts"]["large"]["jetlink_status"]
assert validate_document(older)["layouts"]["large"]["jetlink_status"]["enabled"] is False
for bad in (101, -1, 50.5, True, None):
document["layouts"]["large"]["jetlink_status"]["opacity"] = bad
with pytest.raises(ValueError):
validate_document(document)