Files
StarPilot/openpilot/starpilot/tests/test_ford_location_intent.py

175 lines
9.9 KiB
Python

"""Controlled wire boundaries; actual Ford/native caller coverage runs separately."""
from dataclasses import replace
import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR
from openpilot.cereal import messaging
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
from openpilot.starpilot.tests.test_ford_reverse_intent import NOW, PRODUCER, change, native_change, sample
IDENTITIES = (
CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_EDGE_MK2, CAR.FORD_ESCAPE_MK4, CAR.FORD_ESCAPE_MK4_5,
CAR.FORD_EXPLORER_MK6, CAR.FORD_EXPEDITION_MK4, CAR.FORD_F_150_MK14, CAR.FORD_F_150_LIGHTNING_MK1,
CAR.FORD_FOCUS_MK4, CAR.FORD_MONDEO_MK5, CAR.FORD_MAVERICK_MK1, CAR.FORD_MUSTANG_MACH_E_MK1,
CAR.FORD_RANGER_MK2, CAR.FORD_TRANSIT_MK5,
)
def location_case(identity=CAR.FORD_ESCAPE_MK4, alpha=False):
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x5A] = 8
fingerprint[2].update({0x3D6: 8, 0x186: 8})
with OpenpilotPrefix():
cp = CarInterface.get_params(identity, fingerprint, [], alpha, False, False)
cp.alternativeExperience = 32
cs = structs.CarState(canValid=True, canTimeout=False, vEgo=15., gearShifter='drive')
cs.cruiseState.available = True
feedback = TransportPauseFeedback(cp)
healthy, _ = sample(cp, NOW, NOW, 1)
assert not feedback.observe(healthy, cp, cs, NOW, latched=True, intent_producer_session=PRODUCER)
assert feedback.lateral_only_baseline
stamp = NOW + 10_000_000
peers, native = sample(cp, stamp, stamp, 2, active=False, requested=False, allowed=False, event_stamp=stamp)
def withdrawn(axis):
axis.faultReason = 'critical'
axis.nativeAcknowledged = False
change(peers, 'aolAxisState', withdrawn)
events = messaging.new_message('onroadEvents', 2, valid=True, logMonoTime=stamp)
events.onroadEvents = [
{'name': 'pcmDisable', 'userDisable': True},
{'name': 'locationdTemporaryError', 'softDisable': True, 'noEntry': True},
]
peers.messages['onroadEvents'] = events.as_reader()
peers.logMonoTime['onroadEvents'] = stamp
return cp, cs, feedback, peers, native, stamp
@pytest.mark.parametrize('identity', IDENTITIES)
@pytest.mark.parametrize('alpha', [False, True], ids=['stock', 'alpha-after-brake'])
@pytest.mark.parametrize('event_first', [False, True])
def test_location_loss_retains_only_existing_ford_lateral_intent(identity, alpha, event_first):
cp, cs, feedback, peers, _, stamp = location_case(identity, alpha)
baseline = (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
sources = dict(peers.logMonoTime)
if event_first:
early, _ = sample(cp, NOW, stamp, 1)
early.messages['onroadEvents'] = peers.messages['onroadEvents']
early.logMonoTime['onroadEvents'] = stamp
assert not feedback.observe(early, cp, cs, stamp, latched=True, intent_producer_session=PRODUCER)
# The earlier SOFT event is owned by the shared ordinary-warning join.
# The inactive, matched location proof below still uses the host pause owner.
assert feedback.ordinary_warning_qualified and feedback.ordinary_warning_retained
assert not feedback.temporary_host_qualified and not feedback.failed
assert feedback.lateral_only_baseline and feedback.pending_event == 0
assert baseline == (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
assert not feedback.observe(peers, cp, cs, stamp, latched=True, intent_producer_session=PRODUCER)
assert feedback.temporary_host_qualified and not feedback.failed
assert baseline == (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
assert peers.logMonoTime == sources
assert not any((peers['aolAxisState'].desiredLateral, peers['aolAxisState'].lateralActive,
peers['aolAxisState'].desiredLongitudinal, peers['aolAxisState'].longitudinalActive))
recovered, _ = sample(cp, stamp + 10_000_000, stamp + 10_000_000, 3, event_stamp=stamp + 10_000_000)
assert not feedback.observe(recovered, cp, cs, stamp + 10_000_000, latched=True, intent_producer_session=PRODUCER)
assert not feedback.failed and not feedback.temporary_host_qualified
assert feedback.lateral_source_ns == stamp + 10_000_000
@pytest.mark.parametrize('fault', [
'unlatched', 'invalid-can', 'can-timeout', 'main-off', 'brake', 'regen', 'gas', 'door', 'seatbelt',
'permanent-eps', 'temporary-eps', 'reverse', 'unknown-gear', 'explicit-cancel', 'hard-fault',
'producer', 'epoch', 'session', 'future-source', 'stale-source', 'native-rx', 'native-heartbeat',
'interrupt-can2', 'native-identity', 'native-denial', 'long-active', 'aol-off',
])
def test_location_exception_cannot_hide_independent_fault_or_driver_veto(fault):
cp, cs, feedback, peers, native, stamp = location_case()
latched, producer = True, PRODUCER
if fault == 'unlatched':
latched = False
elif fault in ('invalid-can', 'can-timeout', 'main-off', 'brake', 'regen', 'gas', 'door', 'seatbelt',
'permanent-eps', 'temporary-eps', 'reverse', 'unknown-gear'):
if fault == 'main-off':
cs.cruiseState.available = False
elif fault in ('reverse', 'unknown-gear'):
cs.gearShifter = 'reverse' if fault == 'reverse' else 'unknown'
else:
field = {'invalid-can': 'canValid', 'can-timeout': 'canTimeout', 'brake': 'brakePressed',
'regen': 'regenBraking', 'gas': 'gasPressed', 'door': 'doorOpen', 'seatbelt': 'seatbeltUnlatched',
'permanent-eps': 'steerFaultPermanent', 'temporary-eps': 'steerFaultTemporary'}[fault]
setattr(cs, field, fault != 'invalid-can')
elif fault in ('explicit-cancel', 'hard-fault'):
if fault == 'explicit-cancel':
change(peers, 'onroadEvents', lambda events: setattr(events[0], 'name', 'buttonCancel'))
else:
change(peers, 'onroadEvents', lambda events: setattr(events[0], 'immediateDisable', True))
elif fault == 'producer':
producer = 'other-card'
elif fault in ('epoch', 'session', 'future-source', 'stale-source', 'long-active'):
field, value = {'epoch': ('intentIntegrityEpoch', 2), 'session': ('sessionId', 'other-axis'),
'future-source': ('sourceCarStateMonoTime', stamp + 1),
'stale-source': ('sourceCarStateMonoTime', stamp - 30_000_001),
'long-active': ('longitudinalActive', True)}[fault]
change(peers, 'aolAxisState', lambda axis: setattr(axis, field, value))
elif fault == 'native-identity':
native_change(peers, native, pandaSerial='other-panda')
elif fault == 'native-denial':
native_change(peers, native, requestedLateral=True, lateralAllowed=False)
elif fault == 'aol-off':
cp.alternativeExperience = 0
else:
updates = {'native-rx': {'safetyRxChecksInvalid': True}, 'native-heartbeat': {'heartbeatLost': True},
'interrupt-can2': {'faults': 8}}[fault]
native_change(peers, native, pandaInventory=(replace(native.pandaInventory[0], **updates),))
before = dict(vars(feedback))
assert not feedback._temporary_host_intent_current(peers, cp, cs, peers['aolAxisState'], stamp, stamp,
latched=latched, producer=producer)
assert vars(feedback) == before
@pytest.mark.parametrize('fault', ['unlatched', 'invalid-can', 'can-timeout', 'main-off', 'door', 'permanent-eps',
'native-denial', 'native-rx', 'native-heartbeat', 'interrupt-can2',
'producer', 'epoch', 'session', 'expired-native', 'future-event',
'explicit-cancel', 'hard-fault'])
def test_event_first_ordinary_warning_rejects_independent_faults_without_mutation(fault):
cp, cs, feedback, withdrawn, _, stamp = location_case()
early, native = sample(cp, NOW, stamp, 1)
early.messages['onroadEvents'] = withdrawn.messages['onroadEvents']
early.logMonoTime['onroadEvents'] = stamp
latched, producer = fault != 'unlatched', PRODUCER
if fault in ('invalid-can', 'can-timeout', 'main-off', 'door', 'permanent-eps'):
if fault == 'main-off':
cs.cruiseState.available = False
else:
field = {'invalid-can': 'canValid', 'can-timeout': 'canTimeout',
'door': 'doorOpen', 'permanent-eps': 'steerFaultPermanent'}[fault]
setattr(cs, field, fault != 'invalid-can')
elif fault == 'native-denial':
native_change(early, native, requestedLateral=True, lateralAllowed=False)
elif fault in ('native-rx', 'native-heartbeat', 'interrupt-can2'):
updates = {'native-rx': {'safetyRxChecksInvalid': True}, 'native-heartbeat': {'heartbeatLost': True},
'interrupt-can2': {'faults': 8}}[fault]
native_change(early, native, pandaInventory=(replace(native.pandaInventory[0], **updates),))
elif fault == 'producer':
producer = 'foreign-card'
elif fault in ('epoch', 'session'):
field, value = ('intentIntegrityEpoch', 2) if fault == 'epoch' else ('sessionId', 'foreign-axis')
change(early, 'aolAxisState', lambda axis: setattr(axis, field, value))
elif fault == 'expired-native':
native_change(early, native, validUntilMonoTime=stamp - 1)
elif fault == 'future-event':
early.logMonoTime['onroadEvents'] = stamp + 1
elif fault == 'explicit-cancel':
change(early, 'onroadEvents', lambda events: setattr(events[0], 'name', 'buttonCancel'))
elif fault == 'hard-fault':
change(early, 'onroadEvents', lambda events: setattr(events[0], 'immediateDisable', True))
before = dict(vars(feedback))
assert not feedback._ordinary_warning_intent_current(early, cp, cs, early['aolAxisState'], NOW, stamp,
latched=latched, producer=producer)
assert vars(feedback) == before