Files
StarPilot/openpilot/starpilot/aol/tests/test_intent_publication.py
T
2026-10-26 20:38:11 -04:00

646 lines
34 KiB
Python

"""Exercise the real cross-socket wakeup between card and selfdrived."""
import time
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from opendbc.car.structs import car
from opendbc.car import gen_empty_fingerprint
from opendbc.car.honda.interface import CarInterface as HondaInterface
from opendbc.car.honda.values import CAR as HONDA
from openpilot.cereal import messaging
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.car.tests.publication_fixture import initialize_publication_sources
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.starpilot.aol.runtime import current_intent, current_native, decide_axes
from openpilot.starpilot.aol.wire import IntentState, SafetyState, encode_intent, encode_safety
@pytest.mark.parametrize('pause_lateral,qualified,expected', [(False, True, 'combined'), (True, True, 'longitudinalOnly'),
(False, False, 'off')])
def test_card_companion_is_available_when_car_state_wakes_selfdrive(pause_lateral, qualified, expected):
with OpenpilotPrefix():
messaging.reset_context()
pm = messaging.PubMaster(['carState', 'carOutput', 'aolIntentWire', 'aolSafetyWire'])
sd = SelfdriveD.__new__(SelfdriveD)
sd.sm = messaging.SubMaster(['aolIntentWire', 'aolSafetyWire'])
sd.car_state_sock = messaging.sub_sock('carState', timeout=100)
sd.initialized, sd.enabled = True, False
sd.aol_car_state_log_ns, sd.conditional_car_state_valid = 0, False
sd.CS_prev = car.CarState.new_message(canValid=True, gearShifter=car.CarState.GearShifter.drive, vEgo=20.)
stamp = time.monotonic_ns()
prior = stamp - 29_350_000 # Route: 29 ms CAN gap plus 2.7 ms consumer latency.
old = messaging.new_message('aolIntentWire', 0, valid=True)
old.logMonoTime = prior
old.aolIntentWire = encode_intent(IntentState('card', 1, prior, prior, prior + 200_000_000,
True, False, False, True, True))
pm.send('aolIntentWire', old)
sd.sm.update(100)
assert sd.sm.seen['aolIntentWire']
# Native permission is current and unchanged throughout the race.
cp = HondaInterface.get_params(HONDA.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], True, False, False)
cp.safetyConfigs[-1].safetyParam |= 32
cp.alternativeExperience = 1
native = messaging.new_message('aolSafetyWire', 0, valid=True)
native.logMonoTime = stamp
native.aolSafetyWire = encode_safety(SafetyState(1, True, stamp, stamp + 200_000_000,
int(car.CarParams.SafetyModel.hondaBosch), 34, True, True, True, True, 'panda', 'drive'))
pm.send('aolSafetyWire', native)
sd.sm.update(100)
assert sd.sm.seen['aolSafetyWire']
decisions = []
def send(service, message):
pm.send(service, message)
if service == 'carState':
# Force the consumer to run at the earliest possible publication
# boundary, before card can execute its next line. Both receive paths
# are real sockets and data_sample retains its nonblocking SM update.
state = sd.data_sample()
now = stamp + 2_800_000
intent = current_intent(sd.sm, car_state_ns=sd.aol_car_state_log_ns, now_ns=now)
ack = current_native(sd.sm, cp, now_ns=now, axis_session_id='drive')
assert ack is not None
if qualified:
assert intent is not None
assert intent.carStateLogMonoTime == sd.aol_car_state_log_ns == stamp
else:
assert intent is None
decisions.append(decide_axes(standard_lateral=True, standard_longitudinal=True,
intent=intent, native=ack, car_state=state, initialized=True, model_ready=True,
no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=0))
# The original 30 ms contract remains exact; no arrival-side renewal.
assert current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + 30_000_001) is None
card = Car.__new__(Car)
card.__dict__.update(sm=SimpleNamespace(frame=1, all_checks=lambda _: True), pm=SimpleNamespace(send=send),
CP=cp, car_params_published=True, slc_replay=False, curve_replay=False, conditional_replay=False,
last_actuators_output=car.CarControl.Actuators.new_message(), can_rcv_cum_timeout_counter=0,
rk=SimpleNamespace(remaining=0), aol_replay=True, aol_qualified=qualified, aol_sequence=1,
slc_producer_session='card', aol_card_intent=SimpleNamespace(allowed_latch=True, output=lambda _: (True, pause_lateral, False)),
v_cruise_helper=SimpleNamespace(slc_cruise_change=None, toyota=None))
initialize_publication_sources(card)
create = messaging.new_message
def stamped(service, *args, **kwargs):
message = create(service, *args, **kwargs)
if service == 'carState':
message.logMonoTime = stamp
return message
with patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=stamped):
card.state_publish(sd.CS_prev, None)
assert len(decisions) == 1
assert decisions[0].mode == expected
def test_future_companion_only_reuses_same_car_state_original_lease():
with OpenpilotPrefix():
messaging.reset_context()
pm = messaging.PubMaster(['carState', 'aolIntentWire'])
sd = SelfdriveD.__new__(SelfdriveD)
sd.sm = messaging.SubMaster(['aolIntentWire'])
sd.car_state_sock = messaging.sub_sock('carState', timeout=1)
sd.initialized, sd.enabled = True, False
sd.aol_car_state_log_ns, sd.conditional_car_state_valid = 0, False
sd.CS_prev = car.CarState.new_message(canValid=True, gearShifter=car.CarState.GearShifter.drive, vEgo=20.)
stamp = time.monotonic_ns()
def publish_intent(at, *, valid=True, qualified=True, session='card'):
message = messaging.new_message('aolIntentWire', 0, valid=valid)
message.logMonoTime = at
message.aolIntentWire = encode_intent(IntentState(session, 1, at, at, at + 200_000_000,
True, True, False, qualified, True))
pm.send('aolIntentWire', message)
publish_intent(stamp)
state = messaging.new_message('carState', valid=True)
state.logMonoTime, state.carState = stamp, sd.CS_prev
pm.send('carState', state)
sd.CS_prev = sd.data_sample()
previous = current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + 1_000_000)
assert previous is not None
# Card has sent the next intent, but not its carState. Exercise the real
# carState socket timeout and the real nonblocking SubMaster receive.
publish_intent(stamp + 20_000_000)
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=stamp + 23_000_000):
assert sd.data_sample() is sd.CS_prev
assert sd.aol_car_state_log_ns == stamp
assert current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + 23_000_000) is None
assert current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + 23_000_000, previous=previous) == previous
assert current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + 30_000_001, previous=previous) is None
assert current_intent(sd.sm, car_state_ns=stamp - 1, now_ns=stamp + 23_000_000, previous=previous) is None
for changes in ({'valid': False}, {'qualified': False}, {'session': 'new-card'}):
publish_intent(stamp + 20_000_000, **changes)
sd.sm.update(100)
assert current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + 23_000_000, previous=previous) is None
def _queued_publication():
from openpilot.starpilot.aol.intent_companion import IntentCompanion
pm = messaging.PubMaster(['carState', 'carOutput', 'aolIntentWire'])
sd = SelfdriveD.__new__(SelfdriveD)
sd.sm = messaging.SubMaster(['aolIntentWire'])
sd.car_state_sock = messaging.sub_sock('carState', timeout=1)
sd.aol_intent_companion = IntentCompanion(messaging.sub_sock('aolIntentWire', conflate=False))
sd.initialized, sd.enabled = True, False
sd.aol_car_state_log_ns, sd.conditional_car_state_valid = 0, False
sd.CS_prev = car.CarState.new_message(canValid=True, gearShifter=car.CarState.GearShifter.drive, vEgo=20.)
cp = HondaInterface.get_params(HONDA.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], True, False, False)
card = Car.__new__(Car)
card.__dict__.update(sm=SimpleNamespace(frame=1, all_checks=lambda _: True), pm=pm, CP=cp,
car_params_published=True, slc_replay=False, curve_replay=False, conditional_replay=False,
last_actuators_output=car.CarControl.Actuators.new_message(), can_rcv_cum_timeout_counter=0,
rk=SimpleNamespace(remaining=0), aol_replay=True, aol_qualified=True, aol_sequence=0,
slc_producer_session='card', aol_card_intent=SimpleNamespace(allowed_latch=True, output=lambda _: (True, False, False)),
v_cruise_helper=SimpleNamespace(slc_cruise_change=None, toyota=None))
initialize_publication_sources(card)
return card, sd, time.monotonic_ns()
def _publish_card(card, state, stamp):
create = messaging.new_message
def stamped(service, *args, **kwargs):
message = create(service, *args, **kwargs)
if service == 'carState':
message.logMonoTime = stamp
return message
with patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=stamped):
card.state_publish(state, None)
def _paired(sd, source, now):
return current_intent(sd.sm, car_state_ns=source, now_ns=now, companion=sd.aol_intent_companion)
def test_first_seen_queued_car_state_keeps_its_original_card_intent():
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
# A has never been admitted before the conflated consumer advances to B.
_publish_card(card, sd.CS_prev, stamp)
_publish_card(card, sd.CS_prev, stamp + 10_000_000)
sd.sm.update(100)
assert sd.sm.logMonoTime['aolIntentWire'] == stamp + 10_000_000
sd.CS_prev = sd.data_sample()
assert sd.aol_car_state_log_ns == stamp
assert current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + 20_000_000) is None
original = _paired(sd, stamp, stamp + 20_000_000)
assert original is not None
assert original.carStateLogMonoTime == original.observedMonoTime == stamp
assert original.validUntilMonoTime == stamp + 200_000_000
assert original.sequence == 1
assert _paired(sd, stamp - 1, stamp + 20_000_000) is None
assert _paired(sd, stamp + 1, stamp + 20_000_000) is None
assert _paired(sd, stamp, stamp + 30_000_000) == original
assert _paired(sd, stamp, stamp + 30_000_001) is None
def test_queued_next_car_state_is_not_limited_to_previously_admitted_pair():
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
_publish_card(card, sd.CS_prev, stamp)
sd.CS_prev = sd.data_sample()
prior = _paired(sd, stamp, stamp + 1_000_000)
assert prior is not None
for offset in (6_555_568, 17_161_461, 23_116_265):
_publish_card(card, sd.CS_prev, stamp + offset)
sd.sm.update(100)
sd.CS_prev = sd.data_sample()
source = stamp + 6_555_568
now = stamp + 23_336_153
assert sd.aol_car_state_log_ns == source
assert current_intent(sd.sm, car_state_ns=source, now_ns=now, previous=prior) is None
matched = _paired(sd, source, now)
assert matched is not None
assert matched.carStateLogMonoTime == matched.observedMonoTime == source
assert matched.sequence == 2
assert matched.allowedLatch
# Retaining this exact CAN pair preserves its original lease, then expires.
assert _paired(sd, source, source + 30_000_000) == matched
assert _paired(sd, source, source + 30_000_001) is None
@pytest.mark.parametrize('changes', [{'allowedLatch': False}, {'pauseLateral': True}, {'pauseLongitudinal': True},
{'settingsQualified': False}, {'producerSessionId': 'new-card'},
{'valid': False}, {'malformed': True}],
ids=['allowed-off', 'lateral-pause', 'longitudinal-pause', 'settings-off',
'new-session', 'invalid', 'malformed'])
def test_newer_intent_denials_cannot_be_hidden_by_cached_pair(changes):
from dataclasses import replace
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
_publish_card(card, sd.CS_prev, stamp)
sd.CS_prev = sd.data_sample()
original = _paired(sd, stamp, stamp + 1_000_000)
assert original is not None
fields = {key: value for key, value in changes.items() if key not in ('valid', 'malformed')}
at = stamp + 10_000_000
later = replace(original, sequence=2, carStateLogMonoTime=at, observedMonoTime=at,
validUntilMonoTime=at + 200_000_000, **fields)
message = messaging.new_message('aolIntentWire', 0, valid=changes.get('valid', True))
message.logMonoTime = at
message.aolIntentWire = b'invalid' if changes.get('malformed') else encode_intent(later)
card.pm.send('aolIntentWire', message)
sd.sm.update(100)
result = _paired(sd, stamp, stamp + 20_000_000)
if set(changes) & {'settingsQualified', 'producerSessionId', 'valid', 'malformed'}:
assert result is None
else:
assert result is not None
assert result.carStateLogMonoTime == result.observedMonoTime == stamp
assert result.validUntilMonoTime == original.validUntilMonoTime
assert result.allowedLatch == (original.allowedLatch and later.allowedLatch)
assert result.pauseLateral == (original.pauseLateral or later.pauseLateral)
assert result.pauseLongitudinal == (original.pauseLongitudinal or later.pauseLongitudinal)
@pytest.mark.parametrize('malformed_between', [False, True], ids=['contiguous', 'malformed-gap'])
def test_session_restart_cannot_reuse_original_pair(malformed_between):
from dataclasses import replace
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
_publish_card(card, sd.CS_prev, stamp)
sd.CS_prev = sd.data_sample()
original = _paired(sd, stamp, stamp + 1_000_000)
assert original is not None
for offset, session in ((10_000_000, 'new-card'), (20_000_000, 'card')):
if malformed_between:
malformed = messaging.new_message('aolIntentWire', 0, valid=True)
malformed.logMonoTime = stamp + offset - 1_000_000
malformed.aolIntentWire = b'invalid'
card.pm.send('aolIntentWire', malformed)
sd.sm.update(100)
assert _paired(sd, stamp, stamp + offset) is None
at = stamp + offset
message = messaging.new_message('aolIntentWire', 0, valid=True)
message.logMonoTime = at
message.aolIntentWire = encode_intent(replace(original, producerSessionId=session, sequence=1,
carStateLogMonoTime=at, observedMonoTime=at, validUntilMonoTime=at + 200_000_000))
card.pm.send('aolIntentWire', message)
sd.sm.update(100)
assert _paired(sd, stamp, at + 1_000_000) is None
@pytest.mark.parametrize('conflict', ['sequence', 'duplicate', 'invalid_duplicate'],
ids=['sequence', 'duplicate', 'invalid-duplicate'])
def test_sequence_and_duplicate_conflicts_withdraw_cached_intent(conflict):
from dataclasses import replace
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
_publish_card(card, sd.CS_prev, stamp)
sd.CS_prev = sd.data_sample()
original = _paired(sd, stamp, stamp + 1_000_000)
assert original is not None
at = stamp + 10_000_000 if conflict == 'sequence' else stamp
message = messaging.new_message('aolIntentWire', 0, valid=conflict != 'invalid_duplicate')
message.logMonoTime = at
message.aolIntentWire = encode_intent(replace(original, pauseLateral=conflict == 'duplicate',
carStateLogMonoTime=at, observedMonoTime=at, validUntilMonoTime=at + 200_000_000))
card.pm.send('aolIntentWire', message)
sd.sm.update(100)
assert _paired(sd, stamp, stamp + 20_000_000) is None
def test_queue_overflow_is_bounded_and_cannot_grant_authority():
from openpilot.starpilot.aol.intent_companion import MAX_COMPANIONS, MAX_DRAIN
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
for tick in range(MAX_DRAIN + 1):
_publish_card(card, sd.CS_prev, stamp + tick * 100_000)
sd.sm.update(100)
assert _paired(sd, stamp, stamp + 10_000_000) is None
assert len(sd.aol_intent_companion.samples) <= MAX_COMPANIONS
assert _paired(sd, stamp, stamp + 10_000_000) is None
assert len(sd.aol_intent_companion.samples) <= MAX_COMPANIONS
# Evicted older authority cannot be reconstructed by a later backfill.
old = messaging.new_message('aolIntentWire', 0, valid=True)
old.logMonoTime = stamp
old.aolIntentWire = encode_intent(IntentState('card', 1, stamp, stamp, stamp + 200_000_000,
True, False, False, True, True))
card.pm.send('aolIntentWire', old)
sd.sm.update(100)
assert _paired(sd, stamp, stamp + 10_000_000) is None
_publish_card(card, sd.CS_prev, stamp + 4_000_000)
sd.sm.update(100)
assert _paired(sd, stamp + 4_000_000, stamp + 10_000_000) is not None
assert _paired(sd, stamp, stamp + 10_000_000) is None
# Exhausting the drain budget cannot reset the producer's accepted sequence.
at = stamp + 5_000_000
message = messaging.new_message('aolIntentWire', 0, valid=True)
message.logMonoTime = at
message.aolIntentWire = encode_intent(IntentState('card', 1, at, at, at + 200_000_000,
True, False, False, True, True))
card.pm.send('aolIntentWire', message)
sd.sm.update(100)
assert _paired(sd, at, stamp + 10_000_000) is None
def test_true_card_publication_gap_keeps_original_thirty_ms_withdrawal():
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
_publish_card(card, sd.CS_prev, stamp)
sd.CS_prev = sd.data_sample()
assert _paired(sd, stamp, stamp + 28_885_658) is not None
now = stamp + 53_402_671
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now):
assert sd.data_sample() is sd.CS_prev
assert sd.aol_car_state_log_ns == 0
assert not sd.conditional_car_state_valid
assert _paired(sd, sd.aol_car_state_log_ns, now) is None
_publish_card(card, sd.CS_prev, stamp + 80_877_165)
sd.CS_prev = sd.data_sample()
assert _paired(sd, stamp, stamp + 80_877_165) is None
def test_expired_sample_does_not_reset_producer_sequence():
from dataclasses import replace
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
_publish_card(card, sd.CS_prev, stamp)
sd.CS_prev = sd.data_sample()
original = _paired(sd, stamp, stamp + 1_000_000)
assert original is not None
assert _paired(sd, stamp, stamp + 30_000_001) is None
at = stamp + 40_000_000
message = messaging.new_message('aolIntentWire', 0, valid=True)
message.logMonoTime = at
message.aolIntentWire = encode_intent(replace(original, sequence=0, carStateLogMonoTime=at,
observedMonoTime=at, validUntilMonoTime=at + 200_000_000))
card.pm.send('aolIntentWire', message)
sd.sm.update(100)
assert _paired(sd, at, at + 1_000_000) is None
def _known_older_sm_pair(*, latest_changes=None):
from dataclasses import replace
from openpilot.starpilot.aol.intent_companion import IntentCompanion
prior, source, later, now = 1_081_445_990_726, 1_081_451_053_291, 1_081_455_731_297, 1_081_457_492_005
companion = IntentCompanion(object())
original = IntentState('card', 1, prior, prior, prior + 200_000_000, True, False, False, True, True, True)
pair = replace(original, sequence=2, carStateLogMonoTime=source, observedMonoTime=source,
validUntilMonoTime=source + 200_000_000)
latest = replace(original, sequence=3, carStateLogMonoTime=later, observedMonoTime=later,
validUntilMonoTime=later + 200_000_000, **(latest_changes or {}))
for intent in (original, pair, latest):
companion._observe(intent.carStateLogMonoTime, True, encode_intent(intent))
class SM(dict):
pass
sm = SM(aolIntentWire=encode_intent(original))
sm.seen = sm.valid = sm.alive = {'aolIntentWire': True}
sm.logMonoTime = {'aolIntentWire': prior}
return companion, sm, pair, latest, now
@pytest.mark.parametrize('changes', [{}, {'allowedLatch': False}, {'pauseLateral': True},
{'pauseLongitudinal': True}, {'lateralArmed': False}, {'optionalSetRelease': False}])
def test_known_older_sm_pair_folds_all_restrictions_without_renewing_source(changes):
companion, sm, pair, latest, now = _known_older_sm_pair(latest_changes=changes)
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none', return_value=None):
result = current_intent(sm, car_state_ns=pair.carStateLogMonoTime, now_ns=now, companion=companion)
assert result is not None
assert result.carStateLogMonoTime == result.observedMonoTime == pair.carStateLogMonoTime
assert result.validUntilMonoTime == pair.validUntilMonoTime
assert result.allowedLatch == (pair.allowedLatch and latest.allowedLatch)
assert result.lateralArmed == (pair.lateralArmed and latest.lateralArmed)
assert result.pauseLateral == (pair.pauseLateral or latest.pauseLateral)
assert result.pauseLongitudinal == (pair.pauseLongitudinal or latest.pauseLongitudinal)
assert result.optionalSetRelease == (pair.optionalSetRelease and latest.optionalSetRelease)
assert current_intent(sm, car_state_ns=pair.carStateLogMonoTime,
now_ns=pair.carStateLogMonoTime + 30_000_001, companion=companion) is None
@pytest.mark.parametrize('fault', ['invalid', 'malformed', 'conflicting-duplicate', 'session', 'stale', 'unseen-backwards', 'future'])
def test_older_sm_pair_still_rejects_unknown_corrupt_restart_and_expired_samples(fault):
from dataclasses import replace
companion, sm, pair, latest, now = _known_older_sm_pair()
prior = companion.samples[sm.logMonoTime['aolIntentWire']][2]
if fault == 'invalid':
sm.valid = {'aolIntentWire': False}
elif fault == 'malformed':
sm['aolIntentWire'] = b'invalid'
elif fault in ('conflicting-duplicate', 'session'):
sm['aolIntentWire'] = encode_intent(replace(prior, pauseLateral=True) if fault == 'conflicting-duplicate'
else replace(prior, producerSessionId='new-card'))
elif fault == 'stale':
now = prior.carStateLogMonoTime + 30_000_001
else:
at = prior.carStateLogMonoTime - 1 if fault == 'unseen-backwards' else now + 1
sm.logMonoTime = {'aolIntentWire': at}
sm['aolIntentWire'] = encode_intent(replace(prior, sequence=0 if fault == 'unseen-backwards' else 4,
carStateLogMonoTime=at, observedMonoTime=at, validUntilMonoTime=at + 200_000_000))
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none', return_value=None):
assert current_intent(sm, car_state_ns=pair.carStateLogMonoTime, now_ns=now, companion=companion) is None
def _verified_stale_sm_projection(*, restrictions=None, verified=True, latest_changes=None):
from dataclasses import replace
from openpilot.starpilot.aol.intent_companion import IntentCompanion
c, a, b, before, now = 341_047_855_007, 341_055_154_287, 341_083_039_471, 341_059_508_728, 341_084_947_345
original = IntentState('card', 1, c, c, c + 200_000_000, True, False, False, True, True, True)
projection = replace(original, **(restrictions or {}))
pair = replace(original, sequence=2, carStateLogMonoTime=a, observedMonoTime=a, validUntilMonoTime=a + 200_000_000)
latest = replace(original, sequence=3, carStateLogMonoTime=b, observedMonoTime=b, validUntilMonoTime=b + 200_000_000)
latest = replace(latest, **(latest_changes or {}))
companion = IntentCompanion(object())
for intent in (projection, pair):
companion._observe(intent.carStateLogMonoTime, True, encode_intent(intent))
class SM(dict):
pass
sm = SM(aolIntentWire=encode_intent(projection))
sm.seen, sm.valid, sm.alive = ({'aolIntentWire': True} for _ in range(3))
sm.logMonoTime = {'aolIntentWire': c}
if verified:
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none', return_value=None):
assert current_intent(sm, car_state_ns=a, now_ns=before, companion=companion) is not None
companion._observe(b, True, encode_intent(latest))
return companion, sm, projection, pair, latest, now
@pytest.mark.parametrize('restrictions', [{}, {'allowedLatch': False}, {'pauseLateral': True},
{'pauseLongitudinal': True}, {'lateralArmed': False}, {'optionalSetRelease': False}])
def test_verified_stale_sm_identity_only_folds_restrictions_without_renewing_raw_lease(restrictions):
companion, sm, projection, pair, latest, now = _verified_stale_sm_projection(restrictions=restrictions)
epoch, verified = companion.integrity_epoch, companion.verified_sm_projection
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none', return_value=None):
for at in (now, now + 1, pair.carStateLogMonoTime + 30_000_000):
result = current_intent(sm, car_state_ns=pair.carStateLogMonoTime, now_ns=at, companion=companion)
assert result is not None
assert result.carStateLogMonoTime == result.observedMonoTime == pair.carStateLogMonoTime
assert result.validUntilMonoTime == pair.validUntilMonoTime
assert result.allowedLatch == (pair.allowedLatch and latest.allowedLatch and projection.allowedLatch)
assert result.lateralArmed == (pair.lateralArmed and latest.lateralArmed and projection.lateralArmed)
assert result.pauseLateral == (pair.pauseLateral or latest.pauseLateral or projection.pauseLateral)
assert result.pauseLongitudinal == (pair.pauseLongitudinal or latest.pauseLongitudinal or projection.pauseLongitudinal)
assert result.optionalSetRelease == (pair.optionalSetRelease and latest.optionalSetRelease and projection.optionalSetRelease)
assert companion.integrity_epoch == epoch and companion.verified_sm_projection == verified
assert projection.carStateLogMonoTime not in companion.samples
assert current_intent(sm, car_state_ns=pair.carStateLogMonoTime,
now_ns=pair.carStateLogMonoTime + 30_000_001, companion=companion) is None
@pytest.mark.parametrize('fault', ['unverified', 'invalid', 'dead', 'missing', 'malformed', 'changed', 'settings', 'session',
'unknown-old', 'newer-invalid', 'future', 'restart', 'epoch', 'sequence',
'source-expired', 'latest-expired', 'latest-invalid', 'overflow'])
def test_verified_stale_sm_identity_never_admits_unverified_or_corrupt_current_inputs(fault):
from dataclasses import replace
from types import SimpleNamespace
from openpilot.starpilot.aol.intent_companion import MAX_DRAIN
companion, sm, projection, pair, latest, now = _verified_stale_sm_projection(verified=fault != 'unverified',
latest_changes={'validUntilMonoTime': 341_084_947_344} if fault == 'latest-expired' else None)
messages = None
if fault == 'invalid':
sm.valid['aolIntentWire'] = False
elif fault == 'dead':
sm.alive['aolIntentWire'] = False
elif fault == 'missing':
sm.seen['aolIntentWire'] = False
elif fault == 'malformed':
sm['aolIntentWire'] = b'invalid'
elif fault in ('changed', 'settings', 'session'):
changes = {'pauseLateral': True} if fault == 'changed' else {'settingsQualified': False} if fault == 'settings' else {'producerSessionId': 'other-card'}
sm['aolIntentWire'] = encode_intent(replace(projection, **changes))
elif fault in ('unknown-old', 'newer-invalid', 'future'):
at = projection.carStateLogMonoTime - 1 if fault == 'unknown-old' else now + 1 if fault == 'future' else latest.carStateLogMonoTime
sm.logMonoTime['aolIntentWire'] = at
sm['aolIntentWire'] = encode_intent(replace(projection, sequence=4, carStateLogMonoTime=at,
observedMonoTime=at, validUntilMonoTime=at + 200_000_000))
if fault == 'newer-invalid':
sm.valid['aolIntentWire'] = False
elif fault == 'restart':
companion._observe(latest.carStateLogMonoTime + 1, True, encode_intent(replace(latest,
producerSessionId='restarted-card', sequence=1, carStateLogMonoTime=latest.carStateLogMonoTime + 1)))
elif fault == 'epoch':
companion._observe(projection.carStateLogMonoTime - 1, False, b'invalid')
elif fault == 'sequence':
companion._observe(latest.carStateLogMonoTime + 1, True, encode_intent(replace(latest,
sequence=1, carStateLogMonoTime=latest.carStateLogMonoTime + 1)))
elif fault == 'source-expired':
now = pair.carStateLogMonoTime + 30_000_001
elif fault == 'latest-invalid':
companion._observe(latest.carStateLogMonoTime, False, encode_intent(latest))
elif fault == 'overflow':
messages = [SimpleNamespace(logMonoTime=latest.carStateLogMonoTime, valid=True, aolIntentWire=encode_intent(latest))] * MAX_DRAIN
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none', side_effect=messages, return_value=None):
assert current_intent(sm, car_state_ns=pair.carStateLogMonoTime, now_ns=now, companion=companion) is None
if fault in ('restart', 'epoch', 'sequence', 'overflow', 'latest-invalid'):
assert companion.verified_sm_projection is None
@pytest.mark.parametrize('changes', [{'allowedLatch': False}, {'pauseLateral': True}, {'pauseLongitudinal': True},
{'lateralArmed': False}, {'optionalSetRelease': False}])
def test_verified_stale_sm_identity_cannot_override_new_latest_raw_restrictions(changes):
companion, sm, _, pair, latest, now = _verified_stale_sm_projection(latest_changes=changes)
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none', return_value=None):
result = current_intent(sm, car_state_ns=pair.carStateLogMonoTime, now_ns=now, companion=companion)
assert result is not None
for key, value in changes.items():
assert getattr(result, key) == value
assert result.carStateLogMonoTime == pair.carStateLogMonoTime and result.validUntilMonoTime == pair.validUntilMonoTime
@pytest.mark.parametrize('at,accepted', [(28_000_000, True), (30_000_000, True), (30_000_001, False)])
def test_drain_then_decision_clock_keeps_original_card_source_lease(at, accepted):
# Real Card publisher, SelfdriveD retained source and queued intent sockets.
with OpenpilotPrefix():
messaging.reset_context()
card, sd, stamp = _queued_publication()
_publish_card(card, sd.CS_prev, stamp)
sd.CS_prev = sd.data_sample()
assert _paired(sd, stamp, stamp + 1_000_000) is not None
assert sd.aol_car_state_log_ns == stamp
pre_read_clock = stamp + 25_000_000
_publish_card(card, sd.CS_prev, stamp + 26_000_000)
# Keep original carState A while raw intent B arrives after pre-read time.
assert sd.aol_intent_companion.drain()
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none',
side_effect=AssertionError('Decision validation must not read after clock sampling')):
result = current_intent(sd.sm, car_state_ns=stamp, now_ns=stamp + at,
companion=sd.aol_intent_companion, drain=False)
assert pre_read_clock < stamp + 26_000_000
assert (result is not None) == accepted
if accepted:
assert result.carStateLogMonoTime == result.observedMonoTime == stamp
assert result.validUntilMonoTime == stamp + 200_000_000
def test_split_drain_preserves_overrun_fail_closed():
from dataclasses import replace
from openpilot.starpilot.aol.intent_companion import MAX_DRAIN
companion, sm, pair, latest, now = _known_older_sm_pair()
message = SimpleNamespace(logMonoTime=latest.carStateLogMonoTime, valid=True,
aolIntentWire=encode_intent(latest))
with patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none', return_value=message) as recv:
assert companion.drain() is False
assert recv.call_count == MAX_DRAIN
assert current_intent(sm, car_state_ns=pair.carStateLogMonoTime, now_ns=now,
companion=companion, drain=False) is None
newer = replace(latest, sequence=latest.sequence + 1, carStateLogMonoTime=now,
observedMonoTime=now, validUntilMonoTime=now + 200_000_000)
sm['aolIntentWire'] = encode_intent(newer)
sm.logMonoTime['aolIntentWire'] = now
assert current_intent(sm, car_state_ns=now, now_ns=now, companion=companion, drain=False) is None
assert companion.last_rejection == 'drain_overrun'
def test_late_car_state_after_submaster_update_preserves_batch_and_source():
from types import SimpleNamespace
for kind in ('fresh', 'timeout', 'invalid', 'reordered', 'full'):
sd = SelfdriveD.__new__(SelfdriveD)
sd.aol_replay = sd.axis_transport_required = True
sd.initialized, sd.enabled = True, False
sd.car_state_sock = object()
sd.CS_prev = car.CarState.new_message(canValid=True)
sd.aol_car_state_log_ns = 10_000_000_000
sd.conditional_car_state_valid = True
first = messaging.new_message('carState', valid=True, logMonoTime=10_010_000_000)
first.carState = car.CarState.new_message(canValid=True, brakePressed=True)
late = messaging.new_message('carState', valid=kind != 'invalid', logMonoTime=10_020_000_000)
late.carState = car.CarState.new_message(canValid=True, brakePressed=False)
if kind == 'reordered':
late.logMonoTime = first.logMonoTime
queue = []
def update(_, queue=queue, late=late, kind=kind):
queue.extend([late] if kind != 'full' else [late] * 32)
def receive(_, queue=queue):
return queue.pop(0) if queue else None
with patch.object(sd, 'sm', SimpleNamespace(update=update), create=True), \
patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False), \
patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=None if kind == 'timeout' else first), \
patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive), \
patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=10_025_000_000):
selected = sd.data_sample()
assert selected.brakePressed is False
assert len(sd.car_state_samples) <= 32
if kind in ('fresh', 'timeout'):
assert sd.conditional_car_state_valid
assert sd.aol_car_state_log_ns == late.logMonoTime
else:
assert not sd.conditional_car_state_valid
assert sd.aol_car_state_log_ns == 0
if kind != 'timeout':
assert sd.car_state_samples[0][0].brakePressed