Files
StarPilot/openpilot/starpilot/aol/tests/test_vehicle_policy_cache.py
T
firestar5683 8571e16f68 Runtime AOL
Register vehicle AOL adapters and runtime dispatch after the vehicle factories and native contracts exist.

Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

233 lines
9.6 KiB
Python

"""Exact CarParams snapshots may cache policy; live authority remains current."""
from dataclasses import FrozenInstanceError
from types import SimpleNamespace
from unittest.mock import Mock, patch
import warnings
import pytest
from capnp import KjException
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.car_helpers import interfaces
from opendbc.car.ford.values import CAR as FORD
from opendbc.car.gm.values import CAR as GM
from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.hyundai.values import CAR as HYUNDAI
from opendbc.car.mazda.values import CAR as MAZDA
from opendbc.car.tesla.fingerprints import FINGERPRINTS as TESLA_FINGERPRINTS
from opendbc.car.tesla.values import CAR as TESLA
from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.starpilot.aol import vehicle
@pytest.fixture(autouse=True)
def clear_policy_cache():
vehicle._policy_for_bytes.cache_clear()
yield
vehicle._policy_for_bytes.cache_clear()
def profile(brand):
"""Factory inputs from each port's existing positive admission fixtures."""
models = {'honda': HONDA.HONDA_CIVIC_BOSCH, 'hyundai': HYUNDAI.HYUNDAI_SONATA,
'gm': GM.CHEVROLET_BOLT_CC_2018_2021, 'ford': FORD.FORD_ESCAPE_MK4,
'mazda': MAZDA.MAZDA_CX5_2022, 'tesla': TESLA.TESLA_MODEL_S_PREAP,
'toyota': TOYOTA.TOYOTA_COROLLA_TSS2}
fp = gen_empty_fingerprint()
if brand == 'gm':
fp[0][0x201] = 6
fp[2].update({0x320: 8, 0x180: 4})
elif brand == 'ford':
fp[0][0x5A] = 8
fp[2].update({0x3D6: 8, 0x186: 8})
elif brand == 'tesla':
fp[0] = dict(TESLA_FINGERPRINTS[TESLA.TESLA_MODEL_S_PREAP][0])
return interfaces[models[brand]].get_params(models[brand], fp, [], brand == 'honda', False, False)
def assert_equivalent(cp):
expected = vehicle._policy_for_uncached(cp)
assert vehicle.policy_for(cp) == expected
assert vehicle.policy_for(cp.as_reader()) == expected
return expected
@pytest.mark.parametrize('brand', ('honda', 'hyundai', 'gm', 'ford', 'mazda', 'tesla', 'toyota'))
def test_actual_port_profile_and_startup_marking_match(brand):
cp = profile(brand)
initial = assert_equivalent(cp)
assert initial.intent_supported, brand
cp.safetyConfigs[-1].safetyParam |= initial.safety_param_addition
cp.alternativeExperience |= initial.alternative_experience_addition
marked = assert_equivalent(cp)
assert marked.intent_supported and marked.runtime_supported
assert vehicle.native_matches_cp(cp, int(cp.safetyConfigs[-1].safetyModel.raw), cp.safetyConfigs[-1].safetyParam)
@pytest.mark.parametrize('control', ('torque', 'pid', 'angle'))
def test_controller_variants_match(control):
cp = profile('toyota')
cp.steerControlType = 'angle' if control == 'angle' else 'torque'
cp.lateralTuning.init('pid' if control == 'pid' else 'torque')
assert_equivalent(cp)
@pytest.mark.parametrize('field', ('brand', 'carFingerprint', 'flags', 'passive', 'notCar', 'dashcamOnly',
'pcmCruise', 'openpilotLongitudinalControl', 'alternativeExperience',
'safetyParam', 'safetyModel', 'safetyConfigs', 'lateralTuning', 'carVin'))
def test_mutable_builder_and_alias_reader_never_reuse_a_prior_snapshot(field):
cp = profile('toyota')
alias = cp.as_reader()
accepted = vehicle.policy_for(cp)
assert accepted.intent_supported
before_key = vehicle._car_params_bytes(cp)
before = vehicle._policy_for_bytes.cache_info()
if field == 'safetyParam':
cp.safetyConfigs[0].safetyParam ^= 1
elif field == 'safetyModel':
cp.safetyConfigs[0].safetyModel = 'noOutput'
elif field == 'safetyConfigs':
cp.safetyConfigs = []
elif field == 'lateralTuning':
cp.lateralTuning.init('pid')
elif field in ('passive', 'notCar', 'dashcamOnly', 'pcmCruise', 'openpilotLongitudinalControl'):
setattr(cp, field, not getattr(cp, field))
else:
setattr(cp, field, {'brand': 'unregistered', 'carFingerprint': 'unregistered', 'flags': int(cp.flags) ^ 1,
'alternativeExperience': 1, 'carVin': 'CACHE-MUTATION'}[field])
assert vehicle._car_params_bytes(cp) != before_key
expected = vehicle._policy_for_uncached(cp)
assert vehicle.policy_for(cp) == vehicle.policy_for(alias) == expected
after = vehicle._policy_for_bytes.cache_info()
assert after.misses == before.misses + 1
assert after.hits == before.hits + 1
if field in ('brand', 'carFingerprint', 'passive', 'notCar', 'dashcamOnly', 'safetyModel', 'safetyConfigs'):
assert not expected.intent_supported
def test_duck_types_and_schema_spoofs_bypass_serialization_untouched(monkeypatch):
sentinel = vehicle.AolVehiclePolicy(settings_supported=True)
owner = SimpleNamespace(policy_for=Mock(return_value=sentinel))
monkeypatch.setitem(vehicle._PORTS, 'cache-test', owner)
for schema in (None, structs.CarParams.schema):
cp = SimpleNamespace(brand='cache-test', schema=schema, to_bytes=Mock(side_effect=AssertionError),
copy=Mock(side_effect=AssertionError), as_builder=Mock(side_effect=AssertionError))
assert vehicle._car_params_bytes(cp) is None
assert vehicle.policy_for(cp) is sentinel
owner.policy_for.assert_called_with(cp)
cp.to_bytes.assert_not_called()
cp.copy.assert_not_called()
cp.as_builder.assert_not_called()
class HostileMeta(type):
def __eq__(cls, other):
raise AssertionError('Unknown type equality must not run')
class Unknown(metaclass=HostileMeta):
brand = 'cache-test'
@property
def schema(self):
raise AssertionError('Unknown schema must not be read')
assert vehicle._car_params_bytes(Unknown()) is None
assert vehicle.policy_for(Unknown()) is sentinel
assert vehicle._policy_for_bytes.cache_info().misses == 0
def outcome(call, cp):
try:
return ('result', call(cp))
except Exception as exc:
return ('exception', type(exc), str(exc))
def test_malformed_wrong_schema_and_original_exceptions_are_preserved(monkeypatch):
wrong = structs.CarState(canValid=True)
for cp in (None, b'not a CarParams message', object(), SimpleNamespace(brand=[]), wrong, wrong.as_reader()):
assert vehicle._car_params_bytes(cp) is None
assert outcome(vehicle.policy_for, cp) == outcome(vehicle._policy_for_uncached, cp)
owner = SimpleNamespace(policy_for=Mock(side_effect=RuntimeError('original dispatcher exception')))
monkeypatch.setitem(vehicle._PORTS, 'cache-test', owner)
cp = SimpleNamespace(brand='cache-test')
assert outcome(vehicle.policy_for, cp) == outcome(vehicle._policy_for_uncached, cp)
with pytest.raises((ValueError, KjException)):
vehicle._policy_for_bytes(b'not a CarParams message')
def test_serialization_and_decode_failure_fall_back_to_original_builder(monkeypatch):
cp = profile('toyota')
expected = vehicle._policy_for_uncached(cp)
class BrokenSchema:
@property
def schema(self):
raise RuntimeError('schema unavailable')
with monkeypatch.context() as broken:
broken.setattr(vehicle, 'CarParams', BrokenSchema())
assert vehicle._car_params_bytes(cp) is None
assert vehicle.policy_for(cp) == expected
decoder = SimpleNamespace(schema=structs.CarParams.schema, from_bytes=Mock(side_effect=RuntimeError('decode unavailable')))
monkeypatch.setattr(vehicle, 'CarParams', decoder)
with patch.object(vehicle, '_policy_for_uncached', wraps=vehicle._policy_for_uncached) as uncached:
assert vehicle.policy_for(cp) == expected
uncached.assert_called_once_with(cp)
decoder.from_bytes.assert_called_once()
def test_original_builder_write_state_and_repeat_warning_are_unchanged():
cp = profile('toyota')
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always')
for _ in range(4):
assert_equivalent(cp)
assert not caught
cp.to_bytes()
assert not caught
cp.to_bytes()
assert any('already been written' in str(item.message) for item in caught)
def test_lru_bound_oversize_bypass_and_frozen_result():
cp = profile('toyota')
for index in range(9):
cp.carVin = str(index)
assert_equivalent(cp)
info = vehicle._policy_for_bytes.cache_info()
assert info.maxsize == info.currsize == 8
cp.carVin = '0'
vehicle.policy_for(cp)
assert vehicle._policy_for_bytes.cache_info().misses == info.misses + 1
cp.carVin = 'v' * (64 * 1024)
before = vehicle._policy_for_bytes.cache_info()
assert vehicle._car_params_bytes(cp) is None
result = vehicle.policy_for(cp)
assert result == vehicle._policy_for_uncached(cp)
assert vehicle._policy_for_bytes.cache_info() == before
with pytest.raises(FrozenInstanceError):
result.__setattr__('intent_supported', False)
def test_warm_policy_does_not_cache_current_state_or_native_permission():
cp = profile('mazda')
cp.alternativeExperience = 32
assert vehicle.policy_for(cp).ordinary_axis_ack_required
assert vehicle.policy_for(cp).intent_supported
cs = structs.CarState(canValid=True, canTimeout=False, gearShifter='drive', vEgo=20)
native = SimpleNamespace(safetyModel=int(cp.safetyConfigs[0].safetyModel.raw),
safetyParam=cp.safetyConfigs[0].safetyParam, requestedLateral=True, lateralAllowed=True)
assert vehicle.ordinary_axis_request_allowed(cp, cs)
assert not vehicle.native_latch_rejected(cp, native, state=cs)
native.lateralAllowed = False
assert vehicle.native_latch_rejected(cp, native, state=cs)
cs.canValid = False
assert not vehicle.ordinary_axis_request_allowed(cp, cs)
cs.canValid, cs.steerFaultPermanent = True, True
assert not vehicle.ordinary_axis_request_allowed(cp, cs)
cs.steerFaultPermanent, cs.steerFaultTemporary = False, True
assert not vehicle.native_latch_rejected(cp, native, state=cs)
native.safetyParam ^= 1
assert not vehicle.native_matches_cp(cp, native.safetyModel, native.safetyParam)