mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 22:13:43 +08:00
430 lines
17 KiB
Python
430 lines
17 KiB
Python
from typing import Any
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from openpilot.starpilot.system.starport import car_interface, network
|
|
from openpilot.starpilot.system.starport.bootstrap import WifiCredentials
|
|
from openpilot.starpilot.system.starport.car_interface import DedicatedInterface, DedicatedLease
|
|
from openpilot.starpilot.system.starport.network import DedicatedUnavailable, JoinTimeout, NetworkError, NetworkLease
|
|
|
|
|
|
class Log:
|
|
def __init__(self):
|
|
self.events = []
|
|
|
|
def __call__(self, event, **fields):
|
|
self.events.append((event, fields))
|
|
|
|
def names(self):
|
|
return [event for event, _ in self.events]
|
|
|
|
|
|
class FakeInterface:
|
|
instances: list = []
|
|
|
|
def __init__(self, log, fail=False):
|
|
self.opened = self.closed = False
|
|
self.fail = fail
|
|
FakeInterface.instances.append(self)
|
|
|
|
def open(self):
|
|
if self.fail:
|
|
raise DedicatedUnavailable("no aa0")
|
|
self.opened = True
|
|
return "aa0"
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
|
|
class FakeLease:
|
|
def __init__(self, log, interface, dedicated=False):
|
|
self.interface, self.dedicated = interface, dedicated
|
|
self.local_ip = ""
|
|
self.errors: list = []
|
|
self.releases: list = []
|
|
|
|
def prepare_device(self, cancelled):
|
|
self.prepared = True
|
|
|
|
def acquire(self, credentials, cancelled=lambda: False, timeout=40.0):
|
|
if self.errors:
|
|
raise self.errors.pop(0)
|
|
self.local_ip = "192.168.180.183"
|
|
return self.local_ip
|
|
|
|
def still_connected(self):
|
|
return True
|
|
|
|
def release(self, restore=True):
|
|
self.releases.append(restore)
|
|
|
|
|
|
def credentials():
|
|
return WifiCredentials(ssid="Car-4E91", key="secret-key", bssid="C6:B7:57:F7:4E:91", security=8, ap_type=0)
|
|
|
|
|
|
def make(fail=False, errors=()):
|
|
leases = []
|
|
|
|
def factory(log, interface, dedicated=False):
|
|
lease = FakeLease(log, interface, dedicated)
|
|
if dedicated:
|
|
lease.errors = list(errors)
|
|
leases.append(lease)
|
|
return lease
|
|
FakeInterface.instances = []
|
|
log = Log()
|
|
return DedicatedLease(log, "wlan0", factory, interface_factory=lambda log: FakeInterface(log, fail)), leases, log
|
|
|
|
|
|
def test_dedicated_session_keeps_aa0_between_attempts():
|
|
lease, leases, log = make()
|
|
lease.prepare() # at session start, while Bluetooth connects
|
|
assert lease._opening is not None
|
|
assert lease.acquire(credentials()) == "192.168.180.183"
|
|
assert leases[0].prepared
|
|
assert (leases[0].interface, leases[0].dedicated) == ("aa0", True) and lease.interface == "aa0"
|
|
lease.release(restore=False)
|
|
assert not FakeInterface.instances[0].closed
|
|
lease.acquire(credentials())
|
|
assert len(leases) == 1 and len(FakeInterface.instances) == 1
|
|
lease.release(restore=True)
|
|
assert FakeInterface.instances[0].closed and leases[0].releases == [False, True]
|
|
assert "wifi_dedicated" in log.names()
|
|
|
|
|
|
def test_falls_back_to_wlan0_when_aa0_cannot_be_created():
|
|
lease, leases, log = make(fail=True)
|
|
assert lease.acquire(credentials()) == "192.168.180.183"
|
|
assert [(item.interface, item.dedicated) for item in leases] == [("wlan0", False)]
|
|
assert "wifi_dedicated_unavailable" in log.names()
|
|
lease.release(restore=True)
|
|
assert not lease.legacy # the next session tries aa0 again
|
|
|
|
|
|
def test_overlapping_car_subnet_finishes_join_on_wlan0():
|
|
lease, leases, log = make(errors=[DedicatedUnavailable("overlaps")])
|
|
assert lease.acquire(credentials()) == "192.168.180.183"
|
|
assert [(item.interface, item.dedicated) for item in leases] == [("aa0", True), ("wlan0", False)]
|
|
assert leases[0].releases == [True] and FakeInterface.instances[0].closed
|
|
|
|
|
|
def test_failed_aa0_join_finishes_the_same_join_on_wlan0():
|
|
lease, leases, log = make(errors=[NetworkError("NetworkManager could not join the projection network")])
|
|
assert lease.acquire(credentials()) == "192.168.180.183"
|
|
assert [(item.interface, item.dedicated) for item in leases] == [("aa0", True), ("wlan0", False)]
|
|
assert lease.aa0_failed and not lease.aa0_joined and FakeInterface.instances[0].closed
|
|
|
|
|
|
def test_repeated_aa0_timeouts_fall_back_for_the_session():
|
|
lease, leases, log = make(errors=[JoinTimeout("timed out"), JoinTimeout("timed out")])
|
|
for _ in range(2):
|
|
with pytest.raises(JoinTimeout):
|
|
lease.acquire(credentials())
|
|
lease.release(restore=False)
|
|
assert lease.legacy and FakeInterface.instances[0].closed and not lease.aa0_failed
|
|
lease.acquire(credentials())
|
|
assert leases[-1].interface == "wlan0"
|
|
|
|
|
|
def test_skipped_car_uses_wlan0_without_creating_aa0():
|
|
log = Log()
|
|
leases = []
|
|
def no_aa0(log):
|
|
raise AssertionError("aa0 created")
|
|
lease = DedicatedLease(log, "wlan0", lambda log, interface, dedicated=False: leases.append(FakeLease(log, interface)) or leases[-1],
|
|
interface_factory=no_aa0, enabled=False)
|
|
lease.prepare()
|
|
lease.acquire(credentials())
|
|
lease.release(restore=True)
|
|
assert [item.interface for item in leases] == ["wlan0"] and lease.legacy
|
|
|
|
|
|
def test_cancelled_join_is_not_counted():
|
|
lease, leases, log = make(errors=[NetworkError("cancelled"), NetworkError("cancelled")])
|
|
for _ in range(2):
|
|
with pytest.raises(NetworkError):
|
|
lease.acquire(credentials(), cancelled=lambda: True)
|
|
assert not lease.legacy and lease.timeouts == 0 and not lease.aa0_failed
|
|
|
|
|
|
def test_session_stop_cancels_a_slow_aa0_setup():
|
|
started, finish = threading.Event(), threading.Event()
|
|
|
|
class SlowLease(FakeLease):
|
|
def prepare_device(self, cancelled):
|
|
started.set()
|
|
while not cancelled():
|
|
time.sleep(0.01)
|
|
finish.set()
|
|
raise NetworkError("cancelled")
|
|
|
|
log = Log()
|
|
FakeInterface.instances = []
|
|
lease = DedicatedLease(log, "wlan0", SlowLease, interface_factory=lambda log: FakeInterface(log))
|
|
lease.prepare()
|
|
assert started.wait(2)
|
|
lease.release(restore=True)
|
|
assert finish.is_set() and FakeInterface.instances[0].closed and lease.lease is None
|
|
assert "wifi_dedicated_unavailable" not in log.names()
|
|
|
|
|
|
class FakeNM:
|
|
def __init__(self, devices=None, autoconnect=True, overlap=False, appears_after=0):
|
|
self.devices = devices if devices is not None else {"/dev/wlan0": "wlan0", "/dev/aa0": "aa0"}
|
|
self.autoconnect = autoconnect
|
|
self.overlap = overlap
|
|
self.appears_after = appears_after # GetDevices calls before NetworkManager lists aa0
|
|
self.calls = []
|
|
self.set = []
|
|
|
|
def call(self, path, interface, member, signature=None, body=(), timeout=10.0):
|
|
self.calls.append(member)
|
|
if member == "GetDevices":
|
|
if self.appears_after:
|
|
self.appears_after -= 1
|
|
return [[path for path, name in self.devices.items() if name != "aa0"]]
|
|
return [list(self.devices)]
|
|
if member == "AddAndActivateConnection2":
|
|
return ["/settings/aa", "/active/aa"]
|
|
if member == "GetSettings":
|
|
return [{"connection": {"id": ("s", "home")}}]
|
|
return []
|
|
|
|
def get(self, path, interface, name):
|
|
if name == "Interface":
|
|
return self.devices[path]
|
|
if name == "Ip4Config":
|
|
return {"/active/aa": "/ip4/aa", "/dev/aa0": "/ip4/aa", "/dev/wlan0": "/ip4/home"}.get(path, "/")
|
|
if name == "AddressData":
|
|
if path == "/ip4/home":
|
|
return [{"address": ("s", "192.168.180.40" if self.overlap else "192.168.35.249"), "prefix": ("u", 24)}]
|
|
return [{"address": ("s", "192.168.180.183"), "prefix": ("u", 24)}]
|
|
if name == "Frequency":
|
|
return 5745 if path == "/ap/aa0" else 2437
|
|
return {"WirelessEnabled": True, "DeviceType": 2, "ActiveConnection": "/active/home", "State": 30 if path.startswith("/dev") else 2,
|
|
"Autoconnect": self.autoconnect, "Connection": "/settings/home", "ActiveAccessPoint": "/ap/" + path.rsplit("/", 1)[-1]}[name]
|
|
|
|
def set_property(self, path, interface, name, signature, value):
|
|
self.set.append((path, name, value))
|
|
self.autoconnect = value
|
|
|
|
|
|
def nm_lease(nm, dedicated=True, monkeypatch=None):
|
|
lease: Any = NetworkLease(Log(), "aa0", dedicated=dedicated)
|
|
lease._call, lease._get, lease._set = nm.call, nm.get, nm.set_property
|
|
lease._delete_stale_profiles = lambda: None # test double
|
|
lease.router = type("R", (), {"close": lambda self: None})()
|
|
return lease
|
|
|
|
|
|
def test_dedicated_network_lease_leaves_home_wifi_alone(monkeypatch):
|
|
nm = FakeNM(autoconnect=False)
|
|
lease = nm_lease(nm)
|
|
assert lease.acquire(credentials(), timeout=2.0) == "192.168.180.183"
|
|
assert lease.previous_connection == ""
|
|
lease.release(restore=True)
|
|
assert "ActivateConnection" not in nm.calls
|
|
|
|
|
|
def test_dedicated_network_lease_turns_off_autoconnect_before_joining():
|
|
nm = FakeNM(autoconnect=True)
|
|
lease = nm_lease(nm)
|
|
lease.acquire(credentials(), timeout=2.0)
|
|
assert nm.set == [("/dev/aa0", "Autoconnect", False)]
|
|
assert nm.calls.index("AddAndActivateConnection2") > 0
|
|
joined = dict(lease.log.events)["wifi_joined"]
|
|
assert joined["freq"] == 5745 and joined["wlan0_freq"] == 2437 and joined["wlan0_state"] == 30
|
|
|
|
|
|
def test_dedicated_network_lease_waits_for_networkmanager_to_list_aa0(monkeypatch):
|
|
monkeypatch.setattr(time, "sleep", lambda _: None)
|
|
nm = FakeNM(autoconnect=False, appears_after=3)
|
|
lease = nm_lease(nm)
|
|
lease.prepare_device()
|
|
assert lease.device_path == "/dev/aa0" and lease.device_ready
|
|
assert "wifi_scan_requested" in lease.log.names() # aa0 learns the car's access point before the join
|
|
lease.acquire(credentials(), timeout=2.0)
|
|
assert nm.calls.count("AddAndActivateConnection2") == nm.calls.count("RequestScan") == 1
|
|
assert nm.calls.index("RequestScan") < nm.calls.index("AddAndActivateConnection2")
|
|
|
|
|
|
def test_dedicated_network_lease_never_falls_back_to_another_device(monkeypatch):
|
|
monkeypatch.setattr(network, "DEVICE_READY_TIMEOUT_S", 0.3)
|
|
nm = FakeNM(devices={"/dev/wlan0": "wlan0"}, autoconnect=False)
|
|
lease = nm_lease(nm)
|
|
with pytest.raises(DedicatedUnavailable):
|
|
lease.acquire(credentials(), timeout=2.0)
|
|
assert lease.interface == "aa0" and "wifi_retry_without_bssid" not in lease.log.names()
|
|
|
|
|
|
def test_dedicated_network_lease_rejects_overlapping_subnet_without_a_second_join():
|
|
nm = FakeNM(autoconnect=False, overlap=True)
|
|
lease = nm_lease(nm)
|
|
with pytest.raises(DedicatedUnavailable, match="overlaps"):
|
|
lease.acquire(credentials(), timeout=2.0)
|
|
assert "wifi_subnet_overlap" in lease.log.names()
|
|
assert nm.calls.count("AddAndActivateConnection2") == 1
|
|
|
|
|
|
def test_legacy_lease_still_falls_back():
|
|
lease = nm_lease(FakeNM(devices={"/dev/wlan0": "wlan0"}), dedicated=False)
|
|
lease.acquire(credentials(), timeout=2.0)
|
|
assert lease.interface == "wlan0" and lease.previous_connection == "/settings/home"
|
|
|
|
|
|
def worker(script):
|
|
def popen(args, **kwargs):
|
|
assert args[:2] == ["sudo", "-n"] and args[-1] == "openpilot.starpilot.system.starport.car_interface"
|
|
return subprocess.Popen([sys.executable, "-c", script], **kwargs)
|
|
return popen
|
|
|
|
|
|
def test_interface_lease_ready_heartbeat_and_close(monkeypatch):
|
|
monkeypatch.setattr(car_interface, "HEARTBEAT_S", 0.05)
|
|
script = "\n".join(["import sys", "print('{\"ready\": true, \"interface\": \"aa0\"}', flush=True)", "beats = 0",
|
|
"while sys.stdin.buffer.read(1): beats += 1", "sys.exit(0 if beats else 3)"])
|
|
iface = DedicatedInterface(Log(), popen=worker(script))
|
|
assert iface.open() == "aa0"
|
|
child = iface.child
|
|
time.sleep(0.3)
|
|
iface.close()
|
|
assert child.returncode == 0
|
|
|
|
|
|
@pytest.mark.parametrize("script,match", [("print('{\"ready\": false, \"error\": \"busy\"}', flush=True)", "busy"),
|
|
("import time; time.sleep(5)", "not ready"), ("pass", "Could not create")])
|
|
def test_interface_lease_failures(script, match):
|
|
iface = DedicatedInterface(Log(), popen=worker(script), ready_timeout=0.5)
|
|
with pytest.raises(DedicatedUnavailable, match=match):
|
|
iface.open()
|
|
assert iface.child is None
|
|
|
|
|
|
def test_supervisor_uses_dedicated_lease_only_when_enabled(monkeypatch, tmp_path):
|
|
from openpilot.starpilot.system.starport import identity, supervisor
|
|
monkeypatch.setattr(car_interface, "DedicatedInterface", FakeInterface)
|
|
config = identity.load_config(tmp_path / "missing.json")
|
|
assert config["dedicated_wifi_interface"] is True
|
|
sup = supervisor.Supervisor.__new__(supervisor.Supervisor)
|
|
sup.log, sup._lease_factory, sup.config, sup._dedicated_strikes = Log(), FakeLease, config, {} # ty: ignore[invalid-assignment] # test double
|
|
assert isinstance(sup._lease(), DedicatedLease)
|
|
sup.config = {**config, "dedicated_wifi_interface": False}
|
|
assert isinstance(sup._lease(), FakeLease)
|
|
|
|
|
|
def test_supervisor_keeps_a_car_on_wlan0_after_repeated_aa0_failures(monkeypatch):
|
|
from openpilot.starpilot.system.starport import identity, supervisor
|
|
monkeypatch.setattr(car_interface, "DedicatedInterface", FakeInterface)
|
|
config = {**identity.load_config(None), "dedicated_wifi_interface": True, "receiver_address": "AA:BB:CC:DD:EE:FF"}
|
|
sup = supervisor.Supervisor.__new__(supervisor.Supervisor)
|
|
sup.log, sup._lease_factory, sup.config, sup._dedicated_strikes = Log(), FakeLease, config, {} # ty: ignore[invalid-assignment] # test double
|
|
for _ in range(supervisor.DEDICATED_STRIKES):
|
|
lease = sup._lease()
|
|
assert lease.enabled
|
|
lease.release(restore=True)
|
|
lease.aa0_failed = True
|
|
sup._count_dedicated(lease)
|
|
lease = sup._lease()
|
|
assert not lease.enabled and "wifi_dedicated_skipped" in sup.log.names()
|
|
lease.aa0_joined = True
|
|
sup._count_dedicated(lease)
|
|
assert sup._dedicated_strikes == {}
|
|
|
|
|
|
class FakeWireless:
|
|
def __init__(self, existing=()):
|
|
self.existing = set(existing)
|
|
self.calls = []
|
|
|
|
def __call__(self, method, *args, **kwargs):
|
|
self.calls.append((method, args, kwargs))
|
|
if method == "interfaces":
|
|
return {name: {} for name in self.existing}
|
|
if method == "create":
|
|
self.existing.add(args[0])
|
|
if method == "delete":
|
|
self.existing.discard(args[0])
|
|
return None
|
|
|
|
|
|
def serve(wireless, stdin, stop=None, is_owned=lambda interface, alias: False, timeout=10.0):
|
|
run = []
|
|
thread = threading.Thread(target=car_interface.serve, args=(wireless, stdin, stop or threading.Event()),
|
|
kwargs={"run": lambda args, **kwargs: run.append(args), "is_owned": is_owned, "lease_timeout": timeout})
|
|
thread.start()
|
|
return thread, run
|
|
|
|
|
|
@pytest.mark.parametrize("ending", ["eof", "heartbeat", "stop"])
|
|
def test_worker_deletes_aa0_when_the_lease_ends(ending, capsys):
|
|
read, write = os.pipe()
|
|
wireless, stop = FakeWireless(existing=["wlan0", "p2p0", "galaxy0"]), threading.Event()
|
|
thread, run = serve(wireless, read, stop, timeout=0.3 if ending == "heartbeat" else 10.0)
|
|
time.sleep(0.1)
|
|
if ending == "eof":
|
|
os.close(write)
|
|
elif ending == "stop":
|
|
stop.set()
|
|
thread.join(3)
|
|
assert not thread.is_alive()
|
|
assert '"ready": true' in capsys.readouterr().out
|
|
assert ("create", ("aa0",), {"iftype": 2}) in wireless.calls and wireless.calls[-1] == ("delete", ("aa0",), {})
|
|
assert run == [["ip", "link", "set", "dev", "aa0", "alias", "starpilot-aa-wifi"]]
|
|
touched = [args[0] for method, args, _ in wireless.calls if args] + [word for command in run for word in command]
|
|
assert not {"wlan0", "p2p0", "galaxy0"} & set(touched)
|
|
os.close(read)
|
|
if ending != "eof":
|
|
os.close(write)
|
|
|
|
|
|
def test_worker_leaves_an_aa0_it_does_not_own(capsys):
|
|
read, write = os.pipe()
|
|
wireless = FakeWireless(existing=["wlan0", "aa0"])
|
|
thread, run = serve(wireless, read)
|
|
thread.join(3)
|
|
assert '"ready": false' in capsys.readouterr().out
|
|
assert [method for method, _, _ in wireless.calls] == ["interfaces"]
|
|
os.close(read)
|
|
os.close(write)
|
|
|
|
|
|
def test_worker_replaces_its_own_stale_aa0(capsys):
|
|
read, write = os.pipe()
|
|
wireless = FakeWireless(existing=["wlan0", "aa0"])
|
|
thread, run = serve(wireless, read, is_owned=lambda interface, alias: interface == "aa0" and alias == "starpilot-aa-wifi")
|
|
time.sleep(0.1)
|
|
os.close(write)
|
|
thread.join(3)
|
|
assert [method for method, _, _ in wireless.calls][:3] == ["delete", "interfaces", "create"]
|
|
os.close(read)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def identity(tmp_path_factory):
|
|
from openpilot.starpilot.system.starport.tests.fake_head_unit import make_identity
|
|
return make_identity(tmp_path_factory.mktemp("identity"))
|
|
|
|
|
|
def test_daemon_saves_and_reports_the_dedicated_wifi_choice(identity, tmp_path, monkeypatch):
|
|
import json
|
|
from openpilot.starpilot.system.starport import daemon, identity as identity_store
|
|
from openpilot.starpilot.system.starport.tests.test_starport import make_supervisor
|
|
import openpilot.common.params as params
|
|
store = params.Params(str(tmp_path / "params"))
|
|
store.put_bool("StarPortEnabled", True, block=True)
|
|
monkeypatch.setattr(params, "Params", lambda: store)
|
|
sup, _ = make_supervisor(identity, tmp_path, monkeypatch, lambda *a, **k: None)
|
|
assert daemon.handle(sup, {"command": "status"})["status"]["dedicated_wifi_interface"] is False
|
|
daemon.handle(sup, {"command": "set_dedicated_wifi", "enabled": True})
|
|
assert sup.status()["dedicated_wifi_interface"] is True
|
|
assert json.loads(identity_store.CONFIG_PATH.read_text())["dedicated_wifi_interface"] is True
|
|
daemon.handle(sup, {"command": "set_dedicated_wifi", "enabled": "yes"}) # only a real true turns it on
|
|
assert sup.status()["dedicated_wifi_interface"] is False
|