Files
StarPilot/openpilot/starpilot/controllers/tests/test_global_mode_availability.py
T
2026-10-19 16:16:22 -05:00

120 lines
5.5 KiB
Python

"""Generic mode requests retain their original live source and acknowledgement guards."""
from unittest.mock import Mock
import pytest
from opendbc.car.car_helpers import interfaces
from opendbc.car.ford.values import CAR as FORD
from opendbc.car.honda.values import CAR as HONDA
from opendbc.car.mazda.values import CAR as MAZDA
from opendbc.car.nissan.values import CAR as NISSAN
from opendbc.car.subaru.values import CAR as SUBARU
from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.volkswagen.values import CAR as VW
from openpilot.cereal import messaging
from openpilot.starpilot.controllers.mode_actions import (
ModeActionOwner, ModeActionPublisher, ModeIntent, SwitchbackStatusOwner,
authority, producer_available, publish_switchback,
)
from openpilot.starpilot.controllers.tests.test_mode_actions import DRIVE, NOW, fixture, request
VEHICLES = (TOYOTA.TOYOTA_RAV4_TSS2, HONDA.HONDA_CIVIC, MAZDA.MAZDA_CX5_2022,
SUBARU.SUBARU_IMPREZA, FORD.FORD_ESCAPE_MK4, NISSAN.NISSAN_XTRAIL,
VW.VOLKSWAGEN_GOLF_MK7)
def prepared(candidate):
sm, _ = fixture()
cp = interfaces[candidate].get_non_essential_params(candidate)
# Lateral-only operation is allowed even when the car owns longitudinal.
cp.openpilotLongitudinalControl = False
sm['carControl'].longActive = sm['carControl'].enabled = False
sm['selfdriveState'].enabled = False
heartbeat = publish_switchback(None, ModeIntent(DRIVE, False, False), session='b' * 32,
sequence=1, now_ns=NOW, source_car_control_ns=sm.logMonoTime['carControl'])
sm['slcState'] = heartbeat.slcState
for name in ('seen', 'valid', 'alive'):
getattr(sm, name)['slcState'] = True
sm.logMonoTime['slcState'], sm.recv_time['slcState'] = NOW, NOW / 1e9
return sm, cp
@pytest.mark.parametrize('candidate', VEHICLES)
def test_generic_switchback_publisher_owner_and_status_accept_actual_vehicle(candidate):
sm, cp = prepared(candidate)
sender, publisher, owner = Mock(), ModeActionPublisher(), ModeActionOwner()
assert authority(sm, cp, NOW, 'switchback')
assert producer_available(sm, now_ns=NOW)
assert publisher.dispatch('switchback', sm, cp, sender, now_ns=NOW)
event = messaging.log_from_bytes(sender.send.call_args.args[1].to_bytes())
assert event.slcAction.controllerMode.carFingerprint == cp.carFingerprint
assert owner.update(event, sm, cp, now_ns=NOW)
assert not owner.update(event, sm, cp, now_ns=NOW) # No duplicate toggle.
intent = owner.sample('switchback', sm, cp, now_ns=NOW)
assert intent.requested and intent.effective
state = publish_switchback(None, intent, session='c' * 32, sequence=1,
now_ns=NOW, source_car_control_ns=sm.logMonoTime['carControl'])
serialized = messaging.log_from_bytes(state.to_bytes())
assert SwitchbackStatusOwner().sample(serialized, drive_id=DRIVE, now_ns=NOW)
assert not SwitchbackStatusOwner().sample(serialized, drive_id=DRIVE, now_ns=NOW + 100_000_001)
sm['carControl'].latActive = False
paused = owner.sample('switchback', sm, cp, now_ns=NOW)
assert paused.requested and not paused.effective
assert not publisher.dispatch('traffic', sm, cp, sender, now_ns=NOW + 101_000_000)
@pytest.mark.parametrize('fault', ('passive', 'dashcamOnly', 'notCar', 'canInvalid', 'canTimeout',
'latInactive', 'offroad', 'carStateStale', 'carStateInvalid',
'carControlStale', 'carControlUnseen', 'deviceStale', 'producerStale',
'producerWrongDrive'))
def test_generic_switchback_still_rejects_live_faults_and_missing_producer(fault):
sm, cp = prepared(TOYOTA.TOYOTA_RAV4_TSS2)
if fault in ('passive', 'dashcamOnly', 'notCar'):
setattr(cp, fault, True)
elif fault == 'canInvalid':
sm['carState'].canValid = False
elif fault == 'canTimeout':
sm['carState'].canTimeout = True
elif fault == 'latInactive':
sm['carControl'].latActive = False
elif fault == 'offroad':
sm['deviceState'].started = False
elif fault == 'carStateStale':
sm.logMonoTime['carState'] = NOW - 250_000_001
elif fault == 'carStateInvalid':
sm.valid['carState'] = False
elif fault == 'carControlStale':
sm.logMonoTime['carControl'] = NOW - 250_000_001
elif fault == 'carControlUnseen':
sm.seen['carControl'] = False
elif fault == 'deviceStale':
sm.logMonoTime['deviceState'] = NOW - 1_000_000_001
elif fault == 'producerStale':
sm.logMonoTime['slcState'] = NOW - 100_000_001
else:
sm['slcState'].switchbackMode.driveStartMonoTime = DRIVE + 1
sender = Mock()
assert not ModeActionPublisher().dispatch('switchback', sm, cp, sender, now_ns=NOW)
sender.send.assert_not_called()
def test_generic_mode_owner_rejects_wrong_identity_expiry_and_inactive_traffic():
sm, cp = prepared(TOYOTA.TOYOTA_RAV4_TSS2)
event = request(sm, cp, 'switchbackModeToggle')
wrong = request(sm, cp, 'switchbackModeToggle')
wrong.slcAction.controllerMode.carFingerprint = 'another-car'
assert not ModeActionOwner().update(wrong, sm, cp, now_ns=NOW)
assert not ModeActionOwner().update(event, sm, cp, now_ns=NOW + 250_000_001)
assert not ModeActionOwner().update(request(sm, cp, 'trafficModeToggle'), sm, cp, now_ns=NOW)
def test_actual_dashcam_mazda_cannot_dispatch_generic_switchback():
sm, cp = prepared(MAZDA.MAZDA_CX5)
assert cp.dashcamOnly
assert producer_available(sm, now_ns=NOW)
assert not authority(sm, cp, NOW, 'switchback')
sender = Mock()
assert not ModeActionPublisher().dispatch('switchback', sm, cp, sender, now_ns=NOW)
sender.send.assert_not_called()