"""Интеграционный тест pyfglair.Session против mock_ac.py (без железа).""" from __future__ import annotations import asyncio import time from pyfglair import Config, Prop, Session, State async def _wait_state(states: asyncio.Queue, target: State, timeout: float = 10.0): deadline = time.monotonic() + timeout while True: remaining = deadline - time.monotonic() assert remaining > 0, f"не дождались {target!r}" state, error = await asyncio.wait_for(states.get(), remaining) if state == target: return state, error async def _wait_prop(events: asyncio.Queue, prop: Prop, timeout: float = 10.0): deadline = time.monotonic() + timeout while True: remaining = deadline - time.monotonic() assert remaining > 0, f"не дождались свойства {prop!r}" event = await asyncio.wait_for(events.get(), remaining) if event.prop == prop: return event async def test_session_against_mock(mock_ac, ac_env): proc = mock_ac() states: asyncio.Queue = asyncio.Queue() events: asyncio.Queue = asyncio.Queue() cfg = Config( host="127.0.0.1", device_port=proc.port, dsn=ac_env["dsn"], lanip_key=ac_env["key"], lanip_key_id=ac_env["key_id"], keepalive_ms=1000, ) session = Session( cfg, on_state=lambda st, err: states.put_nowait((st, err)), on_property=lambda ev: events.put_nowait(ev), ) assert session.start() try: state, error = await _wait_state(states, State.ONLINE) assert state == State.ONLINE assert error == 0 session.batch_begin() assert session.get_prop(Prop.OPERATION_MODE) assert session.get_prop(Prop.FAN_SPEED) assert session.batch_commit() op = await _wait_prop(events, Prop.OPERATION_MODE) assert op.value.int_value == 6 assert op.cmd_id >= 0 fan = await _wait_prop(events, Prop.FAN_SPEED) assert fan.value.int_value == 4 assert session.set_int(Prop.FAN_SPEED, 2) cached = session.cached(Prop.FAN_SPEED) assert cached is not None and cached.int_value == 2 assert proc.wait_line("CMD SET fan_speed=2", 10) is not None finally: await session.async_stop() assert session.closed assert session.state == State.IDLE assert proc.wait_line("DELETE", 10) is not None async def test_close_idempotent(mock_ac, ac_env): proc = mock_ac() cfg = Config( host="127.0.0.1", device_port=proc.port, dsn=ac_env["dsn"], lanip_key=ac_env["key"], lanip_key_id=ac_env["key_id"], ) session = Session(cfg) assert session.start() await session.async_stop() await session.async_stop() assert session.close() is None assert session.closed assert session.set_int(Prop.FAN_SPEED, 1) is False assert session.cached(Prop.FAN_SPEED) is None async def test_close_offline_module(ac_env): import socket sock = socket.socket() sock.bind(("127.0.0.1", 0)) dead_port = sock.getsockname()[1] sock.close() cfg = Config( host="127.0.0.1", device_port=dead_port, dsn=ac_env["dsn"], lanip_key=ac_env["key"], lanip_key_id=ac_env["key_id"], keepalive_ms=500, ) session = Session(cfg) session.start() await asyncio.sleep(0.5) await session.async_stop() assert session.closed assert session.events_dropped == 0 async def test_close_from_callback(mock_ac, ac_env): """Без loop колбэк идёт в reader-потоке; close() оттуда не дедлочится.""" import threading proc = mock_ac() holder: list[Session] = [] called = threading.Event() def on_state(state, error): if state == State.REGISTERING and holder: holder[0].close() called.set() cfg = Config( host="127.0.0.1", device_port=proc.port, dsn=ac_env["dsn"], lanip_key=ac_env["key"], lanip_key_id=ac_env["key_id"], ) def build(): session = Session(cfg, loop=None, on_state=on_state) holder.append(session) session.start() return session session = await asyncio.to_thread(build) assert await asyncio.to_thread(called.wait, 10), "колбэк не пришёл" deadline = time.monotonic() + 10 while not session.closed and time.monotonic() < deadline: await asyncio.sleep(0.05) assert session.closed assert session.close() is None async def test_log_delivery(mock_ac, ac_env): from pyfglair import set_log_handler, set_log_level logs: list[tuple[int, str]] = [] set_log_level(0) set_log_handler(lambda level, msg: logs.append((level, msg))) try: proc = mock_ac() cfg = Config( host="127.0.0.1", device_port=proc.port, dsn=ac_env["dsn"], lanip_key=ac_env["key"], lanip_key_id=ac_env["key_id"], keepalive_ms=1000, ) session = Session(cfg) session.start() deadline = time.monotonic() + 10 while not logs and time.monotonic() < deadline: await asyncio.sleep(0.1) await session.async_stop() finally: set_log_handler(None) set_log_level(1) assert logs, "логи ядра не доставлены" assert all(isinstance(level, int) and message for level, message in logs) async def test_callbacks_run_in_loop_thread(mock_ac, ac_env): proc = mock_ac() loop = asyncio.get_running_loop() seen: list[object] = [] cfg = Config( host="127.0.0.1", device_port=proc.port, dsn=ac_env["dsn"], lanip_key=ac_env["key"], lanip_key_id=ac_env["key_id"], ) session = Session(cfg, on_state=lambda st, err: seen.append(asyncio.get_running_loop())) session.start() try: deadline = time.monotonic() + 10 while not seen: assert time.monotonic() < deadline, "колбэки не пришли" await asyncio.sleep(0.05) finally: await session.async_stop() assert all(item is loop for item in seen)