from typing import Any import ast from concurrent.futures import Future from pathlib import Path import re import time import unittest import uuid from unittest.mock import Mock, patch class Widget: def show_event(self): pass def hide_event(self): pass class Scroller(Widget): def __init__(self, rows, **kwargs): self.rows = rows self.scroll_panel = Scroll() class Row(tuple): def set_touch_valid_callback(self, callback): pass class Scroll: def is_touch_valid(self): return True class App: def __init__(self): self.widgets = [] self.mouse_events = [] self.ticks = [] def add_nav_stack_tick(self, callback): self.ticks.append(callback) def remove_nav_stack_tick(self, callback): self.ticks.remove(callback) def push_widget(self, widget): self.widgets.append(widget) def get_active_widget(self): return self.widgets[-1] if self.widgets else None def pop_widget(self): self.widgets.pop() class Executor: def __init__(self, **kwargs): self.jobs = [] def submit(self, callback, *args, **kwargs): future = Future() self.jobs.append((future, callback, args, kwargs)) return future def shutdown(self, **kwargs): pass class Owner: def __init__(self, authority, session_valid): self.authority = authority self.session_valid = session_valid def snapshot(self, **kwargs): pass def request(self, *args, **kwargs): pass def close(self): pass class Dialog: def __init__(self, *args, **kwargs): self.callback = kwargs.get('callback') self.text = '' def set_title(self, *args): pass def set_callback(self, callback): self.callback = callback def clear(self): self.text = '' class TestBluetoothLarge(unittest.TestCase): def setUp(self): path = Path(__file__).parents[1] / 'bluetooth_large.py' tree = ast.parse(path.read_text()) tree.body = [node for node in tree.body if not isinstance(node, (ast.Import, ast.ImportFrom))] self.app = App() self.forgotten: list[str] = [] scope: dict[str, Any] = {'Callable': object, 'Future': Future, 'ThreadPoolExecutor': Executor, 're': re, 'time': time, 'uuid': uuid, 'Widget': Widget, 'Scroller': Scroller, 'BluetoothOwner': Owner, 'BluetoothRejected': ValueError, 'BluetoothUnavailable': RuntimeError, 'gui_app': self.app, 'button_item': lambda *a, **kw: Row((a, kw)), 'text_item': lambda *a, **kw: Row((a, kw)), 'ConfirmDialog': Dialog, 'alert_dialog': Dialog, 'DialogResult': type('Result', (), {'CONFIRM': 1}), 'Keyboard': Dialog, 'setup_status': lambda: 'Ready to pair', 'large_phone_rows': lambda allowed: [], 'forget_phone_keys': lambda allowed, address: self.forgotten.append(address), 'is_app_link': lambda device: bool(device.get('connected')) and not device.get('paired') and not device.get('trusted')} tree.body.insert(0, ast.ImportFrom(module='__future__', names=[ast.alias(name='annotations')], level=0)) exec(compile(ast.fix_missing_locations(tree), str(path), 'exec'), scope) self.panel_type = scope['BluetoothLarge'] self.panel = self.panel_type(lambda: True) def test_constructor_with_menu_mouse_edge_creates_scroller(self): self.app.mouse_events = [object()] panel = self.panel_type(lambda: True) self.assertIsInstance(panel._scroller, Scroller) panel.show_event() panel.hide_event() def test_show_submits_snapshot_without_running_owner(self): self.panel.show_event() self.assertFalse(self.panel.pending.done()) self.assertEqual(self.panel.executor.jobs[0][2], ()) self.assertTrue(self.panel.owner.session_valid(self.panel.session)) def test_large_device_buttons_reach_galaxy_when_projection_owns_bluetooth(self): from openpilot.starpilot.bluetooth.native_owner import NativeBluetoothOwner from openpilot.starpilot.bluetooth.owner import AdapterBusyElsewhere, BluetoothOwner self.panel.owner = NativeBluetoothOwner(lambda: True) self.panel.active = True self.panel.session = ('large', 'test') self.panel.executor = Executor() call = Mock(return_value=(200, {'devices': [], 'available': True, 'powered': True})) with patch.object(BluetoothOwner, 'request', side_effect=AdapterBusyElsewhere('AA holds adapter')), \ patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call', return_value=call): for operation in ('scan', 'pair', 'connect', 'disconnect', 'forget'): address = None if operation == 'scan' else 'AA:BB:CC:DD:EE:FF' self.panel._submit_request(operation, address, None, False, self.panel.session, None, None, '') future, callback, args, kwargs = self.panel.executor.jobs[-1] future.set_result(callback(*args, **kwargs)) payload = {'operation': operation, **({'address': address} if address else {})} call.assert_called_with('POST', '/api/bluetooth/action', payload) def test_open_powered_panel_scans_once_and_stops_on_hide(self): self.panel.show_event() self.panel.pending.set_result({'available': True, 'powered': True, 'parked': True, 'discovering': False}) self.panel._tick() self.assertEqual(self.panel.operation, 'auto_scan') self.assertEqual(self.panel.executor.jobs[-1][3], {'delegate': False}) self.panel.pending.set_result({'available': True, 'powered': True, 'parked': True, 'discovering': False}) self.panel._tick() self.assertEqual(sum(job[2] == ('scan',) for job in self.panel.executor.jobs), 1) executor = self.panel.executor self.panel.hide_event() self.assertEqual(executor.jobs[-1][1], self.panel.owner.close) def test_opening_the_page_never_scans_through_starport(self): from openpilot.starpilot.bluetooth.native_owner import NativeBluetoothOwner from openpilot.starpilot.bluetooth.owner import AdapterBusyElsewhere, BluetoothOwner owner = NativeBluetoothOwner(lambda: True) local = {'available': True, 'powered': True, 'parked': True, 'discovering': False, 'devices': []} with patch.object(BluetoothOwner, 'request', side_effect=AdapterBusyElsewhere('AA holds adapter')), \ patch.object(BluetoothOwner, 'snapshot', return_value=local), \ patch('openpilot.starpilot.galaxy_bluetooth.phone_ui.galaxy_call') as factory: self.assertEqual(owner.request('scan', delegate=False), local) factory.assert_not_called() self.assertIsNone(owner.galaxy) def test_pairing_prompt_opens_once_and_expired_prompt_is_retired(self): prompt = {'id': 'a' * 32, 'kind': 'confirmation', 'value': '123456', 'displayOnly': False} status: dict[str, Any] = {'available': True, 'powered': True, 'parked': True, 'discovering': False, 'pairing': {'state': 'pairing', 'prompt': prompt}} self.panel.show_event() self.panel.pending.set_result(status) self.panel._tick() self.assertEqual(len(self.app.widgets), 1) self.panel._snapshot() self.panel.pending.set_result(status) self.panel._tick() self.assertEqual(len(self.app.widgets), 1) self.panel._snapshot() self.panel.pending.set_result({'available': True, 'powered': True, 'parked': True}) self.panel._tick() self.assertEqual(len(self.app.widgets), 0) def test_pin_cancel_rejects_prompt_and_clears_keyboard(self): prompt = {'id': 'a' * 32, 'kind': 'pin', 'value': '', 'displayOnly': False} self.panel.show_event() self.panel.pending.set_result({'available': True, 'powered': True, 'parked': True, 'pairing': {'state': 'pairing', 'prompt': prompt}}) self.panel._tick() keyboard = self.app.widgets[-1] keyboard.text = '1234' keyboard.callback(0) self.assertEqual(keyboard.text, '') job = self.panel.executor.jobs[-1] self.assertEqual(job[2], ('pairing_response',)) self.assertFalse(job[3]['accepted']) self.assertEqual(job[3]['value'], '') def test_named_devices_and_disconnect_forget_presented(self): self.panel.status = {'available': True, 'powered': True, 'parked': True, 'devices': [ {'address': 'AA:BB', 'name': 'Headphones', 'paired': True, 'connected': True}]} self.panel._scroller = None self.panel._rebuild() rows = self.panel._scroller.rows self.assertIn(('Headphones', 'Disconnect'), [row[0] for row in rows]) self.assertIn(('Forget Headphones', 'Forget'), [row[0] for row in rows]) def test_app_connections_are_grouped_apart_from_bluetooth_devices(self): self.panel.status = {'available': True, 'powered': True, 'parked': True, 'devices': [ {'address': 'AA:BB', 'name': 'Headphones', 'paired': True, 'connected': True}, {'address': 'CC:DD', 'name': 'iPhone', 'paired': False, 'trusted': False, 'connected': True}]} self.panel._scroller = None self.panel._rebuild() names = [row[0] for row in self.panel._scroller.rows] self.assertIn(('Headphones', 'Disconnect'), names) self.assertNotIn(('iPhone', 'Pair'), names) self.assertNotIn(('iPhone', 'Disconnect'), names) self.assertIn('App pairings', [name[0] for name in names]) self.assertIn(('iPhone', 'Forget'), names) def test_forgetting_a_device_also_forgets_its_app_key(self): self.panel.show_event() self.panel.status = {'available': True, 'powered': True, 'parked': True} self.panel._signature = repr((False, True, True, True, None, None, None, ())) self.panel._forget('AA:BB') self.assertEqual(self.forgotten, []) self.assertIsNotNone(self.panel.queued) self.panel.pending.set_result(self.panel.status) self.panel._tick() self.assertEqual(self.forgotten, []) self.panel.pending.set_result(self.panel.status) self.panel._tick() self.assertEqual(self.forgotten, ['AA:BB']) def test_failed_forget_keeps_its_app_key(self): self.panel.show_event() self.panel.status = {'available': True, 'powered': True, 'parked': True} self.panel._forget('AA:BB') self.panel.pending.set_result(self.panel.status) self.panel._tick() self.panel.pending.set_exception(ValueError('Bluetooth forget failed')) self.panel._tick() self.assertEqual(self.forgotten, []) def test_request_waits_for_snapshot_then_submits_async(self): self.panel.show_event() self.panel.status = {'available': True, 'powered': True, 'parked': True} self.panel._signature = repr((False, True, True, True, None, None, None, ())) self.panel._request('connect', address='AA:BB') self.assertIsNotNone(self.panel.queued) self.panel.pending.set_result(self.panel.status) self.panel._tick() self.assertEqual(self.panel.operation, 'connect') self.assertEqual(self.panel.executor.jobs[-1][2], ('connect',)) self.assertFalse(self.panel.pending.done()) def test_refresh_preserves_scroller_and_defers_active_touch(self): self.panel.status = {'available': True, 'powered': True, 'parked': True, 'devices': []} self.panel._rebuild() scroller = self.panel._scroller scroll = scroller.scroll_panel updated: dict[str, Any] = dict(self.panel.status) updated['unshownTelemetry'] = 123 self.panel.status = updated self.panel._rebuild() self.assertIs(self.panel._scroller, scroller) self.panel._touch_held = True self.panel.status['powered'] = False self.panel._rebuild() self.assertIs(self.panel._scroller, scroller) self.panel._touch_held = False self.panel._rebuild() self.assertIs(self.panel._scroller.scroll_panel, scroll) def test_paginated_host_can_refresh_rows_without_rendering_the_native_scroller(self): self.panel.status = {'available': True, 'powered': True, 'parked': True, 'devices': []} self.panel._update_state() self.assertEqual(len(self.panel._scroller.rows), 2) self.panel._touch_held = True self.app.mouse_events = [type('Event', (), {'slot': 0, 'left_down': False})()] self.panel._update_state() self.assertFalse(self.panel._touch_held) self.app.mouse_events = [] self.panel.status['powered'] = False self.panel._update_state() self.assertEqual(len(self.panel._scroller.rows), 1) def test_unnamed_unpaired_devices_not_presented(self): self.panel.status = {'available': True, 'powered': True, 'parked': True, 'devices': [ {'address': 'AA:BB:CC:DD:EE:FF', 'name': 'AA:BB:CC:DD:EE:FF', 'paired': False}, {'address': '11:22:33:44:55:66', 'name': 'Bluetooth Device ยท 55:66', 'paired': False}]} self.panel._rebuild() self.assertEqual(len(self.panel._scroller.rows), 2) def test_hidden_session_cannot_confirm_forget(self): self.panel.show_event() calls = [] self.panel._confirm('Forget?', lambda: calls.append(True)) dialog = self.app.widgets[-1] self.panel.hide_event() dialog.callback(1) self.assertEqual(calls, []) self.assertFalse(self.panel.owner.session_valid(('large', 'old'))) def test_stale_completion_not_applied_on_return(self): self.panel.show_event() self.panel.hide_event() close_future = self.panel.pending self.panel.show_event() close_future.set_result(None) self.panel._tick() self.assertIsNone(self.panel.status) self.assertEqual(self.panel.operation, 'snapshot') if __name__ == '__main__': unittest.main()