mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
487dfc4bf4
Merge bounded control/output FIFO heads using original publisher headers. Preserve original age, native, source, overflow and malformed-input checks; retain producer order after joins. Validated with actual pinned cloud dependencies and fresh-head regression tests.
198 lines
11 KiB
Python
198 lines
11 KiB
Python
"""Actual source reader/parent loop; synthetic transport, no fitted admission."""
|
|
from openpilot.starpilot.flm.tests._drain_fixture import separate_drain_fixture
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
from typing import cast
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from openpilot.starpilot.flm import runtime_adapter_registry as registry, steerlearnerd
|
|
from openpilot.starpilot.flm.fullrate_collector import FullrateCollector, RawBlock, RawPoint
|
|
from openpilot.starpilot.flm.physical_observation import PendingPhysical
|
|
from openpilot.starpilot.flm.tests.test_fixed_manual_baseline import fixed_controls, BASE
|
|
from openpilot.starpilot.flm.tests.test_fullrate_pipeline import SyntheticAdapter, snapshots, serialized_control, serialized_output
|
|
from openpilot.starpilot.lateral.learning_preferences import KEY, replace_preference
|
|
|
|
|
|
def test_actual_saved_read_failure_unknown_then_exact_configuration_recovery():
|
|
with fixed_controls() as (controls, _):
|
|
args = {'now_ns': BASE, 'calibration': (0., 0., 0.), 'vehicle_session': 'source-test', 'calibration_generation': 'a' * 32}
|
|
first = registry.read_source(controls.CP, controls.params, **args)
|
|
assert first.configuration_known and first.enabled and first.source_epoch
|
|
original = registry.read_saved
|
|
with mock.patch.object(registry, 'read_saved', side_effect=lambda params, key, bound:
|
|
(b'', False) if key == KEY else original(params, key, bound)):
|
|
missing = registry.read_source(controls.CP, controls.params, **args)
|
|
assert not missing.configuration_known and not missing.enabled and not missing.source_epoch
|
|
recovered = registry.read_source(controls.CP, controls.params, **args)
|
|
assert steerlearnerd._source_config_binding(recovered) == steerlearnerd._source_config_binding(first)
|
|
|
|
|
|
@pytest.mark.parametrize('raw', (b'', b'{"version":1,"version":1,"vehicles":{}}', b'x' * 4097))
|
|
def test_invalid_actual_preference_is_unknown_not_explicit_off(raw):
|
|
with fixed_controls() as (controls, _):
|
|
controls.params.put(KEY, raw, block=True)
|
|
value = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(0., 0., 0.), vehicle_session='source-test')
|
|
assert not value.configuration_known and not value.enabled and not value.source_epoch
|
|
|
|
|
|
def test_actual_explicit_off_is_known_without_calibration():
|
|
with fixed_controls() as (controls, _):
|
|
controls.params.put(KEY, replace_preference(controls.params.get(KEY), controls.CP, False), block=True)
|
|
value = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(), vehicle_session='source-test')
|
|
assert value.configuration_known and not value.enabled and value.reset == '0'
|
|
assert steerlearnerd._source_config_known(value, BASE)
|
|
|
|
|
|
def test_actual_preference_parsed_from_same_once_captured_bytes_as_epoch():
|
|
with fixed_controls() as (controls, _):
|
|
original, calls = registry.read_saved, []
|
|
def read(params, key, bound):
|
|
raw, readable = original(params, key, bound)
|
|
calls.append(key)
|
|
if key == KEY:
|
|
params.put(KEY, replace_preference(raw, controls.CP, False), block=True)
|
|
return raw, readable
|
|
with mock.patch.object(registry, 'read_saved', side_effect=read), \
|
|
mock.patch('openpilot.starpilot.lateral.learning_preferences.read_saved',
|
|
side_effect=AssertionError('read_source must not reread preference bytes')):
|
|
first = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(0., 0., 0.), vehicle_session='source-test')
|
|
second = registry.read_source(controls.CP, controls.params, now_ns=BASE + 1, calibration=(0., 0., 0.), vehicle_session='source-test')
|
|
assert calls.count(KEY) == 1
|
|
assert first.configuration_known and first.enabled
|
|
assert second.configuration_known and not second.enabled
|
|
|
|
|
|
def exercise(recovery=None, *, publication_uncertain=False, publication_only_recovery=False):
|
|
now, phase = [BASE], [-1]
|
|
key = ('feedforward', 'low', 'steady')
|
|
source = registry.SourceState(True, False, '0', '2' * 64, BASE, BASE + 500_000_000, 'b' * 64,
|
|
(0., 0., 0.), calibration_generation='a' * 32)
|
|
unknown = registry.SourceState(False, False, '', '', BASE, BASE + 500_000_000, 'b' * 64, configuration_known=False)
|
|
objects, published, saves, history = [], [], [], []
|
|
class Adapter(SyntheticAdapter):
|
|
raw_storage = False
|
|
@property
|
|
def event_collection(self):
|
|
return publication_only_recovery and phase[0] == 2
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.current_physical_valid = False
|
|
self.last_physical_ns = 0
|
|
self.fit_profile = None
|
|
self.replay = SimpleNamespace(failed=False, records=('original-frame',), checkpoints=('original-checkpoint',))
|
|
def select_objective(self, value):
|
|
if publication_only_recovery and phase[0] == 2:
|
|
from openpilot.starpilot.flm.event_collection import EventCollectionAdapter
|
|
# Execute the actual event selector guard if the owner calls it without
|
|
# a newly selected control objective; this must never be reached.
|
|
EventCollectionAdapter.select_objective(cast(EventCollectionAdapter, SimpleNamespace(_event_initialized=True)), value)
|
|
raise AssertionError("publication selected an objective before new control")
|
|
def prepare_control(self, value):
|
|
return value
|
|
sm = mock.Mock()
|
|
sm.updated = {'deviceMotion': False}
|
|
sm.logMonoTime = {'deviceMotion': 0}
|
|
def update(timeout=None):
|
|
if timeout == 0:
|
|
return
|
|
phase[0] += 1
|
|
if phase[0] >= 3:
|
|
raise StopIteration
|
|
now[0] = BASE + phase[0] * 300_000_000
|
|
sm.update.side_effect = update
|
|
def read(stamp):
|
|
item = unknown if phase[0] == 1 or (publication_only_recovery and phase[0] == 2) else replace(source, **(recovery or {})) if phase[0] == 2 else source
|
|
return replace(item, observed_ns=stamp, expires_ns=stamp + 500_000_000)
|
|
def publication_read(stamp):
|
|
return replace(unknown if phase[0] == 1 else source, observed_ns=stamp, expires_ns=stamp + 500_000_000)
|
|
def factory(adapter):
|
|
collector = FullrateCollector(adapter)
|
|
objects.append(collector)
|
|
return collector
|
|
c_sock, a_sock = object(), object()
|
|
def drain(socket, callback, receive, sink, stamp):
|
|
if publication_only_recovery and phase[0] == 2:
|
|
return
|
|
control, output, _ = snapshots(now[0] - 1_000_000)
|
|
control = replace(control, source_receipt_ns=now[0], control_ns=now[0], state_ns=now[0] - 1_000_000)
|
|
output = replace(output, control_ns=now[0], state_ns=now[0], apply_ns=now[0])
|
|
callback(serialized_control(control) if socket is c_sock else serialized_output(output), sink, stamp)
|
|
if socket is a_sock and phase[0] == 0 and objects:
|
|
c = objects[0]
|
|
rows = tuple(RawPoint(control.control_ns, output.apply_ns, output.apply_ns + i,
|
|
control, output, {'synthetic_transport_only': i}, ('original-physical',)) for i in range(40))
|
|
c.blocks.append(RawBlock(control.identity, 'original-storage', 'original', control.stage_revision, control.response_revision, rows))
|
|
def send(service, event):
|
|
row = event.autoSteeringLearning.learning
|
|
published.append((phase[0], str(row.status), str(row.reason), row.runtimeProfile.version))
|
|
if objects:
|
|
c = objects[0]
|
|
history.append((phase[0], len(c.blocks), len(c.ready), c.active_since, c.last_qualified_response,
|
|
c.adapter.current_physical_valid, c.adapter.replay.failed))
|
|
fitter = mock.Mock(child=object())
|
|
fitter.poll.return_value = None
|
|
persistence, publisher = mock.Mock(), mock.Mock()
|
|
persistence.save_current.side_effect = lambda *args, **kwargs: saves.append(phase[0])
|
|
publisher.send.side_effect = send
|
|
with mock.patch.object(steerlearnerd, 'drain_pair', separate_drain_fixture(drain)), \
|
|
mock.patch.object(steerlearnerd, 'FullrateCollector', side_effect=factory), \
|
|
mock.patch('openpilot.starpilot.flm.native_health.current_native_health', return_value=True):
|
|
with pytest.raises(StopIteration):
|
|
steerlearnerd.run(SimpleNamespace(), sm, c_sock, a_sock, publisher,
|
|
lambda *args: Adapter(), lambda *args: PendingPhysical(now[0], now[0]),
|
|
(lambda stamp: replace(source, observed_ns=stamp, expires_ns=stamp + 500_000_000)) if publication_uncertain else read,
|
|
lambda _: (key,), lambda _: key, mock.Mock(), persistence, fitter, mock.Mock(), clock=lambda: now[0],
|
|
publication_source_reader=publication_read if publication_uncertain or publication_only_recovery else None)
|
|
return objects, published, saves, history, fitter
|
|
|
|
|
|
def test_actual_owner_uncertainty_keeps_storage_not_current_authority():
|
|
collectors, published, saves, history, fitter = exercise()
|
|
assert len(collectors) == 1 and len(collectors[0].blocks) == 1
|
|
uncertain = next(row for row in history if row[0] == 1)
|
|
assert uncertain[1:] == (1, 0, 0, 0, False, True)
|
|
assert 1 not in saves
|
|
assert any(row[0] == 1 and row[1] == 'unavailable' and row[3] == 0 for row in published)
|
|
assert any(call.args == ('source_configuration_uncertain',) for call in fitter.cancel.call_args_list)
|
|
assert collectors[0].fit_result is None and collectors[0].last_validation is None
|
|
|
|
|
|
@pytest.mark.parametrize('changes', ({'source_epoch': 'd' * 64}, {'reset': '1'},
|
|
{'calibration_generation': 'c' * 32}, {'cp_sha256': 'd' * 64},
|
|
{'manual_baseline': (3., 0., .1)}, {'enabled': False}, {'manual': True}))
|
|
def test_actual_owner_changed_or_explicit_disabled_recovery_purges_storage(changes):
|
|
collectors, _, _, _, _ = exercise(changes)
|
|
assert not collectors[0].blocks
|
|
|
|
|
|
def test_publication_uncertainty_cannot_save_or_publish_admission():
|
|
_, published, saves, _, _ = exercise(publication_uncertain=True)
|
|
assert 1 not in saves
|
|
assert any(row[0] == 1 and row[1] == 'unavailable' and row[3] == 0 for row in published)
|
|
|
|
|
|
|
|
def test_actual_legacy_explicit_off_is_known_without_calibration():
|
|
with fixed_controls() as (controls, _):
|
|
controls.params.remove(KEY)
|
|
controls.params.put_bool('ForceAutoTuneOff', True, block=True)
|
|
source = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(), vehicle_session='source-test')
|
|
assert source.configuration_known and not source.enabled
|
|
|
|
|
|
@pytest.mark.parametrize('changes', ({'observed_ns': BASE + 1}, {'expires_ns': BASE - 1},
|
|
{'expires_ns': BASE + 500_000_001}, {'reset': ''}, {'reset': 'invalid'},
|
|
{'cp_sha256': 'wrong'}, {'enabled': 0}, {'configuration_known': False}))
|
|
def test_malformed_or_stale_receipt_never_qualifies_for_explicit_off_purge(changes):
|
|
source = registry.SourceState(False, False, '0', 'a' * 64, BASE, BASE + 500_000_000, 'b' * 64)
|
|
assert not steerlearnerd._source_config_known(replace(source, **changes), BASE)
|
|
|
|
|
|
def test_actual_owner_publication_recovery_without_new_control_stays_unavailable():
|
|
collectors, published, saves, history, _ = exercise(publication_only_recovery=True)
|
|
assert len(collectors) == 1 and len(collectors[0].blocks) == 1
|
|
assert 2 not in saves
|
|
assert any(row[0] == 2 and row[1] == 'unavailable' and row[3] == 0 for row in published)
|
|
assert next(row for row in history if row[0] == 2)[2:] == (0, 0, 0, False, True)
|