mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 09:03:46 +08:00
294 lines
12 KiB
Python
294 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 build_worker
|
|
with patch('openpilot.common.hardware.COMMA_HARDWARE', False), patch('openpilot.starpilot.analytics.runtime.AnalyticsWorker') as factory:
|
|
assert build_worker() is None
|
|
factory.assert_not_called()
|
|
|
|
|
|
def test_process_saves_counters_when_stopped():
|
|
from unittest.mock import patch
|
|
from openpilot.starpilot.analytics import runtime
|
|
obj, values, _source, _clock, client = worker()
|
|
values.pop('UsageStatsState', None)
|
|
obj.stop_event.set()
|
|
with patch.object(runtime, 'build_worker', return_value=obj), patch('signal.signal'):
|
|
runtime.main()
|
|
assert 'UsageStatsState' in values
|
|
client.close.assert_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) == {}
|