mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
335 lines
19 KiB
Python
335 lines
19 KiB
Python
|
|
from unittest.mock import patch
|
|
from dataclasses import replace
|
|
import io
|
|
import struct
|
|
from types import SimpleNamespace as NS
|
|
import unittest
|
|
from unittest.mock import Mock, call
|
|
import zlib
|
|
|
|
from openpilot.starpilot.ui.layout_preview_renderer import LayoutPreviewRenderer, _Canvas, _DriverMonitorArt, _png_rgba, sample_state
|
|
from openpilot.starpilot.ui.layout_preview_sidebar import SAMPLE_METRICS, metric_rects, render_sidebar
|
|
from openpilot.starpilot.ui.onroad_customization import default_document
|
|
from openpilot.starpilot.ui.presentation import Profile
|
|
import pyray as rl
|
|
|
|
|
|
class LayoutPreviewRendererTests(unittest.TestCase):
|
|
def test_sidebar_choice_expands_native_preview_and_keeps_sprite_crops_in_saved_coordinates(self):
|
|
document = default_document()
|
|
with patch('openpilot.starpilot.ui.layout_preview_renderer.rl.is_window_ready', return_value=True), \
|
|
patch('openpilot.starpilot.ui.layout_preview_renderer._Canvas') as canvas_type:
|
|
canvas = Mock(profile=Profile.LARGE, viewport=None)
|
|
canvas_type.return_value = canvas
|
|
renderer = LayoutPreviewRenderer()
|
|
for sidebar, width in ((True, 1860), (False, 2160)):
|
|
payload = {'document': {**document, 'developerSidebar': sidebar}, 'profile': 'large', 'scene': 'engaged'}
|
|
renderer(payload)
|
|
state = canvas.render.call_args.args[0]
|
|
self.assertEqual(state.viewport_width, width)
|
|
self.assertIs(state.appearance.show_developer_sidebar, sidebar)
|
|
self.assertEqual(state.customization, document)
|
|
renderer({**payload, 'art': 'backdrop'})
|
|
self.assertEqual(canvas.render.call_args.args[0].viewport_width, width)
|
|
renderer({**payload, 'art': True})
|
|
self.assertEqual(canvas.render_widget_art.call_args.args[0].viewport_width, width)
|
|
self.assertEqual(payload['document']['developerSidebar'], sidebar)
|
|
|
|
def test_canvas_omits_sample_sidebar_when_hidden(self):
|
|
from contextlib import ExitStack
|
|
canvas = _Canvas.__new__(_Canvas)
|
|
canvas.profile, canvas.viewport, canvas.map = Profile.LARGE, None, None
|
|
canvas.view = canvas.fonts = canvas.target = Mock()
|
|
canvas._settle = Mock()
|
|
state = sample_state('engaged', default_document())
|
|
state = replace(state, appearance=replace(state.appearance, show_developer_sidebar=True))
|
|
pixels = rl.ffi.new('unsigned char[]', 2160 * 1080 * 4)
|
|
with ExitStack() as stack:
|
|
for name in ('begin_texture_mode', 'clear_background', 'end_texture_mode', 'image_format', 'image_flip_vertical', 'unload_image'):
|
|
stack.enter_context(patch('openpilot.starpilot.ui.layout_preview_renderer.rl.' + name))
|
|
stack.enter_context(patch('openpilot.starpilot.ui.layout_preview_renderer.rl.load_image_from_texture', return_value=NS(data=pixels)))
|
|
stack.enter_context(patch('openpilot.starpilot.ui.layout_preview_renderer._png_rgba', return_value=b'png'))
|
|
sidebar = stack.enter_context(patch('openpilot.starpilot.ui.layout_preview_renderer.render_sidebar'))
|
|
canvas.render(state, 'engaged')
|
|
sidebar.assert_called_once()
|
|
sidebar.reset_mock()
|
|
canvas.render(replace(state, viewport_width=2160, appearance=replace(state.appearance, show_developer_sidebar=False)), 'engaged')
|
|
sidebar.assert_not_called()
|
|
|
|
def test_native_and_projection_previews_supply_timer_sample(self):
|
|
from openpilot.starpilot.system.starport.projection_layout import default_layout_for_viewport
|
|
from openpilot.starpilot.ui.layout_preview_transport import SCENES
|
|
from openpilot.starpilot.ui.onroad_customization import STOPPED_TIMER_WIDGET
|
|
native = default_document()
|
|
native['layouts']['compact'][STOPPED_TIMER_WIDGET].update(enabled=True, y=2)
|
|
projection = default_layout_for_viewport((2880, 1080))
|
|
projection['widgets'][STOPPED_TIMER_WIDGET].update(enabled=True, x=450, y=600)
|
|
payloads = [('compact', native), ('projection', {'base': default_document(), 'layout': projection})]
|
|
with patch('openpilot.starpilot.ui.layout_preview_renderer.rl.is_window_ready', return_value=True), \
|
|
patch('openpilot.starpilot.ui.layout_preview_renderer._Canvas') as canvas_type:
|
|
for profile, document in payloads:
|
|
renderer = LayoutPreviewRenderer()
|
|
renderer({'profile': profile, 'document': document, 'scene': 'stopped'})
|
|
state, scene = canvas_type.return_value.render.call_args.args
|
|
self.assertEqual(scene, 'stopped')
|
|
self.assertEqual(state.speed_mps, 0)
|
|
self.assertEqual(state.stopped_duration_s, 134)
|
|
layout_profile = 'large' if profile == 'projection' else profile
|
|
self.assertTrue(state.customization['layouts'][layout_profile][STOPPED_TIMER_WIDGET]['enabled'])
|
|
if profile == 'projection':
|
|
self.assertEqual(canvas_type.call_args.args, (Profile.LARGE, (2880, 1080)))
|
|
self.assertEqual(state.customization['layouts']['large'][STOPPED_TIMER_WIDGET]['x'], 450)
|
|
for scene in SCENES - {'stopped'}:
|
|
with self.subTest(profile=profile, scene=scene):
|
|
renderer({'profile': profile, 'document': document, 'scene': scene})
|
|
state = canvas_type.return_value.render.call_args.args[0]
|
|
self.assertIsNone(state.stopped_duration_s)
|
|
renderer.close()
|
|
|
|
def test_big_and_projection_previews_supply_route_and_timer_samples_but_compact_does_not(self):
|
|
from openpilot.starpilot.system.starport.projection_layout import default_layout_for_viewport
|
|
from openpilot.starpilot.ui.onroad_customization import STOPPED_TIMER_WIDGET
|
|
native = default_document()
|
|
native['layouts']['large'][STOPPED_TIMER_WIDGET]['enabled'] = True
|
|
native['layouts']['large']['nav_map'].update(enabled=True)
|
|
payloads = [('large', native, Profile.LARGE, None),
|
|
('projection', {'base': default_document(), 'layout': default_layout_for_viewport((2880, 1080))}, Profile.LARGE, (2880, 1080)),
|
|
('compact', default_document(), Profile.COMPACT, None)]
|
|
with patch('openpilot.starpilot.ui.layout_preview_renderer.rl.is_window_ready', return_value=True), \
|
|
patch('openpilot.starpilot.ui.layout_preview_renderer._Canvas') as canvas_type:
|
|
for profile, document, expected, viewport in payloads:
|
|
renderer = LayoutPreviewRenderer()
|
|
renderer({'profile': profile, 'document': document, 'scene': 'stopped'})
|
|
self.assertEqual(canvas_type.call_args.args, (expected, viewport))
|
|
state = canvas_type.return_value.render.call_args.args[0]
|
|
self.assertEqual(state.navigation is not None, expected == Profile.LARGE, profile)
|
|
if profile == 'large':
|
|
self.assertEqual(state.stopped_duration_s, 134)
|
|
self.assertTrue(state.customization['layouts']['large']['nav_card']['enabled'])
|
|
renderer.close()
|
|
|
|
def test_big_preview_fits_widgets_to_the_road_that_is_showing(self):
|
|
from openpilot.starpilot.ui.onroad_customization import widget_size
|
|
document = default_document()
|
|
from openpilot.starpilot.ui.large_speed_geometry import confirmation_footprint
|
|
area = confirmation_footprint('large')
|
|
card = max(widget_size(document, 'large', 'cruise_limits')[0], area['x'] + area['width'])
|
|
document['layouts']['large']['cruise_limits']['x'] = 2130 - card
|
|
document['layouts']['large']['steering_wheel']['x'] = -270
|
|
with patch('openpilot.starpilot.ui.layout_preview_renderer.rl.is_window_ready', return_value=True), \
|
|
patch('openpilot.starpilot.ui.layout_preview_renderer._Canvas') as canvas_type:
|
|
renderer = LayoutPreviewRenderer()
|
|
for sidebar, art, width, viewport in ((False, None, 2160, 2160), (True, None, 1860, 1860), (False, True, 1860, 2160)):
|
|
payload = {'profile': 'large', 'scene': 'engaged', 'document': {**document, 'developerSidebar': sidebar}}
|
|
if art:
|
|
payload['art'] = True
|
|
canvas_type.return_value.render_widget_art.return_value = b'png'
|
|
renderer(payload)
|
|
call = canvas_type.return_value.render_widget_art if art else canvas_type.return_value.render
|
|
state = call.call_args.args[0]
|
|
large = state.customization['layouts']['large']
|
|
self.assertEqual(state.viewport_width, viewport)
|
|
self.assertEqual(large['cruise_limits']['x'], 2130 - card if width == 2160 else 1830 - card)
|
|
self.assertEqual(large['steering_wheel']['x'], -270 if width == 2160 else 30)
|
|
renderer.close()
|
|
|
|
def test_fixed_scenes_have_no_camera_or_live_input(self):
|
|
document = default_document()
|
|
expected = {
|
|
"engaged": (True, True), "aol": (True, False), "long_only": (False, True),
|
|
"experimental": (True, True), "braking": (True, True), "slc_pending": (True, True),
|
|
"cem_stop_light": (True, True), "cem_lead": (True, True), "cem_curve": (True, True),
|
|
}
|
|
for name, axes in expected.items():
|
|
with self.subTest(name=name):
|
|
state = sample_state(name, document)
|
|
self.assertFalse(state.camera_available)
|
|
self.assertEqual((state.lateral_active, state.longitudinal_active), axes)
|
|
self.assertEqual(state.alert.size, "none")
|
|
self.assertEqual(state.wheel_acceleration, -2.0 if name == "braking" else 0.0)
|
|
self.assertTrue(state.appearance.show_speed_limit_sign)
|
|
if state.lateral_active:
|
|
self.assertGreater(state.torque_utilization, 0.5)
|
|
else:
|
|
self.assertEqual(state.torque_utilization, 0)
|
|
from openpilot.starpilot.ui.onroad_state import slc_controls
|
|
pending = sample_state("slc_pending", document)
|
|
for profile in (Profile.LARGE, Profile.COMPACT):
|
|
self.assertEqual([item.label for item in slc_controls(profile, pending)],
|
|
["Accept", "Reject"])
|
|
# The engaged sample already uses its accepted limit, so there is no redundant adopt action.
|
|
self.assertEqual(slc_controls(profile, sample_state("engaged", document)), ())
|
|
manual = sample_state("experimental", document)
|
|
self.assertTrue(manual.experimental_enabled)
|
|
self.assertIsNone(manual.visual_preview)
|
|
self.assertIsNone(manual.conditional_effective)
|
|
for scene, reason in (("cem_stop_light", "STOP LIGHT"), ("cem_lead", "LEAD"), ("cem_curve", "CURVE")):
|
|
with self.subTest(scene=scene):
|
|
sample = sample_state(scene, document)
|
|
self.assertTrue(sample.experimental_enabled)
|
|
self.assertEqual(sample.visual_preview.cem_reason, reason)
|
|
self.assertIsNone(sample.conditional_effective)
|
|
self.assertTrue(sample.visual_preview.layout_sample) # Drawn as the car would, without replay labels.
|
|
from openpilot.starpilot.ui.onroad_conditional import status
|
|
# The gauge still reads the real reason, but the mode caption itself is not drawn in the editor.
|
|
observed = status(sample)
|
|
assert observed is not None
|
|
family, detail, _ = observed
|
|
self.assertEqual(family, "CEM")
|
|
self.assertFalse(detail.startswith("PREVIEW"))
|
|
self.assertEqual(sample_state("cem_lead", document).speed_mps, 0.0)
|
|
with self.assertRaises(ValueError):
|
|
sample_state("live", document)
|
|
|
|
def test_invalid_draft_rejected_before_gl_resources(self):
|
|
renderer = LayoutPreviewRenderer()
|
|
document = default_document()
|
|
document["layouts"]["large"]["current_speed"]["x"] = -200
|
|
with patch("openpilot.starpilot.ui.layout_preview_renderer._Canvas") as canvas:
|
|
with self.assertRaises(ValueError):
|
|
renderer({"document": document, "profile": "large", "scene": "engaged"})
|
|
canvas.assert_not_called()
|
|
|
|
def test_profile_swap_and_inactivity_release_owned_canvas(self):
|
|
renderer = LayoutPreviewRenderer()
|
|
document = default_document()
|
|
with patch("openpilot.starpilot.ui.layout_preview_renderer.rl.is_window_ready", return_value=True), \
|
|
patch("openpilot.starpilot.ui.layout_preview_renderer._Canvas") as canvas_type, \
|
|
patch("openpilot.starpilot.ui.layout_preview_renderer.time.monotonic", side_effect=[10.0, 11.0, 50.0]):
|
|
first, second = Mock(profile=Profile.LARGE), Mock(profile=Profile.COMPACT)
|
|
first.render.return_value = second.render.return_value = b"png"
|
|
canvas_type.side_effect = [first, second]
|
|
self.assertEqual(renderer({"document": document, "profile": "large", "scene": "aol"}), b"png")
|
|
self.assertEqual(renderer({"document": document, "profile": "compact", "scene": "aol"}), b"png")
|
|
first.close.assert_called_once()
|
|
renderer.expire()
|
|
second.close.assert_called_once()
|
|
|
|
def test_backdrop_draws_the_real_screen_without_movable_widgets(self):
|
|
renderer = LayoutPreviewRenderer()
|
|
document = default_document()
|
|
with patch("openpilot.starpilot.ui.layout_preview_renderer.rl.is_window_ready", return_value=True), \
|
|
patch("openpilot.starpilot.ui.layout_preview_renderer._Canvas") as canvas_type:
|
|
canvas = Mock(profile=Profile.COMPACT, viewport=None)
|
|
canvas.render.return_value = b"png"
|
|
canvas_type.return_value = canvas
|
|
self.assertEqual(renderer({"document": document, "profile": "compact", "scene": "braking", "art": "backdrop"}), b"png")
|
|
state, scene = canvas.render.call_args.args
|
|
self.assertEqual(scene, "braking")
|
|
self.assertEqual(canvas.render.call_args.kwargs, {})
|
|
# An order naming no widget keeps the ordered path, so no movable widget is drawn.
|
|
self.assertEqual(state.customization["widgetOrder"]["compact"], ["backdrop"])
|
|
self.assertEqual(state.customization["layouts"], document["layouts"])
|
|
with self.assertRaises(ValueError):
|
|
renderer({"document": document, "profile": "compact", "scene": "braking", "art": "widgets"})
|
|
|
|
def test_png_encoder_preserves_rgba_pixels(self):
|
|
pixels = bytes((255, 0, 0, 255, 0, 255, 0, 128))
|
|
png = _png_rgba(2, 1, pixels)
|
|
self.assertTrue(png.startswith(b"\x89PNG\r\n\x1a\n"))
|
|
stream = io.BytesIO(png[8:])
|
|
chunks = {}
|
|
while length_bytes := stream.read(4):
|
|
length = struct.unpack(">I", length_bytes)[0]
|
|
kind = stream.read(4)
|
|
data = stream.read(length)
|
|
crc = struct.unpack(">I", stream.read(4))[0]
|
|
self.assertEqual(crc, zlib.crc32(kind + data) & 0xffffffff)
|
|
chunks[kind] = data
|
|
self.assertEqual(struct.unpack(">II", chunks[b"IHDR"][:8]), (2, 1))
|
|
self.assertEqual(zlib.decompress(chunks[b"IDAT"]), b"\0" + pixels)
|
|
|
|
def test_compact_sample_seeds_settled_native_widget_filters(self):
|
|
canvas = _Canvas.__new__(_Canvas)
|
|
canvas.profile = Profile.COMPACT
|
|
canvas.viewport = None
|
|
self.enterContext(patch.object(canvas, 'view', NS(compact_hud=NS(_was_cruise_active=False, _set_speed_alpha=NS(x=0.0),
|
|
_wheel_alpha=NS(x=0.0), _wheel_y=NS(x=25.0)),
|
|
torque_bar=NS(_torque_filter=NS(x=0.0), _alpha_filter=NS(x=0.0))), create=True))
|
|
braking = sample_state("braking", default_document())
|
|
canvas._settle(braking)
|
|
self.assertTrue(canvas.view.compact_hud._was_cruise_active)
|
|
self.assertEqual(canvas.view.compact_hud._set_speed_alpha.x, 1.0)
|
|
self.assertGreater(canvas.view.compact_hud._wheel_alpha.x, 200)
|
|
self.assertEqual(canvas.view.compact_hud._wheel_y.x, 0.0)
|
|
self.assertEqual(canvas.view.torque_bar._torque_filter.x, braking.torque_utilization)
|
|
self.assertEqual(canvas.view.torque_bar._alpha_filter.x, 1.0)
|
|
|
|
def test_preview_uses_runtime_monitor_occlusion_for_default_and_moved_widgets(self):
|
|
canvas = _Canvas.__new__(_Canvas)
|
|
canvas.profile = Profile.COMPACT
|
|
canvas.viewport = None
|
|
self.enterContext(patch.object(canvas, 'view', NS(compact_hud=NS(_set_speed_alpha=NS(x=1.0))), create=True))
|
|
canvas.monitor = _DriverMonitorArt.__new__(_DriverMonitorArt)
|
|
canvas.monitor.profile = Profile.COMPACT
|
|
canvas.monitor.renderer = Mock()
|
|
matrix = Mock()
|
|
matrix.attach_mock(canvas.monitor.renderer._render, 'render')
|
|
state = sample_state("engaged", default_document())
|
|
with patch("openpilot.starpilot.ui.layout_preview_renderer.draw_widget_frame"), \
|
|
patch.multiple(rl, rl_push_matrix=matrix.push, rl_translatef=matrix.translate,
|
|
rl_scalef=matrix.scale, rl_pop_matrix=matrix.pop):
|
|
canvas._render_driver_monitor(rl.Rectangle(), state)
|
|
canvas.monitor.renderer._render.assert_not_called()
|
|
self.assertEqual(matrix.mock_calls, [])
|
|
state.customization["layouts"]["compact"]["driver_monitor"].update(x=200, y=100)
|
|
canvas._render_driver_monitor(rl.Rectangle(), state)
|
|
canvas.monitor.renderer._render.assert_called_once()
|
|
self.assertEqual(matrix.mock_calls, [call.push(), call.translate(200, 100, 0), call.scale(1, 1, 1),
|
|
call.translate(-200, -100, 0), call.render(canvas.monitor.renderer._rect), call.pop()])
|
|
matrix.reset_mock()
|
|
canvas.monitor.renderer._render.reset_mock()
|
|
state.customization["layouts"]["compact"]["driver_monitor"].update(x=16, y=10)
|
|
canvas.view.compact_hud._set_speed_alpha.x = 0.0
|
|
canvas._render_driver_monitor(rl.Rectangle(), state)
|
|
canvas.monitor.renderer._render.assert_called_once()
|
|
self.assertEqual(matrix.mock_calls, [call.push(), call.translate(16, 10, 0), call.scale(1, 1, 1),
|
|
call.translate(-16, -10, 0), call.render(canvas.monitor.renderer._rect), call.pop()])
|
|
|
|
def test_big_sidebar_matches_original_right_rail_and_is_sample_only(self):
|
|
frame = rl.Rectangle(1860, 0, 300, 1080)
|
|
rects = metric_rects(frame, len(SAMPLE_METRICS))
|
|
self.assertEqual(len(rects), 7)
|
|
self.assertEqual([(r.x, r.y, r.width, r.height) for r in rects],
|
|
[(1873, y, 275, 126) for y in (24, 174, 324, 474, 624, 774, 924)])
|
|
self.assertEqual([metric.label for metric in SAMPLE_METRICS],
|
|
["ACCEL", "STEER DELAY", "FRICTION", "LAT ACCEL", "LATERAL %", "TORQUE %", "CHESTNUT"])
|
|
with self.assertRaises(ValueError):
|
|
metric_rects(frame, 8)
|
|
|
|
def test_sidebar_render_uses_caller_supplied_values_without_live_state(self):
|
|
fonts = Mock()
|
|
fonts.measure.return_value = NS(width=80, height=25)
|
|
metric = (SAMPLE_METRICS[0],)
|
|
with patch("openpilot.starpilot.ui.layout_preview_sidebar.rl.draw_rectangle_rec"), \
|
|
patch("openpilot.starpilot.ui.layout_preview_sidebar.rl.draw_rectangle_rounded"), \
|
|
patch("openpilot.starpilot.ui.layout_preview_sidebar.rl.draw_rectangle_rounded_lines_ex"):
|
|
render_sidebar(fonts, rl.Rectangle(1860, 0, 300, 1080), metric)
|
|
labels = [call.args[0] for call in fonts.draw.call_args_list]
|
|
self.assertEqual(labels, ["ACCEL", "1.08 ft/s²"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
|
|
|
|
def test_native_preview_draws_independent_camera_circles_without_camera_input():
|
|
canvas = _Canvas.__new__(_Canvas)
|
|
canvas.profile = Profile.COMPACT
|
|
document = default_document()
|
|
state = sample_state('engaged', document)
|
|
with patch('openpilot.starpilot.ui.layout_preview_renderer.rl.draw_circle_v') as circle, \
|
|
patch('openpilot.starpilot.ui.layout_preview_renderer.rl.draw_ring') as ring:
|
|
canvas._render_side_cameras(state)
|
|
assert circle.call_count == ring.call_count == 2
|
|
right_center = circle.call_args.args[0]
|
|
document['layouts']['compact']['pip_left']['enabled'] = False
|
|
document['layouts']['compact']['pip_left']['x'] = 150
|
|
circle.reset_mock()
|
|
ring.reset_mock()
|
|
canvas._render_side_cameras(sample_state('engaged', document))
|
|
assert circle.call_count == ring.call_count == 1
|
|
assert circle.call_args.args[0].x == right_center.x
|
|
assert circle.call_args.args[0].y == right_center.y
|