from contextlib import nullcontext from dataclasses import dataclass, replace from pathlib import Path import shutil from types import SimpleNamespace from typing import Any from unittest.mock import Mock import uuid import pytest from openpilot.starpilot.replay import render from openpilot.starpilot.replay.render import Compositor, Engine, camera_options, frame_matches, parse_wall_offset from openpilot.starpilot.ui.presentation import Profile from openpilot.tools.replay.display_clock import ClockSample def test_camera_selection_uses_selected_sources_and_current_preferences(): sources = {'fcamera.hevc', 'ecamera.hevc', 'dcamera.hevc'} assert camera_options('compact', {}, sources) == (False, False) assert camera_options('large', {}, sources) == (True, False) assert camera_options('compact', {'CameraView': b'0'}, sources) == (True, False) assert camera_options('compact', {'CameraView': b'1'}, sources) == (False, True) assert camera_options('compact', {'CameraView': b'3', 'DriverCamera': b'1'}, sources) == (True, True) assert camera_options('compact', {'CameraView': b'4', 'DriverCamera': b'1'}, sources) == (False, False) assert camera_options('large', {'PIPPreviewEnabled': b'1'}, sources) == (True, True) assert camera_options('compact', {'CameraView': b'1', 'PIPPreviewEnabled': b'1'}, {'qcamera.ts'}) == (False, False) @pytest.mark.parametrize('profile', ('large', 'compact')) @pytest.mark.parametrize('target', ('native', 'web')) @pytest.mark.parametrize('device,pinned', (('tici', True), ('tizi', True), ('mici', True), ('pc', False), ('unknown', False))) def test_native_worker_leaves_ui_cpu_before_starting_replay(tmp_path, monkeypatch, profile, target, device, pinned): from openpilot.common import hardware, realtime pinned = pinned and target == 'native' for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') calls = [] monkeypatch.setattr(render, 'validate_context', lambda path: calls.append('validate') or path) monkeypatch.setattr(hardware.HARDWARE, 'get_device_type', lambda: device) affinity = Mock(side_effect=lambda cores: calls.append('affinity')) monkeypatch.setattr(realtime, 'set_core_affinity', affinity) def prepare(path): calls.append('prepare') raise RuntimeError('stop before worker preparation') monkeypatch.setattr(render, 'seed_display', prepare) with pytest.raises(RuntimeError, match='stop before worker preparation'): render.run(tmp_path, 'recorded-route', tmp_path, target, profile, Mock()) assert calls == (['validate', 'affinity', 'prepare'] if pinned else ['validate', 'prepare']) if pinned: affinity.assert_called_once_with([0, 1, 2, 3, 4, 5]) else: affinity.assert_not_called() @pytest.mark.parametrize('preload', (False, True)) def test_engine_passes_selected_segment_to_machine_replay(tmp_path, monkeypatch, preload): process = Mock() monkeypatch.setattr(render.subprocess, 'Popen', Mock(return_value=process)) monkeypatch.setattr(render.threading, 'Thread', Mock()) Engine('recorded-route', tmp_path, wide=False, cabin=False, start_segment=2, preload_seek=preload) command = render.subprocess.Popen.call_args.args[0] assert command[command.index('--start-segment') + 1] == '2' assert ('--preload-seek' in command) is preload def test_camera_matches_recorded_identity_across_distinct_clock_domains(): state = SimpleNamespace(frameId=5, timestampSof=9_000_000_000, timestampEof=9_010_000_000) client = SimpleNamespace(frame_id=5, timestamp_sof=9_000_000_000, timestamp_eof=9_010_000_000) frame = SimpleNamespace(frame_id=5) stamp, boot_now = 9_020_000_000, 9_050_000_000 assert frame_matches(client, frame, state, stamp, boot_now) assert not frame_matches(client, frame, state, stamp, 1_050_000_000) # MONOTONIC is the wrong domain. assert not frame_matches(client, frame, state, stamp, stamp - 1) assert not frame_matches(client, frame, state, stamp, stamp + 500_000_001) assert not frame_matches(client, None, state, stamp, boot_now) assert not frame_matches(client, frame, SimpleNamespace(frameId=5), stamp, boot_now) assert not frame_matches(client, frame, state, stamp, None) client.frame_id = 4 assert not frame_matches(client, frame, state, stamp, boot_now) client.frame_id, client.timestamp_sof = 5, 9_000_000_001 assert not frame_matches(client, frame, state, stamp, boot_now) def test_paused_same_route_clock_never_advances_filters_or_gpu(): core: Any = object.__new__(Compositor) core.ui = SimpleNamespace(replay_sample=ClockSample(100, 100, 100, epoch=2, paused=True, valid=True)) core.last_route_frame = (2, 100) core.context = Mock() assert core.draw(2, 0, 9_000_000_000) is False core.context.begin_frame.assert_not_called() @pytest.mark.parametrize('sample', [None, SimpleNamespace(now_ns=None)]) def test_missing_route_clock_never_submits_a_gpu_frame(sample): core: Any = object.__new__(Compositor) core.ui = SimpleNamespace(replay_sample=sample) core.context = Mock() assert core.draw(1, 0., 100) is False core.context.begin_frame.assert_not_called() @pytest.mark.parametrize('frame_ready', (False, True)) def test_recorded_camera_preview_does_not_require_initial_device_state(frame_ready): @dataclass class Onroad: camera_available: bool = False viewport_width: int = 0 core: Any = object.__new__(Compositor) core.ui = SimpleNamespace(replay_sample=ClockSample(100, 100, 100, epoch=2, valid=True), sm=SimpleNamespace(seen={'extrinsicsCalibration': False})) core.last_route_frame = None core.context = Mock() core.adapter = Mock() core.adapter.build.return_value = SimpleNamespace(onroad=Onroad()) core._primary_matches = Mock(return_value=frame_ready) core._optional_placeholder_ready = Mock(return_value=False) core.rl = Mock() core.gui = SimpleNamespace(_render_prepare=[], _frame=0) core.content = Mock() core.scale = 1 core.profile = Profile.LARGE core.view = Mock() core.view.render.side_effect = lambda state: setattr(core, 'primary_ready', state.camera_available) core.output = None core.converter = Mock() core.readback = Mock() assert core.draw(2, 0, 9_000_000_000) is frame_ready if not frame_ready: core.context.begin_frame.assert_not_called() core.adapter.build.assert_not_called() core.view.render.assert_not_called() core.readback.start.assert_not_called() return drawn = core.view.render.call_args.args[0] assert drawn.camera_available is frame_ready assert core.adapter.build.return_value.onroad.camera_available is False assert core.readback.start.call_count == int(frame_ready) @pytest.fixture(params=(Profile.LARGE, Profile.COMPACT)) def draw_core(request): from openpilot.cereal.visionipc import VisionStreamType if request.param == Profile.COMPACT: from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView else: from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.starpilot.ui.onroad_state import OnroadState, SpeedLimitObservation class Messages(dict): pass idx = SimpleNamespace(frameId=5, timestampSof=9_000_000_000, timestampEof=9_010_000_000) sm = Messages(narrowRoadEncodeIdx=idx, wideRoadEncodeIdx=idx) sm.seen = dict.fromkeys(('narrowRoadEncodeIdx', 'wideRoadEncodeIdx', 'modelV2', 'radarState', 'carParams', 'carState', 'extrinsicsCalibration'), True) sm.valid = dict(sm.seen) sm.logMonoTime = dict.fromkeys(sm, 9_020_000_000) sm.logMonoTime['extrinsicsCalibration'] = 1_000_000_000 sm.recv_frame = dict.fromkeys(sm.seen, 2) sm.updated = {'modelV2': True} client = Mock(frame_id=5, timestamp_sof=idx.timestampSof, timestamp_eof=idx.timestampEof) frame = SimpleNamespace(frame_id=5) client.recv.return_value = frame core: Any = object.__new__(Compositor) core.profile = request.param core.wide = core.cabin = False core.ui = SimpleNamespace(sm=sm, replay_sample=ClockSample(1_000_000_000, 9_050_000_000, 100, epoch=2, valid=True)) core.camera = Mock(client=client, frame=frame, stream_type=VisionStreamType.VISION_STREAM_NARROW_ROAD) core.camera._switching = False core.camera._ensure_connection.return_value = True core.camera._poll_camera.side_effect = lambda: CameraView._poll_camera(core.camera) core.last_route_frame = None core.render_receipts = {'modelV2': 1} core.context, core.adapter = Mock(), Mock() core.adapter.build.return_value = SimpleNamespace(onroad=OnroadState(True, False, 15., 80., SpeedLimitObservation())) core.rl, core.content = Mock(), Mock() core.gui = SimpleNamespace(_render_prepare=[Mock()], _frame=0) core.scale = 1 core.view, core.converter, core.readback = Mock(), Mock(), Mock() core.output = None def paint(*args, **kwargs): assert kwargs['poll_camera'] is False core.camera.render_camera_model_layer.side_effect = paint def render_view(state): # The real OnroadView enters its camera layer only with this availability flag. if state.camera_available: core._camera_layer(Mock(), state) core.view.render.side_effect = render_view return core @pytest.mark.parametrize('change', ('ahead', 'behind', 'missing_frame', 'timestamp', 'stale_state', 'unseen', 'invalid')) def test_known_camera_rejection_skips_preparation_and_preserves_updates(draw_core, change): core = draw_core if change in ('ahead', 'behind'): core.camera.client.frame_id = 6 if change == 'ahead' else 4 elif change == 'missing_frame': core.camera.frame = None elif change == 'timestamp': core.camera.client.timestamp_sof += 1 elif change == 'stale_state': core.ui.sm.logMonoTime['narrowRoadEncodeIdx'] = 8_000_000_000 else: getattr(core.ui.sm, 'seen' if change == 'unseen' else 'valid')['narrowRoadEncodeIdx'] = False assert core.draw(2, 0., 100) is False core.context.begin_frame.assert_not_called() core.adapter.build.assert_not_called() core.gui._render_prepare[0].assert_not_called() core.view.render.assert_not_called() core.readback.start.assert_not_called() assert core.last_route_frame is None and core.gui._frame == 0 assert core.render_receipts == {'modelV2': 1} and core.ui.sm.updated['modelV2'] @pytest.mark.parametrize('sample', (ClockSample(100, 100, 100, epoch=2), ClockSample(100, 100, 100, epoch=3, valid=True))) def test_invalid_or_other_epoch_clock_never_advances_draw(draw_core, sample): core = draw_core core.ui.replay_sample = sample assert core.draw(2, 0., 100) is False core.context.begin_frame.assert_not_called() core.adapter.build.assert_not_called() def test_camera_poll_precedes_message_snapshot(): core: Any = object.__new__(Compositor) order = [] core.ui = SimpleNamespace(update=lambda: order.append('messages'), replay_sample=ClockSample(1, 1, 1, epoch=2, valid=True)) core.epoch, core.cabin = 2, False core._receive_primary = lambda: order.append('camera') assert core.update() is core.ui.replay_sample assert order == ['camera', 'messages'] @pytest.mark.parametrize('profile', ('onroad', 'mici.onroad')) def test_camera_paint_does_not_receive_or_switch_selected_frame(monkeypatch, profile): import importlib import numpy as np import pyray as rl from openpilot.cereal.visionipc import VisionStreamType module = importlib.import_module(f'openpilot.selfdrive.ui.{profile}.cameraview') monkeypatch.setattr(module, 'COMMA_HARDWARE', True) camera = object.__new__(module.CameraView) camera.close = Mock() camera.client = Mock() camera.frame = SimpleNamespace(frame_id=5, width=100, height=50) frame = camera.frame camera._stream_type = VisionStreamType.VISION_STREAM_NARROW_ROAD camera._switching = True camera._handle_switch = Mock() camera._ensure_connection = Mock() camera._calc_frame_matrix = Mock(return_value=np.eye(3)) camera._render_egl = Mock() camera._render(rl.Rectangle(0, 0, 100, 50), poll_camera=False) assert camera.frame is frame camera.client.recv.assert_not_called() camera._handle_switch.assert_not_called() camera._ensure_connection.assert_not_called() camera._render_egl.assert_called_once() def test_compact_driver_switch_waits_for_camera_poll(monkeypatch): from openpilot.selfdrive.ui.mici.onroad import augmented_road_view as module camera = object.__new__(module.AugmentedRoadView) camera.close = Mock() camera._stream_type = module.NARROW_ROAD_CAM camera._target_stream_type = module.DRIVER_CAM camera._switching = True camera.available_streams = [module.DRIVER_CAM] camera._ensure_connection = Mock() camera._handle_switch = Mock() camera._switch_stream_if_needed = Mock(return_value=module.DRIVER_CAM) monkeypatch.setattr(module.ui_state, 'started', True) monkeypatch.setattr(module.rl, 'draw_rectangle_rec', Mock()) monkeypatch.setattr(module, 'gui_label', Mock()) camera.render_camera_model_layer(Mock(), camera_view=module.CAMERA_VIEW_DRIVER, poll_camera=False) camera._handle_switch.assert_not_called() camera._ensure_connection.assert_not_called() @pytest.mark.parametrize('replacement_id', (5, 6)) def test_selected_camera_is_painted_before_next_poll(draw_core, replacement_id): core = draw_core def receive(**kwargs): core.camera.client.frame_id = replacement_id return SimpleNamespace(frame_id=replacement_id) core.camera.client.recv.side_effect = receive assert core.draw(2, 0., 100) core.camera.client.recv.assert_not_called() core._receive_primary() core.ui.replay_sample = replace(core.ui.replay_sample, now_ns=1_050_000_000) assert core.draw(2, .05, 150) is (replacement_id == 5) core.camera.client.recv.assert_called_once_with(timeout_ms=0) assert core.readback.start.call_count == 1 + int(replacement_id == 5) def test_optional_placeholder_still_allows_later_camera_recovery(draw_core, monkeypatch): from openpilot.cereal.visionipc import VisionStreamType core = draw_core monkeypatch.setattr('openpilot.system.ui.widgets.label.gui_label', Mock()) core.camera.stream_type = VisionStreamType.VISION_STREAM_WIDE_ROAD core.camera.client.frame_id = 4 def receive(**kwargs): core.camera.client.frame_id = 5 return SimpleNamespace(frame_id=5) core.camera.client.recv.side_effect = receive assert core.draw(2, 0., 100) core.camera.client.recv.assert_not_called() core._receive_primary() core.ui.replay_sample = replace(core.ui.replay_sample, now_ns=1_050_000_000) assert core.draw(2, .05, 150) core.camera.client.recv.assert_called_once_with(timeout_ms=0) assert core.readback.start.call_count == 2 def test_selected_buffer_reuse_still_fails_final_identity_check(draw_core): core = draw_core core.camera.render_camera_model_layer.side_effect = lambda *args, **kwargs: setattr(core.camera.frame, 'frame_id', 6) assert not core.draw(2, 0., 100) core.readback.start.assert_not_called() def test_eligible_retained_camera_draws_fresh_hud_and_route_time(draw_core): core = draw_core frame = core.camera.frame core.camera.client.recv.return_value = None assert core.draw(2, 0., 100) core.ui.replay_sample = replace(core.ui.replay_sample, now_ns=1_050_000_000) core.adapter.build.return_value.onroad = replace(core.adapter.build.return_value.onroad, speed_mps=20.) assert core.draw(2, .05, 150) core.ui.replay_sample = replace(core.ui.replay_sample, now_ns=1_100_000_000) assert core.draw(2, .1, 200) # Animation advances even without another source update. assert core.camera.frame is frame assert core.view.render.call_args.args[0].speed_mps == 20. assert [call.args[0] for call in core.context.begin_frame.call_args_list] == [1., 1.05, 1.1] assert core.gui._frame == 3 def test_rejected_camera_can_match_next_update_without_losing_model_receipt(draw_core): core = draw_core core.camera.client.frame_id = 6 core.camera.frame = SimpleNamespace(frame_id=6) assert not core.draw(2, 0., 100) core.ui.sm['narrowRoadEncodeIdx'].frameId = 6 def consume_model(*args, **kwargs): assert core.ui.sm.updated['modelV2'] assert kwargs['poll_camera'] is False core.camera.client.recv.return_value = None core.camera.render_camera_model_layer.side_effect = consume_model assert core.draw(2, 0., 100) assert core.render_receipts['modelV2'] == core.ui.sm.recv_frame['modelV2'] assert core.context.begin_frame.call_count == 1 def test_optional_missing_camera_still_publishes_existing_placeholder(draw_core, monkeypatch): from openpilot.cereal.visionipc import VisionStreamType core = draw_core label = Mock() monkeypatch.setattr('openpilot.system.ui.widgets.label.gui_label', label) core.camera.stream_type = VisionStreamType.VISION_STREAM_WIDE_ROAD core.camera.client.frame_id = 4 core.camera.client.recv.return_value = None assert core.draw(2, 0., 100) label.assert_called_once() core.readback.start.assert_called_once() @pytest.mark.parametrize('camera_view', (1, 3, 4)) @pytest.mark.parametrize('draw_core', (Profile.COMPACT,), indirect=True) def test_compact_hidden_and_unrecorded_camera_keep_selected_frame(draw_core, camera_view, monkeypatch): core = draw_core core.adapter.build.return_value.onroad = replace(core.adapter.build.return_value.onroad, appearance=replace( core.adapter.build.return_value.onroad.appearance, camera_view=camera_view)) monkeypatch.setattr('openpilot.system.ui.widgets.label.gui_label', Mock()) core._receive_primary() assert core.draw(2, 0., 100) core.camera.client.recv.assert_called_once_with(timeout_ms=0) core.camera.render_camera_model_layer.assert_not_called() @pytest.mark.parametrize('profile', ('onroad', 'mici.onroad')) @pytest.mark.parametrize('replay', (False, True)) def test_camera_layers_allow_recorded_preview_and_preserve_live_start_gate(monkeypatch, profile, replay): import importlib from openpilot.cereal.visionipc import VisionStreamType module = importlib.import_module(f'openpilot.selfdrive.ui.{profile}.augmented_road_view') monkeypatch.setattr(module.ui_state, 'started', False) monkeypatch.setattr(module.ui_state, 'replay_clock', object() if replay else None) measure = Mock() monkeypatch.setattr(module.gui_app, 'measure_frame_phase', measure) camera = object.__new__(module.AugmentedRoadView) camera.close = Mock() camera._switch_stream_if_needed = Mock(return_value=VisionStreamType.VISION_STREAM_NARROW_ROAD) camera._update_calibration = Mock() camera._stream_type = VisionStreamType.VISION_STREAM_NARROW_ROAD camera._target_stream_type = None camera._switching = False camera.available_streams = set() camera.frame = None camera.model_renderer = Mock(road_style={}) camera._model_renderer = Mock() camera.render_camera_model_layer(Mock()) assert bool(measure.call_count) is replay assert camera._update_calibration.call_count == int(replay) def test_worker_preserves_final_engine_error_after_exit(tmp_path, monkeypatch): session = tmp_path / ('a' * 32) session.mkdir(mode=0o700) monkeypatch.setenv('SP_REPLAY_CLOCK_PATH', str(tmp_path / 'display-clock')) for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') monkeypatch.setattr(render.signal, 'signal', Mock()) monkeypatch.setattr(render.threading, 'Thread', Mock()) monkeypatch.setattr(render, 'validate_context', lambda path: path) monkeypatch.setattr(render, 'seed_display', lambda path: {}) catalog = Mock() catalog.detail.return_value = {'playable': True, 'segments': []} monkeypatch.setattr('openpilot.starpilot.galaxy.route_inventory.RouteInventory', lambda path: catalog) monkeypatch.setattr(render, 'Compositor', lambda *args, **kwargs: Mock()) engine = Mock(status={'state': 'preparing'}, error=None) engine.process.poll.return_value = 2 def final_status(**kwargs): engine.status = {'state': 'failed', 'error': 'Recorded clock correlation is unavailable'} engine.reader.join.side_effect = final_status monkeypatch.setattr(render, 'Engine', lambda *args, **kwargs: engine) with pytest.raises(RuntimeError, match='Recorded clock correlation is unavailable'): render.run(session, 'recorded-route', tmp_path, 'native', 'large', Mock()) engine.reader.join.assert_called_once_with(timeout=1) @pytest.mark.parametrize('target', ('native', 'web')) def test_engine_starts_before_renderer_and_closes_if_renderer_fails(tmp_path, monkeypatch, target): for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') monkeypatch.setattr(render.signal, 'signal', Mock()) monkeypatch.setattr(render, 'validate_context', lambda path: path) monkeypatch.setattr(render, 'seed_display', lambda path: {}) catalog = Mock() catalog.detail.return_value = {'playable': True, 'segments': []} monkeypatch.setattr('openpilot.starpilot.galaxy.route_inventory.RouteInventory', lambda path: catalog) calls = [] engine = Mock() def start_engine(*args, **kwargs): assert kwargs['preload_seek'] is True calls.append('engine') return engine def start_renderer(*args, **kwargs): calls.append('renderer') raise RuntimeError('renderer unavailable') monkeypatch.setattr(render, 'Engine', start_engine) monkeypatch.setattr(render, 'Compositor', start_renderer) with pytest.raises(RuntimeError, match='renderer unavailable'): render.run(tmp_path, 'recorded-route', tmp_path, target, 'large', Mock()) assert calls == ['engine', 'renderer'] engine.close.assert_called_once() @pytest.mark.parametrize('profile', ('large', 'compact')) def test_web_media_uses_owned_ram_namespace(tmp_path, monkeypatch, profile): from openpilot.starpilot.system.starport import hw_encoder session = tmp_path / uuid.uuid4().hex session.mkdir(mode=0o700) ipc = Path('/dev/shm') / ('msgq_replay-' + session.name[:24]) ipc.mkdir(mode=0o700) for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') monkeypatch.setattr(render.signal, 'signal', Mock()) monkeypatch.setattr(render.threading, 'Thread', Mock()) monkeypatch.setattr(render, 'validate_context', lambda path: path) monkeypatch.setattr(render, 'seed_display', lambda path: {}) catalog = Mock() catalog.detail.return_value = {'playable': True, 'segments': []} monkeypatch.setattr('openpilot.starpilot.galaxy.route_inventory.RouteInventory', lambda path: catalog) engine = Mock(error=None, status={}) engine.process.poll.return_value = None engine.resume.return_value = False monkeypatch.setattr(render, 'Engine', lambda *args, **kwargs: engine) core = Mock() core.update.side_effect = RuntimeError('end fixture after media creation') monkeypatch.setattr(render, 'Compositor', lambda *args, **kwargs: core) monkeypatch.setattr(hw_encoder, 'HardwareH264Encoder', Mock()) factory = Mock(wraps=render.MediaStore) monkeypatch.setattr(render, 'MediaStore', factory) try: with pytest.raises(RuntimeError, match='end fixture'): render.run(session, 'recorded-route', tmp_path, 'web', profile, Mock()) assert factory.call_args.args[:2] == (ipc / session.name, session.name) assert (ipc / session.name / 'media').is_dir() assert not (session / 'media').exists() finally: shutil.rmtree(ipc) @pytest.mark.parametrize('profile', ('large', 'compact')) @pytest.mark.parametrize('handoff', (False, True)) def test_published_readiness_survives_buffering_but_rejects_seeks_and_old_epochs(tmp_path, monkeypatch, profile, handoff): session = tmp_path / ('a' * 32) session.mkdir(mode=0o700) for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') frame_path = Path('/dev/shm') / ('msgq_replay-' + session.name[:24]) / 'frame' monkeypatch.setenv('SP_REPLAY_CLOCK_PATH', str(frame_path.with_name('display-clock'))) monkeypatch.setattr(render.signal, 'signal', Mock()) monkeypatch.setattr(render.threading, 'Thread', Mock()) monkeypatch.setattr(render, 'validate_context', lambda path: path) monkeypatch.setattr(render, 'seed_display', lambda path: {}) catalog = Mock() catalog.detail.return_value = {'playable': True, 'segments': []} monkeypatch.setattr('openpilot.starpilot.galaxy.route_inventory.RouteInventory', lambda path: catalog) publisher = Mock(path=frame_path) publisher_factory = Mock(return_value=publisher) monkeypatch.setattr(render, 'ReplayFramePublisher', publisher_factory) times, step = (1, 1.01, 1.11, 1.31, 1.61, 10, 10.2, 10.4, 10.6, 10.8), [0] monkeypatch.setattr(render.time, 'monotonic', lambda: times[step[0]]) monkeypatch.setattr(render.time, 'monotonic_ns', lambda: round(times[step[0]] * 1e9)) engine = Mock(error=None, status={'state': 'playing', 'paused': False, 'epoch': 1, 'position': 0, 'routeStartNs': 100, 'start': 0, 'end': 20, 'lastRequestId': 'a'}) engine.process.poll.return_value = None engine.seek_request = 'a' engine.resume.side_effect = lambda epoch: handoff and step[0] in (2, 3) monkeypatch.setattr(render, 'Engine', lambda *args, **kwargs: engine) core = Mock(cabin=False) core.camera = Mock(client=None, frame=object(), _target_client=None, _switching=False) core.pip_stream._client = None core.readback.pending = False core.ui.replay_needs_reprime = False available = dict.fromkeys(('modelV2', 'extrinsicsCalibration', 'longitudinalPlan'), True) core.ui.sm = SimpleNamespace(seen=available, valid=available, alive=available) core.update.side_effect = lambda: None if step[0] == 7 else SimpleNamespace( valid=step[0] not in (3, 6, 8), epoch=2 if step[0] == 6 else engine.status['epoch'], now_ns=100, boot_ns=100) core.finish.side_effect = lambda: nullcontext((b'pixels', (1, 0, 1_010_000_000)) if step[0] == 2 else None) core._primary_matches.return_value = core._optional_placeholder_ready.return_value = False attempts = [] def draw(*args): attempts.append(times[step[0]]) return step[0] == 1 core.draw.side_effect = draw monkeypatch.setattr(render, 'Compositor', lambda *args, **kwargs: core) sleeps = [] def next_loop(delay): sleeps.append(delay) if step[0] == len(times) - 1: engine.error = 'test complete' return step[0] += 1 if step[0] == 5: engine.status.update(paused=True, state='paused') if step[0] == 8: engine.status['state'] = 'seeking' if step[0] == 9: engine.status.update(epoch=2, state='paused') monkeypatch.setattr(render.time, 'sleep', next_loop) states = [] with pytest.raises(RuntimeError, match='test complete'): render.run(session, 'recorded-route', tmp_path, 'native', profile, lambda value: states.append(value)) publisher_factory.assert_called_once_with(frame_path, session.name, profile, *render.SIZES['native'][profile]) assert all(value['framePath'] == str(frame_path) for value in states) assert attempts[:2] == [1, 1.01] # A failed draw retries before the 50 ms frame interval. assert [(value['state'], value['frameReady']) for value in states] == [ ('preparing', False), ('seeking', False) if handoff else ('playing', True), ('seeking', False) if handoff else ('buffering', True), ('buffering', True), ('paused', True), ('seeking', False), ('seeking', False), ('seeking', False), ('seeking', False)] assert [value['lastRequestId'] for value in states[:4]] == ([None, None, None, 'a'] if handoff else [None, 'a', 'a', 'a']) publisher.publish.assert_called_once_with(b'pixels', epoch=1, position=0, captured_ns=1_010_000_000) assert sleeps == [.01] * len(times) # Native playback keeps its original idle wait. @pytest.mark.parametrize('matching_frame', (False, True)) def test_epoch_reset_preserves_native_camera_stream_and_eligible_paused_frame(monkeypatch, matching_frame): monkeypatch.setattr('openpilot.starpilot.ui.runtime_snapshot.RuntimeSnapshotAdapter', Mock()) monkeypatch.setattr('openpilot.starpilot.ui.pip_render.PiPRenderer', Mock()) core: Any = object.__new__(Compositor) streams = {0, 2} client = Mock(frame_id=5 if matching_frame else 4, timestamp_sof=9_000_000_000, timestamp_eof=9_010_000_000) client.is_connected.return_value = True frame = SimpleNamespace(frame_id=5) core.camera = Mock(client=client, frame=frame, stream_type=0, available_streams=streams) callbacks = [] replacement = Mock() def transition(): replacement.client, replacement.frame = Mock(), None replacement._offroad_transition = transition def camera_class(**kwargs): callbacks.append(transition) return replacement core.camera_class = camera_class core._close_camera = Mock() core._new_view = Mock() core.readback = Mock(pending=False) core.pip_stream = Mock() core.pip = core.view = core.context = Mock() idx = SimpleNamespace(frameId=5, timestampSof=9_000_000_000, timestampEof=9_010_000_000) sm = Mock() sm.__getitem__ = Mock(return_value=idx) sm.seen = sm.valid = {'narrowRoadEncodeIdx': True} sm.logMonoTime = {'narrowRoadEncodeIdx': 9_020_000_000} core.ui = SimpleNamespace(replay_sample=SimpleNamespace(valid=True, boot_ns=9_050_000_000), sm=sm, _offroad_transition_callbacks=callbacks) core.render_receipts = {'modelV2': 1} core.reset_epoch(3) for callback in callbacks: callback() assert core.render_receipts == {} assert core.camera.client is client assert core.camera.available_streams == streams assert core.camera.available_streams is not streams core.camera._initialize_textures.assert_called_once() assert core.camera.frame is (frame if matching_frame else None) assert core.epoch == 3 and core.last_route_frame is None @pytest.mark.parametrize('profile', ('large', 'compact')) @pytest.mark.parametrize('rate,camera_ready', ((1, True), (.5, True), (2, True), (1, False))) def test_native_playback_renders_each_camera_picture_once(tmp_path, monkeypatch, profile, rate, camera_ready): session = tmp_path / ('a' * 32) session.mkdir(mode=0o700) monkeypatch.setenv('SP_REPLAY_CLOCK_PATH', str(tmp_path / 'display-clock')) for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') monkeypatch.setattr(render.signal, 'signal', Mock()) monkeypatch.setattr(render.threading, 'Thread', Mock()) monkeypatch.setattr(render, 'validate_context', lambda path: path) monkeypatch.setattr(render, 'seed_display', lambda path: {}) catalog = Mock() catalog.detail.return_value = {'playable': True, 'segments': []} monkeypatch.setattr('openpilot.starpilot.galaxy.route_inventory.RouteInventory', lambda path: catalog) times = (0., .02, .051, .061, .09, .101, .13, .151, .161, .201, .211) identities = tuple(range(5, 16)) if rate > 1 else (5, 5, 5, 6, 6, 7, 7, 7, 8, 9, 9) step = [0] monkeypatch.setattr(render.time, 'monotonic', lambda: 1 + times[step[0]]) monkeypatch.setattr(render.time, 'monotonic_ns', lambda: round((1 + times[step[0]]) * 1e9)) engine = Mock(error=None, status={'state': 'playing', 'paused': False, 'rate': rate, 'epoch': 1, 'position': 0, 'routeStartNs': 100, 'start': 0, 'end': 10}) engine.process.poll.return_value = None engine.resume.return_value = False monkeypatch.setattr(render, 'Engine', lambda *args, **kwargs: engine) core = Mock(cabin=False) core.camera = Mock(client=Mock(frame_id=5, timestamp_sof=5), frame=object(), _target_client=None, _switching=False) core.pip_stream._client = None core.readback.pending = core.ui.replay_needs_reprime = False available = dict.fromkeys(('modelV2', 'extrinsicsCalibration', 'longitudinalPlan'), True) core.ui.sm = SimpleNamespace(seen=available, valid=available, alive=available) core._primary_matches.return_value = camera_ready core.finish.return_value = nullcontext(None) core.update.side_effect = lambda: SimpleNamespace(valid=True, epoch=engine.status['epoch'], now_ns=100 + round(times[step[0]] * 1e9), boot_ns=100) drawn = [] core.draw.side_effect = lambda epoch, position, now: drawn.append((epoch, identities[step[0]], now)) or True monkeypatch.setattr(render, 'Compositor', lambda *args, **kwargs: core) def next_loop(delay): if step[0] == len(times) - 1: engine.error = 'test complete' return step[0] += 1 core.camera.client.frame_id = core.camera.client.timestamp_sof = identities[step[0]] if step[0] == len(times) - 1: engine.status['epoch'] = 2 monkeypatch.setattr(render.time, 'sleep', next_loop) with pytest.raises(RuntimeError, match='test complete'): render.run(session, 'recorded-route', tmp_path, 'native', profile, Mock()) if camera_ready and rate <= 1: assert [(epoch, identity) for epoch, identity, _ in drawn] == [(1, n) for n in range(5, 10)] + [(2, 9)] else: assert len(drawn) >= 3 assert all(b[2] - a[2] >= 50_000_000 for a, b in zip(drawn[2:], drawn[3:], strict=False)) def test_initial_hud_primer_required_even_when_camera_received_a_frame(tmp_path, monkeypatch): session = tmp_path / ('a' * 32) session.mkdir(mode=0o700) monkeypatch.setenv('SP_REPLAY_CLOCK_PATH', str(tmp_path / 'display-clock')) for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') monkeypatch.setattr(render.signal, 'signal', Mock()) monkeypatch.setattr(render.threading, 'Thread', Mock()) monkeypatch.setattr(render, 'validate_context', lambda path: path) monkeypatch.setattr(render, 'seed_display', lambda path: {}) catalog = Mock() catalog.detail.return_value = {'playable': True, 'segments': []} monkeypatch.setattr('openpilot.starpilot.galaxy.route_inventory.RouteInventory', lambda path: catalog) core = Mock(cabin=False) core.camera = Mock(client=Mock(), frame=object(), _target_client=None, _switching=False) core.pip_stream._client = None core.readback.pending = False core.ui.replay_needs_reprime = False available = dict.fromkeys(('modelV2', 'extrinsicsCalibration', 'longitudinalPlan'), True) core.ui.sm = SimpleNamespace(seen=available, valid=available, alive=available) core.update.return_value = SimpleNamespace(valid=True, epoch=2, now_ns=100, boot_ns=100) core.finish.return_value = nullcontext(None) monkeypatch.setattr(render, 'Compositor', lambda *args, **kwargs: core) engine = Mock(status={'state': 'paused', 'paused': True, 'epoch': 2, 'position': 0, 'routeStartNs': 100, 'start': 0, 'end': 1}, error=None) engine.process.poll.return_value = None engine.resume.return_value = False monkeypatch.setattr(render, 'Engine', lambda *args, **kwargs: engine) def next_loop(delay): engine.error = 'test complete' monkeypatch.setattr(render.time, 'sleep', next_loop) with pytest.raises(RuntimeError, match='test complete'): render.run(session, 'recorded-route', tmp_path, 'native', 'large', Mock()) engine.command.assert_called_once_with({'op': 'seek', 'position': 0, 'requestId': '0' * 32}, reprime=True) core.draw.assert_not_called() def test_early_camera_subscriber_is_polled_before_paused_reprime(): from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.onroad.cameraview import CameraView sample = SimpleNamespace(valid=True, epoch=1, boot_ns=9_050_000_000) idx = SimpleNamespace(frameId=5, timestampSof=9_000_000_000, timestampEof=9_010_000_000) sm = Mock() sm.__getitem__ = Mock(return_value=idx) sm.seen = sm.valid = {'narrowRoadEncodeIdx': True} sm.logMonoTime = {'narrowRoadEncodeIdx': 9_020_000_000} frame = SimpleNamespace(frame_id=5) client = Mock(frame_id=5, timestamp_sof=idx.timestampSof, timestamp_eof=idx.timestampEof) # The first receive synchronizes with a publisher created after this client. client.recv.side_effect = [None, frame] core: Any = object.__new__(Compositor) core.ui = Mock(replay_sample=sample, sm=sm) core.epoch, core.cabin = 1, False core.camera = Mock(client=client, frame=None, stream_type=VisionStreamType.VISION_STREAM_NARROW_ROAD) core.camera._switching = False core.camera._ensure_connection.return_value = True core.camera._poll_camera.side_effect = lambda: CameraView._poll_camera(core.camera) core.update() assert not core._primary_matches() core.update() assert core.camera.frame is frame and core._primary_matches() def test_recorded_wall_clock_uses_only_verified_route_pair(): from openpilot.starpilot.replay.render import recorded_wall_clock sample = SimpleNamespace(valid=True, now_ns=10_000_000_000) assert recorded_wall_clock(sample, None) is None assert recorded_wall_clock(SimpleNamespace(valid=False), 1_000_000_000) is None assert recorded_wall_clock(SimpleNamespace(valid=True, now_ns=None), 1_000_000_000) is None instant = recorded_wall_clock(sample, 1_000_000_000_000_000_000) assert instant.timestamp() == 1_000_000_010 def test_private_wall_offset_protocol_preserves_full_nanoseconds(): import json offset = 1_791_870_000_000_000_123 value = json.loads('{"wallMinusMonoNs":"1791870000000000123"}') assert parse_wall_offset(value['wallMinusMonoNs']) == offset for invalid in (None, False, offset, float(offset), '0', '-1', '9' * 20, str(2**63), '1.79187e18'): assert parse_wall_offset(invalid) is None def test_camera_reprime_cannot_overwrite_an_unacknowledged_user_action(): import io import json import threading engine: Any = object.__new__(Engine) engine.write_lock = threading.Lock() engine.process = SimpleNamespace(stdin=io.StringIO()) engine.last_request_id = engine.seek_request = None engine.status = engine.resume_request = None engine.resume_epoch = 0 request = {'op': 'seek', 'position': 65, 'requestId': 'a' * 32} assert engine.command(request) assert not engine.command({'op': 'seek', 'position': 0, 'requestId': '0' * 32}, reprime=True) assert engine.seek_request == request['requestId'] assert [json.loads(line) for line in engine.process.stdin.getvalue().splitlines()] == [request] assert engine.command(request, reprime=True) pause = {'op': 'pause', 'paused': True, 'requestId': 'b' * 32} assert engine.command(pause) assert not engine.command(request, reprime=True) assert engine.command({'op': 'seek', 'position': 65, 'requestId': pause['requestId']}, reprime=True) @pytest.mark.parametrize('outcome', ('playing', 'ended', 'buffering', 'pause', 'stop', 'seek', 'paused_seek', 'rate_before', 'rate_after')) def test_seek_waits_for_delivery_and_respects_new_commands(tmp_path, monkeypatch, outcome): import io import json process = SimpleNamespace(stdin=io.StringIO(), stdout=io.StringIO()) monkeypatch.setattr(render.subprocess, 'Popen', Mock(return_value=process)) monkeypatch.setattr(render.threading, 'Thread', Mock()) engine = Engine('recorded-route', tmp_path, wide=False, cabin=False) engine.status = {'event': 'status', 'state': 'playing', 'epoch': 2, 'paused': outcome == 'paused_seek', 'lastRequestId': '0'} seek = {'op': 'seek', 'position': 65, 'requestId': 'a'} engine.command(seek) def sent(): return [json.loads(line) for line in process.stdin.getvalue().splitlines()] if outcome == 'paused_seek': assert sent() == [seek] assert not engine.resume(3) return assert sent() == [{'op': 'pause', 'paused': True, 'requestId': 'a'}, seek] # An old epoch or the pause acknowledgement cannot release a newer seek. assert engine.resume(2) engine.status = {**engine.status, 'state': 'paused', 'paused': True, 'epoch': 3} assert engine.resume(3) engine.status['lastRequestId'] = 'a' assert engine.resume(2) assert len(sent()) == 2 if outcome == 'rate_before': engine.command({'op': 'rate', 'rate': 2, 'requestId': 'b'}) engine.status['lastRequestId'] = 'b' if outcome in ('pause', 'stop', 'seek'): command = {'op': outcome, 'requestId': 'b', **({'paused': True} if outcome == 'pause' else {'position': 70} if outcome == 'seek' else {})} engine.command(command) before = sent() engine.resume(3) assert sent() == before if outcome == 'seek': engine.status.update(epoch=4, lastRequestId='b') assert engine.resume(4) assert sent()[-1] == {'op': 'pause', 'paused': False, 'requestId': 'b'} else: assert engine.resume(3) assert sent()[-1] == {'op': 'pause', 'paused': False, 'requestId': engine.last_request_id} count = len(sent()) assert engine.resume(3) assert len(sent()) == count process.stdout = io.StringIO(json.dumps({**engine.status, 'state': 'buffering', 'paused': False}) + '\n') engine._read() assert engine.resume(3) # Preloading still holds the recorded clock after accepting Play. assert len(sent()) == count if outcome == 'rate_after': engine.command({'op': 'rate', 'rate': 2, 'requestId': 'b'}) engine.status['lastRequestId'] = 'b' state = 'playing' if outcome.startswith('rate_') else outcome process.stdout = io.StringIO(json.dumps({**engine.status, 'state': state, 'paused': state != 'playing', 'error': 'recording_gap' if outcome == 'buffering' else None}) + '\n') engine._read() assert not engine.resume(3) @pytest.mark.parametrize('profile', ('onroad', 'mici.onroad')) def test_replay_draw_retains_updates_until_stock_consumers_read_them(monkeypatch, profile): import importlib import numpy as np import pyray as rl from openpilot.cereal import messaging from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.ui_state import UIStatus model_module = importlib.import_module(f'openpilot.selfdrive.ui.{profile}.model_renderer') camera_module = importlib.import_module(f'openpilot.selfdrive.ui.{profile}.augmented_road_view') sm = messaging.SubMaster(['modelV2', 'extrinsicsCalibration', 'radarState', 'carParams', 'carState', 'selfdriveState', 'carOutput', 'longitudinalPlan']) clock = [1_000_000_000] ui = SimpleNamespace(sm=sm, started=False, started_frame=0, replay_clock=object(), status=UIStatus.ENGAGED, display_time_ns=lambda: clock[0], replay_sample=SimpleNamespace(valid=True, now_ns=clock[0])) monkeypatch.setattr(model_module, 'ui_state', ui) monkeypatch.setattr(camera_module, 'ui_state', ui) monkeypatch.setattr(model_module, 'Params', lambda: SimpleNamespace(get=lambda _: None)) monkeypatch.setattr(camera_module.gui_app, 'measure_frame_phase', lambda phase, fn, *args, **kwargs: None if phase == 'camera' else fn(*args, **kwargs)) monkeypatch.setattr(model_module, 'draw_polygon', Mock()) renderer = model_module.ModelRenderer() renderer._update_raw_points = Mock(wraps=renderer._update_raw_points) renderer._update_model = Mock() renderer._draw_lane_lines = renderer._draw_path = Mock() renderer.render = lambda rect: renderer._render(rect) camera = object.__new__(camera_module.AugmentedRoadView) camera.close = Mock() camera.device_camera = object() camera.view_from_calib = camera.view_from_wide_calib = np.eye(3) camera._switch_stream_if_needed = Mock(return_value=VisionStreamType.VISION_STREAM_NARROW_ROAD) camera._stream_type = VisionStreamType.VISION_STREAM_NARROW_ROAD camera._target_stream_type, camera._switching = None, False camera.available_streams, camera.frame = {camera.stream_type}, object() camera.model_renderer = camera._model_renderer = renderer core: Any = object.__new__(Compositor) core.ui, core.camera = ui, camera core.profile = Profile.COMPACT if profile == 'mici.onroad' else Profile.LARGE core.cabin = core.wide = False core.render_receipts = {} core._primary_matches = Mock(return_value=True) core._optional_placeholder_ready = Mock(return_value=False) core._receive_primary, core.rl = Mock(), Mock() state = SimpleNamespace(appearance=SimpleNamespace(camera_view=2, show_lead_indicator=False, lead_info_mode=0, lead_info_metric=False), reverse_driver_camera=False, lead_indicator_source_fresh=False, lateral_active=True, customization={'roadColors': {'compact': {}, 'large': {}}}) rect = rl.Rectangle(0, 0, 536, 240) model = messaging.new_message('modelV2', valid=True) model.modelV2.position.x = [5., 10., 15.] model.modelV2.position.y = model.modelV2.position.z = [0., 0., 0.] sm.update_msgs(1., [model]) sm.update_msgs(1.01, []) core._camera_layer(rect, state) renderer._update_raw_points.assert_not_called() calib = messaging.new_message('extrinsicsCalibration', valid=True, logMonoTime=clock[0] + 50_000_000) calib.extrinsicsCalibration.rpyCalib = [0.1, 0., 0.] calib.extrinsicsCalibration.calStatus = camera_module.CALIBRATED sm.update_msgs(1.02, [calib]) sm.update_msgs(1.03, []) sm.alive['extrinsicsCalibration'] = False core._camera_layer(rect, state) renderer._update_raw_points.assert_not_called() assert 'extrinsicsCalibration' not in core.render_receipts clock[0] += 50_000_000 ui.replay_sample.now_ns = clock[0] sm.alive['extrinsicsCalibration'] = True original = sm.updated rotation = Mock(wraps=camera_module.rot_from_euler) monkeypatch.setattr(camera_module, 'rot_from_euler', rotation) core._camera_layer(rect, state) core._camera_layer(rect, state) assert sm.updated is original assert renderer._update_raw_points.call_count == 1 np.testing.assert_array_equal(renderer._path.raw_points[:, 0], [5., 10., 15.]) assert rotation.call_count == 1 params = messaging.new_message('carParams', valid=True) params.carParams.openpilotLongitudinalControl = True sm.update_msgs(1.035, [params, messaging.new_message('radarState', valid=False)]) sm.update_msgs(1.036, []) geometry_calls = renderer._update_model.call_count core._camera_layer(rect, state) assert renderer._longitudinal_control assert renderer._update_raw_points.call_count == 1 assert renderer._update_model.call_count == geometry_calls + 1 assert renderer._update_model.call_args.kwargs['update_lane_geometry'] is False renderer._path.projected_points = np.ones((3, 2), dtype=np.float32) renderer._rect = rect renderer._draw_path = model_module.ModelRenderer._draw_path.__get__(renderer) state.customization['roadColors'][core.profile] = {'pathMode': 'color'} car = messaging.new_message('carState', valid=True) car.carState.vEgo = 22. sm.update_msgs(1.037, [car]) sm.update_msgs(1.038, []) core._camera_layer(rect, state) assert 'carState' not in core.render_receipts state.customization['roadColors'][core.profile] = {'pathMode': 'rainbow'} core._camera_layer(rect, state) assert renderer._rainbow_path.speed_mps == 22. sample_ns = renderer._rainbow_path.sample_ns clock[0] += 50_000_000 core._camera_layer(rect, state) assert renderer._rainbow_path.sample_ns == sample_ns sm.update_msgs(1.04, [model]) sm.update_msgs(1.05, []) if core.profile == 'compact': state.appearance.camera_view = 4 core._camera_layer(rect, state) assert renderer._update_raw_points.call_count == 1 state.appearance.camera_view = 2 core._camera_layer(rect, state) assert renderer._update_raw_points.call_count == 2 before, original = dict(core.render_receipts), sm.updated def failed_draw(*args, **kwargs): sm.updated['modelV2'] raise RuntimeError('draw failed') camera.render_camera_model_layer = failed_draw with pytest.raises(RuntimeError, match='draw failed'): core._camera_layer(rect, state) assert sm.updated is original and core.render_receipts == before @pytest.mark.parametrize('valid,epoch,meta', [(True, 1, (1, 0., 100)), (False, 1, (1, 0., 100)), (True, 2, (1, 0., 100)), (True, 1, None)]) def test_finished_frame_is_borrowed_until_delivery_finishes(valid, epoch, meta): core: Any = object.__new__(Compositor) pixels = memoryview(bytearray(b'pixels')) core.readback = Mock(pending=True) core.readback.finish.return_value = pixels core.ui = SimpleNamespace(replay_clock=Mock()) core.ui.replay_clock.sample.return_value = SimpleNamespace(valid=valid, epoch=epoch) core.meta = meta with pytest.raises(RuntimeError, match='delivery failed'): with core.finish() as finished: core.readback.release.assert_not_called() assert finished == ((pixels, meta) if meta is not None and valid and epoch == 1 else None) if finished: assert finished[0] is pixels raise RuntimeError('delivery failed') core.readback.release.assert_called_once() def test_web_delivery_times_packets_and_flushes_pause_end_and_stall(tmp_path, monkeypatch): session = tmp_path / ('a' * 32) session.mkdir(mode=0o700) ram = tmp_path / 'ram' ram.mkdir() monkeypatch.setattr(render, 'web_media_dir', lambda _: ram / session.name) for key in ('STARPILOT_PROJECTION_READ_ONLY', 'BIG', 'SCALE'): monkeypatch.setenv(key, '') monkeypatch.setattr(render.signal, 'signal', Mock()) monkeypatch.setattr(render.threading, 'Thread', Mock()) monkeypatch.setattr(render, 'validate_context', lambda path: path) monkeypatch.setattr(render, 'seed_display', lambda path: {}) catalog = Mock() catalog.detail.return_value = {'playable': True, 'segments': []} monkeypatch.setattr('openpilot.starpilot.galaxy.route_inventory.RouteInventory', lambda path: catalog) times = (1., 1.06, 1.125, 1.15, 10., 10.06, 10.12, 10.125, 10.23, 10.275, 11.3, 11.36, 11.42, 11.54) step = [0] monkeypatch.setattr(render.time, 'monotonic', lambda: times[step[0]]) monkeypatch.setattr(render.time, 'monotonic_ns', lambda: round(times[step[0]] * 1e9)) engine = Mock(error=None, status={'state': 'playing', 'paused': False, 'epoch': 1, 'position': 0, 'routeStartNs': 100, 'start': 0, 'end': 20}) engine.process.poll.return_value = None engine.resume.return_value = False monkeypatch.setattr(render, 'Engine', lambda *args, **kwargs: engine) core = Mock(cabin=False) core.camera = Mock(client=None, frame=object(), _target_client=None, _switching=False) core.pip_stream._client = None core.readback.pending = core.ui.replay_needs_reprime = False available = dict.fromkeys(('modelV2', 'extrinsicsCalibration', 'longitudinalPlan'), True) core.ui.sm = SimpleNamespace(seen=available, valid=available, alive=available) core.update.side_effect = lambda: SimpleNamespace(valid=step[0] != 4, epoch=engine.status['epoch'], now_ns=100, boot_ns=100) delivered = {0, 1, 2, 5, 6, 7, 9, 11, 12} core.finish.side_effect = lambda: nullcontext((b'pixels', (engine.status['epoch'], 0, round(times[step[0]] * 1e9))) if step[0] in delivered else None) core.draw.return_value = True monkeypatch.setattr(render, 'Compositor', lambda *args, **kwargs: core) encoder = Mock(supports_nv12=True) encoder.encode_nv12.side_effect = lambda pixels, keyframe: (b'packet', keyframe) monkeypatch.setattr('openpilot.starpilot.system.starport.hw_encoder.HardwareH264Encoder', lambda *args, **kwargs: encoder) muxes = [] def new_mux(*args): mux = Mock(codec='avc1.42c01f', init=b'init') mux.frame.return_value = b'fragment' muxes.append(mux) return mux monkeypatch.setattr(render, 'MediaMux', new_mux) sleeps = [] def next_loop(delay): sleeps.append(delay) if step[0] == len(times) - 1: engine.error = 'test complete' return step[0] += 1 if step[0] in (3, 7): engine.status.update(paused=True, state='paused') if step[0] == 7: engine.status['epoch'] = 2 if step[0] in (5, 9): engine.status.update(paused=False, state='playing', rate=2) if step[0] == 13: engine.status.update(paused=True, state='ended') monkeypatch.setattr(render.time, 'sleep', next_loop) states = [] with pytest.raises(RuntimeError, match='test complete'): render.run(session, 'recorded-route', tmp_path, 'web', 'large', states.append) assert len(muxes) == 2 assert [(call.kwargs['pts_us'], call.kwargs['duration_us']) for call in muxes[0].frame.call_args_list] == [ (0, 60000), (60000, 65000), (125000, 50000), (175000, 60000)] assert [(call.kwargs['pts_us'], call.kwargs['duration_us']) for call in muxes[1].frame.call_args_list] == [ (0, 50000), (50000, 50000), (100000, 60000), (160000, 50000)] assert states[0]['mediaReady'] is False buffering = [state for state in states if state['state'] == 'buffering'] assert len(buffering) == 2 # An invalid clock and a stale publication do not invalidate existing media. assert all(state['frameReady'] and state['mediaReady'] and state['media'] for state in buffering) assert any(state['epoch'] == 2 and state['state'] == 'paused' and state['mediaReady'] for state in states) assert states[-1]['state'] == 'ended' and states[-1]['mediaReady'] for mux in muxes: mux.close.assert_called_once() assert min(sleeps) == .002 and max(sleeps) == .01 assert sleeps[9] == pytest.approx(.005) # Processing leaves only 5 ms to the next draw. assert [sleeps[i] for i in (3, 4, 7, 8, 13)] == [.01] * 5 # Paused, invalid clock, ended. assert (ram / session.name / 'media.json').is_file() and not (session / 'media.json').exists()