Refresh selected BlueZ path before K1 GATT operations

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 15:50:06 +03:00
parent 7ce09eabcd
commit ad24518b71
8 changed files with 303 additions and 55 deletions
-18
View File
@@ -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)
+170
View File
@@ -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
+1 -17
View File
@@ -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(