Files
StarPilot/openpilot/starpilot/conditional_mode/tests/test_preferences.py
T
firestar5683 d8708b65c7 CEM
Implement conditional driving modes with explicit scene, button and traffic state ownership.

Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>
2026-10-14 20:02:39 -05:00

175 lines
8.3 KiB
Python

"""Strict conditional-mode document and read-only frozen-Params migration tests."""
from dataclasses import replace
import json
import math
import unittest
from openpilot.starpilot.conditional_mode.policy import ModeChoice, ModeSettings
from openpilot.starpilot.conditional_mode.preferences import (
CCMOptions,
CEMOptions,
MAX_DOCUMENT_BYTES,
PreferenceError,
SavedPreferences,
decode_preferences,
encode_preferences,
manual_for_drive,
propose_legacy_adoption,
saved_selection_for_drive,
selection_for_drive,
)
class ConditionalModePreferencesTest(unittest.TestCase):
def test_factory_default_is_cem_and_explicit_stock_round_trip_is_canonical(self):
default = SavedPreferences()
self.assertIs(default.mode, ModeChoice.CEM)
self.assertEqual(default.cem, CEMOptions())
self.assertEqual(default.ccm, CCMOptions())
raw = encode_preferences(default)
self.assertEqual(decode_preferences(raw), default)
self.assertEqual(encode_preferences(decode_preferences(raw)), raw)
self.assertTrue(raw.startswith(b'{"ccm":'))
self.assertNotIn(b'SafeMode', raw)
self.assertLess(len(raw), MAX_DOCUMENT_BYTES)
stock = SavedPreferences(mode=ModeChoice.STOCK)
self.assertIs(decode_preferences(encode_preferences(stock)).mode, ModeChoice.STOCK)
def test_all_options_survive_and_policy_projection_is_explicit(self):
preferences = SavedPreferences(
mode=ModeChoice.CEM,
cem=CEMOptions(speed_mps=10.5, speed_with_lead_mps=7.0, signal_speed_mps=5.0,
open_road=True, curves=True, curves_with_lead=True, lead=True,
slower_lead=False, stopped_lead=True, stop_lights=False,
model_stop_s=6.5, signal_lane_detection=False,
signal_lane_width_m=3.7, persist_manual=True),
ccm=CCMOptions(speed_mps=20.0, speed_with_lead_mps=15.0,
set_speed_margin_mps=2.0, lead=False, launch_assist=True,
persist_manual=True),
)
self.assertEqual(decode_preferences(encode_preferences(preferences)), preferences)
settings = preferences.mode_settings()
self.assertIsInstance(settings, ModeSettings)
self.assertEqual((settings.cem_speed_mps, settings.cem_speed_with_lead_mps,
settings.cem_signal_mps), (10.5, 7.0, 5.0))
self.assertFalse(settings.cem_stop) # CEStopLights gates CEModelStopTime.
self.assertEqual((settings.ccm_speed_mps, settings.ccm_speed_with_lead_mps,
settings.ccm_set_speed_margin_mps), (20.0, 15.0, 2.0))
self.assertFalse(settings.ccm_lead)
self.assertTrue(settings.ccm_launch)
self.assertFalse(replace(preferences, cem=replace(preferences.cem, model_stop_s=0.0)).mode_settings().cem_stop)
def test_drive_binding_preserves_existing_typed_api_and_does_not_grant_authority(self):
preferences = SavedPreferences(mode=ModeChoice.CCM)
selection = saved_selection_for_drive(preferences, 42)
self.assertEqual(selection, selection_for_drive(ModeChoice.CCM, preferences.mode_settings(), 42))
self.assertIsNone(saved_selection_for_drive(preferences, 0))
self.assertIsNone(saved_selection_for_drive(preferences, True))
self.assertIsNone(manual_for_drive('force_chill', 42, 100))
self.assertFalse(hasattr(selection, 'safe_mode'))
self.assertFalse(hasattr(selection, 'system_long_capable'))
def test_malformed_document_never_falls_back_or_silently_rewrites(self):
good = json.loads(encode_preferences(SavedPreferences()))
invalid = []
for key in ('version', 'mode', 'cem', 'ccm'):
document = dict(good)
document.pop(key)
invalid.append(document)
for key, value in (('extra', 1), ('version', 2), ('version', True),
('mode', 'conditional_chill_unknown'), ('mode', 1)):
invalid.append(dict(good, **{key: value}))
for section, name, value in (
('cem', 'speed_mps', True), ('cem', 'speed_mps', -1),
('cem', 'speed_mps', 50.0), ('cem', 'model_stop_s', 9.1),
('cem', 'signal_lane_width_m', 15.1), ('cem', 'open_road', 1),
('ccm', 'set_speed_margin_mps', 9.0), ('ccm', 'lead', 'false'),
):
document = json.loads(json.dumps(good))
document[section][name] = value
invalid.append(document)
missing = json.loads(json.dumps(good))
del missing['cem']['slower_lead']
invalid.append(missing)
extra = json.loads(json.dumps(good))
extra['ccm']['unknown'] = False
invalid.append(extra)
for document in invalid:
with self.subTest(document=document):
with self.assertRaises(PreferenceError):
decode_preferences(json.dumps(document))
for raw in (b'\xff', '{"version":1,"version":1}',
'{"value":NaN}', b'x' * (MAX_DOCUMENT_BYTES + 1),
'not-json', ''):
with self.subTest(raw=raw):
with self.assertRaises(PreferenceError):
decode_preferences(raw)
with self.assertRaises(PreferenceError):
encode_preferences(replace(SavedPreferences(), cem=replace(CEMOptions(), speed_mps=math.nan)))
with self.assertRaises(PreferenceError):
encode_preferences(replace(SavedPreferences(), ccm=replace(CCMOptions(), speed_mps=10**350)))
def test_legacy_adoption_requires_explicit_mode_bits_and_unit_system(self):
# Frozen params_keys.h stores numeric values in display units; old
# starpilot_variables.py multiplied by MPH_TO_MS/KPH_TO_MS at runtime.
imperial = propose_legacy_adoption({
'ConditionalExperimental': b'1', 'ConditionalChill': b'0',
'CESpeed': b'25.0', 'CESpeedLead': b'15.0', 'CESignalSpeed': b'12.0',
'CCMSpeed': b'45.0', 'CCMSetSpeedMargin': b'3.0',
'CEModelStopTime': b'7.7', 'CEStopLights': b'1',
'CESlowerLead': b'0', 'CEStoppedLead': b'1',
'CESignalLaneDetection': b'1', 'LaneDetectionWidth': b'12.0',
'PersistExperimentalState': b'1', 'PersistChillState': b'0',
}, units='imperial')
self.assertIs(imperial.preferences.mode, ModeChoice.CEM)
self.assertAlmostEqual(imperial.preferences.cem.speed_mps, 25 * 0.44704)
self.assertAlmostEqual(imperial.preferences.cem.signal_lane_width_m, 12 * 0.3048)
self.assertEqual(imperial.preferences.cem.model_stop_s, 7.7)
self.assertFalse(imperial.preferences.cem.slower_lead)
self.assertTrue(imperial.preferences.cem.stopped_lead)
self.assertTrue(imperial.preferences.cem.persist_manual)
self.assertFalse(imperial.preferences.ccm.persist_manual)
self.assertIn('CESpeed', imperial.source_keys)
self.assertEqual(decode_preferences(encode_preferences(imperial.preferences)), imperial.preferences)
metric = propose_legacy_adoption({
'ConditionalExperimental': False, 'ConditionalChill': True,
'CCMSpeed': '72.0', 'CCMSpeedLead': '54.0',
'CCMSetSpeedMargin': '10.0', 'LaneDetectionWidth': '3.5',
}, units='metric')
self.assertIs(metric.preferences.mode, ModeChoice.CCM)
self.assertEqual(metric.preferences.ccm.speed_mps, 20.0)
self.assertEqual(metric.preferences.ccm.speed_with_lead_mps, 15.0)
self.assertAlmostEqual(metric.preferences.ccm.set_speed_margin_mps, 10 / 3.6)
self.assertEqual(metric.preferences.cem.signal_lane_width_m, 3.5)
stock = propose_legacy_adoption({'ConditionalExperimental': '0',
'ConditionalChill': '0'}, units='imperial')
self.assertIs(stock.preferences.mode, ModeChoice.STOCK)
def test_legacy_adoption_rejects_ambiguity_corruption_and_safe_mode_coupling(self):
base = {'ConditionalExperimental': '0', 'ConditionalChill': '1'}
bad = (
({'ConditionalExperimental': '1', 'ConditionalChill': '1'}, 'imperial'),
({'ConditionalExperimental': '1'}, 'imperial'),
({**base, 'SafeMode': '0'}, 'imperial'),
({**base, 'CCMSpeed': '1e2'}, 'imperial'),
({**base, 'CCMSpeed': 'nan'}, 'imperial'),
({**base, 'CCMSpeed': '100'}, 'imperial'),
({**base, 'CCMSetSpeedMargin': '16'}, 'imperial'),
({**base, 'CCMSetSpeedMargin': '31'}, 'metric'),
({**base, 'CEModelStopTime': '9.1'}, 'metric'),
({**base, 'CEStopLights': 'true'}, 'metric'),
({**base, 'LaneDetectionWidth': '15.1'}, 'metric'),
(base, 'unknown'),
)
for values, units in bad:
with self.subTest(values=values, units=units):
with self.assertRaises(PreferenceError):
propose_legacy_adoption(values, units=units)
if __name__ == '__main__':
unittest.main()