"""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()