Files
StarPilot/openpilot/starpilot/ui/tests/test_ui_started_authority.py
T
2026-10-22 12:04:37 -05:00

365 lines
15 KiB
Python

"""Exercise the actual UI owner with separately delivered hardware/Panda state."""
from unittest.mock import MagicMock
from openpilot.cereal.messaging import SubMaster
from types import SimpleNamespace as NS
from typing import Any
from unittest.mock import patch
import pytest
from openpilot.cereal import log
from openpilot.selfdrive.ui.ui_state import UIState, UIStatus
from openpilot.starpilot.ui.runtime_snapshot import current_message
def test_replay_messages_use_clock_sample_after_receipt():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.runtime_snapshot import display_message
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW, SubMasterFake
ui = MagicMock(spec=UIState)
ui.sm = SubMasterFake()
ui.replay_sample = ClockSample(NOW, None, NOW, epoch=1, valid=True)
ui._projection_params_at = time_now = 0
ui.replay_clock = MagicMock()
ui.replay_clock.sample.side_effect = lambda: ClockSample(time_now, None, time_now, epoch=1, valid=True)
def receive(_):
nonlocal time_now
time_now = NOW + 100_000
ui.sm.put('carControl', NS(latActive=True), age_ns=-100_000)
ui.sm.update = receive
UIState.update(ui)
assert ui.replay_sample.now_ns is not None
assert display_message(ui.sm, 'carControl', ui.replay_sample.now_ns).latActive
assert ui.sm.replay_sample is ui.replay_sample
ui._reset_replay_state.assert_not_called()
@pytest.mark.parametrize('enabled', [False, True])
def test_ui_reads_same_aol_runtime_admission_as_control_owner(enabled):
from openpilot.starpilot.ui.tests.test_runtime_snapshot import SubMasterFake
ui: Any = object.__new__(UIState)
ui.params = MagicMock()
ui.params.get.return_value = None
ui.params.get_bool.return_value = False
ui.replay_clock = object()
ui.sm = SubMasterFake()
cp = NS(openpilotLongitudinalControl=True)
ui.sm.put('carParams', cp)
ui.started = ui.chestnut_output_seen = False
with patch('openpilot.selfdrive.ui.ui_state.get_cache', return_value=None), \
patch('openpilot.selfdrive.ui.ui_state.feature_enabled', return_value=enabled) as admission:
ui.update_params()
assert ui.aol_runtime is enabled
assert admission.call_args.args[:3] == (ui.params, cp, 'aol')
ui.sm.seen['carParams'] = False
ui.update_params()
assert not ui.aol_runtime
admission.assert_called_once()
def owner():
ui = object.__new__(UIState)
messages = {'deviceState': NS(started=False, chestnutPresent=False),
'pandaStates': [NS(pandaType=log.PandaState.PandaType.uno, ignitionLine=False, ignitionCan=False)],
'selfdriveState': NS(enabled=True)}
ui.sm = MagicMock(spec=SubMaster)
ui.sm.__getitem__.side_effect = messages.__getitem__
ui.sm.frame = 10
ui.sm.updated = {'pandaStates': True, 'wideRoadCameraState': False, 'selfdriveState': False}
ui.sm.alive = {'pandaStates': True, 'wideRoadCameraState': False}
ui.sm.valid = {'wideRoadCameraState': False}
ui.started = ui.ignition = ui._started_prev = ui._engaged_prev = False
ui.CP = None
ui.is_body = ui.chestnut_compiled = ui.usb_connected = False
ui.status = UIStatus.DISENGAGED
ui.started_frame = 0
ui.engaged_edges = []
ui._engaged_transition_callbacks = [lambda: ui.engaged_edges.append((ui.sm.frame, ui.engaged))]
ui._on_body_changed_callbacks = []
transitions = []
ui._offroad_transition_callbacks = [lambda: transitions.append((ui.sm.frame, ui.started))]
return ui, messages, transitions
def step(ui, messages, hardware, physical):
messages['deviceState'].started = hardware
messages['pandaStates'][0].ignitionLine = physical
with patch('openpilot.selfdrive.ui.ui_state.gui_app.measure_frame_phase', side_effect=lambda _, fn: fn()):
ui._update_state()
ui._update_status()
ui.sm.frame += 1
@pytest.mark.parametrize('raw', [(True, False, True), (False, False, True), (True, True, True)])
def test_raw_ignition_delivery_does_not_reset_hardware_drive(raw):
ui, messages, transitions = owner()
for physical in raw:
step(ui, messages, True, physical)
assert ui.started and ui.started_frame == 10
assert ui.ignition == physical
assert transitions == [(10, True)]
assert ui.engaged_edges == [(10, True)]
step(ui, messages, False, True)
assert not ui.started and ui.ignition
assert transitions[-1] == (13, False)
step(ui, messages, False, False)
assert len(transitions) == 2
step(ui, messages, True, True)
assert ui.started_frame == 15
assert transitions[-1] == (15, True)
assert ui.engaged_edges == [(10, True), (13, False), (15, True)]
def test_initial_offroad_publication_and_first_hardware_drive():
ui, messages, transitions = owner()
ui.sm.frame = 1
step(ui, messages, False, False)
step(ui, messages, False, False)
assert transitions == [(1, False)]
step(ui, messages, True, False)
assert transitions[-1] == (3, True)
assert ui.started_frame == 3 and not ui.ignition
def test_started_presentation_does_not_replace_existing_message_freshness():
ui, messages, _ = owner()
step(ui, messages, True, False)
assert ui.started and not ui.ignition
ui.sm.seen = {'selfdriveState': True}
ui.sm.alive = {'selfdriveState': True}
ui.sm.valid = {'selfdriveState': True}
ui.sm.recv_frame = {'selfdriveState': 9}
ui.sm.logMonoTime = {'selfdriveState': 10_000_000_000}
ui.sm.recv_time = {'selfdriveState': 10.0}
assert current_message(ui.sm, 'selfdriveState', 10_000_000_000, after_frame=ui.started_frame) is None
ui.sm.recv_frame['selfdriveState'] = 11
assert current_message(ui.sm, 'selfdriveState', 10_000_000_000, after_frame=ui.started_frame) is messages['selfdriveState']
assert current_message(ui.sm, 'selfdriveState', 20_000_000_000, after_frame=ui.started_frame) is None
def replay_owner(sample):
from openpilot.starpilot.ui.tests.test_runtime_snapshot import SubMasterFake
ui: Any = object.__new__(UIState)
ui.aol_runtime = True
ui.sm = SubMasterFake()
ui.sm.sock = {name: object() for name in ('carControl', 'carState', 'selfdriveState')}
ui.sm.static_freq_services = set(ui.sm.sock)
ui.sm.data = ui.sm.messages
for name in ui.sm.sock:
ui.sm.put(name, NS(latActive=True, enabled=True), age_ns=0)
ui._replay_state_epoch = 1
ui.replay_sample = sample
ui.replay_clock = MagicMock()
ui._projection_params_at = 0
for name in ('_update_state', '_update_status', 'update_params', '_update_chestnut_state'):
setattr(ui, name, MagicMock())
return ui
def test_epoch_reset_precedes_paused_primer_and_never_drains_socket():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
from openpilot.starpilot.ui.runtime_snapshot import display_message
sample = ClockSample(NOW, None, NOW, epoch=2, paused=True, valid=True)
ui = replay_owner(ClockSample(NOW, None, NOW, epoch=1, valid=True))
ui.replay_clock.sample.return_value = sample
def receive(_):
assert not ui.sm.seen['carControl']
assert not ui.sm['carControl'].latActive
ui.sm.frame += 1
ui.sm.put('carControl', NS(latActive=True), frame=ui.sm.frame)
ui.sm.update = receive
with patch('openpilot.selfdrive.ui.ui_state.messaging.drain_sock_raw') as drain:
ui.update()
drain.assert_not_called()
assert display_message(ui.sm, 'carControl', NOW, after_frame=ui.started_frame).latActive
assert ui.sm.alive['carControl']
assert not ui.aol_runtime
def test_straddled_epoch_withholds_frame_but_retains_new_paused_primer():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
from openpilot.starpilot.ui.runtime_snapshot import display_message
old = ClockSample(NOW + 1_000_000, None, NOW, epoch=1, valid=True)
new = ClockSample(NOW, None, NOW + 1_000_000, epoch=2, paused=True, valid=True)
ui = replay_owner(old)
ui.replay_clock.sample.side_effect = [old, new, new, new]
def receive(_):
ui.sm.frame += 1
ui.sm.updated = dict.fromkeys(ui.sm.sock, False)
if ui.sm.frame == 4:
ui.sm.put('carControl', NS(latActive=True), frame=ui.sm.frame)
ui.sm.put('carState', NS(vEgo=30), age_ns=-1, frame=ui.sm.frame)
ui.sm.update = receive
ui.update()
assert not ui.replay_sample.valid
assert display_message(ui.sm, 'carControl', NOW) is None
assert ui.sm.seen['carControl']
assert not ui.sm.seen['selfdriveState'] # old/unupdated cached service
assert not ui.sm.seen['carState'] # future stamp from the old timeline
ui.update()
assert ui.replay_sample.valid
assert display_message(ui.sm, 'carControl', NOW).latActive
assert ui.sm.alive['carControl'] # the paused primer remains logically current
def test_straddle_during_seeking_requests_one_renderer_reprime():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
old = ClockSample(NOW, None, NOW, epoch=1, valid=True)
seeking = ClockSample(None, None, NOW, epoch=2, paused=True, valid=False)
ui = replay_owner(old)
ui.replay_clock.sample.side_effect = [old, seeking]
ui.sm.update = lambda _: None
ui.update()
assert ui.replay_needs_reprime
assert not any(ui.sm.seen.values())
assert not ui.replay_sample.valid
@pytest.mark.parametrize('paused', (False, True))
@pytest.mark.parametrize('refresh', ('current', 'unread', 'partial', 'missing', 'future', 'invalid'))
@pytest.mark.parametrize('service', ('carControl', 'narrowRoadEncodeIdx'))
def test_seek_primer_received_after_a_straddle_avoids_reseeking(paused, refresh, service):
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
old = ClockSample(NOW, None, NOW, epoch=1, valid=True)
seeking = ClockSample(None, None, NOW, epoch=2, paused=paused)
ready = ClockSample(NOW, NOW + 2_000_000_000, NOW, epoch=2, paused=paused, valid=True)
ui = replay_owner(old)
ui.sm.sock[service] = object()
ui.sm.static_freq_services.add(service)
ui.sm.put(service, NS(latActive=True))
ui.replay_clock.sample.side_effect = [old, seeking, ready, ready]
receives = 0
def receive(_):
nonlocal receives
receives += 1
ui.sm.frame += 1
ui.sm.updated = dict.fromkeys(ui.sm.sock, False)
if receives == 1 and refresh != 'unread' or receives == 2 and refresh != 'missing':
ui.sm.put(service, NS(latActive=True), frame=ui.sm.frame)
ui.sm.valid[service] = refresh != 'invalid'
stamp = ready.boot_ns if service == 'narrowRoadEncodeIdx' else ready.now_ns
assert stamp is not None
ui.sm.logMonoTime[service] = stamp + (refresh == 'future')
if receives == 1 and refresh == 'partial':
ui.sm.put('selfdriveState', NS(enabled=True), frame=ui.sm.frame)
ui.sm.update = receive
ui.update()
assert ui.replay_needs_reprime and not ui.sm.seen[service]
ui.update()
assert ui.replay_sample.valid
assert ui.replay_needs_reprime == (refresh not in ('current', 'unread'))
assert not ui.sm.seen['selfdriveState']
def test_missing_clock_heartbeat_does_not_discard_same_epoch_paused_data():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
paused = ClockSample(NOW, None, NOW, epoch=1, paused=True, valid=True)
missing = ClockSample(None, None, NOW + 1_000_000_000)
ui = replay_owner(paused)
ui.replay_clock.sample.side_effect = [missing, missing, paused, paused]
ui.sm.update = lambda _: None
ui.update()
assert not ui.sm.alive['carControl']
assert ui.sm.seen['carControl']
ui.update()
assert ui.sm.alive['carControl']
assert ui.sm['carControl'].latActive
def test_paused_primer_is_eligible_in_same_started_receive():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
ui, messages, _ = owner()
ui.replay_clock = MagicMock()
ui.replay_sample = ClockSample(NOW, None, NOW, epoch=1, paused=True, valid=True)
messages['deviceState'].started = True
messages['selfdriveState'].state = log.SelfdriveState.OpenpilotState.enabled
messages['selfdriveState'].personality = 'standard'
ui.sm.updated['selfdriveState'] = True
ui.sm.valid['selfdriveState'] = ui.sm.alive['selfdriveState'] = True
ui.sm.recv_frame = {'selfdriveState': 10}
ui.sm.logMonoTime = {'selfdriveState': NOW}
ui.sm.recv_time = {'selfdriveState': NOW / 1e9}
ui.sm.replay_sample = ui.replay_sample
with patch('openpilot.selfdrive.ui.ui_state.gui_app.measure_frame_phase', side_effect=lambda _, fn: fn()):
ui._update_state()
ui._update_status()
assert ui.started_frame == 9
assert ui.status == UIStatus.ENGAGED
assert current_message(ui.sm, 'selfdriveState', NOW, after_frame=ui.started_frame).enabled
def test_replay_boot_time_camera_primer_retention_and_freshness():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
from openpilot.starpilot.ui.runtime_snapshot import display_message
sample = ClockSample(NOW, NOW + 2_000_000_000, NOW, epoch=2, paused=True, valid=True)
assert sample.boot_ns is not None
ui = replay_owner(sample)
ui.sm.sock['cabinCameraState'] = object()
ui.sm.static_freq_services.add('cabinCameraState')
ui.sm.put('cabinCameraState', NS(frameId=42))
ui.sm.logMonoTime['cabinCameraState'] = sample.boot_ns - 1_000_000
ui.sm.replay_sample = sample
ui._update_replay_alive(sample)
assert ui.sm.alive['cabinCameraState']
ui._reset_replay_state(retain_sample=sample)
assert ui.sm.seen['cabinCameraState']
assert display_message(ui.sm, 'cabinCameraState', NOW).frameId == 42
ui.sm.replay_sample = ClockSample(NOW, None, NOW, epoch=2, paused=True, valid=True)
assert display_message(ui.sm, 'cabinCameraState', NOW) is None
def test_replay_python_gps_timestamp_uses_mono_domain():
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
sample = ClockSample(NOW, NOW + 100_000_000_000, NOW, epoch=1, valid=True)
ui = replay_owner(sample)
ui.sm.sock['gpsLocationExternal'] = object()
ui.sm.static_freq_services.add('gpsLocationExternal')
ui.sm.put('gpsLocationExternal', NS(latitude=1))
ui._update_replay_alive(sample)
assert ui.sm.alive['gpsLocationExternal']
ui.sm.logMonoTime['gpsLocationExternal'] = NOW + 1_000_000
ui._update_replay_alive(sample)
assert not ui.sm.alive['gpsLocationExternal']
@pytest.mark.parametrize("service", ["carControl", "carOutput", "controlsState"])
def test_sparse_replay_display_survives_sample_gap_without_extending_action_freshness(service):
from openpilot.tools.replay.display_clock import ClockSample
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW
from openpilot.starpilot.ui.runtime_snapshot import display_message
sample = ClockSample(NOW, None, NOW, epoch=1, valid=True)
ui = replay_owner(sample)
ui.sm.sock[service] = object()
ui.sm.static_freq_services.add(service)
ui.sm.put(service, NS(latActive=True))
ui.sm.logMonoTime[service] = NOW - 150_000_000
ui.sm.replay_sample = sample
ui._update_replay_alive(sample)
assert ui.sm.alive[service]
assert display_message(ui.sm, service, NOW) is not None
assert current_message(ui.sm, service, NOW) is None
ui.sm.logMonoTime[service] = NOW - 201_000_000
ui._update_replay_alive(sample)
assert not ui.sm.alive[service]
assert display_message(ui.sm, service, NOW) is None
ui.sm.logMonoTime[service] = NOW + 1
ui._update_replay_alive(sample)
assert not ui.sm.alive[service]