Refresh selected BlueZ path before K1 GATT operations
This commit is contained in:
@@ -1702,21 +1702,3 @@ def test_exact_session_invalidation_does_not_clear_new_scan_handle() -> None:
|
||||
) is not None
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform,present", [("linux", True), ("linux", False), ("darwin", False)])
|
||||
def test_status_read_native_cache_check_is_linux_only(monkeypatch, platform, present):
|
||||
from types import SimpleNamespace
|
||||
|
||||
calls = []
|
||||
device = BLEDevice("AA:BB:CC:DD:EE:FF", "synthetic", {"path": "/synthetic/bluez"})
|
||||
|
||||
async def retrieve(address, details):
|
||||
calls.append((address, details))
|
||||
return device if present else None
|
||||
|
||||
monkeypatch.setattr(scanner_module, "sys", SimpleNamespace(platform=platform))
|
||||
monkeypatch.setattr(scanner_module, "_retrieve_bluez_device", retrieve)
|
||||
assert asyncio.run(scanner_module.status_read_device_is_current(device)) is (
|
||||
present or platform == "darwin")
|
||||
assert len(calls) == (1 if platform == "linux" else 0)
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Native cache loss between owner selection and one admitted GATT operation."""
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.exc import BleakDeviceNotFoundError, BleakError
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.ble import scanner
|
||||
from k1link.device_plugins.xgrids_k1.ble import wifi_provisioning as wifi
|
||||
from k1link.device_plugins.xgrids_k1.ble.runtime_arbiter import (
|
||||
bind_ble_runtime_owner_loop,
|
||||
configure_ble_runtime_process_lease,
|
||||
reset_ble_runtime_arbiter_for_tests,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_owner(tmp_path):
|
||||
scanner.reset_runtime_handles_for_tests()
|
||||
reset_ble_runtime_arbiter_for_tests()
|
||||
configure_ble_runtime_process_lease(tmp_path)
|
||||
yield
|
||||
scanner.reset_runtime_handles_for_tests()
|
||||
reset_ble_runtime_arbiter_for_tests()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["read", "provision"])
|
||||
@pytest.mark.parametrize("native", [
|
||||
"macos", "present", "vanished", "absent", "wrong-address", "wrong-adapter",
|
||||
"vanished-again", "owner-changed", "invalidated", "connect-failed", "write-failed",
|
||||
])
|
||||
def test_selected_transport_survives_native_cache_loss(monkeypatch, operation, native):
|
||||
address = "AA:BB:CC:DD:EE:FF"
|
||||
path = "/org/bluez/hci7/dev_AA_BB_CC_DD_EE_FF"
|
||||
device = BLEDevice(address, "synthetic", {"path": path})
|
||||
events = []
|
||||
capture = None
|
||||
validations = []
|
||||
|
||||
async def retrieve(selected_address, details):
|
||||
assert selected_address == address and details["path"] == path
|
||||
events.append("cache")
|
||||
present = native == "present" or (
|
||||
events.count("cache") == 2 and native != "vanished-again"
|
||||
)
|
||||
return device if present else None
|
||||
|
||||
async def find(selected_address, *, timeout, bluez):
|
||||
assert selected_address == address
|
||||
assert 0 < timeout <= 8
|
||||
assert bluez == {"adapter": "hci7"}
|
||||
events.append("scan")
|
||||
assert events.count("scan") == 1
|
||||
if native == "absent":
|
||||
return None
|
||||
if native == "owner-changed":
|
||||
monkeypatch.setattr(scanner, "ble_runtime_owner_epoch_for_current_loop", lambda: -1)
|
||||
if native == "invalidated":
|
||||
scanner.demote_connected_device_handle_after_gatt_failure(capture)
|
||||
return BLEDevice(
|
||||
"AA:BB:CC:DD:EE:00" if native == "wrong-address" else address,
|
||||
"synthetic",
|
||||
{"path": path.replace("hci7", "hci8") if native == "wrong-adapter" else path},
|
||||
)
|
||||
|
||||
service = SimpleNamespace(uuid=wifi.SERVICE_UUID)
|
||||
write = SimpleNamespace(
|
||||
uuid=wifi.WRITE_CHARACTERISTIC_UUID, service_uuid=wifi.SERVICE_UUID,
|
||||
properties=["write"],
|
||||
)
|
||||
status = SimpleNamespace(
|
||||
uuid=wifi.STATUS_CHARACTERISTIC_UUID, service_uuid=wifi.SERVICE_UUID,
|
||||
properties=["read"],
|
||||
)
|
||||
services = SimpleNamespace(
|
||||
get_service=lambda uuid: service if uuid == service.uuid else None,
|
||||
get_characteristic=lambda uuid: {write.uuid: write, status.uuid: status}.get(uuid),
|
||||
)
|
||||
|
||||
class Client:
|
||||
name = "synthetic"
|
||||
is_connected = True
|
||||
|
||||
def __init__(self, selected, **_kwargs):
|
||||
# Restore the native path without replacing the owner capture.
|
||||
assert selected is device
|
||||
self.services = services
|
||||
|
||||
async def __aenter__(self):
|
||||
events.append("connect")
|
||||
if native == "connect-failed":
|
||||
raise BleakError("synthetic native connection failure")
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
events.append("disconnect")
|
||||
|
||||
async def read_gatt_char(self, characteristic):
|
||||
assert characteristic is status
|
||||
events.append("read")
|
||||
value = bytearray(52)
|
||||
value[0] = 11
|
||||
value[1:12] = b"WIFI_CLIENT"
|
||||
value[33] = 4
|
||||
value[34:38] = bytes((10, 255, 254, 77))
|
||||
return value
|
||||
|
||||
async def write_gatt_char(self, characteristic, _frame, *, response):
|
||||
assert characteristic is write and response is True
|
||||
assert events[-2:] == ["read", "dispatch"]
|
||||
events.append("write")
|
||||
if native == "write-failed":
|
||||
raise BleakError("synthetic unconfirmed write")
|
||||
|
||||
monkeypatch.setattr(scanner, "sys", SimpleNamespace(
|
||||
platform="darwin" if native == "macos" else "linux",
|
||||
))
|
||||
monkeypatch.setattr(scanner, "_retrieve_bluez_device", retrieve)
|
||||
monkeypatch.setattr(scanner.BleakScanner, "find_device_by_address", find)
|
||||
monkeypatch.setattr(wifi, "BleakClient", Client)
|
||||
|
||||
async def scenario():
|
||||
nonlocal capture
|
||||
owner_epoch = bind_ble_runtime_owner_loop()
|
||||
capture = scanner.CapturedDiscoveredDevice(
|
||||
device=device, macos_uuid=address, owner_epoch=owner_epoch,
|
||||
)
|
||||
scanner.pin_connected_device_handle(capture, device_session_id="synthetic-session")
|
||||
generation = scanner._runtime_handle_generation # noqa: SLF001
|
||||
kwargs = dict(captured_device=capture, timeout_seconds=1)
|
||||
action = (
|
||||
wifi.read_wifi_status_once(address, on_gatt_validated=validations.append, **kwargs)
|
||||
if operation == "read" else
|
||||
wifi.provision_wifi_once(
|
||||
address, "SyntheticNet", "x" * 13, write_mode="with_response",
|
||||
on_write_dispatch=lambda *_args: events.append("dispatch"), **kwargs,
|
||||
)
|
||||
)
|
||||
fails_before_connect = native in {
|
||||
"absent", "wrong-address", "wrong-adapter", "vanished-again",
|
||||
"owner-changed", "invalidated",
|
||||
}
|
||||
if fails_before_connect or native == "connect-failed" or (
|
||||
native == "write-failed" and operation == "provision"
|
||||
):
|
||||
with pytest.raises(BleakError) as raised:
|
||||
await action
|
||||
if fails_before_connect:
|
||||
assert isinstance(raised.value, BleakDeviceNotFoundError)
|
||||
assert "connect" not in events
|
||||
if operation == "provision":
|
||||
assert raised.value.device_write_attempted is (native == "write-failed")
|
||||
assert events.count("write") == (native == "write-failed")
|
||||
else:
|
||||
result = await action
|
||||
assert result.get("outcome", "lan_address_observed") == "lan_address_observed"
|
||||
assert scanner.connected_device_capture(
|
||||
address, device_session_id="synthetic-session",
|
||||
).device is device
|
||||
if operation == "read":
|
||||
assert validations == [capture]
|
||||
assert events.count("write") == (operation == "provision")
|
||||
assert scanner._runtime_handle_generation == generation # noqa: SLF001
|
||||
|
||||
asyncio.run(scenario())
|
||||
assert events.count("scan") == (native not in {"macos", "present"})
|
||||
assert events.count("connect") <= 1
|
||||
assert events.count("write") <= 1
|
||||
@@ -134,10 +134,8 @@ def test_parse_wifi_status_rejects_short_frame() -> None:
|
||||
parse_wifi_status(bytes(50))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("current_handle", [True, False])
|
||||
def test_read_wifi_status_once_reads_only_and_returns_current_dhcp_address(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
current_handle: bool,
|
||||
) -> None:
|
||||
value = bytearray(54)
|
||||
value[0] = 11
|
||||
@@ -202,19 +200,6 @@ def test_read_wifi_status_once_reads_only_and_returns_current_dhcp_address(
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(wifi_module, "BleakClient", FakeClient)
|
||||
fresh_handle, capture, scans = object(), object(), []
|
||||
|
||||
async def is_current(_device):
|
||||
return current_handle
|
||||
|
||||
async def refresh_exact(address, **_kwargs):
|
||||
scans.append(address)
|
||||
return capture
|
||||
|
||||
monkeypatch.setattr(wifi_module, "status_read_device_is_current", is_current)
|
||||
monkeypatch.setattr(wifi_module, "discover_known_device_capture_for_status_read", refresh_exact)
|
||||
monkeypatch.setattr(wifi_module, "captured_device_handle", lambda value: fresh_handle)
|
||||
monkeypatch.setattr(wifi_module, "mark_captured_device_gatt_validated", lambda *_a, **_k: True)
|
||||
|
||||
result = asyncio.run(read_wifi_status_once("synthetic-corebluetooth-uuid"))
|
||||
|
||||
@@ -225,8 +210,7 @@ def test_read_wifi_status_once_reads_only_and_returns_current_dhcp_address(
|
||||
assert result["max_write_without_response_size"] == 244
|
||||
assert result["mtu_size"] == 256
|
||||
assert result["status"]["ipv4"] == "10.255.254.77"
|
||||
assert connected == [retained_handle if current_handle else fresh_handle]
|
||||
assert scans == ([] if current_handle else ["synthetic-corebluetooth-uuid"])
|
||||
assert connected == [retained_handle]
|
||||
|
||||
|
||||
def test_read_wifi_status_recovery_keeps_fresh_retained_handle(
|
||||
|
||||
Reference in New Issue
Block a user