From 37dc1533efdf29957e540c712e3d4be8fba15522 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:42:44 -0500 Subject: [PATCH] Reuse exact native qualification across all vehicle makes --- .../tests/test_crossmake_native_admission.py | 220 ++++++++++++++++++ .../test_native_observation_admission.py | 4 +- openpilot/starpilot/aol/vehicle.py | 15 +- prebuilt.json | 2 +- tools/ci/run_vehicle_tests.py | 1 + 5 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 openpilot/starpilot/aol/tests/test_crossmake_native_admission.py diff --git a/openpilot/starpilot/aol/tests/test_crossmake_native_admission.py b/openpilot/starpilot/aol/tests/test_crossmake_native_admission.py new file mode 100644 index 0000000000..86b637a793 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_crossmake_native_admission.py @@ -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 diff --git a/openpilot/starpilot/aol/tests/test_native_observation_admission.py b/openpilot/starpilot/aol/tests/test_native_observation_admission.py index 6c74a61cb9..8191e7ce9b 100644 --- a/openpilot/starpilot/aol/tests/test_native_observation_admission.py +++ b/openpilot/starpilot/aol/tests/test_native_observation_admission.py @@ -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(): diff --git a/openpilot/starpilot/aol/vehicle.py b/openpilot/starpilot/aol/vehicle.py index d22991ab25..0d474b7e39 100644 --- a/openpilot/starpilot/aol/vehicle.py +++ b/openpilot/starpilot/aol/vehicle.py @@ -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 diff --git a/prebuilt.json b/prebuilt.json index b000efe0f2..63132ec339 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "9ac218d1b06065b7f3c3d769ab507101e89aeedd5d90b68de21f463b476096f3", "schema_version": 1} +{"protected_inventory_sha256": "4e7069cd3da0a1f7965dac62ce06f6bfc8711c9ca7ae71c48488a1b1ad2be14d", "schema_version": 1} diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index 2ab8f99c43..83e6da04ba 100644 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -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',