diff --git a/openpilot/starpilot/system/android_auto/supervisor.py b/openpilot/starpilot/system/android_auto/supervisor.py index 9b3bab5761..eaf04ef523 100644 --- a/openpilot/starpilot/system/android_auto/supervisor.py +++ b/openpilot/starpilot/system/android_auto/supervisor.py @@ -1080,7 +1080,7 @@ class Supervisor: """Wait for the car's AOA START; True when it never came and the comma should present as an accessory itself. The wait starts when the car first connects, not when it configures the comma: a head unit - that reads the descriptors of a device it does not recognise may never configure it at all. + that reads the descriptors of a device it does not recognize may never configure it at all. """ connected_at = None while True: @@ -1099,7 +1099,7 @@ class Supervisor: elif state == "DISCONNECTED": connected_at = None if fallback and connected_at is not None and time.monotonic() - connected_at >= USB_HANDSHAKE_WAIT: - # Some head units only send the handshake to devices they recognise as phones. + # Some head units only send the handshake to devices they recognize as phones. self.log("usb_no_accessory_start", waited=USB_HANDSHAKE_WAIT) return True diff --git a/openpilot/starpilot/system/android_auto/tests/test_usb_accessory.py b/openpilot/starpilot/system/android_auto/tests/test_usb_accessory.py index 2c792feffd..b57c29db2e 100644 --- a/openpilot/starpilot/system/android_auto/tests/test_usb_accessory.py +++ b/openpilot/starpilot/system/android_auto/tests/test_usb_accessory.py @@ -2,6 +2,7 @@ import os import subprocess import tty from pathlib import Path +from unittest.mock import Mock import pytest @@ -274,6 +275,7 @@ def test_gadget_detach_settles_for_switch_restore_and_crash_recovery(tmp_path, m gadget = usb.AccessoryGadget(lambda *a, **k: None, configfs=configfs, run=run, adb_wanted=lambda: None) gadget.prepare() root = gadget.root + assert root is not None gadget.switch_to_accessory() # Simulate a crashed daemon: a fresh owner must detach before editing configfs. recovered = usb.AccessoryGadget(lambda *a, **k: None, configfs=configfs, run=run, adb_wanted=lambda: None) @@ -361,7 +363,7 @@ def test_recover_after_a_crash_follows_the_adb_setting(tmp_path, monkeypatch): def test_recover_never_mounts_configfs(tmp_path, monkeypatch): monkeypatch.setattr(usb, "configfs_mount", lambda: None) calls = [] - gadget = usb.AccessoryGadget(lambda *a, **k: None, run=lambda args, **k: calls.append(args), adb_wanted=lambda: True) + gadget = usb.AccessoryGadget(lambda *a, **k: None, run=Mock(side_effect=lambda args, **k: calls.append(args)), adb_wanted=lambda: True) assert not gadget.recover() assert calls == [] @@ -389,6 +391,7 @@ def test_prepare_without_adb_mounts_configfs_itself(tmp_path, monkeypatch): gadget.prepare() assert ("mount", "-t", "configfs", "none", str(fallback), "") in calls assert gadget.root == fallback / "usb_gadget" / usb.GADGET_NAME + assert gadget.root is not None assert (gadget.root / "UDC").read_text().strip() == usb.UDC_NAME assert "usb_configfs_mounted" in events gadget.restore() @@ -398,7 +401,7 @@ def test_prepare_without_adb_mounts_configfs_itself(tmp_path, monkeypatch): def test_prepare_explains_missing_kernel_support(tmp_path, monkeypatch): configfs = tmp_path / "config" configfs.mkdir() # configfs mounted, but no usb_gadget directory: no libcomposite - gadget = usb.AccessoryGadget(lambda *a, **k: None, configfs=configfs, run=lambda *a, **k: None, adb_wanted=lambda: None) + gadget = usb.AccessoryGadget(lambda *a, **k: None, configfs=configfs, run=Mock(return_value=None), adb_wanted=lambda: None) with pytest.raises(RuntimeError, match="no USB gadget support"): gadget.prepare() @@ -418,7 +421,7 @@ def test_usb_cleanup_error_never_hides_why_the_session_ended(): from openpilot.starpilot.system.android_auto import supervisor as supervisor_module sup = supervisor_module.Supervisor.__new__(supervisor_module.Supervisor) events, steps = [], [] - sup.log = lambda name, **values: events.append((name, values)) + sup.log = Mock(side_effect=lambda name, **values: events.append((name, values))) class Part: def __init__(self, name, fails=False): @@ -448,10 +451,10 @@ def test_usb_attempt_keeps_a_car_requested_stop(monkeypatch): from openpilot.starpilot.system.android_auto import identity as identity_store, supervisor as supervisor_module from openpilot.starpilot.system.android_auto.session import PeerRequestedStop sup = supervisor_module.Supervisor.__new__(supervisor_module.Supervisor) - sup.log = lambda *a, **k: None + sup.log = Mock(return_value=None) sup.config = {"usb_mode": "direct"} - sup._stage = lambda *a, **k: None - sup._await_usb_configured = lambda *a, **k: True + sup._stage = Mock(return_value=None) + sup._await_usb_configured = Mock(return_value=True) monkeypatch.setattr(identity_store, "load_identity", lambda: None) class Gadget: @@ -472,7 +475,7 @@ def test_usb_attempt_keeps_a_car_requested_stop(monkeypatch): def project(*args, **kwargs): raise PeerRequestedStop("Head unit ended projection (reason 1)") - sup._project = project + sup._project = Mock(side_effect=project) with pytest.raises(PeerRequestedStop): sup._attempt_usb() @@ -493,3 +496,28 @@ def test_prepare_reuses_a_pre_rename_gadget_that_still_holds_the_accessory(tmp_p assert gadget.root == legacy and not (configfs / "usb_gadget" / usb.GADGET_NAME).exists() assert [path.name for path in (configfs / "usb_gadget").glob("*/functions/accessory.*")] == [usb.ACCESSORY_FUNCTION] assert gadget.recover() is True + + +def test_usb_device_echo_log_preserves_session_log(tmp_path, capsys): + import json + from typing import cast + from openpilot.starpilot.system.android_auto.supervisor import EventLog + from openpilot.tools.android_auto.usb_device import EchoLog + + original = EventLog(tmp_path) + proxy = EchoLog(original) + log = cast(EventLog, proxy) + assert log is proxy + assert log.recent is original.recent + assert log.open == original.open + assert log.close == original.close + log.open() + try: + log("session_test", count=3) + finally: + log.close() + assert json.loads(capsys.readouterr().out) == {"event": "session_test", "count": 3} + assert original.recent[-1]["event"] == "session_test" + assert original.recent[-1]["count"] == 3 + records = [json.loads(line) for path in tmp_path.glob("session-*.jsonl") for line in path.read_text().splitlines()] + assert records == list(original.recent) diff --git a/openpilot/starpilot/system/android_auto/usb_accessory.py b/openpilot/starpilot/system/android_auto/usb_accessory.py index ae8c104921..19d8c583c3 100644 --- a/openpilot/starpilot/system/android_auto/usb_accessory.py +++ b/openpilot/starpilot/system/android_auto/usb_accessory.py @@ -14,7 +14,7 @@ Two ways in, as head units differ: answers and reports as an ``ACCESSORY=START`` uevent; the comma then re-enumerates as Google's accessory (18d1:2d00), as a phone does. * direct: present as Google's accessory from the start, for head units that - never send START to a device they do not recognise as a phone. + never send START to a device they do not recognize as a phone. Either way the car then opens bulk endpoints carrying the same Android Auto protocol as the Wi-Fi link. ``/dev/usb_accessory`` has no poll() and each diff --git a/openpilot/starpilot/ui/tests/test_lane_strength_native.py b/openpilot/starpilot/ui/tests/test_lane_strength_native.py index 64a1f5e7c0..9336f95dd6 100644 --- a/openpilot/starpilot/ui/tests/test_lane_strength_native.py +++ b/openpilot/starpilot/ui/tests/test_lane_strength_native.py @@ -1,8 +1,7 @@ """Native lane-strength presentation preserves canonical owner percent transport.""" from dataclasses import replace -from types import SimpleNamespace import unittest -from unittest.mock import patch +from unittest.mock import Mock, patch from openpilot.starpilot.ui.feature_settings_state import ( FeatureRow, lane_strength_level, row_change, row_default, value_reason, value_text, @@ -53,11 +52,11 @@ class TestNativeLaneStrength(unittest.TestCase): def set_click_callback(self, click): self.click = click sent = [] - session = SimpleNamespace(feature_request=lambda request: sent.append(request) or True) + session = Mock(spec=compact.FeatureSession, feature_request=lambda request: sent.append(request) or True) adapter = compact.FeatureSettingsCompact(session) refreshed = [] with patch.object(compact, 'BigButton', Button): - cards = adapter._editable(self.row(), lambda: refreshed.append(True), None) + cards = adapter._editable(self.row(), lambda: refreshed.append(True), Mock(spec=compact.NavScroller)) self.assertEqual(len(cards), 2) self.assertTrue(all(card.detail.startswith('5 · 1–10; 5 is normal') for card in cards)) cards[1].click() diff --git a/openpilot/tools/android_auto/usb_device.py b/openpilot/tools/android_auto/usb_device.py old mode 100644 new mode 100755 index f371a1df7d..15c6df9fd3 --- a/openpilot/tools/android_auto/usb_device.py +++ b/openpilot/tools/android_auto/usb_device.py @@ -19,9 +19,10 @@ import argparse import json import signal import threading +from typing import cast from openpilot.starpilot.system.android_auto import identity as identity_store -from openpilot.starpilot.system.android_auto.supervisor import Cancelled, Supervisor +from openpilot.starpilot.system.android_auto.supervisor import Cancelled, EventLog, Supervisor class EchoLog: @@ -54,7 +55,7 @@ def main() -> int: supervisor.config[config_key] = getattr(args, key) ident = identity_store.load_identity() print(f"identity ok, expires {ident.expires} ({ident.days_left} days)", flush=True) - supervisor.log = EchoLog(supervisor.log) + supervisor.log = cast(EventLog, EchoLog(supervisor.log)) def stop(*_): supervisor._stop.set() diff --git a/prebuilt.json b/prebuilt.json index 68e719a40e..ccc011bc2e 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "b935256c073116c6ca95b769de0141ca440eb2ea6aae23baefd8c6f7c1cac04d", "schema_version": 1} +{"protected_inventory_sha256": "6898cb0e30c38f6155bc57606d14421592f78684e956514af89a271c302f4a0d", "schema_version": 1}