Files
StarPilot/openpilot/starpilot/ui/tests/test_long_profile_feature.py
T
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

256 lines
13 KiB
Python

"""Real Params and native intent checks for Long Planner saved-value recovery."""
from pathlib import Path
from dataclasses import replace
from types import SimpleNamespace
import tempfile
import unittest
from unittest.mock import patch
from openpilot.common.params import Params
from openpilot.starpilot.longitudinal.profile_preferences import SCALARS, read_profile_health
from openpilot.starpilot.longitudinal.profile_runtime import read_settings
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import feature_row_top, FeatureInput, FeatureSettingsRequest, row_change
from openpilot.starpilot.ui import feature_settings_compact as compact
from openpilot.starpilot.ui.long_profile_feature import long_confirm_question
from openpilot.starpilot.ui.runtime_app import StarShellSession
class LongProfileFeatureTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.params = Params(self.temp.name)
self.cp = SimpleNamespace(carFingerprint="TEST MODEL", openpilotLongitudinalControl=True,
pcmCruise=False, passive=False, notCar=False, dashcamOnly=False, carVin="VIN1")
self.allowed = True
self.owner = FeatureSettingsOwner(self.params, lambda _: self.allowed,
vehicle_fingerprint=lambda: self.cp.carFingerprint,
vehicle_params=lambda: self.cp)
def path(self, key):
return Path(self.params.get_param_path(key))
def row(self, page, key):
state = self.owner.snapshot(page, parked=True, system_long=True, lateral_context=True, metric=False)
return next(row for row in state.rows if row.key == key)
@staticmethod
def confirm(row):
return FeatureSettingsRequest(row.key, row.source, "confirm", confirmation=True,
vehicle_fingerprint=row.vehicle_fingerprint, capability=row.capability,
dependencies=row.dependencies)
def test_health_runtime_and_repair_of_disabled_profile(self):
self.params.put_bool("CustomPersonalities", True, block=True)
self.path("RelaxedFollow").write_bytes(b"nan")
self.assertFalse(read_profile_health(self.params).dependencies_valid)
self.assertIsNone(read_settings(self.params))
master = self.row("profiles", "CustomPersonalities")
self.assertTrue(master.available) # Off remains possible with a corrupt dependent.
repair = self.row("relaxed", "long_repair:RelaxedFollow")
self.assertTrue(repair.available)
self.assertIsNone(row_change(repair))
self.assertTrue(self.owner.apply(self.confirm(repair)))
self.assertAlmostEqual(self.params.get("RelaxedFollow"), 1.6)
self.assertIsNotNone(read_settings(self.params))
def test_onroad_profile_values_edit_but_repair_remains_parked(self):
owner = FeatureSettingsOwner(self.params, lambda group: group != "parked_preferences",
vehicle_fingerprint=lambda: self.cp.carFingerprint,
vehicle_params=lambda: self.cp)
state = owner.snapshot("relaxed", parked=False, system_long=True, lateral_context=True, metric=False,
configure_while_driving=True)
follow = next(row for row in state.rows if row.key == "RelaxedFollow")
self.assertTrue(follow.available)
request = row_change(follow)
assert request is not None
self.assertTrue(owner.apply(request))
self.assertFalse(owner.apply(request)) # The displayed source is now stale.
self.path("RelaxedJerkDanger").write_bytes(b"bad")
state = owner.snapshot("relaxed", parked=False, system_long=True, lateral_context=True, metric=False,
configure_while_driving=True)
repair = next(row for row in state.rows if row.key == "long_repair:RelaxedJerkDanger")
self.assertFalse(repair.available)
self.assertFalse(owner.apply(self.confirm(repair)))
def test_master_on_requires_all_saved_dependencies_valid(self):
self.assertTrue(read_profile_health(self.params).dependencies_valid)
self.path("RelaxedJerkDanger").write_bytes(b"201")
master = self.row("profiles", "CustomPersonalities")
self.assertFalse(master.available)
self.assertIsNone(row_change(master))
self.assertFalse(self.params.get_bool("CustomPersonalities"))
repair = self.row("relaxed", "long_repair:RelaxedJerkDanger")
self.assertTrue(self.owner.apply(self.confirm(repair)))
master = self.row("profiles", "CustomPersonalities")
self.assertTrue(master.available)
request = row_change(master)
self.assertIsNotNone(request)
assert request is not None
self.assertTrue(self.owner.apply(request))
self.assertIsNotNone(read_settings(self.params))
def test_nonfinite_and_deep_document_fail_closed(self):
self.params.put_bool("CustomPersonalities", True, block=True)
self.path("StandardFollow").write_bytes(b"inf")
self.assertIsNone(read_settings(self.params))
self.path("StandardFollow").unlink()
self.path("LongitudinalPersonalityProfiles").write_bytes(b"[" * 1100 + b"0" + b"]" * 1100)
self.assertFalse(read_profile_health(self.params).dependencies_valid)
self.assertIsNone(read_settings(self.params))
def test_invalid_flag_and_master_repair_never_enables(self):
self.path("StandardPersonalityProfile").write_bytes(b"bad")
self.path("CustomPersonalities").write_bytes(b"bad")
self.assertIsNone(read_settings(self.params))
master_repair = self.row("profiles", "long_repair:CustomPersonalities")
self.assertIn("to Off", master_repair.label)
self.assertIn("to Off", long_confirm_question(master_repair))
self.assertTrue(self.owner.apply(self.confirm(master_repair)))
self.assertFalse(self.params.get_bool("CustomPersonalities"))
self.assertFalse(any(row.key == "long_repair:StandardPersonalityProfile" for row in self.owner.snapshot(
"standard", parked=True, system_long=True, lateral_context=False, metric=False).rows))
self.assertEqual(self.path("StandardPersonalityProfile").read_bytes(), b"bad")
self.assertFalse(self.params.get_bool("CustomPersonalities"))
self.assertTrue(read_profile_health(self.params).dependencies_valid)
def test_disabled_runtime_reads_only_master_and_invalid_defaults_fail(self):
original_path = self.params.get_param_path
seen = []
def path(key):
seen.append(key)
return original_path(key)
with patch.object(self.params, "get_param_path", side_effect=path):
self.assertIsNone(read_settings(self.params))
self.assertEqual(seen, ["CustomPersonalities"])
original_default = self.params.get_default_value
with patch.object(self.params, "get_default_value", side_effect=lambda key: 999.0 if key == "RelaxedFollow" else original_default(key)):
self.assertFalse(read_profile_health(self.params).dependencies_valid)
with patch.object(self.params, "get_default_value", side_effect=lambda key: "true" if key == "RelaxedPersonalityProfile" else original_default(key)):
self.assertTrue(read_profile_health(self.params).dependencies_valid)
def test_oversized_source_is_not_repairable_without_full_binding(self):
self.params.put_bool("CustomPersonalities", True, block=True)
self.path("RelaxedFollow").write_bytes(b"x" * 129)
health = read_profile_health(self.params)
self.assertFalse(health.values["RelaxedFollow"].readable)
self.assertIsNone(read_settings(self.params))
repair = self.row("relaxed", "long_repair:RelaxedFollow")
self.assertFalse(repair.available)
self.assertFalse(self.owner.apply(self.confirm(repair)))
self.assertFalse(self.row("relaxed", "long_reset:relaxed").available)
master = self.row("profiles", "CustomPersonalities")
self.assertTrue(master.available)
request = row_change(master)
self.assertIsNotNone(request)
assert request is not None
self.assertTrue(self.owner.apply(request))
self.assertFalse(self.params.get_bool("CustomPersonalities"))
def test_oversized_document_blocks_reset_but_allows_master_off(self):
self.params.put_bool("CustomPersonalities", True, block=True)
self.path("LongitudinalPersonalityProfiles").write_bytes(b"{" + b"x" * 65536)
self.assertFalse(read_profile_health(self.params).values["LongitudinalPersonalityProfiles"].readable)
reset = self.row("profiles", "reset_profiles")
self.assertFalse(reset.available)
self.assertFalse(self.owner.apply(self.confirm(reset)))
self.assertFalse(self.row("aggressive", "long_reset:aggressive").available)
master = self.row("profiles", "CustomPersonalities")
self.assertTrue(master.available)
request = row_change(master)
self.assertIsNotNone(request)
assert request is not None
self.assertTrue(self.owner.apply(request))
self.assertFalse(self.params.get_bool("CustomPersonalities"))
def test_reset_exact_seven_preserves_other_sources_and_curves(self):
self.params.put_bool("CustomPersonalities", True, block=True)
self.params.put("AggressiveFollow", 2.0, block=True)
self.params.put("StandardFollow", 2.1, block=True)
self.params.put_bool("AggressivePersonalityProfile", False, block=True)
self.params.put("LongitudinalPersonalityProfiles", {}, block=True)
before = {key: self.path(key).read_bytes() for key in
("StandardFollow", "AggressivePersonalityProfile", "LongitudinalPersonalityProfiles")}
row = self.row("aggressive", "long_reset:aggressive")
self.assertTrue(self.owner.apply(self.confirm(row)))
self.assertTrue(all(not self.path(key).exists() for key in SCALARS["aggressive"]))
self.assertEqual(before, {key: self.path(key).read_bytes() for key in before})
self.assertTrue(self.params.get_bool("CustomPersonalities"))
self.assertIsNotNone(read_settings(self.params))
def test_stale_and_changed_capability_cancel_before_write(self):
self.path("RelaxedFollow").write_bytes(b"bad")
row = self.row("relaxed", "long_repair:RelaxedFollow")
request = self.confirm(row)
self.path("StandardJerkSpeed").write_bytes(b"151")
self.assertFalse(self.owner.apply(request))
self.assertEqual(self.path("RelaxedFollow").read_bytes(), b"bad")
request = self.confirm(self.row("relaxed", "long_repair:RelaxedFollow"))
self.cp.carVin = "VIN2"
self.assertFalse(self.owner.apply(request))
self.cp.carVin = "VIN1"
request = self.confirm(self.row("relaxed", "long_repair:RelaxedFollow"))
self.allowed = False
self.assertFalse(self.owner.apply(request))
def test_partial_reset_leaves_master_off(self):
self.params.put_bool("CustomPersonalities", True, block=True)
self.params.put("AggressiveFollow", 2.0, block=True)
row = self.row("aggressive", "long_reset:aggressive")
original_remove = self.params.remove
def remove_then_lose(key):
original_remove(key)
self.allowed = False
with patch.object(self.params, "remove", side_effect=remove_then_lose):
self.assertFalse(self.owner.apply(self.confirm(row)))
self.assertFalse(self.params.get_bool("CustomPersonalities"))
def test_large_native_target_routes_confirm(self):
self.path("AggressiveFollow").write_bytes(b"broken")
state = self.owner.snapshot("aggressive", parked=True, system_long=True, lateral_context=True, metric=False)
index = next(i for i, row in enumerate(state.rows) if row.key == "long_repair:AggressiveFollow")
state = replace(state, scroll=index)
self.assertEqual(FeatureInput.target(1900, feature_row_top(state) + 77, state).kind, "reset")
def test_compact_native_confirmation_routes_captured_request(self):
self.path("AggressiveFollow").write_bytes(b"broken")
class Button:
def __init__(self, text, value):
self.text, self.value, self.click = text, value, None
def set_click_callback(self, callback):
self.click = callback
def set_enabled(self, enabled):
self.enabled = enabled
class Scroller:
def __init__(self):
self.items = []
self._scroller = self
def add_widgets(self, cards):
self.items.extend(cards)
class Dialog:
def __init__(self, question, icon, confirm_callback, **kwargs):
self.question, self.confirm = question, confirm_callback
class Session(StarShellSession):
def __init__(self, owner):
self.owner = owner
def feature_snapshot(self, page: str | None = None, *, favorite: bool = False):
return self.owner.snapshot(page or "hub", parked=True, system_long=True, lateral_context=True, metric=False)
def feature_request(self, request):
return self.owner.apply(request)
pushed = []
with patch.object(compact, "BigButton", Button), patch.object(compact, "GreyBigButton", Button), \
patch.object(compact, "NavScroller", Scroller), patch.object(compact, "BigConfirmationDialog", Dialog), \
patch.object(compact.gui_app, "push_widget", pushed.append), patch.object(compact.gui_app, "texture", lambda *args: None):
compact.FeatureSettingsCompact(Session(self.owner)).open("aggressive")
page = pushed[-1]
next(item for item in page.items if item.text == "restore low-speed follow default").click()
self.assertIn("may resume", pushed[-1].question)
pushed[-1].confirm()
self.assertAlmostEqual(self.params.get("AggressiveFollow"), 1.25)
if __name__ == "__main__":
unittest.main()