Files
StarPilot/openpilot/starpilot/ui/tests/test_spacepilot_timing.py
T
firestar5683 718a2c3ddd Keep SpacePilot open across cold setup and interrupted clock reads
Discard a preempted clock pair and try up to three fresh pairs while
retaining the 1 ms accepted span and all existing freshness/resume gates.
Yield after receive-only decoder construction so normal UI collection
refreshes before full physical admission, Offroad request or input.

Recorded d5 clock failure; reproduced timing regressions; 250 focused
native cases, lint/type checks and independent source/evidence review.
No vehicle commands, device deployment or physical cure claim.
2026-10-26 18:17:20 -05:00

165 lines
6.4 KiB
Python

"""Cold native entry and preempted clock reads from the recorded d5 exit paths."""
from types import SimpleNamespace as NS
from unittest.mock import Mock
import pytest
from openpilot.starpilot.ui.spacepilot import ignition, native, session
from openpilot.starpilot.ui.tests.test_spacepilot_session import Clock, Vehicle, game_ui, offroad, setup as setup
from openpilot.starpilot.ui.tests.test_spacepilot_state import mature_gate, read
from openpilot.system.ui.lib.application import gui_app
def clock_reads(monkeypatch, *, delayed=1, backwards=False, offset=2_000_000_000):
now = 10_000_000_000
monos, boots = [], []
for index in range(4):
delta = -1 if backwards else 5_000_000 if index < delayed else 1_000
monos.extend((now, now + delta))
boots.append(now + offset)
now += max(delta, 0)
mono = Mock(side_effect=monos)
monkeypatch.setattr(session.time, 'monotonic_ns', mono)
monkeypatch.setattr(session.time, 'clock_gettime_ns', Mock(side_effect=boots))
return mono
def witness_ui(monkeypatch):
ui = game_ui()
Clock().refresh(ui, 0)
monkeypatch.setattr(native, 'ui_state', ui)
monkeypatch.setattr(native.HARDWARE, 'get_device_type', lambda: 'mici')
return ui
@pytest.mark.parametrize('caller', ['session', 'entry', 'ignition'])
@pytest.mark.parametrize('delayed', [1, 2])
def test_preempted_read_is_discarded_then_fresh_pair_used(monkeypatch, caller, delayed):
ui = witness_ui(monkeypatch)
game = session.GameSession(ui, Mock(), native_device=True)
mono = clock_reads(monkeypatch, delayed=delayed)
if caller == 'session':
result = game._clocks()
assert result[0] == 10_000_000_000 + delayed * 5_000_000 + 1_000
assert result[1] - result[0] == 1_999_999_000
elif caller == 'entry':
witness = native.live_witness()
assert witness is not None and witness.mono_ns == 10_000_000_000 + delayed * 5_000_000 + 1_000
else:
sample = ignition.sample_ignition(ui, native_device=True)
assert sample is not None and sample.mono == 10_000_000_000 + delayed * 5_000_000 + 1_000
assert mono.call_count == 2 * (delayed + 1)
@pytest.mark.parametrize('caller', ['session', 'entry', 'ignition'])
def test_three_preemptions_are_bounded_and_still_refuse(monkeypatch, caller):
ui = witness_ui(monkeypatch)
game = session.GameSession(ui, Mock(), native_device=True)
mono = clock_reads(monkeypatch, delayed=3)
if caller == 'session':
with pytest.raises(ValueError, match='stable device clock'):
game._clocks()
elif caller == 'entry':
assert native.live_witness() is None
else:
assert ignition.sample_ignition(ui, native_device=True) is None
assert mono.call_count <= 6
def test_resumed_clock_is_not_hidden_by_successful_resample(monkeypatch):
game = session.GameSession(NS(), Mock(), native_device=True)
game._clock_offset = 2_000_000_000
clock_reads(monkeypatch, offset=2_006_000_000)
with pytest.raises(ValueError):
game._clocks()
assert game._clock_offset == 2_000_000_000
@pytest.mark.parametrize('caller', ['session', 'entry', 'ignition'])
def test_backwards_clock_never_retries_into_admission(monkeypatch, caller):
ui = witness_ui(monkeypatch)
game = session.GameSession(ui, Mock(), native_device=True)
mono = clock_reads(monkeypatch, backwards=True)
if caller == 'session':
with pytest.raises(ValueError):
game._clocks()
elif caller == 'entry':
assert native.live_witness() is None
else:
assert ignition.sample_ignition(ui, native_device=True) is None
assert mono.call_count == 2
@pytest.mark.parametrize('change', ['none', 'drive', 'engaged', 'stale', 'timeout'])
def test_cold_reader_yields_to_next_ui_sample_without_request_or_input(setup, monkeypatch, change):
game, ui, owner, clock = setup
monkeypatch.setattr(native, 'ui_state', ui)
monkeypatch.setattr(native, 'live_witness', lambda: read(ui, clock.now))
monkeypatch.setattr(native, 'GameSession', lambda *_a, **_k: game)
monkeypatch.setattr(native, 'device', Mock())
monkeypatch.setattr(gui_app, '_nav_stack', [Mock()])
monkeypatch.setattr(gui_app, '_nav_stack_ticks', [])
monkeypatch.setattr(native, '_ignition_return', None)
def cold_reader(cp):
clock.now += 400_000_000 # Recorded first launch spent396ms in a UI frame.
return Vehicle(cp)
game._reader_factory = cold_reader
page = native.SpacePilotPage(mature_gate(), Mock())
gui_app.push_widget(page)
try:
page._tick()
assert not page.closed and game.stage == session.Stage.VERIFYING
assert owner.snapshot().mode == 'auto' and not game.playable()
assert game.gate.since is None and not game.wheel.armed
if change == 'timeout':
clock.refresh(ui, session.TRANSITION_NS)
elif change != 'stale':
clock.refresh(ui)
if change == 'drive':
ui.sm['carState'].gearShifter = 'drive'
elif change == 'engaged':
ui.sm['selfdriveState'].enabled = True
page._tick()
if change != 'none':
assert page.closed and game.stage == session.Stage.CLOSED and game.vehicle is None
assert owner.snapshot().mode == 'auto'
return
assert game.gate.since == clock.now and game.stage == session.Stage.VERIFYING
for _ in range(9):
clock.refresh(ui)
page._tick()
assert owner.snapshot().mode == 'auto' and not game.playable()
clock.refresh(ui)
page._tick()
assert game.stage == session.Stage.STOPPING and owner.snapshot().mode == 'offroad'
assert not game.playable()
offroad(ui)
for _ in range(12):
clock.refresh(ui)
page._tick()
assert game.stage == session.Stage.PLAYING and not page.closed and game.playable()
finally:
page.hide_event()
def test_playing_offroad_game_survives_preempted_read_without_bypassing_witness(monkeypatch):
from openpilot.starpilot.ui.spacepilot.guard import read_offroad_witness
ui = witness_ui(monkeypatch)
offroad(ui)
ui.sm['pandaStates'][0].ignitionLine = False
game = session.GameSession(ui, Mock(), native_device=True)
game.stage = session.Stage.PLAYING
game._was_ignition = False
game._clock_offset = 2_000_000_000
game.gate.since = 9_000_000_000
game.gate.previous = read_offroad_witness(ui, 10_000_000_000, 12_000_000_000, native_device=True)
assert game.gate.previous is not None
game.gate.ready = True
mono = clock_reads(monkeypatch)
assert not game.step()
assert game.stage == session.Stage.PLAYING and game.gate.ready
assert mono.call_count == 6
assert not game.requested_offroad and game.vehicle is None
game.close()