from dataclasses import replace import json import os from pathlib import Path import tempfile import unittest from unittest import mock from openpilot.starpilot.models.catalog import BUNDLED_CURRENT from openpilot.starpilot.models.receipt import (ModelReceiptOwner, ReceiptUnavailable, artifact_digest, artifact_identity, clear_receipt, prepare_artifact, process_start_ticks, read_receipt, record_prepared, write_receipt, logged_model_load, log_loaded_model) from openpilot.starpilot.models.status import ModelLoad, ModelVariant class TestModelReceipt(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) self.path = self.root / "owned" / "load-v1.json" self.load = ModelLoad(os.getpid(), 12345, 987654321, BUNDLED_CURRENT, ModelVariant.SMALL, "a" * 64) def test_atomic_roundtrip_clear_and_malformed(self): self.assertIsNone(read_receipt(self.path)) write_receipt(self.load, self.path) self.assertEqual(read_receipt(self.path), self.load) self.assertEqual(self.path.stat().st_mode & 0o777, 0o600) self.assertEqual(self.path.parent.stat().st_mode & 0o777, 0o700) raw = self.path.read_text() self.path.write_text(raw.replace('"pid":', '"pid":1,"pid":')) self.assertIsNone(read_receipt(self.path)) self.path.write_bytes(b"{" * 1025) self.assertIsNone(read_receipt(self.path)) self.path.write_text(json.dumps({"version": 1})) self.assertIsNone(read_receipt(self.path)) clear_receipt(self.path) self.assertFalse(self.path.exists()) def test_catalog_id_roundtrip_and_wrong_hardware_rejection(self): for model_id, variant in (("sc23", ModelVariant.SMALL), ("cinquev3", ModelVariant.CHESTNUT)): load = replace(self.load, model_id=model_id, variant=variant) write_receipt(load, self.path) self.assertEqual(read_receipt(self.path), load) with self.assertRaises(ReceiptUnavailable): write_receipt(replace(self.load, model_id="cinquev3"), self.path) with self.assertRaises(ReceiptUnavailable): write_receipt(replace(self.load, model_id="unknown"), self.path) def test_symlink_fifo_and_unowned_directory_are_unavailable(self): target = self.root / "outside" target.write_bytes(b"untouched") self.path.parent.symlink_to(self.root, target_is_directory=True) self.assertIsNone(read_receipt(self.path)) with self.assertRaises(OSError): write_receipt(self.load, self.path) clear_receipt(self.path) self.assertEqual(target.read_bytes(), b"untouched") self.path.parent.unlink() self.path.parent.mkdir(mode=0o700) os.mkfifo(self.path) self.assertIsNone(read_receipt(self.path)) clear_receipt(self.path) self.path.parent.chmod(0o755) self.assertIsNone(read_receipt(self.path)) with self.assertRaises(ReceiptUnavailable): write_receipt(self.load, self.path) def test_invalid_namespace_is_unavailable_to_reader(self): with mock.patch.dict(os.environ, {"OPENPILOT_PREFIX": "../../escape"}): self.assertIsNone(read_receipt()) def test_installed_artifact_same_file_and_regular(self): artifact = self.root / "model.pkl" artifact.write_bytes(b"compiled-artifact") identity = artifact_identity(artifact) self.assertEqual(len(artifact_digest(artifact, identity)), 64) prepared = prepare_artifact(artifact, identity) recorded = record_prepared(prepared, ModelVariant.SMALL, receipt_file=self.path, pid=421, start_ticks=12345, now_mono_ns=987654321) self.assertEqual(read_receipt(self.path), recorded) artifact.write_bytes(b"changed") with self.assertRaises(ReceiptUnavailable): artifact_digest(artifact, identity) artifact.unlink() artifact.symlink_to(self.root / "outside") with self.assertRaises(ReceiptUnavailable): artifact_identity(artifact) artifact.unlink() os.mkfifo(artifact) with self.assertRaises(ReceiptUnavailable): artifact_identity(artifact) def test_proc_start_parser(self): path = self.root / "421" / "stat" path.parent.mkdir() fields = ["S"] + ["0"] * 18 + ["98765"] + ["0"] * 30 path.write_text("421 (modeld with ) parens) " + " ".join(fields)) self.assertEqual(process_start_ticks(421, proc_root=self.root), 98765) self.assertEqual(process_start_ticks(422, proc_root=self.root), 0) def test_best_effort_start_and_fallback_transitions(self): artifact = self.root / "compiled.pkl" artifact.write_bytes(b"compiled-small") reasons = [] recorded = [] owner = ModelReceiptOwner(self.path, reasons.append, record=recorded.append) owner.start() identity = owner.capture(artifact) self.assertIsNotNone(identity) if identity is None: self.fail("regular fixture artifact had no identity") # A diagnostic identity failure leaves model operation available. self.assertIsNone(owner.prepare(artifact, None)) self.assertEqual(reasons, ["artifact-identity-unavailable"]) prepared = owner.prepare(artifact, identity) self.assertIsNotNone(prepared) # Runtime fallback only emits a tiny receipt: even a broken hash/open path # cannot be reached from loaded() after startup preparation. with mock.patch('openpilot.starpilot.models.receipt.artifact_digest', side_effect=AssertionError('bulk I/O')): with mock.patch('openpilot.starpilot.models.receipt.process_start_ticks', return_value=12345): self.assertTrue(owner.loaded(prepared, ModelVariant.SMALL, "chestnut-load-failed")) self.assertEqual(read_receipt(self.path).fallback_reason, "chestnut-load-failed") self.assertEqual(recorded, [read_receipt(self.path)]) owner.start() self.assertIsNone(read_receipt(self.path)) def test_unknown_or_failed_replacement_retires_previous_file_and_heartbeat(self): write_receipt(self.load, self.path) reasons = [] owner = ModelReceiptOwner(self.path, reasons.append, record=lambda _load: None) owner.load = self.load self.assertFalse(owner.loaded(None, ModelVariant.CHESTNUT)) self.assertIsNone(owner.load) self.assertIsNone(read_receipt(self.path)) self.assertEqual(reasons, ['prepared-artifact-unavailable']) write_receipt(self.load, self.path) artifact = self.root / 'compiled.pkl' artifact.write_bytes(b'compiled') prepared = prepare_artifact(artifact, artifact_identity(artifact)) with mock.patch('openpilot.starpilot.models.receipt.record_prepared', side_effect=OSError('record unavailable')): self.assertFalse(owner.loaded(prepared, ModelVariant.SMALL)) self.assertIsNone(read_receipt(self.path)) self.assertEqual(reasons[-1], 'record-failed') def test_failed_clear_remains_nonfatal_and_unavailable_heartbeat(self): write_receipt(self.load, self.path) reasons = [] owner = ModelReceiptOwner(self.path, reasons.append, record=lambda _load: None) owner.load = self.load with mock.patch('openpilot.starpilot.models.receipt.clear_receipt', side_effect=OSError('unlink failed')): self.assertFalse(owner.loaded(None, ModelVariant.CHESTNUT)) self.assertIsNone(owner.load) self.assertEqual(read_receipt(self.path), self.load) # Last-confirmed only, never current-identity proof. self.assertEqual(reasons, ['clear-failed', 'prepared-artifact-unavailable']) class Publisher: raw = b'' def send(self, _service, message): self.raw = message.to_bytes() publisher = Publisher() self.assertTrue(owner.publish(publisher, self.load.loaded_mono_ns + 1)) from openpilot.cereal import messaging message = messaging.log_from_bytes(publisher.raw) self.assertEqual(json.loads(str(message.logMessage))['msg']['event'], 'modeld.identity-unavailable') def test_runtime_stall_reason_roundtrips_without_altering_loaded_identity(self): load = replace(self.load, fallback_reason='chestnut-run-stalled') write_receipt(load, self.path) self.assertEqual(read_receipt(self.path), load) with mock.patch('openpilot.common.swaglog.cloudlog.event') as record: log_loaded_model(load) payload = {'event': record.call_args.args[0], **record.call_args.kwargs} self.assertEqual(logged_model_load(json.dumps({'process': load.pid, 'msg': payload})), load) def test_log_identity_roundtrip_is_actual_loaded_variant_and_bound_to_process(self): with mock.patch('openpilot.common.swaglog.cloudlog.event') as record: log_loaded_model(self.load) payload = {'event': record.call_args.args[0], **record.call_args.kwargs} raw = {'process': self.load.pid, 'msg': payload} self.assertEqual(logged_model_load(json.dumps(raw)), self.load) raw['process'] += 1 self.assertIsNone(logged_model_load(json.dumps(raw))) raw['process'] = self.load.pid payload['modelId'] = 'cinquev3' # A big selection cannot masquerade as the small runner. self.assertIsNone(logged_model_load(json.dumps(raw))) for value in ('not json', '{' * 8193, '[]', json.dumps({'msg': 'loading model'})): self.assertIsNone(logged_model_load(value)) def test_log_failure_does_not_invalidate_successful_live_receipt(self): reasons = [] owner = ModelReceiptOwner(self.path, reasons.append, record=mock.Mock(side_effect=OSError('logger absent'))) artifact = self.root / 'compiled.pkl' artifact.write_bytes(b'compiled') prepared = prepare_artifact(artifact, artifact_identity(artifact)) with mock.patch('openpilot.starpilot.models.receipt.process_start_ticks', return_value=12345): self.assertTrue(owner.loaded(prepared, ModelVariant.SMALL)) self.assertIsNotNone(read_receipt(self.path)) self.assertEqual(reasons, ['log-failed']) if __name__ == "__main__": unittest.main()