mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
f11f2357d8
Reuse the existing original proof deadline for qualified Mach-E safety19 only. Require fresh independent authority for successors and retain fault, cancel, identity and expiry checks.
243 lines
12 KiB
Python
243 lines
12 KiB
Python
"""Ford2b recorded held-brake source cutoff; private actual-caller reproduction."""
|
|
from contextlib import ExitStack
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from opendbc.car import gen_empty_fingerprint
|
|
from opendbc.car.ford.interface import CarInterface
|
|
from opendbc.car.ford.values import CAR
|
|
from openpilot.cereal import log, messaging
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
|
|
from openpilot.starpilot.aol.tests.test_ordinary_source_availability import settings
|
|
|
|
SOURCE=10_020_000_000
|
|
PUBLISHED_OFFSET=5_920_000
|
|
SOURCE_AGE=33_452_000
|
|
|
|
def profile():
|
|
fingerprint = gen_empty_fingerprint()
|
|
fingerprint[0].update({0x5A: 8, 0x3A6: 8, 0x3A7: 8})
|
|
fingerprint[2].update({0x3D6: 8, 0x186: 8})
|
|
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint, [], True, False, False)
|
|
cp.alternativeExperience = 32
|
|
assert cp.flags == 3 and cp.safetyConfigs[0].safetyParam == 19
|
|
return cp
|
|
|
|
def callers(case):
|
|
sd, controls, drive = case._authority_callers(False, cp=profile(), genuine_events=True)
|
|
def stationary(tick, **kwargs):
|
|
state = kwargs.get('observed_state')
|
|
if state is None:
|
|
state = fixture.car_state(brake=kwargs.get('brake', False))
|
|
state.vEgo = state.vEgoRaw = 0.
|
|
state.standstill = True
|
|
kwargs['observed_state'] = state
|
|
return drive(tick, **kwargs)
|
|
return sd, controls, stationary
|
|
|
|
@pytest.mark.parametrize('socket_timeout', [False, True])
|
|
@pytest.mark.parametrize('published_offset,source_age', [(PUBLISHED_OFFSET, SOURCE_AGE)])
|
|
def test_ford_held_brake_keeps_only_the_recorded_unexpired_proof( socket_timeout, published_offset, source_age):
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, controls, drive = callers(case)
|
|
for tick in (0, 1):
|
|
stamp = 10_000_000_000 + tick * 10_000_000
|
|
drive(tick, brake=True, long_allowed=False, **settings('lateral_only', stamp, tick + 1))
|
|
old, state, command = drive(2, state_stamp=SOURCE, offset_ns=published_offset, brake=True,
|
|
long_allowed=False, **settings('lateral_only', SOURCE, 3))
|
|
assert old.lateralActive and command.latActive and not old.longitudinalActive and not command.longActive
|
|
assert not state.enabled
|
|
deadline = old.validUntilMonoTime
|
|
assert deadline == SOURCE + published_offset + 30_000_000
|
|
old_data = old.to_dict()
|
|
sd.car_state_poller = mock.Mock()
|
|
sd.car_state_poller.poll.return_value = []
|
|
axis, state, command = drive(5, timeout=socket_timeout, state_stamp=SOURCE, offset_ns=source_age - 30_000_000,
|
|
brake=True, long_allowed=False, **settings('lateral_only', SOURCE, 3))
|
|
assert sd.aol_axis_publication_deferred, 'FORD_HELD_BRAKE_SOURCE_WAIT_EXCLUDED'
|
|
assert axis.to_dict() == old_data and axis.validUntilMonoTime == deadline
|
|
assert not state.enabled and not command.longActive
|
|
assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names
|
|
with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=deadline + 1):
|
|
expired, _ = controls.state_control()
|
|
assert not expired.latActive and not expired.longActive
|
|
fresh = SOURCE + 40_000_000
|
|
axis, state, command = drive(6, state_stamp=fresh, brake=True,
|
|
long_allowed=False, **settings('lateral_only', fresh, 4))
|
|
assert axis.lateralActive and command.latActive and not state.enabled and not command.longActive, (axis.to_dict(), sd.events.names, sd.aol_axis_decision)
|
|
|
|
|
|
@pytest.mark.parametrize('fault', ['new_brake', 'lateral_denied', 'rx', 'steer_fault', 'source_invalid'])
|
|
def test_ford_held_brake_extension_does_not_preserve_a_new_fault(fault):
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive = callers(case)
|
|
held = fault != 'new_brake'
|
|
for tick in (0, 1, 2):
|
|
stamp = 10_000_000_000 + tick * 10_000_000
|
|
drive(tick, brake=held, state_stamp=stamp, offset_ns=PUBLISHED_OFFSET if tick == 2 else 0,
|
|
long_allowed=False, **settings('lateral_only', stamp, tick + 1))
|
|
sd.car_state_poller = mock.Mock()
|
|
sd.car_state_poller.poll.return_value = []
|
|
options = settings('lateral_only', SOURCE, 3)
|
|
changes = {}
|
|
if fault == 'lateral_denied':
|
|
options['allowed'] = False
|
|
elif fault == 'rx':
|
|
changes['panda_fault'] = True
|
|
elif fault in ('steer_fault', 'source_invalid'):
|
|
state = fixture.car_state(brake=True)
|
|
if fault == 'steer_fault':
|
|
state.steerFaultPermanent = True
|
|
else:
|
|
state.canValid = False
|
|
changes['observed_state'] = state
|
|
_, _, command = drive(5, state_stamp=SOURCE, offset_ns=SOURCE_AGE - 30_000_000,
|
|
brake=True, long_allowed=False, **options, **changes)
|
|
assert not sd.aol_axis_publication_deferred
|
|
assert not command.longActive
|
|
|
|
|
|
@pytest.mark.parametrize('corruption', ['retained_valid', 'retained_source', 'queue_invalid', 'expired', 'native_denied', 'native_rx'])
|
|
def test_timeout_cannot_borrow_an_untrusted_or_expired_receipt(corruption):
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive = callers(case)
|
|
for tick in (0, 1, 2):
|
|
stamp = 10_000_000_000 + tick * 10_000_000
|
|
old, _, command = drive(tick, brake=True, long_allowed=False, state_stamp=stamp,
|
|
offset_ns=5_631_474 if tick == 2 else 0, **settings('lateral_only', stamp, tick + 1))
|
|
assert old.lateralActive and command.latActive
|
|
sd.car_state_poller = mock.Mock()
|
|
sd.car_state_poller.poll.return_value = []
|
|
original = sd.defer_expired_axis_publication
|
|
def corrupt_receipt(cs, initial_state, **kwargs):
|
|
if corruption == 'retained_valid':
|
|
sd.car_state_retained_valid = False
|
|
elif corruption == 'retained_source':
|
|
sd.car_state_retained_source_ns -= 1
|
|
elif corruption == 'queue_invalid':
|
|
sd.car_state_queue_invalid = True
|
|
return original(cs, initial_state, **kwargs)
|
|
options = settings('lateral_only', SOURCE, 3)
|
|
if corruption == 'native_denied':
|
|
options['allowed'] = False
|
|
elif corruption == 'native_rx':
|
|
options['panda_fault'] = True
|
|
with mock.patch.object(sd, 'defer_expired_axis_publication', side_effect=corrupt_receipt) as intercept:
|
|
_, _, command = drive(6 if corruption == 'expired' else 5, timeout=True, state_stamp=SOURCE,
|
|
offset_ns=0 if corruption == 'expired' else 894_772, brake=True, long_allowed=False, **options)
|
|
if corruption in ('retained_valid', 'retained_source', 'queue_invalid'):
|
|
assert intercept.called
|
|
assert not sd.aol_axis_publication_deferred
|
|
assert not command.latActive and not command.longActive
|
|
|
|
@pytest.mark.parametrize('fault', ['relay', 'native_invalid', 'new_gas', 'button', 'producer', 'session', 'identity', 'safety_word'])
|
|
def test_ford_hold_does_not_borrow_changed_authority(fault):
|
|
from dataclasses import replace
|
|
|
|
from openpilot.starpilot.aol.wire import decode_intent, encode_intent
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive = callers(case)
|
|
for tick in (0, 1, 2):
|
|
stamp = 10_000_000_000 + tick * 10_000_000
|
|
old, _, command = drive(tick, brake=True, long_allowed=False, state_stamp=stamp,
|
|
offset_ns=PUBLISHED_OFFSET if tick == 2 else 0, **settings('lateral_only', stamp, tick + 1))
|
|
assert old.lateralActive and command.latActive
|
|
sd.car_state_poller = mock.Mock()
|
|
sd.car_state_poller.poll.return_value = []
|
|
options = settings('lateral_only', SOURCE, 3)
|
|
changes = {}
|
|
if fault == 'relay':
|
|
changes['relay_fault'] = True
|
|
elif fault == 'native_invalid':
|
|
changes['native_valid'] = False
|
|
elif fault == 'new_gas':
|
|
changes['gas'] = True
|
|
elif fault == 'button':
|
|
state = fixture.car_state(brake=True)
|
|
state.buttonEvents = [{'type': 'cancel', 'pressed': True}]
|
|
changes['observed_state'] = state
|
|
elif fault == 'producer':
|
|
msg = options['intent_message'].copy()
|
|
value = decode_intent(msg.aolIntentWire)
|
|
msg.logMonoTime = SOURCE + SOURCE_AGE
|
|
msg.aolIntentWire = encode_intent(replace(value, producerSessionId='foreign', sequence=4,
|
|
observedMonoTime=msg.logMonoTime, validUntilMonoTime=msg.logMonoTime+200_000_000))
|
|
options['intent_message'] = msg
|
|
pending = [msg.as_reader()]
|
|
receive = messaging.recv_one_or_none
|
|
def queued(sock):
|
|
if sock is sd.aol_intent_companion.sock:
|
|
return pending.pop(0) if pending else None
|
|
return receive(sock)
|
|
cleanup.enter_context(mock.patch('openpilot.cereal.messaging.recv_one_or_none', side_effect=queued))
|
|
elif fault == 'session':
|
|
sd.aol_session_id = 'foreign'
|
|
else:
|
|
cp = sd.CP.as_builder()
|
|
if fault == 'identity':
|
|
cp.carFingerprint = 'FORD_F_150_MK14'
|
|
else:
|
|
cp.safetyConfigs[0].safetyParam = 18
|
|
sd.CP = cp.as_reader()
|
|
_, _, command = drive(5, state_stamp=SOURCE, offset_ns=SOURCE_AGE-30_000_000,
|
|
brake=True, long_allowed=False, **options, **changes)
|
|
assert not sd.aol_axis_publication_deferred
|
|
assert not command.longActive
|
|
|
|
@pytest.mark.parametrize('edge', ['main_off', 'cancel', 'brake_release'])
|
|
@pytest.mark.parametrize('native_allowed', [False, True])
|
|
def test_fresh_successor_cannot_reuse_a_held_brake_proof_across_new_edges(edge, native_allowed):
|
|
from dataclasses import replace
|
|
|
|
from openpilot.starpilot.aol.wire import decode_intent, encode_intent
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive = callers(case)
|
|
for tick in (0, 1, 2):
|
|
stamp = 10_000_000_000 + tick * 10_000_000
|
|
old, _, command = drive(tick, brake=True, long_allowed=False, state_stamp=stamp,
|
|
offset_ns=PUBLISHED_OFFSET if tick == 2 else 0, **settings('lateral_only', stamp, tick+1))
|
|
assert command.latActive
|
|
old_deadline = old.validUntilMonoTime
|
|
sd.car_state_poller = mock.Mock()
|
|
sd.car_state_poller.poll.return_value = []
|
|
held, _, _ = drive(5, timeout=True, state_stamp=SOURCE, offset_ns=SOURCE_AGE-30_000_000,
|
|
brake=True, long_allowed=False, **settings('lateral_only', SOURCE, 3))
|
|
assert sd.aol_axis_publication_deferred and held.validUntilMonoTime == old_deadline
|
|
fresh = SOURCE+40_000_000
|
|
state = fixture.car_state(brake=edge != 'brake_release')
|
|
if edge != 'brake_release':
|
|
state.buttonEvents = [{'type': 'mainCruise' if edge == 'main_off' else 'cancel', 'pressed': True}]
|
|
options = settings('lateral_only', fresh, 4)
|
|
if edge != 'brake_release':
|
|
wire = options['intent_message']
|
|
wire.aolIntentWire = encode_intent(replace(decode_intent(wire.aolIntentWire), allowedLatch=False, lateralArmed=False))
|
|
options['latched'] = False
|
|
# A fresh physical change cannot borrow the old native permission either.
|
|
options['allowed'] = native_allowed
|
|
axis, _, command = drive(6, state_stamp=fresh, observed_state=state,
|
|
long_allowed=False, **options)
|
|
assert not sd.aol_axis_publication_deferred
|
|
assert not command.longActive and not axis.longitudinalActive
|
|
if edge != 'brake_release' or not native_allowed:
|
|
assert not command.latActive and not axis.lateralActive
|
|
else:
|
|
assert axis.sourceCarStateMonoTime == fresh
|
|
assert axis.validUntilMonoTime != old_deadline
|
|
assert old.validUntilMonoTime == old_deadline < fresh
|