diff --git a/components/esp_matter/test/CMakeLists.txt b/components/esp_matter/test/CMakeLists.txt index 5c6377549..671198715 100644 --- a/components/esp_matter/test/CMakeLists.txt +++ b/components/esp_matter/test/CMakeLists.txt @@ -1,5 +1,6 @@ list(APPEND srcs_list "attribute_get_val.cpp") list(APPEND srcs_list "attribute_get_val_type.cpp") +list(APPEND srcs_list "attribute_report.cpp") idf_component_register(SRCS ${srcs_list} INCLUDE_DIRS "." diff --git a/components/esp_matter/test/attribute_report.cpp b/components/esp_matter/test/attribute_report.cpp new file mode 100644 index 000000000..5e5837058 --- /dev/null +++ b/components/esp_matter/test/attribute_report.cpp @@ -0,0 +1,295 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include + +using namespace esp_matter; +using namespace chip::app::Clusters; + +static node_t *test_node = nullptr; +static endpoint_t *test_endpoint = nullptr; +static uint16_t test_endpoint_id = 0; + +struct callback_record_t { + bool called; + attribute::callback_type_t type; + uint16_t endpoint_id; + uint32_t cluster_id; + uint32_t attribute_id; + esp_matter_attr_val_t val; +}; + +static callback_record_t cb_pre_update; +static callback_record_t cb_post_update; + +static void reset_callback_records() +{ + memset(&cb_pre_update, 0, sizeof(cb_pre_update)); + memset(&cb_post_update, 0, sizeof(cb_post_update)); +} + +static esp_err_t test_attribute_callback(attribute::callback_type_t type, uint16_t endpoint_id, + uint32_t cluster_id, uint32_t attribute_id, + esp_matter_attr_val_t *val, void *priv_data) +{ + callback_record_t *record = nullptr; + if (type == attribute::PRE_UPDATE) { + record = &cb_pre_update; + } else if (type == attribute::POST_UPDATE) { + record = &cb_post_update; + } else { + return ESP_OK; + } + record->called = true; + record->type = type; + record->endpoint_id = endpoint_id; + record->cluster_id = cluster_id; + record->attribute_id = attribute_id; + record->val = *val; + return ESP_OK; +} + +void setup_for_update_report() +{ + static bool setup_done = false; + if (setup_done) { + return; + } + + esp_err_t err = nvs_flash_init(); + TEST_ASSERT_EQUAL(ESP_OK, err); + + node::config_t node_config; + test_node = node::create(&node_config, test_attribute_callback, nullptr); + TEST_ASSERT_NOT_NULL(test_node); + + endpoint::extended_color_light::config_t light_config; + light_config.on_off.on_off = false; + light_config.level_control.current_level = nullable(100); + test_endpoint = endpoint::extended_color_light::create(test_node, &light_config, ENDPOINT_FLAG_NONE, nullptr); + TEST_ASSERT_NOT_NULL(test_endpoint); + + test_endpoint_id = endpoint::get_id(test_endpoint); + + err = esp_matter::start(nullptr); + TEST_ASSERT_EQUAL(ESP_OK, err); + + setup_done = true; +} + +void teardown_for_update_report() +{ + reset_callback_records(); +} + +// ============================================================ +// attribute::report() tests +// ============================================================ + +TEST_CASE("report returns ESP_ERR_INVALID_ARG for null val", "[report][invalid]") +{ + setup_for_update_report(); + + esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, nullptr); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + teardown_for_update_report(); +} + +TEST_CASE("report returns ESP_ERR_INVALID_ARG for invalid endpoint", "[report][invalid]") +{ + setup_for_update_report(); + + esp_matter_attr_val_t val = esp_matter_bool(true); + esp_err_t err = attribute::report(chip::kInvalidEndpointId, OnOff::Id, OnOff::Attributes::OnOff::Id, &val); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + teardown_for_update_report(); +} + +TEST_CASE("report returns ESP_ERR_INVALID_ARG for invalid cluster", "[report][invalid]") +{ + setup_for_update_report(); + + esp_matter_attr_val_t val = esp_matter_bool(true); + esp_err_t err = attribute::report(test_endpoint_id, chip::kInvalidClusterId, OnOff::Attributes::OnOff::Id, &val); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + teardown_for_update_report(); +} + +TEST_CASE("report returns ESP_ERR_INVALID_ARG for invalid attribute", "[report][invalid]") +{ + setup_for_update_report(); + + esp_matter_attr_val_t val = esp_matter_bool(true); + esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, chip::kInvalidAttributeId, &val); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + teardown_for_update_report(); +} + +TEST_CASE("report bool updates stored value", "[report][bool]") +{ + setup_for_update_report(); + + esp_matter_attr_val_t val = esp_matter_bool(true); + esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + esp_matter_attr_val_t retrieved; + err = attribute::get_val(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &retrieved); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, retrieved.type); + TEST_ASSERT_EQUAL(true, retrieved.val.b); + + val = esp_matter_bool(false); + err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + err = attribute::get_val(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &retrieved); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(false, retrieved.val.b); + + teardown_for_update_report(); +} + +TEST_CASE("report nullable uint8 updates stored value", "[report][nullable][uint8]") +{ + setup_for_update_report(); + + esp_matter_attr_val_t val = esp_matter_nullable_uint8(150); + esp_err_t err = attribute::report(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + esp_matter_attr_val_t retrieved; + err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &retrieved); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, retrieved.type); + nullable data(retrieved.val.u8); + TEST_ASSERT_FALSE(data.is_null()); + TEST_ASSERT_EQUAL(150, data.value()); + + teardown_for_update_report(); +} + +TEST_CASE("report same value returns ESP_OK", "[report][no_change]") +{ + setup_for_update_report(); + + esp_matter_attr_val_t val = esp_matter_bool(false); + esp_err_t err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + err = attribute::report(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + teardown_for_update_report(); +} + +// ============================================================ +// report() must NOT invoke attribute callbacks +// ============================================================ + +TEST_CASE("report does not call attribute callbacks", "[report][callback]") +{ + setup_for_update_report(); + reset_callback_records(); + + esp_matter_attr_val_t val = esp_matter_nullable_uint8(42); + esp_err_t err = attribute::report(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + TEST_ASSERT_FALSE(cb_pre_update.called); + TEST_ASSERT_FALSE(cb_post_update.called); + + teardown_for_update_report(); +} + +// ============================================================ +// update() MUST invoke attribute callbacks with correct values +// ============================================================ + +TEST_CASE("update calls PRE_UPDATE and POST_UPDATE callbacks", "[update][callback]") +{ + setup_for_update_report(); + reset_callback_records(); + + esp_matter_attr_val_t val = esp_matter_bool(true); + esp_err_t err = attribute::update(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + TEST_ASSERT_TRUE(cb_pre_update.called); + TEST_ASSERT_EQUAL(attribute::PRE_UPDATE, cb_pre_update.type); + TEST_ASSERT_EQUAL(test_endpoint_id, cb_pre_update.endpoint_id); + TEST_ASSERT_EQUAL(OnOff::Id, cb_pre_update.cluster_id); + TEST_ASSERT_EQUAL(OnOff::Attributes::OnOff::Id, cb_pre_update.attribute_id); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, cb_pre_update.val.type); + TEST_ASSERT_EQUAL(true, cb_pre_update.val.val.b); + + TEST_ASSERT_TRUE(cb_post_update.called); + TEST_ASSERT_EQUAL(attribute::POST_UPDATE, cb_post_update.type); + TEST_ASSERT_EQUAL(test_endpoint_id, cb_post_update.endpoint_id); + TEST_ASSERT_EQUAL(OnOff::Id, cb_post_update.cluster_id); + TEST_ASSERT_EQUAL(OnOff::Attributes::OnOff::Id, cb_post_update.attribute_id); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, cb_post_update.val.type); + TEST_ASSERT_EQUAL(true, cb_post_update.val.val.b); + + teardown_for_update_report(); +} + +TEST_CASE("update calls callbacks with correct value for nullable uint8", "[update][callback][nullable][uint8]") +{ + setup_for_update_report(); + reset_callback_records(); + + esp_matter_attr_val_t val = esp_matter_nullable_uint8(77); + esp_err_t err = attribute::update(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + TEST_ASSERT_TRUE(cb_pre_update.called); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, cb_pre_update.val.type); + TEST_ASSERT_EQUAL(77, cb_pre_update.val.val.u8); + + TEST_ASSERT_TRUE(cb_post_update.called); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, cb_post_update.val.type); + TEST_ASSERT_EQUAL(77, cb_post_update.val.val.u8); + + teardown_for_update_report(); +} + +// ============================================================ +// update() and report() behavioral parity (stored value) +// ============================================================ + +TEST_CASE("update and report both store the value correctly", "[update][report_parity]") +{ + setup_for_update_report(); + + esp_matter_attr_val_t val = esp_matter_nullable_uint8(55); + esp_err_t err = attribute::update(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + esp_matter_attr_val_t after_update; + err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &after_update); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(55, after_update.val.u8); + + val = esp_matter_nullable_uint8(88); + err = attribute::report(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + esp_matter_attr_val_t after_report; + err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &after_report); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(88, after_report.val.u8); + + TEST_ASSERT_EQUAL(after_update.type, after_report.type); + + teardown_for_update_report(); +}