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

162 lines
8.2 KiB
Python

from typing import cast
import json
import struct
import zlib
import pytest
from openpilot.starpilot.ui.layout_preview_renderer import _png_rgba, _straight_rgba
from openpilot.starpilot.ui.layout_preview_transport import _payload, validate_png, PreviewInvalid
from openpilot.starpilot.ui.onroad_customization import RUNTIME_PREVIEW_WIDGETS, customization_metadata, default_document
def test_widget_art_request_uses_same_validated_document_and_scene():
request = {'document': default_document(), 'profile': 'compact', 'scene': 'cem_curve', 'art': True}
assert _payload(request) == request
assert _payload({**request, 'art': 'backdrop'}) == {**request, 'art': 'backdrop'}
for art in (False, None, 1, 'yes', 'Backdrop'):
with pytest.raises(PreviewInvalid):
_payload({**request, 'art': art})
with pytest.raises(PreviewInvalid):
_payload({**request, 'scene': 'live'})
without = {key: value for key, value in request.items() if key != 'art'}
assert _payload(without) == without
@pytest.mark.parametrize('sidebar', (True, False))
def test_native_preview_sidebar_choice_is_strict_and_does_not_change_saved_layout(sidebar):
document = default_document()
payload = {'document': {**document, 'developerSidebar': sidebar}, 'profile': 'large', 'scene': 'engaged'}
assert _payload(payload) == payload
assert 'developerSidebar' not in document
for invalid in (0, 1, 'Off', None, [], {}):
with pytest.raises(PreviewInvalid):
_payload({**payload, 'document': {**document, 'developerSidebar': invalid}})
with pytest.raises(PreviewInvalid):
_payload({**payload, 'document': list(document.items())})
def test_widget_art_registry_is_shared_with_native_and_projection_metadata():
from openpilot.starpilot.system.starport.projection_layout import layout_metadata_for_viewport
metadata = customization_metadata()['profiles']
for profile, ids in RUNTIME_PREVIEW_WIDGETS.items():
assert metadata[profile]['runtimePreviewWidgets'] == list(ids)
assert all(key in metadata[profile]['widgets'] for key in ids)
assert layout_metadata_for_viewport((2880, 1080))['runtimePreviewWidgets'] == list(RUNTIME_PREVIEW_WIDGETS['large'])
def test_transparent_render_coverage_exports_straight_alpha_without_darkening():
straight = _straight_rgba(bytes((128, 64, 0, 128, 9, 8, 7, 0, 255, 255, 255, 255)))
assert straight == bytes((255, 128, 0, 128, 0, 0, 0, 0, 255, 255, 255, 255))
# Browser source-over reconstructs the native scratch RGB to within one byte.
assert [round(value * 128 / 255) for value in straight[:3]] == [128, 64, 0]
def test_sprite_sheet_metadata_and_pixels_share_actual_png_dimensions():
metadata = {'version': 1, 'width': 2, 'height': 1, 'sprites': {'conditional_mode': {'x': 0, 'y': 0, 'width': 2, 'height': 1}}}
pixels = bytes((255, 255, 255, 255, 0, 0, 0, 0))
image = _png_rgba(2, 1, pixels, widget_art=metadata)
assert validate_png(image) == image
offset = 8
chunks = {}
while offset < len(image):
size = struct.unpack('!I', image[offset:offset + 4])[0]
tag, data = image[offset + 4:offset + 8], image[offset + 8:offset + 8 + size]
assert zlib.crc32(tag + data) & 0xffffffff == struct.unpack('!I', image[offset + 8 + size:offset + 12 + size])[0]
chunks[tag] = data
offset += size + 12
assert json.loads(chunks[b'tEXt'].split(b'\0', 1)[1]) == metadata
assert zlib.decompress(chunks[b'IDAT']) == b'\0' + pixels
def test_static_compact_sample_settles_actual_hud_without_changing_live_blink_policy():
from pathlib import Path
from types import SimpleNamespace as NS
from unittest.mock import Mock
import time
from openpilot.starpilot.ui.layout_preview_renderer import _Canvas, sample_state
from openpilot.starpilot.ui.onroad_compact_widgets import CompactHudRenderer
from openpilot.starpilot.ui.presentation import Profile
from openpilot.starpilot.ui.onroad import OnroadView
hud = CompactHudRenderer(Mock(), Path('/unused-assets'))
canvas = _Canvas.__new__(_Canvas)
canvas.profile = Profile.COMPACT
canvas.view = cast(OnroadView, NS(compact_hud=hud, torque_bar=NS(_torque_filter=NS(x=0), _alpha_filter=NS(x=0))))
sample = sample_state('engaged', default_document())
for _ in range(2):
canvas._settle(sample)
assert hud._set_speed_opacity(sample, time.monotonic_ns()) == pytest.approx(1.)
lateral_only = sample_state('aol', default_document())
canvas._settle(lateral_only)
assert hud._set_speed_opacity(lateral_only, time.monotonic_ns()) == 0.
def test_scene_switches_settle_cem_geometry_independently_of_previous_scene():
from types import SimpleNamespace as NS
from unittest.mock import Mock
from openpilot.starpilot.ui.layout_preview_renderer import _Canvas, sample_state
from openpilot.starpilot.ui.onroad_cem_gauge import AetherGauge, _get_road_height, gauge_data
from openpilot.starpilot.ui.onroad import OnroadView
from openpilot.starpilot.ui.presentation import Profile
gauge = AetherGauge(Mock())
canvas = _Canvas.__new__(_Canvas)
canvas.profile = Profile.LARGE
canvas.view = cast(OnroadView, NS(cem_gauge=gauge, torque_bar=NS(_torque_filter=NS(x=0), _alpha_filter=NS(x=0))))
for scene in ('cem_stop_light', 'cem_curve', 'cem_lead', 'engaged', 'cem_curve'):
state = sample_state(scene, default_document())
gauge._road_h_filter.x = -1 # A previous animation must never leak into a static scene.
canvas._settle(state)
assert gauge._road_h_filter.x == _get_road_height(gauge_data(state))
assert gauge._drive_frame == state.drive_frame
@pytest.mark.parametrize('viewport', (None, (2880, 1080)))
@pytest.mark.parametrize('sidebar', (True, False))
def test_large_wheel_art_is_drawn_inside_its_crop_with_or_without_sidebar(monkeypatch, viewport, sidebar):
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace as NS
from unittest.mock import Mock
import pyray as rl
from openpilot.starpilot.ui import layout_preview_renderer as renderer
from openpilot.starpilot.ui.onroad import OnroadView
from openpilot.starpilot.ui.onroad_large_widgets import SteeringWheelWidget
from openpilot.starpilot.ui.presentation import Profile
from openpilot.starpilot.system.starport.projection_layout import default_layout_for_viewport, projection_customization
document = default_document()
positions = document['layouts']['large']
if viewport:
projection = default_layout_for_viewport(viewport)
positions = projection['widgets']
document = projection_customization(projection, document)
canvas = renderer._Canvas.__new__(renderer._Canvas)
canvas.profile, canvas.viewport = Profile.LARGE, viewport
monkeypatch.setattr(canvas, 'fonts', NS(profile=Profile.LARGE), raising=False)
canvas.target = NS(texture=object())
view = OnroadView.__new__(OnroadView)
monkeypatch.setattr(view, 'fonts', canvas.fonts, raising=False)
view.projection_viewport = viewport
view.steering_wheel = SteeringWheelWidget(Path('/unused'))
view.steering_wheel._texture = object() # Real widget geometry, without loading a GL texture.
canvas.view = view
monkeypatch.setattr(canvas, '_settle', lambda state: None)
monkeypatch.setitem(renderer.RUNTIME_PREVIEW_WIDGETS, 'large', ('steering_wheel',))
monkeypatch.setattr(rl, 'rl', NS(rlSetBlendFactorsSeparate=Mock()))
for method in ('begin_texture_mode', 'clear_background', 'begin_blend_mode', 'end_blend_mode', 'end_texture_mode',
'image_format', 'image_flip_vertical', 'image_crop', 'unload_image', 'draw_circle', 'draw_ring', 'draw_texture_pro'):
monkeypatch.setattr(rl, method, Mock())
pixels = rl.ffi.new('unsigned char[]', 192 * 192 * 4)
monkeypatch.setattr(rl, 'load_image_from_texture', Mock(return_value=NS(data=pixels)))
monkeypatch.setattr(renderer, '_png_rgba', Mock(return_value=b'png'))
state = renderer.sample_state('braking', document)
canvas.render_widget_art(replace(state, appearance=replace(state.appearance, show_developer_sidebar=sidebar)), positions)
drawn = rl.draw_texture_pro.call_args.args[2]
crop = rl.image_crop.call_args.args[1]
assert crop.x <= drawn.x < drawn.x + drawn.width <= crop.x + crop.width
assert crop.y <= drawn.y < drawn.y + drawn.height <= crop.y + crop.height
color = rl.draw_texture_pro.call_args.args[-1]
assert (color.r, color.g, color.b) == (255, 0, 0), 'The braking wheel must remain visible and red'