Files
StarPilot/openpilot/starpilot/tests/test_jetlink_lifecycle.py
T
Zeph Leggett ca183df497 Jetlink API 3 adapter
StarPilotAdapter subclasses jetlink's Stock: StarPilot's param names, the
AOL engagement rule, the model root and a CatalogModelState history reset
that keeps its JIT buffers. jetlinkd runs jetlink's owner as it is, modeld
calls the hooks, and Galaxy and analytics read jetlink's runtime record in
place of JetlinkRuntime.

- no big-model slot or catalog keys: jetlink provisions its default model
- jetlink restores the owner's tuning itself; the port draw stays cut with
  a host plugged in
- the Jetson is asked to power off on DoShutdown only, capped at 30 s
2026-10-18 16:18:37 -05:00

138 lines
5.2 KiB
Python

"""Opt-in process ownership and remote shutdown before manager cleanup."""
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from openpilot.starpilot.system.jetlink import lifecycle
def _cleanup_child(ready, cleaned, release):
import signal
import time
stopping = False
def stop(signum, frame):
nonlocal stopping
stopping = True
signal.signal(signal.SIGINT, stop)
ready.set()
while not stopping:
time.sleep(.01)
release.wait(2)
cleaned.set()
class TestJetlinkLifecycle(unittest.TestCase):
def jetlink(self, on):
link = Mock()
for name, value in (('hooks', lambda: link), ('link_on', lambda: on)):
patcher = patch(f'openpilot.starpilot.models.jetlink_adapter.{name}', value)
patcher.start()
self.addCleanup(patcher.stop)
return link
def test_with_the_link_off_jetlink_is_never_bound(self):
from jetlink.openpilot.hooks import Hooks
from openpilot.starpilot.models.jetlink_adapter import API
self.jetlink(False)
factory = Mock()
with patch('openpilot.starpilot.models.jetlink_adapter.hooks', lambda: Hooks(API, factory, link_on=lambda: False)):
self.assertFalse(lifecycle.enabled(None))
lifecycle.shutdown(SimpleNamespace(get_bool=lambda key: key == 'DoShutdown'))
factory.assert_not_called()
def test_the_owner_follows_jetlink(self):
link = self.jetlink(True)
for available in (True, False):
link.should_run.return_value = available
self.assertEqual(lifecycle.enabled(None), available)
def test_only_power_off_asks_the_host_and_is_capped(self):
link = self.jetlink(True)
for reason in (None, 'DoReboot', 'DoUninstall', 'DoShutdown'):
link.shutdown_step.reset_mock()
link.shutdown_step.return_value = True
params = SimpleNamespace(get_bool=lambda key, reason=reason: key == reason)
lifecycle.shutdown(params)
if reason == 'DoShutdown':
link.shutdown_step.assert_called_once_with(True, 'DoShutdown')
else:
link.shutdown_step.assert_not_called()
# a host that never answers holds the power-off for the cap, no longer
now, waits = [0.], []
def sleep(seconds):
waits.append(seconds)
if len(waits) > 1000:
raise AssertionError("the power-off wait has no cap")
now[0] += seconds
link.shutdown_step.return_value = False
lifecycle.shutdown(params, clock=lambda: now[0], sleep=sleep)
self.assertGreaterEqual(now[0], lifecycle.SHUTDOWN_CAP_S)
self.assertLess(now[0], lifecycle.SHUTDOWN_CAP_S + 1)
def test_registered_native_int_and_owner_file_reader_agree(self):
from pathlib import Path
from jetlink.openpilot.settings import FileParams, Settings
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.models.jetlink_adapter import owner_config
with OpenpilotPrefix():
params = Params()
config = owner_config()
settings = Settings(FileParams(config.params_dir), config.keys)
from openpilot.starpilot.models import jetlink_adapter
jetlink_adapter.hooks.cache_clear()
self.addCleanup(jetlink_adapter.hooks.cache_clear)
with patch('jetlink.openpilot.parts.Parts.chestnut_fitted', return_value=False), \
patch('openpilot.common.hardware.HARDWARE.get_device_type', return_value='tizi'):
for value, mode in ((0, 'off'), (1, 'usb'), (2, 'ios'), (3, 'off'), (-1, 'off')):
with self.subTest(value=value):
params.put('JetlinkMode', value, block=True)
self.assertIs(type(params.get('JetlinkMode')), int)
self.assertEqual(params.get('JetlinkMode'), value)
self.assertEqual(FileParams(config.params_dir).get_int('JetlinkMode'), value)
self.assertEqual(settings.mode(), mode)
self.assertEqual(jetlink_adapter.link_on(), value in (1, 2))
self.assertEqual(lifecycle.enabled(params), value in (1, 2))
params.put('JetlinkMode', 1, block=True)
Path(params.get_param_path('JetlinkMode')).write_bytes(b'corrupt')
self.assertEqual(settings.mode(), 'off')
self.assertFalse(lifecycle.enabled(params))
def test_process_nonblocking_stop_and_blocking_signal_cleanup(self):
import multiprocessing
import time
from openpilot.starpilot.system.jetlink.process import JetlinkProcess
context = multiprocessing.get_context('spawn')
ready = context.Event()
cleaned = context.Event()
release = context.Event()
process = context.Process(target=_cleanup_child, args=(ready, cleaned, release))
process.start()
def cleanup():
if process.is_alive():
process.kill()
process.join(.5)
self.addCleanup(cleanup)
self.assertTrue(ready.wait(2))
owner = JetlinkProcess('jetlinkd', 'unused', lambda *args: True)
vars(owner)['proc'] = process
start = time.monotonic()
owner.stop(block=False)
owner.stop(block=False)
self.assertLess(time.monotonic() - start, .5)
self.assertTrue(owner.shutting_down)
release.set()
owner.stop(block=True)
self.assertTrue(cleaned.is_set())
self.assertEqual(process.exitcode, 0)
self.assertIsNone(owner.proc)
self.assertLess(time.monotonic() - start, 2)