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.
260 lines
11 KiB
Python
260 lines
11 KiB
Python
"""Actual daemon loop and cereal publication; fake actor does not model permission."""
|
|
from openpilot.starpilot.flm.tests._drain_fixture import separate_drain_fixture
|
|
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
import pytest
|
|
from collections.abc import Callable
|
|
from typing import NoReturn, cast
|
|
from openpilot.starpilot.flm import steerlearnerd
|
|
from openpilot.starpilot.flm.runtime_adapter_registry import SourceState
|
|
from openpilot.starpilot.flm.tests.test_live_trial_owner import offer
|
|
|
|
BASE = 10_000_000_000
|
|
|
|
|
|
def source(now):
|
|
return SourceState(True, False, 'a' * 64, 'b' * 64, now, now + 500_000_000, 'c' * 64, (0.0, 0.0, 0.0), 'd' * 32, 'e' * 64)
|
|
|
|
|
|
class NoLegacyFitter:
|
|
def __init__(self):
|
|
self.closed = False
|
|
|
|
def poll(self, **kwargs):
|
|
cast(Callable[[str], NoReturn], pytest.fail)('live daemon must never poll legacy full FD fitter')
|
|
|
|
def submit(self, *args, **kwargs):
|
|
cast(Callable[[str], NoReturn], pytest.fail)('live daemon must never submit legacy full FD fitter')
|
|
|
|
@property
|
|
def child(self):
|
|
cast(Callable[[str], NoReturn], pytest.fail)('live daemon must never inspect legacy fitter child')
|
|
|
|
def close(self, reason):
|
|
self.closed = True
|
|
|
|
|
|
class RecordingActor:
|
|
reason = 'production_bootstrap_budget_not_yet_qualified'
|
|
ingress = None
|
|
pending = False
|
|
certificate = None
|
|
|
|
def __init__(self):
|
|
self.calls = []
|
|
self.closed = False
|
|
|
|
def tick(self, collector, current_source, **kwargs):
|
|
self.calls.append((collector, current_source, kwargs))
|
|
return None
|
|
|
|
def persistence_inputs(self, now_ns):
|
|
return None
|
|
|
|
def close(self, reason):
|
|
self.closed = True
|
|
|
|
|
|
class LoopComplete(Exception):
|
|
pass
|
|
|
|
|
|
@pytest.mark.parametrize('shutdown_reaped', (True, False))
|
|
def test_actual_daemon_publishes_separate20_and4_without_legacy_fit(monkeypatch, shutdown_reaped):
|
|
now = [BASE - 50_000_000]
|
|
|
|
class SM:
|
|
updated = {'deviceMotion': False}
|
|
logMonoTime = {'deviceMotion': 0}
|
|
|
|
def update(self, timeout=None):
|
|
now[0] += 50_000_000
|
|
if now[0] > BASE + 1_000_000_000:
|
|
raise LoopComplete
|
|
self.updated['deviceMotion'] = True
|
|
self.logMonoTime['deviceMotion'] = now[0]
|
|
|
|
sent = []
|
|
pm = SimpleNamespace(send=lambda topic, message: sent.append((now[0], topic, message.as_reader())))
|
|
actor, fitter = RecordingActor(), NoLegacyFitter()
|
|
original_close = actor.close
|
|
|
|
def actor_close(reason):
|
|
original_close(reason)
|
|
return shutdown_reaped
|
|
|
|
monkeypatch.setattr(actor, 'close', actor_close)
|
|
# No control/native is invented: this exercises actual collecting publications.
|
|
monkeypatch.setattr(steerlearnerd, 'drain_pair', lambda *args: None)
|
|
with pytest.raises(LoopComplete if shutdown_reaped else RuntimeError):
|
|
steerlearnerd.run(
|
|
SimpleNamespace(),
|
|
SM(),
|
|
object(),
|
|
object(),
|
|
pm,
|
|
None,
|
|
None,
|
|
lambda stamp: source(stamp),
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
fitter,
|
|
None,
|
|
clock=lambda: now[0],
|
|
live_actor=actor,
|
|
)
|
|
tracking = [(stamp, msg) for stamp, topic, msg in sent if topic == 'starpilotSteeringTracking']
|
|
legacy = [(stamp, msg) for stamp, topic, msg in sent if topic == 'autoSteeringLearning']
|
|
assert len(tracking) == 21 and len(legacy) == 5
|
|
assert all(b[0] - a[0] == 50_000_000 for a, b in zip(tracking, tracking[1:], strict=False))
|
|
assert all(b[0] - a[0] == 250_000_000 for a, b in zip(legacy, legacy[1:], strict=False))
|
|
assert all(msg.starpilotSteeringTracking.version == 1 and msg.starpilotSteeringTracking.trial.version == 0 for _, msg in tracking)
|
|
assert all(msg.autoSteeringLearning.learning.status == 'collecting' for _, msg in legacy)
|
|
assert all(call[0] is None and call[2]['current_native'] is False for call in actor.calls)
|
|
assert actor.closed and fitter.closed
|
|
|
|
|
|
def test_tracking_serialization_never_renews_existing_proof_or_fabricates_native():
|
|
pending = offer(BASE)
|
|
initial = steerlearnerd.tracking_message(source(BASE), now_ns=BASE, current_native=True, offer=pending)
|
|
renewed = steerlearnerd.tracking_message(source(BASE + 50_000_000), now_ns=BASE + 50_000_000, current_native=True, offer=pending)
|
|
for message in (initial, renewed):
|
|
row = message.starpilotSteeringTracking.trial
|
|
assert row.observedMonoTime == pending.observed_ns and row.validUntilMonoTime == pending.expires_ns
|
|
assert row.deadlineMonoTime == pending.deadline_ns and row.sequence == pending.sequence
|
|
expired = steerlearnerd.tracking_message(source(BASE + 110_000_000), now_ns=BASE + 110_000_000, current_native=True, offer=pending)
|
|
denied = steerlearnerd.tracking_message(source(BASE), now_ns=BASE, current_native=False, offer=pending)
|
|
manual = steerlearnerd.tracking_message(replace(source(BASE), manual=True), now_ns=BASE, current_native=True, offer=pending)
|
|
assert all(message.starpilotSteeringTracking.trial.version == 0 for message in (expired, denied, manual))
|
|
|
|
|
|
def _run_poll_script(monkeypatch, rows, *, expire_during_drain=False):
|
|
now = [BASE]
|
|
|
|
class SM:
|
|
def __init__(self):
|
|
self.updated = {'deviceMotion': False}
|
|
self.logMonoTime = {'deviceMotion': 0}
|
|
self.valid = {'deviceMotion': False}
|
|
self.index = self.zero_polls = 0
|
|
|
|
def update(self, timeout=None):
|
|
if timeout == 0:
|
|
self.zero_polls += 1
|
|
self.updated['deviceMotion'] = False
|
|
return
|
|
if self.index == len(rows):
|
|
raise LoopComplete
|
|
stamp, header, valid = rows[self.index]
|
|
self.index += 1
|
|
now[0] = stamp
|
|
self.updated['deviceMotion'] = header is not None
|
|
if header is not None:
|
|
self.logMonoTime['deviceMotion'] = header
|
|
self.valid['deviceMotion'] = valid
|
|
|
|
sm = SM()
|
|
actor, fitter = RecordingActor(), NoLegacyFitter()
|
|
sent = []
|
|
pm = SimpleNamespace(send=lambda topic, message: sent.append((now[0], topic, message.as_reader())))
|
|
expired = False
|
|
|
|
def drain(*args):
|
|
nonlocal expired
|
|
if expire_during_drain and not expired:
|
|
# Trigger the REAL source_current expiry path and its update(0). No
|
|
# physical/native/controller evidence is injected into this loop test.
|
|
now[0] += 600_000_000
|
|
expired = True
|
|
|
|
monkeypatch.setattr(steerlearnerd, 'drain_pair', separate_drain_fixture(drain))
|
|
with pytest.raises(LoopComplete):
|
|
steerlearnerd.run(SimpleNamespace(), sm, object(), object(), pm, None, None,
|
|
lambda stamp: source(stamp), None, None, None, None, fitter, None,
|
|
clock=lambda: now[0], live_actor=actor)
|
|
assert actor.closed and fitter.closed
|
|
assert all(call[0] is None and call[2]['current_native'] is False for call in actor.calls)
|
|
return sent, sm
|
|
|
|
|
|
def _tracking_frequency(sent):
|
|
from openpilot.cereal.messaging import FrequencyTracker
|
|
tracker = FrequencyTracker(20., 100., False) # Actual Controls receiver: selfdriveState100Hz poll.
|
|
for stamp, topic, _message in sent:
|
|
if topic == 'starpilotSteeringTracking':
|
|
tracker.record_recv_time(stamp/1e9)
|
|
return tracker
|
|
|
|
|
|
def test_actual_run_tracking_survives_alternating_poll_jitter(monkeypatch):
|
|
# Existing reset-to-now >=50ms gate drops every49ms interval, yielding10Hz.
|
|
# These are21 genuine20Hz source deliveries with realistic poll jitter.
|
|
offsets = [0]
|
|
for index in range(20):
|
|
offsets.append(offsets[-1]+(49_000_000 if index % 2 == 0 else 51_000_000))
|
|
rows = tuple((BASE+offset, BASE+offset, True) for offset in offsets)
|
|
sent, _sm = _run_poll_script(monkeypatch, rows)
|
|
tracking = [(stamp, message) for stamp, topic, message in sent if topic == 'starpilotSteeringTracking']
|
|
assert [stamp for stamp, _ in tracking] == [stamp for stamp, _header, _valid in rows]
|
|
assert _tracking_frequency(sent).valid
|
|
assert all(message.valid and message.starpilotSteeringTracking.version == 1 and
|
|
message.starpilotSteeringTracking.trial.version == 0 for _, message in tracking)
|
|
|
|
|
|
def test_actual_run_neutral_status_does_not_promote_duplicate_regressed_future_or_missing_input(monkeypatch):
|
|
rows = (
|
|
(BASE, BASE, True),
|
|
(BASE+49_000_000, BASE+49_000_000, True),
|
|
(BASE+100_000_000, BASE+100_000_000, True),
|
|
(BASE+149_000_000, BASE+100_000_000, True), # Actual duplicate header.
|
|
(BASE+200_000_000, None, False), # Poll timeout, no new source delivery.
|
|
(BASE+250_000_000, BASE+90_000_000, True), # Regressed source header.
|
|
(BASE+300_000_000, BASE+350_000_000, True), # Future source header.
|
|
(BASE+350_000_000, 0, False),
|
|
(BASE+400_000_000, BASE+400_000_000, False), # Genuine invalid raw packet: neutral status remains observable.
|
|
(BASE+449_000_000, BASE+449_000_000, True),
|
|
)
|
|
sent, _sm = _run_poll_script(monkeypatch, rows)
|
|
tracking = [(stamp, message) for stamp, topic, message in sent if topic == 'starpilotSteeringTracking']
|
|
assert [stamp for stamp, _ in tracking] == [stamp for stamp, _header, _valid in rows]
|
|
assert _tracking_frequency(sent).valid # Settings transport health is independent of physical pose delivery.
|
|
assert all(message.starpilotSteeringTracking.trial.version == 0 and
|
|
message.starpilotSteeringTracking.restore.version == 0 and
|
|
message.starpilotSteeringTracking.sourceReceipt.observedMonoTime == stamp and
|
|
message.starpilotSteeringTracking.sourceReceipt.validUntilMonoTime == stamp + 500_000_000
|
|
for stamp, message in tracking)
|
|
|
|
|
|
def test_actual_run_captures_polled_header_before_source_current_zero_poll(monkeypatch):
|
|
sent, sm = _run_poll_script(monkeypatch, ((BASE, BASE, True),), expire_during_drain=True)
|
|
assert sm.zero_polls >= 1 and sm.updated['deviceMotion'] is False
|
|
tracking = [(stamp, message) for stamp, topic, message in sent if topic == 'starpilotSteeringTracking']
|
|
assert len(tracking) == 1 and tracking[0][0] == BASE+600_000_000
|
|
assert tracking[0][1].starpilotSteeringTracking.trial.version == 0
|
|
|
|
|
|
def test_actual_nonlive_run_does_not_read_tracking_poll_metadata(monkeypatch):
|
|
now = [BASE-50_000_000]
|
|
|
|
class SM:
|
|
# Ordinary SubMaster delivery metadata is required; no tracking-service metadata exists.
|
|
updated = {'deviceMotion': False}
|
|
def update(self, timeout=None):
|
|
now[0] += 50_000_000
|
|
if now[0] > BASE+100_000_000:
|
|
raise LoopComplete
|
|
|
|
# This pre-existing run API does not have a live actor or a tracking service.
|
|
# Minimal SM intentionally lacks tracking metadata; the legacy path is intact.
|
|
sent = []
|
|
pm = SimpleNamespace(send=lambda topic, message: sent.append(topic))
|
|
monkeypatch.setattr(steerlearnerd, 'drain_pair', lambda *args: None)
|
|
fitter = SimpleNamespace(poll=lambda **kwargs: None, close=lambda reason: True, child=None)
|
|
with pytest.raises(LoopComplete):
|
|
steerlearnerd.run(SimpleNamespace(), SM(), object(), object(), pm, None, None,
|
|
lambda stamp: source(stamp), None, None, None, None, fitter, None, clock=lambda: now[0])
|
|
assert sent == ['autoSteeringLearning']
|