fix(mcp): 异常面适配真实 bleak——重试/错误码映射覆盖异常全家
- C1: _run_with_retry 捕获面 TransportError → (base+local 两个 TransportError,
BleakError, OSError, asyncio.TimeoutError);真实掉线 bleak 裸抛
BleakError("Not connected") 不再绕过重连重试
- C2/I1a: _map_transport_error 接受异常全家,类型优先(asyncio.TimeoutError、
BleakDeviceNotFoundError → CONNECT_TIMEOUT)再按中英文消息映射;_call /
scan_devices / device_status 的 except 同步扩展,device_status 不再把传输层
错误兜底成 INTERNAL
- I2: _ensure_connected 的 __aenter__ 半途失败补 __aexit__,防半开连接泄漏
- M1: status() 先算 idle_seconds 再 _touch(),读数不再被自身归零
- docstring 校准:scan_devices 返回形状 data.devices;device_status 注明
触发连接守候且刷新空闲计时
- 新测试 8 个(先红后绿):raw BleakError 重连成功/耗尽上抛、aenter 清理、
idle_seconds 语义、local 中文 TransportError 两条映射、raw BleakError 映射、
device_status 传输错误映射
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
16dbf494c9
commit
1f78587bbd
4 files changed
+164
-15
No files matched your search
@@ -2,6 +2,7 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from bleak.exc import BleakError
|
||||
|
||||
from ppclock.client import PPClient
|
||||
from ppclock.device_manager import DeviceManager
|
||||
@@ -15,6 +16,9 @@ class FakeTransport:
|
||||
self.exits = 0
|
||||
self.rxtx_writes = []
|
||||
self.fail_next_write = False
|
||||
# fail_next_write 触发时抛出的异常实例(默认 SDK 包装后的 TransportError;
|
||||
# 真实硬件路径常是 bleak 裸抛的 BleakError,测试可替换)
|
||||
self.fail_exc = TransportError("connection lost")
|
||||
|
||||
async def __aenter__(self):
|
||||
self.enters += 1
|
||||
@@ -30,7 +34,7 @@ class FakeTransport:
|
||||
async def write_rxtx(self, data, response=True):
|
||||
if self.fail_next_write:
|
||||
self.fail_next_write = False
|
||||
raise TransportError("connection lost")
|
||||
raise self.fail_exc
|
||||
self.rxtx_writes.append(data)
|
||||
|
||||
async def read_rxtx(self):
|
||||
@@ -139,6 +143,55 @@ class TestReconnect:
|
||||
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconnect_on_raw_bleak_error(self):
|
||||
"""真实掉线 bleak 裸抛 BleakError(不经 SDK 包装),也要重连重试。"""
|
||||
t1, t2 = FakeTransport(), FakeTransport()
|
||||
t1.fail_next_write = True
|
||||
t1.fail_exc = BleakError("Not connected")
|
||||
m = make_manager([t1, t2])
|
||||
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||
assert t1.enters == 1 and t2.enters == 1 # 掉线重连一次并成功
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retry_exhaustion_raises_bleak_error(self):
|
||||
"""重连后仍抛 BleakError → 原样上抛。"""
|
||||
t1, t2 = FakeTransport(), FakeTransport()
|
||||
for t in (t1, t2):
|
||||
t.fail_next_write = True
|
||||
t.fail_exc = BleakError("Not connected")
|
||||
m = make_manager([t1, t2])
|
||||
with pytest.raises(BleakError):
|
||||
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||
await m.close()
|
||||
|
||||
|
||||
class TestEnterFailure:
|
||||
@pytest.mark.asyncio
|
||||
async def test_aenter_failure_cleans_up(self):
|
||||
"""__aenter__ 抛异常时必须补 __aexit__(否则真实硬件泄漏半开连接)。"""
|
||||
|
||||
class HalfOpenClient:
|
||||
def __init__(self):
|
||||
self.exits = 0
|
||||
|
||||
async def __aenter__(self):
|
||||
raise BleakError("connect boom")
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
self.exits += 1
|
||||
return False
|
||||
|
||||
client = HalfOpenClient()
|
||||
m = DeviceManager(mac="AA:BB:CC:DD:EE:FF",
|
||||
client_factory=lambda mac, timeout: client)
|
||||
with pytest.raises(BleakError):
|
||||
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||
assert client.exits == 1
|
||||
assert not m.connected
|
||||
await m.close()
|
||||
|
||||
|
||||
class TestStatus:
|
||||
@pytest.mark.asyncio
|
||||
@@ -150,5 +203,16 @@ class TestStatus:
|
||||
assert s["device_id"] == "81233F3C267112"
|
||||
assert s["connect_timeout"] == 90.0
|
||||
assert s["idle_timeout"] == 300.0
|
||||
assert isinstance(s["idle_seconds"], float)
|
||||
assert s["idle_seconds"] is None # 无先前活动;status 自身不得把读数归零
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_idle_seconds_reflects_last_activity(self):
|
||||
"""status 自身的 _touch 不得把 idle_seconds 归零(先算后摸)。"""
|
||||
t = FakeTransport()
|
||||
m = make_manager([t])
|
||||
await m.run(lambda dev: dev.set_mode("clock1"))
|
||||
await asyncio.sleep(0.12)
|
||||
s = await m.status()
|
||||
assert s["idle_seconds"] >= 0.1
|
||||
await m.close()
|
||||
Reference in new issue
Block a user