mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 21:13:41 +08:00
709 lines
39 KiB
Python
709 lines
39 KiB
Python
"""Disposable Params and real shared-owner conditional editor requests."""
|
|
from unittest.mock import Mock
|
|
from openpilot.starpilot.ui.presentation import BitmapFonts
|
|
|
|
|
|
from pathlib import Path
|
|
import json
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
import warnings
|
|
from unittest.mock import patch
|
|
|
|
from openpilot.common.params import Params
|
|
from opendbc.car.car_helpers import interfaces
|
|
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
|
from openpilot.starpilot.conditional_mode.button_actions import MEDIA_KEYS
|
|
from openpilot.starpilot.ui.wheel_feature import PREFIX as BUTTON_PREFIX
|
|
from openpilot.starpilot.conditional_mode.preferences import MPH_TO_MPS, SavedPreferences, decode_preferences, encode_preferences
|
|
from openpilot.starpilot.conditional_mode.actions import commit, commit_manual, commit_mode, stock_document
|
|
from openpilot.starpilot.conditional_mode.runtime_settings import ConditionalSettingsOwner, read_mode_sources
|
|
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
|
|
from openpilot.starpilot.conditional_mode.policy import ModeChoice
|
|
from openpilot.starpilot.conditional_mode.manual_saved import KEY as MANUAL_KEY, SavedCodes, decode as decode_manual, encode as encode_manual
|
|
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
|
|
from openpilot.starpilot.ui.feature_settings_state import FeatureInput, FeaturePage, FeatureRow, FeatureSettingsRequest, row_change
|
|
from openpilot.starpilot.ui import feature_settings_compact as compact
|
|
from openpilot.starpilot.ui import feature_settings as large
|
|
from openpilot.starpilot.ui.presentation import Profile
|
|
|
|
|
|
def required_change(row: FeatureRow, direction: int = 1) -> FeatureSettingsRequest:
|
|
request = row_change(row, direction)
|
|
assert request is not None
|
|
return request
|
|
|
|
|
|
class ConditionalFeatureTests(unittest.TestCase):
|
|
def setUp(self):
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
self.params = Params(temporary.name)
|
|
self.parked = True
|
|
from opendbc.car.honda.values import CAR as HONDA
|
|
self.cp = interfaces[HONDA.HONDA_CIVIC_BOSCH].get_non_essential_params(HONDA.HONDA_CIVIC_BOSCH)
|
|
self.cp.openpilotLongitudinalControl = True
|
|
self.cp.carVin = "VIN1"
|
|
self.owner = FeatureSettingsOwner(self.params, lambda group: self.parked and group in
|
|
("conditional", "preferences", "parked_preferences"),
|
|
vehicle_fingerprint=lambda: getattr(self.cp, "carFingerprint", None),
|
|
vehicle_params=lambda: self.cp)
|
|
|
|
def test_four_saved_modes_round_trip_through_actual_baseline_owner(self):
|
|
original = SavedPreferences(cem=replace(SavedPreferences().cem, speed_mps=13.0))
|
|
self.params.put("ConditionalModeConfig", json.loads(encode_preferences(original)), block=True)
|
|
manual = encode_manual(SavedCodes())
|
|
Path(self.params.get_param_path(MANUAL_KEY)).write_bytes(manual)
|
|
drive = SelfdriveD.__new__(SelfdriveD)
|
|
drive.params, drive.CP = self.params, self.cp
|
|
drive.conditional_settings = ConditionalSettingsOwner(self.params)
|
|
drive.conditional_replay = False
|
|
now = 1_000_000_000
|
|
for value, choice, baseline in (("Experimental", ModeChoice.STOCK, True), ("Chill", ModeChoice.STOCK, False),
|
|
("Conditional Experimental", ModeChoice.CEM, False), ("Conditional Chill", ModeChoice.CCM, False)):
|
|
row = self.row("conditional:mode")
|
|
self.assertEqual(row.choices, ("Chill", "Experimental", "Conditional Experimental", "Conditional Chill"))
|
|
self.assertTrue(self.owner.apply(replace(required_change(row), value=value)))
|
|
self.assertEqual(self.row("conditional:mode").value, value)
|
|
saved = decode_preferences(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes())
|
|
self.assertEqual(saved.mode, choice)
|
|
self.assertEqual(saved.cem, original.cem)
|
|
self.assertEqual(saved.ccm, original.ccm)
|
|
self.assertEqual(Path(self.params.get_param_path(MANUAL_KEY)).read_bytes(), manual)
|
|
drive.refresh_saved_driving_mode(now)
|
|
drive.update_conditional_mode(None)
|
|
self.assertEqual(drive.experimental_mode, baseline)
|
|
now += 100_000_000
|
|
self.assertTrue(self.owner.apply(replace(required_change(self.row("conditional:mode")), value="Experimental")))
|
|
self.params.put_bool("SafeMode", True, block=True)
|
|
drive.refresh_saved_driving_mode(now)
|
|
drive.update_conditional_mode(None)
|
|
self.assertFalse(drive.experimental_mode)
|
|
|
|
def test_actual_firmware_data_round_trip_and_changed_firmware_rejects_request(self):
|
|
from opendbc.car.honda.fingerprints import FW_VERSIONS
|
|
from opendbc.car.honda.values import CAR as HONDA
|
|
(ecu, address, subaddress), versions = next(iter(FW_VERSIONS[HONDA.HONDA_CIVIC_BOSCH].items()))
|
|
firmware = self.cp.init("carFw", 1)[0]
|
|
firmware.ecu, firmware.address, firmware.subAddress = ecu, address, subaddress or 0
|
|
firmware.fwVersion, firmware.brand = versions[0], "honda"
|
|
self.assertIsInstance(self.cp.to_dict()["carFw"][0]["fwVersion"], bytes)
|
|
row = self.row("conditional:mode")
|
|
self.assertIsNotNone(row.capability)
|
|
for malformed in ((), (self.cp.carFingerprint,), (self.cp.carFingerprint, "invalid")):
|
|
self.assertFalse(self.owner.apply(replace(required_change(row), value="Experimental", capability=malformed)))
|
|
self.assertTrue(self.owner.apply(replace(required_change(row), value="Experimental")))
|
|
self.assertEqual(self.row("conditional:mode").value, "Experimental")
|
|
self.assertTrue(self.owner.apply(replace(required_change(self.row("conditional:mode")), value="Chill")))
|
|
request = replace(required_change(self.row("conditional:mode")), value="Experimental")
|
|
firmware.fwVersion = versions[0] + b"changed"
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertFalse(self.params.get_bool("ExperimentalMode"))
|
|
# Hashing copies the full CP instead of marking the live builder serialized.
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("error", UserWarning)
|
|
self.cp.to_bytes()
|
|
|
|
def test_mode_request_rejects_changed_baseline_safe_mode_and_final_cp(self):
|
|
cp = self.cp
|
|
assert cp is not None and not isinstance(cp, SimpleNamespace)
|
|
for change in ("baseline", "safe", "cp"):
|
|
self.params.put_bool("ExperimentalMode", False, block=True)
|
|
self.params.put_bool("SafeMode", False, block=True)
|
|
cp.carVin = "VIN1"
|
|
request = replace(required_change(self.row("conditional:mode")), value="Experimental")
|
|
if change == "baseline":
|
|
self.params.put_bool("ExperimentalMode", True, block=True)
|
|
elif change == "safe":
|
|
self.params.put_bool("SafeMode", True, block=True)
|
|
else:
|
|
cp.carVin = "VIN2"
|
|
before = self.params.get("ConditionalModeConfig")
|
|
self.assertFalse(self.owner.apply(request), change)
|
|
self.assertEqual(self.params.get("ConditionalModeConfig"), before)
|
|
self.params.put_bool("SafeMode", False, block=True)
|
|
cp.openpilotLongitudinalControl = False
|
|
row = self.row("conditional:mode")
|
|
self.assertNotIn("Experimental", row.choices)
|
|
self.assertFalse(self.owner.apply(replace(required_change(row), value="Experimental")))
|
|
Path(self.params.get_param_path("ExperimentalMode")).write_bytes(b"01")
|
|
self.assertFalse(self.row("conditional:mode").available)
|
|
self.assertEqual(Path(self.params.get_param_path("ExperimentalMode")).read_bytes(), b"01")
|
|
|
|
def test_mode_second_write_failure_restores_both_sources_and_reports_rollback_failure(self):
|
|
self.params.put_bool("ExperimentalMode", False, block=True)
|
|
old = self.params.get("ConditionalModeConfig")
|
|
baseline_path = Path(self.params.get_param_path("ExperimentalMode"))
|
|
original_replace = os.replace
|
|
attempts = 0
|
|
|
|
def fail_second(source, destination):
|
|
nonlocal attempts
|
|
attempts += 1
|
|
if attempts == 2:
|
|
self.assertEqual(read_mode_sources(self.params), ((b"", False), (b"", False), (b"", False)))
|
|
raise OSError("second replacement failed")
|
|
return original_replace(source, destination)
|
|
|
|
with patch("openpilot.starpilot.conditional_mode.actions.os.replace", side_effect=fail_second):
|
|
result = commit_mode(self.params, stock_document(), b"1", expected=old, expected_baseline=b"0",
|
|
expected_units=None, expected_safe=None, authorized=lambda: True)
|
|
self.assertFalse(result.committed)
|
|
self.assertFalse(result.verified)
|
|
self.assertFalse(result.rollback_failed)
|
|
self.assertEqual(self.params.get("ConditionalModeConfig"), old)
|
|
self.assertEqual(baseline_path.read_bytes(), b"0")
|
|
attempts = 0
|
|
|
|
def fail_write_and_rollback(source, destination):
|
|
nonlocal attempts
|
|
attempts += 1
|
|
if attempts >= 2:
|
|
raise OSError("replacement and restoration failed")
|
|
return original_replace(source, destination)
|
|
|
|
self.params.put("ConditionalModeConfig", json.loads(encode_preferences(SavedPreferences())), block=True)
|
|
old = Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes()
|
|
with patch("openpilot.starpilot.conditional_mode.actions.os.replace", side_effect=fail_write_and_rollback):
|
|
result = commit_mode(self.params, stock_document(), b"1", expected=old, expected_baseline=b"0",
|
|
expected_units=None, expected_safe=None, authorized=lambda: True)
|
|
self.assertFalse(result.committed)
|
|
self.assertFalse(result.verified)
|
|
self.assertTrue(result.rollback_failed)
|
|
|
|
def test_saved_mode_and_conditions_can_change_during_drive_with_exact_source(self):
|
|
self.parked = False
|
|
self.owner.authority = lambda group: group == "preferences"
|
|
mode = self.row("conditional:mode")
|
|
self.assertTrue(mode.available)
|
|
self.assertTrue(self.owner.apply(required_change(mode)))
|
|
threshold = self.row("conditional:cem:speed_mps", FeaturePage.CONDITIONAL_CEM)
|
|
self.assertTrue(threshold.available)
|
|
request = required_change(threshold)
|
|
self.params.put_bool("IsMetric", True, block=True)
|
|
self.assertFalse(self.owner.apply(request))
|
|
|
|
def test_repair_and_manual_choice_reset_stay_parked(self):
|
|
self.parked = False
|
|
self.owner.authority = lambda group: group == "preferences"
|
|
persist = self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM)
|
|
self.assertFalse(persist.available)
|
|
self.assertFalse(self.owner.apply(FeatureSettingsRequest(persist.key, persist.source, "On",
|
|
dependencies=persist.dependencies)))
|
|
invalid = b'{"version":2}'
|
|
Path(self.params.get_param_path("ConditionalModeConfig")).write_bytes(invalid)
|
|
reset = self.row("conditional:reset")
|
|
self.assertFalse(reset.available)
|
|
self.assertFalse(self.owner.apply(FeatureSettingsRequest(reset.key, reset.source, "confirm", confirmation=True,
|
|
dependencies=reset.dependencies)))
|
|
self.assertEqual(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes(), invalid)
|
|
|
|
def test_supported_wheel_assignment_can_change_during_drive_but_rechecks_sources(self):
|
|
cp = interfaces[CAR.HYUNDAI_IONIQ_6].get_non_essential_params(CAR.HYUNDAI_IONIQ_6)
|
|
cp.flags = int(cp.flags | HyundaiFlags.CANFD_LKA_STEER_MSG)
|
|
cp.openpilotLongitudinalControl = True
|
|
self.cp = cp
|
|
self.parked = False
|
|
self.owner.authority = lambda group: group == "preferences"
|
|
row = self.row(BUTTON_PREFIX + MEDIA_KEYS[0])
|
|
self.assertTrue(row.available)
|
|
request = required_change(row)
|
|
self.assertTrue(self.owner.apply(request))
|
|
self.assertFalse(self.owner.apply(request))
|
|
other = required_change(self.row(BUTTON_PREFIX + MEDIA_KEYS[1]))
|
|
cp.flags = int(cp.flags & ~HyundaiFlags.CANFD_LKA_STEER_MSG)
|
|
self.assertFalse(self.owner.apply(other))
|
|
|
|
def rows(self, page=FeaturePage.CONDITIONAL):
|
|
return self.owner.snapshot(page, parked=self.parked, system_long=False,
|
|
lateral_context=False, metric=False).rows
|
|
|
|
def row(self, key, page=FeaturePage.CONDITIONAL):
|
|
return next(row for row in self.rows(FeaturePage.WHEEL if key.startswith(BUTTON_PREFIX) else page) if row.key == key)
|
|
|
|
def test_factory_cem_and_honda_pcm_cruise_does_not_veto(self):
|
|
self.assertIsNone(self.params.get("ConditionalModeConfig"))
|
|
self.assertIn(FeaturePage.CONDITIONAL, [row.page for row in self.rows(FeaturePage.HUB)])
|
|
mode = self.row("conditional:mode")
|
|
self.assertEqual(mode.value, "Conditional Experimental")
|
|
self.assertTrue(mode.available)
|
|
self.assertTrue(self.owner.apply(required_change(mode)))
|
|
saved = decode_preferences(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes())
|
|
self.assertEqual(saved.mode.value, "conditional_chill")
|
|
self.assertFalse(saved.cem.persist_manual)
|
|
|
|
def test_chill_label_and_signal_rows_preserve_existing_consumers(self):
|
|
path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
path.write_bytes(stock_document())
|
|
original = path.read_bytes()
|
|
mode = self.row("conditional:mode")
|
|
self.assertEqual(mode.value, "Chill")
|
|
self.assertNotIn("Stock", mode.choices)
|
|
self.assertEqual(path.read_bytes(), original)
|
|
self.assertFalse(any(row.key.endswith((":signal_lane_detection", ":signal_lane_width_m"))
|
|
for row in self.rows(FeaturePage.CONDITIONAL_CEM)))
|
|
row = self.row("conditional:cem:signal_lane_detection", FeaturePage.LANE_CHANGE)
|
|
self.assertIn("requests Experimental", row.reason)
|
|
self.assertTrue(self.owner.apply(required_change(row)))
|
|
saved = decode_preferences(path.read_bytes())
|
|
self.assertEqual(saved.mode, ModeChoice.STOCK)
|
|
self.assertNotEqual(saved.cem.signal_lane_detection, decode_preferences(original).cem.signal_lane_detection)
|
|
mode = self.row("conditional:mode")
|
|
self.assertTrue(self.owner.apply(replace(required_change(mode), value="Chill")))
|
|
self.assertEqual(decode_preferences(path.read_bytes()).mode, ModeChoice.STOCK)
|
|
|
|
def test_core_configuration_without_car_params_keeps_media_vehicle_bound(self):
|
|
self.cp = None
|
|
mode = self.row("conditional:mode")
|
|
self.assertTrue(mode.available)
|
|
self.assertIsNone(mode.vehicle_fingerprint)
|
|
self.assertTrue(self.owner.apply(required_change(mode)))
|
|
self.assertFalse(any(row.key.startswith(BUTTON_PREFIX) for row in self.rows(FeaturePage.WHEEL)))
|
|
self.cp = SimpleNamespace(carFingerprint="UNSUPPORTED", openpilotLongitudinalControl=False,
|
|
pcmCruise=True, notCar=False, dashcamOnly=False, passive=False)
|
|
threshold = self.row("conditional:cem:speed_mps", FeaturePage.CONDITIONAL_CEM)
|
|
self.assertTrue(threshold.available)
|
|
self.assertTrue(self.owner.apply(required_change(threshold)))
|
|
pending = required_change(self.row("conditional:ccm:launch_assist", FeaturePage.CONDITIONAL_CCM))
|
|
self.parked = False
|
|
self.assertFalse(self.owner.apply(pending))
|
|
|
|
def test_six_ioniq_media_rows_and_exact_source_guards(self):
|
|
cp = interfaces[CAR.HYUNDAI_IONIQ_6].get_non_essential_params(CAR.HYUNDAI_IONIQ_6)
|
|
cp.flags = int(cp.flags | HyundaiFlags.CANFD_LKA_STEER_MSG)
|
|
cp.openpilotLongitudinalControl = True
|
|
self.cp = cp
|
|
rows = [self.row(BUTTON_PREFIX + key) for key in MEDIA_KEYS]
|
|
self.assertTrue(all(row.available and row.value == "Off" for row in rows))
|
|
first = rows[0]
|
|
request = required_change(first)
|
|
self.parked = False
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.parked = True
|
|
self.params.put(MEDIA_KEYS[0], 5, block=True)
|
|
self.assertFalse(self.owner.apply(request))
|
|
row = self.row(first.key)
|
|
self.assertEqual(row.value, "Experimental override")
|
|
self.assertTrue(self.owner.apply(required_change(row)))
|
|
self.assertEqual(self.params.get(MEDIA_KEYS[0]), 6)
|
|
self.cp = None
|
|
self.assertFalse(any(row.key.startswith(BUTTON_PREFIX) for row in self.rows(FeaturePage.WHEEL)))
|
|
|
|
def test_captured_wheel_request_stays_vehicle_bound_with_generic_preferences(self):
|
|
cp = interfaces[CAR.HYUNDAI_IONIQ_6].get_non_essential_params(CAR.HYUNDAI_IONIQ_6)
|
|
cp.flags = int(cp.flags | HyundaiFlags.CANFD_LKA_STEER_MSG)
|
|
cp.openpilotLongitudinalControl = True
|
|
self.cp = cp
|
|
request = required_change(self.row(BUTTON_PREFIX + MEDIA_KEYS[0]))
|
|
destination = Path(self.params.get_param_path(MEDIA_KEYS[0]))
|
|
for replacement in (None, interfaces["HONDA_CIVIC_BOSCH"].get_non_essential_params("HONDA_CIVIC_BOSCH")):
|
|
self.cp = replacement
|
|
self.assertTrue(self.parked)
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertFalse(destination.exists())
|
|
|
|
def test_native_authority_uses_system_long_without_pcm_cruise_veto(self):
|
|
from openpilot.starpilot.ui import runtime_app
|
|
shell = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
shell._mode = runtime_app.ShellMode.SETTINGS
|
|
with patch.object(shell, "confirmed_offroad", return_value=True), \
|
|
patch.object(runtime_app.ui_state, "CP", self.cp):
|
|
self.assertTrue(shell._feature_authority("conditional"))
|
|
self.assertTrue(shell._feature_authority("long"))
|
|
assert self.cp is not None
|
|
self.cp.openpilotLongitudinalControl = False
|
|
self.assertFalse(shell._feature_authority("conditional"))
|
|
self.assertFalse(shell._feature_authority("long"))
|
|
self.assertTrue(shell._feature_authority("preferences"))
|
|
self.cp.openpilotLongitudinalControl = True
|
|
for field in ('passive', 'dashcamOnly', 'notCar'):
|
|
with self.subTest(field=field):
|
|
setattr(self.cp, field, True)
|
|
self.assertFalse(shell._feature_authority("conditional"))
|
|
self.assertFalse(shell._feature_authority("long"))
|
|
self.assertTrue(shell._feature_authority("preferences"))
|
|
setattr(self.cp, field, False)
|
|
|
|
def test_native_saved_actions_use_settings_mode_and_supported_current_car(self):
|
|
from openpilot.starpilot.ui import runtime_app
|
|
shell = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
shell._mode = runtime_app.ShellMode.SETTINGS
|
|
with patch.object(shell, "confirmed_offroad", return_value=False), \
|
|
patch.object(runtime_app.ui_state, "CP", self.cp):
|
|
self.assertTrue(shell._feature_authority("lane_change"))
|
|
self.assertTrue(shell._feature_authority("conditional_wheel"))
|
|
self.assertTrue(shell._feature_authority("conditional"))
|
|
self.assertTrue(shell._feature_authority("aol"))
|
|
shell._mode = runtime_app.ShellMode.ONROAD
|
|
self.assertFalse(shell._feature_authority("lane_change"))
|
|
self.assertFalse(shell._feature_authority("conditional_wheel"))
|
|
|
|
def test_threshold_units_and_exact_source_guard(self):
|
|
speed = self.row("conditional:cem:speed_mps", FeaturePage.CONDITIONAL_CEM)
|
|
self.assertEqual(speed.unit, "mph")
|
|
request = required_change(speed)
|
|
self.params.put_bool("IsMetric", True, block=True)
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertIsNone(self.params.get("ConditionalModeConfig"))
|
|
speed = self.row("conditional:cem:speed_mps", FeaturePage.CONDITIONAL_CEM)
|
|
self.assertEqual(speed.unit, "km/h")
|
|
self.assertTrue(self.owner.apply(required_change(speed)))
|
|
saved = decode_preferences(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes())
|
|
self.assertAlmostEqual(saved.cem.speed_mps, 1 / 3.6)
|
|
stale = required_change(self.row("conditional:ccm:launch_assist", FeaturePage.CONDITIONAL_CCM))
|
|
Path(self.params.get_param_path("ConditionalModeConfig")).write_bytes(b'{"bad":true}')
|
|
self.assertFalse(self.owner.apply(stale))
|
|
|
|
def test_bounds_and_manual_persistence_clears_only_selected_code(self):
|
|
row = self.row("conditional:ccm:set_speed_margin_mps", FeaturePage.CONDITIONAL_CCM)
|
|
forged = FeatureSettingsRequest(row.key, row.source, "100", vehicle_fingerprint=row.vehicle_fingerprint,
|
|
capability=row.capability, dependencies=row.dependencies, display_unit=row.display_unit)
|
|
self.assertFalse(self.owner.apply(forged))
|
|
self.assertIsNone(self.params.get("ConditionalModeConfig"))
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
manual_path.write_bytes(encode_manual(SavedCodes(cem=2, ccm=1)))
|
|
persist = self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM)
|
|
self.assertEqual(persist.value, "Off")
|
|
self.assertTrue(self.owner.apply(required_change(persist)))
|
|
self.assertTrue(decode_preferences(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes()).cem.persist_manual)
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes(cem=0, ccm=1))
|
|
manual_path.write_bytes(encode_manual(SavedCodes(cem=2, ccm=1)))
|
|
self.assertTrue(self.owner.apply(required_change(self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM))))
|
|
self.assertFalse(decode_preferences(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes()).cem.persist_manual)
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes(cem=0, ccm=1))
|
|
|
|
def test_manual_source_stale_and_partial_clear_cannot_resurrect_override(self):
|
|
from openpilot.starpilot.conditional_mode import actions
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
config_path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
manual_path.write_bytes(encode_manual(SavedCodes(cem=2, ccm=1)))
|
|
request = required_change(self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM))
|
|
manual_path.write_bytes(encode_manual(SavedCodes(cem=1, ccm=1)))
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertFalse(config_path.exists())
|
|
request = required_change(self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM))
|
|
original = actions.os.replace
|
|
def fail_config(source, destination):
|
|
if Path(destination) == config_path:
|
|
raise OSError("simulated config rename failure")
|
|
return original(source, destination)
|
|
with patch.object(actions.os, "replace", side_effect=fail_config):
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes(cem=0, ccm=1))
|
|
self.assertFalse(config_path.exists())
|
|
|
|
def test_failed_manual_clear_never_changes_config(self):
|
|
from openpilot.starpilot.conditional_mode import actions
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
config_path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
manual_path.write_bytes(encode_manual(SavedCodes(cem=2, ccm=1)))
|
|
request = required_change(self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM))
|
|
original = actions.os.replace
|
|
def fail_manual(source, destination):
|
|
if Path(destination) == manual_path:
|
|
raise OSError("simulated manual rename failure")
|
|
return original(source, destination)
|
|
with patch.object(actions.os, "replace", side_effect=fail_manual):
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes(cem=2, ccm=1))
|
|
self.assertFalse(config_path.exists())
|
|
|
|
def test_parked_authority_loss_after_clear_leaves_config_off(self):
|
|
from openpilot.starpilot.conditional_mode.preferences import encode_preferences
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
config_path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
original = encode_manual(SavedCodes(cem=2, ccm=1))
|
|
manual_path.write_bytes(original)
|
|
desired = SavedPreferences()
|
|
desired = replace(desired, cem=replace(desired.cem, persist_manual=True))
|
|
calls = 0
|
|
def authority():
|
|
nonlocal calls
|
|
calls += 1
|
|
return calls < 3
|
|
result = commit_manual(self.params, expected_config=None, expected_units=None, expected_safe=None,
|
|
expected_manual=original, authorized=authority, choice=ModeChoice.CEM,
|
|
config_raw=encode_preferences(desired))
|
|
self.assertTrue(result.manual_cleared)
|
|
self.assertFalse(result.committed)
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes(cem=0, ccm=1))
|
|
self.assertFalse(config_path.exists())
|
|
|
|
def test_stock_is_not_a_manual_saved_family(self):
|
|
path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
raw = encode_manual(SavedCodes(cem=2, ccm=1))
|
|
path.write_bytes(raw)
|
|
result = commit_manual(self.params, expected_config=None, expected_units=None, expected_safe=None,
|
|
expected_manual=raw, authorized=lambda: True, choice=ModeChoice.STOCK,
|
|
config_raw=stock_document())
|
|
self.assertFalse(result.committed)
|
|
self.assertEqual(path.read_bytes(), raw)
|
|
malformed_commit = Mock(wraps=commit_manual)
|
|
result = malformed_commit(self.params, expected_config=None, expected_units=None, expected_safe=None,
|
|
expected_manual=raw, authorized=lambda: True, choice="conditional_experimental",
|
|
config_raw=stock_document())
|
|
malformed_commit.assert_called_once()
|
|
self.assertFalse(result.committed)
|
|
self.assertEqual(path.read_bytes(), raw)
|
|
|
|
def test_invalid_manual_bytes_need_explicit_confirmed_reset(self):
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
manual_path.write_bytes(b'{"version":9}')
|
|
persist = self.row("conditional:ccm:persist_manual", FeaturePage.CONDITIONAL_CCM)
|
|
self.assertFalse(persist.available)
|
|
reset = self.row("conditional:manual_reset")
|
|
request = FeatureSettingsRequest(reset.key, reset.source, "confirm", vehicle_fingerprint=reset.vehicle_fingerprint,
|
|
capability=reset.capability, dependencies=reset.dependencies)
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertEqual(manual_path.read_bytes(), b'{"version":9}')
|
|
request = replace(request, confirmation=True)
|
|
self.parked = False
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.parked = True
|
|
self.assertTrue(self.owner.apply(request))
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes())
|
|
|
|
def test_active_manual_writer_lease_defers_persist_toggle(self):
|
|
from openpilot.starpilot.conditional_mode.manual_saved import manual_lock_path
|
|
path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
path.write_bytes(encode_manual(SavedCodes(cem=2, ccm=1)))
|
|
request = required_change(self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM))
|
|
script = "import fcntl,sys; f=open(sys.argv[1],'a'); fcntl.flock(f,fcntl.LOCK_EX); print('ready',flush=True); sys.stdin.read(1)"
|
|
child = subprocess.Popen([sys.executable, "-c", script, str(manual_lock_path(self.params))],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
|
|
try:
|
|
self.assertEqual(child.stdout.readline().strip(), "ready")
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertEqual(decode_manual(path.read_bytes()), SavedCodes(cem=2, ccm=1))
|
|
self.assertIsNone(self.params.get("ConditionalModeConfig"))
|
|
finally:
|
|
child.communicate(input="x", timeout=5)
|
|
self.assertTrue(self.owner.apply(request))
|
|
|
|
def test_safe_mode_source_change_invalidates_pending_toggle(self):
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
manual_path.write_bytes(encode_manual(SavedCodes(cem=2, ccm=1)))
|
|
request = required_change(self.row("conditional:cem:persist_manual", FeaturePage.CONDITIONAL_CEM))
|
|
self.params.put_bool("SafeMode", True, block=True)
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes(cem=2, ccm=1))
|
|
self.assertIsNone(self.params.get("ConditionalModeConfig"))
|
|
|
|
def test_valid_other_unit_value_can_step_down_without_clamping_on_render(self):
|
|
preferences = SavedPreferences()
|
|
preferences = replace(preferences, cem=replace(preferences.cem, speed_mps=99 * MPH_TO_MPS))
|
|
path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
path.write_bytes(encode_preferences(preferences))
|
|
self.params.put_bool("IsMetric", True, block=True)
|
|
row = self.row("conditional:cem:speed_mps", FeaturePage.CONDITIONAL_CEM)
|
|
self.assertGreater(float(row.value), 150.0)
|
|
self.assertEqual(row.maximum, float(row.value))
|
|
self.assertIsNone(row_change(row, 1))
|
|
self.assertTrue(self.owner.apply(required_change(row, -1)))
|
|
self.assertLess(decode_preferences(path.read_bytes()).cem.speed_mps, 99 * MPH_TO_MPS)
|
|
preferences = SavedPreferences()
|
|
preferences = replace(preferences, cem=replace(preferences.cem, signal_lane_width_m=15.0),
|
|
ccm=replace(preferences.ccm, set_speed_margin_mps=30 / 3.6))
|
|
path.write_bytes(encode_preferences(preferences))
|
|
self.params.put_bool("IsMetric", False, block=True)
|
|
for key, page, old in (("conditional:cem:signal_lane_width_m", FeaturePage.LANE_CHANGE, 15.0),
|
|
("conditional:ccm:set_speed_margin_mps", FeaturePage.CONDITIONAL_CCM, 30 / 3.6)):
|
|
row = self.row(key, page)
|
|
self.assertGreater(float(row.value), 15.0)
|
|
self.assertTrue(self.owner.apply(required_change(row, -1)))
|
|
current = decode_preferences(path.read_bytes())
|
|
section, field = key.split(":")[1:]
|
|
self.assertLess(getattr(getattr(current, section), field), old)
|
|
|
|
def test_corrupt_bytes_preserved_until_confirmed_reset(self):
|
|
path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
path.write_bytes(b'{"version":2}')
|
|
self.assertFalse(any(row.key == "conditional:mode" for row in self.rows(FeaturePage.WHEEL)))
|
|
reset = self.row("conditional:reset")
|
|
self.assertFalse(self.owner.apply(FeatureSettingsRequest(reset.key, reset.source, "confirm",
|
|
vehicle_fingerprint=reset.vehicle_fingerprint,
|
|
capability=reset.capability, dependencies=reset.dependencies)))
|
|
self.assertEqual(path.read_bytes(), b'{"version":2}')
|
|
request = FeatureSettingsRequest(reset.key, reset.source, "confirm", confirmation=True,
|
|
vehicle_fingerprint=reset.vehicle_fingerprint, capability=reset.capability,
|
|
dependencies=reset.dependencies)
|
|
self.parked = False
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.parked = True
|
|
self.assertTrue(self.owner.apply(request))
|
|
self.assertEqual(decode_preferences(path.read_bytes()).mode.value, "stock")
|
|
|
|
def test_core_configuration_survives_car_change_and_params_lock_contention(self):
|
|
request = required_change(self.row("conditional:mode"))
|
|
assert self.cp is not None
|
|
self.cp.carVin = "VIN2"
|
|
self.assertTrue(self.owner.apply(request))
|
|
self.cp.carVin = "VIN1"
|
|
Path(self.params.get_param_path("ConditionalModeConfig")).unlink()
|
|
request = required_change(self.row("conditional:mode"))
|
|
root = Path(self.params.get_param_path("ConditionalModeConfig")).parent.parent
|
|
script = "import fcntl,sys; f=open(sys.argv[1],'a'); fcntl.flock(f,fcntl.LOCK_EX); print('ready',flush=True); sys.stdin.read(1)"
|
|
child = subprocess.Popen([sys.executable, "-c", script, str(root / ".lock")],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
|
|
try:
|
|
self.assertEqual(child.stdout.readline().strip(), "ready")
|
|
self.assertFalse(self.owner.apply(request))
|
|
self.assertIsNone(self.params.get("ConditionalModeConfig"))
|
|
finally:
|
|
child.communicate(input="x", timeout=5)
|
|
self.assertTrue(self.owner.apply(request))
|
|
|
|
def test_oversized_document_never_repaired_by_render(self):
|
|
path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
path.write_bytes(b'x' * 4097)
|
|
self.assertFalse(any(row.key == "conditional:reset" for row in self.rows(FeaturePage.WHEEL)))
|
|
self.assertEqual(path.read_bytes(), b'x' * 4097)
|
|
|
|
def test_nonregular_document_does_not_block_or_offer_repair(self):
|
|
path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
os.mkfifo(path)
|
|
self.assertFalse(any(row.key == "conditional:reset" for row in self.rows(FeaturePage.WHEEL)))
|
|
path.unlink()
|
|
outside = path.parent.parent / "outside-conditional"
|
|
outside.write_bytes(b'{"version":2}')
|
|
path.symlink_to(outside)
|
|
self.assertFalse(any(row.key == "conditional:reset" for row in self.rows(FeaturePage.WHEEL)))
|
|
self.assertEqual(outside.read_bytes(), b'{"version":2}')
|
|
|
|
def test_final_locked_guard_rejects_new_editor_bytes_and_revoked_authority(self):
|
|
path = Path(self.params.get_param_path("ConditionalModeConfig"))
|
|
calls = 0
|
|
def changed_source():
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls == 2:
|
|
path.write_bytes(b'other editor')
|
|
return True
|
|
result = commit(self.params, stock_document(), None, None, changed_source)
|
|
self.assertFalse(result.committed)
|
|
self.assertEqual(path.read_bytes(), b'other editor')
|
|
path.unlink()
|
|
calls = 0
|
|
def revoked():
|
|
nonlocal calls
|
|
calls += 1
|
|
return calls == 1
|
|
self.assertFalse(commit(self.params, stock_document(), None, None, revoked).committed)
|
|
self.assertFalse(path.exists())
|
|
|
|
def test_large_hit_target_and_compact_parent_child_reset(self):
|
|
state = self.owner.snapshot(FeaturePage.CONDITIONAL, parked=True, system_long=False,
|
|
lateral_context=False, metric=False)
|
|
hit = FeatureInput.target(2000, 285, state)
|
|
assert hit is not None and hit.row is not None
|
|
self.assertEqual(hit.row.key, "conditional:mode")
|
|
class Button:
|
|
def __init__(self, text, value):
|
|
self.text, self.value, self.click = text, value, None
|
|
def set_click_callback(self, callback):
|
|
self.click = callback
|
|
def set_enabled(self, enabled):
|
|
self.enabled = enabled
|
|
class Scroller:
|
|
def __init__(self, *_args):
|
|
self.items = []
|
|
self._scroller = self
|
|
def add_widgets(self, widgets):
|
|
self.items.extend(widgets)
|
|
def dismiss(self, callback):
|
|
callback()
|
|
class Session:
|
|
def __init__(self, owner):
|
|
self.owner = owner
|
|
def feature_snapshot(self, page):
|
|
return self.owner.snapshot(page, parked=True, system_long=False, lateral_context=False, metric=False)
|
|
def feature_request(self, request):
|
|
return self.owner.apply(request)
|
|
stack = []
|
|
with patch.object(compact, "BigButton", Button), patch.object(compact, "GreyBigButton", Button), \
|
|
patch.object(compact, "NavScroller", Scroller), \
|
|
patch.object(compact, "SettingDescriptionDialog", Scroller), patch.object(compact.gui_app, "texture"), \
|
|
patch.object(compact, "BigConfirmationCircleButton", side_effect=lambda _title, _icon, callback: SimpleNamespace(callback=callback)), \
|
|
patch.object(compact.gui_app, "push_widget", stack.append), \
|
|
patch.object(compact.gui_app, "get_active_widget", side_effect=lambda: stack[-1]):
|
|
adapter = compact.FeatureSettingsCompact(Session(self.owner))
|
|
adapter.open(FeaturePage.HUB)
|
|
next(card for card in stack[-1].items if card.text == "conditional driving modes").click()
|
|
child = stack[-1]
|
|
next(card for card in child.items if card.text == "saved driving mode").click()
|
|
self.assertEqual(decode_preferences(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes()).mode.value,
|
|
"conditional_chill")
|
|
next(card for card in child.items if card.text == "experimental conditions").click()
|
|
self.assertTrue(any(card.text.startswith("speed threshold") for card in stack[-1].items))
|
|
stack.pop()
|
|
Path(self.params.get_param_path("ConditionalModeConfig")).write_bytes(b'{"version":2}')
|
|
adapter.open(FeaturePage.CONDITIONAL)
|
|
next(card for card in stack[-1].items if card.text == "restore chill defaults").click()
|
|
dialog = stack.pop()
|
|
dialog.items[-1].callback()
|
|
self.assertEqual(decode_preferences(Path(self.params.get_param_path("ConditionalModeConfig")).read_bytes()).mode.value,
|
|
"stock")
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
manual_path.write_bytes(b'{"version":9}')
|
|
adapter.open(FeaturePage.CONDITIONAL)
|
|
next(card for card in stack[-1].items if card.text == "reset remembered manual choices").click()
|
|
dialog = stack.pop()
|
|
dialog.items[-1].callback()
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes())
|
|
|
|
def test_large_pane_renders_mode_and_child_labels_in_existing_geometry(self):
|
|
labels = []
|
|
fonts = SimpleNamespace(profile=Profile.LARGE,
|
|
draw=lambda value, *_args: labels.append(value),
|
|
measure=lambda value, _role, size: SimpleNamespace(width=len(value) * size * .5),
|
|
vertical_ink=lambda _value, _role, size: (size * .2, size * .8))
|
|
state = self.owner.snapshot(FeaturePage.CONDITIONAL, parked=True, system_long=False,
|
|
lateral_context=False, metric=False)
|
|
with patch.object(large.rl, "draw_rectangle_rounded"), patch.object(large.rl, "draw_rectangle_rounded_lines_ex"), \
|
|
patch.object(large.rl, "draw_line"), patch.object(large.rl, "draw_line_ex"), patch.object(large.rl, "draw_circle"), \
|
|
patch.object(large.clip, "begin_scissor_mode"), \
|
|
patch.object(large.clip, "end_scissor_mode"):
|
|
large.FeatureSettingsView(Mock(spec=BitmapFonts, **vars(fonts))).render(state)
|
|
self.assertIn("Conditional Driving Modes", labels)
|
|
self.assertIn("Saved driving mode", labels)
|
|
self.assertIn("Experimental conditions", labels)
|
|
self.assertIn("Chill conditions", labels)
|
|
|
|
def test_large_manual_reset_confirmation_revoked_on_navigation(self):
|
|
from openpilot.starpilot.ui.runtime_app import StarShellSession
|
|
from openpilot.starpilot.ui.settings_state import Destination
|
|
from openpilot.starpilot.ui.shell import ShellMode
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
manual_path = Path(self.params.get_param_path("ConditionalManualState"))
|
|
manual_path.write_bytes(b'{"version":9}')
|
|
row = self.row("conditional:manual_reset")
|
|
request = FeatureSettingsRequest(row.key, row.source, "confirm", confirmation=True,
|
|
vehicle_fingerprint=row.vehicle_fingerprint, capability=row.capability,
|
|
dependencies=row.dependencies)
|
|
session = StarShellSession.__new__(StarShellSession)
|
|
session._mode = ShellMode.SETTINGS
|
|
session.selected = Destination.DRIVING_CONTROLS
|
|
session.feature_page = FeaturePage.CONDITIONAL
|
|
session._lane_change_request_epoch = 0
|
|
dialogs = []
|
|
with patch.object(StarShellSession, "feature_request", side_effect=self.owner.apply), \
|
|
patch("openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog",
|
|
side_effect=lambda *args, **kwargs: SimpleNamespace(callback=kwargs["callback"])), \
|
|
patch("openpilot.starpilot.ui.runtime_app.gui_app.push_widget", dialogs.append):
|
|
session._confirm_conditional(request)
|
|
session._lane_change_request_epoch += 1
|
|
dialogs[-1].callback(DialogResult.CONFIRM)
|
|
self.assertEqual(manual_path.read_bytes(), b'{"version":9}')
|
|
session._confirm_conditional(request)
|
|
dialogs[-1].callback(DialogResult.CONFIRM)
|
|
self.assertEqual(decode_manual(manual_path.read_bytes()), SavedCodes())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|