Files
StarPilot/openpilot/starpilot/aol/tests/test_gm_source_deadline.py
T
2026-10-21 10:54:26 -05:00

220 lines
12 KiB
Python

"""Volt route c0508: source cutoff must not replace an unexpired axis proof."""
from collections import deque
from contextlib import ExitStack
from unittest.mock import patch
import pytest
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
from openpilot.cereal import messaging
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.intent import read_settings
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
from openpilot.starpilot.aol.tests.test_input_deadline_continuity import BASE, intent
def bind_steering_receipts(sd, step, cleanup):
"""Deliver the same declared producer receipt through its actual socket path."""
if sd.steering_companion is None:
return step
pending = deque()
original = messaging.recv_one_or_none
def receive(sock):
if sock is sd.steering_companion.sock:
return pending.popleft() if pending else None
return original(sock)
cleanup.enter_context(patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive))
def observed_step(tick, **kwargs):
if not kwargs.get('timeout', False):
stamp = kwargs.get('state_stamp', 10_000_000_000 + tick * 10_000_000 + kwargs.get('offset_ns', 0))
receipt = messaging.new_message('starpilotCarState', valid=True, logMonoTime=stamp)
receipt.starpilotCarState.sourceCarStateMonoTime = stamp
receipt.starpilotCarState.lateralAuthorityUnavailable = kwargs.get('authority_unavailable', False)
kwargs['authority_message'] = receipt
pending.append(receipt.as_reader())
return step(tick, **kwargs)
return observed_step
def profiles():
from opendbc.car.gm.tests.test_volt_camera_control import camera_params
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
from opendbc.car.gm.tests.test_bolt_pedal import params as bolt_params
from opendbc.car.gm.values import CAR
result = [sdgm_params(alpha=True, brake_c9=c9) for c9 in (False, True)]
result += [camera_params(alpha=True)]
result += [removed_params(alpha=True, alternate=alternate) for alternate in (False, True)]
result += [ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=True, sascm=True,
accelerator=not c9, radar=radar) for c9 in (False, True) for radar in (False, True)]
result += [bolt_params(candidate, pedal=True, camera=not removed, removed=removed) for candidate in
(CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, CAR.CHEVROLET_BOLT_CC_2022_2023,
CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL) for removed in (False, True)]
return result
@pytest.mark.parametrize('cp', [cp for cp in profiles() if cp.carFingerprint != 'CHEVROLET_VOLT_ASCM'],
ids=lambda cp: f'{cp.carFingerprint}-{cp.safetyConfigs[0].safetyParam:x}')
@pytest.mark.parametrize('mode', ['ordinary', 'brake', 'regen'])
@pytest.mark.parametrize('successor', ['fresh', 'missing'])
def test_volt_cutoff_keeps_only_original_lease_then_consumes_next_source(mode, successor, cp):
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with OpenpilotPrefix(), ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
full = mode == 'ordinary'
sd, controls, step = case._authority_callers(full, cp=cp.as_reader().as_builder(), genuine_events=True)
step = bind_steering_receipts(sd, step, cleanup)
sd.aol_settings = read_settings(sd.params)
cs = fixture.car_state(brake=mode == 'brake')
cs.regenBraking = mode == 'regen'
old, _, command = step(2, state_stamp=BASE, offset_ns=5_467_396,
intent_message=intent(BASE, 3), observed_state=cs, native_request_long=full)
assert old.lateralActive and command.latActive and bool(command.longActive) == full
deadline = old.validUntilMonoTime
clock = [BASE + 29_900_000]
actual_controls_receive = controls.sm.update_msgs
def empty_control_receive(timeout):
assert timeout == 0
actual_controls_receive(clock[0] / 1e9, [])
cleanup.enter_context(patch.object(controls.sm, 'update', side_effect=empty_control_receive))
original = sd.update_process_health
def finish_preamble():
value = original()
clock[0] = BASE + 32_985_260
return value
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', side_effect=lambda: clock[0]), \
patch.object(sd, 'update_process_health', side_effect=finish_preamble):
held, _, _ = step(5, timeout=True, state_stamp=BASE, offset_ns=-100_000,
intent_message=intent(BASE, 3), observed_state=cs, native_request_long=full)
from opendbc.car.gm.steering_authority import monitored_profile
if monitored_profile(cp):
# This monitored profile also needs its original steering-health receipt.
# At this point that source is older than30ms. Its missing successor cannot
# inherit health from an unexpired axis envelope or a fresh Panda grant.
assert not sd.aol_axis_publication_deferred
assert not held.lateralActive and not held.longitudinalActive
assert sd.steering_authority_unavailable and sd.aol_authority_lost
return
assert sd.aol_axis_publication_deferred
assert held.to_dict() == old.to_dict() and held.validUntilMonoTime == deadline
assert not sd.aol_authority_lost
# Consumers continue to enforce the OLD deadline independently.
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=deadline + 1):
expired, _ = controls.state_control()
assert not expired.latActive and not expired.longActive
fresh = BASE + 36_000_000
axis, state, command = step(6, state_stamp=fresh, timeout=successor == 'missing',
intent_message=intent(fresh, 4) if successor == 'fresh' else intent(BASE, 3),
observed_state=cs, native_request_long=full)
assert not sd.aol_axis_publication_deferred
if successor == 'fresh':
assert axis.sourceCarStateMonoTime == fresh and axis.lateralActive and command.latActive
assert bool(command.longActive) == full and bool(state.enabled) == full
assert not sd.aol_authority_lost
else:
assert not axis.lateralActive and not axis.longitudinalActive
assert not command.latActive and not command.longActive
@pytest.mark.parametrize('bad', ['cancel', 'new_brake', 'new_paddle', 'door', 'belt', 'temporary_eps',
'permanent_eps', 'invalid_can', 'main_off', 'native_deny', 'native_rx', 'producer', 'pause', 'deadline'])
def test_volt_deferral_cannot_hide_driver_edges_or_failed_proof(bad):
from dataclasses import replace
from opendbc.car.structs import car
from openpilot.cereal import messaging
from openpilot.starpilot.aol.wire import decode_intent, encode_intent, decode_safety, encode_safety
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with OpenpilotPrefix(), ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
sd, _, step = case._authority_callers(False, cp=sdgm_params(alpha=True), genuine_events=True)
step = bind_steering_receipts(sd, step, cleanup)
sd.aol_settings = read_settings(sd.params)
old, _, command = step(2, state_stamp=BASE, offset_ns=4_530_833,
intent_message=intent(BASE, 3), native_request_long=False)
assert old.lateralActive and command.latActive
now = BASE + 30_721_946
cs = sd.CS_prev.as_builder()
if bad == 'cancel':
cs.buttonEvents = [car.CarState.ButtonEvent(type='cancel', pressed=True)]
elif bad in ('new_brake', 'new_paddle', 'door', 'belt', 'temporary_eps', 'permanent_eps', 'invalid_can', 'main_off'):
if bad == 'main_off':
cs.cruiseState.available = False
else:
setattr(cs, {'new_brake': 'brakePressed', 'new_paddle': 'regenBraking', 'door': 'doorOpen',
'belt': 'seatbeltUnlatched', 'temporary_eps': 'steerFaultTemporary', 'permanent_eps': 'steerFaultPermanent',
'invalid_can': 'canValid'}[bad], bad != 'invalid_can')
elif bad.startswith('native_'):
native = decode_safety(sd.sm['aolSafetyWire'])
native = replace(native, lateralAllowed=False) if bad == 'native_deny' else replace(native,
pandaInventory=tuple(replace(p, safetyRxChecksInvalid=True) for p in native.pandaInventory))
message = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=native.observedMonoTime)
message.aolSafetyWire = encode_safety(native)
sd.sm.update_msgs(now / 1e9, [message.as_reader()])
elif bad in ('producer', 'pause'):
message = intent(BASE + 29_000_000, 4)
value = decode_intent(message.aolIntentWire)
message.aolIntentWire = encode_intent(replace(value, **(
{'producerSessionId': 'different-card'} if bad == 'producer' else {'pauseLateral': True})))
sd.aol_intent_companion._observe(message.logMonoTime, message.valid, message.aolIntentWire)
else:
now = old.validUntilMonoTime
initial = (sd.state_machine.state, sd.state_machine.soft_disable_timer, sd.enabled, sd.active)
sd.car_state_samples = ((cs.as_reader(), True),)
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now):
assert not sd.defer_expired_axis_publication(cs, initial, now_ns=now)
assert old.validUntilMonoTime == BASE + 34_530_833
@pytest.mark.parametrize('cp', [cp for cp in profiles() if cp.carFingerprint == 'CHEVROLET_VOLT_ASCM'],
ids=lambda cp: f'{cp.safetyConfigs[0].safetyParam:x}')
def test_ascm_separate_steering_companion_expiry_still_withdraws_outputs(cp):
from openpilot.cereal import messaging
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with OpenpilotPrefix(), ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
sd, _, step = case._authority_callers(True, cp=cp.as_reader().as_builder(), genuine_events=True)
step = bind_steering_receipts(sd, step, cleanup)
sd.aol_settings = read_settings(sd.params)
from collections import deque
queue = deque()
original_update = sd.sm.update_msgs
original_receive = messaging.recv_one_or_none
def update(timestamp, frames):
queue.extend(frame for frame in frames if frame.which() == 'starpilotCarState')
return original_update(timestamp, frames)
def receive(sock):
if sock is sd.steering_companion.sock:
return queue.popleft() if queue else None
return original_receive(sock)
cleanup.enter_context(patch.object(sd.sm, 'update_msgs', side_effect=update))
cleanup.enter_context(patch.object(messaging, 'recv_one_or_none', side_effect=receive))
authority = messaging.new_message('starpilotCarState', valid=True, logMonoTime=BASE)
authority.starpilotCarState.sourceCarStateMonoTime = BASE
# This family has an ADDITIONAL source-bound steering companion. Its
# original lifetime is independent of the axis publication's later expiry.
old, _, command = step(2, state_stamp=BASE, offset_ns=5_467_396,
intent_message=intent(BASE, 3), authority_message=authority)
assert old.lateralActive and command.latActive and command.longActive
clock = [BASE + 29_900_000]
original = sd.update_process_health
def finish_preamble():
value = original()
clock[0] = BASE + 32_985_260
return value
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', side_effect=lambda: clock[0]), \
patch.object(sd, 'update_process_health', side_effect=finish_preamble):
axis, _, command = step(5, timeout=True, offset_ns=-100_000,
intent_message=intent(BASE, 3), authority_message=authority)
assert not sd.aol_axis_publication_deferred
assert not axis.lateralActive and not command.latActive and not command.longActive
assert old.validUntilMonoTime == BASE + 35_467_396