mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 17:13:54 +08:00
Reuse exact native qualification across all vehicle makes
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
"""All registered makes share exact-packet structural reuse and live vetoes."""
|
||||
from dataclasses import replace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.starpilot.aol import runtime, transport_pause, vehicle
|
||||
from openpilot.starpilot.aol.tests.test_native_observation_admission import receive
|
||||
from openpilot.starpilot.aol.tests.test_panda_inventory import NOW, bound_state, sm_for
|
||||
from openpilot.starpilot.aol.tests.test_runtime import car_state
|
||||
|
||||
BRANDS = ('honda', 'hyundai', 'gm', 'ford', 'mazda', 'tesla', 'toyota', 'subaru', 'nissan', 'chrysler', 'rivian', 'volkswagen')
|
||||
|
||||
|
||||
def profile(brand):
|
||||
if brand in ('honda', 'hyundai', 'gm', 'ford', 'mazda', 'tesla', 'toyota'):
|
||||
from openpilot.starpilot.aol.tests.test_vehicle_policy_cache import profile as factory
|
||||
cp = factory(brand)
|
||||
elif brand == 'chrysler':
|
||||
from opendbc.car.chrysler.aol_contract import CHRYSLER_AOL_CARS
|
||||
from openpilot.starpilot.aol.tests.test_chrysler import cp_for
|
||||
cp = cp_for(sorted(CHRYSLER_AOL_CARS)[0])
|
||||
elif brand == 'volkswagen':
|
||||
from opendbc.car.volkswagen.values import CAR
|
||||
from openpilot.starpilot.aol.tests.test_mqb_topology_contract import params
|
||||
cp = params(CAR.VOLKSWAGEN_GOLF_MK7, False, False, 4, False)
|
||||
else:
|
||||
fp = gen_empty_fingerprint()
|
||||
if brand == 'subaru':
|
||||
from opendbc.car.subaru.values import CAR
|
||||
identity = CAR.SUBARU_OUTBACK_2023
|
||||
elif brand == 'nissan':
|
||||
from opendbc.car.nissan.values import CAR
|
||||
identity = CAR.NISSAN_LEAF
|
||||
else:
|
||||
from opendbc.car.rivian.values import CAR
|
||||
identity = CAR.RIVIAN_R1
|
||||
fp[0][0x321] = 7
|
||||
cp = interfaces[identity].get_params(identity, fp, [], False, False, False)
|
||||
policy = vehicle.policy_for(cp)
|
||||
assert policy.intent_supported, brand
|
||||
config = (vehicle.hyundai.aol_safety_config(cp) if brand == 'hyundai' else
|
||||
vehicle.ford.aol_safety_config(cp) if brand == 'ford' else
|
||||
vehicle.volkswagen.aol_safety_config(cp) if brand == 'volkswagen' else cp.safetyConfigs[-1])
|
||||
assert config is not None
|
||||
config.safetyParam |= policy.safety_param_addition
|
||||
cp.alternativeExperience |= policy.alternative_experience_addition
|
||||
assert vehicle.native_matches_cp(cp, int(config.safetyModel.raw), int(config.safetyParam)), brand
|
||||
return cp
|
||||
|
||||
|
||||
@pytest.fixture(params=BRANDS)
|
||||
def cp(request):
|
||||
return profile(request.param)
|
||||
|
||||
|
||||
def full_oracle(sm, cp, *, now=NOW, session=None):
|
||||
actual = runtime.current_native(sm, cp, now_ns=now, axis_session_id=session)
|
||||
with patch.object(runtime, 'native_admission_key', return_value=None):
|
||||
expected = runtime.current_native(sm, cp, now_ns=now, axis_session_id=session)
|
||||
assert actual == expected
|
||||
return actual
|
||||
|
||||
|
||||
def test_matrix_covers_every_registered_make():
|
||||
assert set(BRANDS) == set(vehicle._PORTS)
|
||||
|
||||
|
||||
def test_one_validation_per_original_packet_and_owner_isolation(cp):
|
||||
before = cp.to_dict()
|
||||
state = bound_state(cp)
|
||||
sm = sm_for(cp, state)
|
||||
other = sm_for(cp, state)
|
||||
with patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as check:
|
||||
for tick in range(10):
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW + tick * 1_000_000) == state
|
||||
assert check.call_count == 1
|
||||
assert runtime.current_native(other, cp, now_ns=NOW) == state
|
||||
assert check.call_count == 2
|
||||
updated = bound_state(cp, now=NOW + 10_000_000)
|
||||
receive(sm, updated)
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW + 10_000_000) == updated
|
||||
assert check.call_count == 3
|
||||
assert cp.to_dict() == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field', ('brand', 'carFingerprint', 'flags', 'passive', 'notCar', 'dashcamOnly',
|
||||
'pcmCruise', 'openpilotLongitudinalControl', 'alternativeExperience',
|
||||
'safetyParam', 'safetyModel', 'safetyConfigs', 'carVin'))
|
||||
def test_hot_configuration_mutations_match_full_validation(cp, field):
|
||||
state = bound_state(cp)
|
||||
sm = sm_for(cp, state)
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
key = vehicle.native_admission_key(cp)
|
||||
if field == 'safetyParam':
|
||||
cp.safetyConfigs[-1].safetyParam ^= 1
|
||||
elif field == 'safetyModel':
|
||||
cp.safetyConfigs[-1].safetyModel = 'noOutput'
|
||||
elif field == 'safetyConfigs':
|
||||
cp.safetyConfigs = []
|
||||
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': int(cp.alternativeExperience) ^ 1, 'carVin': 'MUTATED'}[field])
|
||||
assert vehicle.native_admission_key(cp) != key
|
||||
with patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as check:
|
||||
full_oracle(sm, cp)
|
||||
assert check.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize('change', ('rx', 'heartbeat', 'fault', 'inventory', 'serial', 'lateral', 'longitudinal',
|
||||
'session', 'expired', 'future', 'invalid', 'not_alive', 'not_seen'))
|
||||
def test_every_live_change_matches_full_validation(cp, change):
|
||||
state = bound_state(cp)
|
||||
sm = sm_for(cp, state)
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
now, session = NOW, None
|
||||
if change in ('rx', 'heartbeat', 'fault', 'inventory'):
|
||||
field, value = {'rx': ('safetyRxChecksInvalid', True), 'heartbeat': ('heartbeatLost', True),
|
||||
'fault': ('faults', 1), 'inventory': ('safetyParam', state.pandaInventory[-1].safetyParam ^ 1)}[change]
|
||||
slots = (*state.pandaInventory[:-1], replace(state.pandaInventory[-1], **{field: value}))
|
||||
receive(sm, replace(state, pandaInventory=slots))
|
||||
elif change in ('serial', 'lateral', 'longitudinal'):
|
||||
field, value = {'serial': ('pandaSerial', 'other'), 'lateral': ('lateralAllowed', False),
|
||||
'longitudinal': ('longitudinalAllowed', False)}[change]
|
||||
receive(sm, replace(state, **{field: value}))
|
||||
elif change == 'session':
|
||||
session = 'other'
|
||||
elif change == 'expired':
|
||||
now = state.validUntilMonoTime + 1
|
||||
elif change == 'future':
|
||||
now -= 1
|
||||
else:
|
||||
getattr(sm, {'invalid': 'valid', 'not_alive': 'alive', 'not_seen': 'seen'}[change])['aolSafetyWire'] = False
|
||||
result = full_oracle(sm, cp, now=now, session=session)
|
||||
if change in ('lateral', 'longitudinal'):
|
||||
assert result is not None and not getattr(result, 'lateralAllowed' if change == 'lateral' else 'longitudinalAllowed')
|
||||
else:
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_latest_panda_veto_is_checked_after_structural_hit(cp):
|
||||
state = bound_state(cp)
|
||||
sm = sm_for(cp, state)
|
||||
assert transport_pause.healthy_transport(sm, cp, car_state(), state.axisSessionId, NOW)
|
||||
message = messaging.new_message('pandaStates', len(cp.safetyConfigs), valid=True, logMonoTime=state.sourcePandaStatesMonoTime)
|
||||
for index, panda in enumerate(sm['pandaStates']):
|
||||
message.pandaStates[index] = panda
|
||||
message.pandaStates[-1].heartbeatLost = True
|
||||
sm.update_msgs(NOW / 1e9, [message.as_reader()])
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
assert not transport_pause.healthy_transport(sm, cp, car_state(), state.axisSessionId, NOW)
|
||||
|
||||
|
||||
def test_same_builder_mutation_during_miss_cannot_bind_old_key(cp):
|
||||
state = bound_state(cp)
|
||||
sm = sm_for(cp, state)
|
||||
original = runtime.native_inventory_matches_cp
|
||||
|
||||
def mutate(native, params):
|
||||
result = original(native, params)
|
||||
params.carVin = 'CHANGED-DURING-MISS'
|
||||
return result
|
||||
|
||||
with patch.object(runtime, 'native_inventory_matches_cp', side_effect=mutate):
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
assert sm._aol_native_admission is None
|
||||
full_oracle(sm, cp)
|
||||
|
||||
|
||||
def test_oversize_cp_bypasses_reuse_without_changing_admission(cp):
|
||||
cp.carVin = 'X' * (vehicle._POLICY_CACHE_MAX_BYTES + 1)
|
||||
state = bound_state(cp)
|
||||
sm = sm_for(cp, state)
|
||||
assert vehicle.native_admission_key(cp) is None
|
||||
with patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as check:
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
assert check.call_count == 2
|
||||
|
||||
|
||||
def test_snapshot_failure_falls_back_to_full_validation(cp):
|
||||
state = bound_state(cp)
|
||||
sm = sm_for(cp, state)
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
with patch.object(vehicle, '_car_params_bytes', return_value=None), \
|
||||
patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as check:
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == state
|
||||
assert check.call_count == 2
|
||||
assert sm._aol_native_admission is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('legacy', (False, True))
|
||||
def test_legacy_or_slotted_owner_keeps_full_validation(cp, legacy):
|
||||
state = bound_state(cp)
|
||||
if legacy:
|
||||
state = replace(state, sourcePandaStatesMonoTime=0, pandaInventory=())
|
||||
original = sm_for(cp, state)
|
||||
|
||||
class SlottedOwner:
|
||||
__slots__ = ('valid', 'alive', 'seen', 'logMonoTime')
|
||||
|
||||
def __init__(self):
|
||||
for name in self.__slots__:
|
||||
setattr(self, name, getattr(original, name))
|
||||
|
||||
def __getitem__(self, name):
|
||||
return original[name]
|
||||
|
||||
sm = original if legacy else SlottedOwner()
|
||||
expected = state if not legacy or len(cp.safetyConfigs) == 1 else None
|
||||
with patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as check:
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == expected
|
||||
assert runtime.current_native(sm, cp, now_ns=NOW) == expected
|
||||
assert check.call_count == 2
|
||||
@@ -211,8 +211,8 @@ def test_other_admitted_profiles_use_only_scoped_reuse(profile_name):
|
||||
with mock.patch.object(runtime, 'native_matches_cp', wraps=runtime.native_matches_cp) as profile:
|
||||
assert current(sm, cp) == state
|
||||
assert current(sm, cp) == state
|
||||
assert profile.call_count == (1 if profile_name == 'ev6_two_pandas' else 2)
|
||||
assert (sm._aol_native_admission is not None) == (profile_name == 'ev6_two_pandas')
|
||||
assert profile.call_count == 1
|
||||
assert sm._aol_native_admission is not None
|
||||
|
||||
|
||||
def test_rejected_observation_then_valid_recovery_cannot_reuse_a_failed_verdict():
|
||||
|
||||
@@ -104,12 +104,17 @@ def native_matches_cp(CP, model: int, param: int) -> bool:
|
||||
def native_admission_key(CP):
|
||||
"""Bind structural reuse to the complete mutable vehicle configuration."""
|
||||
try:
|
||||
if CP.brand == 'hyundai' and CP.carFingerprint in (hyundai.HyundaiCAR.KIA_EV6, hyundai.HyundaiCAR.KIA_EV9):
|
||||
# Cache only structural admission of one exact wire packet. Full CP bytes
|
||||
# invalidate every mutable field; unknown types keep full validation.
|
||||
if CP.brand == 'hyundai':
|
||||
existing = hyundai.native_admission_key(CP)
|
||||
if existing is not None:
|
||||
return existing
|
||||
if CP.brand in _PORTS:
|
||||
# Reuse only one exact packet's structural admission, for every owner.
|
||||
# Full CP bytes invalidate every mutable field; live gates stay per call.
|
||||
key = _car_params_bytes(CP)
|
||||
return ('hkg-full-cp', key) if key is not None else None
|
||||
return hyundai.native_admission_key(CP) if CP.brand == 'hyundai' else None
|
||||
if key is not None:
|
||||
return ('vehicle-full-cp', key)
|
||||
return None
|
||||
except (AttributeError, TypeError, ValueError, OverflowError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "9ac218d1b06065b7f3c3d769ab507101e89aeedd5d90b68de21f463b476096f3", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "4e7069cd3da0a1f7965dac62ce06f6bfc8711c9ca7ae71c48488a1b1ad2be14d", "schema_version": 1}
|
||||
|
||||
@@ -24,6 +24,7 @@ from tools.ci import lifecycle_shards, vehicle_pytest
|
||||
# Representative production callers plus their source/native refusal cases.
|
||||
# The emitted selected_test_ids record exact parametrized configurations.
|
||||
AOL_LIFECYCLE_TARGETS = (
|
||||
'openpilot/starpilot/aol/tests/test_crossmake_native_admission.py',
|
||||
'openpilot/starpilot/aol/tests/test_hkg_source_policy_cache.py',
|
||||
'openpilot/starpilot/aol/tests/test_hkg_native_admission.py',
|
||||
'openpilot/starpilot/aol/tests/test_native_observation_admission.py',
|
||||
|
||||
Reference in New Issue
Block a user