mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
a82935f326
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>
194 lines
9.0 KiB
Python
194 lines
9.0 KiB
Python
from typing import Any
|
|
import ast
|
|
from dataclasses import replace
|
|
from enum import StrEnum
|
|
from pathlib import Path
|
|
import sys
|
|
from types import ModuleType
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load_source(name, dependencies=None, classes=None):
|
|
path = ROOT / name
|
|
tree = ast.parse(path.read_text())
|
|
if classes:
|
|
tree.body = [node for node in tree.body if isinstance(node, ast.ClassDef) and node.name in classes]
|
|
else:
|
|
tree.body = [node for node in tree.body if not isinstance(node, ast.ImportFrom) or
|
|
not node.module.startswith('openpilot.')]
|
|
module = ModuleType('isolated_' + path.stem)
|
|
sys.modules[module.__name__] = module
|
|
module.__dict__.update(dependencies or {})
|
|
exec(compile(ast.fix_missing_locations(tree), str(path), 'exec'), module.__dict__)
|
|
return module
|
|
|
|
|
|
profile = load_source('presentation.py', {'StrEnum': StrEnum}, {'Profile'})
|
|
access = load_source('../galaxy/access.py', {'StrEnum': StrEnum}, {'AccessStatus'})
|
|
device = load_source('device_state.py', vars(access))
|
|
software = load_source('software_state.py')
|
|
toggles = load_source('toggles_state.py')
|
|
features = load_source('feature_settings_state.py')
|
|
home = load_source('home_state.py', {'Profile': profile.Profile})
|
|
settings = load_source('settings_state.py', {**vars(device), **vars(software), **vars(toggles), **vars(home),
|
|
'Profile': profile.Profile})
|
|
|
|
|
|
def snapshot_selection():
|
|
tree = ast.parse((ROOT / 'runtime_snapshot.py').read_text())
|
|
adapter = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'RuntimeSnapshotAdapter')
|
|
build = next(node for node in adapter.body if isinstance(node, ast.FunctionDef) and node.name == 'build')
|
|
selected_nodes = [node for node in build.body if
|
|
isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == 'settings_pages'
|
|
for target in node.targets) or
|
|
isinstance(node, ast.If) and any(isinstance(target, ast.Name) and target.id == 'selected'
|
|
for child in node.body if isinstance(child, ast.Assign)
|
|
for target in child.targets)]
|
|
function = ast.parse('def normalize(mode, selected):\n return selected, settings_pages').body[0]
|
|
function.body = selected_nodes + function.body
|
|
namespace: dict[str, Any] = {'Destination': settings.Destination, 'ShellMode': SimpleNamespace(SETTINGS='settings')}
|
|
exec(compile(ast.fix_missing_locations(ast.Module(body=[function], type_ignores=[])), 'snapshot_selection', 'exec'), namespace)
|
|
return namespace['normalize']
|
|
|
|
|
|
normalize = snapshot_selection()
|
|
|
|
|
|
class TestLargeTouchDrift(unittest.TestCase):
|
|
def test_registered_snapshot_selection_survives_three_transitions_then_close(self):
|
|
selected = settings.Destination.STAR
|
|
for point, destination in (((260, 477), settings.Destination.DEVICE),
|
|
((256, 573), settings.Destination.NETWORK),
|
|
((241, 683), settings.Destination.BLUETOOTH)):
|
|
actions = []
|
|
handler = settings.SettingsInput(profile.Profile.LARGE, actions.append, lambda selected=selected: selected)
|
|
normalized, _ = normalize('settings', selected)
|
|
self.assertEqual(normalized, selected)
|
|
handler.press(*point, settings.SettingsState())
|
|
handler.release(*point, settings.SettingsState())
|
|
selected = actions[0].destination.destination
|
|
self.assertEqual(selected, destination)
|
|
self.assertEqual(normalize('settings', selected)[0], settings.Destination.BLUETOOTH)
|
|
for expected in (settings.Destination.STAR, 'home'):
|
|
actions = []
|
|
handler = settings.SettingsInput(profile.Profile.LARGE, actions.append, lambda selected=selected: selected)
|
|
self.assertEqual(normalize('settings', selected)[0], selected)
|
|
handler.press(196, 188, settings.SettingsState())
|
|
handler.release(216, 211, settings.SettingsState())
|
|
self.assertEqual(actions[0].kind, settings.SettingsActionKind.CLOSE)
|
|
selected = settings.Destination.STAR if selected != settings.Destination.STAR else 'home'
|
|
self.assertEqual(selected, expected)
|
|
|
|
def test_all_registered_snapshot_destinations_and_invalid_fallback(self):
|
|
_, registered = normalize('settings', settings.Destination.STAR)
|
|
for destination in (*registered, settings.Destination.NETWORK):
|
|
self.assertEqual(normalize('settings', destination)[0], destination)
|
|
self.assertEqual(normalize('settings', 'unknown')[0], settings.Destination.STAR)
|
|
self.assertEqual(normalize('home', settings.Destination.BLUETOOTH)[0], settings.Destination.STAR)
|
|
|
|
def test_observed_close_taps_accept_finger_drift(self):
|
|
for start, end in (((196, 188), (216, 211)), ((310, 133), (329, 150))):
|
|
actions = []
|
|
handler = settings.SettingsInput(profile.Profile.LARGE, actions.append)
|
|
state = settings.SettingsState()
|
|
handler.press(*start, state)
|
|
handler.move(*end, state)
|
|
handler.release(*end, state)
|
|
self.assertEqual([item.kind for item in actions], [settings.SettingsActionKind.CLOSE])
|
|
|
|
def test_menu_target_drift_and_cross_target_refusal(self):
|
|
state = settings.SettingsState()
|
|
actions = []
|
|
handler = settings.SettingsInput(profile.Profile.LARGE, actions.append)
|
|
handler.press(196, 450, state)
|
|
handler.move(216, 473, state)
|
|
handler.release(216, 473, state)
|
|
self.assertEqual(actions[0].destination.destination, settings.Destination.DEVICE)
|
|
actions.clear()
|
|
handler.press(196, 450, state)
|
|
handler.move(216, 560, state)
|
|
handler.release(196, 450, state)
|
|
self.assertEqual(actions, [])
|
|
|
|
def test_compact_scroll_gesture_still_cancels(self):
|
|
actions = []
|
|
handler = settings.SettingsInput(profile.Profile.COMPACT, actions.append)
|
|
state = settings.SettingsState()
|
|
handler.press(200, 100, state)
|
|
handler.move(220, 123, state)
|
|
handler.release(220, 123, state)
|
|
self.assertEqual(actions, [])
|
|
|
|
def test_large_device_software_and_toggle_static_controls_allow_drift(self):
|
|
cases = ((device.DeviceInput, device.DeviceState(), (1980, 650)),
|
|
(software.SoftwareInput, software.SoftwareState(), (1980, 475)),
|
|
(toggles.TogglesInput, toggles.TogglesState(), (1950, 130)))
|
|
for factory, state, start in cases:
|
|
actions = []
|
|
handler = factory(actions.append)
|
|
end = start[0] + 20, start[1] + 23
|
|
handler.press(*start, state)
|
|
handler.move(*end, state)
|
|
handler.release(*end, state)
|
|
self.assertEqual(len(actions), 1)
|
|
handler.release(*end, state)
|
|
self.assertEqual(len(actions), 1)
|
|
actions.clear()
|
|
handler.press(*start, state)
|
|
handler.move(100, 500, state)
|
|
handler.release(*start, state)
|
|
self.assertEqual(actions, [])
|
|
|
|
def test_toggle_scrolling_cancels_even_if_same_target_is_under_finger(self):
|
|
actions = []
|
|
handler = toggles.TogglesInput(actions.append)
|
|
state = toggles.TogglesState()
|
|
handler.press(1950, 130, state)
|
|
handler.release(1970, 153, replace(state, scroll_y=1))
|
|
self.assertEqual(actions, [])
|
|
|
|
def test_stale_toggle_value_and_device_authority_still_cancel(self):
|
|
actions = []
|
|
handler = toggles.TogglesInput(actions.append)
|
|
state = toggles.TogglesState()
|
|
handler.press(1950, 130, state)
|
|
handler.release(1970, 153, replace(state, enabled=False))
|
|
handler = device.DeviceInput(actions.append)
|
|
state = device.DeviceState()
|
|
handler.press(1980, 650, state)
|
|
handler.release(2000, 673, replace(state, offroad=False))
|
|
self.assertEqual(actions, [])
|
|
|
|
def test_feature_static_control_drift_and_cross_target_refusal(self):
|
|
actions = []
|
|
handler = features.FeatureInput(actions.append)
|
|
row = features.FeatureRow('sample', 'Sample', 'Off', b'0', available=True)
|
|
state = features.FeatureSettingsState(rows=(row, row), parked=True)
|
|
handler.press(1770, features.feature_row_top(state) + 102, state)
|
|
handler.move(1790, features.feature_row_top(state) + 125, state)
|
|
handler.release(1790, features.feature_row_top(state) + 125, state)
|
|
self.assertEqual(len(actions), 1)
|
|
actions.clear()
|
|
handler.press(1910, features.feature_row_top(state) + 102, state)
|
|
handler.move(1940, features.feature_row_top(state) + 125, state)
|
|
handler.release(1910, features.feature_row_top(state) + 102, state)
|
|
self.assertEqual(actions, [])
|
|
|
|
def test_feature_scroll_page_and_source_changes_cancel(self):
|
|
row = features.FeatureRow('sample', 'Sample', 'Off', b'0', available=True)
|
|
state = features.FeatureSettingsState(rows=(row, row), parked=True)
|
|
for changed in (replace(state, scroll=1), replace(state, page='new_page'),
|
|
replace(state, rows=(replace(row, source=b'1'), row))):
|
|
actions = []
|
|
handler = features.FeatureInput(actions.append)
|
|
handler.press(1770, features.feature_row_top(state) + 102, state)
|
|
handler.release(1790, features.feature_row_top(state) + 125, changed)
|
|
self.assertEqual(actions, [])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|