Files
StarPilot/openpilot/starpilot/tests/test_settings_retirement.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

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: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
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: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

172 lines
9.2 KiB
Python

import unittest
import tempfile
import json
from pathlib import Path
from openpilot.starpilot.lateral.tests.test_inferred_torque_overrides import SavedParams
from openpilot.starpilot.settings_retirement import retire_settings, PROFILE_FLAGS
from openpilot.starpilot.longitudinal.profile_document import default_personality_profiles, profile_document, migrate_profile_document
from openpilot.starpilot.ui.appearance_preferences import RETIRED_DISPLAY_KEYS
from openpilot.starpilot.ui.onroad_customization import PARAM_KEY as LAYOUT_KEY, default_document, read_customization
class TestSettingsRetirement(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.params = SavedParams(self.directory.name)
def test_inactive_cruise_features_do_not_become_enabled(self):
self.params.put_bool('QOLLongitudinal', False)
self.params.put_bool('ForceStops', True)
self.params.put_bool('ReverseCruise', True)
self.params.put('CustomCruise', 7.0)
retire_settings(self.params)
self.assertEqual(self.params.get('ForceStops'), b'0')
self.assertEqual(self.params.get('ReverseCruise'), b'0')
self.assertEqual(self.params.get('CustomCruise'), b'1.0')
self.assertIsNone(self.params.get('QOLLongitudinal'))
self.params.put_bool('ForceStops', True)
retire_settings(self.params)
self.assertEqual(self.params.get('ForceStops'), b'1')
def test_active_cruise_values_and_steering_values_survive(self):
self.params.put_bool('QOLLongitudinal', True)
self.params.put_bool('ForceStops', True)
self.params.put('CustomCruise', 7.0)
self.params.put('SteerLatAccel', 3.2)
self.params.put_bool('AdvancedLateralTune', False)
retire_settings(self.params)
self.assertEqual(self.params.get('ForceStops'), b'1')
self.assertEqual(self.params.get('CustomCruise'), b'7.0')
self.assertEqual(self.params.get('SteerLatAccel'), b'3.2')
self.assertIsNone(self.params.get('AdvancedLateralTune'))
def test_inactive_legacy_curve_does_not_activate(self):
profiles = default_personality_profiles(False)
profiles['standard']['following'] = {'preset': 'custom', 'curve': [2.0]*10}
profiles['standard']['acceleration'] = {'preset': 'sport', 'curve': [], 'legacyActivation': True}
profiles['relaxed']['following'] = {'preset': 'custom', 'curve': [2.5]*10}
document = profile_document(profiles, enabled=True)
self.params.put('LongitudinalPersonalityProfiles', document)
self.params.put_bool('StandardPersonalityProfile', False)
self.params.put_bool('RelaxedPersonalityProfile', True)
retire_settings(self.params)
result = migrate_profile_document(self.params.get('LongitudinalPersonalityProfiles'))
assert result is not None
assert result is not None
self.assertEqual(result['profiles']['standard']['following']['preset'], 'dom_default')
self.assertEqual(result['profiles']['standard']['acceleration']['preset'], 'selected_profile')
self.assertEqual(result['profiles']['relaxed']['following']['curve'], [2.5]*10)
self.assertTrue(all(self.params.get(key) is None for key in PROFILE_FLAGS))
before = self.params.get('LongitudinalPersonalityProfiles')
retire_settings(self.params)
self.assertEqual(self.params.get('LongitudinalPersonalityProfiles'), before)
def test_invalid_documents_retained_and_unconfigured_allowed(self):
self.params.put_bool('StandardPersonalityProfile', False)
self.params.put('LongitudinalPersonalityProfiles', 'bad')
self.assertTrue(retire_settings(self.params))
self.assertEqual(self.params.get('LongitudinalPersonalityProfiles'), b'bad')
self.assertIsNone(self.params.get('StandardPersonalityProfile'))
self.params.put('LongitudinalPersonalityProfiles', {})
retire_settings(self.params)
self.assertIsNone(self.params.get('StandardPersonalityProfile'))
def test_unsafe_switch_and_invalid_evidence_are_bounded_and_preserved(self):
self.params.put('RetiredSettingsEvidence', 'not-json')
self.params.put('QOLLongitudinal', 'x' * 100000)
self.params.put_bool('ForceStops', True)
issues = retire_settings(self.params)
self.assertTrue(any('existing evidence retained' in issue for issue in issues))
self.assertEqual(self.params.get('RetiredSettingsEvidence'), b'not-json')
self.assertEqual(self.params.get('ForceStops'), b'0')
self.assertIsNone(self.params.get('QOLLongitudinal'))
def test_absent_parent_is_off_only_on_first_reconciliation(self):
self.params.put_bool('ForceStops', True)
self.params.put_bool('ReverseCruise', True)
profiles = default_personality_profiles(False)
profiles['standard']['following'] = {'preset': 'custom', 'curve': [2.0]*10}
self.params.put('LongitudinalPersonalityProfiles', profile_document(profiles, enabled=True))
retire_settings(self.params)
self.assertEqual(self.params.get('ForceStops'), b'0')
self.assertEqual(self.params.get('ReverseCruise'), b'0')
result = migrate_profile_document(self.params.get('LongitudinalPersonalityProfiles'))
assert result is not None
self.assertEqual(result['profiles']['standard']['following']['preset'], 'dom_default')
self.params.put_bool('ForceStops', True)
self.params.put_bool('ReverseCruise', True)
self.params.put('LongitudinalPersonalityProfiles', profile_document(profiles, enabled=True))
retire_settings(self.params)
self.assertEqual(self.params.get('ForceStops'), b'1')
self.assertEqual(self.params.get('ReverseCruise'), b'1')
result = migrate_profile_document(self.params.get('LongitudinalPersonalityProfiles'))
assert result is not None
self.assertEqual(result['profiles']['standard']['following']['curve'], [2.0]*10)
def test_fresh_namespace_completes_and_preserves_later_choice(self):
retire_settings(self.params)
state = json.loads(self.params.get('SettingsRetirementState'))
self.assertEqual(set(state['completed']), {'cruise', 'profiles', 'display'})
self.params.put_bool('ForceStops', True)
retire_settings(self.params)
self.assertEqual(self.params.get('ForceStops'), b'1')
def write(self, key, raw):
Path(self.params.get_param_path(key)).write_bytes(raw)
def test_display_switches_fold_into_layout_once(self):
for key, raw in (("HideSpeed", b"1"), ("HideMaxSpeed", b"1"), ("HideSteeringWheel", b"1"), ("HideDMIcon", b"1"),
("EnableTorqueBarWidget", b"0"), ("StoppedTimer", b"1"), ("RainbowPath", b"1")):
self.write(key, raw)
custom = default_document()
custom["roadColors"]["compact"]["pathMode"] = "color"
self.params.put(LAYOUT_KEY, custom)
retire_settings(self.params)
document = read_customization(self.params)
large, compact = document["layouts"]["large"], document["layouts"]["compact"]
self.assertEqual([large[key]["enabled"] for key in ("current_speed", "steering_wheel", "driver_monitor", "torque_bar")],
[False, False, False, False])
# The small UI never honored the speed and wheel switches.
self.assertEqual([compact[key]["enabled"] for key in ("steering_wheel", "driver_monitor", "torque_bar", "stopped_timer")],
[True, False, False, True])
self.assertIs(document["showMaxSpeed"], False)
self.assertEqual((document["roadColors"]["large"]["pathMode"], document["roadColors"]["compact"]["pathMode"]), ("rainbow", "color"))
for key in RETIRED_DISPLAY_KEYS:
self.assertIsNone(self.params.get(key))
# Completed: a later layout edit survives even if a default switch file reappears.
document["layouts"]["large"]["current_speed"]["enabled"] = True
self.params.put(LAYOUT_KEY, document)
self.write("HideSpeed", b"1")
retire_settings(self.params)
self.assertTrue(read_customization(self.params)["layouts"]["large"]["current_speed"]["enabled"])
def test_default_and_corrupt_switches_leave_layout_bytes_alone(self):
self.write("HideSpeed", b"0")
self.write("EnableTorqueBarWidget", b"bad")
retire_settings(self.params)
self.assertIsNone(self.params.get(LAYOUT_KEY))
self.assertIsNone(self.params.get("EnableTorqueBarWidget"))
def test_invalid_layout_is_kept_and_unreadable_layout_retries(self):
self.write(LAYOUT_KEY, b'{"version": 99}')
self.write("HideSpeed", b"1")
issues = retire_settings(self.params)
self.assertEqual(self.params.get(LAYOUT_KEY), b'{"version": 99}')
self.assertEqual(self.params.get("HideSpeed"), b"1")
self.assertNotIn("display", json.loads(self.params.get("SettingsRetirementState"))["completed"])
self.assertIn("Invalid onroad layout kept; display switches were not carried over", issues)
self.params.put(LAYOUT_KEY, default_document())
retire_settings(self.params)
self.assertFalse(read_customization(self.params)["layouts"]["large"]["current_speed"]["enabled"])
self.assertIsNone(self.params.get("HideSpeed"))
other = tempfile.TemporaryDirectory()
self.addCleanup(other.cleanup)
self.params = SavedParams(other.name)
Path(self.params.get_param_path(LAYOUT_KEY)).mkdir()
self.write("HideSpeed", b"1")
retire_settings(self.params)
self.assertNotIn("display", json.loads(self.params.get("SettingsRetirementState"))["completed"])
self.assertEqual(self.params.get("HideSpeed"), b"1")
if __name__ == '__main__':
unittest.main()