mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
139 lines
5.9 KiB
Python
139 lines
5.9 KiB
Python
"""Optional-child lifecycle only; no controller/native admission is mocked here."""
|
|
import pytest
|
|
|
|
from openpilot.starpilot.flm.live_tracking_actor import LiveTrackingActor
|
|
|
|
|
|
class Process:
|
|
def __init__(self, closed=True, error=None):
|
|
self.closed, self.error, self.calls = closed, error, []
|
|
self.child = None
|
|
|
|
def close(self, reason):
|
|
self.calls.append(reason)
|
|
if self.error is not None:
|
|
raise self.error
|
|
return self.closed
|
|
|
|
|
|
@pytest.mark.parametrize('first_closed', (False, True))
|
|
def test_shutdown_reaps_both_optional_workers(first_closed):
|
|
bootstrap, probe = Process(first_closed), Process()
|
|
actor = LiveTrackingActor(bootstrap, probe_process=probe)
|
|
assert actor.close('shutdown') is first_closed
|
|
assert bootstrap.calls == probe.calls == ['shutdown']
|
|
|
|
|
|
def test_unexpected_shutdown_failure_still_attempts_other_reaper():
|
|
failure = RuntimeError('bootstrap lifecycle failed')
|
|
bootstrap, probe = Process(error=failure), Process()
|
|
actor = LiveTrackingActor(bootstrap, probe_process=probe)
|
|
with pytest.raises(RuntimeError, match='bootstrap lifecycle failed'):
|
|
actor.close('shutdown')
|
|
assert bootstrap.calls == probe.calls == ['shutdown']
|
|
|
|
|
|
class PollProcess(Process):
|
|
def __init__(self, *, reap_after=0):
|
|
super().__init__()
|
|
self.child = object() if reap_after else None
|
|
self.reap_after = reap_after
|
|
self.polls, self.submissions = [], []
|
|
|
|
def poll(self, *, token, now_ns):
|
|
self.polls.append((token, now_ns))
|
|
if self.reap_after:
|
|
self.reap_after -= 1
|
|
if not self.reap_after:
|
|
self.child = None
|
|
return None
|
|
|
|
def submit(self, snapshot, *, token, now_ns):
|
|
self.submissions.append((snapshot, token, now_ns))
|
|
return True
|
|
|
|
|
|
def test_changed_source_waits_for_actual_old_probe_reap_before_bootstrap(monkeypatch):
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
from openpilot.starpilot.flm.tests.test_live_tracking_rollout import _optimizer
|
|
from openpilot.starpilot.flm import live_tracking_bootstrap_producer
|
|
old = _optimizer().binding
|
|
new = replace(old, source_epoch='9'*64)
|
|
bootstrap, probe = PollProcess(), PollProcess(reap_after=3)
|
|
actor = LiveTrackingActor(bootstrap, probe_process=probe)
|
|
# Lifecycle seam: preserve the current asynchronous preparation/poll contract.
|
|
# Response/controller/native admission is covered by the separate real collector tests.
|
|
class SnapshotPreparation:
|
|
pid = epoch = token = pending = None
|
|
reason = 'snapshot_submitted'
|
|
submissions = []
|
|
|
|
def cancel(self, reason):
|
|
self.pid = self.pending = None
|
|
|
|
def poll(self, *, token, now_ns):
|
|
result, self.pending = self.pending, None
|
|
self.pid = None
|
|
return result if token == self.token else None
|
|
|
|
def submit(self, collector, seed, *, token, now_ns):
|
|
assert probe.child is None # Never start new snapshot work before the old probe is reaped.
|
|
self.epoch, self.token, self.pid = collector.original_source_capture.epoch, token, 1
|
|
self.submissions.append((token, now_ns))
|
|
self.pending = {'snapshot': live_tracking_bootstrap_producer.response_snapshot(collector, seed, now_ns)}
|
|
return True
|
|
|
|
snapshot = SnapshotPreparation()
|
|
from typing import Any, cast
|
|
actor.snapshot_process = cast(Any, snapshot)
|
|
monkeypatch.setattr(actor, '_same_bootstrap_source', lambda source, now_ns: True)
|
|
actor.seed = old
|
|
monkeypatch.setattr(actor, 'binding_for', lambda collector, source: new)
|
|
collector = SimpleNamespace(ready=[None], original_source_capture=SimpleNamespace(epoch=1), live_tracking=None)
|
|
monkeypatch.setattr(live_tracking_bootstrap_producer, 'response_snapshot', lambda *args: {'lifecycle_test_only': True})
|
|
assert actor.tick(collector, object(), now_ns=10, current_native=True) is None
|
|
assert probe.child is not None and not bootstrap.submissions and not snapshot.submissions
|
|
assert actor.tick(collector, object(), now_ns=20, current_native=True) is None
|
|
assert probe.child is None and len(snapshot.submissions) == 1 and not bootstrap.submissions
|
|
assert actor.tick(collector, object(), now_ns=30, current_native=True) is None
|
|
assert len(bootstrap.submissions) == 1
|
|
assert all(token is None for token, _ in probe.polls)
|
|
|
|
|
|
def test_invalid_binding_continues_nonblocking_reaping_without_new_job(monkeypatch):
|
|
from types import SimpleNamespace
|
|
bootstrap, probe = PollProcess(), PollProcess(reap_after=4)
|
|
actor = LiveTrackingActor(bootstrap, probe_process=probe)
|
|
def invalid(collector, source):
|
|
raise ValueError('current binding unavailable')
|
|
monkeypatch.setattr(actor, 'binding_for', invalid)
|
|
collector = SimpleNamespace(ready=[None])
|
|
for now in (10, 20, 30):
|
|
assert actor.tick(collector, object(), now_ns=now, current_native=True) is None
|
|
assert probe.child is None and not bootstrap.submissions
|
|
assert all(token is None for token, _ in bootstrap.polls+probe.polls)
|
|
assert not bootstrap.calls and not probe.calls # No blocking close in tick.
|
|
|
|
|
|
|
|
def test_probe_transport_splits_only_work_not_genuine_endpoint_support(monkeypatch):
|
|
from openpilot.starpilot.flm import live_tracking_probe_process
|
|
process = PollProcess()
|
|
process.submit_probe = process.submit
|
|
actor = LiveTrackingActor(probe_process=process)
|
|
jobs = [({'endpoint': index}, index) for index in range(4)]
|
|
actor.probe_queue = list(jobs)
|
|
actor.probe_queue_key = ((None, 'a'*64, 'b'*64), 1, 'ff.left', ('ff_gain_left',))
|
|
def bounded_batch(rows, *, queued_ns):
|
|
if len(rows) > 2:
|
|
raise ValueError('numerical batch byte bound')
|
|
return {'rows': rows, 'queued_ns': queued_ns}
|
|
monkeypatch.setattr(live_tracking_probe_process, 'make_probe_batch', bounded_batch)
|
|
assert actor._submit_probe_queue('token', 100)
|
|
assert actor.probe_queue == jobs[2:]
|
|
assert len(process.submissions[0][0]['rows']) == 2
|
|
assert actor._submit_probe_queue('token', 200)
|
|
assert actor.probe_queue == [] and actor.probe_queue_key is None
|
|
assert [row['endpoint'] for submission, _, _ in process.submissions for row in submission['rows']] == list(range(4))
|