// Тесты конверсий: шаблонные по умолчанию, linear/custom override, диапазоны. #include "doctest/doctest.h" #include #include "fgl-aircon/templates.hpp" using fgl::aircon::ConvKind; using fgl::aircon::Conversion; using fgl::aircon::Linear; using fgl::aircon::Prop; using fgl::aircon::PropOverride; using fgl::aircon::convert_from_input; using fgl::aircon::convert_to_display; using fgl::aircon::prop_api_range; using fgl::aircon::Template; namespace { const PropOverride kTerm{Prop::kCount}; } TEST_CASE("Конверсия по умолчанию: тождество и DisplayTemperature") { // Большинство свойств тождественны (API == raw). CHECK(convert_to_display(Template::kA, Prop::OperationMode, 6, &kTerm) == 6); CHECK(convert_to_display(Template::kA, Prop::AdjustTemperature, 200, &kTerm) == 200); CHECK(convert_from_input(Template::kA, Prop::AdjustTemperature, 250, &kTerm) == 250); // DisplayTemperature: raw 0.01°C(+5000) -> API 0.1°C. // Прибор: 7000 → 20.0°C → 200 (проверено на приборе). CHECK(convert_to_display(Template::kA, Prop::DisplayTemperature, 7000, &kTerm) == 200); CHECK(convert_to_display(Template::kA, Prop::DisplayTemperature, 7300, &kTerm) == 230); CHECK(convert_to_display(Template::kA, Prop::DisplayTemperature, 4975, &kTerm) == -2); // Округление к нулю. CHECK(convert_to_display(Template::kA, Prop::DisplayTemperature, 7004, &kTerm) == 200); CHECK(convert_to_display(Template::kA, Prop::DisplayTemperature, 7005, &kTerm) == 200); // Обратная (симметричная, для тестов; свойство RO). CHECK(convert_from_input(Template::kA, Prop::DisplayTemperature, 230, &kTerm) == 7300); } TEST_CASE("Linear override: to_display и from_input") { // ТЕ ЖЕ коэффициенты в оба направления (from_input = инверсия): // api = raw*1/2 (250 -> 125); raw = api*2 (125 -> 250). PropOverride ov{}; ov.prop = Prop::AdjustTemperature; ov.to_display.kind = ConvKind::kLinear; ov.to_display.linear = Linear{1, 2, 0}; ov.from_input.kind = ConvKind::kLinear; ov.from_input.linear = Linear{1, 2, 0}; const PropOverride list[] = {ov, kTerm}; CHECK(convert_to_display(Template::kA, Prop::AdjustTemperature, 250, list) == 125); CHECK(convert_from_input(Template::kA, Prop::AdjustTemperature, 125, list) == 250); // Смещение: Фаренгейт из сырых 0.1°C: F = raw*9/5 + 320 (те же коэфф.). PropOverride ovF{}; ovF.prop = Prop::AdjustTemperature; ovF.to_display.kind = ConvKind::kLinear; ovF.to_display.linear = Linear{9, 5, 320}; ovF.from_input.kind = ConvKind::kLinear; ovF.from_input.linear = Linear{9, 5, 320}; const PropOverride listF[] = {ovF, kTerm}; // 25.0°C = 77°F: 250*9/5+320 = 770 (0.1°F). CHECK(convert_to_display(Template::kA, Prop::AdjustTemperature, 250, listF) == 770); CHECK(convert_from_input(Template::kA, Prop::AdjustTemperature, 770, listF) == 250); // Отрицательные: -100 (−10°C) → 140 (14.0°F). CHECK(convert_to_display(Template::kA, Prop::AdjustTemperature, -100, listF) == 140); // Override не задет другие свойства. CHECK(convert_to_display(Template::kA, Prop::FanSpeed, 3, listF) == 3); } namespace { int64_t double_it(int64_t raw, void*) { return raw * 2; } int64_t half_it(int64_t v, void*) { return v / 2; } } TEST_CASE("Custom fn override") { PropOverride ov{}; ov.prop = Prop::FanSpeed; ov.to_display.kind = ConvKind::kCustomFn; ov.to_display.fn = double_it; ov.from_input.kind = ConvKind::kCustomFn; ov.from_input.fn = half_it; const PropOverride list[] = {ov, kTerm}; CHECK(convert_to_display(Template::kA, Prop::FanSpeed, 2, list) == 4); CHECK(convert_from_input(Template::kA, Prop::FanSpeed, 4, list) == 2); } TEST_CASE("Диапазоны API-значений: шаблонные и override") { int64_t lo = 0, hi = 0; // adjust_temperature: raw -100..450 → тождественно. REQUIRE(prop_api_range(Template::kA, Prop::AdjustTemperature, &kTerm, &lo, &hi)); CHECK(lo == -100); CHECK(hi == 450); // display_temperature: raw 4000..9500 (таблица приложения −10..45°C) // → api -100..450. REQUIRE(prop_api_range(Template::kA, Prop::DisplayTemperature, &kTerm, &lo, &hi)); CHECK(lo == -100); CHECK(hi == 450); // error_code 0..4095. REQUIRE(prop_api_range(Template::kA, Prop::ErrorCode, &kTerm, &lo, &hi)); CHECK(lo == 0); CHECK(hi == 4095); // Без диапазона (0..0 в таблице). CHECK_FALSE(prop_api_range(Template::kA, Prop::DeviceName, &kTerm, &lo, &hi)); CHECK_FALSE(prop_api_range(Template::kA, Prop::OpStatus, &kTerm, &lo, &hi)); // Override-диапазон перекрывает шаблонный. PropOverride ov{}; ov.prop = Prop::AdjustTemperature; ov.has_range = true; ov.min = 160; ov.max = 300; // 16..30°C — фактические границы прибора const PropOverride list[] = {ov, kTerm}; REQUIRE(prop_api_range(Template::kA, Prop::AdjustTemperature, list, &lo, &hi)); CHECK(lo == 160); CHECK(hi == 300); // Свойство не из шаблона. CHECK_FALSE(prop_api_range(Template::kB, Prop::AfVerticalDirection, &kTerm, &lo, &hi)); } TEST_CASE("Насыщение linear при переполнении int64") { // to_display: api = raw*num/den; raw=4 * num=INT64_MAX/2 -> переполнение. PropOverride ov{}; ov.prop = Prop::AdjustTemperature; ov.to_display.kind = ConvKind::kLinear; ov.to_display.linear = Linear{INT64_MAX / 2, 1, 0}; // from_input: raw = (api-offset)*den/num; api=4 * den=INT64_MAX/2. ov.from_input.kind = ConvKind::kLinear; ov.from_input.linear = Linear{1, INT64_MAX / 2, 0}; const PropOverride list[] = {ov, kTerm}; CHECK(convert_to_display(Template::kA, Prop::AdjustTemperature, 4, list) == INT64_MAX); CHECK(convert_from_input(Template::kA, Prop::AdjustTemperature, 4, list) == INT64_MAX); // Смещение с переполнением сложения. PropOverride ov2{}; ov2.prop = Prop::FanSpeed; ov2.to_display.kind = ConvKind::kLinear; ov2.to_display.linear = Linear{1, 1, INT64_MAX}; const PropOverride list2[] = {ov2, kTerm}; CHECK(convert_to_display(Template::kA, Prop::FanSpeed, 2, list2) == INT64_MAX); } TEST_CASE("template_is_known") { using fgl::aircon::template_is_known; CHECK(template_is_known("AP-WC1E")); CHECK_FALSE(template_is_known("UNKNOWN")); CHECK_FALSE(template_is_known(nullptr)); } TEST_CASE("Насыщение linear: негативные экстремумы (UBSan)") { // raw=-4 * num=INT64_MAX/2 -> INT64_MIN-переполнение -> насыщение MIN. PropOverride ov{}; ov.prop = Prop::AdjustTemperature; ov.to_display.kind = ConvKind::kLinear; ov.to_display.linear = Linear{INT64_MAX / 2, 1, 0}; ov.from_input.kind = ConvKind::kLinear; ov.from_input.linear = Linear{1, INT64_MAX / 2, 0}; const PropOverride list[] = {ov, kTerm}; CHECK(convert_to_display(Template::kA, Prop::AdjustTemperature, -4, list) == INT64_MIN); // (-4)*(MAX/2) -> переполнение в минус CHECK(convert_from_input(Template::kA, Prop::AdjustTemperature, -4, list) == INT64_MIN); // add-переполнение в минус. PropOverride ov2{}; ov2.prop = Prop::FanSpeed; ov2.to_display.kind = ConvKind::kLinear; ov2.to_display.linear = Linear{1, 1, INT64_MIN}; const PropOverride list2[] = {ov2, kTerm}; // q=2 + offset=INT64_MIN = INT64_MIN+2 — корректно (в диапазоне int64), // насыщение не требуется; убеждаемся что нет падения/UB. int64_t v = convert_to_display(Template::kA, Prop::FanSpeed, 2, list2); CHECK(v == INT64_MIN + 2); } TEST_CASE("DisplayTemperature: экстремумы без UB") { CHECK(convert_to_display(Template::kA, Prop::DisplayTemperature, INT64_MIN + 1, &kTerm) != 0); // Значение определяется насыщением, точный результат не важен — нет UB. int64_t r = convert_from_input(Template::kA, Prop::DisplayTemperature, INT64_MIN / 10, &kTerm); CHECK(r != 0); // насыщение или корректное значение — но не падение }