diff --git a/.gitignore b/.gitignore index 76b0b6e74..4421a8125 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ _build/ tools/chip-tool/ .zap/ .DS_Store +pytest_embedded_log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 93583f546..b160749a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -612,6 +612,61 @@ pytest_esp32h2_esp_matter_dut: fi tags: ["esp32h2", "esp_matter_dut"] +build_unit_test_app_qemu: + extends: + - .build_examples_template + artifacts: + paths: + - "examples/unit_test_app/build/*.bin" + - "examples/unit_test_app/build/flasher_args.json" + - "examples/unit_test_app/build/config/sdkconfig.json" + - "examples/unit_test_app/build/bootloader/*.bin" + - "examples/unit_test_app/build/partition_table/*.bin" + - "examples/unit_test_app/build/build_log.txt" + when: always + expire_in: 4 days + script: + - cd ${ESP_MATTER_PATH}/examples/unit_test_app + - idf.py set-target esp32c3 build + +pytest_unit_test_app_qemu: + stage: target_test + image: ${DOCKER_IMAGE_NAME}:chip_${CHIP_SHORT_HASH}_idf_${IDF_CHECKOUT_REF} + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "main" || $CI_PIPELINE_SOURCE == "push" + needs: + - build_unit_test_app_qemu + before_script: + - *add_gitlab_ssh_key + - *get_build_caches + - *setup_idf + - cd ${ESP_MATTER_PATH} + - mkdir -p ${REPOS_PATH} + - *setup_matter + - *update_build_caches + variables: + REPOS_PATH: "$CI_PROJECT_DIR/repos" + script: + - cd ${ESP_MATTER_PATH} + - apt-get update && apt-get install -y -q libslirp0 + - python ${IDF_PATH}/tools/idf_tools.py install qemu-riscv32 + - eval "$(python ${IDF_PATH}/tools/idf_tools.py export)" + - pip install -r tools/ci/requirements-pytest.txt + - pytest examples/unit_test_app/pytest_unit_test_app.py + --target esp32c3 + -m qemu + --embedded-services idf,qemu + --junitxml=XUNIT_RESULT.xml + --qemu-extra-args="-global driver=timer.esp32c3.timg,property=wdt_disable,value=true" + artifacts: + paths: + - "pytest_embedded_log/" + reports: + junit: XUNIT_RESULT.xml + when: always + expire_in: 4 days + tags: ["host_test"] + build_upstream_examples: resource_group: build_upstream_examples extends: diff --git a/components/esp_matter/test/CMakeLists.txt b/components/esp_matter/test/CMakeLists.txt new file mode 100644 index 000000000..671198715 --- /dev/null +++ b/components/esp_matter/test/CMakeLists.txt @@ -0,0 +1,7 @@ +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 "." + REQUIRES unity esp_matter) diff --git a/components/esp_matter/test/attribute_get_val.cpp b/components/esp_matter/test/attribute_get_val.cpp new file mode 100644 index 000000000..014723d57 --- /dev/null +++ b/components/esp_matter/test/attribute_get_val.cpp @@ -0,0 +1,397 @@ +/* + * SPDX-FileCopyrightText: 2025 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 *root_endpoint = nullptr; +static endpoint_t *test_endpoint = nullptr; +static uint16_t test_endpoint_id = 0; +static uint16_t root_endpoint_id = 0; + +// Configurable test values for ESP Matter managed attributes +static uint8_t test_current_level = 42; +static bool test_on_off = true; +static uint16_t test_identify_time = 10; +static uint8_t test_identify_type = 2; +static uint8_t test_color_mode = 1; +static uint16_t test_color_temp = 250; + +void setup_for_get_val() +{ + 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, nullptr, nullptr); + TEST_ASSERT_NOT_NULL(test_node); + + root_endpoint = endpoint::get(test_node, 0); + TEST_ASSERT_NOT_NULL(root_endpoint); + + root_endpoint_id = endpoint::get_id(root_endpoint); + TEST_ASSERT_EQUAL(0, root_endpoint_id); + + // basically we would need some dataset that we should use for the test + // for now we are using the extended_color_light::config_t + endpoint::extended_color_light::config_t light_config; + light_config.on_off.on_off = test_on_off; + light_config.level_control.current_level = nullable(test_current_level); + light_config.identify.identify_time = test_identify_time; + light_config.identify.identify_type = test_identify_type; + light_config.color_control.color_mode = test_color_mode; + 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_get_val() +{ + // TODO: Add proper cleanup once the Matter stack supports a clean shutdown sequence + return; +} + +TEST_CASE("get_val invalid inputs", "[get_val][invalid]") +{ + setup_for_get_val(); + + esp_err_t err = attribute::get_val(nullptr, nullptr); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + esp_matter_attr_val_t val; + err = attribute::get_val(nullptr, &val); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + attribute_t *attr = attribute::get(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id); + TEST_ASSERT_NOT_NULL(attr); + err = attribute::get_val(attr, nullptr); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + /// with ids as input + err = attribute::get_val(chip::kInvalidEndpointId, chip::kInvalidClusterId, chip::kInvalidAttributeId, nullptr); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + err = attribute::get_val(chip::kInvalidEndpointId, chip::kInvalidClusterId, chip::kInvalidAttributeId, &val); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + teardown_for_get_val(); +} + +// Arrays - not supported for get_val +TEST_CASE("get_val array not supported", "[get_val][esp_matter_managed][invalid][array]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + esp_err_t err = attribute::get_val(test_endpoint_id, Descriptor::Id, Descriptor::Attributes::ServerList::Id, &val); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, err); + + err = attribute::get_val(test_endpoint_id, Descriptor::Id, Descriptor::Attributes::DeviceTypeList::Id, &val); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, err); + + teardown_for_get_val(); +} + +// Primitive Types - ESP Matter Managed + +TEST_CASE("get_val bool - OnOff", "[get_val][esp_matter_managed][bool]") +{ + setup_for_get_val(); + + attribute_t *attr = attribute::get(test_endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id); + TEST_ASSERT_NOT_NULL(attr); + esp_matter_attr_val_t setable_val = esp_matter_bool(true); + esp_err_t err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + esp_matter_attr_val_t true_val; + err = attribute::get_val(attr, &true_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, true_val.type); + TEST_ASSERT_EQUAL(true, true_val.val.b); + + setable_val = esp_matter_bool(false); + err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + esp_matter_attr_val_t false_val; + err = attribute::get_val(attr, &false_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, false_val.type); + TEST_ASSERT_EQUAL(false, false_val.val.b); + + teardown_for_get_val(); +} + +TEST_CASE("get_val uint8 - ColorMode", "[get_val][esp_matter_managed][uint8]") +{ + setup_for_get_val(); + + attribute_t *attr = attribute::get(test_endpoint_id, ColorControl::Id, ColorControl::Attributes::ColorMode::Id); + TEST_ASSERT_NOT_NULL(attr); + esp_matter_attr_val_t setable_val = esp_matter_enum8(0); + esp_err_t err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + esp_matter_attr_val_t retrieved_val; + err = attribute::get_val(attr, &retrieved_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ENUM8, retrieved_val.type); + TEST_ASSERT_EQUAL(0, retrieved_val.val.u8); + + setable_val = esp_matter_enum8(1); + err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + err = attribute::get_val(attr, &retrieved_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ENUM8, retrieved_val.type); + TEST_ASSERT_EQUAL(1, retrieved_val.val.u8); + + setable_val = esp_matter_enum8(2); + err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + err = attribute::get_val(attr, &retrieved_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ENUM8, retrieved_val.type); + TEST_ASSERT_EQUAL(2, retrieved_val.val.u8); + + // Invalid enum value (ColorMode valid range is 0-2) + setable_val = esp_matter_enum8(3); + err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, err); + + teardown_for_get_val(); +} + +TEST_CASE("get_val uint16 - ColorTemperatureMireds", "[get_val][esp_matter_managed][uint16]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + esp_err_t err = attribute::get_val(test_endpoint_id, ColorControl::Id, ColorControl::Attributes::ColorTemperatureMireds::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT16, val.type); + TEST_ASSERT_EQUAL(test_color_temp, val.val.u16); + + teardown_for_get_val(); +} + +TEST_CASE("get_val uint32 - Feature map", "[get_val][esp_matter_managed][uint32]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + esp_err_t err = attribute::get_val(test_endpoint_id, OnOff::Id, Globals::Attributes::FeatureMap::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BITMAP32, val.type); + TEST_ASSERT_EQUAL(1, val.val.u32); + + teardown_for_get_val(); +} + +// Nullable Types - ESP Matter Managed + +TEST_CASE("get_val nullable uint8 - CurrentLevel", "[get_val][esp_matter_managed][nullable][uint8]") +{ + setup_for_get_val(); + + attribute_t *attr = attribute::get(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id); + TEST_ASSERT_NOT_NULL(attr); + esp_matter_attr_val_t setable_val = esp_matter_nullable_uint8(test_current_level); + esp_err_t err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + esp_matter_attr_val_t retrieved_val; + err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &retrieved_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, retrieved_val.type); + nullable data(retrieved_val.val.u8); + TEST_ASSERT_FALSE(data.is_null()); + TEST_ASSERT_EQUAL(test_current_level, data.value()); + setable_val = esp_matter_nullable_uint8(nullable()); + err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + err = attribute::get_val(test_endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, &retrieved_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT8, retrieved_val.type); + nullable null_data(retrieved_val.val.u8); + TEST_ASSERT_TRUE(null_data.is_null()); + + teardown_for_get_val(); +} + +TEST_CASE("get_val nullable uint16 - StartUpColorTemperatureMireds", "[get_val][esp_matter_managed][nullable][uint16]") +{ + setup_for_get_val(); + + attribute_t *attr = attribute::get(test_endpoint_id, ColorControl::Id, ColorControl::Attributes::StartUpColorTemperatureMireds::Id); + TEST_ASSERT_NOT_NULL(attr); + + // this is nvs type value, so it may screw up our tests if ran without clearing the nvs + // hence, always set and verify the value + + esp_matter_attr_val_t setable_val = esp_matter_nullable_uint16(test_color_temp); + esp_err_t err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_TRUE(err == ESP_OK || err == ESP_ERR_NOT_FINISHED); + + esp_matter_attr_val_t retrieved_val; + err = attribute::get_val(attr, &retrieved_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT16, retrieved_val.type); + nullable data(retrieved_val.val.u16); + TEST_ASSERT_FALSE(data.is_null()); + TEST_ASSERT_EQUAL(test_color_temp, data.value()); + + setable_val = esp_matter_nullable_uint16(nullable()); + err = attribute::set_val(attr, &setable_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + err = attribute::get_val(attr, &retrieved_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_UINT16, retrieved_val.type); + nullable data2(retrieved_val.val.u16); + TEST_ASSERT_TRUE(data2.is_null()); + + teardown_for_get_val(); +} + +// ==================================================================================== +// get_val() -> Internally Managed Attributes (by ConnectedHomeIP) +// ==================================================================================== + +// Arrays - not supported for get_val +TEST_CASE("get_val array not supported", "[get_val][internal_managed][invalid][array]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + + esp_err_t err = attribute::get_val(test_endpoint_id, Descriptor::Id, Descriptor::Attributes::ServerList::Id, &val); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, err); + + err = attribute::get_val(test_endpoint_id, Descriptor::Id, Descriptor::Attributes::DeviceTypeList::Id, &val); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, err); + + err = attribute::get_val(root_endpoint_id, Descriptor::Id, Descriptor::Attributes::PartsList::Id, &val); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, err); + + teardown_for_get_val(); +} + +// Primitive Types - Internally Managed + +TEST_CASE("get_val uint16", "[get_val][internal_managed][uint16]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t vendor_id_val; + esp_err_t err = attribute::get_val(root_endpoint_id, BasicInformation::Id, BasicInformation::Attributes::VendorID::Id, &vendor_id_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT16, vendor_id_val.type); + TEST_ASSERT_EQUAL(CONFIG_DEVICE_VENDOR_ID, vendor_id_val.val.u16); + esp_matter_attr_val_t product_id_val; + err = attribute::get_val(root_endpoint_id, BasicInformation::Id, BasicInformation::Attributes::ProductID::Id, &product_id_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT16, product_id_val.type); + TEST_ASSERT_EQUAL(CONFIG_DEVICE_PRODUCT_ID, product_id_val.val.u16); + + teardown_for_get_val(); +} + +TEST_CASE("get_val uint32 - SoftwareVersion", "[get_val][internal_managed][uint32]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + esp_err_t err = attribute::get_val(root_endpoint_id, BasicInformation::Id, BasicInformation::Attributes::SoftwareVersion::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT32, val.type); + TEST_ASSERT_EQUAL(1, val.val.u32); + + teardown_for_get_val(); +} + +TEST_CASE("get_val uint8 - MaxNetworks", "[get_val][internal_managed][uint8]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + esp_err_t err = attribute::get_val(root_endpoint_id, NetworkCommissioning::Id, NetworkCommissioning::Attributes::MaxNetworks::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, val.type); + TEST_ASSERT_EQUAL(1, val.val.u8); + + teardown_for_get_val(); +} + +TEST_CASE("get_val bool - SupportsConcurrentConnection", "[get_val][internal_managed][bool]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + esp_err_t err = attribute::get_val(root_endpoint_id, GeneralCommissioning::Id, GeneralCommissioning::Attributes::SupportsConcurrentConnection::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, val.type); + TEST_ASSERT_EQUAL(true, val.val.b); + + teardown_for_get_val(); +} + +// Strings - Internally Managed + +TEST_CASE("get_val char_string", "[get_val][internal_managed][char_string]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t vendor_name_val; + esp_err_t err = attribute::get_val(root_endpoint_id, BasicInformation::Id, BasicInformation::Attributes::VendorName::Id, &vendor_name_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_CHAR_STRING, vendor_name_val.type); + TEST_ASSERT_NOT_NULL(vendor_name_val.val.a.b); + TEST_ASSERT_GREATER_THAN(0, vendor_name_val.val.a.s); + TEST_ASSERT_EQUAL_STRING("TEST_VENDOR", vendor_name_val.val.a.b); + free(vendor_name_val.val.a.b); + esp_matter_attr_val_t product_name_val; + err = attribute::get_val(root_endpoint_id, BasicInformation::Id, BasicInformation::Attributes::ProductName::Id, &product_name_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_CHAR_STRING, product_name_val.type); + TEST_ASSERT_NOT_NULL(product_name_val.val.a.b); + TEST_ASSERT_GREATER_THAN(0, product_name_val.val.a.s); + TEST_ASSERT_EQUAL_STRING("TEST_PRODUCT", product_name_val.val.a.b); + free(product_name_val.val.a.b); + + teardown_for_get_val(); +} + +// Nullable Types - Internally Managed + +TEST_CASE("get_val nullable int32 - LastConnectErrorValue", "[get_val][internal_managed][nullable][int32]") +{ + setup_for_get_val(); + + esp_matter_attr_val_t val; + esp_err_t err = attribute::get_val(root_endpoint_id, NetworkCommissioning::Id, NetworkCommissioning::Attributes::LastConnectErrorValue::Id, &val); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_NULLABLE_INT32, val.type); + nullable nullable_val(val.val.i32); + TEST_ASSERT_EQUAL(true, nullable_val.is_null()); + + teardown_for_get_val(); +} diff --git a/components/esp_matter/test/attribute_get_val_type.cpp b/components/esp_matter/test/attribute_get_val_type.cpp new file mode 100644 index 000000000..2dfc70544 --- /dev/null +++ b/components/esp_matter/test/attribute_get_val_type.cpp @@ -0,0 +1,410 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +using namespace esp_matter; + +// Test data model +static node_t *test_node = nullptr; +static endpoint_t *test_endpoint = nullptr; +static cluster_t *test_cluster = nullptr; +static uint16_t test_endpoint_id = 1; +static uint32_t test_cluster_id = 0xFFF1; // Custom test cluster + +static bool node_created = false; + +void setup_for_get_val_type() +{ + if (!node_created) { + esp_err_t err = nvs_flash_init(); + TEST_ASSERT_EQUAL(ESP_OK, err); + + node::config_t node_config; + test_node = node::create(&node_config, nullptr, nullptr); + TEST_ASSERT_NOT_NULL(test_node); + + test_endpoint = endpoint::create(test_node, ENDPOINT_FLAG_NONE, nullptr); + TEST_ASSERT_NOT_NULL(test_endpoint); + test_endpoint_id = endpoint::get_id(test_endpoint); + + node_created = true; + } + + // Fresh cluster per test case + test_cluster = cluster::create(test_endpoint, test_cluster_id, CLUSTER_FLAG_SERVER); + TEST_ASSERT_NOT_NULL(test_cluster); +} + +void teardown_for_get_val_type() +{ + // Only destroy the cluster, not the endpoint — endpoint is reused across test cases + // since node/endpoint creation involves starting the Matter stack which cannot be cleanly restarted. + if (test_cluster) { + cluster::destroy(test_cluster); + test_cluster = nullptr; + } +} + +TEST_CASE("invalid inputs to get_val_type", "[get_val_type][invalid]") +{ + setup_for_get_val_type(); + + esp_matter_val_type_t type = attribute::get_val_type((attribute_t *)nullptr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INVALID, type); + + type = attribute::get_val_type(0xFFFF, 0x0006, 0x0000); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INVALID, type); + + type = attribute::get_val_type(1, 0xFFFFFFFF, 0x0000); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INVALID, type); + + type = attribute::get_val_type(1, 0x0006, 0xFFFFFFFF); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INVALID, type); + + teardown_for_get_val_type(); +} + +// Test all primitive types from attribute_utils.h +TEST_CASE("get_val_type for all primitive types", "[get_val_type][all_types]") +{ + setup_for_get_val_type(); + + uint32_t attr_id = 1; // Start attribute IDs from 1 + + // Test 1: Boolean + { + esp_matter_attr_val_t val = esp_matter_bool(true); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, type); + + // Test with IDs + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, type); + } + + // Test 2: Integer (32-bit signed) + { + esp_matter_attr_val_t val = esp_matter_int(42); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INTEGER, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INTEGER, type); + } + + // Test 3: Float + { + esp_matter_attr_val_t val = esp_matter_float(3.14f); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_FLOAT, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_FLOAT, type); + } + + // Test 4: INT8 + { + esp_matter_attr_val_t val = esp_matter_int8(-128); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT8, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT8, type); + } + + // Test 5: UINT8 + { + esp_matter_attr_val_t val = esp_matter_uint8(255); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, type); + } + + // Test 6: INT16 + { + esp_matter_attr_val_t val = esp_matter_int16(-32768); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT16, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT16, type); + } + + // Test 7: UINT16 + { + esp_matter_attr_val_t val = esp_matter_uint16(65535); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT16, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT16, type); + } + + // Test 8: INT32 + { + esp_matter_attr_val_t val = esp_matter_int32(-2147483648); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT32, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT32, type); + } + + // Test 9: UINT32 + { + esp_matter_attr_val_t val = esp_matter_uint32(0xFFFFFFFF); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT32, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT32, type); + } + + // Test 10: INT64 + { + esp_matter_attr_val_t val = esp_matter_int64(-9223372036854775807LL); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT64, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT64, type); + } + + // Test 11: UINT64 + { + esp_matter_attr_val_t val = esp_matter_uint64(0xFFFFFFFFFFFFFFFFULL); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT64, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT64, type); + } + + // Test 12: ENUM8 + { + esp_matter_attr_val_t val = esp_matter_enum8(5); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ENUM8, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ENUM8, type); + } + + // Test 13: ENUM16 + { + esp_matter_attr_val_t val = esp_matter_enum16(1000); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ENUM16, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ENUM16, type); + } + + // Test 14: BITMAP8 + { + esp_matter_attr_val_t val = esp_matter_bitmap8(0xAB); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BITMAP8, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BITMAP8, type); + } + + // Test 15: BITMAP16 + { + esp_matter_attr_val_t val = esp_matter_bitmap16(0xABCD); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BITMAP16, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BITMAP16, type); + } + + // Test 16: BITMAP32 + { + esp_matter_attr_val_t val = esp_matter_bitmap32(0xABCD1234); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BITMAP32, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BITMAP32, type); + } + + // Test 17: CHAR_STRING + { + char test_str[] = "TestString"; + esp_matter_attr_val_t val = esp_matter_char_str(test_str, strlen(test_str)); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_CHAR_STRING, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_CHAR_STRING, type); + } + + // Test 18: OCTET_STRING + { + uint8_t test_octets[] = {0x01, 0x02, 0x03, 0x04}; + esp_matter_attr_val_t val = esp_matter_octet_str(test_octets, sizeof(test_octets)); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_OCTET_STRING, type); + + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id - 1); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_OCTET_STRING, type); + } + + // Test 19: ARRAY + { + // apparently our internal impl wants this to be malloced, + // otherwise at the time of destruction, we will try to free a stack allocated buffer + uint8_t *test_array = (uint8_t *)malloc(3 * sizeof(uint8_t)); + TEST_ASSERT_NOT_NULL(test_array); + test_array[0] = 1; test_array[1] = 2; test_array[2] = 3; + + esp_matter_attr_val_t val = esp_matter_array(test_array, 3 * sizeof(uint8_t), 3); + attribute_t *attr = attribute::create(test_cluster, attr_id++, ATTRIBUTE_FLAG_NONE, val); + TEST_ASSERT_NOT_NULL(attr); + + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_ARRAY, type); + + // no need to free test_array here, it will be freed when the attribute is destroyed + } + + teardown_for_get_val_type(); +} + +// Test that type persists after value updates +TEST_CASE("get_val_type persists after attribute update", "[get_val_type][persistence]") +{ + setup_for_get_val_type(); + + uint32_t attr_id = 100; + // Create a UINT8 attribute + esp_matter_attr_val_t initial_val = esp_matter_uint8(10); + attribute_t *attr = attribute::create(test_cluster, attr_id, ATTRIBUTE_FLAG_NONE, initial_val); + TEST_ASSERT_NOT_NULL(attr); + + // Verify initial type + esp_matter_val_type_t type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, type); + + // Update value + esp_matter_attr_val_t new_val = esp_matter_uint8(200); + esp_err_t err = attribute::set_val(attr, &new_val); + TEST_ASSERT_EQUAL(ESP_OK, err); + + // Type should remain the same + type = attribute::get_val_type(attr); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, type); + + // Verify with IDs as well + type = attribute::get_val_type(test_endpoint_id, test_cluster_id, attr_id); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, type); + + teardown_for_get_val_type(); +} + +// Test multiple attributes in same cluster +TEST_CASE("get_val_type for multiple attributes in same cluster", "[get_val_type][multiple]") +{ + setup_for_get_val_type(); + + // Create attributes with different types + esp_matter_attr_val_t bool_val = esp_matter_bool(false); + attribute_t *bool_attr = attribute::create(test_cluster, 1, ATTRIBUTE_FLAG_NONE, bool_val); + TEST_ASSERT_NOT_NULL(bool_attr); + + esp_matter_attr_val_t uint8_val = esp_matter_uint8(42); + attribute_t *uint8_attr = attribute::create(test_cluster, 2, ATTRIBUTE_FLAG_NONE, uint8_val); + TEST_ASSERT_NOT_NULL(uint8_attr); + + esp_matter_attr_val_t uint16_val = esp_matter_uint16(1234); + attribute_t *uint16_attr = attribute::create(test_cluster, 3, ATTRIBUTE_FLAG_NONE, uint16_val); + TEST_ASSERT_NOT_NULL(uint16_attr); + + esp_matter_attr_val_t int32_val = esp_matter_int32(-9999); + attribute_t *int32_attr = attribute::create(test_cluster, 4, ATTRIBUTE_FLAG_NONE, int32_val); + TEST_ASSERT_NOT_NULL(int32_attr); + + // Verify all types via attribute handle + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, attribute::get_val_type(bool_attr)); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, attribute::get_val_type(uint8_attr)); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT16, attribute::get_val_type(uint16_attr)); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT32, attribute::get_val_type(int32_attr)); + + // Verify all types via IDs + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_BOOLEAN, + attribute::get_val_type(test_endpoint_id, test_cluster_id, 1)); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT8, + attribute::get_val_type(test_endpoint_id, test_cluster_id, 2)); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_UINT16, + attribute::get_val_type(test_endpoint_id, test_cluster_id, 3)); + TEST_ASSERT_EQUAL(ESP_MATTER_VAL_TYPE_INT32, + attribute::get_val_type(test_endpoint_id, test_cluster_id, 4)); + + teardown_for_get_val_type(); +} 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(); +} diff --git a/examples/.build-rules.yml b/examples/.build-rules.yml index 2f6047544..879355005 100644 --- a/examples/.build-rules.yml +++ b/examples/.build-rules.yml @@ -134,8 +134,15 @@ examples/bridge_apps/esp_rainmaker_bridge: - if: IDF_TARGET in ["esp32s3"] temporary: true reason: the other targets are not tested yet + examples/camera: enable: - if: IDF_TARGET in [""] temporary: true reason: Another CI has been added + +examples/unit_test_app: + enable: + - if: IDF_TARGET in ["esp32c3"] + temporary: true + reason: the other targets are not tested yet diff --git a/examples/unit_test_app/CMakeLists.txt b/examples/unit_test_app/CMakeLists.txt new file mode 100644 index 000000000..fcf32bb98 --- /dev/null +++ b/examples/unit_test_app/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.5) + +set(PROJECT_VER "1.0") +set(PROJECT_VER_NUMBER 1) + +set(ESP_MATTER_PATH $ENV{ESP_MATTER_PATH}) +set(MATTER_SDK_PATH ${ESP_MATTER_PATH}/connectedhomeip/connectedhomeip) + +set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../../components" + "${MATTER_SDK_PATH}/config/esp32/components") + +# Set the components to include the tests for. +set(TEST_COMPONENTS "esp_matter" CACHE STRING "List of components to test") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(unit_test_app) + +# TODO: Remove -Wno-error=unused-result once submodules are updated to not treat unused return values as errors. +idf_build_set_property(CXX_COMPILE_OPTIONS "-std=gnu++17;-Os;-DCHIP_HAVE_CONFIG_H;-Wno-overloaded-virtual;-Wno-error=unused-result" APPEND) +idf_build_set_property(C_COMPILE_OPTIONS "-Os" APPEND) +# For RISCV chips, project_include.cmake sets -Wno-format, but does not clear various +# flags that depend on -Wformat +idf_build_set_property(COMPILE_OPTIONS "-Wno-format-nonliteral;-Wno-format-security" APPEND) diff --git a/examples/unit_test_app/README.md b/examples/unit_test_app/README.md new file mode 100644 index 000000000..163035f41 --- /dev/null +++ b/examples/unit_test_app/README.md @@ -0,0 +1,109 @@ +# ESP Matter Unit Test App + +This application runs unit tests for the ESP Matter component using the Unity test framework. + +## Unity Test Framework + +This test app uses the Unity Test Framework suggested by ESP-IDF. + +Further reads: +- [Unit Testing with Unity](https://docs.espressif.com/projects/vscode-esp-idf-extension/en/latest/additionalfeatures/unit-testing.html) +- [Unit Testing in ESP32](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/unit-tests.html) + +## Running the Tests + +1. Build the application: +```bash +cd examples/unit_test_app +idf.py build +``` + +2. Flash to device: +```bash +idf.py -p flash monitor +``` + +3. Run tests: +Once flashed, the test menu will appear in the serial monitor. You can: +- Press `Enter` to see the list of available tests +- Enter a test number to run a specific test +- Enter `*` to run all tests + +## Running Tests with QEMU (no hardware needed) + +You can run the unit tests locally under QEMU emulation without physical hardware. This is the same method used in CI. + +### Prerequisites + +- Install QEMU for RISC-V (esp32c3): +```bash +python3 -m pip install pytest-embedded-qemu +``` + +- Ensure the QEMU binary (`qemu-system-riscv32`) is available. Install it via ESP-IDF tools: +```bash +$IDF_PATH/tools/idf_tools.py install qemu-riscv32 +source $IDF_PATH/export.sh # re-source to pick up the new tool in PATH +``` + +### Build and Run + +```bash +cd examples/unit_test_app +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.qemu" set-target esp32c3 build + +# Run all QEMU test groups (each gets a fresh QEMU reboot) +pytest pytest_unit_test_app.py \ + --target esp32c3 \ + -m qemu \ + --embedded-services idf,qemu \ + --qemu-extra-args="-global driver=timer.esp32c3.timg,property=wdt_disable,value=true" + +# Run a single test group +pytest pytest_unit_test_app.py \ + --target esp32c3 \ + -m qemu \ + --embedded-services idf,qemu \ + --qemu-extra-args="-global driver=timer.esp32c3.timg,property=wdt_disable,value=true" \ + -k test_get_val +``` + +### Why multiple test functions? + +Each test file has its own `setup_*()` function that calls `esp_matter::start()`, and there is no teardown/stop. +Since only one setup can succeed per boot, tests are grouped so each group runs after a fresh QEMU reboot. +Each pytest function (eg: `test_get_val`, `test_get_val_type`, `test_update_report`) gets its own QEMU instance. + +## Extending the Tests + +### Adding tests to existing component + +1. Create a new `.cpp` file in `components//test/` +2. Add the filename to the source list in `components//test/CMakeLists.txt`: +```cmake +list(APPEND srcs_list "your_new_test_file.cpp") +``` +3. Write the test cases in the new test file. + +### Adding new component tests + +Please refer to components/esp_matter/test directory for comprehensive structure and example. + +- After adding the new component tests, you need to add the component to the TEST_COMPONENTS list in CMakeLists.txt +- Append the component name to the TEST_COMPONENTS list. For example, if you add a new component called "new_component", +you need to add it to the TEST_COMPONENTS list in CMakeLists.txt: + +```cmake +set(TEST_COMPONENTS "esp_matter new_component" CACHE STRING "List of components to test") +``` + +### For running them in the CI, +- Add the test group to the `pytest_unit_test_app.py` file with the appropriate marker and test function name. + +```python +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.esp32c3 +def test_my_unit_tests(dut: QemuDut) -> None: + run_group(dut, 'my_test_group') +``` diff --git a/examples/unit_test_app/main/CMakeLists.txt b/examples/unit_test_app/main/CMakeLists.txt new file mode 100644 index 000000000..9d7ee8c59 --- /dev/null +++ b/examples/unit_test_app/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "app_main.c" + INCLUDE_DIRS "." + REQUIRES unity esp_matter) diff --git a/examples/unit_test_app/main/app_main.c b/examples/unit_test_app/main/app_main.c new file mode 100644 index 000000000..063c2cd58 --- /dev/null +++ b/examples/unit_test_app/main/app_main.c @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "unity.h" +#include "esp_log.h" + +static const char *TAG = "UT"; + +void app_main(void) +{ + ESP_LOGI(TAG, "esp-matter unit test app"); + unity_run_menu(); +} \ No newline at end of file diff --git a/examples/unit_test_app/partitions.csv b/examples/unit_test_app/partitions.csv new file mode 100644 index 000000000..ffe5f242e --- /dev/null +++ b/examples/unit_test_app/partitions.csv @@ -0,0 +1,10 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table +esp_secure_cert, 0x3F, ,0xd000, 0x2000, encrypted +nvs, data, nvs, 0x10000, 0xC000, +nvs_keys, data, nvs_keys,, 0x1000, encrypted +otadata, data, ota, , 0x2000 +phy_init, data, phy, , 0x1000, +ota_0, app, ota_0, 0x20000, 0x1E0000, +ota_1, app, ota_1, 0x200000, 0x1E0000, +fctry, data, nvs, 0x3E0000, 0x6000 diff --git a/examples/unit_test_app/pytest_unit_test_app.py b/examples/unit_test_app/pytest_unit_test_app.py new file mode 100644 index 000000000..59df44f5b --- /dev/null +++ b/examples/unit_test_app/pytest_unit_test_app.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from pytest_embedded_qemu.dut import QemuDut + + +def run_group(dut: QemuDut, group: str, timeout: int = 120) -> None: + """Run all Unity cases matching a group tag, then verify no failures. + + pytest-embedded records Unity results without raising on failure, + so we check dut.testsuite afterwards to surface failures to pytest. + """ + cases = [c for c in dut.test_menu if group in c.groups] + assert cases, f'No cases for group "{group}" (parsed {len(dut.test_menu)} total)' + + dut.run_all_single_board_cases(group=group, timeout=timeout) + + failed = dut.testsuite.failed_cases + if failed: + names = [tc.name for tc in failed] + pytest.fail(f'{len(failed)} failed in [{group}]: {", ".join(names)}') + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.esp32c3 +def test_get_val(dut: QemuDut) -> None: + run_group(dut, 'get_val') + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.esp32c3 +def test_get_val_type(dut: QemuDut) -> None: + run_group(dut, 'get_val_type') + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.esp32c3 +def test_update_report(dut: QemuDut) -> None: + run_group(dut, 'report') + run_group(dut, 'update') diff --git a/examples/unit_test_app/sdkconfig.defaults b/examples/unit_test_app/sdkconfig.defaults new file mode 100644 index 000000000..a10b08b9c --- /dev/null +++ b/examples/unit_test_app/sdkconfig.defaults @@ -0,0 +1,57 @@ +# Unity Framework Configuration +CONFIG_UNITY_ENABLE_FLOAT=y +CONFIG_UNITY_ENABLE_DOUBLE=y +CONFIG_UNITY_ENABLE_64BIT=y + +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +#enable BT +CONFIG_BT_ENABLED=n +#enable lwip ipv6 autoconfig +CONFIG_LWIP_IPV6_AUTOCONFIG=y + +# Use a custom partition table +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0xC000 + +# Enable chip shell +CONFIG_ENABLE_CHIP_SHELL=y + +# Disable WiFi — unit tests don't need networking and WiFi PHY calibration hangs in QEMU +CONFIG_ENABLE_WIFI_STATION=n +CONFIG_ENABLE_WIFI_AP=n +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n + +# Use QEMU virtual Ethernet instead of WiFi +CONFIG_ETH_USE_OPENETH=y +CONFIG_ENABLE_ETHERNET_TELEMETRY=y + +#enable lwIP route hooks +CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT=y +CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT=y + +# This example only use 2 dynamic endpoints +CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT=16 + +# Enable HKDF in mbedtls +CONFIG_MBEDTLS_HKDF_C=y + +# Increase LwIP IPv6 address number to 6 (MAX_FABRIC + 1) +# unique local addresses for fabrics(MAX_FABRIC), a link local address(1) +CONFIG_LWIP_IPV6_NUM_ADDRESSES=6 + +# borrowed from unit-test-app +CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y +CONFIG_HEAP_POISONING_COMPREHENSIVE=y +CONFIG_SPI_FLASH_ENABLE_COUNTERS=y +CONFIG_ESP_TASK_WDT_INIT=n +CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS=y +CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y +CONFIG_COMPILER_STACK_CHECK=y +CONFIG_ADC_DISABLE_DAC=n +CONFIG_COMPILER_WARN_WRITE_STRINGS=y +CONFIG_SPI_MASTER_IN_IRAM=y +CONFIG_EFUSE_VIRTUAL=y +CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y + diff --git a/examples/unit_test_app/sdkconfig.defaults.qemu b/examples/unit_test_app/sdkconfig.defaults.qemu new file mode 100644 index 000000000..de05eb294 --- /dev/null +++ b/examples/unit_test_app/sdkconfig.defaults.qemu @@ -0,0 +1,5 @@ +# Disable WiFi — unit tests don't need networking and WiFi PHY calibration hangs in QEMU +CONFIG_ENABLE_WIFI_STATION=n +# Use QEMU virtual Ethernet instead of WiFi +CONFIG_ETH_USE_OPENETH=y +CONFIG_ENABLE_ETHERNET_TELEMETRY=y diff --git a/pytest.ini b/pytest.ini index 015c0b84a..7151ab374 100644 --- a/pytest.ini +++ b/pytest.ini @@ -25,6 +25,8 @@ markers = esp32s3: support esp32s3 target # env markers esp_matter_dut: esp matter runner which have single dut + host_test: test runs on host machine (not on target hardware) + qemu: test runs under QEMU emulation # log related log_cli = True