"""Real v2 cereal packets through the Maps consumer and actual saved owners.""" from dataclasses import replace from pathlib import Path import os import tempfile from types import SimpleNamespace as NS from unittest.mock import patch from openpilot.cereal import log from openpilot.common.params import Params from openpilot.starpilot.maps import shadow_lifecycle from openpilot.starpilot.speed_limits import acceptance as acc, map_source as ms, selection as sel from openpilot.starpilot.speed_limits.map_observation import MapSource, copy_frame from openpilot.starpilot.speed_limits.runtime import Runtime from openpilot.starpilot.speed_limits.runtime_settings import parse, read_params from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner from openpilot.starpilot.ui.feature_settings_state import row_change MONO = 1_000_000_000 BOOT = 5_000_000_000 def packet(**changes): event = log.Event.new_message() event.valid = True event.logMonoTime = BOOT road = event.init('mapdOut') values = {'sampleVersion': 2, 'roadStatus': 'matchedLimit', 'gpsSource': 'external', 'gpsMonoTime': BOOT-100_000_000, 'computedMonoTime': BOOT, 'sourceGeneration': 1, 'producerSession': 41, 'speedLimit': 13.4112, 'tileLoaded': True, 'wayId': 123, 'waySelectionType': 'current'} values.update(changes) for n, v in values.items(): setattr(road, n, v) return event class SM: def __init__(self, event): self.data = {'mapdOut': event.mapdOut, 'carState': NS(canTimeout=False), 'slcDashboardObservation': NS(status='valid', observedMonoTime=MONO, validUntilMonoTime=MONO+1_000_000_000, episode=1, producerSessionId='card', speedMps=20., carStateLogMonoTime=MONO)} self.valid = dict.fromkeys(self.data, True) self.alive = dict.fromkeys(self.data, True) self.updated = dict.fromkeys(self.data, True) self.logMonoTime = dict.fromkeys(self.data, MONO) self.logMonoTime['mapdOut'] = int(event.logMonoTime) def __getitem__(self, n): return self.data[n] def test_maps_priority_slots_and_defaults_use_actual_runtime_projection(): sm = SM(packet()) cp = NS(carFingerprint='vehicle') for first, second, expected in (('Maps', 'Dashboard', sel.Source.MAP), ('Dashboard', 'Maps', sel.Source.DASHBOARD), ('Vision', 'Maps', sel.Source.MAP)): settings = parse({'SpeedLimitController': True, 'SLCPriority1': first, 'SLCPriority2': second}) runtime = Runtime(settings, session_id='drive') observations = runtime._observations(sm, cp, MONO, BOOT) chosen = sel.select_limit(observations, settings.selection) assert chosen.selected_source is expected settings = parse({}) assert settings.selection.slots == (sel.Source.VISION, sel.Source.DASHBOARD) chosen = sel.select_limit(Runtime(settings)._observations(sm, cp, MONO, BOOT), settings.selection) assert chosen.selected_source is sel.Source.DASHBOARD assert parse({'SLCPriority1': 'Map Data'}).selection.slots[0] is sel.Source.MAP def test_expired_map_falls_through_to_current_dashboard_without_renewal(): sm = SM(packet()) settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Maps', 'SLCPriority2': 'Dashboard'}) runtime = Runtime(settings, session_id='drive') cp = NS(carFingerprint='vehicle') assert runtime._observations(sm, cp, MONO, BOOT)[sel.Source.MAP].kind is acc.ObservationKind.VALID sm.updated['mapdOut'] = False values = runtime._observations(sm, cp, MONO, BOOT+ms.COMPUTED_MAX_AGE_NS+1) assert values[sel.Source.MAP].kind is acc.ObservationKind.STALE assert sel.select_limit(values, settings.selection).selected_source is sel.Source.DASHBOARD values = runtime._observations(sm, cp, MONO, BOOT+ms.COMPUTED_MAX_AGE_NS+2) assert values[sel.Source.MAP].candidate is None def test_map_loss_no_metadata_and_lookahead_retain_only_previously_accepted_limit(): authority = acc.Authority(acc.Mode.COMBINED, acc.LongitudinalOwner.SYSTEM, True, True, False, False) prior = acc.AcceptedLimit(acc.Candidate('map', acc.ObservationIdentity(acc.IdentityKind.GEOGRAPHIC, value='previous-way'), 20.), 'drive', 0) frame = ms.decode_event(packet().to_bytes()) for changed in (replace(frame, way_selection='possible'), replace(frame, way_selection='predicted'), replace(frame, way_selection='extended'), replace(frame, version=1), replace(frame, tile_loaded=False), replace(frame, event_mono_ns=BOOT+1), replace(frame, status='matchedNoLimit', speed_mps=0.), replace(frame, status='noCoverage', event_valid=False, tile_loaded=False, way_id=0, speed_mps=0.)): observation = MapSource().step(changed, received=True, now_boot_ns=BOOT, transport_alive=True, drive_session='drive', car_fingerprint='vehicle') assert observation.candidate is None assert observation.kind is not acc.ObservationKind.ABSENT for fallback in (False, True): decision = acc.step(acc.new_session('drive', prior), observation, authority, acc.Policy(fallback_previous=fallback), now_ns=MONO) assert decision.control_target_mps == (20. if fallback else None) assert decision.state.accepted == prior assert decision.history_write is None def test_map_identity_order_restart_and_transport_loss_are_fail_closed(): source = MapSource() frame = ms.decode_event(packet().to_bytes()) def apply(f, now=BOOT, alive=True, received=True): return source.step(f, received=received, now_boot_ns=now, transport_alive=alive, drive_session='drive', car_fingerprint='vehicle') first = apply(frame) assert first.kind is acc.ObservationKind.VALID second = apply(replace(frame, event_mono_ns=BOOT+1, computed_mono_ns=BOOT+1), BOOT+1) assert first.candidate.observation_identity == second.candidate.observation_identity other_road = replace(frame, event_mono_ns=BOOT+2, computed_mono_ns=BOOT+2, way_id=124) road = apply(other_road, BOOT+2) assert road.candidate.observation_identity != first.candidate.observation_identity restarted = replace(other_road, event_mono_ns=BOOT+3, computed_mono_ns=BOOT+3, producer_session=42) newer = apply(restarted, BOOT+3) assert newer.candidate.observation_identity != road.candidate.observation_identity assert apply(replace(other_road, event_mono_ns=BOOT+4, computed_mono_ns=BOOT+4), BOOT+4).candidate == newer.candidate assert apply(None, BOOT+5, alive=False, received=False).kind is acc.ObservationKind.STALE assert apply(restarted, BOOT-1).candidate is None source.reset() assert apply(restarted, BOOT+6).kind is acc.ObservationKind.VALID def test_copied_reader_is_actual_v2_wire_and_diagnostic_tracker_stays_unqualified(): event = packet() copied = copy_frame(event.mapdOut, event_valid=bool(event.valid), event_boot_ns=int(event.logMonoTime)) assert copied == ms.decode_event(event.to_bytes()) assert ms.MapTracker().step(copied, received=True, now_boot_ns=BOOT).observation.kind is acc.ObservationKind.UNKNOWN assert MapSource().step(copied, received=True, now_boot_ns=BOOT, transport_alive=True, drive_session='drive', car_fingerprint='vehicle').kind is acc.ObservationKind.VALID def test_actual_saved_maps_priority_starts_existing_readonly_provider_without_new_flag(): with tempfile.TemporaryDirectory() as directory: params = Params(directory) cp = NS(notCar=False) assert read_params(params).selection.slots == (sel.Source.VISION, sel.Source.DASHBOARD) assert not shadow_lifecycle.enabled(True, params, cp) params.put('SLCPriority2', 'Map Data', block=True) assert shadow_lifecycle.enabled(True, params, cp) assert not Path(params.get_param_path('MapdShadowEnabled')).exists() params.put_bool('ShowSpeedLimits', False, block=True) assert not shadow_lifecycle.enabled(True, params, cp) params.put_bool('SpeedLimitController', True, block=True) assert shadow_lifecycle.enabled(True, params, cp) assert not shadow_lifecycle.enabled(False, params, cp) params.put('SLCPriority2', 'bad value', block=True) assert not shadow_lifecycle.enabled(True, params, cp) def test_shared_settings_owner_maps_labels_both_slots_defaults_and_ready_change_guard(): with tempfile.TemporaryDirectory() as directory: params = Params(directory) ready = True owner = FeatureSettingsOwner(params, lambda group: True, vehicle_fingerprint=lambda: 'vehicle', vision_development=lambda: True, maps_available=lambda: ready) def row(key): state = owner.snapshot('slc', parked=True, system_long=False, lateral_context=False, metric=False) return next(r for r in state.rows if r.key == key) assert row('SLCPriority1').value == 'Vision' assert row('SLCPriority2').value == 'Dashboard' for key in ('SLCPriority1', 'SLCPriority2'): assert 'Maps' in row(key).choices while row(key).value != 'Maps': request = row_change(row(key)) assert request is not None assert owner.apply(request) assert params.get(key) == 'Map Data' request = row_change(row('SLCPriority1')) assert request is not None ready = False assert not owner.apply(request) assert params.get('SLCPriority1') == 'Map Data' assert 'Maps' not in owner._source_choices() hub = owner.snapshot('hub', parked=True, system_long=False, lateral_context=False, metric=False) pages = [r.page for r in hub.rows] assert pages.index('curve') < pages.index('slc') def test_manager_default_demand_does_not_read_full_slc_settings(): with tempfile.TemporaryDirectory() as directory: params = Params(directory) with patch('openpilot.starpilot.speed_limits.runtime_settings.read_params', side_effect=AssertionError('full scan')): assert not shadow_lifecycle.enabled(True, params, NS(notCar=False)) params.put('SLCPriority1', 'Vision', block=True) params.put('SLCPriority2', 'Dashboard', block=True) assert not shadow_lifecycle.enabled(True, params, NS(notCar=False)) params.put('SLCPriority2', 'Map Data', block=True) with patch('openpilot.starpilot.speed_limits.runtime_settings.read_params', wraps=read_params) as read: assert shadow_lifecycle.enabled(True, params, NS(notCar=False)) assert read.call_count == 1 def test_cached_provider_verification_invalidates_on_replacement_and_source_addition(): from openpilot.starpilot.maps.operation_owner import _PackageReadinessCache # Exercise real file identity changes on Linux-local storage. with tempfile.TemporaryDirectory(dir='/tmp') as directory: root = Path(directory) binary, source = root / 'provider', root / 'source' binary.write_bytes(b'qualified') source.mkdir() calls = [] def validate(): calls.append(True) return binary.read_bytes() == b'qualified' and not (source / 'new.go').exists() def watch(): return (root, binary, source, *sorted(source.iterdir())) ready = _PackageReadinessCache(validate, watch) assert ready() assert ready() and len(calls) == 1 replacement = root / 'replacement' replacement.write_bytes(b'corrupted') replacement.replace(binary) assert not ready() and len(calls) == 2 assert not ready() and len(calls) == 2 prior = binary.stat() binary.write_bytes(b'qualified') # Equal-length writes in one filesystem timestamp tick may retain every # watched metadata field. This tests metadata invalidation, so publish a # distinct mtime explicitly rather than depending on clock resolution. os.utime(binary, ns=(prior.st_atime_ns, prior.st_mtime_ns + 1_000_000_000)) assert binary.stat().st_mtime_ns != prior.st_mtime_ns assert ready() and len(calls) == 3 prior_source = source.stat() (source / 'new.go').write_bytes(b'package main') # Directory additions can share a metadata timestamp tick too. The cache # watches directory identity before enumerating newly added source files. os.utime(source, ns=(prior_source.st_atime_ns, prior_source.st_mtime_ns + 1_000_000_000)) assert source.stat().st_mtime_ns != prior_source.st_mtime_ns assert not ready() and len(calls) == 4 def test_lazy_malformed_reader_revokes_maps_without_escaping_runtime(): import pytest class MalformedReader: @property def sampleVersion(self): raise RuntimeError('lazy segment pointer failure') with pytest.raises(ValueError, match='invalid mapdOut reader'): copy_frame(MalformedReader(), event_valid=True, event_boot_ns=BOOT) settings = parse({'SpeedLimitController': True, 'SLCPriority1': 'Maps', 'SLCPriority2': 'Dashboard'}) runtime = Runtime(settings, session_id='drive') sm = SM(packet()) cp = NS(carFingerprint='vehicle') assert runtime._observations(sm, cp, MONO, BOOT)[sel.Source.MAP].kind is acc.ObservationKind.VALID sm.data['mapdOut'] = MalformedReader() observations = runtime._observations(sm, cp, MONO+1, BOOT+1) assert observations[sel.Source.MAP].candidate is None assert observations[sel.Source.MAP].kind is acc.ObservationKind.UNKNOWN assert sel.select_limit(observations, settings.selection).selected_source is sel.Source.DASHBOARD def test_actual_runtime_maps_display_only_publishes_sign_without_ceiling_or_command(): from openpilot.cereal import messaging from opendbc.car.honda.interface import CarInterface from opendbc.car.honda.values import CAR from openpilot.starpilot.longitudinal.tests.test_cruise_ceiling import messages from openpilot.starpilot.speed_limits.tests.test_runtime_replay import ReplaySM cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC) assert cp.openpilotLongitudinalControl data, envelopes = messages() dashboard = messaging.new_message('slcDashboardObservation') data['slcDashboardObservation'] = dashboard.slcDashboardObservation event = packet() data['mapdOut'] = event.mapdOut sm = ReplaySM(data, MONO) sm.logMonoTime['mapdOut'] = BOOT sm['carState'].vCruiseCluster = 100.0 sm['carState'].vEgoCluster = 20.0 sm['carControl'].enabled = True sm['carControl'].longActive = True settings = parse({'SpeedLimitController': False, 'ShowSpeedLimits': True, 'SLCPriority1': 'Maps', 'SLCPriority2': 'Dashboard'}) assert settings.display and not settings.enabled runtime = Runtime(settings, session_id='display-drive') result = runtime.step(sm, cp, now_ns=MONO, now_boot_ns=BOOT) state = result.message.slcState assert state.displayOnly and not state.enabled assert state.observationKind == 'valid' and state.source == 'map' assert abs(state.speedLimit-13.4112) < .001 assert not state.hasCeiling and result.result.ceiling is None assert result.command is None assert not state.hasPending and not state.hasAccepted # Keep the real cereal envelopes live for the complete reader lifetime. assert len(envelopes) == 7 and dashboard.which() == 'slcDashboardObservation'