/* * * Copyright (c) 2021 Project CHIP Authors * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include "app-common/zap-generated/ids/Attributes.h" #include "app-common/zap-generated/ids/Clusters.h" #include "lib/core/TLVTags.h" #include "lib/core/TLVWriter.h" #include "protocols/interaction_model/Constants.h" #include "system/SystemPacketBuffer.h" #include "system/TLVPacketBufferBackingStore.h" #include #include #include #include #include #include #include #include #include using namespace chip::app; using namespace chip; namespace { struct AttributeInstruction { enum AttributeType { kAttributeA = 0, // int kAttributeB = 1, // byte string kAttributeC = 2, // struct kAttributeD = 3, // list }; enum ValueType { kData = 0, kStatus = 1 }; AttributeType mAttributeType; EndpointId mEndpointId; ValueType mValueType; uint8_t mInstructionId; AttributeInstruction() { mInstructionId = sInstructionId++; } bool operator<(const AttributeInstruction & instruction) const { return (mAttributeType < instruction.mAttributeType || (!(mAttributeType < instruction.mAttributeType) && (mEndpointId < instruction.mEndpointId))); } AttributeInstruction(AttributeType attributeType, EndpointId endpointId, ValueType valueType) : AttributeInstruction() { mAttributeType = attributeType; mEndpointId = endpointId; mValueType = valueType; } AttributeId GetAttributeId() const { switch (mAttributeType) { case kAttributeA: return Clusters::UnitTesting::Attributes::Int16u::Id; break; case kAttributeB: return Clusters::UnitTesting::Attributes::OctetString::Id; break; case kAttributeC: return Clusters::UnitTesting::Attributes::StructAttr::Id; break; default: return Clusters::UnitTesting::Attributes::ListStructOctetString::Id; break; } } ConcreteAttributePath GetAttributePath() const { return ConcreteAttributePath(mEndpointId, Clusters::UnitTesting::Id, GetAttributeId()); } static uint8_t sInstructionId; }; uint8_t AttributeInstruction::sInstructionId = 0; using AttributeInstructionListType = std::vector; using TestClusterStateCache = chip::Test::AppContext; class ForwardedDataCallbackValidator final { public: void SetExpectation(TLV::TLVReader & aData, EndpointId endpointId, AttributeInstruction::AttributeType attributeType) { auto length = aData.GetRemainingLength(); std::vector buffer(aData.GetReadPoint(), aData.GetReadPoint() + length); if (!mExpectedBuffers.empty() && endpointId == mLastEndpointId && attributeType == mLastAttributeType) { // For overriding test, the last buffered data is removed. mExpectedBuffers.pop_back(); } mExpectedBuffers.push_back(buffer); mLastEndpointId = endpointId; mLastAttributeType = attributeType; } void SetExpectation() { mExpectedBuffers.clear(); } void ValidateData(TLV::TLVReader & aData, bool isListOperation) { EXPECT_FALSE(mExpectedBuffers.empty()); if (!mExpectedBuffers.empty() > 0) { auto buffer = mExpectedBuffers.front(); mExpectedBuffers.erase(mExpectedBuffers.begin()); uint32_t length = static_cast(buffer.size()); if (isListOperation) { // List operation will attach end of container EXPECT_LT(length, aData.GetRemainingLength()); } else { EXPECT_EQ(length, aData.GetRemainingLength()); } if (length <= aData.GetRemainingLength() && length > 0) { EXPECT_EQ(memcmp(aData.GetReadPoint(), buffer.data(), length), 0); if (memcmp(aData.GetReadPoint(), buffer.data(), length) != 0) { ChipLogProgress(DataManagement, "Failed"); } } } } void ValidateNoData() { EXPECT_TRUE(mExpectedBuffers.empty()); } private: std::vector> mExpectedBuffers; EndpointId mLastEndpointId; AttributeInstruction::AttributeType mLastAttributeType; }; class DataSeriesGenerator { public: DataSeriesGenerator(ReadClient::Callback * readCallback, AttributeInstructionListType & instructionList) : mReadCallback(readCallback), mInstructionList(instructionList) {} void Generate(ForwardedDataCallbackValidator & dataCallbackValidator); private: ReadClient::Callback * mReadCallback; AttributeInstructionListType & mInstructionList; }; void DataSeriesGenerator::Generate(ForwardedDataCallbackValidator & dataCallbackValidator) { ReadClient::Callback * callback = mReadCallback; StatusIB status; callback->OnReportBegin(); for (auto & instruction : mInstructionList) { ConcreteDataAttributePath path(instruction.mEndpointId, Clusters::UnitTesting::Id, 0); Platform::ScopedMemoryBufferWithSize handle; handle.Calloc(3000); TLV::ScopedBufferTLVWriter writer(std::move(handle), 3000); status = StatusIB(); path.mAttributeId = instruction.GetAttributeId(); path.mDataVersion.SetValue(1); ChipLogProgress(DataManagement, "\t -- Generating Instruction ID %d", instruction.mInstructionId); if (instruction.mValueType == AttributeInstruction::kData) { switch (instruction.mAttributeType) { case AttributeInstruction::kAttributeA: { ChipLogProgress(DataManagement, "\t -- Generating A"); Clusters::UnitTesting::Attributes::Int16u::TypeInfo::Type value = instruction.mInstructionId; EXPECT_EQ(DataModel::Encode(writer, TLV::AnonymousTag(), value), CHIP_NO_ERROR); break; } case AttributeInstruction::kAttributeB: { ChipLogProgress(DataManagement, "\t -- Generating B"); Clusters::UnitTesting::Attributes::OctetString::TypeInfo::Type value; uint8_t buf[] = { 'h', 'e', 'l', 'l', 'o' }; value = buf; EXPECT_EQ(DataModel::Encode(writer, TLV::AnonymousTag(), value), CHIP_NO_ERROR); break; } case AttributeInstruction::kAttributeC: { ChipLogProgress(DataManagement, "\t -- Generating C"); Clusters::UnitTesting::Attributes::StructAttr::TypeInfo::Type value; value.a = instruction.mInstructionId; value.b = true; EXPECT_EQ(DataModel::Encode(writer, TLV::AnonymousTag(), value), CHIP_NO_ERROR); break; } case AttributeInstruction::kAttributeD: { ChipLogProgress(DataManagement, "\t -- Generating D"); // buf[200] is 1.6k Clusters::UnitTesting::Structs::TestListStructOctet::Type buf[200]; for (auto & i : buf) { i.member1 = instruction.mInstructionId; } Clusters::UnitTesting::Attributes::ListStructOctetString::TypeInfo::Type value; path.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll; value = buf; EXPECT_EQ(DataModel::Encode(writer, TLV::AnonymousTag(), value), CHIP_NO_ERROR); break; } default: break; } uint32_t writtenLength = writer.GetLengthWritten(); writer.Finalize(handle); TLV::ScopedBufferTLVReader reader; reader.Init(std::move(handle), writtenLength); EXPECT_EQ(reader.Next(), CHIP_NO_ERROR); dataCallbackValidator.SetExpectation(reader, instruction.mEndpointId, instruction.mAttributeType); callback->OnAttributeData(path, &reader, status); } else { ChipLogProgress(DataManagement, "\t -- Generating Status"); status.mStatus = Protocols::InteractionModel::Status::Failure; dataCallbackValidator.SetExpectation(); callback->OnAttributeData(path, nullptr, status); } } callback->OnReportEnd(); } class CacheValidator : public ClusterStateCache::Callback { public: CacheValidator(AttributeInstructionListType & instructionList, ForwardedDataCallbackValidator & dataCallbackValidator); Clusters::UnitTesting::Attributes::TypeInfo::DecodableType clusterValue; private: void OnDone(ReadClient *) override {} void OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) override { ChipLogProgress(DataManagement, "\t\t -- Validating OnAttributeData callback"); // Ensure that the provided path is one that we're expecting to find auto iter = mExpectedAttributes.find(aPath); ASSERT_NE(iter, mExpectedAttributes.end()); if (aStatus.IsSuccess()) { // Verify that the apData is passed as nonnull ASSERT_NE(apData, nullptr); if (apData) { mDataCallbackValidator.ValidateData(*apData, aPath.IsListOperation()); } } else { mDataCallbackValidator.ValidateNoData(); } } void DecodeAttribute(const AttributeInstruction & instruction, const ConcreteAttributePath & path, ClusterStateCache * cache) { CHIP_ERROR err; bool gotStatus = false; ChipLogProgress(DataManagement, "\t\t -- Validating Instruction ID: %d", instruction.mInstructionId); switch (instruction.mAttributeType) { case AttributeInstruction::kAttributeA: { ChipLogProgress(DataManagement, "\t\t -- Validating A"); Clusters::UnitTesting::Attributes::Int16u::TypeInfo::DecodableType v = 0; err = cache->Get(path, v); if (err == CHIP_ERROR_IM_STATUS_CODE_RECEIVED) { gotStatus = true; err = CHIP_NO_ERROR; } else { EXPECT_EQ(err, CHIP_NO_ERROR); EXPECT_EQ(v, instruction.mInstructionId); } break; } case AttributeInstruction::kAttributeB: { ChipLogProgress(DataManagement, "\t\t -- Validating B"); Clusters::UnitTesting::Attributes::OctetString::TypeInfo::DecodableType v; err = cache->Get(path, v); if (err == CHIP_ERROR_IM_STATUS_CODE_RECEIVED) { gotStatus = true; err = CHIP_NO_ERROR; } else { EXPECT_EQ(err, CHIP_NO_ERROR); EXPECT_EQ(strncmp((char *) v.data(), "hello", v.size()), 0); } break; } case AttributeInstruction::kAttributeC: { ChipLogProgress(DataManagement, "\t\t -- Validating C"); Clusters::UnitTesting::Attributes::StructAttr::TypeInfo::DecodableType v; err = cache->Get(path, v); if (err == CHIP_ERROR_IM_STATUS_CODE_RECEIVED) { gotStatus = true; err = CHIP_NO_ERROR; } else { EXPECT_EQ(v.a, instruction.mInstructionId); EXPECT_TRUE(v.b); } break; } case AttributeInstruction::kAttributeD: { ChipLogProgress(DataManagement, "\t\t -- Validating D"); Clusters::UnitTesting::Attributes::ListStructOctetString::TypeInfo::DecodableType v; err = cache->Get(path, v); if (err == CHIP_ERROR_IM_STATUS_CODE_RECEIVED) { gotStatus = true; err = CHIP_NO_ERROR; } else { auto listIter = v.begin(); while (listIter.Next()) { EXPECT_EQ(listIter.GetValue().member1, instruction.mInstructionId); } EXPECT_EQ(listIter.GetStatus(), CHIP_NO_ERROR); } break; } } EXPECT_EQ(err, CHIP_NO_ERROR); if (gotStatus) { ChipLogProgress(DataManagement, "\t\t -- Validating status"); EXPECT_EQ(instruction.mValueType, AttributeInstruction::kStatus); } } void DecodeClusterObject(const AttributeInstruction & instruction, const ConcreteAttributePath & path, ClusterStateCache * cache) { std::list statusList; EXPECT_EQ(cache->Get(path.mEndpointId, path.mClusterId, clusterValue, statusList), CHIP_NO_ERROR); if (instruction.mValueType == AttributeInstruction::kData) { EXPECT_EQ(statusList.size(), 0u); switch (instruction.mAttributeType) { case AttributeInstruction::kAttributeA: ChipLogProgress(DataManagement, "\t\t -- Validating A (Cluster Obj)"); EXPECT_EQ(clusterValue.int16u, instruction.mInstructionId); break; case AttributeInstruction::kAttributeB: ChipLogProgress(DataManagement, "\t\t -- Validating B (Cluster Obj)"); EXPECT_EQ(strncmp((char *) clusterValue.octetString.data(), "hello", clusterValue.octetString.size()), 0); break; case AttributeInstruction::kAttributeC: ChipLogProgress(DataManagement, "\t\t -- Validating C (Cluster Obj)"); EXPECT_EQ(clusterValue.structAttr.a, instruction.mInstructionId); EXPECT_TRUE(clusterValue.structAttr.b); break; case AttributeInstruction::kAttributeD: ChipLogProgress(DataManagement, "\t\t -- Validating D (Cluster Obj)"); auto listIter = clusterValue.listStructOctetString.begin(); while (listIter.Next()) { EXPECT_EQ(listIter.GetValue().member1, instruction.mInstructionId); } EXPECT_EQ(listIter.GetStatus(), CHIP_NO_ERROR); break; } } else { EXPECT_EQ(statusList.size(), 1u); auto status = statusList.front(); EXPECT_EQ(status.mPath.mEndpointId, instruction.mEndpointId); EXPECT_EQ(status.mPath.mClusterId, Clusters::UnitTesting::Id); EXPECT_EQ(status.mPath.mAttributeId, instruction.GetAttributeId()); EXPECT_EQ(status.mStatus.mStatus, Protocols::InteractionModel::Status::Failure); } } void OnAttributeChanged(ClusterStateCache * cache, const ConcreteAttributePath & path) override { StatusIB status; // Ensure that the provided path is one that we're expecting to find auto iter = mExpectedAttributes.find(path); ASSERT_NE(iter, mExpectedAttributes.end()); // Once retrieved, let's erase it from the expected set so that we can catch duplicates coming back // as well as validating that we've seen all attributes at the end. mExpectedAttributes.erase(iter); for (auto & instruction : mInstructionSet) { if (instruction.mEndpointId == path.mEndpointId && instruction.GetAttributeId() == path.mAttributeId && path.mClusterId == Clusters::UnitTesting::Id) { // // Validate both decoding into attribute objects as well as // cluster objects. // DecodeAttribute(instruction, path, cache); DecodeClusterObject(instruction, path, cache); } } } void OnClusterChanged(ClusterStateCache * cache, EndpointId endpointId, ClusterId clusterId) override { auto iter = mExpectedClusters.find(std::make_tuple(endpointId, clusterId)); ASSERT_NE(iter, mExpectedClusters.end()); mExpectedClusters.erase(iter); } void OnEndpointAdded(ClusterStateCache * cache, EndpointId endpointId) override { auto iter = mExpectedEndpoints.find(endpointId); ASSERT_NE(iter, mExpectedEndpoints.end()); mExpectedEndpoints.erase(iter); } void OnReportEnd() override { EXPECT_EQ(mExpectedAttributes.size(), 0u); EXPECT_EQ(mExpectedClusters.size(), 0u); EXPECT_EQ(mExpectedEndpoints.size(), 0u); } // // We use sets for tracking most of the expected data since we're expecting // unique data items being provided in the callbacks. // std::set mInstructionSet; std::set mExpectedAttributes; std::set> mExpectedClusters; std::set mExpectedEndpoints; ForwardedDataCallbackValidator & mDataCallbackValidator; }; CacheValidator::CacheValidator(AttributeInstructionListType & instructionList, ForwardedDataCallbackValidator & dataCallbackValidator) : mDataCallbackValidator(dataCallbackValidator) { for (auto & instruction : instructionList) { // // We need to replace a matching instruction with the latest one we see in the list to ensure we get // the instruction with the highest InstructionID. Hence the erase and insert (i.e replace) operation. // mInstructionSet.erase(instruction); mInstructionSet.insert(instruction); mExpectedAttributes.insert( ConcreteAttributePath(instruction.mEndpointId, Clusters::UnitTesting::Id, instruction.GetAttributeId())); mExpectedClusters.insert(std::make_tuple(instruction.mEndpointId, Clusters::UnitTesting::Id)); mExpectedEndpoints.insert(instruction.mEndpointId); } } void RunAndValidateSequence(AttributeInstructionListType list) { ForwardedDataCallbackValidator dataCallbackValidator; CacheValidator client(list, dataCallbackValidator); ClusterStateCache cache(client); // In order for the cache to track our data versions, we need to claim to it // that we are dealing with a wildcard path. And we need to do that before // it has seen any reports. AttributePathParams wildcardPath; const Span pathSpan(&wildcardPath, 1); { // Just need a buffer big enough that we can start the list. We don't // care about the actual data versions here. uint8_t buf[20]; TLV::TLVWriter writer; writer.Init(buf); DataVersionFilterIBs::Builder builder; EXPECT_EQ(builder.Init(&writer), CHIP_NO_ERROR); bool encodedDataVersionList = false; // We had nothing to encode so far. EXPECT_EQ(cache.GetBufferedCallback().OnUpdateDataVersionFilterList(builder, pathSpan, encodedDataVersionList), CHIP_NO_ERROR); EXPECT_FALSE(encodedDataVersionList); } DataSeriesGenerator generator(&cache.GetBufferedCallback(), list); generator.Generate(dataCallbackValidator); // Now verify that we would do the right thing when encoding our data // versions. size_t bufferSize = 1; do { Platform::ScopedMemoryBuffer buf; ASSERT_TRUE(buf.Calloc(bufferSize)); TLV::TLVWriter writer; writer.Init(buf.Get(), bufferSize); DataVersionFilterIBs::Builder builder; CHIP_ERROR err = builder.Init(&writer); EXPECT_TRUE(err == CHIP_NO_ERROR || err == CHIP_ERROR_BUFFER_TOO_SMALL); if (err == CHIP_NO_ERROR) { // We had enough space to start the list. Now try encoding the data // version filters. bool encodedDataVersionList = false; // We should be rolling back properly if we run out of space. EXPECT_EQ(cache.GetBufferedCallback().OnUpdateDataVersionFilterList(builder, pathSpan, encodedDataVersionList), CHIP_NO_ERROR); EXPECT_EQ(builder.GetError(), CHIP_NO_ERROR); if (writer.GetRemainingFreeLength() > 40) { // We have lots of empty space left, so we did not end up // needing to roll back; no point testing larger buffer sizes. // // Note: we may still have encodedDataVersionList false here, if // there were no non-status attribute values cached. break; } } ++bufferSize; } while (true); // Now check clearing behavior. First for attributes. ConcreteAttributePath firstAttr = list[0].GetAttributePath(); TLV::TLVReader reader; CHIP_ERROR err = cache.Get(firstAttr, reader); // Should have gotten a value or status for now. EXPECT_NE(err, CHIP_ERROR_KEY_NOT_FOUND); cache.ClearAttribute(firstAttr); err = cache.Get(firstAttr, reader); // Should have gotten no value. EXPECT_EQ(err, CHIP_ERROR_KEY_NOT_FOUND); // Now clearing for clusters. First check that things that should be there are. for (auto & listItem : list) { ConcreteAttributePath path = listItem.GetAttributePath(); if (path == firstAttr) { // We removed this one already. continue; } err = cache.Get(path, reader); // Should have gotten a value or status for now. EXPECT_NE(err, CHIP_ERROR_KEY_NOT_FOUND); } auto firstCluster = ConcreteClusterPath(firstAttr); cache.ClearAttributes(firstCluster); for (auto & listItem : list) { ConcreteAttributePath path = listItem.GetAttributePath(); err = cache.Get(path, reader); if (ConcreteClusterPath(path) == firstCluster) { EXPECT_EQ(err, CHIP_ERROR_KEY_NOT_FOUND); } else { // Should still have a value or status EXPECT_NE(err, CHIP_ERROR_KEY_NOT_FOUND); } } // Now clearing for endpoints. First check that things that should be there are. // TODO: Since all our attributes have the same cluster, this is not // actually testing anything useful right now. for (auto & listItem : list) { ConcreteAttributePath path = listItem.GetAttributePath(); if (ConcreteClusterPath(path) == firstCluster) { // We removed this one already. continue; } err = cache.Get(path, reader); // Should have gotten a value or status for now. EXPECT_NE(err, CHIP_ERROR_KEY_NOT_FOUND); } auto firstEndpoint = firstAttr.mEndpointId; cache.ClearAttributes(firstEndpoint); for (auto & listItem : list) { ConcreteAttributePath path = listItem.GetAttributePath(); err = cache.Get(path, reader); if (path.mEndpointId == firstEndpoint) { EXPECT_EQ(err, CHIP_ERROR_KEY_NOT_FOUND); } else { // Should still have a value or status EXPECT_NE(err, CHIP_ERROR_KEY_NOT_FOUND); } } } /* * This validates the cache by issuing different sequences of attribute combinations * and ensuring that the latest view in the cache matches up with expectations. * * The print statements indicate the expected output. * * The legend is as follows: * * E1:A1 --- Endpoint 1, Attribute A, Version 1 * */ TEST_F(TestClusterStateCache, TestCache) { ChipLogProgress(DataManagement, "Validating various sequences of attribute data IBs..."); // // Validate a range of types and ensure that they can be successfully decoded. // ChipLogProgress(DataManagement, "E1:A1 --> E1:A1"); RunAndValidateSequence({ AttributeInstruction( AttributeInstruction::kAttributeA, 1, AttributeInstruction::kData) }); ChipLogProgress(DataManagement, "E1:B1 --> E1:B1"); RunAndValidateSequence({ AttributeInstruction( AttributeInstruction::kAttributeB, 1, AttributeInstruction::kData) }); ChipLogProgress(DataManagement, "E1:C1 --> E1:C1"); RunAndValidateSequence({ AttributeInstruction(AttributeInstruction::kAttributeC, 1, AttributeInstruction::kData) }); ChipLogProgress(DataManagement, "E1:D1 --> E1:D1"); RunAndValidateSequence({ AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kData) }); // // Validate that a newer version of a data item over-rides the // previous copy. // ChipLogProgress(DataManagement, "E1:D1 E1:D2 --> E1:D2"); RunAndValidateSequence({ AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kData), AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kData) }); // // Validate that a newer StatusIB over-rides a previous data value. // ChipLogProgress(DataManagement, "E1:D1 E1:D2s --> E1:D2s"); RunAndValidateSequence({ AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kData), AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kStatus) }); // // Validate that a newer data value over-rides a previous status value. // ChipLogProgress(DataManagement, "E1:D1s E1:D2 --> E1:D2"); RunAndValidateSequence({ AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kStatus), AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kData) }); // // Validate data across different endpoints. // ChipLogProgress(DataManagement, "E0:D1 E1:D2 --> E0:D1 E1:D2"); RunAndValidateSequence({ AttributeInstruction(AttributeInstruction::kAttributeD, 0, AttributeInstruction::kData), AttributeInstruction(AttributeInstruction::kAttributeD, 1, AttributeInstruction::kData) }); ChipLogProgress(DataManagement, "E0:A1 E0:B2 E0:A3 E0:B4 --> E0:A3 E0:B4"); RunAndValidateSequence({ AttributeInstruction(AttributeInstruction::kAttributeA, 0, AttributeInstruction::kData), AttributeInstruction(AttributeInstruction::kAttributeB, 0, AttributeInstruction::kData), AttributeInstruction(AttributeInstruction::kAttributeA, 0, AttributeInstruction::kData), AttributeInstruction(AttributeInstruction::kAttributeB, 0, AttributeInstruction::kData) }); } } // namespace