Files
StarPilot/tools/ci/replay_fixture_cache.py
T
firestar5683 e9dd458699 Replay
Provide route replay, log migration and recorded vehicle fixtures for regression checks.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: YassineYousfi <yyousfi1@binghamton.edu>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
2026-10-14 20:02:39 -05:00

99 lines
5.3 KiB
Python

"""Explicit cache conversion for reviewed, immutable replay recordings.
Published reference Event and CarParams use their producing schemas; input decoding
only compares firmware/VIN identity and does not prove the original input schema.
Input, reference and schemas are pinned before a current typed cache is made.
Unknown recordings require their own reviewed conversion, never current decoding.
"""
import hashlib
import json
from pathlib import Path
import tempfile
import capnp
from opendbc.car.structs import car
from openpilot.starpilot.schema_cache import put_cache
from openpilot.tools.lib.logreader import decompress_stream
from openpilot.tools.lib.url_file import URLFile
OPENPILOT_COMMIT = 'ec95db3f1fa19f76940497fedfdb62e09ea19912'
ARTIFACT_COMMIT = '9ae451c262c7402c9623f3baa1580c6242d6777b'
OPENDBC_COMMIT = '35f7e0813462607ef1d703e52313e7571e31405d'
TARGET_CAR_SHA256 = '1511eaaa36581a2182694e3cdf5a9b3cf05276e90c36bb5d82ee095e84c23d69'
FIXTURES = json.loads(Path(__file__).with_name('replay-fixture-cache.json').read_text())
# The reviewed target differs from the source only by SafetyModel.teslaPreap @39.
# No source field is dropped, renamed, defaulted or reinterpreted by this adapter.
SOURCE_FILES = {
'car.capnp': ('opendbc', OPENDBC_COMMIT, 'opendbc/car/car.capnp',
'098a19d69e8d233ac653503b53761a94824f31812757a11a7ded1589fe1e0788'),
'include/c++.capnp': ('opendbc', OPENDBC_COMMIT, 'opendbc/car/include/c++.capnp',
'fb306076cd38c27af1aed20fac6395e9a46fbe5b5df6a248b5f1b6845a079c44'),
'log.capnp': ('openpilot', OPENPILOT_COMMIT, 'openpilot/cereal/log.capnp',
'3f8906bd908494e20eb77b7d27536be541d4cf9f8d5277ae8528b3ee59a55673'),
'custom.capnp': ('openpilot', OPENPILOT_COMMIT, 'openpilot/cereal/custom.capnp',
'6b06a0d2d5ac6b902ca2f433c1663ed83ea4b41ba4c16cddfc7acd8a630be82d'),
'deprecated.capnp': ('openpilot', OPENPILOT_COMMIT, 'openpilot/cereal/deprecated.capnp',
'b3aa5c13de40001ed7376cef858500fb003f7698f96131cd0db18d878fa1af0d'),
}
def _digest(raw):
return hashlib.sha256(raw).hexdigest()
class _Sink:
def put(self, key, value, *, block):
self.raw = value
def verified_fixture_params(segment: str, raw: bytes) -> dict[str, bytes]:
"""Create a cache only after independent source decoding and lossless conversion."""
expected = FIXTURES.get(segment)
if expected is None or len(raw) != expected['input_bytes'] or _digest(raw) != expected['input_sha256']:
raise ValueError(f'{segment}: no reviewed immutable recording cache conversion')
target_path = Path(__file__).resolve().parents[2] / 'opendbc_repo/opendbc/car/car.capnp'
if _digest(target_path.read_bytes()) != TARGET_CAR_SHA256:
raise ValueError('Replay cache target schema changed; review the source conversion')
with tempfile.TemporaryDirectory(prefix='replay-source-schema-') as directory:
root = Path(directory)
for local, (repo, commit, path, digest) in SOURCE_FILES.items():
source = URLFile(f'https://raw.githubusercontent.com/commaai/{repo}/{commit}/{path}').read()
if _digest(source) != digest:
raise ValueError(f'{local}: replay producing schema digest mismatch')
destination = root / local
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(source)
parser = capnp.SchemaParser()
source_log = parser.load(str(root / 'log.capnp'), imports=[str(root)])
reference_name = f'{segment.replace("|", "_")}_card_{OPENPILOT_COMMIT}.zst'
reference = URLFile(f'https://raw.githubusercontent.com/commaai/ci-artifacts/{ARTIFACT_COMMIT}/{reference_name}').read()
if len(reference) != expected['reference_bytes'] or _digest(reference) != expected['reference_sha256']:
raise ValueError('Published replay CarParams reference digest mismatch')
source_cp = next((message.carParams.to_dict()
for message in source_log.Event.read_multiple_bytes(decompress_stream(reference))
if message.which() == 'carParams'), None)
if source_cp is None:
raise ValueError('Published card reference is missing CarParams')
# Compare only the firmware/VIN identity consumed by the current fingerprint
# path. Historical input control settings never seed the verified cache.
input_cp = next((message.carParams.to_dict()
for message in source_log.Event.read_multiple_bytes(decompress_stream(raw))
if message.which() == 'carParams'), None)
# The pinned Rivian input predates the existing R1 name migration. Its
# reference uses the current platform name; firmware and VIN remain exact.
if segment == 'regen5FCAC896BBE|2025-04-08--23-13-35--0' and input_cp is not None:
if input_cp.get('carFingerprint') == 'RIVIAN_R1_GEN1':
input_cp['carFingerprint'] = 'RIVIAN_R1'
identity = ('carFingerprint', 'carVin', 'carFw')
if input_cp is None or any(input_cp[key] != source_cp[key] for key in identity):
raise ValueError('Recording firmware/VIN identity differs from its published card reference')
converted = car.CarParams.new_message(**source_cp)
if converted.to_dict() != source_cp:
raise ValueError('Replay CarParams source conversion changed typed values')
sink = _Sink()
put_cache(sink, 'CarParamsCache', converted, block=True)
return {'CarParamsCache': sink.raw}