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

220 lines
12 KiB
Python

from collections import deque
from contextlib import ExitStack
from unittest import mock
import pytest
from openpilot.cereal import log, messaging
from openpilot.common.prefix import OpenpilotPrefix
from opendbc.car.structs import car
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
from openpilot.starpilot.aol.tests.test_carstate_queue import message
from openpilot.starpilot.aol.wire import IntentState, encode_intent
BASE = 10_020_000_000
def intent(stamp, sequence, *, latched=True):
wire = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=stamp)
wire.aolIntentWire = encode_intent(IntentState('authority-card', sequence, stamp, stamp,
stamp + 200_000_000, latched, False, False, True, latched))
return wire
@pytest.mark.parametrize('pulse', ['healthy', 'cancel', 'brake', 'permanent_eps', 'temporary_eps', 'invalid_envelope', 'reordered'])
def test_late_carstate_after_event_preamble_recomputes_exact_source_once(pulse):
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
with OpenpilotPrefix():
sd, _, step, _, _ = case._ioniq_availability_callers()
step(2, offset_ns=4_012_592, state_stamp=BASE, intent_message=intent(BASE, 3))
a, b, now = BASE, BASE + 27_819_143, BASE + 30_721_946
late = fixture.car_state()
late.vEgo = 25.0
if pulse == 'cancel':
late.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
elif pulse == 'brake':
late.brakePressed = True
elif pulse == 'permanent_eps':
late.steerFaultPermanent = True
elif pulse == 'temporary_eps':
late.steerFaultTemporary = late.standstill = True
queued = deque()
original_status = sd.update_big_model_status
original_receive = messaging.recv_one_or_none
def finish_preamble():
result = original_status()
queued.append(message(a - 1 if pulse == 'reordered' else b, late, valid=pulse != 'invalid_envelope'))
return result
def receive(sock):
return queued.popleft() if sock is sd.car_state_sock and queued else (
None if sock is sd.car_state_sock else original_receive(sock))
with mock.patch.object(sd, 'update_big_model_status', side_effect=finish_preamble), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive), \
mock.patch.object(sd.car_events, 'update', wraps=sd.car_events.update) as car_events, \
mock.patch.object(sd.excessive_actuation_check, 'update', wraps=sd.excessive_actuation_check.update) as excessive, \
mock.patch.object(sd.state_machine, 'update', wraps=sd.state_machine.update) as transition:
axis, _, command = step(5, offset_ns=721_946, state_stamp=a, intent_message=intent(b, 4))
assert now - a == 30_721_946 and now - b == 2_902_803
assert not queued and sd.CS_prev.vEgo == 25.0
assert car_events.call_count == 2 and transition.call_count == 1
assert excessive.call_count == (1 if sd.calibrated_pose is not None else 0)
if pulse == 'healthy':
assert axis.sourceCarStateMonoTime == b and sd.aol_last_intent.carStateLogMonoTime == b
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
assert sd.enabled and sd.active and not sd.aol_authority_lost
else:
if pulse == 'temporary_eps':
assert axis.longitudinalActive and command.longActive and sd.enabled and sd.active
else:
assert not axis.longitudinalActive and not command.longActive
if pulse not in ('brake',):
assert not axis.lateralActive and not command.latActive
if pulse in ('invalid_envelope', 'reordered'):
assert axis.sourceCarStateMonoTime == 0 and not sd.conditional_car_state_valid
if pulse == 'temporary_eps':
assert log.OnroadEvent.EventName.steerTempUnavailableSilent in sd.events.names
assert not sd.aol_authority_lost and sd.aol_last_intent.allowedLatch
if pulse in ('cancel', 'brake'):
assert not sd.enabled and not sd.events.contains(fixture.ET.ENABLE)
@pytest.mark.parametrize('outcome', ['next_frame', 'unlatched_next_frame', 'deadline', 'native_fault', 'validation_crosses_deadline',
'withdrawal', 'expired_intent', 'unqualified_intent'])
def test_true_producer_gap_wait_never_extends_original_output_proof(outcome):
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
with OpenpilotPrefix():
sd, controls, step, _, _ = case._ioniq_availability_callers()
latched = outcome != 'unlatched_next_frame'
axis, _, command = step(2, offset_ns=4_266_961, state_stamp=BASE, latched=latched,
intent_message=intent(BASE, 3, latched=latched))
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
deadline = BASE + 34_266_961
assert axis.validUntilMonoTime == deadline
original_sequence = axis.sequence
now = [BASE + 30_254_367]
next_stamp = BASE + 30_582_763
queued, wires = deque(), deque()
original_receive = messaging.recv_one_or_none
def receive(sock):
if sock is sd.car_state_sock:
return queued.popleft() if queued else None
if sock is sd.aol_intent_companion.sock:
return wires.popleft() if wires else None
return original_receive(sock)
def burn_validation(*args, **kwargs):
from openpilot.starpilot.aol.transport_pause import healthy_transport
result = healthy_transport(*args, **kwargs)
now[0] = deadline + 1
return result
def poll(timeout):
assert 0 < timeout <= (deadline - now[0]) // 1_000_000
assert sd.pm.messages['aolAxisState'].aolAxisState.sequence == original_sequence
assert sd.pm.messages['aolAxisState'].aolAxisState.validUntilMonoTime == deadline
# The already-created real consumer still holds the original envelope;
# a source past30ms does not restamp or extend its publication lease.
leased_command, _ = controls.state_control()
assert leased_command.latActive and leased_command.longActive
if outcome in ('next_frame', 'unlatched_next_frame'):
now[0] = next_stamp + 100_000
queued.append(message(next_stamp))
wires.append(intent(next_stamp, 4, latched=latched).as_reader())
return [sd.car_state_sock]
now[0] = deadline + 1
expired_command, _ = controls.state_control()
assert not expired_command.latActive and not expired_command.longActive
assert sd.pm.messages['aolAxisState'].aolAxisState.validUntilMonoTime == deadline
return []
poller = mock.Mock()
poller.poll.side_effect = poll
sd.car_state_poller = poller
if outcome in ('withdrawal', 'expired_intent', 'unqualified_intent'):
denied = IntentState('authority-card', 4, BASE, BASE,
BASE + (1 if outcome == 'expired_intent' else 200_000_000),
outcome != 'withdrawal', False, False, outcome != 'unqualified_intent', outcome != 'withdrawal')
denied_wire = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=BASE)
denied_wire.aolIntentWire = encode_intent(denied)
wires.append(denied_wire.as_reader())
with ExitStack() as validation:
if outcome == 'validation_crosses_deadline':
validation.enter_context(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.healthy_transport',
side_effect=burn_validation))
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', side_effect=lambda: now[0]), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive), \
mock.patch.object(sd.state_machine, 'update', wraps=sd.state_machine.update) as transition:
failed_axis, _, failed_command = step(5, timeout=True, offset_ns=254_367,
intent_message=intent(BASE, 3, latched=latched), latched=latched, allowed=outcome != 'native_fault')
assert transition.call_count == 1
assert poller.poll.call_count == (1 if outcome in ('next_frame', 'unlatched_next_frame', 'deadline') else 0)
if outcome in ('next_frame', 'unlatched_next_frame'):
assert failed_axis.sourceCarStateMonoTime == next_stamp
assert sd.aol_last_intent.carStateLogMonoTime == next_stamp
assert failed_axis.lateralActive and failed_axis.longitudinalActive
assert failed_command.latActive and failed_command.longActive and sd.enabled
assert failed_axis.observedMonoTime == now[0]
assert failed_axis.validUntilMonoTime == now[0] + 30_000_000
assert failed_axis.sequence == original_sequence + 1
assert sd.aol_last_intent.allowedLatch == latched and sd.aol_last_intent.lateralArmed == latched
else:
assert not failed_axis.lateralActive and not failed_axis.longitudinalActive
assert not failed_command.latActive and not failed_command.longActive
if outcome in ('deadline', 'validation_crosses_deadline'):
fixture.assert_ordinary_source_pause(sd)
recovered, _, recovered_command = step(7, native_request_long=True)
fixture.assert_fresh_ordinary_recovery(sd, recovered, recovered_command, BASE + 50_000_000)
assert axis.validUntilMonoTime == deadline
else:
assert not sd.enabled
recovered, _, recovered_command = step(7, native_request_long=True)
assert not recovered.longitudinalActive and not recovered_command.longActive and not sd.enabled
def test_fresh_late_source_expiring_during_remaining_event_work_still_withdraws():
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
with OpenpilotPrefix():
sd, _, step, _, _ = case._ioniq_availability_callers()
step(2, offset_ns=4_012_592, state_stamp=BASE, intent_message=intent(BASE, 3))
b = BASE + 27_819_143
now = [BASE + 30_721_946]
queued = deque()
original_status = sd.update_big_model_status
original_receive = messaging.recv_one_or_none
original_check = sd.excessive_actuation_check.update
def finish_preamble():
result = original_status()
queued.append(message(b))
return result
def receive(sock):
return queued.popleft() if sock is sd.car_state_sock and queued else (
None if sock is sd.car_state_sock else original_receive(sock))
def consume_remaining_budget(*args, **kwargs):
result = original_check(*args, **kwargs)
now[0] = b + 30_000_001
return result
with mock.patch.object(sd, 'update_big_model_status', side_effect=finish_preamble), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', side_effect=lambda: now[0]), \
mock.patch.object(sd.excessive_actuation_check, 'update', side_effect=consume_remaining_budget):
axis, _, command = step(5, offset_ns=721_946, state_stamp=BASE, intent_message=intent(b, 4))
assert not queued and now[0] - b == 30_000_001
assert axis.sourceCarStateMonoTime == 0
assert not axis.lateralActive and not axis.longitudinalActive and not command.latActive and not command.longActive
assert not sd.enabled