Files
StarPilot/openpilot/starpilot/conditional_mode/tests/test_manual_saved.py
T
firestar5683 d8708b65c7 CEM
Implement conditional driving modes with explicit scene, button and traffic state ownership.

Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>
2026-10-14 20:02:39 -05:00

240 lines
12 KiB
Python

"""Actual temporary Params and bounded saved-manual transaction tests."""
import json
from pathlib import Path
import tempfile
import threading
import time
import unittest
from unittest.mock import patch
from openpilot.common.params import Params
from openpilot.starpilot.conditional_mode.manual_saved import (
KEY, ManualSavedOwner, SavedCodes, decode, encode, read_codes,
)
from openpilot.starpilot.conditional_mode.policy import ManualIntent, ModeChoice
from openpilot.starpilot.conditional_mode.preferences import CEMOptions, CCMOptions, SavedPreferences, encode_preferences
from openpilot.starpilot.conditional_mode.runtime_settings import ConditionalSettingsOwner, SettingsSnapshot
from openpilot.starpilot.conditional_mode import manual_saved
class ManualSavedTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.params = Params(self.directory.name)
self.now = time.monotonic_ns()
self.drive_id = self.now - 1_000_000_000
self.settings = ConditionalSettingsOwner(self.params)
def configure(self, choice: ModeChoice, *, persist: bool) -> SettingsSnapshot:
preferences = SavedPreferences(mode=choice, cem=CEMOptions(persist_manual=persist), ccm=CCMOptions(persist_manual=persist))
self.params.put('ConditionalModeConfig', json.loads(encode_preferences(preferences)), block=True)
return self.settings.refresh(self.now)
def owner(self, choice: ModeChoice, *, persist: bool) -> tuple[ManualSavedOwner, SettingsSnapshot]:
snapshot = self.configure(choice, persist=persist)
owner = ManualSavedOwner(self.params, self.settings)
self.addCleanup(owner.close)
return owner, snapshot
def settle(self, owner: ManualSavedOwner, snapshot: SettingsSnapshot, *, advance: int = 0):
deadline = time.monotonic() + 2.0
while time.monotonic() < deadline:
result = owner.poll(snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now + advance)
if result is not None:
return result
time.sleep(0.005)
self.fail('manual saved worker did not finish')
def test_strict_codec_absence_and_invalid_bytes_untouched(self):
self.assertEqual(read_codes(self.params).codes, SavedCodes())
self.assertEqual(decode(encode(SavedCodes(1, 2))), SavedCodes(1, 2))
for raw in (b'', b'{}', b'{"version":1,"cem":true,"ccm":0}',
b'{"version":1,"cem":3,"ccm":0}', b'{"version":1,"cem":1,"cem":2,"ccm":0}',
b'{"version":2,"cem":0,"ccm":0}', b'{"version":1,"cem":0,"ccm":0,"extra":1}'):
with self.subTest(raw=raw):
Path(self.params.get_param_path(KEY)).write_bytes(raw)
self.assertEqual(read_codes(self.params).status, 'invalid')
self.assertEqual(Path(self.params.get_param_path(KEY)).read_bytes(), raw)
def test_persisted_modes_restore_independently_and_auto_zero_is_saved(self):
self.params.put(KEY, json.loads(encode(SavedCodes(2, 1))), block=True)
owner, snapshot = self.owner(ModeChoice.CEM, persist=True)
start = owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.assertEqual((start.code, start.intent), (2, ManualIntent.FORCE_EXPERIMENTAL))
self.assertTrue(owner.queue_code(0, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
result = self.settle(owner, snapshot)
self.assertEqual((result.status, result.committed), ('saved', True))
self.assertEqual(read_codes(self.params).codes, SavedCodes(0, 1))
owner.close()
self.now += 2_000_000_000
second_snapshot = self.configure(ModeChoice.CCM, persist=True)
second = ManualSavedOwner(self.params, self.settings)
self.addCleanup(second.close)
restored = second.begin_drive(second_snapshot, choice=ModeChoice.CCM, drive_id=self.drive_id + 2_000_000_000,
now_ns=self.now)
self.assertEqual((restored.code, restored.intent), (1, ManualIntent.FORCE_EXPERIMENTAL))
def test_optout_clears_selected_code_only(self):
self.params.put(KEY, json.loads(encode(SavedCodes(1, 2))), block=True)
owner, snapshot = self.owner(ModeChoice.CEM, persist=False)
start = owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.assertEqual((start.code, start.intent), (0, ManualIntent.NONE))
self.assertFalse(owner.queue_code(2, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
self.assertEqual(self.settle(owner, snapshot).status, 'saved')
self.assertEqual(read_codes(self.params).codes, SavedCodes(0, 2))
def test_external_saved_or_config_edit_blocks_cas_without_clobber(self):
for edited_key in (KEY, 'ConditionalModeConfig', 'SafeMode'):
with self.subTest(key=edited_key):
self.settings = ConditionalSettingsOwner(self.params)
self.now += 2_000_000_000
owner, snapshot = self.owner(ModeChoice.CEM, persist=True)
owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.assertTrue(owner.queue_code(1, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
if edited_key == KEY:
external = encode(SavedCodes(2, 2))
Path(self.params.get_param_path(KEY)).write_bytes(external)
elif edited_key == 'ConditionalModeConfig':
external = b'{"version":1,"mode":"stock"}'
Path(self.params.get_param_path('ConditionalModeConfig')).write_bytes(external)
else:
external = b'1'
Path(self.params.get_param_path('SafeMode')).write_bytes(external)
result = self.settle(owner, snapshot)
self.assertEqual(result.status, 'external_edit')
self.assertEqual(Path(self.params.get_param_path(edited_key)).read_bytes(), external)
owner.close()
def test_wrong_drive_revision_and_close_cancel(self):
owner, snapshot = self.owner(ModeChoice.CEM, persist=True)
owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.assertFalse(owner.queue_code(1, snapshot=snapshot, drive_id=self.drive_id + 1, now_ns=self.now))
self.assertFalse(owner.queue_code(True, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
self.assertTrue(owner.queue_code(1, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
owner.close()
self.assertIsNone(owner.poll(snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
self.assertIsNone(read_codes(self.params).raw)
def test_second_writer_lease_denied_and_queued_revision_reaches_disk(self):
owner, snapshot = self.owner(ModeChoice.CEM, persist=True)
owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
second = ManualSavedOwner(self.params, self.settings)
self.addCleanup(second.close)
blocked = second.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.assertEqual(blocked.status, 'active_elsewhere')
self.assertTrue(owner.queue_code(1, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
owner.poll(snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now)
self.assertTrue(owner.queue_code(2, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
self.assertEqual(self.settle(owner, snapshot).status, 'saved')
self.assertEqual(self.settle(owner, snapshot).status, 'saved')
self.assertEqual(read_codes(self.params).codes, SavedCodes(2, 0))
def test_close_during_blocked_staging_prevents_rename(self):
owner, snapshot = self.owner(ModeChoice.CEM, persist=True)
owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.assertTrue(owner.queue_code(1, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
entered = threading.Event()
release = threading.Event()
actual_fsync = manual_saved.os.fsync
def blocked_fsync(fd):
if not entered.is_set():
entered.set()
self.assertTrue(release.wait(1.0))
return actual_fsync(fd)
with patch.object(manual_saved.os, 'fsync', side_effect=blocked_fsync):
owner.poll(snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now)
self.assertTrue(entered.wait(1.0))
before = time.monotonic()
owner.close()
self.assertLess(time.monotonic() - before, 0.1)
release.set()
owner.worker.join(timeout=1.0)
self.assertIsNone(read_codes(self.params).raw)
def test_close_after_unpolled_completed_worker_releases_lease(self):
owner, snapshot = self.owner(ModeChoice.CEM, persist=True)
owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
owner.queue_code(1, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now)
owner.poll(snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now)
assert owner.worker is not None
owner.worker.join(timeout=1.0)
self.assertFalse(owner.worker.is_alive())
owner.close()
second = ManualSavedOwner(self.params, self.settings)
self.addCleanup(second.close)
self.assertNotEqual(second.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id,
now_ns=self.now).status, 'active_elsewhere')
def test_native_params_edit_during_staging_wins_cas(self):
owner, snapshot = self.owner(ModeChoice.CEM, persist=True)
owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
owner.queue_code(1, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now)
entered = threading.Event()
release = threading.Event()
actual_fsync = manual_saved.os.fsync
def blocked_fsync(fd):
if not entered.is_set():
entered.set()
self.assertTrue(release.wait(1.0))
return actual_fsync(fd)
with patch.object(manual_saved.os, 'fsync', side_effect=blocked_fsync):
owner.poll(snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now)
self.assertTrue(entered.wait(1.0))
self.params.put(KEY, json.loads(encode(SavedCodes(2, 2))), block=True)
release.set()
assert owner.worker is not None
owner.worker.join(timeout=1.0)
self.assertEqual(owner.poll(snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now).status, 'external_edit')
self.assertEqual(read_codes(self.params).codes, SavedCodes(2, 2))
def test_same_source_refresh_allows_queue_and_staged_commit(self):
owner, initial = self.owner(ModeChoice.CEM, persist=True)
owner.begin_drive(initial, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.now += 1_100_000_000
refreshed = self.settings.refresh(self.now)
self.assertIsNot(refreshed, initial)
self.assertEqual(refreshed.revision, initial.revision)
self.assertTrue(owner.queue_code(1, snapshot=refreshed, drive_id=self.drive_id, now_ns=self.now))
entered = threading.Event()
release = threading.Event()
actual_fsync = manual_saved.os.fsync
def blocked_fsync(fd):
if not entered.is_set():
entered.set()
self.assertTrue(release.wait(1.0))
return actual_fsync(fd)
with patch.object(manual_saved.os, 'fsync', side_effect=blocked_fsync):
owner.poll(snapshot=refreshed, drive_id=self.drive_id, now_ns=self.now)
self.assertTrue(entered.wait(1.0))
self.now += 1_100_000_000
again = self.settings.refresh(self.now)
self.assertIsNot(again, refreshed)
self.assertEqual(again.revision, initial.revision)
release.set()
assert owner.worker is not None
owner.worker.join(timeout=1.0)
self.assertEqual(owner.poll(snapshot=again, drive_id=self.drive_id, now_ns=self.now).status, 'saved')
self.assertEqual(read_codes(self.params).codes, SavedCodes(1, 0))
def test_invalid_saved_bytes_do_not_disable_nonpersistent_manual_start(self):
raw = b'{"version":1,"cem":true,"ccm":0}'
Path(self.params.get_param_path(KEY)).write_bytes(raw)
owner, snapshot = self.owner(ModeChoice.CEM, persist=False)
start = owner.begin_drive(snapshot, choice=ModeChoice.CEM, drive_id=self.drive_id, now_ns=self.now)
self.assertEqual((start.status, start.intent, start.code), ('invalid', ManualIntent.NONE, 0))
self.assertFalse(owner.queue_code(2, snapshot=snapshot, drive_id=self.drive_id, now_ns=self.now))
self.assertEqual(Path(self.params.get_param_path(KEY)).read_bytes(), raw)
if __name__ == '__main__':
unittest.main()