mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
d8708b65c7
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>
240 lines
12 KiB
Python
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()
|