From 0e766168d71cfd16c9fc310988ddd672e527de6c Mon Sep 17 00:00:00 2001 From: lpbeliveau-silabs <112982107+lpbeliveau-silabs@users.noreply.github.com> Date: Sat, 31 Aug 2024 11:57:38 -0400 Subject: [PATCH] [Scenes] Missing Tests Cases (#32632) * Implemented missing yaml tests cases and added EFS validation in SceneHandlerImpl * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Cleanup up comments and useless static cast --------- Co-authored-by: Boris Zbarsky --- .../scenes-server/SceneHandlerImpl.cpp | 243 ++++++- .../clusters/scenes-server/scenes-server.cpp | 5 + src/app/tests/TestSceneTable.cpp | 682 ++++++++++++++++-- .../suites/certification/Test_TC_S_2_2.yaml | 86 +++ .../suites/certification/Test_TC_S_2_3.yaml | 75 ++ src/app/util/attribute-metadata.h | 21 +- src/app/util/attribute-table.cpp | 9 +- src/app/util/mock/MockNodeConfig.h | 1 + 8 files changed, 1067 insertions(+), 55 deletions(-) diff --git a/src/app/clusters/scenes-server/SceneHandlerImpl.cpp b/src/app/clusters/scenes-server/SceneHandlerImpl.cpp index c29d1bbd5d27e8..4e01802f00f34a 100644 --- a/src/app/clusters/scenes-server/SceneHandlerImpl.cpp +++ b/src/app/clusters/scenes-server/SceneHandlerImpl.cpp @@ -16,10 +16,249 @@ */ #include +#include +#include +#include namespace chip { namespace scenes { +namespace { + +template +using OddSizedInteger = app::OddSizedInteger; +using ConcreteAttributePath = app::ConcreteAttributePath; +using AttributeValuePairType = app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type; + +/// ConvertDefaultValueToWorkingValue +/// @brief Helper function to convert a byte array to a value of the given type. +/// @param EmberAfDefaultAttributeValue & defaultValue +/// @return Value converted to the given working type +template +typename app::NumericAttributeTraits::WorkingType +ConvertDefaultValueToWorkingValue(const EmberAfDefaultAttributeValue & defaultValue) +{ + if constexpr (sizeof(typename app::NumericAttributeTraits::WorkingType) <= 2) + { + return static_cast::WorkingType>(defaultValue.defaultValue); + } + + typename app::NumericAttributeTraits::StorageType sValue; + memcpy(&sValue, defaultValue.ptrToDefaultValue, sizeof(sValue)); + return app::NumericAttributeTraits::StorageToWorking(sValue); +} + +/// IsExactlyOneValuePopulated +/// @brief Helper function to verify that exactly one value is populated in a given AttributeValuePairType +/// @param AttributeValuePairType & type AttributeValuePairType to verify +/// @return bool true if only one value is populated, false otherwise +bool IsExactlyOneValuePopulated(const AttributeValuePairType & type) +{ + int count = 0; + if (type.valueUnsigned8.HasValue()) + count++; + if (type.valueSigned8.HasValue()) + count++; + if (type.valueUnsigned16.HasValue()) + count++; + if (type.valueSigned16.HasValue()) + count++; + if (type.valueUnsigned32.HasValue()) + count++; + if (type.valueSigned32.HasValue()) + count++; + if (type.valueUnsigned64.HasValue()) + count++; + if (type.valueSigned64.HasValue()) + count++; + return count == 1; +} + +/// CapAttributeValue +/// Cap the attribute value based on the attribute's min and max if they are defined, +/// or based on the attribute's size if they are not. +/// @param[in] aVPair AttributeValuePairType +/// @param[in] metadata EmberAfAttributeMetadata +/// +template +void CapAttributeValue(typename app::NumericAttributeTraits::WorkingType & value, const EmberAfAttributeMetadata * metadata) +{ + using IntType = app::NumericAttributeTraits; + using WorkingType = typename IntType::WorkingType; + + WorkingType maxValue; + WorkingType minValue; + uint16_t bitWidth = static_cast(emberAfAttributeSize(metadata) * 8); + + // TODO Use min/max values from Type to obtain min/max instead of relying on metadata. See: + // https://github.com/project-chip/connectedhomeip/issues/35328 + + // Min/Max Value caps for the OddSize integers + if (metadata->IsSignedIntegerAttribute()) + { + // We use emberAfAttributeSize for cases like INT24S, INT40S, INT48S, INT56S where numeric_limits::max() + // wouldn't work + maxValue = static_cast((1ULL << (bitWidth - 1)) - 1); + minValue = static_cast(-(1ULL << (bitWidth - 1))); + } + else + { + // We use emberAfAttributeSize for cases like INT24U, INT40U, INT48U, INT56U where numeric_limits::max() + // wouldn't work + if (ZCL_INT64U_ATTRIBUTE_TYPE == app::Compatibility::Internal::AttributeBaseType(metadata->attributeType)) + { + maxValue = static_cast(UINT64_MAX); // Bit shift of 64 is undefined so we use UINT64_MAX + } + else + { + maxValue = static_cast((1ULL << bitWidth) - 1); + } + minValue = static_cast(0); + } + + // Ensure that the metadata's signedness matches the working type's signedness + VerifyOrDie(metadata->IsSignedIntegerAttribute() == std::is_signed::value); + + if (metadata->IsBoolean()) + { + if (metadata->IsNullable() && (value != 1 && value != 0)) + { + // If the attribute is nullable, the value can be set to NULL + app::NumericAttributeTraits::SetNull(value); + } + else + { + // Caping the value to 1 in case values greater than 1 are set + value = value ? 1 : 0; + } + return; + } + + // Check metadata for min and max values + if (metadata->HasMinMax()) + { + const EmberAfAttributeMinMaxValue * minMaxValue = metadata->defaultValue.ptrToMinMaxValue; + minValue = ConvertDefaultValueToWorkingValue(minMaxValue->minValue); + maxValue = ConvertDefaultValueToWorkingValue(minMaxValue->maxValue); + } + + // If the attribute is nullable, the min and max values calculated for types will not be valid, however this does not + // change the behavior here as the value will already be NULL if it is out of range. E.g. a nullable INT8U has a minValue of + // -127. The code above determin minValue = -128, so an input value of -128 would not enter the condition block below, but would + // be considered NULL nonetheless. + if (metadata->IsNullable() && (minValue > value || maxValue < value)) + { + // If the attribute is nullable, the value can be set to NULL + app::NumericAttributeTraits::SetNull(value); + return; + } + + if (minValue > value) + { + value = minValue; + } + else if (maxValue < value) + { + value = maxValue; + } +} + +/// @brief Validate the attribute exists for a given cluster +/// @param[in] endpoint Endpoint ID +/// @param[in] clusterID Cluster ID +/// @param[in] aVPair AttributeValuePairType, will be mutated to cap the value if it is out of range +/// @return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute) if the attribute does not exist for a given cluster or is not scenable +/// @note This will allways fail for global list attributes. If we do want to make them scenable someday, we will need to +/// use a different validation method. +// TODO: Add check for "S" quality to determine if the attribute is scenable once suported : +// https://github.com/project-chip/connectedhomeip/issues/24177 +CHIP_ERROR ValidateAttributePath(EndpointId endpoint, ClusterId cluster, AttributeValuePairType & aVPair) +{ + const EmberAfAttributeMetadata * metadata = emberAfLocateAttributeMetadata(endpoint, cluster, aVPair.attributeID); + + if (nullptr == metadata) + { + return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); + } + + // There should never be more than one populated value in an ExtensionFieldSet + VerifyOrReturnError(IsExactlyOneValuePopulated(aVPair), CHIP_ERROR_INVALID_ARGUMENT); + + switch (app::Compatibility::Internal::AttributeBaseType(metadata->attributeType)) + { + case ZCL_BOOLEAN_ATTRIBUTE_TYPE: + case ZCL_INT8U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned8.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueUnsigned8.Value(), metadata); + break; + case ZCL_INT16U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueUnsigned16.Value(), metadata); + break; + case ZCL_INT24U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned32.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueUnsigned32.Value(), metadata); + break; + case ZCL_INT32U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned32.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueUnsigned32.Value(), metadata); + break; + case ZCL_INT40U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueUnsigned64.Value(), metadata); + break; + case ZCL_INT48U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueUnsigned64.Value(), metadata); + break; + case ZCL_INT56U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueUnsigned64.Value(), metadata); + break; + case ZCL_INT64U_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueUnsigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueUnsigned64.Value(), metadata); + break; + case ZCL_INT8S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned8.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueSigned8.Value(), metadata); + break; + case ZCL_INT16S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueSigned16.Value(), metadata); + break; + case ZCL_INT24S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned32.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueSigned32.Value(), metadata); + break; + case ZCL_INT32S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned32.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueSigned32.Value(), metadata); + break; + case ZCL_INT40S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueSigned64.Value(), metadata); + break; + case ZCL_INT48S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueSigned64.Value(), metadata); + break; + case ZCL_INT56S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue>(aVPair.valueSigned64.Value(), metadata); + break; + case ZCL_INT64S_ATTRIBUTE_TYPE: + VerifyOrReturnError(aVPair.valueSigned64.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + CapAttributeValue(aVPair.valueSigned64.Value(), metadata); + break; + default: + return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); + } + + return CHIP_NO_ERROR; +} +} // namespace + CHIP_ERROR DefaultSceneHandlerImpl::EncodeAttributeValueList(const List & aVlist, MutableByteSpan & serializedBytes) { @@ -58,7 +297,9 @@ DefaultSceneHandlerImpl::SerializeAdd(EndpointId endpoint, const ExtensionFieldS auto pair_iterator = extensionFieldSet.attributeValueList.begin(); while (pair_iterator.Next()) { - aVPairs[pairCount] = pair_iterator.GetValue(); + AttributeValuePairType currentPair = pair_iterator.GetValue(); + ReturnErrorOnFailure(ValidateAttributePath(endpoint, extensionFieldSet.clusterID, currentPair)); + aVPairs[pairCount] = currentPair; pairCount++; } ReturnErrorOnFailure(pair_iterator.GetStatus()); diff --git a/src/app/clusters/scenes-server/scenes-server.cpp b/src/app/clusters/scenes-server/scenes-server.cpp index acc2cd3a7dff80..ae10a3dd7ce11d 100644 --- a/src/app/clusters/scenes-server/scenes-server.cpp +++ b/src/app/clusters/scenes-server/scenes-server.cpp @@ -74,6 +74,11 @@ CHIP_ERROR AddResponseOnError(CommandHandlerInterface::HandlerContext & ctx, Res { resp.status = to_underlying(Protocols::InteractionModel::Status::ResourceExhausted); } + else if (CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute) == err) + { + // TODO: Confirm if we need to add UnsupportedAttribute status as a return for Scene Commands + resp.status = to_underlying(Protocols::InteractionModel::Status::InvalidCommand); + } else { resp.status = to_underlying(StatusIB(err).mStatus); diff --git a/src/app/tests/TestSceneTable.cpp b/src/app/tests/TestSceneTable.cpp index ffb0a1fd5650ff..2e541cb0e76a95 100644 --- a/src/app/tests/TestSceneTable.cpp +++ b/src/app/tests/TestSceneTable.cpp @@ -17,7 +17,11 @@ */ #include +#include #include +#include +#include +#include #include #include #include @@ -26,6 +30,8 @@ #include #include using namespace chip; +using namespace chip::Test; +using namespace chip::app::Clusters::Globals::Attributes; using SceneTable = scenes::SceneTable; using SceneTableEntry = scenes::DefaultSceneTableImpl::SceneTableEntry; @@ -44,25 +50,29 @@ constexpr uint8_t defaultTestFabricCapacity = (defaultTestTableSize - 1) / 2; // Test Cluster ID constexpr chip::ClusterId kOnOffClusterId = 0x0006; constexpr chip::ClusterId kLevelControlClusterId = 0x0008; +constexpr chip::ClusterId kFakeClusterId = 0x0007; constexpr chip::ClusterId kColorControlClusterId = 0x0300; +constexpr chip::ClusterId kScenesClusterId = 0x0062; // Test Endpoint ID constexpr chip::EndpointId kTestEndpoint1 = chip::Test::kMockEndpoint1; constexpr chip::EndpointId kTestEndpoint2 = chip::Test::kMockEndpoint2; constexpr chip::EndpointId kTestEndpoint3 = chip::Test::kMockEndpoint3; +constexpr chip::EndpointId kTestEndpoint4 = kMockEndpointMin; // Test Attribute ID -constexpr uint32_t kOnOffAttId = 0x0000; -constexpr uint32_t kCurrentLevelId = 0x0000; -constexpr uint32_t kCurrentFrequencyId = 0x0004; -constexpr uint32_t kCurrentSaturationId = 0x0001; -constexpr uint32_t kCurrentXId = 0x0003; -constexpr uint32_t kCurrentYId = 0x0004; -constexpr uint32_t kColorTemperatureMiredsId = 0x0007; -constexpr uint32_t kEnhancedCurrentHueId = 0x4000; -constexpr uint32_t kColorLoopActiveId = 0x4002; -constexpr uint32_t kColorLoopDirectionId = 0x4003; -constexpr uint32_t kColorLoopTimeId = 0x4004; +constexpr uint32_t kOnOffAttId = app::Clusters::OnOff::Attributes::OnOff::Id; +constexpr uint32_t kCurrentLevelId = app::Clusters::LevelControl::Attributes::CurrentLevel::Id; +constexpr uint32_t kCurrentFrequencyId = app::Clusters::LevelControl::Attributes::CurrentFrequency::Id; +constexpr uint32_t kCurrentSaturationId = app::Clusters::ColorControl::Attributes::CurrentSaturation::Id; +constexpr uint32_t kCurrentXId = app::Clusters::ColorControl::Attributes::CurrentX::Id; +constexpr uint32_t kCurrentYId = app::Clusters::ColorControl::Attributes::CurrentY::Id; +constexpr uint32_t kColorTemperatureMiredsId = app::Clusters::ColorControl::Attributes::ColorTemperatureMireds::Id; +constexpr uint32_t kEnhancedCurrentHueId = app::Clusters::ColorControl::Attributes::EnhancedCurrentHue::Id; +constexpr uint32_t kEnhancedColorMode = app::Clusters::ColorControl::Attributes::EnhancedColorMode::Id; +constexpr uint32_t kColorLoopActiveId = app::Clusters::ColorControl::Attributes::ColorLoopActive::Id; +constexpr uint32_t kColorLoopDirectionId = app::Clusters::ColorControl::Attributes::ColorLoopDirection::Id; +constexpr uint32_t kColorLoopTimeId = app::Clusters::ColorControl::Attributes::ColorLoopTime::Id; // Test Group ID constexpr chip::GroupId kGroup1 = 0x101; @@ -142,7 +152,7 @@ static app::Clusters::ScenesManagement::Structs::ExtensionFieldSet::Type CCexten static app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type OOPairs[1]; static app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type LCPairs[2]; -static app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type CCPairs[8]; +static app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type CCPairs[9]; static uint8_t OO_buffer[scenes::kMaxFieldBytesPerCluster] = { 0 }; static uint8_t LC_buffer[scenes::kMaxFieldBytesPerCluster] = { 0 }; @@ -152,6 +162,222 @@ static uint32_t OO_buffer_serialized_length = 0; static uint32_t LC_buffer_serialized_length = 0; static uint32_t CC_buffer_serialized_length = 0; +static const uint8_t defaultValueData64[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + +static EmberAfAttributeMinMaxValue minMaxValueBool = { false, false, true }; +static EmberAfAttributeMinMaxValue minMaxValue8 = { static_cast(0), static_cast(1), static_cast(0xFE) }; +static EmberAfAttributeMinMaxValue minMaxValue8S = { static_cast(0), static_cast(-1), + static_cast(0x7F) }; +static EmberAfAttributeMinMaxValue minMaxValue16S = { static_cast(0), static_cast(-1), + static_cast(0x7FFD) }; + +static EmberAfAttributeMetadata mockMetadataBool = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(&minMaxValueBool), + .attributeId = 0, + .size = 1, + .attributeType = ZCL_BOOLEAN_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint8 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(static_cast(0)), + .attributeId = 0, + .size = 1, + .attributeType = ZCL_INT8U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE | ATTRIBUTE_MASK_NULLABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint8Max = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(&minMaxValue8), + .attributeId = 0, + .size = 1, + .attributeType = ZCL_INT8U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE | ATTRIBUTE_MASK_NULLABLE | ATTRIBUTE_MASK_MIN_MAX, +}; + +static EmberAfAttributeMetadata mockMetadataUint16 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(static_cast(0)), + .attributeId = 0, + .size = 2, + .attributeType = ZCL_INT16U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint24 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(static_cast(0)), + .attributeId = 0, + .size = 3, + .attributeType = ZCL_INT24U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint32 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(static_cast(0)), + .attributeId = 0, + .size = 4, + .attributeType = ZCL_INT32U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint40 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 5, + .attributeType = ZCL_INT40U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint48 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 6, + .attributeType = ZCL_INT48U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint56 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 7, + .attributeType = ZCL_INT56U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataUint64 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 8, + .attributeType = ZCL_INT64U_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataInt8 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(&minMaxValue8S), + .attributeId = 0, + .size = 1, + .attributeType = ZCL_INT8S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE | ATTRIBUTE_MASK_MIN_MAX, +}; + +static EmberAfAttributeMetadata mockMetadataInt16 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(&minMaxValue16S), + .attributeId = 0, + .size = 2, + .attributeType = ZCL_INT16S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE | ATTRIBUTE_MASK_MIN_MAX, +}; + +static EmberAfAttributeMetadata mockMetadataInt24 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(static_cast(0)), + .attributeId = 0, + .size = 3, + .attributeType = ZCL_INT24S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataInt32 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(static_cast(0)), + .attributeId = 0, + .size = 4, + .attributeType = ZCL_INT32S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataInt40 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 5, + .attributeType = ZCL_INT40S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataInt48 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 6, + .attributeType = ZCL_INT48S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataInt56 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 7, + .attributeType = ZCL_INT56S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +static EmberAfAttributeMetadata mockMetadataInt64 = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(defaultValueData64), + .attributeId = 0, + .size = 8, + .attributeType = ZCL_INT64S_ATTRIBUTE_TYPE, + .mask = ATTRIBUTE_MASK_WRITABLE, +}; + +// clang-format off +static const MockNodeConfig SceneMockNodeConfig({ + MockEndpointConfig(kTestEndpoint1, { + MockClusterConfig(kScenesClusterId, {}), + MockClusterConfig(kOnOffClusterId, { + MockAttributeConfig(kOnOffAttId, mockMetadataBool) + }), + MockClusterConfig(kLevelControlClusterId, { + MockAttributeConfig(kCurrentLevelId, mockMetadataUint8Max), MockAttributeConfig(kCurrentFrequencyId, mockMetadataUint16) + }), + }), + MockEndpointConfig(kTestEndpoint2, { + MockClusterConfig(kScenesClusterId, {}), + MockClusterConfig(kOnOffClusterId, { + MockAttributeConfig(kOnOffAttId, mockMetadataBool) + }), + MockClusterConfig(kColorControlClusterId, { + MockAttributeConfig(kCurrentSaturationId, mockMetadataUint8), MockAttributeConfig(kCurrentXId, mockMetadataUint16), + MockAttributeConfig(kCurrentYId, mockMetadataUint16), MockAttributeConfig(kColorTemperatureMiredsId, mockMetadataUint16), + MockAttributeConfig(kEnhancedCurrentHueId, mockMetadataUint16), MockAttributeConfig(kEnhancedColorMode, mockMetadataUint8), + MockAttributeConfig(kColorLoopActiveId, mockMetadataUint8), MockAttributeConfig(kColorLoopDirectionId, mockMetadataUint8), + MockAttributeConfig(kColorLoopTimeId, mockMetadataUint16) + }), + }), + MockEndpointConfig(kTestEndpoint3, { + MockClusterConfig(kScenesClusterId, {}), + MockClusterConfig(kOnOffClusterId, { + MockAttributeConfig(kOnOffAttId, mockMetadataBool) + }), + MockClusterConfig(kLevelControlClusterId, { + MockAttributeConfig(kCurrentLevelId, mockMetadataUint8Max), MockAttributeConfig(kCurrentFrequencyId, mockMetadataUint16) + }), + MockClusterConfig(kColorControlClusterId, { + MockAttributeConfig(kCurrentSaturationId, mockMetadataUint8), MockAttributeConfig(kCurrentXId, mockMetadataUint16), + MockAttributeConfig(kCurrentYId, mockMetadataUint16), MockAttributeConfig(kColorTemperatureMiredsId, mockMetadataUint16), + MockAttributeConfig(kEnhancedCurrentHueId, mockMetadataUint16), MockAttributeConfig(kEnhancedColorMode, mockMetadataUint8), + MockAttributeConfig(kColorLoopActiveId, mockMetadataUint8), MockAttributeConfig(kColorLoopDirectionId, mockMetadataUint8), + MockAttributeConfig(kColorLoopTimeId, mockMetadataUint8) + }), + }), + + MockEndpointConfig(kTestEndpoint4, { + MockClusterConfig(kScenesClusterId, {}), + MockClusterConfig(MockClusterId(kColorControlClusterId), { + MockAttributeConfig(MockAttributeId(kCurrentSaturationId), mockMetadataUint24), MockAttributeConfig(MockAttributeId(kCurrentXId), mockMetadataUint32), + MockAttributeConfig(MockAttributeId(kCurrentYId), mockMetadataUint48), MockAttributeConfig(MockAttributeId(kColorTemperatureMiredsId), mockMetadataUint56), + MockAttributeConfig(MockAttributeId(kEnhancedCurrentHueId), mockMetadataUint64), MockAttributeConfig(MockAttributeId(kEnhancedColorMode), mockMetadataInt8), + MockAttributeConfig(MockAttributeId(kColorLoopActiveId), mockMetadataInt16), MockAttributeConfig(MockAttributeId(kColorLoopDirectionId), mockMetadataInt24), + MockAttributeConfig(MockAttributeId(kColorLoopTimeId), mockMetadataInt32) + }), + MockClusterConfig(MockClusterId(kOnOffClusterId), { + MockAttributeConfig(MockAttributeId(kOnOffAttId), mockMetadataInt48) + }), + MockClusterConfig(MockClusterId(kLevelControlClusterId), { + MockAttributeConfig(MockAttributeId(kCurrentLevelId), mockMetadataInt56), MockAttributeConfig(MockAttributeId(kCurrentFrequencyId), mockMetadataInt64) + }), + MockClusterConfig(MockClusterId(kFakeClusterId), { + MockAttributeConfig(MockAttributeId(kCurrentLevelId), mockMetadataUint40), MockAttributeConfig(MockAttributeId(kCurrentFrequencyId), mockMetadataInt40) + }), + }), +}); +// clang-format on + /// @brief Simulates a Handler where Endpoint 1 supports onoff and level control and Endpoint 2 supports onoff and color control class TestSceneHandler : public scenes::DefaultSceneHandlerImpl { @@ -191,6 +417,16 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl clusterBuffer.reduce_size(3); } } + else if (endpoint == kTestEndpoint4) + { + if (clusterBuffer.size() >= 3) + { + buffer[0] = MockClusterId(kOnOffClusterId); + buffer[1] = MockClusterId(kLevelControlClusterId); + buffer[2] = MockClusterId(kColorControlClusterId); + clusterBuffer.reduce_size(3); + } + } else { clusterBuffer.reduce_size(0); @@ -208,7 +444,7 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl } } - if (endpoint == kTestEndpoint1) + if (endpoint == kTestEndpoint2) { if (cluster == kOnOffClusterId || cluster == kColorControlClusterId) { @@ -216,7 +452,7 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl } } - if (endpoint == kTestEndpoint1) + if (endpoint == kTestEndpoint3) { if (cluster == kOnOffClusterId || cluster == kLevelControlClusterId || cluster == kColorControlClusterId) { @@ -224,6 +460,15 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl } } + if (endpoint == kTestEndpoint4) + { + if (cluster == MockClusterId(kColorControlClusterId) || cluster == MockClusterId(kLevelControlClusterId) || + cluster == MockClusterId(kColorControlClusterId) || cluster == MockClusterId(kFakeClusterId)) + { + return true; + } + } + return false; } @@ -258,7 +503,7 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl break; } } - if (endpoint == kTestEndpoint1) + if (endpoint == kTestEndpoint2) { switch (cluster) { @@ -280,7 +525,7 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl break; } } - if (endpoint == kTestEndpoint1) + if (endpoint == kTestEndpoint3) { switch (cluster) { @@ -347,7 +592,7 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl } // Takes values from cluster in Endpoint 2 - if (endpoint == kTestEndpoint1) + if (endpoint == kTestEndpoint2) { switch (cluster) { @@ -369,7 +614,7 @@ class TestSceneHandler : public scenes::DefaultSceneHandlerImpl } // Takes values from cluster in Endpoint 3 - if (endpoint == kTestEndpoint1) + if (endpoint == kTestEndpoint3) { switch (cluster) { @@ -440,6 +685,7 @@ class TestSceneTable : public ::testing::Test SceneTable * sceneTable = scenes::GetSceneTableImpl(); ASSERT_NE(sceneTable, nullptr); ASSERT_EQ(sceneTable->Init(mpTestStorage), CHIP_NO_ERROR); + SetMockNodeConfig(SceneMockNodeConfig); } static void TearDownTestSuite() @@ -531,34 +777,32 @@ TEST_F(TestSceneTable, TestHandlerFunctions) TLV::TLVReader reader; TLV::TLVWriter writer; - static const uint8_t OO_av_payload = 0x01; - static const uint16_t LC_av_payload[2] = { 0x64, 0x01F0 }; - static const uint16_t CC_av_payload[8] = { 0 }; - OOPairs[0].attributeID = kOnOffAttId; - OOPairs[0].valueUnsigned8.SetValue(OO_av_payload); + OOPairs[0].valueUnsigned8.SetValue(0x01); LCPairs[0].attributeID = kCurrentLevelId; - LCPairs[0].valueUnsigned8.SetValue(static_cast(LC_av_payload[0])); + LCPairs[0].valueUnsigned8.SetValue(0x64); LCPairs[1].attributeID = kCurrentFrequencyId; - LCPairs[1].valueUnsigned16.SetValue(LC_av_payload[1]); + LCPairs[1].valueUnsigned16.SetValue(0x01F0); CCPairs[0].attributeID = kCurrentSaturationId; - CCPairs[0].valueUnsigned8.SetValue(static_cast(CC_av_payload[0])); + CCPairs[0].valueUnsigned8.SetValue(0); CCPairs[1].attributeID = kCurrentXId; - CCPairs[1].valueUnsigned16.SetValue(CC_av_payload[1]); + CCPairs[1].valueUnsigned16.SetValue(0); CCPairs[2].attributeID = kCurrentYId; - CCPairs[2].valueUnsigned16.SetValue(CC_av_payload[2]); + CCPairs[2].valueUnsigned16.SetValue(0); CCPairs[3].attributeID = kColorTemperatureMiredsId; - CCPairs[3].valueUnsigned16.SetValue(CC_av_payload[3]); + CCPairs[3].valueUnsigned16.SetValue(0); CCPairs[4].attributeID = kEnhancedCurrentHueId; - CCPairs[4].valueUnsigned16.SetValue(CC_av_payload[4]); - CCPairs[5].attributeID = kColorLoopActiveId; - CCPairs[5].valueUnsigned8.SetValue(static_cast(CC_av_payload[5])); - CCPairs[6].attributeID = kColorLoopDirectionId; - CCPairs[6].valueUnsigned8.SetValue(static_cast(CC_av_payload[6])); - CCPairs[7].attributeID = kColorLoopTimeId; - CCPairs[7].valueUnsigned8.SetValue(static_cast(CC_av_payload[7])); + CCPairs[4].valueUnsigned16.SetValue(0); + CCPairs[5].attributeID = kEnhancedColorMode; + CCPairs[5].valueUnsigned8.SetValue(0); + CCPairs[6].attributeID = kColorLoopActiveId; + CCPairs[6].valueUnsigned8.SetValue(0); + CCPairs[7].attributeID = kColorLoopDirectionId; + CCPairs[7].valueUnsigned8.SetValue(0); + CCPairs[8].attributeID = kColorLoopTimeId; + CCPairs[8].valueUnsigned16.SetValue(0); // Initialize Extension Field sets as if they were received by add commands OOextensionFieldSet.clusterID = kOnOffClusterId; @@ -572,7 +816,8 @@ TEST_F(TestSceneTable, TestHandlerFunctions) ByteSpan LC_list(LC_buffer); ByteSpan CC_list(CC_buffer); - uint8_t buffer[scenes::kMaxFieldBytesPerCluster] = { 0 }; + constexpr uint16_t bufferSize = 1024; + uint8_t buffer[bufferSize] = { 0 }; MutableByteSpan buff_span(buffer); // Serialize Extension Field sets as if they were recovered from memory @@ -626,11 +871,11 @@ TEST_F(TestSceneTable, TestHandlerFunctions) EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); EXPECT_EQ(CHIP_NO_ERROR, extensionFieldSetIn.attributeValueList.Decode(reader)); - EXPECT_TRUE(mpSceneHandler->SupportsCluster(kTestEndpoint1, extensionFieldSetIn.clusterID)); - EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint1, extensionFieldSetIn, buff_span)); + EXPECT_TRUE(mpSceneHandler->SupportsCluster(kTestEndpoint2, extensionFieldSetIn.clusterID)); + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint2, extensionFieldSetIn, buff_span)); // Verify the handler extracted buffer matches the initial field sets - EXPECT_EQ(0, memcmp(CC_list.data(), buff_span.data(), buff_span.size())); + EXPECT_EQ(0, memcmp(CC_list.data(), buff_span.data(), CC_list.size())); memset(buffer, 0, buff_span.size()); buff_span = MutableByteSpan(buffer); @@ -641,7 +886,7 @@ TEST_F(TestSceneTable, TestHandlerFunctions) // Verify Encoding the Extension field set returns the same data as the one serialized for on off previously writer.Init(buff_span); EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), extensionFieldSetOut.attributeValueList)); - EXPECT_EQ(0, memcmp(OO_list.data(), buff_span.data(), buff_span.size())); + EXPECT_EQ(0, memcmp(OO_list.data(), buff_span.data(), OO_list.size())); memset(buffer, 0, buff_span.size()); // Verify Deserializing is properly filling out output extension field set for level control @@ -651,17 +896,17 @@ TEST_F(TestSceneTable, TestHandlerFunctions) // Verify Encoding the Extension field set returns the same data as the one serialized for level control previously writer.Init(buff_span); EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), extensionFieldSetOut.attributeValueList)); - EXPECT_EQ(0, memcmp(LC_list.data(), buff_span.data(), buff_span.size())); + EXPECT_EQ(0, memcmp(LC_list.data(), buff_span.data(), LC_list.size())); memset(buffer, 0, buff_span.size()); // Verify Deserializing is properly filling out output extension field set for color control - EXPECT_TRUE(mpSceneHandler->SupportsCluster(kTestEndpoint1, kColorControlClusterId)); - EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->Deserialize(kTestEndpoint1, kColorControlClusterId, CC_list, extensionFieldSetOut)); + EXPECT_TRUE(mpSceneHandler->SupportsCluster(kTestEndpoint2, kColorControlClusterId)); + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->Deserialize(kTestEndpoint2, kColorControlClusterId, CC_list, extensionFieldSetOut)); // Verify Encoding the Extension field set returns the same data as the one serialized for color control previously writer.Init(buff_span); EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), extensionFieldSetOut.attributeValueList)); - EXPECT_EQ(0, memcmp(CC_list.data(), buff_span.data(), buff_span.size())); + EXPECT_EQ(0, memcmp(CC_list.data(), buff_span.data(), CC_list.size())); memset(buffer, 0, buff_span.size()); // To test failure on serialize and deserialize when too many pairs are in the field sets @@ -698,6 +943,353 @@ TEST_F(TestSceneTable, TestHandlerFunctions) memset(failBuffer, 0, fail_list.size()); memset(buffer, 0, buff_span.size()); + + // Test Serialize Add of an attribute value that is greater than the mock attribute max (Max bool value) + OOPairs[0].valueUnsigned8.SetValue(0xFF); + + // EFS to test caping of value once a variable above the mock attribute size is serialized + app::Clusters::ScenesManagement::Structs::ExtensionFieldSet::Type extensionFieldValueCapOut; + app::Clusters::ScenesManagement::Structs::ExtensionFieldSet::DecodableType extensionFieldValueCapIn; + + extensionFieldValueCapOut.clusterID = kOnOffClusterId; + extensionFieldValueCapOut.attributeValueList = OOPairs; + + /// Setup of input EFS (by temporary using the output one) + writer.Init(buff_span); + EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), extensionFieldValueCapOut.attributeValueList)); + + reader.Init(buffer); + extensionFieldValueCapIn.clusterID = kOnOffClusterId; + EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); + EXPECT_EQ(CHIP_NO_ERROR, extensionFieldValueCapIn.attributeValueList.Decode(reader)); + + // Verify that the initial value is not capped + auto pair_iterator = extensionFieldValueCapIn.attributeValueList.begin(); + pair_iterator.Next(); + app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type pair = pair_iterator.GetValue(); + EXPECT_EQ(pair.valueUnsigned8.Value(), OOPairs[0].valueUnsigned8.Value()); + + // Verify that we cap the value to the mock attribute size when serializing + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint1, extensionFieldValueCapIn, buff_span)); + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->Deserialize(kTestEndpoint1, kOnOffClusterId, buff_span, extensionFieldValueCapOut)); + + // Verify that the output value is capped to 1 + EXPECT_EQ(1, extensionFieldValueCapOut.attributeValueList[0].valueUnsigned8.Value()); + + // Clear buffer + memset(buffer, 0, buff_span.size()); + + // Test Serialize Add of an attribute value that is smaller than the mock attribute min (1) for LC current level + LCPairs[0].valueUnsigned8.SetValue(0); + + extensionFieldValueCapOut.clusterID = kLevelControlClusterId; + extensionFieldValueCapOut.attributeValueList = LCPairs; + + /// Setup of input EFS (by temporary using the output one) + buff_span = MutableByteSpan(buffer); + writer.Init(buff_span); + EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), extensionFieldValueCapOut.attributeValueList)); + + reader.Init(buffer); + extensionFieldValueCapIn.clusterID = kLevelControlClusterId; + EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); + EXPECT_EQ(CHIP_NO_ERROR, extensionFieldValueCapIn.attributeValueList.Decode(reader)); + + // Verify that the initial value is not capped + auto iteratorMin = extensionFieldValueCapIn.attributeValueList.begin(); + iteratorMin.Next(); + pair = iteratorMin.GetValue(); + EXPECT_EQ(pair.valueUnsigned8.Value(), LCPairs[0].valueUnsigned8.Value()); + + // Verify that we cap the value to the mock attribute size when serializing + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint1, extensionFieldValueCapIn, buff_span)); + EXPECT_EQ(CHIP_NO_ERROR, + mpSceneHandler->Deserialize(kTestEndpoint1, kLevelControlClusterId, buff_span, extensionFieldValueCapOut)); + + // Verify that the output value is capped to 255 (NULL) as Level Control Current Level is a nullable uint8_t + EXPECT_EQ(255, extensionFieldValueCapOut.attributeValueList[0].valueUnsigned8.Value()); + + // Clear buffer + memset(buffer, 0, buff_span.size()); + + // Test Serialize Add of an attribute value that is higher than the mock attribute max (0xFE) for LC current level + LCPairs[0].valueUnsigned8.SetValue(0xFF); + + extensionFieldValueCapOut.clusterID = kLevelControlClusterId; + extensionFieldValueCapOut.attributeValueList = LCPairs; + + /// Setup of input EFS (by temporary using the output one) + buff_span = MutableByteSpan(buffer); + writer.Init(buff_span); + EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), extensionFieldValueCapOut.attributeValueList)); + + reader.Init(buffer); + extensionFieldValueCapIn.clusterID = kLevelControlClusterId; + EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); + EXPECT_EQ(CHIP_NO_ERROR, extensionFieldValueCapIn.attributeValueList.Decode(reader)); + + // Verify that the initial value is not capped + auto iteratorMax = extensionFieldValueCapIn.attributeValueList.begin(); + iteratorMax.Next(); + pair = iteratorMax.GetValue(); + EXPECT_EQ(pair.valueUnsigned8.Value(), LCPairs[0].valueUnsigned8.Value()); + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint1, extensionFieldValueCapIn, buff_span)); + EXPECT_EQ(CHIP_NO_ERROR, + mpSceneHandler->Deserialize(kTestEndpoint1, kLevelControlClusterId, buff_span, extensionFieldValueCapOut)); + + // Verify that the output value is 0xFF (NULL) as Level Control Current Level is a nullable uint8_t + EXPECT_EQ(0xFF, extensionFieldValueCapOut.attributeValueList[0].valueUnsigned8.Value()); + + // Clear buffer + memset(buffer, 0, buff_span.size()); + buff_span = MutableByteSpan(buffer); + + // Test for attribtues types that are in no Real clusters yet but are supported in scenes + { + // Setup EFS for mock cluster testing all attributes types + app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type MockOOPairs[1]; + app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type MockLCPairs[2]; + app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type MockCCPairs[9]; + app::Clusters::ScenesManagement::Structs::AttributeValuePairStruct::Type MockFKPairs[2]; + // Mock CC + MockCCPairs[0].attributeID = MockAttributeId(kCurrentSaturationId); + MockCCPairs[0].valueUnsigned32.SetValue(UINT32_MAX); // will cap to 0x00FFFFFF (uint24) + MockCCPairs[1].attributeID = MockAttributeId(kCurrentXId); + MockCCPairs[1].valueUnsigned32.SetValue(UINT32_MAX); // not capped + MockCCPairs[2].attributeID = MockAttributeId(kCurrentYId); + MockCCPairs[2].valueUnsigned64.SetValue(UINT64_MAX); // will cap to 0x0000FFFFFFFFFFFF (uint48) + MockCCPairs[3].attributeID = MockAttributeId(kColorTemperatureMiredsId); + MockCCPairs[3].valueUnsigned64.SetValue(UINT64_MAX); // will cap to 0x00FFFFFFFFFFFFFF (uint56) + MockCCPairs[4].attributeID = MockAttributeId(kEnhancedCurrentHueId); + MockCCPairs[4].valueUnsigned64.SetValue(UINT64_MAX); // not capped + MockCCPairs[5].attributeID = MockAttributeId(kEnhancedColorMode); + MockCCPairs[5].valueSigned8.SetValue(static_cast(-2)); // will cap to -1 + MockCCPairs[6].attributeID = MockAttributeId(kColorLoopActiveId); + MockCCPairs[6].valueSigned16.SetValue( + static_cast(0x7FFE)); // will cap to 0x7FFD in int16 due to declared maximum in the attribute's mock metadata + MockCCPairs[7].attributeID = MockAttributeId(kColorLoopDirectionId); + MockCCPairs[7].valueSigned32.SetValue(-1); // will cap to -1 in int24 + MockCCPairs[8].attributeID = MockAttributeId(kColorLoopTimeId); + MockCCPairs[8].valueSigned32.SetValue(-1); // not capped + // Mock OO + MockOOPairs[0].attributeID = MockAttributeId(kOnOffAttId); + MockOOPairs[0].valueSigned64.SetValue(INT64_MAX); // will cap to 0x00007FFFFFFFFFFF (int48) + // Mock LC + MockLCPairs[0].attributeID = MockAttributeId(kCurrentLevelId); + MockLCPairs[0].valueSigned64.SetValue(INT64_MIN); // will cap to 0x0080000000000000 (int56 min) + MockLCPairs[1].attributeID = MockAttributeId(kCurrentFrequencyId); + MockLCPairs[1].valueSigned64.SetValue(INT64_MIN); // not capped + // Mock Fake + MockFKPairs[0].attributeID = MockAttributeId(kCurrentLevelId); + MockFKPairs[0].valueUnsigned64.SetValue(UINT64_MAX); // will cap to UINT40_MAX + MockFKPairs[1].attributeID = MockAttributeId(kCurrentFrequencyId); + MockFKPairs[1].valueSigned64.SetValue(INT64_MAX); // will cap to INT40_MIN + + // Initialize Extension Field sets as if they were received by add commands + OOextensionFieldSet.clusterID = MockClusterId(kOnOffClusterId); + OOextensionFieldSet.attributeValueList = MockOOPairs; + LCextensionFieldSet.clusterID = MockClusterId(kLevelControlClusterId); + LCextensionFieldSet.attributeValueList = MockLCPairs; + CCextensionFieldSet.clusterID = MockClusterId(kColorControlClusterId); + CCextensionFieldSet.attributeValueList = MockCCPairs; + + uint8_t mock_OO_buffer[scenes::kMaxFieldBytesPerCluster] = { 0 }; + uint8_t mock_LC_buffer[scenes::kMaxFieldBytesPerCluster] = { 0 }; + uint8_t mock_CC_buffer[scenes::kMaxFieldBytesPerCluster * 2] = { + 0 + }; // Using mock attributes way bigger than the real ones so we increase the buffer size for this test + ByteSpan Mock_OO_list(mock_OO_buffer); + ByteSpan Mock_LC_list(mock_LC_buffer); + ByteSpan Mock_CC_list(mock_CC_buffer); + + // Serialize Extension Field sets as if they were recovered from memory + writer.Init(mock_OO_buffer); + EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), OOextensionFieldSet.attributeValueList)); + OO_buffer_serialized_length = writer.GetLengthWritten(); + + writer.Init(mock_LC_buffer); + EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), LCextensionFieldSet.attributeValueList)); + LC_buffer_serialized_length = writer.GetLengthWritten(); + + writer.Init(mock_CC_buffer); + EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), CCextensionFieldSet.attributeValueList)); + CC_buffer_serialized_length = writer.GetLengthWritten(); + + // Setup the On Off Extension field set in the expected state from a command + reader.Init(Mock_OO_list); + extensionFieldValueCapIn.clusterID = MockClusterId(kOnOffClusterId); + EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); + EXPECT_EQ(CHIP_NO_ERROR, extensionFieldValueCapIn.attributeValueList.Decode(reader)); + + // Verify that the initial value is not capped + auto iteratorOO = extensionFieldValueCapIn.attributeValueList.begin(); + iteratorOO.Next(); + pair = iteratorOO.GetValue(); + EXPECT_EQ(pair.valueSigned64.Value(), MockOOPairs[0].valueSigned64.Value()); + + // Verify that we cap the value to the mock attribute size when serializing + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint4, extensionFieldValueCapIn, buff_span)); + EXPECT_EQ( + CHIP_NO_ERROR, + mpSceneHandler->Deserialize(kTestEndpoint4, MockClusterId(kOnOffClusterId), buff_span, extensionFieldValueCapOut)); + + // Verify that the output value is capped to int48 max value + int64_t int48Max = static_cast(0x00007FFFFFFFFFFF); + EXPECT_EQ(int48Max, extensionFieldValueCapOut.attributeValueList[0].valueSigned64.Value()); + + // Clear buffer + memset(buffer, 0, buff_span.size()); + // Reinit buffer + buff_span = MutableByteSpan(buffer); + + reader.Init(Mock_LC_list); + extensionFieldValueCapIn.clusterID = MockClusterId(kLevelControlClusterId); + EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); + EXPECT_EQ(CHIP_NO_ERROR, extensionFieldValueCapIn.attributeValueList.Decode(reader)); + + // Verify that the initial values are not capped + auto iteratorLC = extensionFieldValueCapIn.attributeValueList.begin(); + iteratorLC.Next(); + pair = iteratorLC.GetValue(); + EXPECT_EQ(pair.valueSigned64.Value(), MockLCPairs[0].valueSigned64.Value()); + iteratorLC.Next(); + pair = iteratorLC.GetValue(); + EXPECT_EQ(pair.valueSigned64.Value(), MockLCPairs[1].valueSigned64.Value()); + + // Verify that we cap the value to the mock attribute size when serializing + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint4, extensionFieldValueCapIn, buff_span)); + EXPECT_EQ(CHIP_NO_ERROR, + mpSceneHandler->Deserialize(kTestEndpoint4, MockClusterId(kLevelControlClusterId), buff_span, + extensionFieldValueCapOut)); + + // Verify that the output value is capped to int56 min value + int64_t int56Min = static_cast(0xFF80000000000000); + EXPECT_EQ(int56Min, static_cast(extensionFieldValueCapOut.attributeValueList[0].valueSigned64.Value())); + + // Verify that the output value is not capped + EXPECT_EQ(INT64_MIN, extensionFieldValueCapOut.attributeValueList[1].valueSigned64.Value()); + + // Clear buffer + memset(buffer, 0, buff_span.size()); + // Reinit buffer + buff_span = MutableByteSpan(buffer); + + reader.Init(Mock_CC_list); + extensionFieldValueCapIn.clusterID = MockClusterId(kColorControlClusterId); + EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); + EXPECT_EQ(CHIP_NO_ERROR, extensionFieldValueCapIn.attributeValueList.Decode(reader)); + + // Verify that the initial values are not capped + auto iteratorCC = extensionFieldValueCapIn.attributeValueList.begin(); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueUnsigned32.Value(), MockCCPairs[0].valueUnsigned32.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueUnsigned32.Value(), MockCCPairs[1].valueUnsigned32.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueUnsigned64.Value(), MockCCPairs[2].valueUnsigned64.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueUnsigned64.Value(), MockCCPairs[3].valueUnsigned64.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueUnsigned64.Value(), MockCCPairs[4].valueUnsigned64.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueSigned8.Value(), MockCCPairs[5].valueSigned8.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueSigned16.Value(), MockCCPairs[6].valueSigned16.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueSigned32.Value(), MockCCPairs[7].valueSigned32.Value()); + iteratorCC.Next(); + pair = iteratorCC.GetValue(); + EXPECT_EQ(pair.valueSigned32.Value(), MockCCPairs[8].valueSigned32.Value()); + + // Verify that we cap the value to the mock attribute size when serializing + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint4, extensionFieldValueCapIn, buff_span)); + EXPECT_EQ(CHIP_NO_ERROR, + mpSceneHandler->Deserialize(kTestEndpoint4, MockClusterId(kColorControlClusterId), buff_span, + extensionFieldValueCapOut)); + + // Verify that the output value is capped to uint24t max value + uint32_t uint24Max = static_cast(0x00FFFFFF); + EXPECT_EQ(uint24Max, extensionFieldValueCapOut.attributeValueList[0].valueUnsigned32.Value()); + + // Verify that the output value is not capped + EXPECT_EQ(UINT32_MAX, extensionFieldValueCapOut.attributeValueList[1].valueUnsigned32.Value()); + + // Verify that the output value is capped to int48_t max value + uint64_t uint48Max = static_cast(0x0000FFFFFFFFFFFF); + EXPECT_EQ(uint48Max, extensionFieldValueCapOut.attributeValueList[2].valueUnsigned64.Value()); + + // Verify that the output value is capped to int56_t max value + uint64_t uint56Max = static_cast(0x00FFFFFFFFFFFFFF); + EXPECT_EQ(uint56Max, extensionFieldValueCapOut.attributeValueList[3].valueUnsigned64.Value()); + + // Verify that the output value is not capped + EXPECT_EQ(UINT64_MAX, extensionFieldValueCapOut.attributeValueList[4].valueUnsigned64.Value()); + + // Verify that the output value is capped to the defined min for this attribute + EXPECT_EQ(static_cast(-1), extensionFieldValueCapOut.attributeValueList[5].valueSigned8.Value()); + + // Verify that the output value is capped to the defined max for this attribute + EXPECT_EQ(0x7FFD, extensionFieldValueCapOut.attributeValueList[6].valueSigned16.Value()); + + // Verify that the output value is not capped to -1 in int24t + using Int24Type = app::NumericAttributeTraits>::WorkingType; + EXPECT_EQ(static_cast(-1), extensionFieldValueCapOut.attributeValueList[7].valueSigned32.Value()); + + // Verify that the output value will not cap + EXPECT_EQ(-1, extensionFieldValueCapOut.attributeValueList[8].valueSigned32.Value()); + + // Clear buffer + memset(buffer, 0, buff_span.size()); + + LCextensionFieldSet.clusterID = MockClusterId(kFakeClusterId); + LCextensionFieldSet.attributeValueList = MockFKPairs; + + writer.Init(mock_LC_buffer); + EXPECT_EQ(CHIP_NO_ERROR, app::DataModel::Encode(writer, TLV::AnonymousTag(), LCextensionFieldSet.attributeValueList)); + LC_buffer_serialized_length = writer.GetLengthWritten(); + + // Reinit buffer + buff_span = MutableByteSpan(buffer); + + reader.Init(Mock_LC_list); + extensionFieldValueCapIn.clusterID = MockClusterId(kFakeClusterId); + EXPECT_EQ(CHIP_NO_ERROR, reader.Next()); + EXPECT_EQ(CHIP_NO_ERROR, extensionFieldValueCapIn.attributeValueList.Decode(reader)); + + // Verify that the initial values are not capped + auto iteratorFK = extensionFieldValueCapIn.attributeValueList.begin(); + iteratorFK.Next(); + pair = iteratorFK.GetValue(); + EXPECT_EQ(pair.valueUnsigned64.Value(), MockFKPairs[0].valueUnsigned64.Value()); + iteratorFK.Next(); + pair = iteratorFK.GetValue(); + EXPECT_EQ(pair.valueSigned64.Value(), MockFKPairs[1].valueSigned64.Value()); + + // Verify that we cap the value to the mock attribute size when serializing + EXPECT_EQ(CHIP_NO_ERROR, mpSceneHandler->SerializeAdd(kTestEndpoint4, extensionFieldValueCapIn, buff_span)); + EXPECT_EQ(CHIP_NO_ERROR, + mpSceneHandler->Deserialize(kTestEndpoint4, MockClusterId(kFakeClusterId), buff_span, extensionFieldValueCapOut)); + + // Verify that the output value is capped to uint40 max value + uint64_t uint40Max = static_cast(0x000000FFFFFFFFFF); + EXPECT_EQ(uint40Max, extensionFieldValueCapOut.attributeValueList[0].valueUnsigned64.Value()); + + // Verify that the output value is capped to int40 max value + int64_t int40Max = static_cast(0x0000007FFFFFFFFF); + EXPECT_EQ(int40Max, extensionFieldValueCapOut.attributeValueList[1].valueSigned64.Value()); + + // Clear buffer + memset(buffer, 0, buff_span.size()); + // Reinit buffer + } }; TEST_F(TestSceneTable, TestStoreScenes) diff --git a/src/app/tests/suites/certification/Test_TC_S_2_2.yaml b/src/app/tests/suites/certification/Test_TC_S_2_2.yaml index 7e5121680ceffd..95005e30c40e42 100644 --- a/src/app/tests/suites/certification/Test_TC_S_2_2.yaml +++ b/src/app/tests/suites/certification/Test_TC_S_2_2.yaml @@ -451,6 +451,20 @@ tests: response: error: CONSTRAINT_ERROR + - label: + "Step 4f: TH sends a RecallScene command to DUT with the GroupID field + set to GI (Where GI is a group currently absent from the group table) + and the SceneID field set to 0x01." + command: "RecallScene" + arguments: + values: + - name: "GroupID" + value: GI + - name: "SceneID" + value: 0x01 + response: + error: INVALID_COMMAND + - label: "Step 5a: TH sends a ViewScene command to DUT with the GroupID field set to G1 and the SceneID field set to 0x01." @@ -843,6 +857,78 @@ tests: - name: "SceneID" value: 0xFF + - label: + "Step 8g: TH sends a AddScene command to DUT with the GroupID field + set to G1, the SceneID field set to 0x01, the TransitionTime field set + to 1000 (1s) and extension field sets holding an invalid + ExtensionField (Unscenable attribute ID for given cluster). This + should fail and return a status of 0x85 (INVALID_COMMAND)." + ## TODO: Change test to test for an existing bu non scenable attribute ID once scenability check is possible, see issue: https://github.com/project-chip/connectedhomeip/issues/24177 + PICS: S.S.C00.Rsp && PICS_SDK_CI_ONLY + command: "AddScene" + arguments: + values: + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x01 + - name: "TransitionTime" + value: 1000 + - name: "SceneName" + value: "Scene1" + - name: "ExtensionFieldSets" + value: + [ + { + ClusterID: 0x0006, + AttributeValueList: + [{ AttributeID: 0x4011, ValueUnsigned8: 0x01 }], + }, + ] + response: + values: + - name: "Status" + value: 0x85 + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x01 + + - label: + "Step 8g: TH sends a AddScene command to DUT with the GroupID field + set to G1, the SceneID field set to 0x01, the TransitionTime field set + to 1000 (1s) and extension field sets holding an invalid + ExtensionField (Unscenable attribute ID for given cluster). This + should fail and return a status of 0x85 (INVALID_COMMAND)." + ## TODO: Change test to test for an existing bu non scenable attribute ID once scenability check is possible, see issue: https://github.com/project-chip/connectedhomeip/issues/24177 + verification: | + ./chip-tool scenesmanagement add-scene 0x0001 0x01 1000 "scene name" '[{"clusterID": "0x006", "attributeValueList":[{"attributeID": "0x4001", "attributeValue": "0x01"}]}]' 1 1 + + Verify DUT sends a AddSceneResponse command to TH with the Status field set to 0x85 (INVALID_COMMAND), the GroupID field set to G1 and the SceneID field set to 0x01 on the TH(Chip-tool) + Log and below is the sample log provided for the raspi platform: + + [1706763610.675038][4232:4234] CHIP:DMG: }, + [1706763610.675108][4232:4234] CHIP:DMG: Received Command Response Data, Endpoint=1 Cluster=0x0000_0062 Command=0x0000_0000 + [1706763610.675134][4232:4234] CHIP:TOO: Endpoint: 1 Cluster: 0x0000_0062 Command 0x0000_0000 + [1706763610.675187][4232:4234] CHIP:TOO: AddSceneResponse: { + [1706763610.675215][4232:4234] CHIP:TOO: status: 133 + [1706763610.675229][4232:4234] CHIP:TOO: groupID: 1 + [1706763610.675244][4232:4234] CHIP:TOO: sceneID: 1 + [1706763610.675258][4232:4234] CHIP:TOO: } + cluster: "LogCommands" + command: "UserPrompt" + PICS: PICS_SKIP_SAMPLE_APP + arguments: + values: + - name: "message" + value: + "Please execute the add scene command with an invalid + extensionfieldsets due to a non sceneable attribute not in + an extensionfieldset on DUT and enter 'y' if the command + returned a status of 0x85 (INVALID_COMMAND)" + - name: "expectedValue" + value: "y" + - label: "Step 9a: TH sends a RemoveScene command to DUT with the GroupID field set to G1 and the SceneID field set to 0x01." diff --git a/src/app/tests/suites/certification/Test_TC_S_2_3.yaml b/src/app/tests/suites/certification/Test_TC_S_2_3.yaml index 817ab532bad66c..1e66f4c803834d 100644 --- a/src/app/tests/suites/certification/Test_TC_S_2_3.yaml +++ b/src/app/tests/suites/certification/Test_TC_S_2_3.yaml @@ -676,6 +676,81 @@ tests: - name: "SceneID" value: 0x03 + - label: + "Step 6f: TH sends a StoreScene command to to group G1 with the + GroupID field set to G1 and the SceneID field set to 0x03." + PICS: S.S.C04.Rsp + command: "StoreScene" + groupId: G1 + arguments: + values: + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x03 + + - label: + "Step 6g: TH sends a ViewScene command to DUT with the GroupID field + set to G1 and the SceneID field set to 0x03." + PICS: S.S.C01.Rsp + command: "ViewScene" + arguments: + values: + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x03 + response: + values: + - name: "Status" + value: 0x00 + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x03 + + - label: + "Step 6h: TH sends a RemoveScene command to group G1 with the GroupID + field set to G1 and the SceneID field set to 0x03." + PICS: S.S.C02.Rsp + command: "RemoveScene" + groupId: G1 + arguments: + values: + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x03 + + - label: "Wait 1+ s to give CI time to process the RemoveScene command." + PICS: PICS_SDK_CI_ONLY + cluster: "DelayCommands" + command: "WaitForMs" + arguments: + values: + - name: "ms" + value: 1250 + + - label: + "Step 6i: TH sends a ViewScene command to DUT with the GroupID field + set to G1 and the SceneID field set to 0x03." + PICS: S.S.C01.Rsp + command: "ViewScene" + arguments: + values: + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x03 + response: + values: + - name: "Status" + value: 0x8b + - name: "GroupID" + value: G1 + - name: "SceneID" + value: 0x03 + - label: "Step 7a: TH sends a CopyScene command to DUT with the mode field set to 0x00, the group identifier from field set to G1, the scene diff --git a/src/app/util/attribute-metadata.h b/src/app/util/attribute-metadata.h index a380599a8f48b6..1a2b2fd04ed447 100644 --- a/src/app/util/attribute-metadata.h +++ b/src/app/util/attribute-metadata.h @@ -17,8 +17,8 @@ #pragma once +#include #include - #include /** @@ -158,6 +158,25 @@ struct EmberAfAttributeMetadata */ EmberAfAttributeMask mask; + /** + * Check wether this attribute is a boolean based on its type according to the spec. + */ + bool IsBoolean() const { return attributeType == ZCL_BOOLEAN_ATTRIBUTE_TYPE; } + + /** + * Check wether this attribute is signed based on its type according to the spec. + */ + bool IsSignedIntegerAttribute() const + { + return (attributeType >= ZCL_INT8S_ATTRIBUTE_TYPE && attributeType <= ZCL_INT64S_ATTRIBUTE_TYPE) || + attributeType == ZCL_TEMPERATURE_ATTRIBUTE_TYPE; + } + + /** + * Check whether this attribute has a define min and max. + */ + bool HasMinMax() const { return mask & ATTRIBUTE_MASK_MIN_MAX; } + /** * Check whether this attribute is nullable. */ diff --git a/src/app/util/attribute-table.cpp b/src/app/util/attribute-table.cpp index 211d2a19123dbd..ab59e56016e544 100644 --- a/src/app/util/attribute-table.cpp +++ b/src/app/util/attribute-table.cpp @@ -41,13 +41,6 @@ using namespace chip; using namespace chip::app; namespace { -// Zigbee spec says types between signed 8 bit and signed 64 bit -bool emberAfIsTypeSigned(EmberAfAttributeType dataType) -{ - return (dataType >= ZCL_INT8S_ATTRIBUTE_TYPE && dataType <= ZCL_INT64S_ATTRIBUTE_TYPE) || - dataType == ZCL_TEMPERATURE_ATTRIBUTE_TYPE; -} - /** * @brief Simple integer comparison function. * Compares two values of a known length as integers. @@ -386,7 +379,7 @@ Status emAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId at maxBytes = maxv.ptrToDefaultValue; } - bool isAttributeSigned = emberAfIsTypeSigned(metadata->attributeType); + bool isAttributeSigned = metadata->IsSignedIntegerAttribute(); bool isOutOfRange = emberAfCompareValues(minBytes, data, dataLen, isAttributeSigned) == 1 || emberAfCompareValues(maxBytes, data, dataLen, isAttributeSigned) == -1; diff --git a/src/app/util/mock/MockNodeConfig.h b/src/app/util/mock/MockNodeConfig.h index 10110604678c20..668d81478d4659 100644 --- a/src/app/util/mock/MockNodeConfig.h +++ b/src/app/util/mock/MockNodeConfig.h @@ -53,6 +53,7 @@ constexpr EmberAfAttributeMetadata DefaultAttributeMetadata(chip::AttributeId id struct MockAttributeConfig { MockAttributeConfig(AttributeId aId) : id(aId), attributeMetaData(internal::DefaultAttributeMetadata(aId)) {} + MockAttributeConfig(AttributeId aId, EmberAfAttributeMetadata metadata) : id(aId), attributeMetaData(metadata) {} MockAttributeConfig(AttributeId aId, EmberAfAttributeType type, EmberAfAttributeMask mask = ATTRIBUTE_MASK_WRITABLE | ATTRIBUTE_MASK_NULLABLE) : id(aId),