Files
fgl-aircon/tests/aircon/test_tables.cpp
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

134 lines
5.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Тесты таблиц шаблонов: состав против PROTOCOL.md §8.2, типы, RO, диапазоны.
#include "doctest/doctest.h"
#include <cstring>
#include <string>
#include "fgl-aircon/templates.hpp"
using fgl::aircon::Prop;
using fgl::aircon::prop_info_count;
using fgl::aircon::prop_info_find;
using fgl::aircon::prop_info_find_by_name;
using fgl::aircon::Template;
using fgl::aircon::template_detect;
namespace {
// Список имён свойств шаблона (для сверки с протоколом).
std::string names_joined(Template t) {
std::string out;
for (auto* p = fgl::aircon::prop_info_begin(t);
p != fgl::aircon::prop_info_begin(t) + prop_info_count(t); p++) {
if (!out.empty()) out += ",";
out += p->name;
}
return out;
}
bool has(Template t, const char* name) {
return prop_info_find_by_name(t, name) != nullptr;
}
} // namespace
TEST_CASE("Шаблон A: состав против PROTOCOL §8.2 (все 33)") {
const char* expected[] = {
"operation_mode", "fan_speed", "adjust_temperature",
"af_vertical_direction", "af_vertical_swing", "af_horizontal_direction",
"af_horizontal_swing", "outdoor_low_noise", "indoor_fan_control",
"human_det_auto_save", "min_heat", "powerful_mode", "coil_dry_mode",
"economy_mode", "master_timer_on_off_1", "master_timer_on_off_2",
"error_code", "demand_control", "filter_sign_reset_display", "op_status",
"device_name", "building_name", "wifi_led_enable",
"service_contact_name", "service_contact_phone", "service_contact_email",
"af_horizontal_num_dir", "af_vertical_num_dir", "device_capabilities",
"display_temperature", "get_prop", "human_det", "refresh"};
for (const char* n : expected) {
CAPTURE(n);
CHECK(has(Template::kA, n));
}
CHECK(prop_info_count(Template::kA) == 33);
}
TEST_CASE("Шаблон B: состав (20) и отличия от A") {
CHECK(prop_info_count(Template::kB) == 20);
CHECK(has(Template::kB, "af_vertical_move_step1"));
CHECK(has(Template::kB, "af_horizontal_move_step1"));
CHECK_FALSE(has(Template::kB, "af_vertical_direction"));
CHECK_FALSE(has(Template::kB, "display_temperature"));
CHECK_FALSE(has(Template::kB, "powerful_mode"));
CHECK_FALSE(has(Template::kB, "min_heat"));
}
TEST_CASE("Шаблон F: = A + monitor1 + filter_sign_reset (без display-варианта)") {
CHECK(prop_info_count(Template::kF) == 34);
CHECK(has(Template::kF, "monitor1"));
CHECK(has(Template::kF, "filter_sign_reset"));
CHECK_FALSE(has(Template::kF, "filter_sign_reset_display"));
CHECK(has(Template::kF, "display_temperature"));
}
TEST_CASE("template_detect: oem_model → шаблон (PROTOCOL §8.1)") {
CHECK(template_detect("AP-WC1E") == Template::kA);
CHECK(template_detect("AP-WA3E") == Template::kA);
CHECK(template_detect("AP-WB2E") == Template::kB);
CHECK(template_detect("AP-WF4E") == Template::kF);
CHECK(template_detect("UNKNOWN-MODEL") == Template::kA); // как legacy
CHECK(template_detect(nullptr) == Template::kA);
}
TEST_CASE("Атрибуты свойств: base_type/read_only/диапазоны") {
auto* om = prop_info_find_by_name(Template::kA, "operation_mode");
REQUIRE(om != nullptr);
CHECK(std::string(om->base_type) == "integer");
CHECK_FALSE(om->read_only);
CHECK(om->raw_min == 0);
CHECK(om->raw_max == 6);
auto* err = prop_info_find_by_name(Template::kA, "error_code");
REQUIRE(err != nullptr);
CHECK(err->read_only);
CHECK(err->raw_max == 4095);
auto* eco = prop_info_find_by_name(Template::kA, "economy_mode");
REQUIRE(eco != nullptr);
CHECK(std::string(eco->base_type) == "boolean");
CHECK(eco->kind == fgl::aircon::ValueKind::kBool);
auto* dn = prop_info_find_by_name(Template::kA, "device_name");
REQUIRE(dn != nullptr);
CHECK(dn->kind == fgl::aircon::ValueKind::kString);
CHECK(std::string(dn->base_type) == "string");
// prop_info_find по enum согласован с find_by_name.
auto* by_enum = prop_info_find(Template::kA, Prop::FanSpeed);
REQUIRE(by_enum != nullptr);
CHECK(std::string(by_enum->name) == "fan_speed");
CHECK(prop_info_find(Template::kB, Prop::AfVerticalDirection) == nullptr);
CHECK(prop_info_find(Template::kA, Prop::kCount) == nullptr);
CHECK(prop_info_find_by_name(Template::kA, "no_such") == nullptr);
CHECK(prop_info_find_by_name(Template::kA, nullptr) == nullptr);
}
TEST_CASE("Таблицы: без дублей имен и enum'ов внутри шаблона") {
for (Template t : {Template::kA, Template::kB, Template::kF}) {
size_t n = prop_info_count(t);
for (size_t i = 0; i < n; i++) {
for (size_t j = i + 1; j < n; j++) {
auto* a = fgl::aircon::prop_info_begin(t) + i;
auto* b = fgl::aircon::prop_info_begin(t) + j;
CHECK_FALSE(std::string(a->name) == std::string(b->name));
CHECK_FALSE(a->prop == b->prop);
}
}
}
}
TEST_CASE("af_*_swing: base_type integer (PROTOCOL §8.3), kind bool в API") {
auto* vs = prop_info_find_by_name(Template::kA, "af_vertical_swing");
REQUIRE(vs != nullptr);
CHECK(std::string(vs->base_type) == "integer");
CHECK(vs->kind == fgl::aircon::ValueKind::kBool);
}