Files
StarPilot/openpilot/starpilot/ui/tests/test_pip_settings.py
T
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

294 lines
14 KiB
Python

"""Side-camera saved controls through real disposable Params."""
import fcntl
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from unittest.mock import Mock
from openpilot.common.params import Params
from openpilot.starpilot.ui.feature_settings_state import FeatureRow, FeatureSettingsRequest, row_change
from openpilot.starpilot.ui.feature_settings_state import FeatureUiAction
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.appearance_owner import AppearanceOwner
from openpilot.starpilot.ui.presentation import Profile
from openpilot.starpilot.ui.settings_state import Destination
from openpilot.starpilot.ui.shell import ShellMode
from openpilot.starpilot.ui.pip_owner import EDITOR, FORMAT_PREFIX, PiPOwner, RESET
from openpilot.starpilot.ui.pip_preferences import BLINKER, ENABLED, MASK, decode_mask, encode_mask, read_pip, starting_mask
class PiPSettingsTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.params = Params(temporary.name)
self.parked = True
self.owner = PiPOwner(self.params, lambda: self.parked, lambda: False)
def row(self, key):
return next(row for row in self.owner.snapshot().rows if row.key == key)
def test_absence_is_default_off_and_reader_does_not_write(self):
saved = read_pip(self.params)
self.assertFalse(saved.enabled)
self.assertIsNotNone(saved.mask)
self.assertIsNone(saved.source(MASK)[0])
self.assertEqual(self.row(ENABLED).value, "Off")
self.assertEqual(self.params.get_type(MASK).name, "JSON")
self.assertEqual(self.row("pip:mask:right:x").value, "315.0")
def test_explicit_edit_preserves_other_bytes_and_rejects_stale_source(self):
action = row_change(self.row(BLINKER))
assert action is not None
self.assertTrue(self.owner.apply(action))
self.assertEqual(Path(self.params.get_param_path(BLINKER)).read_bytes(), b"1")
self.assertFalse(self.owner.apply(action))
mask_request = row_change(self.row("pip:mask:right:x"))
assert mask_request is not None
self.assertTrue(self.owner.apply(mask_request))
saved = read_pip(self.params)
assert saved.mask is not None and saved.mask.center_left is not None
self.assertEqual(saved.mask.center_left[0], 325)
self.assertEqual(saved.source(BLINKER)[0], b"1")
self.assertFalse(self.owner.apply(mask_request))
self.assertIsNone(saved.source(ENABLED)[0])
def test_invalid_document_requires_confirmed_reset_and_preserves_source(self):
path = Path(self.params.get_param_path(MASK))
invalid = b'{"width":1928,"width":1928}'
path.write_bytes(invalid)
self.assertIsNone(read_pip(self.params).mask)
self.assertFalse(self.row(ENABLED).available)
reset = self.row(RESET)
self.assertTrue(reset.available)
request = FeatureSettingsRequest(RESET, reset.source, "confirm", confirmation=True,
dependencies=reset.dependencies)
self.assertFalse(self.owner.apply(FeatureSettingsRequest(RESET, reset.source, "confirm",
dependencies=reset.dependencies)))
self.assertEqual(path.read_bytes(), invalid)
self.assertTrue(self.owner.apply(request))
self.assertIsNotNone(decode_mask(path.read_bytes()))
self.assertFalse(self.owner.apply(request))
def test_camera_format_is_automatic_without_rewriting_saved_crop(self):
original = encode_mask(starting_mask(1344, 760))
path = Path(self.params.get_param_path(MASK))
path.write_bytes(original)
page = self.owner.snapshot(include_editor=True)
self.assertEqual(page.title, 'Blind Spot Camera and Preview')
self.assertFalse(any(row.key.startswith(FORMAT_PREFIX) for row in page.rows))
self.assertEqual(path.read_bytes(), original)
editor, source = self.owner.editor_with_source()
assert editor is not None
self.assertEqual(editor['width'], 1344)
def test_visual_editor_uses_integer_source_pixels_and_one_crop_write(self):
import json
editor, dependencies = self.owner.editor_with_source()
self.assertEqual(editor, {"width": 1928, "height": 1208, "centerLeft": [315, 548],
"centerRight": [1571, 539], "cropSize": 580, "invert": False})
row = next(row for row in self.owner.snapshot(include_editor=True).rows if row.key == EDITOR)
self.assertEqual(dependencies, row.dependencies)
self.assertTrue(row.available)
draft = {"width": 1928, "height": 1208, "center_left": [320, 550],
"center_right": None, "crop_size": 580}
request = FeatureSettingsRequest(EDITOR, row.source, json.dumps(draft), confirmation=True,
dependencies=row.dependencies)
self.assertTrue(self.owner.apply(request))
saved = read_pip(self.params)
self.assertEqual(saved.mask.center_left, (320.0, 550.0))
self.assertIsNone(saved.mask.center_right)
self.assertFalse(self.owner.apply(request))
self.assertIsNone(saved.source(ENABLED)[0])
def test_visual_editor_refuses_format_change_fractional_source_and_invalid_invert(self):
import json
row = next(row for row in self.owner.snapshot(include_editor=True).rows if row.key == EDITOR)
draft = {"width": 1344, "height": 760, "center_left": [320, 550],
"center_right": None, "crop_size": 580}
self.assertFalse(self.owner.apply(FeatureSettingsRequest(EDITOR, row.source, json.dumps(draft), confirmation=True,
dependencies=row.dependencies)))
draft["width"], draft["height"] = 1928, 1208
draft["crop_size"] = 580.5
self.assertFalse(self.owner.apply(FeatureSettingsRequest(EDITOR, row.source, json.dumps(draft), confirmation=True,
dependencies=row.dependencies)))
path = Path(self.params.get_param_path(MASK))
path.write_bytes(b'{"width":1928,"height":1208,"crop_size":580.5,"center_left":[315,548],"center_right":[1571,539]}')
self.assertIsNone(self.owner.editor_with_source()[0])
self.assertFalse(next(row for row in self.owner.snapshot(include_editor=True).rows if row.key == EDITOR).available)
self.assertIsNotNone(read_pip(self.params).mask)
path.write_bytes(encode_mask(starting_mask(1928, 1208)))
Path(self.params.get_param_path("PIPPreviewInvert")).write_bytes(b"invalid")
self.assertIsNone(self.owner.editor_with_source()[0])
self.assertFalse(next(row for row in self.owner.snapshot(include_editor=True).rows if row.key == EDITOR).available)
Path(self.params.get_param_path("PIPPreviewInvert")).write_bytes(b"0")
path.write_bytes(b'{"width":1928,"height":1208,"crop_size":19,"center_left":[315,548],"center_right":[1571,539]}')
self.assertIsNotNone(read_pip(self.params).mask)
self.assertIsNone(self.owner.editor_with_source()[0])
self.assertFalse(next(row for row in self.owner.snapshot(include_editor=True).rows if row.key == EDITOR).available)
def test_corrupt_crop_can_reset_without_silent_repair(self):
path = Path(self.params.get_param_path(MASK))
path.write_bytes(b"{invalid")
row = self.row(RESET)
self.assertTrue(row.available)
self.assertEqual(path.read_bytes(), b"{invalid")
self.assertTrue(self.owner.apply(FeatureSettingsRequest(row.key, row.source, "confirm", confirmation=True,
dependencies=row.dependencies)))
self.assertEqual(path.read_bytes(), encode_mask(starting_mask(1928, 1208)))
def test_parked_rechecks_and_source_swap_before_lock(self):
request = row_change(self.row(ENABLED))
assert request is not None
self.parked = False
self.assertFalse(self.owner.apply(request))
self.assertIsNone(read_pip(self.params).source(ENABLED)[0])
self.parked = True
calls = 0
def parked():
nonlocal calls
calls += 1
return calls == 1
owner = PiPOwner(self.params, parked, lambda: False)
self.assertFalse(owner.apply(request))
self.assertIsNone(read_pip(self.params).source(ENABLED)[0])
with patch("openpilot.starpilot.ui.pip_owner.tempfile.NamedTemporaryFile") as stage:
stage.side_effect = OSError("cannot stage")
self.assertFalse(self.owner.apply(request))
self.assertIsNone(read_pip(self.params).source(ENABLED)[0])
def test_nonregular_and_oversized_sources_cannot_authorize_repair(self):
path = Path(self.params.get_param_path(MASK))
path.write_bytes(b"x" * 5000)
self.assertFalse(self.row(RESET).available)
self.assertEqual(path.read_bytes(), b"x" * 5000)
path.unlink()
path.symlink_to(Path(self.params.get_param_path(BLINKER)))
self.assertFalse(self.row(RESET).available)
path.unlink()
os.mkfifo(path)
self.assertFalse(self.row(RESET).available)
path.unlink()
def test_params_lock_contention_cannot_write(self):
request = row_change(self.row(ENABLED))
assert request is not None
root = Path(self.params.get_param_path(ENABLED)).parent.parent
lock = os.open(root / ".lock", os.O_CREAT | os.O_RDONLY, 0o775)
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
self.assertFalse(self.owner.apply(request))
self.assertIsNone(read_pip(self.params).source(ENABLED)[0])
finally:
os.close(lock)
def test_large_native_parent_child_edit_and_abandoned_reset_dialog(self):
from openpilot.starpilot.ui.runtime_app import StarShellSession
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets import confirm_dialog
session = StarShellSession.__new__(StarShellSession)
session._mode = ShellMode.SETTINGS
session.selected = Destination.APPEARANCE
session.appearance_page = "appearance"
session.appearance_owner = AppearanceOwner(self.params, lambda: self.parked)
session.pip_owner = self.owner
session.profile = Profile.LARGE
session.adapter = Mock()
session._snapshot_cache = None
session.appearance_scroll = 0
session._pip_request_epoch = 0
session.input = Mock()
session._unavailable = Mock()
link = FeatureRow("", "Blind Spot Camera", "", page="pip", available=True)
session._appearance_ui(FeatureUiAction("open", link))
self.assertEqual(session.appearance_page, "appearance")
session.appearance_page = "pip"
row = next(row for row in session.appearance_snapshot().rows if row.key == BLINKER)
session._appearance_ui(FeatureUiAction("change", row))
self.assertEqual(read_pip(self.params).on_blinker, True)
dialogs = []
class Dialog:
def __init__(self, question, button, callback):
dialogs.append((question, callback))
with patch.object(confirm_dialog, "ConfirmDialog", Dialog), \
patch("openpilot.starpilot.ui.runtime_app.gui_app.push_widget"):
reset = next(row for row in session.appearance_snapshot().rows if row.key == RESET)
session._appearance_ui(FeatureUiAction("reset", reset))
self.assertEqual(len(dialogs), 1)
self.assertIn("may resume on the next drive", dialogs[0][0])
session._appearance_ui(FeatureUiAction("back"))
session.appearance_page = "pip"
dialogs[0][1](DialogResult.CONFIRM)
self.assertIsNone(read_pip(self.params).source(MASK)[0])
self.assertFalse(any(row.key.startswith(FORMAT_PREFIX) for row in session.appearance_snapshot().rows))
session._appearance_ui(FeatureUiAction("back"))
self.assertEqual(session.appearance_page, "appearance")
session._appearance_ui(FeatureUiAction("back"))
self.assertEqual(session.selected, Destination.STAR)
def test_compact_visuals_child_edit_and_confirm_reset(self):
from openpilot.starpilot.ui import appearance_compact
class Button:
def __init__(self, label, value):
self.label, self.value = label, value
self.callback = None
self.enabled = True
def set_click_callback(self, callback):
self.callback = callback
def set_enabled(self, enabled):
self.enabled = enabled
def click(self):
assert self.enabled and self.callback is not None
self.callback()
class Scroller:
def __init__(self):
self._scroller = self
self.items = []
def add_widgets(self, cards):
self.items.extend(cards)
class Dialog:
def __init__(self, question, icon, callback, red):
self.question, self.callback = question, callback
class Session:
def __init__(inner):
inner.feature_owner = FeatureSettingsOwner(self.params, lambda group: self.parked,
vehicle_fingerprint=lambda: "TEST CAR")
def appearance_snapshot(inner):
return AppearanceOwner(self.params, lambda: self.parked).snapshot(Profile.COMPACT)
def pip_snapshot(inner):
return self.owner.snapshot()
def appearance_request(inner, request):
return self.owner.apply(request)
def feature_snapshot(inner, page):
return inner.feature_owner.snapshot(page, parked=self.parked, system_long=self.parked,
lateral_context=False, metric=False)
def feature_request(inner, request):
return inner.feature_owner.apply(request)
shown = []
with patch.object(appearance_compact, "BigButton", Button), \
patch.object(appearance_compact, "GreyBigButton", Button), \
patch.object(appearance_compact, "NavScroller", Scroller), \
patch.object(appearance_compact, "BigConfirmationDialog", Dialog), \
patch.object(appearance_compact.gui_app, "push_widget", shown.append), \
patch.object(appearance_compact.gui_app, "get_active_widget", side_effect=lambda: shown[-1]), \
patch.object(appearance_compact.gui_app, "texture", return_value=None):
appearance_compact.AppearanceCompact(Session()).entry_button().click()
parent = shown[-1]
self.assertFalse(any(item.label == "blind spot camera" for item in parent.items))
appearance_compact.AppearanceCompact(Session()).open("pip")
child = shown[-1]
next(item for item in child.items if item.label == "show on turn signal").click()
self.assertEqual(read_pip(self.params).on_blinker, True)
next(item for item in child.items if item.label == "restore default camera crop").click()
dialog = shown.pop()
self.assertEqual(dialog.question, "reset crop?\npreview may\nresume if on")
dialog.callback()
self.assertIsNotNone(read_pip(self.params).mask)
child = shown[-1]
self.assertFalse(any('camera format' in getattr(item, 'label', '') for item in child.items))