mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
365 lines
15 KiB
Python
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]
|