mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
80fbb4f6db
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior. 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: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> 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: haraschax <6804392+haraschax@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
220 lines
11 KiB
Python
220 lines
11 KiB
Python
"""Local Galaxy stays passwordless, including when credential storage is unavailable."""
|
|
|
|
import tempfile
|
|
import threading
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
from openpilot.starpilot.galaxy.access import AccessStatus, GalaxyAccessOwner
|
|
from openpilot.starpilot.galaxy.remote import RemotePairing
|
|
from openpilot.starpilot.galaxy.server import make_server
|
|
import pyray as rl
|
|
|
|
from openpilot.starpilot.ui import galaxy_access
|
|
from openpilot.starpilot.ui.galaxy_access import GalaxyAccessFlow, GalaxyConnectionPage, GalaxyConnectionView, RemoteStatus, connection_url
|
|
from openpilot.system.ui.widgets import Widget
|
|
|
|
|
|
class TestGalaxyConnection(unittest.TestCase):
|
|
def test_native_client_uses_local_authenticated_pairing_routes(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
pairing = RemotePairing(root / "pairing")
|
|
parked = [True]
|
|
server = make_server(port=0, owner=GalaxyAccessOwner(root / "credentials"),
|
|
remote_pairing=pairing, parked=lambda: parked[0])
|
|
worker = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": .01}, daemon=True)
|
|
worker.start()
|
|
try:
|
|
with patch.object(galaxy_access, "LOCAL_PORT", server.server_port), \
|
|
patch.object(galaxy_access, "LOCAL_ORIGIN", f"http://127.0.0.1:{server.server_port}"):
|
|
self.assertFalse(galaxy_access._remote_operation("status").paired)
|
|
paired = galaxy_access._remote_operation("pair", " password123 ")
|
|
self.assertTrue(paired.paired)
|
|
self.assertTrue(galaxy_access.REMOTE_URL.fullmatch(paired.url))
|
|
record = pairing.read()
|
|
assert record is not None
|
|
self.assertEqual(record["slug"], paired.url.rsplit("/", 1)[1])
|
|
self.assertTrue(galaxy_access._remote_operation("status").paired)
|
|
parked[0] = False
|
|
self.assertIn("Turn off the vehicle", galaxy_access._remote_operation("unpair").error)
|
|
self.assertIsNotNone(pairing.read())
|
|
parked[0] = True
|
|
self.assertFalse(galaxy_access._remote_operation("unpair").paired)
|
|
self.assertIsNone(pairing.read())
|
|
finally:
|
|
server.shutdown()
|
|
worker.join(timeout=2)
|
|
server.server_close()
|
|
|
|
def test_only_numeric_lan_ipv4_becomes_a_browser_url(self):
|
|
self.assertEqual(connection_url("192.168.1.111"), "http://192.168.1.111:8082/#/")
|
|
self.assertEqual(connection_url("10.0.0.1"), "http://10.0.0.1:8082/#/")
|
|
for address in ("", "localhost", "127.0.0.1", "0.0.0.0", "169.254.1.1", "224.0.0.1", "::1",
|
|
"192.168.1.111/?password=secret"):
|
|
with self.subTest(address=address):
|
|
self.assertIsNone(connection_url(address))
|
|
|
|
def test_native_wifi_address_updates_without_credentials(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
flow = GalaxyAccessFlow(GalaxyAccessOwner(Path(directory)), lambda: True)
|
|
wifi = SimpleNamespace(ipv4_address="192.168.1.111", stop=Mock())
|
|
with patch("openpilot.starpilot.ui.galaxy_access.WifiManager", return_value=wifi) as provider, \
|
|
patch("openpilot.starpilot.ui.galaxy_access.time.monotonic", side_effect=[1.0, 1.5, 2.1]):
|
|
self.assertEqual(flow._url(), "http://192.168.1.111:8082/#/")
|
|
wifi.ipv4_address = "192.168.1.112"
|
|
self.assertEqual(flow._url(), "http://192.168.1.111:8082/#/")
|
|
self.assertEqual(flow._url(), "http://192.168.1.112:8082/#/")
|
|
provider.assert_called_once_with()
|
|
|
|
def test_opening_connection_does_not_remove_configured_access(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
owner = GalaxyAccessOwner(Path(directory))
|
|
self.assertTrue(owner.configure("secure-password", lambda: True))
|
|
flow = GalaxyAccessFlow(owner, lambda: True)
|
|
with patch("openpilot.starpilot.ui.galaxy_access.GalaxyConnectionView", return_value="connection page"), \
|
|
patch("openpilot.starpilot.ui.galaxy_access.gui_app.push_widget") as pushed:
|
|
flow.open_large()
|
|
pushed.assert_called_once_with("connection page")
|
|
self.assertEqual(owner.status().status, AccessStatus.CONFIGURED_LOCAL)
|
|
|
|
def test_both_pages_show_local_access_even_when_driving_without_reading_credentials(self):
|
|
owner = Mock()
|
|
owner.status.side_effect = RuntimeError("credential storage unavailable")
|
|
parked = Mock(return_value=True)
|
|
flow = GalaxyAccessFlow(owner, parked)
|
|
for method, page in ((flow.open_large, "GalaxyConnectionView"), (flow.open_compact, "GalaxyConnectionPage")):
|
|
with self.subTest(page=page), patch(f"openpilot.starpilot.ui.galaxy_access.{page}", return_value="connection page"), \
|
|
patch("openpilot.starpilot.ui.galaxy_access.gui_app.push_widget") as pushed:
|
|
parked.return_value = True
|
|
method()
|
|
pushed.assert_called_once_with("connection page")
|
|
pushed.reset_mock()
|
|
parked.return_value = False
|
|
method()
|
|
pushed.assert_called_once_with("connection page")
|
|
owner.assert_not_called()
|
|
self.assertEqual(owner.mock_calls, [])
|
|
|
|
def test_compact_button_available_without_credential_storage(self):
|
|
class Button(Widget):
|
|
def __init__(self, text, value, icon):
|
|
super().__init__()
|
|
self.text, self.value = text, value
|
|
|
|
def _render(self, rect):
|
|
pass
|
|
|
|
owner = Mock()
|
|
owner.status.side_effect = RuntimeError("credential storage unavailable")
|
|
parked = Mock(return_value=True)
|
|
flow = GalaxyAccessFlow(owner, parked)
|
|
with patch("openpilot.selfdrive.ui.mici.widgets.button.BigButton", Button), \
|
|
patch("openpilot.starpilot.ui.galaxy_access.gui_app.texture"):
|
|
button = flow.compact_button()
|
|
button._update_state()
|
|
self.assertTrue(button.enabled)
|
|
self.assertEqual(button.value, "pair your comma")
|
|
parked.return_value = False
|
|
self.assertTrue(button.enabled)
|
|
self.assertEqual(owner.mock_calls, [])
|
|
|
|
def test_pairing_requires_parked_and_single_flight(self):
|
|
parked = Mock(return_value=False)
|
|
flow = GalaxyAccessFlow(Mock(), parked)
|
|
self.assertFalse(flow.pair("password123"))
|
|
self.assertFalse(flow.unpair())
|
|
parked.return_value = True
|
|
self.assertFalse(flow.pair("1234567"))
|
|
self.assertIn("at least 8", flow.remote.error)
|
|
with patch.object(flow, "_remote_worker", Mock()) as worker:
|
|
self.assertTrue(flow.pair("password123"))
|
|
self.assertFalse(flow.unpair())
|
|
worker.submit.assert_called_once()
|
|
flow.close()
|
|
|
|
def test_status_error_keeps_last_pairing_metadata(self):
|
|
flow = GalaxyAccessFlow(Mock(), lambda: True)
|
|
url = "https://galaxy.firestar.link/Abcdef1234567890"
|
|
flow.remote = RemoteStatus(True, url, True)
|
|
from concurrent.futures import Future
|
|
pending = Future()
|
|
pending.set_result(RemoteStatus(error="Local Galaxy is unavailable"))
|
|
flow._remote_future = pending
|
|
flow._remote_next_check = float("inf")
|
|
self.assertEqual(flow.update_remote(), RemoteStatus(True, url, True, error="Local Galaxy is unavailable"))
|
|
|
|
def test_large_page_shows_passwordless_address_and_qr_without_credential_storage(self):
|
|
class Button(Widget):
|
|
def __init__(self, text, callback, **kwargs):
|
|
super().__init__()
|
|
self.text = text
|
|
|
|
def _render(self, rect):
|
|
pass
|
|
|
|
owner = Mock()
|
|
owner.status.side_effect = RuntimeError("credential storage unavailable")
|
|
flow = GalaxyAccessFlow(owner, lambda: True)
|
|
url = "http://192.168.1.111:8082/#/"
|
|
with patch("openpilot.system.ui.widgets.button.Button", Button), \
|
|
patch.object(flow, "_url", return_value=url), \
|
|
patch.object(flow, "update_remote", return_value=RemoteStatus()), \
|
|
patch("openpilot.starpilot.ui.galaxy_access.make_texture", return_value=None) as qr, \
|
|
patch("openpilot.starpilot.ui.galaxy_access.gui_app.font"), \
|
|
patch.object(rl, "draw_rectangle_rec"), patch.object(rl, "draw_text_ex") as draw:
|
|
view = GalaxyConnectionView(flow)
|
|
view._update_state()
|
|
view._render(rl.Rectangle(0, 0, 2160, 1080))
|
|
qr.assert_called_once_with(url)
|
|
texts = [call.args[1] for call in draw.call_args_list]
|
|
self.assertIn(url, texts)
|
|
self.assertIn("No password is needed on this network.", texts)
|
|
self.assertLess(texts.index("Choose a password, then pair your comma with Galaxy."), texts.index("Local network"))
|
|
self.assertEqual((view._close.text, view._pair.text, view._unpair.text),
|
|
("Close", "Set password & pair", "Unpair remote Galaxy"))
|
|
self.assertEqual(owner.mock_calls, [])
|
|
|
|
def test_compact_page_has_local_and_remote_views_without_credential_access(self):
|
|
class Scroller:
|
|
def __init__(self):
|
|
self._scroller = Mock()
|
|
|
|
def _update_state(self):
|
|
pass
|
|
|
|
owner = Mock()
|
|
owner.status.side_effect = RuntimeError("credential storage unavailable")
|
|
flow = GalaxyAccessFlow(owner, lambda: True)
|
|
url = "http://192.168.1.111:8082/#/"
|
|
qr_widgets = [Mock(_texture=None, _url=""), Mock(_texture=None, _url="")]
|
|
with patch("openpilot.system.ui.widgets.scroller.NavScroller", Scroller), \
|
|
patch("openpilot.selfdrive.ui.mici.widgets.qr.QR", side_effect=qr_widgets), \
|
|
patch("openpilot.selfdrive.ui.mici.widgets.button.GreyBigButton") as button, \
|
|
patch("openpilot.selfdrive.ui.mici.widgets.button.BigButton"), \
|
|
patch("openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog", return_value="masked prompt") as prompt, \
|
|
patch("openpilot.starpilot.ui.galaxy_access.gui_app.texture"), \
|
|
patch.object(flow, "_url", return_value=url), \
|
|
patch.object(flow, "update_remote", return_value=RemoteStatus()):
|
|
page = GalaxyConnectionPage(flow)
|
|
page._update_state()
|
|
self.assertEqual(qr_widgets[0]._url, url)
|
|
qr_widgets[0]._generate_qr_code.assert_called_once_with()
|
|
qr_widgets[0].set_visible.assert_called_once_with(True)
|
|
qr_widgets[1].set_visible.assert_called_once_with(False)
|
|
self.assertEqual(button.call_args_list[0].args, ("local Galaxy • no password", "connect to Wi-Fi"))
|
|
self.assertEqual([call.args for call in button.call_args_list], [
|
|
("local Galaxy • no password", "connect to Wi-Fi"),
|
|
("remote Galaxy", "checking pairing"),
|
|
("remote connection", "")])
|
|
self.assertEqual(page._scroller.add_widgets.call_args.args[0],
|
|
[page._pair, page._remote_qr, page._remote_address, page._remote_state, page._unpair, page._qr, page._address])
|
|
with patch("openpilot.starpilot.ui.galaxy_access.gui_app.push_widget") as pushed:
|
|
page._enter_password()
|
|
self.assertIs(prompt.call_args.kwargs["password_mode"], True)
|
|
self.assertEqual(prompt.call_args.kwargs["minimum_length"], 8)
|
|
pushed.assert_called_once_with("masked prompt")
|
|
self.assertEqual(owner.mock_calls, [])
|