Files
StarPilot/openpilot/starpilot/aol/tests/test_fresh_lag_continuity.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

172 lines
9.1 KiB
Python

"""Existing ordinary controls follow the normal soft-disable countdown."""
from contextlib import ExitStack, contextmanager
from dataclasses import replace
from unittest import mock
import pytest
from openpilot.cereal import log
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.runtime import current_native
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
from openpilot.starpilot.aol.tests.test_input_deadline_continuity import BASE
from openpilot.starpilot.aol.tests.test_ordinary_source_availability import settings
from openpilot.starpilot.aol.wire import decode_safety, encode_safety
MODES = ['long_only', 'combined', 'combined_unarmed']
@contextmanager
def callers(mode):
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
cleanup.enter_context(OpenpilotPrefix())
owners = case._ioniq_availability_callers(full_op=mode != 'lateral_only')
sd, controls, drive, card, receive = owners
old, state, command = drive(2, state_stamp=BASE, **settings(mode, BASE, 3))
assert old.longitudinalActive == (mode != 'lateral_only')
assert command.longActive == old.longitudinalActive
assert old.lateralActive == (mode != 'long_only')
if mode in ('combined', 'lateral_only'):
assert receive(BASE, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
yield owners, old
def healthy_soft_checkpoint(sd, axis, state, stamp):
assert sd.rk.lagging and log.OnroadEvent.EventName.selfdrivedLagging in sd.events.names
assert sd.events.contains(fixture.ET.NO_ENTRY) and sd.events.contains(fixture.ET.SOFT_DISABLE)
assert not sd.events.contains(fixture.ET.USER_DISABLE) and not sd.events.contains(fixture.ET.IMMEDIATE_DISABLE)
assert state.enabled and state.active and state.state == log.SelfdriveState.OpenpilotState.softDisabling
assert sd.state_machine.soft_disable_timer == 300
assert axis.sourceCarStateMonoTime == stamp and sd.conditional_car_state_valid
assert axis.observedMonoTime == stamp and axis.validUntilMonoTime == stamp + 30_000_000
native = current_native(sd.sm, sd.CP, now_ns=stamp, axis_session_id=sd.aol_session_id)
assert native is not None and native.requestedLongitudinal and native.longitudinalAllowed
assert not sd.aol_authority_lost and not sd.events.contains(fixture.ET.ENABLE)
@pytest.mark.parametrize('mode', MODES)
def test_expected_established_ordinary_axes_follow_fresh_soft_disable_semantics(mode):
with callers(mode) as ((sd, _, drive, _, _), _):
stamp = BASE + 10_000_000
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=0.02):
axis, state, command = drive(3, state_stamp=stamp, **settings(mode, stamp, 4))
healthy_soft_checkpoint(sd, axis, state, stamp)
assert axis.desiredLongitudinal and axis.longitudinalActive and command.longActive
assert bool(axis.lateralActive) == bool(command.latActive) == (mode != 'long_only')
@pytest.mark.parametrize('mode', MODES)
def test_fresh_lag_countdown_disables_without_long_auto_enable(mode):
with callers(mode) as ((sd, _, drive, _, _), old):
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=0.02):
for tick in range(3, 304):
stamp = BASE + (tick - 2) * 10_000_000
axis, state, command = drive(tick, state_stamp=stamp, **settings(mode, stamp, tick + 1))
assert axis.sourceCarStateMonoTime == stamp and axis.validUntilMonoTime == stamp + 30_000_000
assert not sd.aol_authority_lost and not sd.events.contains(fixture.ET.ENABLE)
assert sd.state_machine.soft_disable_timer == max(0, 303 - tick)
assert state.enabled == (tick < 303) and state.active == (tick < 303)
assert not state.enabled and not state.active and sd.state_machine.soft_disable_timer == 0
stamp += 10_000_000
axis, state, command = drive(304, state_stamp=stamp, **settings(mode, stamp, 305))
assert not state.enabled and not state.active
assert not axis.desiredLongitudinal and not axis.longitudinalActive and not command.longActive
assert not sd.events.contains(fixture.ET.ENABLE) and old.validUntilMonoTime < stamp
@pytest.mark.parametrize('mode', MODES)
def test_lag_clear_before_timeout_requires_current_native_long_regrant(mode):
with callers(mode) as ((sd, _, drive, _, _), old):
stamp = BASE + 10_000_000
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=0.02):
drive(3, state_stamp=stamp, **settings(mode, stamp, 4))
stamp += 10_000_000
options = settings(mode, stamp, 5)
options.update(native_request_long=False, long_allowed=False)
axis, state, command = drive(4, state_stamp=stamp, **options)
assert state.enabled and state.active and state.state == log.SelfdriveState.OpenpilotState.enabled
assert axis.desiredLongitudinal and not axis.longitudinalActive and not command.longActive
assert not sd.events.contains(fixture.ET.ENABLE)
stamp += 10_000_000
axis, state, command = drive(5, state_stamp=stamp, **settings(mode, stamp, 6))
assert state.enabled and axis.longitudinalActive and command.longActive
assert not sd.events.contains(fixture.ET.ENABLE) and old.validUntilMonoTime <= stamp
def test_independent_lat_only_lag_stays_off_without_ordinary_enable_then_recovers():
mode = 'lateral_only'
with callers(mode) as ((sd, _, drive, card, receive), _):
stamp = BASE + 10_000_000
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=0.02):
axis, state, command = drive(3, state_stamp=stamp, **settings(mode, stamp, 4))
assert not state.enabled and not state.active and not axis.lateralActive and not command.latActive
assert not axis.longitudinalActive and not command.longActive
assert receive(stamp, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
assert card.aol_card_intent.allowed_latch
stamp += 10_000_000
axis, state, command = drive(4, state_stamp=stamp, **settings(mode, stamp, 5))
assert axis.lateralActive and command.latActive
assert not state.enabled and not axis.longitudinalActive and not command.longActive
assert not sd.events.contains(fixture.ET.ENABLE)
@pytest.mark.parametrize('fault', ['native_invalid', 'native_lat_denied', 'native_long_denied',
'native_session', 'can_invalid', 'permanent_eps', 'dm_three', 'dm_lockout', 'mixed_immediate'])
def test_fresh_lag_does_not_waive_fault_dm_or_native_gates(fault):
mode = 'combined'
with callers(mode) as ((sd, _, drive, _, _), _):
# DM delivery is intercepted at the actual20Hz envelope boundary before consumers run.
for tick in (3, 4):
stamp = BASE + (tick - 2) * 10_000_000
drive(tick, state_stamp=stamp, **settings(mode, stamp, tick + 1))
stamp = BASE + 30_000_000
original_update = sd.sm.update_msgs
delivered = []
def update(timestamp, messages):
frames = []
for message in messages:
if message.which() == 'driverMonitoringState' and fault in ('dm_three', 'dm_lockout'):
message = message.as_builder()
message.driverMonitoringState.activePolicy = 'vision'
if fault == 'dm_three':
message.driverMonitoringState.alertLevel = 'three'
else:
message.driverMonitoringState.lockout = True
delivered.append(int(message.logMonoTime))
message = message.as_reader()
elif message.which() == 'aolSafetyWire' and fault == 'native_session':
message = message.as_builder()
message.aolSafetyWire = encode_safety(replace(decode_safety(message.aolSafetyWire), axisSessionId='other-session'))
message = message.as_reader()
frames.append(message)
return original_update(timestamp, frames)
observed = fixture.car_state()
observed.canValid = fault != 'can_invalid'
observed.steerFaultPermanent = fault == 'permanent_eps'
options = settings(mode, stamp, 6)
options.update(native_valid=fault != 'native_invalid', allowed=fault != 'native_lat_denied',
long_allowed=fault != 'native_long_denied')
original_events = sd.update_events
def events(cs):
result = original_events(cs)
if fault == 'mixed_immediate':
sd.events.add(log.OnroadEvent.EventName.controlsMismatch)
return result
with mock.patch.object(sd.sm, 'update_msgs', side_effect=update), \
mock.patch.object(sd, 'update_events', side_effect=events), \
mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=0.02):
axis, _, command = drive(5, state_stamp=stamp, observed_state=observed, **options)
if fault in ('dm_three', 'dm_lockout'):
assert delivered == [stamp] and sd.aol_availability_long_inhibit
assert not sd.aol_dm_lateral_inhibit and not sd.enabled
assert sd.aol_dm_retained_owner == (sd.aol_last_intent.producerSessionId, sd.aol_session_id)
expected = log.OnroadEvent.EventName.driverDistracted3 if fault == 'dm_three' else log.OnroadEvent.EventName.tooDistracted
assert expected in sd.events.names
assert not axis.lateralActive and not axis.longitudinalActive
assert not command.latActive and not command.longActive
assert not sd.events.contains(fixture.ET.ENABLE)