Files
StarPilot/openpilot/starpilot/aol/tests/test_availability_timer_deferral.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

183 lines
9.1 KiB
Python

from contextlib import ExitStack
from dataclasses import replace
from unittest import mock
import pytest
from openpilot.cereal import log, messaging
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.selfdrived import selfdrived as owner
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
from openpilot.starpilot.aol.tests.test_long_only_input_wait import intent
from openpilot.starpilot.aol.wire import decode_intent, encode_intent, decode_safety, encode_safety
SOURCE = 10_030_000_000
PUB = SOURCE + 4_837_661
DECISION = SOURCE + 31_625_140
def wire(stamp, seq):
message = intent(stamp, seq, pause_lat=False)
value = decode_intent(message.aolIntentWire)
message.aolIntentWire = encode_intent(replace(value, allowedLatch=True, lateralArmed=True))
return message
def prepare(stack):
case = fixture.IoniqAvailabilityTests(methodName='runTest')
stack.callback(case.doCleanups)
stack.enter_context(OpenpilotPrefix())
sd, controls, drive, card, receive = case._ioniq_availability_callers(full_op=True)
axis, state, command = drive(2, car_output_valid=False)
assert str(axis.faultReason) == 'availabilityPause'
assert state.alertType == 'commIssue/softDisable'
assert not state.enabled and not state.active and not command.latActive and not command.longActive
assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.disabled
assert sd.state_machine.soft_disable_timer == 300
axis, state, command = drive(3, state_stamp=SOURCE, offset_ns=4_837_661,
intent_message=wire(SOURCE, 4), native_request_long=False)
assert axis.lateralActive and command.latActive
assert not axis.desiredLongitudinal and not axis.longitudinalActive and not command.longActive
assert not state.enabled and not state.active and sd.aol_availability_long_inhibit
assert str(axis.faultReason) == 'none' and axis.nativeAcknowledged and axis.qualified
assert axis.sourceCarStateMonoTime == SOURCE and axis.observedMonoTime == PUB
assert axis.validUntilMonoTime == PUB + 30_000_000
assert sd.state_machine.soft_disable_timer == 299
return sd, controls, drive, axis
def test_first_pause_then_recovered_lateral_source_cutoff():
with ExitStack() as stack:
sd, controls, drive, original = prepare(stack)
previous_axis = original.to_dict()
timer_before = sd.state_machine.soft_disable_timer
attempts = []
actual_defer = sd.defer_expired_axis_publication
now = [SOURCE + 29_900_000]
actual_events = sd.update_events
def events(cs):
result = actual_events(cs)
now[0] = DECISION
return result
sd.car_state_poller = mock.Mock()
sd.car_state_poller.poll.return_value = []
def defer(cs, initial, **kwargs):
result = actual_defer(cs, initial, **kwargs)
attempts.append(result)
return result
with mock.patch.object(owner.time, 'monotonic_ns', side_effect=lambda: now[0]), \
mock.patch.object(sd, 'defer_expired_axis_publication', side_effect=defer), \
mock.patch.object(sd, 'update_events', side_effect=events):
axis, state, command = drive(6, timeout=True, offset_ns=-100_000,
intent_message=wire(SOURCE, 4), native_request_long=False)
assert sd.aol_axis_publication_deferred
assert not state.enabled and not state.active and not command.longActive and not axis.longitudinalActive
assert sd.state_machine.soft_disable_timer == timer_before == 299
assert axis.to_dict() == previous_axis and command.latActive
assert attempts == [True]
# One tick only, even before the original proof's unchanged deadline.
with mock.patch.object(owner.time, 'monotonic_ns', return_value=SOURCE + 33_000_000):
next_axis, _, next_command = drive(6, timeout=True, offset_ns=3_000_000,
intent_message=wire(SOURCE, 4), native_request_long=False)
assert not sd.aol_axis_publication_deferred
assert sd.state_machine.soft_disable_timer == 298
assert not next_axis.lateralActive and not next_command.latActive and not next_command.longActive
@pytest.mark.parametrize('condition', ['current_soft', 'current_immediate', 'current_enable', 'current_user',
'deadline', 'initial_state_changed', 'invalid_carstate', 'settings_revoked', 'intent_pause',
'intent_unlatched', 'producer_changed', 'session_changed', 'active_soft_disabling',
'native_denial', 'native_invalid', 'can_invalid', 'can_timeout', 'eps_temp', 'eps_perm',
'dm_event_changed', 'lagging', 'thermal', 'wrong_gear', 'physical_cancel',
'no_inhibit', 'authority_lost', 'proof_changed', 'integrity_epoch'])
def test_timer_fix_retains_fault_identity_and_original_deadline_gates(condition):
with ExitStack() as stack:
sd, _, _, axis = prepare(stack)
previous = axis.to_dict()
source_proof = sd.aol_published_input_proof
sd.aol_step_input_proof = source_proof
sd.aol_step_deferral_checked = False
sd.aol_step_intent_drain_count = 0
now = DECISION
cs = sd.CS_prev
event = {'current_soft': owner.EventName.commIssue,
'current_immediate': owner.EventName.controlsMismatch,
'current_enable': owner.EventName.buttonEnable,
'current_user': owner.EventName.buttonCancel}.get(condition)
if event is not None:
sd.events.add(event)
# Keep event-set equality so the actual event-kind guards are exercised.
sd.events_prev = sd.events.names.copy()
elif condition == 'deadline':
now = source_proof[3]
elif condition == 'invalid_carstate':
sd.conditional_car_state_valid = False
elif condition in ('settings_revoked', 'intent_pause', 'intent_unlatched', 'producer_changed'):
latest = sd.aol_intent_companion.latest
changes = {'settings_revoked': {'settingsQualified': False}, 'intent_pause': {'pauseLateral': True},
'intent_unlatched': {'allowedLatch': False}, 'producer_changed': {'producerSessionId': 'other-card'}}[condition]
sd.aol_intent_companion.latest = (latest[0], latest[1], replace(latest[2], **changes))
elif condition == 'session_changed':
sd.aol_session_id = 'other-owner'
elif condition == 'active_soft_disabling':
# Reach the genuine production soft-disabling state and countdown.
sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled
sd.events.add(owner.EventName.commIssue)
sd.enabled, sd.active = sd.state_machine.update(sd.events)
assert sd.enabled and sd.active and sd.state_machine.soft_disable_timer == 300
assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.softDisabling
sd.events_prev = sd.events.names.copy()
if condition in ('can_invalid', 'can_timeout', 'eps_temp', 'eps_perm', 'wrong_gear', 'physical_cancel'):
from opendbc.car.structs import car
cs = sd.CS_prev.as_builder()
if condition == 'can_invalid':
cs.canValid = False
elif condition == 'can_timeout':
cs.canTimeout = True
elif condition == 'eps_temp':
cs.steerFaultTemporary = True
elif condition == 'eps_perm':
cs.steerFaultPermanent = True
elif condition == 'wrong_gear':
cs.gearShifter = car.CarState.GearShifter.park
else:
cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
sd.car_state_samples = ((cs, True),)
elif condition == 'native_denial':
native = decode_safety(sd.sm['aolSafetyWire'])
message = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=PUB)
message.aolSafetyWire = encode_safety(replace(native, lateralAllowed=False))
sd.sm.update_msgs(PUB / 1e9, [message.as_reader()])
elif condition == 'native_invalid':
sd.sm.valid['aolSafetyWire'] = False
elif condition == 'dm_event_changed':
sd.events.add(owner.EventName.driverDistracted3)
elif condition == 'lagging':
stack.enter_context(mock.patch.object(type(sd.rk), 'lagging', new_callable=mock.PropertyMock, return_value=True))
elif condition == 'thermal':
message = messaging.new_message('deviceState', valid=True, logMonoTime=PUB)
message.deviceState = sd.sm['deviceState']
message.deviceState.thermalStatus = 'overheated'
sd.sm.update_msgs(PUB / 1e9, [message.as_reader()])
elif condition == 'no_inhibit':
sd.aol_availability_long_inhibit = False
elif condition == 'authority_lost':
sd.aol_authority_lost = True
elif condition == 'proof_changed':
sd.aol_step_input_proof = (*source_proof[:3], source_proof[3] + 1, *source_proof[4:])
elif condition == 'integrity_epoch':
sd.aol_intent_companion.integrity_epoch += 1
initial = (sd.state_machine.state, sd.state_machine.soft_disable_timer, sd.enabled, sd.active)
if condition == 'initial_state_changed':
sd.state_machine.soft_disable_timer += 1
with mock.patch.object(owner.time, 'monotonic_ns', return_value=now), \
mock.patch.object(messaging, 'recv_one_or_none', return_value=None):
result = sd.defer_expired_axis_publication(cs, initial, now_ns=now)
assert not result
assert sd.pm.messages['aolAxisState'].aolAxisState.to_dict() == previous
assert sd.aol_published_input_proof == source_proof