"""DeviceManager 测试:按需连接/串行/空闲超时/掉线重连。""" import asyncio import pytest from ppclock.client import PPClient from ppclock.device_manager import DeviceManager from ppclock.transports.base import TransportError class FakeTransport: def __init__(self): self.address = "AA:BB:CC:DD:EE:FF" self.enters = 0 self.exits = 0 self.rxtx_writes = [] self.fail_next_write = False async def __aenter__(self): self.enters += 1 return self async def __aexit__(self, *exc): self.exits += 1 return False async def write_epd(self, data, response=True): pass async def write_rxtx(self, data, response=True): if self.fail_next_write: self.fail_next_write = False raise TransportError("connection lost") self.rxtx_writes.append(data) async def read_rxtx(self): return b"\x00" async def request_device_id(self, timeout=8.0): return "81233F3C267112" async def run_ota(self, image, on_progress=None): return {} async def delay(self, seconds): pass def make_manager(transports, **kw): """client_factory 依次弹出预置 transport 包成 PPClient。""" def factory(mac, timeout): return PPClient(transports.pop(0)) kw.setdefault("client_factory", factory) return DeviceManager(**kw) class TestConnect: @pytest.mark.asyncio async def test_connect_on_demand_and_reuse(self): t = FakeTransport() m = make_manager([t]) assert not m.connected await m.run(lambda dev: dev.set_mode("clock1")) await m.run(lambda dev: dev.set_mode("clock2")) assert t.enters == 1 # 第二次复用连接 assert m.connected await m.close() @pytest.mark.asyncio async def test_remembers_address(self): t = FakeTransport() m = make_manager([t], mac=None) await m.run(lambda dev: dev.set_mode("clock1")) assert (await m.status())["mac"] == "AA:BB:CC:DD:EE:FF" await m.close() class TestSerialization: @pytest.mark.asyncio async def test_ops_are_serialized(self): t = FakeTransport() m = make_manager([t]) concurrent = 0 peak = 0 async def op(dev): nonlocal concurrent, peak concurrent += 1 peak = max(peak, concurrent) await asyncio.sleep(0.02) concurrent -= 1 return 1 await asyncio.gather(*(m.run(op) for _ in range(5))) assert peak == 1 await m.close() class TestIdleTimeout: @pytest.mark.asyncio async def test_idle_disconnect(self): t = FakeTransport() m = make_manager([t], idle_timeout=0.05) await m.run(lambda dev: dev.set_mode("clock1")) assert m.connected await asyncio.sleep(0.15) assert not m.connected assert t.exits == 1 @pytest.mark.asyncio async def test_activity_resets_idle(self): t = FakeTransport() m = make_manager([t], idle_timeout=0.1) await m.run(lambda dev: dev.set_mode("clock1")) await asyncio.sleep(0.06) await m.run(lambda dev: dev.set_mode("clock2")) await asyncio.sleep(0.06) assert m.connected # 第二次活动续期 await m.close() class TestReconnect: @pytest.mark.asyncio async def test_reconnect_once_on_transport_error(self): t1, t2 = FakeTransport(), FakeTransport() t1.fail_next_write = True 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(self): t1, t2 = FakeTransport(), FakeTransport() t1.fail_next_write = True t2.fail_next_write = True m = make_manager([t1, t2]) with pytest.raises(TransportError): await m.run(lambda dev: dev.set_mode("clock1")) await m.close() class TestStatus: @pytest.mark.asyncio async def test_status_fields(self): t = FakeTransport() m = make_manager([t], connect_timeout=90.0, idle_timeout=300.0) s = await m.status() assert s["connected"] is True # status 自身触发连接 assert s["device_id"] == "81233F3C267112" assert s["connect_timeout"] == 90.0 assert s["idle_timeout"] == 300.0 assert isinstance(s["idle_seconds"], float) await m.close()