blob: d9e63c2b5702db2af3ab711b1779cecda1784558 [file] [log] [blame]
/*
* Copyright (c) 2025 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 <pw_unit_test/framework.h>
#include <access/Privilege.h>
#include <app/ConcreteClusterPath.h>
#include <app/data-model-provider/MetadataTypes.h>
#include <app/server-cluster/DefaultServerCluster.h>
#include <app/server-cluster/ServerClusterExtension.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <clusters/shared/GlobalIds.h>
#include <lib/core/CHIPError.h>
#include <lib/core/StringBuilderAdapters.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/ReadOnlyBuffer.h>
#include <protocols/interaction_model/Constants.h>
#include <app/server-cluster/testing/AttributeTesting.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <app/server-cluster/testing/ValidateGlobalAttributes.h>
#include <string>
namespace {
using namespace chip;
using namespace chip::app;
using namespace chip::app::DataModel;
using namespace chip::Testing;
using namespace chip::Protocols::InteractionModel;
constexpr uint32_t kMockRevision = 123;
constexpr uint32_t kMockFeatureMap = 0x112233;
/// a basic mock cluster, just supporting one path
class MockServerCluster : public DefaultServerCluster
{
public:
MockServerCluster(const ConcreteClusterPath & path) : DefaultServerCluster(path) {}
DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder) override
{
switch (request.path.mAttributeId)
{
case chip::app::Clusters::Globals::Attributes::ClusterRevision::Id:
return encoder.Encode(kMockRevision);
case chip::app::Clusters::Globals::Attributes::FeatureMap::Id:
return encoder.Encode(kMockFeatureMap);
default:
return Status::UnsupportedAttribute;
}
}
};
/// A dual path server cluster (just mocks GetPaths, nothing else)
class MockDualPathServerCluster : public DefaultServerCluster
{
public:
MockDualPathServerCluster(const ConcreteClusterPath & p1, const ConcreteClusterPath & p2) :
DefaultServerCluster(p1), mPaths{ p1, p2 }
{}
// mock GetPaths, to have something
Span<const ConcreteClusterPath> GetPaths() const override { return Span(mPaths); }
DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder) override
{
switch (request.path.mAttributeId)
{
case chip::app::Clusters::Globals::Attributes::ClusterRevision::Id:
return encoder.Encode(kMockRevision);
case chip::app::Clusters::Globals::Attributes::FeatureMap::Id:
return encoder.Encode(kMockFeatureMap);
default:
return Status::UnsupportedAttribute;
}
}
private:
const ConcreteClusterPath mPaths[2] = {};
};
constexpr AttributeId kTestAttribute1 = 0xFFF10000;
constexpr AttributeId kTestAttribute2 = 0xFFF10001;
constexpr DataModel::AttributeEntry kExtraAttributeMetadata[] = {
{ kTestAttribute1, {} /* qualities */, Access::Privilege::kView /* readPriv */, Access::Privilege::kOperate /* writePriv */ },
{ kTestAttribute2, {} /* qualities */, Access::Privilege::kView /* readPriv */, std::nullopt /* writePriv */ },
};
class TestableServerClusterExtension : public ServerClusterExtension
{
public:
TestableServerClusterExtension(const ConcreteClusterPath & path, ServerClusterInterface & underlying) :
ServerClusterExtension(path, underlying)
{}
DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder) override
{
if (mClusterPath == request.path)
{
switch (request.path.mAttributeId)
{
case kTestAttribute1:
return encoder.Encode<CharSpan>({ mStringAttribute.data(), mStringAttribute.size() });
case kTestAttribute2:
return encoder.Encode<uint32_t>(1234);
}
}
return mUnderlying.ReadAttribute(request, encoder);
}
CHIP_ERROR Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) override
{
if (path == mClusterPath)
{
ReturnErrorOnFailure(builder.ReferenceExisting(kExtraAttributeMetadata));
}
// Delegate to underlying for other paths
return mUnderlying.Attributes(path, builder);
}
DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request,
AttributeValueDecoder & decoder) override
{
if (mClusterPath == request.path)
{
// we are guaranteed to only be called for writable attributes
if (request.path.mAttributeId == kTestAttribute1)
{
CharSpan value;
ReturnErrorOnFailure(decoder.Decode(value));
mStringAttribute = std::string(value.data(), value.size());
NotifyAttributeChanged(kTestAttribute1);
return Status::Success;
}
}
return mUnderlying.WriteAttribute(request, decoder);
}
void TestNotifyAttributeChanged(AttributeId id) { NotifyAttributeChanged(id); }
private:
std::string mStringAttribute = "Sample String";
};
struct TestServerClusterExtension : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
};
TEST_F(TestServerClusterExtension, TestExtensionPath)
{
const ConcreteClusterPath mockPath = { 1, 2 };
MockServerCluster underlying(mockPath);
const ConcreteClusterPath extensionPath = { 1, 2 };
ServerClusterExtension extension(extensionPath, underlying);
// The extension should return the paths of its underlying interface.
ASSERT_EQ(extension.GetPaths().size(), 1u);
ASSERT_EQ(extension.GetPaths()[0], mockPath);
}
TEST_F(TestServerClusterExtension, TestGetDataVersion)
{
const ConcreteClusterPath mockPath = { 2, 3 };
MockServerCluster underlying(mockPath);
TestableServerClusterExtension extension(mockPath, underlying);
// Initially, version is the same as underlying (since mVersionDelta is 0).
ASSERT_EQ(extension.GetDataVersion(mockPath), underlying.GetDataVersion(mockPath));
// Without a context, there is no need to increment (and cannot mark dirty).
extension.TestNotifyAttributeChanged(4);
ASSERT_EQ(extension.GetDataVersion(mockPath),
underlying.GetDataVersion(mockPath)); // Should still be the same as no context is set
// Set a context and then notify change. This time mVersionDelta should increment.
TestServerClusterContext context;
ASSERT_EQ(extension.Startup(context.Get()), CHIP_NO_ERROR);
DataVersion oldVersion = extension.GetDataVersion(mockPath);
extension.TestNotifyAttributeChanged(5);
ASSERT_EQ(extension.GetDataVersion(mockPath), oldVersion + 1);
}
TEST_F(TestServerClusterExtension, TestNotifyAttributeChangedWithContext)
{
const ConcreteClusterPath mockPath = { 1, 2 };
const ConcreteClusterPath mockPath2 = { 1, 3 };
MockDualPathServerCluster underlying(mockPath, mockPath2);
TestableServerClusterExtension extension(mockPath, underlying);
// Verify that NotifyAttributeChanged does NOT mark dirty when no context is set.
extension.TestNotifyAttributeChanged(123);
// No context set, so no dirty list updates. Default TestServerClusterContext has an empty list.
// We cannot easily check mVersionDelta directly as it's protected in ServerClusterExtension.
// The effect of mVersionDelta is visible through GetDataVersion.
// Create a ServerClusterContext and verify that attribute change notifications are processed.
TestServerClusterContext context;
ASSERT_EQ(extension.Startup(context.Get()), CHIP_NO_ERROR);
// Clear any previous dirty marks from calls before Startup.
context.ChangeListener().DirtyList().clear();
DataVersion oldVersion = extension.GetDataVersion(mockPath);
extension.TestNotifyAttributeChanged(234);
ASSERT_EQ(extension.GetDataVersion(mockPath), oldVersion + 1);
ASSERT_EQ(extension.GetDataVersion(mockPath2), oldVersion);
ASSERT_EQ(context.ChangeListener().DirtyList().size(), 1u);
ASSERT_EQ(context.ChangeListener().DirtyList()[0], AttributePathParams(mockPath.mEndpointId, mockPath.mClusterId, 234));
}
TEST_F(TestServerClusterExtension, TestExtensionAttributes)
{
const ConcreteClusterPath mockPath = { 1, 2 };
MockServerCluster underlying(mockPath);
TestableServerClusterExtension extension(mockPath, underlying);
chip::Testing::ClusterTester tester(extension);
// Verify attribute listing includes global and extended attributes
ASSERT_TRUE(chip::Testing::IsAttributesListEqualTo(extension, { kExtraAttributeMetadata[0], kExtraAttributeMetadata[1] }));
// Test reading kTestAttribute1 (string)
CharSpan readString;
ASSERT_EQ(tester.ReadAttribute(kTestAttribute1, readString), Status::Success);
ASSERT_TRUE(readString.data_equal("Sample String"_span));
// Test reading kTestAttribute2 (uint32_t)
uint32_t readUint{};
ASSERT_EQ(tester.ReadAttribute(kTestAttribute2, readUint), Status::Success);
ASSERT_EQ(readUint, 1234u);
// Test writing kTestAttribute1 (string)
CharSpan newString = "New Test String"_span;
ASSERT_EQ(tester.WriteAttribute(kTestAttribute1, newString), Status::Success);
// Read back to verify write
CharSpan verifiedString;
ASSERT_EQ(tester.ReadAttribute(kTestAttribute1, verifiedString), Status::Success);
ASSERT_TRUE(verifiedString.data_equal(newString));
}
} // namespace