mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
80fbb4f6db
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>
220 lines
12 KiB
Python
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
|