Unify K1 discovery ownership and verification across enrollment paths
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""Exercise the installed Bleak manager against BlueZ's single-session contract.
|
||||
|
||||
Only the D-Bus wire is synthetic. No Bluetooth adapter or device is accessed.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from bleak import BleakScanner
|
||||
from bleak.backends.bluezdbus import defs
|
||||
from bleak.backends.bluezdbus import manager as manager_module
|
||||
from bleak.backends.bluezdbus import scanner as backend_module
|
||||
from bleak.backends.bluezdbus.manager import BlueZManager
|
||||
from bleak.backends.bluezdbus.scanner import BleakScannerBlueZDBus
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.exc import BleakDBusError
|
||||
from dbus_fast import Message, MessageType
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.ble import scanner
|
||||
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,
|
||||
run_ble_operation,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["status-read", "wifi-provision", "ap-enable"])
|
||||
@pytest.mark.parametrize(
|
||||
"mode", ["present", "missing", "busy", "absent", "cancelled", "connect-failed"]
|
||||
)
|
||||
def test_one_discovery_session_retains_exact_path_until_connect(monkeypatch, tmp_path, kind, mode):
|
||||
async def scenario():
|
||||
reset_ble_runtime_arbiter_for_tests()
|
||||
configure_ble_runtime_process_lease(tmp_path)
|
||||
bind_ble_runtime_owner_loop()
|
||||
manager = BlueZManager()
|
||||
adapter = "/org/bluez/hci7"
|
||||
address = "AA:BB:CC:DD:EE:FF"
|
||||
path = adapter + "/dev_AA_BB_CC_DD_EE_FF"
|
||||
device = BLEDevice(address, "synthetic", {"path": path})
|
||||
manager._properties[adapter] = {defs.ADAPTER_INTERFACE: {"Powered": True}}
|
||||
calls = []
|
||||
discovery_active = False
|
||||
|
||||
def advertise():
|
||||
props = {
|
||||
"Address": address,
|
||||
"Alias": "synthetic",
|
||||
"Name": "synthetic",
|
||||
"Adapter": adapter,
|
||||
"RSSI": -45,
|
||||
}
|
||||
manager._properties[path] = {defs.DEVICE_INTERFACE: props}
|
||||
for callback in tuple(manager._advertisement_callbacks[adapter]):
|
||||
callback(path, props)
|
||||
|
||||
class Bus:
|
||||
async def call(self, message):
|
||||
nonlocal discovery_active
|
||||
calls.append(message.member)
|
||||
if message.member == "StartDiscovery":
|
||||
if discovery_active or mode == "busy":
|
||||
return Message(
|
||||
message_type=MessageType.ERROR,
|
||||
reply_serial=1,
|
||||
error_name="org.bluez.Error.InProgress",
|
||||
signature="s",
|
||||
body=["Operation already in progress"],
|
||||
)
|
||||
discovery_active = True
|
||||
if mode != "absent":
|
||||
asyncio.get_running_loop().call_later(0.01, advertise)
|
||||
elif message.member == "StopDiscovery":
|
||||
discovery_active = False
|
||||
manager._properties.pop(path, None)
|
||||
return Message(message_type=MessageType.METHOD_RETURN, reply_serial=1)
|
||||
|
||||
manager._bus = Bus()
|
||||
if mode == "present":
|
||||
advertise()
|
||||
|
||||
async def get_manager():
|
||||
return manager
|
||||
|
||||
monkeypatch.setattr(manager_module, "get_global_bluez_manager", get_manager)
|
||||
monkeypatch.setattr(backend_module, "get_global_bluez_manager", get_manager)
|
||||
monkeypatch.setattr(scanner, "sys", SimpleNamespace(platform="linux"))
|
||||
|
||||
class LinuxScanner(BleakScanner):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, backend=BleakScannerBlueZDBus, **kwargs)
|
||||
|
||||
monkeypatch.setattr(scanner, "BleakScanner", LinuxScanner)
|
||||
|
||||
async def connect():
|
||||
async with scanner.selected_device_discovery(device, timeout_seconds=1):
|
||||
await asyncio.sleep(0)
|
||||
assert manager.get_device_address(path) == address
|
||||
assert discovery_active
|
||||
calls.append("Connect")
|
||||
if mode == "cancelled":
|
||||
raise asyncio.CancelledError()
|
||||
if mode == "connect-failed":
|
||||
raise ConnectionError("synthetic connection failure")
|
||||
|
||||
try:
|
||||
action = run_ble_operation(
|
||||
kind, operation=lambda _progress: connect(), hard_timeout_seconds=2
|
||||
)
|
||||
expected_error = {
|
||||
"busy": BleakDBusError,
|
||||
"absent": TimeoutError,
|
||||
"cancelled": asyncio.CancelledError,
|
||||
"connect-failed": ConnectionError,
|
||||
}.get(mode)
|
||||
if expected_error:
|
||||
with pytest.raises(expected_error) as raised:
|
||||
await action
|
||||
if mode in {"busy", "absent"}:
|
||||
assert "Connect" not in calls
|
||||
assert raised.value.reason_code == (
|
||||
"ble-discovery-busy"
|
||||
if mode == "busy"
|
||||
else "ble-selected-device-unavailable"
|
||||
)
|
||||
else:
|
||||
await action
|
||||
assert calls.count("StartDiscovery") == 1
|
||||
assert calls.count("StopDiscovery") == (mode != "busy")
|
||||
if "Connect" in calls:
|
||||
assert calls.index("Connect") < calls.index("StopDiscovery")
|
||||
assert not discovery_active
|
||||
assert not manager._advertisement_callbacks[adapter]
|
||||
assert not manager._device_removed_callbacks
|
||||
finally:
|
||||
reset_ble_runtime_arbiter_for_tests()
|
||||
|
||||
asyncio.run(scenario())
|
||||
Reference in New Issue
Block a user