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

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