mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 04:53:47 +08:00
739 lines
38 KiB
Python
739 lines
38 KiB
Python
"""Protected Settings root coordinates and guarded large child actions."""
|
|
|
|
import unittest
|
|
from dataclasses import replace
|
|
from unittest.mock import patch
|
|
from pathlib import Path
|
|
import tempfile
|
|
from types import SimpleNamespace as NS
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.starpilot.longitudinal.profile_document import migrate_profile_document
|
|
from openpilot.starpilot.ui import feature_settings_compact as compact
|
|
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
|
|
from openpilot.starpilot.ui.feature_settings_state import (
|
|
FEATURE_ROW_TOP, FEATURE_ROW_HEIGHT, feature_row_top, FeatureInput, FeatureRow, FeatureSettingsState, FeatureUiAction, row_change,
|
|
FEATURE_PAGE_COUNTER_WIDTH, feature_page_counter_left,
|
|
FEATURE_FOOTER_BOTTOM, feature_footer_buttons, feature_footer_top,
|
|
)
|
|
from openpilot.starpilot.ui.presentation import Profile
|
|
from openpilot.starpilot.ui.settings_state import Destination, SettingsInput, SettingsState, tile_rects
|
|
from opendbc.car.honda.interface import CarInterface as HondaCarInterface
|
|
from opendbc.car.honda.values import CAR as HONDA_CAR
|
|
|
|
|
|
class FeatureNavigationTests(unittest.TestCase):
|
|
def test_page_counter_is_inert_and_footer_buttons_remain_tappable(self):
|
|
for expanded in (True, False):
|
|
state = FeatureSettingsState(sidebar_expanded=expanded)
|
|
counter_left = feature_page_counter_left(state)
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
for x in (counter_left, counter_left + FEATURE_PAGE_COUNTER_WIDTH / 2, counter_left + FEATURE_PAGE_COUNTER_WIDTH):
|
|
with self.subTest(expanded=expanded, x=x):
|
|
controller.press(x, 1015, state)
|
|
controller.release(x, 1015, state)
|
|
self.assertFalse(actions)
|
|
self.assertIsNone(controller.held)
|
|
left = 520 if expanded else 20
|
|
for x, direction in (((left + 25 + 1320) / 2, -1), (1720, 1)):
|
|
controller.press(x, 1015, state)
|
|
controller.release(x, 1015, state)
|
|
self.assertEqual(actions.pop(), FeatureUiAction("scroll", direction=direction))
|
|
|
|
def test_footer_touch_zones_cover_the_full_band_around_the_drawn_buttons(self):
|
|
for expanded in (True, False):
|
|
for subtitle in ("", "Panel description"):
|
|
state = FeatureSettingsState(sidebar_expanded=expanded, subtitle=subtitle)
|
|
left = 520 if expanded else 20
|
|
top = feature_footer_top(state)
|
|
counter_left = feature_page_counter_left(state)
|
|
counter_right = counter_left + FEATURE_PAGE_COUNTER_WIDTH
|
|
buttons = feature_footer_buttons(state)
|
|
for direction, (bx, by, width, height) in zip((-1, 1), buttons, strict=True):
|
|
self.assertGreaterEqual(by, top)
|
|
self.assertLessEqual(by + height, FEATURE_FOOTER_BOTTOM)
|
|
self.assertTrue(bx + width < counter_left if direction == -1 else bx > counter_right)
|
|
for x in (bx, bx + width / 2, bx + width):
|
|
for y in (by, by + height / 2, by + height):
|
|
with self.subTest(expanded=expanded, subtitle=subtitle, x=x, y=y):
|
|
self.assertEqual(FeatureInput.target(x, y, state), FeatureUiAction("scroll", direction=direction))
|
|
for x, y, direction in ((left, top, -1), (counter_left - 1, FEATURE_FOOTER_BOTTOM, -1),
|
|
(counter_right + 1, top, 1), (2140, FEATURE_FOOTER_BOTTOM, 1)):
|
|
with self.subTest(expanded=expanded, subtitle=subtitle, x=x, y=y):
|
|
self.assertEqual(FeatureInput.target(x, y, state), FeatureUiAction("scroll", direction=direction))
|
|
self.assertIsNone(FeatureInput.target(1720, FEATURE_FOOTER_BOTTOM + 1, state))
|
|
self.assertIsNone(FeatureInput.target(1720, FEATURE_FOOTER_BOTTOM - 5, replace(state, editor=True)))
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
bx, by, width, height = buttons[1]
|
|
controller.press(bx + width / 2, by + 4, state)
|
|
controller.move(bx + width / 2 + 20, by - 26, state)
|
|
controller.release(bx + width / 2 + 20, by - 26, state)
|
|
self.assertEqual(actions, [FeatureUiAction("scroll", direction=1)])
|
|
|
|
def test_drag_tracks_only_horizontal_body_movement(self):
|
|
state = FeatureSettingsState()
|
|
controller = FeatureInput(lambda _action: None)
|
|
for dx, dy, expected, canceled in ((20, 0, 0, False), (-80, 0, -80, False), (80, 10, 80, False),
|
|
(80, 41, 0, False), (0, 80, 0, True)):
|
|
with self.subTest(dx=dx, dy=dy):
|
|
controller.press(1000, 200, state)
|
|
controller.move(1000 + dx, 200 + dy, state)
|
|
self.assertEqual(controller.drag_x, expected)
|
|
self.assertEqual(controller.held is None, canceled)
|
|
controller.press(1000, 200, state)
|
|
controller.move(920, 200, state)
|
|
controller.move(980, 200, state)
|
|
self.assertEqual(controller.drag_x, 0)
|
|
controller.move(1080, 200, state)
|
|
self.assertEqual(controller.drag_x, 80)
|
|
|
|
def test_drag_resets_through_existing_cancel_paths(self):
|
|
state = FeatureSettingsState()
|
|
controller = FeatureInput(lambda _action: None)
|
|
resets = (lambda: controller.cancel(), lambda: controller.press(1000, 200, state),
|
|
lambda: controller.release(920, 200, state),
|
|
lambda: controller.move(920, 80, state),
|
|
lambda: controller.move(920, 200, replace(state, scroll=5)),
|
|
lambda: controller.move(920, 200, replace(state, page="child")),
|
|
lambda: controller.move(920, 200, replace(state, sidebar_expanded=False)))
|
|
for reset in resets:
|
|
controller.press(1000, 200, state)
|
|
controller.move(920, 200, state)
|
|
self.assertEqual(controller.drag_x, -80)
|
|
reset()
|
|
self.assertEqual(controller.drag_x, 0)
|
|
|
|
def test_native_toyota_swap_has_separate_authority_on_both_profiles(self):
|
|
from opendbc.car.toyota.interface import CarInterface as ToyotaInterface
|
|
from opendbc.car.toyota.values import CAR as ToyotaCAR
|
|
from openpilot.starpilot.controllers.toyota_cruise import ToyotaCruisePreference
|
|
from openpilot.starpilot.ui import runtime_app
|
|
|
|
vehicle = ToyotaInterface.get_non_essential_params(ToyotaCAR.TOYOTA_COROLLA_TSS2)
|
|
self.assertTrue(vehicle.openpilotLongitudinalControl and vehicle.pcmCruise)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
for profile in (Profile.LARGE, Profile.COMPACT):
|
|
with self.subTest(profile=profile):
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session._mode, session.profile = runtime_app.ShellMode.SETTINGS, profile
|
|
self.enterContext(patch.object(session, '_unavailable', lambda _reason: None))
|
|
session.feature_owner = FeatureSettingsOwner(params, session._feature_authority,
|
|
vehicle_fingerprint=lambda: vehicle.carFingerprint, vehicle_params=lambda: vehicle)
|
|
with patch.object(runtime_app, 'ui_state', NS(CP=vehicle)):
|
|
self.assertTrue(session._feature_authority('long'))
|
|
self.assertTrue(session._feature_authority('toyota_cruise'))
|
|
for expected in (True, False):
|
|
state = session.feature_owner.snapshot('profiles', parked=True, system_long=False,
|
|
lateral_context=False, metric=False)
|
|
rows = {row.key: row for row in state.rows}
|
|
self.assertNotIn('CustomCruise', rows)
|
|
self.assertNotIn('CustomCruiseLong', rows)
|
|
self.assertTrue(rows['ReverseCruise'].available)
|
|
request = row_change(rows['ReverseCruise'])
|
|
assert request is not None
|
|
self.assertTrue(session.feature_request(request))
|
|
self.assertEqual(params.get_bool('ReverseCruise'), expected)
|
|
self.assertEqual(ToyotaCruisePreference(vehicle, params).update(), expected)
|
|
session._mode = runtime_app.ShellMode.ONROAD
|
|
with patch.object(session, '_favorite_authority', return_value=False):
|
|
self.assertFalse(session._feature_authority('toyota_cruise'))
|
|
session._mode = runtime_app.ShellMode.SETTINGS
|
|
other = HondaCarInterface.get_non_essential_params(HONDA_CAR.HONDA_CIVIC)
|
|
with patch.object(runtime_app, 'ui_state', NS(CP=other)):
|
|
self.assertFalse(session._feature_authority('toyota_cruise'))
|
|
self.assertTrue(session._feature_authority('long'))
|
|
|
|
def test_native_saved_preferences_need_settings_or_favorite_context_not_cp(self):
|
|
from openpilot.starpilot.ui import runtime_app
|
|
|
|
cp = HondaCarInterface.get_non_essential_params(HONDA_CAR.HONDA_CIVIC)
|
|
self.assertTrue(cp.openpilotLongitudinalControl and cp.pcmCruise)
|
|
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session._mode = runtime_app.ShellMode.SETTINGS
|
|
with patch.object(session, "confirmed_offroad", return_value=True), patch.object(runtime_app, "ui_state", NS(CP=cp)):
|
|
self.assertTrue(session._feature_authority("long"))
|
|
self.assertTrue(session._feature_authority("preferences"))
|
|
cp.openpilotLongitudinalControl = False
|
|
self.assertFalse(session._feature_authority("long"))
|
|
self.assertTrue(session._feature_authority("preferences"))
|
|
cp.openpilotLongitudinalControl = True
|
|
cp.passive = True
|
|
self.assertFalse(session._feature_authority("long"))
|
|
self.assertTrue(session._feature_authority("preferences"))
|
|
with patch.object(session, "confirmed_offroad", return_value=True), patch.object(runtime_app, "ui_state", NS(CP=None)):
|
|
self.assertTrue(session._feature_authority("preferences"))
|
|
with patch.object(session, "confirmed_offroad", return_value=False), patch.object(runtime_app, "ui_state", NS(CP=None)):
|
|
self.assertTrue(session._feature_authority("preferences"))
|
|
self.assertFalse(session._feature_authority("lane"))
|
|
session._mode = runtime_app.ShellMode.ONROAD
|
|
with patch.object(session, "_favorite_authority", return_value=False):
|
|
self.assertFalse(session._feature_authority("preferences"))
|
|
with patch.object(session, "_favorite_authority", return_value=True):
|
|
self.assertTrue(session._feature_authority("preferences"))
|
|
self.assertFalse(session._feature_authority("parked_preferences"))
|
|
|
|
def test_aol_save_does_not_block_navigation_or_queue_duplicate_requests(self):
|
|
import fcntl
|
|
from typing import Any
|
|
import threading
|
|
import time
|
|
from openpilot.starpilot.ui import runtime_app
|
|
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsRequest
|
|
from openpilot.starpilot import saved_document
|
|
from openpilot.starpilot.ui.feature_settings import FeatureSettingsView
|
|
from openpilot.starpilot.ui.presentation import BitmapFonts
|
|
|
|
for profile, withdrawal in ((profile, withdrawal) for profile in (Profile.LARGE, Profile.COMPACT)
|
|
for withdrawal in ("back", "cancel", "close")):
|
|
with self.subTest(profile=profile, withdrawal=withdrawal), tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
params.put_bool("AlwaysOnLateral", True, block=True)
|
|
session: Any = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session.profile = profile
|
|
session._mode = runtime_app.ShellMode.SETTINGS
|
|
session.selected = Destination.DRIVING_CONTROLS
|
|
session.feature_page, session.feature_root_page = "aol", "hub"
|
|
session.feature_scroll = 0
|
|
session.input = NS(cancel=lambda: None)
|
|
session.favorites = NS(cancel=lambda: None)
|
|
fonts = BitmapFonts.__new__(BitmapFonts)
|
|
fonts.profile = profile
|
|
session.view = NS(features=FeatureSettingsView(fonts) if profile == Profile.LARGE else None,
|
|
onroad=NS(navigation=NS(cancel=lambda: None)), close=lambda: None)
|
|
session.drive_state = NS(physical=NS(close=lambda: None))
|
|
session.galaxy_flow = session.pip_warning = session.pip_renderer = session.fonts = NS(close=lambda: None)
|
|
session.bluetooth_source = session.model_source = session.map_source = None
|
|
session.feature_owner = FeatureSettingsOwner(params, lambda _: True, vehicle_fingerprint=lambda: None)
|
|
request = FeatureSettingsRequest("AlwaysOnLateral", b"1", "Off")
|
|
entered = threading.Event()
|
|
acquire = saved_document.acquire_native_lock
|
|
|
|
def blocked(fd, entered=entered, acquire=acquire):
|
|
entered.set()
|
|
acquire(fd)
|
|
root = Path(params.get_param_path("AlwaysOnLateral")).parent.parent
|
|
with (root / ".lock").open("a") as lock, patch.object(session, "_feature_authority", return_value=True), \
|
|
patch.object(session, "feature_snapshot", return_value=FeatureSettingsState(page="aol")), \
|
|
patch.object(saved_document, "acquire_native_lock", blocked):
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
started = time.monotonic()
|
|
self.assertFalse(session.feature_request(request))
|
|
self.assertLess(time.monotonic() - started, .2)
|
|
pending = session._aol_save
|
|
self.assertTrue(entered.wait(1.))
|
|
self.assertEqual(Path(params.get_param_path("AlwaysOnLateral")).read_bytes(), b"1")
|
|
self.assertIn("Saving", session.notice)
|
|
self.assertFalse(session.feature_request(request))
|
|
self.assertIs(session._aol_save, pending)
|
|
if withdrawal == "back":
|
|
session._feature_ui(FeatureUiAction("back"))
|
|
self.assertEqual(session.feature_page, "hub")
|
|
else:
|
|
getattr(session, withdrawal)()
|
|
self.assertTrue(pending.cancel.is_set())
|
|
fcntl.flock(lock, fcntl.LOCK_UN)
|
|
self.assertTrue(pending.done.wait(1.))
|
|
session._poll_aol_save()
|
|
self.assertEqual(Path(params.get_param_path("AlwaysOnLateral")).read_bytes(), b"1")
|
|
self.assertIn("not saved", session.notice)
|
|
if withdrawal == "close":
|
|
self.assertFalse(session.feature_request(request))
|
|
self.assertIsNone(session._aol_save)
|
|
continue
|
|
session.feature_page = "aol"
|
|
self.assertFalse(session.feature_request(request))
|
|
pending = session._aol_save
|
|
self.assertTrue(pending.done.wait(1.))
|
|
session._poll_aol_save()
|
|
self.assertEqual(Path(params.get_param_path("AlwaysOnLateral")).read_bytes(), b"0")
|
|
self.assertEqual(session.notice, "AOL preference saved")
|
|
self.assertIsNone(session._aol_save)
|
|
|
|
def test_aol_enable_worker_rechecks_live_vehicle_after_native_lock(self):
|
|
import fcntl
|
|
import threading
|
|
from typing import Any
|
|
from openpilot.starpilot import saved_document
|
|
from openpilot.starpilot.ui import runtime_app
|
|
from opendbc.car import gen_empty_fingerprint
|
|
from opendbc.car.gm.interface import CarInterface
|
|
from opendbc.car.gm.values import CAR
|
|
|
|
fingerprint = gen_empty_fingerprint()
|
|
fingerprint[0][0x201] = 6
|
|
fingerprint[2][0x320] = 8
|
|
fingerprint[2][0x180] = 4
|
|
cp = CarInterface.get_params(CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, fingerprint, [], True, False, False)
|
|
current = [cp]
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
params.put_bool("AlwaysOnLateral", False, block=True)
|
|
session: Any = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
|
|
session._mode = runtime_app.ShellMode.SETTINGS
|
|
session.selected = Destination.DRIVING_CONTROLS
|
|
session.feature_page = "aol"
|
|
session.feature_owner = FeatureSettingsOwner(params, lambda _: True, vehicle_fingerprint=lambda: cp.carFingerprint,
|
|
vehicle_params=lambda: current[0])
|
|
def request():
|
|
state = session.feature_owner.snapshot("aol", parked=True, system_long=True, lateral_context=True, metric=False)
|
|
row = next(row for row in state.rows if row.key == "AlwaysOnLateral")
|
|
self.assertTrue(row.available)
|
|
return row_change(row)
|
|
with patch.object(session, "_feature_authority", return_value=True):
|
|
self.assertFalse(session.feature_request(request()))
|
|
self.assertTrue(session._aol_save.done.wait(1.))
|
|
session._poll_aol_save()
|
|
self.assertTrue(params.get_bool("AlwaysOnLateral"))
|
|
self.assertEqual(session.notice, "AOL preference saved")
|
|
params.put_bool("AlwaysOnLateral", False, block=True)
|
|
entered = threading.Event()
|
|
acquire = saved_document.acquire_native_lock
|
|
def blocked(fd):
|
|
entered.set()
|
|
acquire(fd)
|
|
root = Path(params.get_param_path("AlwaysOnLateral")).parent.parent
|
|
with (root / ".lock").open("a") as lock, patch.object(saved_document, "acquire_native_lock", blocked):
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
self.assertFalse(session.feature_request(request()))
|
|
pending = session._aol_save
|
|
self.assertTrue(entered.wait(1.))
|
|
current[0] = None
|
|
fcntl.flock(lock, fcntl.LOCK_UN)
|
|
self.assertTrue(pending.done.wait(1.))
|
|
session._poll_aol_save()
|
|
self.assertFalse(params.get_bool("AlwaysOnLateral"))
|
|
self.assertIn("not saved", session.notice)
|
|
|
|
def test_large_root_tile_geometry_and_destination(self):
|
|
state = SettingsState()
|
|
self.assertEqual(tile_rects(state)[2], (1611, 112, 529, 461))
|
|
actions = []
|
|
controller = SettingsInput(Profile.LARGE, actions.append)
|
|
controller.press(1800, 300, state)
|
|
controller.release(1800, 300, state)
|
|
self.assertEqual(actions[0].destination.destination, Destination.DRIVING_CONTROLS)
|
|
|
|
def test_press_release_cancels_on_drag_or_changed_source(self):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
source = FeatureRow("LaneCentering", "Enable Lane Centering", "Off", b"0", ("Off", "On"), available=True)
|
|
state = FeatureSettingsState(page="lane", rows=(source,))
|
|
controller.press(1990, feature_row_top(state) + 92, state)
|
|
controller.move(1800, feature_row_top(state) + 92, state)
|
|
controller.release(1800, feature_row_top(state) + 92, state)
|
|
self.assertFalse(actions)
|
|
controller.press(1990, feature_row_top(state) + 92, state)
|
|
changed = FeatureSettingsState(page="lane", rows=(FeatureRow("LaneCentering", "Enable Lane Centering", "On", b"1", ("Off", "On"), available=True),))
|
|
controller.release(1990, feature_row_top(state) + 92, changed)
|
|
self.assertFalse(actions)
|
|
controller.press(1990, feature_row_top(state) + 92, state)
|
|
controller.release(1990, feature_row_top(state) + 92, state)
|
|
self.assertEqual(row_change(actions[0].row).expected, b"0")
|
|
|
|
def test_swipes_page_from_controls_disabled_rows_and_blank_space(self):
|
|
controls = (FeatureRow("switch", "Switch", "Off", choices=("Off", "On"), available=True),
|
|
FeatureRow("number", "Number", "50", step=5, available=True),
|
|
FeatureRow("", "Child", "", page="child", available=True),
|
|
FeatureRow("pip:reset", "Reset", "", available=True),
|
|
FeatureRow("action", "Action", "", actions=(("OPEN", True),), available=True),
|
|
FeatureRow("disabled", "Disabled", "", actions=(("OPEN", False),)))
|
|
for expanded in (True, False):
|
|
for subtitle in ("", "Description"):
|
|
for row in controls:
|
|
for scroll, start, end, direction in ((0, 2000, 1700, 1), (5, 1800, 2100, -1)):
|
|
with self.subTest(expanded=expanded, subtitle=subtitle, row=row.key, direction=direction):
|
|
state = FeatureSettingsState(rows=(row,) * 7, scroll=scroll, sidebar_expanded=expanded, subtitle=subtitle)
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
y = feature_row_top(state) + 102
|
|
controller.press(start, y, state)
|
|
controller.move(end, y, state)
|
|
self.assertFalse(actions)
|
|
controller.release(end, y, state)
|
|
controller.release(end, y, state)
|
|
self.assertEqual(actions, [FeatureUiAction("scroll", direction=direction)])
|
|
state = FeatureSettingsState(rows=(controls[0],) * 7, scroll=5, sidebar_expanded=expanded, subtitle=subtitle)
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
y = feature_row_top(state) + 3.5 * FEATURE_ROW_HEIGHT
|
|
controller.press(1500, y, state)
|
|
controller.release(1800, y, state)
|
|
self.assertEqual(actions, [FeatureUiAction("scroll", direction=-1)])
|
|
|
|
def test_swipe_rejections_never_restore_a_canceled_tap(self):
|
|
row = FeatureRow("", "Child", "", page="child", available=True)
|
|
for expanded in (True, False):
|
|
state = FeatureSettingsState(rows=(row,) * 7, sidebar_expanded=expanded)
|
|
cases = (((1500, 200), [(1619, 200)]), # short drag
|
|
((1500, 200), [(1620, 261)]), # diagonal
|
|
((1500, 200), [(1500, 280), (1800, 280)]), # vertical first
|
|
((1500, 200), [(1800, 200), (1500, 200)]), # return to origin
|
|
((1500, 200), [(1500, 80), (1800, 200)]), # leave vertically and reenter
|
|
((700, 50), [(1000, 50)]), # header
|
|
((1000, 1015), [(1300, 1015)]), # footer
|
|
((30, 580), [(330, 580)]), # collapse handle
|
|
((200, 200), [(1000, 200)]) if expanded else ((10, 200), [(1000, 200)]))
|
|
for start, moves in cases:
|
|
with self.subTest(expanded=expanded, start=start, moves=moves):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(*start, state)
|
|
for position in moves:
|
|
controller.move(*position, state)
|
|
controller.release(*moves[-1], state)
|
|
self.assertFalse(actions)
|
|
for scroll, count, end in ((0, 7, 1800), (5, 7, 1200), (0, 5, 1200)):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
boundary = replace(state, scroll=scroll, rows=(row,) * count)
|
|
controller.press(1500, 200, boundary)
|
|
controller.release(end, 200, boundary)
|
|
self.assertFalse(actions)
|
|
|
|
def test_canceled_footer_tap_cannot_become_a_body_swipe(self):
|
|
for expanded in (True, False):
|
|
with self.subTest(expanded=expanded):
|
|
state = FeatureSettingsState(rows=(FeatureRow("row", "Row", ""),) * 7, scroll=5, sidebar_expanded=expanded)
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(900, 1015, state)
|
|
self.assertIsNotNone(controller.held)
|
|
controller.cancel_tap()
|
|
controller.release(1800, 800, state)
|
|
self.assertFalse(actions)
|
|
self.assertIsNone(controller.held)
|
|
|
|
def test_flicks_use_recent_movement_and_reject_noise_pauses_and_reversals(self):
|
|
row = FeatureRow("", "Child", "", page="child", available=True)
|
|
cases = (
|
|
("fast", ((0, 0), (.02, 22), (.05, 55), (.075, 80)), (.08, 80), True),
|
|
("hold then flick", ((0, 0), (.5, 0), (.52, 24), (.55, 58), (.575, 80)), (.58, 80), True),
|
|
("long slow drag", ((0, 0), (.15, 40), (.3, 90), (.5, 125)), (.51, 125), True),
|
|
("short slow drag", ((0, 0), (.15, 25), (.3, 55), (.5, 80)), (.51, 80), False),
|
|
("below flick speed", ((0, 0), (.04, 25), (.08, 55), (.12, 80)), (.13, 80), False),
|
|
("too short", ((0, 0), (.02, 25), (.04, 45)), (.05, 45), False),
|
|
("pause before lift", ((0, 0), (.02, 22), (.05, 55), (.075, 80)), (.18, 80), False),
|
|
("lift position spike", ((0, 0), (.02, 22), (.05, 55)), (.06, 80), False),
|
|
("single movement spike", ((0, 0), (.05, 80)), (.06, 80), False),
|
|
("reversal", ((0, 0), (.02, 40), (.05, 110), (.075, 70)), (.08, 70), False),
|
|
("duplicate timestamps", ((0, 0), (0, 22), (0, 55), (0, 80)), (.01, 80), False),
|
|
("out of order timestamps", ((0, 0), (.02, 22), (.01, 55), (.04, 80)), (.05, 80), False),
|
|
)
|
|
for scroll, start, direction in ((0, 2000, 1), (5, 1800, -1), (5, 519, -1)):
|
|
state = FeatureSettingsState(rows=(row,) * 13, scroll=scroll)
|
|
for name, samples, release, accepted in cases:
|
|
with self.subTest(direction=direction, gesture=name):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(start, 200, state)
|
|
for timestamp, distance in samples:
|
|
controller.move(start - direction * distance, 200, state, now=timestamp)
|
|
self.assertFalse(actions)
|
|
timestamp, distance = release
|
|
controller.release(start - direction * distance, 200, state, now=timestamp)
|
|
controller.release(start - direction * distance, 200, state, now=timestamp)
|
|
self.assertEqual(actions, [FeatureUiAction("scroll", direction=direction)] if accepted else [])
|
|
|
|
def test_sidebar_swipes_require_horizontal_travel_and_a_release_in_the_body(self):
|
|
row = FeatureRow("action", "Action", "", actions=(("OPEN", True),), available=True)
|
|
for subtitle in ("", "Description"):
|
|
state = FeatureSettingsState(rows=(row,) * 7, scroll=5, subtitle=subtitle)
|
|
y = feature_row_top(state) + 80
|
|
cases = (((400, y), ((900, y),), True),
|
|
((519, y), ((638, y),), False), # below distance threshold
|
|
((519, y), ((639, y),), True),
|
|
((100, y), ((500, y),), False), # remains in sidebar
|
|
((400, y), ((900, y), (400, y)), False),
|
|
((400, y), ((2145, y),), False),
|
|
((400, y), ((900, y + 250),), True), # horizontal ratio threshold
|
|
((400, y), ((900, y + 251),), False),
|
|
((400, y), ((400, y + 80), (900, y + 80)), False),
|
|
((400, y), ((400, 80), (900, y)), False),
|
|
((400, 50), ((900, y),), False),
|
|
((400, 1015), ((900, y),), False),
|
|
((-1, y), ((900, y),), False))
|
|
for start, moves, accepted in cases:
|
|
with self.subTest(subtitle=subtitle, start=start, moves=moves):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(*start, state)
|
|
for position in moves:
|
|
controller.move(*position, state)
|
|
self.assertFalse(actions)
|
|
controller.release(*moves[-1], state)
|
|
controller.release(*moves[-1], state)
|
|
self.assertEqual(actions, [FeatureUiAction("scroll", direction=-1)] if accepted else [])
|
|
self.assertIsNone(controller.held)
|
|
for changed in (replace(state, page="child"), replace(state, scroll=0), replace(state, sidebar_expanded=False)):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(400, y, state)
|
|
controller.move(900, y, changed)
|
|
controller.release(900, y, state)
|
|
self.assertFalse(actions)
|
|
|
|
def test_flick_history_does_not_leak_into_a_new_press_or_canceled_touch(self):
|
|
state = FeatureSettingsState(rows=(FeatureRow("", "Child", "", page="child", available=True),) * 7)
|
|
for reset in (lambda controller: controller.cancel(), lambda controller: controller.press(2000, 200, state)):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(2000, 200, state)
|
|
for timestamp, x in ((0, 2000), (.03, 1960), (.06, 1920)):
|
|
controller.move(x, 200, state, now=timestamp)
|
|
reset(controller)
|
|
controller.release(1920, 200, state, now=.07)
|
|
self.assertFalse(actions)
|
|
|
|
def test_swipe_context_changes_cancel_but_row_value_refresh_does_not(self):
|
|
row = FeatureRow("switch", "Switch", "Off", b"0", ("Off", "On"), available=True)
|
|
state = FeatureSettingsState(rows=(row,) * 7)
|
|
for changed in (replace(state, page="child"), replace(state, scroll=5), replace(state, sidebar_expanded=False)):
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(2000, 204, state)
|
|
controller.move(1850, 204, changed)
|
|
controller.release(1700, 204, state)
|
|
self.assertFalse(actions)
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
changed = replace(state, rows=(replace(row, value="On", source=b"1"),) * 7)
|
|
controller.press(2000, 204, state)
|
|
controller.move(1850, 204, changed)
|
|
controller.release(1700, 204, changed)
|
|
self.assertEqual(actions, [FeatureUiAction("scroll")])
|
|
actions.clear()
|
|
controller.press(2000, 204, state)
|
|
controller.cancel()
|
|
controller.release(1700, 204, state)
|
|
self.assertFalse(actions)
|
|
|
|
def test_swipe_clears_touch_before_dispatch(self):
|
|
state = FeatureSettingsState(rows=(FeatureRow("", "Child", "", page="child", available=True),) * 7)
|
|
actions = []
|
|
def emit(action):
|
|
self.assertIsNone(controller.held)
|
|
actions.append(action)
|
|
controller = FeatureInput(emit)
|
|
controller.press(1500, 200, state)
|
|
controller.release(1380, 260, state)
|
|
self.assertEqual(actions, [FeatureUiAction("scroll")])
|
|
|
|
def test_boolean_both_action_halves_preserve_requests_and_source_evidence(self):
|
|
for value, source, desired in (("Off", b"0", "On"), ("On", b"1", "Off"),
|
|
("Off (saved mode 0 or 1)", b"1", "On")):
|
|
for x in (1800, 1990):
|
|
with self.subTest(value=value, x=x):
|
|
row = FeatureRow("SLCFallback" if value.startswith("Off (") else "LaneCentering", "Switch", value,
|
|
source, ("Off", "On"), available=True, dependencies=(("master", b"1"),))
|
|
state = FeatureSettingsState(page="lane", rows=(row,))
|
|
actions = []
|
|
controller = FeatureInput(actions.append)
|
|
controller.press(x, feature_row_top(state) + 92, state)
|
|
controller.release(x, feature_row_top(state) + 92, state)
|
|
self.assertEqual(len(actions), 1)
|
|
request = row_change(actions[0].row, actions[0].direction)
|
|
self.assertIsNotNone(request)
|
|
self.assertEqual((request.value, request.expected, request.dependencies), (desired, source, row.dependencies))
|
|
actions.clear()
|
|
unavailable = replace(row, available=False)
|
|
state = replace(state, rows=(unavailable,))
|
|
controller.press(x, feature_row_top(state) + 92, state)
|
|
controller.release(x, feature_row_top(state) + 92, state)
|
|
self.assertFalse(actions)
|
|
self.assertIsNone(row_change(unavailable))
|
|
|
|
def test_curve_page_uses_shared_large_input_rows(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
owner = FeatureSettingsOwner(params, lambda group: group == "long", vehicle_fingerprint=lambda: "TOYOTA COROLLA TSS2")
|
|
hub = owner.snapshot("hub", parked=True, system_long=True, lateral_context=True, metric=False)
|
|
self.assertIn("curve", [row.page for row in hub.rows])
|
|
page = owner.snapshot("curve", parked=True, system_long=True, lateral_context=True, metric=False)
|
|
master = page.rows[0]
|
|
self.assertEqual(master.key, "CurveSpeedController")
|
|
request = row_change(master)
|
|
self.assertIsNotNone(request)
|
|
if request is None:
|
|
self.fail("the curve master has no action")
|
|
self.assertTrue(owner.apply(request))
|
|
self.assertTrue(params.get_bool("CurveSpeedController"))
|
|
|
|
def test_compact_entry_opens_child_scroller(self):
|
|
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
|
|
|
|
class Scroller:
|
|
def __init__(self):
|
|
self.cards = []
|
|
self.items = self.cards
|
|
self._scroller = self
|
|
|
|
def add_widgets(self, cards):
|
|
self.cards.extend(cards)
|
|
|
|
class Session:
|
|
def feature_snapshot(self, page):
|
|
return FeatureSettingsState(page=page, title="Driving Controls",
|
|
rows=(FeatureRow("", "Speed Limit Controller", "Saved settings", page="slc", available=True),))
|
|
def feature_request(self, request):
|
|
return False
|
|
|
|
pushed = []
|
|
with patch.object(compact, "BigButton", Button), patch.object(compact, "GreyBigButton", Button), \
|
|
patch.object(compact, "NavScroller", Scroller), patch.object(compact.gui_app, "push_widget", pushed.append):
|
|
entry = compact.FeatureSettingsCompact(Session()).entry_button()
|
|
self.assertEqual(entry.text, "driving controls")
|
|
entry.click()
|
|
self.assertEqual([card.text for card in pushed[0].cards], ["driving controls", "speed limit controller"])
|
|
|
|
def test_compact_dependent_cards_and_confirmed_reset(self):
|
|
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 ReadButton(Button):
|
|
pass
|
|
|
|
class Scroller:
|
|
def __init__(self):
|
|
self.items = []
|
|
self._scroller = self
|
|
|
|
def add_widgets(self, cards):
|
|
self.items.extend(cards)
|
|
|
|
class Dialog:
|
|
def __init__(self, title, icon, confirm_callback, **kwargs):
|
|
self.confirm = confirm_callback
|
|
|
|
class Session:
|
|
def __init__(self, owner):
|
|
self.owner = owner
|
|
|
|
def feature_snapshot(self, page):
|
|
return self.owner.snapshot(page, parked=True, system_long=True, lateral_context=True, metric=False)
|
|
|
|
def feature_request(self, request):
|
|
return self.owner.apply(request)
|
|
|
|
def card(page, text):
|
|
return next(item for item in page.items if item.text == text)
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
cp = HondaCarInterface.get_non_essential_params("HONDA_CIVIC_BOSCH")
|
|
cp.openpilotLongitudinalControl = True
|
|
cp.pcmCruise = False
|
|
cp.carVin = "TEST"
|
|
owner = FeatureSettingsOwner(params, lambda group: True, vehicle_fingerprint=lambda: "TOYOTA COROLLA TSS2",
|
|
vehicle_params=lambda: cp)
|
|
adapter = compact.FeatureSettingsCompact(Session(owner))
|
|
pushed = []
|
|
with patch.object(compact, "BigButton", Button), patch.object(compact, "GreyBigButton", ReadButton), \
|
|
patch.object(compact, "NavScroller", Scroller), patch.object(compact, "BigConfirmationDialog", Dialog), \
|
|
patch.object(compact.gui_app, "push_widget", pushed.append), patch.object(compact.gui_app, "texture", lambda *args: None):
|
|
adapter.open("slc")
|
|
slc = pushed[-1]
|
|
self.assertFalse(any(item.text in ('confirm lower limits', 'confirm higher limits') for item in slc.items))
|
|
adoption = card(slc, "automatic adoption")
|
|
self.assertFalse(params.get_bool('SLCConfirmation'))
|
|
adoption.click()
|
|
self.assertTrue(params.get_bool('SLCConfirmation'))
|
|
self.assertFalse(any(item.text in ('confirm lower limits', 'confirm higher limits') for item in slc.items))
|
|
|
|
adapter.open("aggressive/acceleration")
|
|
curve = pushed[-1]
|
|
for _ in range(6):
|
|
card(curve, "preset").click()
|
|
self.assertEqual(len([item for item in curve.items if item.text.endswith("mph point +")]), 10)
|
|
|
|
params.put_bool("CustomPersonalities", True, block=True)
|
|
path = Path(params.get_param_path("LongitudinalPersonalityProfiles"))
|
|
path.write_bytes(b"{invalid")
|
|
adapter.open("profiles")
|
|
profiles = pushed[-1]
|
|
card(profiles, "reset invalid profiles").click()
|
|
pushed[-1].confirm()
|
|
self.assertFalse(params.get_bool("CustomPersonalities"))
|
|
self.assertIsNotNone(migrate_profile_document(path.read_bytes()))
|
|
self.assertFalse(any(item.text == "reset invalid profiles" for item in profiles.items))
|
|
|
|
def test_large_hub_back_returns_to_settings_root(self):
|
|
from openpilot.starpilot.ui.runtime_app import StarShellSession
|
|
from openpilot.starpilot.ui.shell import ShellMode
|
|
session = StarShellSession.__new__(StarShellSession)
|
|
session._mode = ShellMode.SETTINGS
|
|
session.selected = Destination.DRIVING_CONTROLS
|
|
session.feature_page = "hub"
|
|
session.feature_scroll = 0
|
|
self.enterContext(patch.object(session, "feature_snapshot", lambda: FeatureSettingsState()))
|
|
self.enterContext(patch.object(session, "input", type("Input", (), {"cancel": lambda self: None})(), create=True))
|
|
session._feature_ui(FeatureUiAction("back"))
|
|
self.assertEqual(session.selected, Destination.STAR)
|
|
|
|
def test_curve_reset_confirmation_on_both_native_pages(self):
|
|
from openpilot.starpilot.ui.runtime_app import StarShellSession
|
|
from openpilot.system.ui.widgets import DialogResult
|
|
|
|
class Dialog:
|
|
def __init__(self, _question, *_args, callback=None, **_kwargs):
|
|
self.confirm = callback or _args[-1]
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
params = Params(directory)
|
|
legacy = Path(params.get_param_path("CurvatureData"))
|
|
legacy.write_bytes(b'{"0.001":{"average":2.0,"count":2}}')
|
|
owner = FeatureSettingsOwner(params, lambda _group: True, vehicle_fingerprint=lambda: "TOYOTA COROLLA TSS2")
|
|
row = next(row for row in owner.snapshot("curve", parked=True, system_long=True,
|
|
lateral_context=True, metric=False).rows if row.key == "curve_reset")
|
|
self.assertEqual(FeatureInput.target(1900, FEATURE_ROW_TOP + 77, FeatureSettingsState(page="curve", rows=(row,))).kind, "reset")
|
|
large = StarShellSession.__new__(StarShellSession)
|
|
self.enterContext(patch.object(large, "feature_request", owner.apply))
|
|
pushed = []
|
|
with patch("openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", Dialog), \
|
|
patch("openpilot.starpilot.ui.runtime_app.gui_app.push_widget", pushed.append):
|
|
large._confirm_feature_reset(row)
|
|
self.assertIsNone(params.get("CurveComfortData"))
|
|
pushed[-1].confirm(DialogResult.CONFIRM)
|
|
self.assertIsNotNone(params.get("CurveComfortData"))
|
|
|
|
params.remove("CurveComfortData")
|
|
row = next(row for row in owner.snapshot("curve", parked=True, system_long=True,
|
|
lateral_context=True, metric=False).rows if row.key == "curve_reset")
|
|
class Session:
|
|
def feature_snapshot(self, page):
|
|
return owner.snapshot(page, parked=True, system_long=True, lateral_context=True, metric=False)
|
|
def feature_request(self, request):
|
|
return owner.apply(request)
|
|
adapter = compact.FeatureSettingsCompact(Session())
|
|
pushed = []
|
|
with patch.object(compact, "BigConfirmationDialog", Dialog), \
|
|
patch.object(compact.gui_app, "push_widget", pushed.append), \
|
|
patch.object(compact.gui_app, "texture", lambda *_args: None):
|
|
adapter._confirm_reset(row, lambda: None)
|
|
legacy.write_bytes(b'{"0.001":{"average":2.0,"count":2}}')
|
|
pushed[-1].confirm()
|
|
self.assertEqual(params.get("CurveComfortData"), {"version": 1, "buckets": {}})
|
|
row = next(row for row in owner.snapshot("curve", parked=True, system_long=True,
|
|
lateral_context=True, metric=False).rows if row.key == "curve_reset")
|
|
adapter._confirm_reset(row, lambda: None)
|
|
pushed[-1].confirm()
|
|
self.assertIsNotNone(params.get("CurveComfortData"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|