mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +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.
168 lines
7.9 KiB
Python
168 lines
7.9 KiB
Python
"""Actual parent/readmit/wire lifecycle with existing explicit profile fixture.
|
|
|
|
NOT a route-fit/registered-native/model qualification. BoundaryAdapter is the
|
|
existing synthetic lifecycle adapter; scientific validation is not invented.
|
|
"""
|
|
from openpilot.starpilot.flm.tests._drain_fixture import separate_drain_fixture
|
|
from dataclasses import asdict, replace
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from openpilot.cereal import messaging
|
|
from openpilot.starpilot.flm import steerlearnerd
|
|
from openpilot.starpilot.flm.adaptive_profile import replay_accepts
|
|
from openpilot.starpilot.flm.canonical_surface_adapter import GROUPS, parameters_for
|
|
from openpilot.starpilot.flm.event_collection import event_key, event_keys
|
|
from openpilot.starpilot.flm.event_collector import EventFullrateCollector
|
|
from openpilot.starpilot.flm.learner_factory import decode_fit_result
|
|
from openpilot.starpilot.flm.learning_wire import read_runtime_admission
|
|
from openpilot.starpilot.flm.runtime_adapter_registry import SourceState
|
|
from openpilot.starpilot.flm.tests.test_event_target_renewal import BoundaryAdapter
|
|
from openpilot.starpilot.flm.tests.test_full_surface_profile import base, wire_profile
|
|
from openpilot.starpilot.flm.tests.test_fullrate_pipeline import BASE, snapshots, serialized_control, serialized_output
|
|
|
|
|
|
def exercise(*, no_new_pose=False):
|
|
old_key, new_key = ('event', 'deadband.high'), ('event', 'center')
|
|
full = base(GROUPS[old_key[1]]).with_proposal({'center_deadband_fast_deg': .005, 'center_deadband_highway_deg': .005})
|
|
profile, validation = wire_profile(full)
|
|
profile = replace(profile, observed_ns=BASE+2_500_000_000, expires_ns=BASE+3_000_000_000)
|
|
validation = replace(validation, profile=profile)
|
|
report = {'status': 'qualified', 'reason': 'existing-explicit-lifecycle-fixture',
|
|
'profile': asdict(profile), 'validation': asdict(validation)}
|
|
decoded = decode_fit_result(report)
|
|
assert decoded is not None and decoded[0].profile == profile and decoded[1] == validation
|
|
phase, now = [-1], [BASE]
|
|
installs, admitted, sent, collectors = [], [], [], []
|
|
|
|
class Adapter(BoundaryAdapter):
|
|
event_collection = True
|
|
raw_storage = False
|
|
target_changed = False
|
|
def __init__(self):
|
|
super().__init__(profile)
|
|
self.envelope = full
|
|
self.key = old_key
|
|
self.installed = False
|
|
def prepare_control(self, evidence):
|
|
return evidence
|
|
def select_objective(self, key):
|
|
self.key = key
|
|
def parameters(self):
|
|
return parameters_for(GROUPS[self.key[1]])
|
|
def install_fit(self, offered, result, proof, *, physical_context):
|
|
assert offered == report and self.key == old_key
|
|
assert result.profile == profile and replay_accepts(proof, profile)
|
|
assert physical_context == ('cal', 'vm', 'lag', 0)
|
|
installs.append((phase[0], self.key))
|
|
# Existing adapter lifecycle boundary: installing a new model requires a
|
|
# later independent pose. Repeated SAME immutable proposal cannot reset it.
|
|
if not self.installed:
|
|
self.current_physical_valid, self.last_physical_ns = False, 0
|
|
self.installed = True
|
|
self.fit_profile = profile
|
|
return True
|
|
def current_admission(self, profile, control, output, context):
|
|
result = super().current_admission(profile, control, output, context)
|
|
admitted.append((phase[0], result.healthy, self.last_physical_ns))
|
|
return result
|
|
|
|
class Collector(EventFullrateCollector):
|
|
def __init__(self, adapter):
|
|
super().__init__(adapter)
|
|
self.offered = False
|
|
collectors.append(self)
|
|
def job_snapshot(self, now_ns):
|
|
if phase[0] == 50 and not self.offered and self.ready and self.physical_context is not None:
|
|
self.offered = True
|
|
return {'unit_transport_only': True}
|
|
return None
|
|
|
|
class Fitter:
|
|
child = None
|
|
def __init__(self):
|
|
self.token = None
|
|
self.complete = False
|
|
def submit(self, snapshot, *, token, now_ns):
|
|
assert phase[0] == 50 and snapshot == {'unit_transport_only': True}
|
|
self.token, self.child = token, object()
|
|
return True
|
|
def poll(self, *, token, now_ns):
|
|
if self.child is not None and phase[0] == 51:
|
|
assert token == self.token
|
|
self.child, self.complete = None, True
|
|
return report
|
|
return None
|
|
def cancel(self, reason):
|
|
self.child = None
|
|
def close(self, reason):
|
|
self.child = None
|
|
|
|
csock, osock = object(), object()
|
|
fitter, sm, persistence = Fitter(), mock.Mock(updated={'deviceMotion': False}, logMonoTime={'deviceMotion': 0}), mock.Mock()
|
|
def update(timeout=None):
|
|
if timeout == 0:
|
|
return
|
|
phase[0] += 1
|
|
if phase[0] == 67:
|
|
raise StopIteration
|
|
now[0] = BASE + phase[0]*50_000_000
|
|
sm.update.side_effect = update
|
|
def source(stamp):
|
|
return SourceState(True, False, '0', full.source_epoch, stamp, stamp+500_000_000,
|
|
full.cp_sha256, (0.,0.,0.), configuration_known=True)
|
|
def rows():
|
|
stamp = now[0]-1_000_000
|
|
control, output, physical = snapshots(stamp)
|
|
control = replace(control, identity=profile.identity, baseline_revision='a'*64, source_epoch=full.source_epoch,
|
|
controller_revision=full.controller_revision, source_receipt_ns=stamp,
|
|
replay_inputs=(('control_frame', float(5949+phase[0])),))
|
|
return control, output, physical
|
|
def drain(socket, callback, receive, sink, stamp):
|
|
control, output, _ = rows()
|
|
callback(serialized_control(control) if socket is csock else serialized_output(output), sink, stamp)
|
|
def physical(*args):
|
|
# Before and at the installation publication there are genuine fixture poses;
|
|
# only the negative case withdraws all LATER poses after the flags reset.
|
|
return None if no_new_pose and phase[0] > 55 else rows()[2]
|
|
def send(service, message):
|
|
wire = messaging.log_from_bytes(message.to_bytes())
|
|
admission = read_runtime_admission(wire)
|
|
sent.append((phase[0], admission))
|
|
publisher = mock.Mock()
|
|
publisher.send.side_effect = send
|
|
with mock.patch.object(steerlearnerd, 'drain_pair', separate_drain_fixture(drain)), \
|
|
mock.patch('openpilot.starpilot.flm.event_collector.EventFullrateCollector', Collector), \
|
|
mock.patch('openpilot.starpilot.flm.native_health.current_native_health', return_value=True):
|
|
with pytest.raises(StopIteration):
|
|
steerlearnerd.run(SimpleNamespace(), sm, csock, osock, publisher, lambda *args: Adapter(), physical, source,
|
|
event_keys, event_key, decode_fit_result, persistence, fitter, mock.Mock(), clock=lambda: now[0])
|
|
assert fitter.complete and len(collectors) == 1
|
|
assert collectors[0].adapter.key == new_key
|
|
assert installs and installs[0] == (55, old_key)
|
|
assert admitted[0][0] == 55 and admitted[0][1] is False
|
|
assert all(admission is None for tick, admission in sent if tick <= 55)
|
|
return profile, validation, sent, admitted
|
|
|
|
|
|
def test_pending_old_group_fixture_applies_after_next_pose_without_cycle_wait():
|
|
original, proof, sent, admissions = exercise()
|
|
qualified = [(tick, pair) for tick, pair in sent if pair is not None]
|
|
assert qualified and qualified[0][0] == 60 # active selector has been CENTER since51.
|
|
assert any(tick >= 60 and healthy and response > 0 for tick, healthy, response in admissions)
|
|
for _, (profile, validation) in qualified:
|
|
assert profile.coefficients == original.coefficients
|
|
assert profile.train_ids == original.train_ids and profile.heldout_ids == original.heldout_ids
|
|
assert profile.effect_envelope == original.effect_envelope
|
|
assert validation.baseline_revision == proof.baseline_revision
|
|
assert validation.physical_model_revision == proof.physical_model_revision
|
|
assert profile.observed_ns > original.observed_ns and profile.expires_ns-profile.observed_ns == 500_000_000
|
|
|
|
|
|
def test_pending_successful_install_without_next_pose_never_applies():
|
|
_, _, sent, admissions = exercise(no_new_pose=True)
|
|
assert all(admission is None for _, admission in sent)
|
|
assert not any(healthy for _, healthy, _ in admissions)
|