mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
a82935f326
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>
256 lines
13 KiB
Python
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()
|