mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 21:43:44 +08:00
1ab5058bc8
Implement conditional driving modes with explicit scene, button and traffic state ownership.
172 lines
8.7 KiB
Python
172 lines
8.7 KiB
Python
"""Actual temporary-Params receipts for the read-only conditional owner."""
|
|
|
|
from dataclasses import replace
|
|
import json
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.starpilot.conditional_mode.policy import ModeChoice
|
|
from openpilot.starpilot.conditional_mode.preferences import CEMOptions, SavedPreferences, encode_preferences
|
|
from openpilot.starpilot.conditional_mode.runtime_settings import (
|
|
DOCUMENT_KEY, REFRESH_NS, SAFE_MODE_KEY, ConditionalSettingsOwner,
|
|
DocumentState, SafeModeState,
|
|
)
|
|
|
|
|
|
class ConditionalRuntimeSettingsTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.directory = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.directory.cleanup)
|
|
self.params = Params(self.directory.name)
|
|
self.owner = ConditionalSettingsOwner(self.params)
|
|
|
|
def path(self, key: str) -> Path:
|
|
return Path(self.params.get_param_path(key))
|
|
|
|
def saved(self, mode: ModeChoice = ModeChoice.CEM) -> SavedPreferences:
|
|
return SavedPreferences(mode=mode, cem=replace(CEMOptions(), speed_mps=10.0))
|
|
|
|
def test_absent_is_known_factory_cem_and_safe_mode_false(self):
|
|
snapshot = self.owner.refresh(1_000_000_000)
|
|
self.assertIs(snapshot.document_state, DocumentState.ABSENT)
|
|
self.assertIs(snapshot.safe_mode_state, SafeModeState.ABSENT_FALSE)
|
|
self.assertIsNone(snapshot.document_raw)
|
|
self.assertIsNone(snapshot.safe_mode_raw)
|
|
self.assertEqual(snapshot.revision, 1)
|
|
verdict = self.owner.verdict(snapshot, now_mono_ns=1_100_000_000, drive_id=7)
|
|
self.assertEqual(verdict.status, 'ready')
|
|
self.assertIs(verdict.selection.choice, ModeChoice.CEM)
|
|
self.assertFalse(verdict.safe_mode)
|
|
self.assertIsNone(self.params.get(DOCUMENT_KEY))
|
|
self.assertIsNone(self.params.get(SAFE_MODE_KEY))
|
|
|
|
def test_valid_document_immutable_revision_and_original_stamp(self):
|
|
document = self.saved()
|
|
self.params.put(DOCUMENT_KEY, json.loads(encode_preferences(document)), block=True)
|
|
owner = self.owner
|
|
first = owner.refresh(1_000_000_000)
|
|
self.assertIs(first.document_state, DocumentState.VALID)
|
|
self.assertEqual(first.preferences, document)
|
|
self.assertEqual(owner.verdict(first, now_mono_ns=1_050_000_000, drive_id=9).selection.settings.cem_speed_mps, 10.0)
|
|
with patch('openpilot.starpilot.conditional_mode.runtime_settings.read_saved') as saved_read:
|
|
for i in range(20):
|
|
self.assertTrue(owner.affirm(first, now_mono_ns=1_050_000_000 + i * 10_000_000))
|
|
self.assertEqual(owner.verdict(first, now_mono_ns=1_050_000_000 + i * 10_000_000,
|
|
drive_id=9).status, 'ready')
|
|
saved_read.assert_not_called()
|
|
same = owner.refresh(1_000_000_000 + REFRESH_NS)
|
|
self.assertEqual(same.revision, first.revision)
|
|
self.assertEqual(same.observed_mono_ns, first.observed_mono_ns)
|
|
self.assertEqual(same.verified_mono_ns, 1_000_000_000 + REFRESH_NS)
|
|
self.assertTrue(owner.affirm(first, now_mono_ns=2_100_000_000))
|
|
self.assertFalse(owner.affirm(first, now_mono_ns=3_100_000_001))
|
|
|
|
def test_periodic_refresh_keeps_revision_valid_between_thread_ticks(self):
|
|
start = 1_000_000_000
|
|
snapshot = self.owner.refresh(start)
|
|
for elapsed in range(0, 3_000_000_000, 10_000_000):
|
|
if elapsed % 110_000_000 == 0:
|
|
self.owner.refresh(start + elapsed)
|
|
self.assertTrue(self.owner.affirm(snapshot, now_mono_ns=start + elapsed), elapsed)
|
|
self.assertEqual(self.owner.current.revision, snapshot.revision)
|
|
self.assertFalse(self.owner.affirm(snapshot, now_mono_ns=self.owner.current.verified_mono_ns + REFRESH_NS + 1))
|
|
|
|
def test_external_revision_and_safe_mode_are_independent(self):
|
|
first = self.owner.refresh(1_000_000_000)
|
|
self.params.put(DOCUMENT_KEY, json.loads(encode_preferences(self.saved())), block=True)
|
|
self.params.put_bool(SAFE_MODE_KEY, True, block=True)
|
|
changed = self.owner.refresh(2_000_000_000)
|
|
self.assertEqual(changed.revision, 2)
|
|
self.assertIs(changed.document_state, DocumentState.VALID)
|
|
self.assertIs(changed.safe_mode_state, SafeModeState.TRUE)
|
|
self.assertFalse(self.owner.affirm(first, now_mono_ns=2_000_000_000))
|
|
verdict = self.owner.verdict(changed, now_mono_ns=2_050_000_000, drive_id=9)
|
|
self.assertEqual(verdict.status, 'safe_mode')
|
|
self.assertTrue(verdict.safe_mode)
|
|
self.assertIsNone(verdict.selection)
|
|
self.params.put_bool(SAFE_MODE_KEY, False, block=True)
|
|
recovered = self.owner.refresh(3_000_000_000)
|
|
self.assertEqual(recovered.revision, 3)
|
|
self.assertIs(recovered.safe_mode_state, SafeModeState.FALSE)
|
|
self.assertIs(self.owner.verdict(recovered, now_mono_ns=3_050_000_000, drive_id=9).selection.choice, ModeChoice.CEM)
|
|
|
|
def test_corrupt_document_preserves_source_then_recovers(self):
|
|
raw = b'{"version":1,"version":1}'
|
|
self.path(DOCUMENT_KEY).write_bytes(raw)
|
|
snapshot = self.owner.refresh(1_000_000_000)
|
|
self.assertIs(snapshot.document_state, DocumentState.INVALID)
|
|
self.assertEqual(snapshot.document_raw, raw)
|
|
self.assertEqual(self.owner.verdict(snapshot, now_mono_ns=1_050_000_000, drive_id=1).status,
|
|
'unavailable_document')
|
|
self.assertEqual(self.path(DOCUMENT_KEY).read_bytes(), raw)
|
|
self.path(DOCUMENT_KEY).write_bytes(encode_preferences(self.saved(ModeChoice.CCM)))
|
|
recovered = self.owner.refresh(2_000_000_000)
|
|
self.assertEqual(recovered.revision, 2)
|
|
self.assertEqual(self.owner.verdict(recovered, now_mono_ns=2_050_000_000, drive_id=1).selection.choice,
|
|
ModeChoice.CCM)
|
|
self.assertEqual(self.path(DOCUMENT_KEY).read_bytes(), encode_preferences(self.saved(ModeChoice.CCM)))
|
|
|
|
def test_read_error_is_distinct_from_absence_and_stops_old_affirmation(self):
|
|
first = self.owner.refresh(1_000_000_000)
|
|
self.path(DOCUMENT_KEY).mkdir()
|
|
failed = self.owner.refresh(2_000_000_000)
|
|
self.assertIs(failed.document_state, DocumentState.READ_ERROR)
|
|
self.assertEqual(self.owner.verdict(failed, now_mono_ns=2_050_000_000, drive_id=1).status, 'read_error')
|
|
self.assertFalse(self.owner.affirm(first, now_mono_ns=2_050_000_000))
|
|
self.path(DOCUMENT_KEY).rmdir()
|
|
recovered = self.owner.refresh(3_000_000_000)
|
|
self.assertIs(recovered.document_state, DocumentState.ABSENT)
|
|
self.assertEqual(recovered.revision, 3)
|
|
|
|
def test_safe_mode_raw_is_strict_and_unreadable_is_not_false(self):
|
|
self.path(SAFE_MODE_KEY).write_bytes(b'true')
|
|
bad = self.owner.refresh(1_000_000_000)
|
|
self.assertIs(bad.safe_mode_state, SafeModeState.INVALID)
|
|
self.assertIsNone(self.owner.verdict(bad, now_mono_ns=1_010_000_000, drive_id=1).safe_mode)
|
|
self.assertEqual(self.owner.verdict(bad, now_mono_ns=1_010_000_000, drive_id=1).status,
|
|
'invalid_safe_mode')
|
|
self.path(SAFE_MODE_KEY).unlink()
|
|
self.path(SAFE_MODE_KEY).mkdir()
|
|
failed = self.owner.refresh(2_000_000_000)
|
|
self.assertIs(failed.safe_mode_state, SafeModeState.READ_ERROR)
|
|
self.assertEqual(self.owner.verdict(failed, now_mono_ns=2_010_000_000, drive_id=1).status, 'read_error')
|
|
self.path(SAFE_MODE_KEY).rmdir()
|
|
restored = self.owner.refresh(3_000_000_000)
|
|
self.assertIs(restored.safe_mode_state, SafeModeState.ABSENT_FALSE)
|
|
self.assertEqual(self.owner.verdict(restored, now_mono_ns=3_010_000_000, drive_id=1).status, 'ready')
|
|
|
|
def test_document_failure_remains_visible_when_safe_mode_is_true(self):
|
|
raw = b'{bad json'
|
|
self.path(DOCUMENT_KEY).write_bytes(raw)
|
|
self.params.put_bool(SAFE_MODE_KEY, True, block=True)
|
|
snapshot = self.owner.refresh(1_000_000_000)
|
|
verdict = self.owner.verdict(snapshot, now_mono_ns=1_010_000_000, drive_id=1)
|
|
self.assertEqual(verdict.status, 'unavailable_document')
|
|
self.assertTrue(verdict.safe_mode)
|
|
self.assertIsNone(verdict.selection)
|
|
self.assertEqual(self.path(DOCUMENT_KEY).read_bytes(), raw)
|
|
|
|
def test_foreign_owner_old_revision_clock_rollback_and_rate_limit(self):
|
|
first = self.owner.refresh(1_000_000_000)
|
|
other = ConditionalSettingsOwner(self.params)
|
|
other.refresh(1_000_000_000)
|
|
self.assertFalse(other.affirm(first, now_mono_ns=1_010_000_000))
|
|
self.path(DOCUMENT_KEY).write_bytes(encode_preferences(self.saved()))
|
|
not_due = self.owner.refresh(1_250_000_000)
|
|
self.assertIs(not_due, first)
|
|
self.assertIs(self.owner.verdict(first, now_mono_ns=1_250_000_000, drive_id=1).selection.choice,
|
|
ModeChoice.CEM)
|
|
changed = self.owner.refresh(2_000_000_000)
|
|
self.assertIs(changed.document_state, DocumentState.VALID)
|
|
self.assertFalse(self.owner.affirm(first, now_mono_ns=2_000_000_000))
|
|
rollback = self.owner.refresh(1_999_999_999)
|
|
self.assertIs(rollback.document_state, DocumentState.READ_ERROR)
|
|
self.assertFalse(self.owner.affirm(changed, now_mono_ns=2_000_000_000))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|