Files
fgl-aircon/tests/aircon/test_aircon_mock.py
T
petr.polezhaev fa1718f405 core(M3): aircon-слой — шаблоны A/B/F, конверсии+override, публичный API + C-API
- include/fgl-aircon: types.hpp (Prop/State/Error/Value/Template/
  Conversion-linear-custom/PropOverride/Config с задокументированными
  lifetime-контрактами), templates.hpp (PropInfo-таблицы, интроспекция,
  конверсии c контрактом Linear num/den!=0), session.hpp (Session:
  set_int/bool/string, get_prop, batch, cached, set_log_sink с контрактом),
  c_api.h (extern "C" для cffi/pyfglair).
- src/aircon: tables.cpp (шаблоны A=33/B=20/F=34 по PROTOCOL §8.2,
  template_detect + template_is_known; конверсии: default тождественно,
  DisplayTemperature raw 0.01°C(+5000)→API 0.1°C; linear с инверсией
  raw=(api-offset)*den/num и насыщением __builtin_*_overflow; custom fn;
  af_*_swing base_type integer, kind bool), session.cpp (маппинг имя↔Prop,
  коэрсинг int-push к kBool по таблице, кэш со спинлоком, оптимистичный кэш,
  валидация overrides: терминатор/linear/custom_fn), c_api.cpp (шейм,
  static_assert'ы на все enum-значения, fgl_template_detect -1 для
  неизвестных).
- ayla: set_property_string (Command.str_value, coalescing копирует строку
  в т.ч. в batch-ветке); kMaxQueue 48 + static_assert (полный батч A=33);
  commit_batch всегда ставит notify; oversized-строка дропает событие
  (кэш не затирается); max_queue default 40.
- tests/aircon: test_tables (составы/атрибуты/дубли/template_detect/
  swings-integer), test_convert (default/linear/custom/диапазоны/насыщение
  в обе стороны/UB-экстремумы), aircon_runner + test_aircon_mock
  (маппинг+конверсии+batch=1-notify с ожиданием async REG; SET+кэш+
  RO/не-шаблон отказы+клэмпинг raw 450 на модуле; C-API smoke).
- Прибор AP-WC1E: полный батч шаблона A одним notify — 28 int/bool свойств
  (DisplayTemperature 7000→200=20.0°C, DeviceCapabilities=5119), boolean SET
  (JSON true) принят, RO/клэмп/NOTMPL отказы корректны.
- CI: 10/10 ×3 стабильно (3 полных прогона); ESP-IDF esp32 build complete.
Ревью независимым агентом: 3 круга — B1 kMaxQueue<батча, B2 linear div/0,
B3 c_api контракт -1, B4 batch-строки, BL1 INT64_MIN negation UB,
BL2 DisplayTemperature экстремумы, flake aircon_mock — всё закрыто;
APPROVED (условие круга 3: фикс + зелёный CI ×3).
2026-09-28 20:16:54 +03:00

112 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Интеграционные сценарии aircon-слоя (публичный API) против mock-модуля.
usage: test_aircon_mock.py <aircon_runner> <mock_ac.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()