#!/usr/bin/env python3 """Интеграционные сценарии aircon-слоя (публичный API) против mock-модуля. usage: test_aircon_mock.py Проверяет: маппинг enum↔имя, конверсию в событиях (DisplayTemperature!), оптимистичный кэш, batch=1 notify, RO/не-шаблонные отказы, клэмпинг. """ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "ayla")) from test_session_mock import ( # noqa: E402 FAILURES, Proc, check, free_port, LANIP_KEY, KEY_ID, DSN, run_mock) RUNNER = sys.argv[1] MOCK = sys.argv[2] def run_runner(mock_port, listen, duration, props=None, env_extra=None, timeout=None): env = dict(os.environ) if env_extra: env.update(env_extra) argv = [RUNNER, "127.0.0.1", str(mock_port), str(listen), DSN, LANIP_KEY, str(KEY_ID), str(duration)] if props: argv += props p = Proc(argv, env=env) return p, (timeout or duration + 25) def scenario_mapping_and_batch(): print("== aircon: маппинг/конверсии, batch=1 notify, полный шаблон A") mock_port, listen = free_port(), free_port() mock = run_mock([], mock_port) runner, timeout = run_runner(mock_port, listen, 5, props=["OperationMode", "FanSpeed", "DisplayTemperature"]) check(runner.wait_line("STATE online", 10) is not None, "online") p1 = runner.wait_line("PROP OperationMode", 10) check(p1 is not None and " i:6" in p1, f"operation_mode: {p1}") p2 = runner.wait_line("PROP FanSpeed", 10) check(p2 is not None and " i:4" in p2, f"fan_speed: {p2}") # Конверсия: mock шлёт display_temperature=7000? Нет — mock вернёт # значение из своих props (нет в словаре -> 0) => raw 0 -> api -500. p3 = runner.wait_line("PROP DisplayTemperature", 10) check(p3 is not None and " i:-500" in p3, f"display_temp raw0 -> -500: {p3}") # C-API smoke. check(runner.wait_line("CVER 0.", 5) is not None, "c_api version") cname = runner.wait_line("CNAME", 5) check(cname is not None and "fan_speed" in cname, "c_api prop_name") conv = runner.wait_line("CCONV 200", 5) check(conv is not None, "c_api convert 7000 -> 200") # batch=1 notify: ждём >=2 REG (второй — асинхронный put после выдачи # батча), затем ассертим ровно один notify=True. import time as _time deadline = _time.time() + 5 while _time.time() < deadline: regs = [l for l in mock.all_lines() if l.startswith("REG")] if len(regs) >= 2: break _time.sleep(0.2) notifies = [l for l in regs if "notify=True" in l] check(len(regs) >= 2 and len(notifies) == 1, f"один notify=True на батч: regs={regs}") runner.wait_line("DELETED", timeout) runner.stop() mock.stop() def scenario_set_cache_clamp(): print("== aircon: SET + оптимистичный кэш + клэмпинг + отказы") mock_port, listen = free_port(), free_port() mock = run_mock([], mock_port) runner, timeout = run_runner(mock_port, listen, 5, env_extra={"RUNNER_SET_PROP": "FanSpeed", "RUNNER_SET_VALUE": "3", "RUNNER_SET_AT": "1"}) check(runner.wait_line("STATE online", 10) is not None, "online") check(runner.wait_line("~SET_DONE FanSpeed=3 ok=1", 10) is not None, "SET выполнена") check(runner.wait_line("CACHED FanSpeed 3", 5) is not None, "оптимистичный кэш") check(mock.wait_line("~CMD SET fan_speed=3", 10) is not None, "mock применил SET (имя из таблицы)") # Отказы и клэмпинг печатаются перед DELETED. check(runner.wait_line("RO_SET 0", timeout) is not None, "RO отказ") check(runner.wait_line("NOTMPL_SET 0", 5) is not None, "не-шаблонное свойство отказ") check(runner.wait_line("CLAMP_SET 1", 5) is not None, "клэмпинг применён") check(mock.wait_line("~CMD SET adjust_temperature=450", 15) is not None, "клэмпинг: 1000 -> raw max 450 на модуле") runner.wait_line("DELETED", 10) runner.stop() mock.stop() def main(): scenario_mapping_and_batch() scenario_set_cache_clamp() print() if FAILURES: print("ПРОВАЛЕНО:", len(FAILURES)) for f in FAILURES: print(" -", f) sys.exit(1) print("ВСЕ СЦЕНАРИИ ПРОЙДЕНЫ") if __name__ == "__main__": main()