Files
StarPilot/selfdrive/ui/tests/test_navigation_card.py

177 lines
5.7 KiB
Python

import json
from collections import Counter
from types import SimpleNamespace
import pyray as rl
import pytest
from openpilot.common.params import UnknownKeyName
from openpilot.selfdrive.ui.onroad.starpilot import navigation_card
class FakeParams:
def __init__(self, values):
self.values = values
self.reads = Counter()
def get(self, key):
self.reads[key] += 1
return self.values.get(key)
def get_bool(self, key):
return bool(self.get(key))
def put_bool(self, key, value):
self.values[key] = value
def remove(self, key):
self.values.pop(key, None)
@pytest.fixture
def card_state(monkeypatch):
params = FakeParams({"NavigationUI": True, "NavDestination": {"name": "Home"}})
nav_state = {"valid": True, "maneuverPrimaryText": "Main Street", "maneuverDistance": 1500}
memory = FakeParams({"NavInstructionState": json.dumps(nav_state), "NavInstructionCollapsed": False})
ui = SimpleNamespace(ui_params=params, params_memory=memory, is_metric=True)
clock = [0.0]
monkeypatch.setattr(navigation_card, "ui_state", ui)
monkeypatch.setattr(navigation_card.time, "monotonic", lambda: clock[0])
monkeypatch.setattr(navigation_card.gui_app, "font", lambda _: None)
card = navigation_card.NavigationCardRenderer()
return card, ui, clock
def test_navigation_state_reads_are_bounded_at_sixty_fps(card_state):
card, ui, clock = card_state
for frame in range(60):
clock[0] = frame / 60
card._update_state()
assert card._valid
assert 8 <= ui.params_memory.reads["NavInstructionState"] <= 10
assert ui.params_memory.reads["NavInstructionCollapsed"] == ui.params_memory.reads["NavInstructionState"]
def test_render_does_not_repeat_state_update(card_state, monkeypatch):
card, ui, _ = card_state
card._update_state()
reads = ui.ui_params.reads.copy()
monkeypatch.setattr(card, "_render_default", lambda _: None)
card._render(rl.Rectangle(0, 0, 100, 100))
assert ui.ui_params.reads == reads
def test_metric_and_destination_changes_bypass_refresh_interval(card_state):
card, ui, clock = card_state
card._update_state()
assert card._distance == "1.5 km"
clock[0] = 0.01
ui.is_metric = False
card._update_state()
assert card._distance == "0.9 mi"
ui.ui_params.values["NavDestination"] = {"name": "Work"}
ui.params_memory.values["NavInstructionState"] = {"valid": True, "maneuverPrimaryText": "Second Street"}
card._update_state()
assert card._primary_text == "Second Street"
assert ui.params_memory.reads["NavInstructionState"] == 3
@pytest.mark.parametrize("key, value", [("NavigationUI", False), ("NavDestination", None)])
def test_disabled_navigation_clears_card_and_reenables_immediately(card_state, key, value):
card, ui, clock = card_state
card._update_state()
card._interactive_rect = rl.Rectangle(0, 0, 100, 100)
previous = ui.ui_params.values[key]
ui.ui_params.values[key] = value
clock[0] = 0.01
card._update_state()
assert not card._valid
assert card._hit_rect.width == 0
assert ui.params_memory.reads["NavInstructionState"] == 1
ui.ui_params.values[key] = previous
card._update_state()
assert card._valid
assert ui.params_memory.reads["NavInstructionState"] == 2
@pytest.mark.parametrize("state", [None, "{invalid json", {"valid": False}, {"valid": True, "maneuverPrimaryText": ""}])
def test_invalid_state_clears_card_at_next_refresh(card_state, state):
card, ui, clock = card_state
card._update_state()
card._interactive_rect = rl.Rectangle(0, 0, 100, 100)
ui.params_memory.values["NavInstructionState"] = state
clock[0] = 0.1
card._update_state()
assert not card._valid
assert card._hit_rect.width == 0
@pytest.mark.parametrize("supported", [False, True])
def test_collapse_changes_immediately_and_forces_refresh(card_state, monkeypatch, supported):
card, ui, _ = card_state
card._update_state()
if not supported:
def unsupported(*_):
raise UnknownKeyName("NavInstructionCollapsed")
monkeypatch.setattr(ui.params_memory, "put_bool", unsupported)
card._toggle_collapsed()
assert card._collapsed
card._update_state()
assert card._collapsed
assert ui.params_memory.reads["NavInstructionState"] == 2
card._toggle_collapsed()
assert not card._collapsed
def test_cancel_navigation_clears_card_immediately(card_state):
card, ui, _ = card_state
card._update_state()
card._interactive_rect = rl.Rectangle(0, 0, 100, 100)
card._cancel_navigation()
assert not card._valid
assert card._hit_rect.width == 0
assert "NavDestination" not in ui.ui_params.values
assert "NavInstructionState" not in ui.params_memory.values
assert "NavInstructionCollapsed" not in ui.params_memory.values
card._update_state()
assert not card._valid
@pytest.mark.parametrize("maneuver, modifier, expected", [
("turn", "left", "direction_turn_left.png"),
("turn", "uturn", "direction_uturn.png"),
("missing", "right", navigation_card.FALLBACK_ICON),
])
def test_icon_resolution_and_texture_load_are_cached(card_state, monkeypatch, maneuver, modifier, expected):
card, _, _ = card_state
checks = []
loads = []
texture = object()
def exists(path):
checks.append(path.name)
return not path.name.startswith("direction_missing")
monkeypatch.setattr(navigation_card.Path, "exists", exists)
monkeypatch.setattr(rl, "load_image", lambda path: loads.append(path))
monkeypatch.setattr(rl, "load_texture_from_image", lambda _: texture)
monkeypatch.setattr(rl, "set_texture_filter", lambda *_: None)
monkeypatch.setattr(rl, "set_texture_wrap", lambda *_: None)
monkeypatch.setattr(rl, "unload_image", lambda _: None)
for _ in range(60):
assert card._get_icon(maneuver, modifier) is texture
assert len(checks) == 1
assert loads == [str(navigation_card.ASSETS_PATH / expected)]