Files
StarPilot/openpilot/starpilot/ui/tests/test_controller_feature.py
T
firestar5683 2c8e8b5879 CI Contracts
2026-10-04 17:23:26 -05:00

216 lines
11 KiB
Python

"""One shared saved steering-controller choice with exact source and CP guards."""
from unittest.mock import Mock
from openpilot.starpilot.galaxy.settings import ContextSource
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
from opendbc.car.car_helpers import interfaces
from opendbc.car.hyundai.values import CAR as HYUNDAI
from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.common.params import Params
from openpilot.starpilot.lateral.controller_selection import (
DOCUMENT_KEY, ControllerMode, parse_document, read_selection,
)
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsRequest, row_change
from openpilot.starpilot.galaxy.settings import (
AuthorityContext, SettingsChanged, SettingsGateway, _onroad_preference, _projection,
)
def required_change(row: FeatureRow) -> FeatureSettingsRequest:
request = row_change(row)
assert request is not None
return request
class TestControllerFeature(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.params = Params(self.temp.name)
self.cp = interfaces[HYUNDAI.HYUNDAI_IONIQ_6].get_non_essential_params(HYUNDAI.HYUNDAI_IONIQ_6)
self.permitted = True
self.owner = FeatureSettingsOwner(self.params, lambda group: self.permitted and group == 'preferences',
vehicle_fingerprint=lambda: str(self.cp.carFingerprint),
vehicle_params=lambda: self.cp)
def row(self, key=DOCUMENT_KEY):
state = self.owner.snapshot('torque', parked=False, system_long=False, lateral_context=False, metric=False)
return next((row for row in state.rows if row.key == key), None)
def require_row(self, key=DOCUMENT_KEY):
row = self.row(key)
assert row is not None
return row
def test_exact_choice_visible_without_manual_tuning_or_park_lock(self):
for vehicle in (HYUNDAI.HYUNDAI_IONIQ_6, HYUNDAI.GENESIS_G70_2020, TOYOTA.TOYOTA_COROLLA_TSS2):
with self.subTest(vehicle=vehicle):
self.cp = interfaces[vehicle].get_non_essential_params(vehicle)
row = self.require_row()
self.assertIsNotNone(row)
self.assertTrue(row.available)
self.assertEqual((row.label, row.value), ('Steering Controller', 'StarPilot Controller'))
self.assertEqual(row.choices, ('Stock Controller', 'StarPilot Controller'))
self.assertIn('next drive', row.reason)
self.assertNotIn('active', row.reason.lower())
self.assertEqual(_projection(row, 'torque')['choices'], list(row.choices))
self.assertTrue(_onroad_preference('torque', DOCUMENT_KEY))
self.assertFalse(self.params.get_bool('AdvancedLateralTune'))
self.cp = interfaces[HYUNDAI.KIA_EV6].get_non_essential_params(HYUNDAI.KIA_EV6)
self.assertEqual(self.require_row().value, "Stock Controller")
self.cp = interfaces[HYUNDAI.HYUNDAI_IONIQ_6].get_non_essential_params(HYUNDAI.HYUNDAI_IONIQ_6)
self.cp.notCar = True
self.assertIsNone(self.row())
def test_write_preserves_other_vehicle_and_manual_preferences(self):
self.params.put_bool('AdvancedLateralTune', True, block=True)
self.params.put_bool('ForceAutoTuneOff', True, block=True)
first = self.require_row()
request = required_change(first)
self.assertEqual(request.value, 'Stock Controller')
self.assertTrue(self.owner.apply(request))
raw = Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes()
self.assertEqual(parse_document(raw)['vehicles']['HYUNDAI_IONIQ_6']['mode'], 'standard')
self.assertEqual(read_selection(self.params, self.cp).mode, ControllerMode.STANDARD)
self.cp = interfaces[TOYOTA.TOYOTA_COROLLA_TSS2].get_non_essential_params(TOYOTA.TOYOTA_COROLLA_TSS2)
other = self.require_row()
self.assertEqual(other.value, 'StarPilot Controller')
self.assertTrue(self.owner.apply(required_change(other)))
after = parse_document(Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes())['vehicles']
self.assertEqual(after['HYUNDAI_IONIQ_6']['mode'], 'standard')
self.assertEqual(after['TOYOTA_COROLLA_TSS2']['mode'], 'standard')
self.assertTrue(self.params.get_bool('AdvancedLateralTune'))
self.assertTrue(self.params.get_bool('ForceAutoTuneOff'))
def test_stale_document_cp_and_authority_rejected(self):
row = self.require_row()
request = required_change(row)
self.cp.lateralTuning.torque.latAccelFactor *= 1.05
self.assertFalse(self.owner.apply(request))
self.cp = interfaces[HYUNDAI.HYUNDAI_IONIQ_6].get_non_essential_params(HYUNDAI.HYUNDAI_IONIQ_6)
row = self.require_row()
request = required_change(row)
self.permitted = False
self.assertFalse(self.owner.apply(request))
self.permitted = True
row = self.require_row()
request = required_change(row)
self.cp = interfaces[HYUNDAI.GENESIS_G70_2020].get_non_essential_params(HYUNDAI.GENESIS_G70_2020)
self.assertFalse(self.owner.apply(request))
self.cp = interfaces[HYUNDAI.HYUNDAI_IONIQ_6].get_non_essential_params(HYUNDAI.HYUNDAI_IONIQ_6)
request = required_change(self.require_row())
Path(self.params.get_param_path(DOCUMENT_KEY)).write_bytes(b'{"version":1,"vehicles":{}}')
self.assertFalse(self.owner.apply(request))
def test_starpilot_disables_learning_and_standard_requires_explicit_enable(self):
self.params.put('SteerLatAccel', 3.3, block=True)
self.params.put('SteerFriction', 0.12, block=True)
self.assertFalse(self.require_row('ForceAutoTuneOff').available)
self.assertEqual(self.require_row('ForceAutoTuneOff').value, 'Off')
self.assertIn('StarPilot Discord', self.require_row('ForceAutoTuneOff').reason)
self.assertTrue(self.owner.apply(required_change(self.require_row()))) # Standard
self.assertEqual(self.require_row('ForceAutoTuneOff').value, 'Off')
self.assertTrue(self.owner.apply(required_change(self.require_row('ForceAutoTuneOff'))))
self.assertEqual(self.require_row('ForceAutoTuneOff').value, 'On')
self.assertTrue(self.owner.apply(required_change(self.require_row()))) # StarPilot
self.assertTrue(self.params.get_bool('ForceAutoTuneOff'))
self.assertFalse(self.params.get_bool('AdvancedLateralTune'))
self.assertEqual(self.params.get('SteerLatAccel'), 3.3)
self.assertEqual(self.params.get('SteerFriction'), 0.12)
self.assertFalse(self.require_row('ForceAutoTuneOff').available)
self.assertTrue(self.owner.apply(required_change(self.require_row()))) # Standard does not opt in to learning
learning = self.require_row('ForceAutoTuneOff')
self.assertTrue(learning.available)
self.assertEqual(learning.value, 'Off')
self.assertTrue(self.owner.apply(required_change(learning)))
self.assertFalse(self.params.get_bool('ForceAutoTuneOff'))
def test_learning_source_and_controller_changes_revoke_requests(self):
self.assertTrue(self.owner.apply(required_change(self.require_row())))
self.assertTrue(self.owner.apply(required_change(self.require_row('ForceAutoTuneOff'))))
request = required_change(self.require_row())
self.params.put_bool('ForceAutoTuneOff', True, block=True)
self.assertFalse(self.owner.apply(request))
learning = required_change(self.require_row('ForceAutoTuneOff'))
self.assertTrue(self.owner.apply(required_change(self.require_row())))
self.assertFalse(self.owner.apply(learning))
Path(self.params.get_param_path('ForceAutoTuneOff')).write_bytes(b'invalid')
self.assertFalse(self.require_row().available)
self.assertFalse(self.require_row('ForceAutoTuneOff').available)
def test_interrupted_starpilot_save_leaves_learning_off_without_changing_controller(self):
self.assertTrue(self.owner.apply(required_change(self.require_row())))
self.assertTrue(self.owner.apply(required_change(self.require_row('ForceAutoTuneOff'))))
request = required_change(self.require_row())
from openpilot.starpilot.ui import controller_feature
commit = controller_feature.commit_exact
def revoke_after_learning(**kwargs):
result = commit(self.params, **kwargs)
if kwargs['key'] == 'ForceAutoTuneOff':
self.permitted = False
return result
with mock.patch.object(controller_feature, 'commit_exact', side_effect=lambda _params, **kwargs: revoke_after_learning(**kwargs)):
self.assertFalse(self.owner.apply(request))
self.assertTrue(self.params.get_bool('ForceAutoTuneOff'))
self.assertEqual(read_selection(self.params, self.cp).mode, ControllerMode.STANDARD)
def test_corrupt_or_unreadable_document_fails_closed_without_overwrite(self):
path = Path(self.params.get_param_path(DOCUMENT_KEY))
for raw in (b'not-json', b'{"version":1,"vehicles":{},"vehicles":{}}',
b'{"version":2,"vehicles":{}}', b'x' * 4097):
with self.subTest(raw=raw[:30]):
path.write_bytes(raw)
row = self.require_row()
self.assertFalse(row.available)
self.assertEqual(row.value, 'Invalid saved controller choice')
self.assertFalse(self.owner.apply(FeatureSettingsRequest(DOCUMENT_KEY, raw, 'Stock Controller',
vehicle_fingerprint=str(self.cp.carFingerprint),
capability=row.capability)))
self.assertEqual(path.read_bytes(), raw)
def test_galaxy_onroad_saved_choice_preview_confirm_and_revocation(self):
current = SimpleNamespace(parked=False, cp=self.cp, raw=b'current-cp')
context = Mock(spec=ContextSource, sample=lambda: AuthorityContext(current.parked, current.cp, current.raw))
gateway = SettingsGateway(self.params, context, clock=lambda: 10)
def preview():
page = gateway.page('torque', 'session', b'generation')
index = next(i for i, row in enumerate(page['rows']) if row['label'] == 'Steering Controller')
return gateway.preview(page['view'], index, 0, 'session', b'generation', value='Stock Controller')
intent = preview()
self.assertIn('next drive', intent['question'])
self.assertTrue(gateway.confirm(intent['intent'], 'session', b'generation'))
self.assertEqual(read_selection(self.params, self.cp).mode, ControllerMode.STANDARD)
self.assertFalse(current.parked) # Saved preference did not require a Park state.
stale = preview()
path = Path(self.params.get_param_path(DOCUMENT_KEY))
path.write_bytes(b'{"version":1,"vehicles":{}}')
self.assertFalse(gateway.confirm(stale['intent'], 'session', b'generation'))
fresh = preview()
current.raw = b'changed-cp'
with self.assertRaises(SettingsChanged):
gateway.confirm(fresh['intent'], 'session', b'generation')
current.raw = b'current-cp'
expired = preview()
with self.assertRaises(SettingsChanged):
gateway.confirm(expired['intent'], 'session', b'other-generation')
current.cp.lateralTuning.torque.latAccelFactor *= 1.05
self.assertFalse(gateway.confirm(expired['intent'], 'session', b'generation'))
if __name__ == '__main__':
unittest.main()