mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-10 14:33:42 +08:00
ca183df497
StarPilotAdapter subclasses jetlink's Stock: StarPilot's param names, the AOL engagement rule, the model root and a CatalogModelState history reset that keeps its JIT buffers. jetlinkd runs jetlink's owner as it is, modeld calls the hooks, and Galaxy and analytics read jetlink's runtime record in place of JetlinkRuntime. - no big-model slot or catalog keys: jetlink provisions its default model - jetlink restores the owner's tuning itself; the port draw stays cut with a host plugged in - the Jetson is asked to power off on DoShutdown only, capped at 30 s
282 lines
12 KiB
Python
282 lines
12 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
from openpilot.starpilot.analytics.runtime import AnalyticsWorker
|
|
|
|
|
|
def jetlink(record):
|
|
"""Jetlink's hooks with runtime() answering `record`."""
|
|
return SimpleNamespace(runtime=lambda: record)
|
|
|
|
|
|
class Source:
|
|
def __init__(self):
|
|
self.data = {'deviceState': SimpleNamespace(started=False), 'carState': SimpleNamespace(vEgo=10),
|
|
'carControl': SimpleNamespace(latActive=True, longActive=False),
|
|
'selfdriveState': SimpleNamespace(enabled=False), 'modelV2': SimpleNamespace(big=False)}
|
|
self.valid = dict.fromkeys(self.data, True)
|
|
self.alive = dict(self.valid)
|
|
self.logMonoTime = dict.fromkeys(self.data, 1_000_000_000)
|
|
self.valid.update(carParams=False, gpsLocationExternal=False)
|
|
self.alive.update(carParams=False, gpsLocationExternal=False)
|
|
self.logMonoTime.update(carParams=0, gpsLocationExternal=0)
|
|
|
|
def __getitem__(self, key):
|
|
return self.data[key]
|
|
|
|
def update(self, timeout):
|
|
pass
|
|
|
|
|
|
def worker():
|
|
values = {'IsOffroad': True}
|
|
params = Mock()
|
|
params.get.side_effect = values.get
|
|
params.get_bool.side_effect = lambda key: values.get(key, False)
|
|
params.put.side_effect = lambda key, value, **kwargs: values.__setitem__(key, value)
|
|
source = Source()
|
|
clock = [1.0]
|
|
client = Mock()
|
|
client.send.return_value = True
|
|
obj = AnalyticsWorker(params, source, collector=lambda *a: {}, token_reader=lambda: 'token',
|
|
client=client, commit_reader=lambda *a, **k: None, clock_valid=lambda: True, monotonic=lambda: clock[0], wall=lambda: 1_800_000_000,
|
|
identity=lambda *a: {'driving_model': 'executed'})
|
|
return obj, values, source, clock, client
|
|
|
|
|
|
def test_initial_send_and_offroad_rechecked_at_transport():
|
|
obj, values, source, clock, client = worker()
|
|
obj.poll_once()
|
|
assert client.send.call_count == 1
|
|
gate = client.send.call_args.kwargs['allowed']
|
|
source.data['deviceState'].started = True
|
|
assert not gate()
|
|
source.data['deviceState'].started = False
|
|
values['IsOffroad'] = False
|
|
assert not gate()
|
|
|
|
|
|
def test_driving_snapshot_survives_offroad_clear_and_counts_actual_aol():
|
|
obj, values, source, clock, client = worker()
|
|
obj.poll_once()
|
|
source.data['deviceState'].started = True
|
|
values['IsOffroad'] = False
|
|
clock[0] = 1.1
|
|
obj.poll_once()
|
|
assert client.send.call_count == 1
|
|
assert obj.counters['drives'] == 1
|
|
assert abs(obj.counters['total_aol_seconds'] - .1) < 1e-6
|
|
source.valid['modelV2'] = False
|
|
source.data['deviceState'].started = False
|
|
values['IsOffroad'] = True
|
|
clock[0] = 1.2
|
|
obj.poll_once()
|
|
assert b'driving_model=executed' in client.send.call_args.args[0]
|
|
|
|
|
|
def test_missing_credentials_and_stale_source_never_send():
|
|
obj, values, source, clock, client = worker()
|
|
obj.token_reader = lambda: None
|
|
obj.poll_once()
|
|
client.send.assert_not_called()
|
|
assert values['UsageStatsStatus']['state'] == 'awaiting_credentials'
|
|
obj.token_reader = lambda: 'token'
|
|
clock[0] = 40
|
|
obj.poll_once()
|
|
client.send.assert_not_called()
|
|
|
|
|
|
def test_failures_have_finite_parked_retry_budget_and_bounded_stop():
|
|
obj, values, source, clock, client = worker()
|
|
client.send.return_value = False
|
|
for _ in range(10):
|
|
source.logMonoTime = dict.fromkeys(source.logMonoTime, int(clock[0] * 1e9))
|
|
obj.poll_once()
|
|
clock[0] += 301
|
|
assert client.send.call_count == 5
|
|
obj.start()
|
|
obj.close()
|
|
assert not obj.thread.is_alive()
|
|
|
|
|
|
def test_startup_onroad_never_contacts_transport():
|
|
obj, values, source, clock, client = worker()
|
|
source.data['deviceState'].started = True
|
|
values['IsOffroad'] = False
|
|
obj.poll_once()
|
|
client.send.assert_not_called()
|
|
source.data['deviceState'].started = False
|
|
values['IsOffroad'] = True
|
|
obj.poll_once()
|
|
assert client.send.call_count == 1
|
|
|
|
|
|
def test_host_optional_hook_does_not_construct_network_worker():
|
|
from unittest.mock import patch
|
|
from openpilot.starpilot.analytics.runtime import start_optional_worker
|
|
with patch('openpilot.common.hardware.COMMA_HARDWARE', False), patch('openpilot.starpilot.analytics.runtime.AnalyticsWorker') as factory:
|
|
assert start_optional_worker() is None
|
|
factory.assert_not_called()
|
|
|
|
|
|
def test_malformed_saved_state_has_finite_sanitized_counters():
|
|
obj, values, source, clock, client = worker()
|
|
values['UsageStatsState'] = {'version': 1, 'counters': 'bad', 'region': {'latitude': 12.345678}, 'extra': 'x' * 100000}
|
|
other = AnalyticsWorker(obj.params, source, client=client, collector=lambda *a: {}, token_reader=lambda: None,
|
|
monotonic=lambda: clock[0], clock_valid=lambda: True)
|
|
assert sum(other.counters.values()) == 0
|
|
assert 'extra' not in other.state and 'region' not in other.state
|
|
|
|
|
|
def test_registered_native_params_roundtrip_and_restart_context():
|
|
import tempfile
|
|
from openpilot.common.params import Params
|
|
from pathlib import Path
|
|
from openpilot.starpilot.state_migration import prepare_manager_start
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(str(Path(directory) / 'params'))
|
|
storage = Path(directory) / 'recovery'
|
|
prepare_manager_start(params, storage)
|
|
params.put_bool('IsOffroad', True, block=True)
|
|
source = Source()
|
|
obj = AnalyticsWorker(params, source, collector=lambda *a: {}, client=Mock(), token_reader=lambda: None,
|
|
monotonic=lambda: 1.0, clock_valid=lambda: True)
|
|
obj.context = {'car_make': 'gm', 'car_model': 'actual', 'driving_model': 'verified'}
|
|
obj.persist()
|
|
assert isinstance(params.get('UsageStatsState'), dict)
|
|
saved = params.get('UsageStatsState')
|
|
prepare_manager_start(params, storage, dry_run=True)
|
|
prepare_manager_start(params, storage)
|
|
assert params.get('UsageStatsState') == saved
|
|
restored = AnalyticsWorker(params, source, collector=lambda *a: {}, client=Mock(), token_reader=lambda: None,
|
|
monotonic=lambda: 1.0, clock_valid=lambda: True)
|
|
assert restored.context['driving_model'] == 'verified'
|
|
assert restored.metadata['dongle_id'] == obj.metadata['dongle_id']
|
|
restored.poll_once()
|
|
restored.client.send.assert_not_called()
|
|
del restored, obj, params
|
|
|
|
|
|
def test_legacy_sharing_value_cannot_disable_automatic_report():
|
|
obj, values, source, clock, client = worker()
|
|
values['ShareUsageStats'] = False
|
|
obj.poll_once()
|
|
assert client.send.call_count == 1
|
|
assert obj.allowed()
|
|
|
|
|
|
def test_executed_model_reports_verified_fallback_not_saved_request():
|
|
from unittest.mock import patch
|
|
from openpilot.starpilot.analytics.runtime import executed_model
|
|
from openpilot.starpilot.models.catalog import BY_ID
|
|
from openpilot.starpilot.models.status import ModelHealth, ModelVariant
|
|
source = Source()
|
|
status = SimpleNamespace(health=ModelHealth.ACTIVE, loaded_id='rdf43', artifact_sha256='a' * 64,
|
|
variant=ModelVariant.SMALL, requested_id='some-selected-large-model')
|
|
with patch('openpilot.starpilot.models.jetlink_adapter.hooks', return_value=jetlink(None)), \
|
|
patch('openpilot.starpilot.models.runtime.snapshot', return_value=status):
|
|
result = executed_model(source, Mock(), 1_000_000_000)
|
|
assert result['driving_model'] == BY_ID['rdf43'].name
|
|
assert result['metrics']['model_variant'] == 'small'
|
|
assert 'some-selected-large-model' not in str(result)
|
|
source.data['modelV2'].big = True
|
|
with patch('openpilot.starpilot.models.jetlink_adapter.hooks',
|
|
return_value=jetlink(SimpleNamespace(active=True, sha256='b' * 64))):
|
|
remote = executed_model(source, Mock(), 1_000_000_000)
|
|
assert remote['metrics']['model_sha256'] == 'b' * 64
|
|
assert remote['metrics']['model_variant'] == 'jetlink'
|
|
|
|
|
|
def test_new_drive_clears_prior_identity_brief_unavailable_retains_last_used():
|
|
obj, values, source, clock, client = worker()
|
|
obj.poll_once()
|
|
source.data['deviceState'].started = True
|
|
values['IsOffroad'] = False
|
|
obj.poll_once()
|
|
assert obj.context['driving_model'] == 'executed'
|
|
obj.identity = lambda *a: {}
|
|
clock[0] = 2.0
|
|
source.logMonoTime['modelV2'] = 2_000_000_000
|
|
source.logMonoTime['deviceState'] = 2_000_000_000
|
|
obj.poll_once()
|
|
assert obj.context['driving_model'] == 'executed'
|
|
source.data['deviceState'].started = False
|
|
values['IsOffroad'] = True
|
|
obj.poll_once()
|
|
source.data['deviceState'].started = True
|
|
values['IsOffroad'] = False
|
|
obj.poll_once()
|
|
assert 'driving_model' not in obj.context
|
|
obj.identity = lambda *a: {'driving_model': 'verified-fallback'}
|
|
clock[0] = 3.1
|
|
source.logMonoTime['modelV2'] = 3_100_000_000
|
|
source.logMonoTime['deviceState'] = 3_100_000_000
|
|
obj.poll_once()
|
|
assert obj.context['driving_model'] == 'verified-fallback'
|
|
|
|
|
|
def test_new_month_offroad_does_not_report_old_month_distance():
|
|
obj, values, source, clock, client = worker()
|
|
obj.state['month'] = '2000-01'
|
|
obj.counters['current_months_meters'] = 1000
|
|
obj.poll_once()
|
|
assert obj.counters['current_months_meters'] == 0
|
|
|
|
|
|
def test_executed_model_real_message_catalog_identity_and_actual_small_fallback():
|
|
from unittest.mock import patch
|
|
from openpilot.cereal import messaging
|
|
from openpilot.starpilot.analytics.runtime import executed_model
|
|
from openpilot.starpilot.models.catalog import BY_ID
|
|
from openpilot.starpilot.models.status import ModelHealth, ModelStatus, ModelVariant
|
|
source = Source()
|
|
source.data['modelV2'] = messaging.new_message('modelV2').modelV2
|
|
status = ModelStatus('selected-large', 'rdf43', ModelVariant.SMALL, ModelHealth.ACTIVE, True,
|
|
'large model unavailable', 'a' * 64)
|
|
with patch('openpilot.starpilot.models.jetlink_adapter.hooks', return_value=jetlink(None)), \
|
|
patch('openpilot.starpilot.models.runtime.snapshot', return_value=status) as projection:
|
|
params = Mock()
|
|
result = executed_model(source, params, 1_000_000_000)
|
|
projection.assert_called_once_with(source, params, 1_000_000_000)
|
|
assert result == {'driving_model': BY_ID['rdf43'].name,
|
|
'metrics': {'model_sha256': 'a' * 64, 'model_variant': 'small'}}
|
|
assert 'selected-large' not in str(result)
|
|
|
|
|
|
def test_executed_model_remote_requires_actual_big_output_not_remote_selection():
|
|
from unittest.mock import patch
|
|
from openpilot.cereal import messaging
|
|
from openpilot.starpilot.analytics.runtime import executed_model
|
|
from openpilot.starpilot.models.catalog import BY_ID
|
|
from openpilot.starpilot.models.status import ModelHealth, ModelStatus, ModelVariant
|
|
source = Source()
|
|
source.data['modelV2'] = messaging.new_message('modelV2').modelV2
|
|
small = ModelStatus('selected-large', 'rdf43', ModelVariant.SMALL, ModelHealth.ACTIVE, False, None, 'a' * 64)
|
|
remote = SimpleNamespace(active=True, sha256='b' * 64)
|
|
with patch('openpilot.starpilot.models.jetlink_adapter.hooks', return_value=jetlink(remote)), \
|
|
patch('openpilot.starpilot.models.runtime.snapshot', return_value=small) as projection:
|
|
source.data['modelV2'].big = True
|
|
big = executed_model(source, Mock(), 1_000_000_000)
|
|
projection.assert_not_called()
|
|
assert big == {'driving_model': 'jetlink:' + 'b' * 16,
|
|
'metrics': {'model_sha256': 'b' * 64, 'model_variant': 'jetlink'}}
|
|
source.data['modelV2'].big = False
|
|
fallback = executed_model(source, Mock(), 1_000_000_000)
|
|
assert fallback['driving_model'] == BY_ID['rdf43'].name
|
|
assert fallback['metrics']['model_sha256'] == 'a' * 64
|
|
|
|
|
|
def test_executed_model_unverified_receipt_never_reports_requested_identity():
|
|
from unittest.mock import patch
|
|
from openpilot.cereal import messaging
|
|
from openpilot.starpilot.analytics.runtime import executed_model
|
|
from openpilot.starpilot.models.status import ModelHealth, ModelStatus
|
|
source = Source()
|
|
source.data['modelV2'] = messaging.new_message('modelV2').modelV2
|
|
for health in (ModelHealth.UNAVAILABLE, ModelHealth.IDENTITY_UNAVAILABLE, ModelHealth.LOADING,
|
|
ModelHealth.STALE, ModelHealth.FAILED):
|
|
status = ModelStatus('selected-large', None, None, health, True, 'unverified', None)
|
|
with patch('openpilot.starpilot.models.jetlink_adapter.hooks', return_value=jetlink(None)), \
|
|
patch('openpilot.starpilot.models.runtime.snapshot', return_value=status):
|
|
assert executed_model(source, Mock(), 1_000_000_000) == {}
|