Files
StarPilot/tools/ci/tests/test_check_startup.py
T
firestar5683 694b460f80 Developer Tools
Provide laptop and device build entry points, cache compatibility checks and developer tooling. Keep distribution assets and model packaging available before runtime consumers.

Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

122 lines
6.1 KiB
Python

import tempfile
import json
import struct
import unittest
from pathlib import Path
from opendbc.car import structs
from openpilot.common.params import Params
from openpilot.starpilot import schema_cache as cache
from openpilot.starpilot.state_migration import MigrationRequired, load_snapshot, prepare_manager_start
from openpilot.cereal import messaging
from tools.check_startup import check_startup
class TestCheckStartup(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.params = Params(str(self.root / "params"))
self.storage = self.root / "recovery"
self.storage.mkdir(mode=0o700)
def tree(self):
return {str(path.relative_to(self.root)): path.read_bytes() if path.is_file() else None
for path in self.root.rglob("*")}
def test_valid_initialized_cache_accepted_without_writes(self):
prepare_manager_start(self.params, self.storage)
cache.put_cache(self.params, "LiveDelay", messaging.new_message("lateralDelay"), block=True)
before = self.tree()
check_startup(Path(self.params.get_param_path()), self.storage)
self.assertEqual(self.tree(), before)
def test_incompatible_retained_cache_rejected_without_writes(self):
prepare_manager_start(self.params, self.storage)
payload = messaging.new_message("lateralDelay").to_bytes()
header = cache._header(cache.CONTRACTS["LiveDelay"], "LiveDelay", payload, version=1)
header["schema_sha256"] = "0" * 64
encoded = json.dumps(header, separators=(",", ":")).encode()
raw = cache.MAGIC + struct.pack(">I", len(encoded)) + encoded + payload
self.assertEqual(cache.inspect_cache("LiveDelay", raw).status, "incompatible")
(Path(self.params.get_param_path()) / "LiveDelay").write_bytes(raw)
before = self.tree()
with self.assertRaisesRegex(MigrationRequired, "incompatible retained cache"):
check_startup(Path(self.params.get_param_path()), self.storage)
self.assertEqual(self.tree(), before)
def stale_cache(self, key):
contract = cache.CONTRACTS[key]
message = contract.root.new_message() if contract.service is None else messaging.new_message(contract.service)
payload = message.to_bytes()
header = cache._header(contract, key, payload)
header.update(producer_contract="prior-runtime-contract", schema_sha256="0" * 64)
encoded = json.dumps(header, separators=(",", ":")).encode()
raw = cache.MAGIC + struct.pack(">I", len(encoded)) + encoded + payload
self.assertEqual(cache.inspect_cache(key, raw).status, "incompatible")
self.params.put(key, raw, block=True)
return raw
def test_initialized_actual_registry_archives_all_vehicle_contracts_only(self):
prepare_manager_start(self.params, self.storage, auto_migrate=True)
vehicle_keys = {key for key, contract in cache.CONTRACTS.items() if contract.root is structs.CarParams}
self.assertTrue(vehicle_keys)
stale = {key: self.stale_cache(key) for key in vehicle_keys}
cache.put_cache(self.params, "CalibrationParams", messaging.new_message("extrinsicsCalibration"), block=True)
cache.put_cache(self.params, "LiveDelay", messaging.new_message("lateralDelay"), block=True)
self.params.put_bool("IsMetric", True, block=True)
self.params.put_bool("AlwaysOnLateral", True, block=True)
self.params.put("DongleId", "preserved-operational-identity", block=True)
namespace = Path(self.params.get_param_path())
original = {path.name: path.read_bytes() for path in namespace.iterdir()}
before = self.tree()
with self.assertRaisesRegex(MigrationRequired, "incompatible retained cache"):
check_startup(namespace, self.storage)
self.assertEqual(self.tree(), before)
prepare_manager_start(self.params, self.storage, auto_migrate=True)
expected = {key: raw for key, raw in original.items() if key not in vehicle_keys}
self.assertEqual({path.name: path.read_bytes() for path in namespace.iterdir()}, expected)
snapshot, = (self.storage / "snapshots").iterdir()
self.assertEqual(load_snapshot(snapshot), original)
receipt = json.loads((snapshot / "migration.json").read_bytes())
self.assertEqual(set(receipt["actions"]), set(stale))
self.assertEqual(receipt["status"], "migrated")
after = self.tree()
prepare_manager_start(self.params, self.storage, auto_migrate=True)
check_startup(namespace, self.storage)
self.assertEqual(self.tree(), after)
def test_initialized_vehicle_upgrade_does_not_admit_invalid_other_cache(self):
prepare_manager_start(self.params, self.storage, auto_migrate=True)
vehicle_keys = {key for key, contract in cache.CONTRACTS.items() if contract.root is structs.CarParams}
for key in vehicle_keys:
self.stale_cache(key)
self.stale_cache("LiveDelay")
namespace = Path(self.params.get_param_path())
original = {path.name: path.read_bytes() for path in namespace.iterdir()}
with self.assertRaises(MigrationRequired):
prepare_manager_start(self.params, self.storage, auto_migrate=True)
self.assertEqual({path.name: path.read_bytes() for path in namespace.iterdir()}, original)
snapshot, = (self.storage / "snapshots").iterdir()
self.assertEqual(load_snapshot(snapshot), original)
def test_missing_input_rejected_without_creating_namespace(self):
missing = self.root / "missing"
with self.assertRaisesRegex(ValueError, "Existing Params namespace required"):
check_startup(missing, self.storage)
self.assertFalse(missing.exists())
def test_root_with_named_link_or_empty_directory_is_rejected(self):
named = self.root / "named"
named.mkdir()
(named / "other").symlink_to(Path(self.params.get_param_path()), target_is_directory=True)
before = self.tree()
with self.assertRaisesRegex(ValueError, "named-link roots are ambiguous"):
check_startup(named, self.storage)
empty = self.root / "empty"
empty.mkdir()
with self.assertRaisesRegex(ValueError, "empty or named-link roots are ambiguous"):
check_startup(empty, self.storage)
self.assertEqual({key: value for key, value in self.tree().items() if key != "empty"}, before)