Files
StarPilot/openpilot/starpilot/ui/tests/test_conditional_feature.py
T

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()