| /* |
| * 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 "app/server-cluster/ServerClusterInterface.h" |
| #include <pw_unit_test/framework.h> |
| |
| #include <app-common/zap-generated/ids/Attributes.h> |
| #include <app/ConcreteClusterPath.h> |
| #include <app/server-cluster/DefaultServerCluster.h> |
| #include <app/server-cluster/ServerClusterContext.h> |
| #include <app/server-cluster/testing/TestServerClusterContext.h> |
| #include <data-model-providers/codegen/ServerClusterInterfaceRegistry.h> |
| #include <lib/core/CHIPError.h> |
| #include <lib/core/DataModelTypes.h> |
| #include <lib/core/StringBuilderAdapters.h> |
| |
| #include <algorithm> |
| #include <cstdlib> |
| #include <random> |
| |
| using namespace chip; |
| using namespace chip::Test; |
| using namespace chip::app; |
| using namespace chip::app::DataModel; |
| using namespace chip::app::Clusters; |
| |
| namespace { |
| |
| constexpr chip::EndpointId kEp1 = 1; |
| constexpr chip::EndpointId kEp2 = 2; |
| constexpr chip::EndpointId kEp3 = 3; |
| constexpr chip::ClusterId kCluster1 = 1; |
| constexpr chip::ClusterId kCluster2 = 2; |
| constexpr chip::ClusterId kCluster3 = 3; |
| |
| class FakeServerClusterInterface : public DefaultServerCluster |
| { |
| public: |
| FakeServerClusterInterface(const ConcreteClusterPath & path) : DefaultServerCluster(path) {} |
| FakeServerClusterInterface(EndpointId endpoint, ClusterId cluster) : DefaultServerCluster({ endpoint, cluster }) {} |
| |
| DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, |
| AttributeValueEncoder & encoder) override |
| { |
| switch (request.path.mAttributeId) |
| { |
| case Globals::Attributes::FeatureMap::Id: |
| return encoder.Encode<uint32_t>(0); |
| case Globals::Attributes::ClusterRevision::Id: |
| return encoder.Encode<uint32_t>(123); |
| } |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| |
| bool HasContext() { return mContext != nullptr; } |
| |
| const ConcreteClusterPath & GetPath() const { return mPath; } |
| }; |
| |
| class MultiPathCluster : public DefaultServerCluster |
| { |
| public: |
| MultiPathCluster(Span<const ConcreteClusterPath> paths) : DefaultServerCluster(paths[0]), mActualPaths(paths) {} |
| |
| DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, |
| AttributeValueEncoder & encoder) override |
| { |
| return CHIP_ERROR_NOT_IMPLEMENTED; |
| } |
| |
| Span<const ConcreteClusterPath> GetPaths() const override { return mActualPaths; } |
| |
| private: |
| Span<const ConcreteClusterPath> mActualPaths; |
| }; |
| |
| class CannotStartUpCluster : public FakeServerClusterInterface |
| { |
| public: |
| CannotStartUpCluster(EndpointId endpoint, ClusterId id) : FakeServerClusterInterface(endpoint, id) {} |
| CHIP_ERROR Startup(ServerClusterContext & context) override { return CHIP_ERROR_BUSY; } |
| }; |
| |
| struct TestServerClusterInterfaceRegistry : public ::testing::Test |
| { |
| static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } |
| static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } |
| }; |
| |
| } // namespace |
| |
| TEST_F(TestServerClusterInterfaceRegistry, BasicTest) |
| { |
| ServerClusterInterfaceRegistry registry; |
| |
| FakeServerClusterInterface cluster1(kEp1, kCluster1); |
| FakeServerClusterInterface cluster2(kEp2, kCluster2); |
| FakeServerClusterInterface cluster3(kEp2, kCluster3); |
| |
| // there should be nothing registered to start with. |
| EXPECT_EQ(registry.Get({ kEp1, kCluster1 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp1, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster3 }), nullptr); |
| EXPECT_EQ(registry.Get({ kInvalidEndpointId, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp1, kInvalidClusterId }), nullptr); |
| |
| // registration of invalid values is not acceptable |
| { |
| // registration has NULL interface |
| // next is not null (meaning registration2 looks like already registered) |
| ServerClusterRegistration registration1(cluster1); |
| ServerClusterRegistration registration2(cluster2); |
| registration2.next = ®istration1; |
| EXPECT_EQ(registry.Register(registration2), CHIP_ERROR_INVALID_ARGUMENT); |
| |
| // invalid path in cluster |
| FakeServerClusterInterface invalidPathInterface(kInvalidEndpointId, kCluster1); |
| ServerClusterRegistration registration3(invalidPathInterface); |
| EXPECT_EQ(registry.Register(registration3), CHIP_ERROR_INVALID_ARGUMENT); |
| |
| // invalid path in cluster |
| FakeServerClusterInterface invalidPathInterface2(kEp1, kInvalidClusterId); |
| ServerClusterRegistration registration4(invalidPathInterface); |
| EXPECT_EQ(registry.Register(registration4), CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| |
| ServerClusterRegistration registration1(cluster1); |
| ServerClusterRegistration registration2(cluster2); |
| ServerClusterRegistration registration3(cluster3); |
| |
| // should be able to register |
| EXPECT_EQ(registry.Register(registration1), CHIP_NO_ERROR); |
| EXPECT_EQ(registry.Register(registration2), CHIP_NO_ERROR); |
| EXPECT_EQ(registry.Register(registration3), CHIP_NO_ERROR); |
| |
| // cannot register two implementations on the same path |
| { |
| FakeServerClusterInterface another1(kEp1, kCluster1); |
| ServerClusterRegistration anotherRegisration1(another1); |
| EXPECT_EQ(registry.Register(anotherRegisration1), CHIP_ERROR_DUPLICATE_KEY_ID); |
| } |
| |
| // Items can be found back |
| EXPECT_EQ(registry.Get({ kEp1, kCluster1 }), &cluster1); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster2 }), &cluster2); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster3 }), &cluster3); |
| |
| EXPECT_EQ(registry.Get({ kEp2, kCluster1 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp1, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp3, kCluster2 }), nullptr); |
| |
| // repeated calls work |
| EXPECT_EQ(registry.Get({ kEp1, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp1, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp1, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster1 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster1 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster1 }), nullptr); |
| |
| // remove registrations |
| EXPECT_EQ(registry.Unregister(&cluster2), CHIP_NO_ERROR); |
| EXPECT_EQ(registry.Unregister(&cluster2), CHIP_ERROR_NOT_FOUND); |
| |
| // Re-adding works |
| EXPECT_EQ(registry.Get({ kEp2, kCluster2 }), nullptr); |
| EXPECT_EQ(registry.Register(registration2), CHIP_NO_ERROR); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster2 }), &cluster2); |
| |
| // clean of an entire endpoint works |
| EXPECT_EQ(registry.Get({ kEp2, kCluster3 }), &cluster3); |
| registry.UnregisterAllFromEndpoint(kEp2); |
| EXPECT_EQ(registry.Get({ kEp1, kCluster1 }), &cluster1); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster3 }), nullptr); |
| |
| registry.UnregisterAllFromEndpoint(kEp1); |
| EXPECT_EQ(registry.Get({ kEp1, kCluster1 }), nullptr); |
| EXPECT_EQ(registry.Get({ kEp2, kCluster3 }), nullptr); |
| } |
| |
| TEST_F(TestServerClusterInterfaceRegistry, StressTest) |
| { |
| // make the test repeatable |
| srand(1234); |
| |
| std::vector<FakeServerClusterInterface> items; |
| std::vector<ServerClusterRegistration> registrations; |
| |
| static constexpr ClusterId kClusterTestCount = 200; |
| static constexpr EndpointId kEndpointTestCount = 10; |
| static constexpr size_t kTestIterations = 4; |
| |
| static_assert(kInvalidClusterId > kClusterTestCount, "Tests assume all clusters IDs [0...] are valid"); |
| static_assert(kTestIterations > 1, "Tests use different unregister methods. Need 2 or more passes."); |
| |
| items.reserve(kClusterTestCount); |
| for (ClusterId i = 0; i < kClusterTestCount; i++) |
| { |
| auto endpointId = static_cast<EndpointId>(rand() % kEndpointTestCount); |
| items.emplace_back(endpointId, i); |
| } |
| |
| for (ClusterId i = 0; i < kClusterTestCount; i++) |
| { |
| registrations.emplace_back(items[i]); |
| } |
| |
| ServerClusterInterfaceRegistry registry; |
| |
| for (size_t test = 0; test < kTestIterations; test++) |
| { |
| for (ClusterId i = 0; i < kClusterTestCount; i++) |
| { |
| ASSERT_EQ(registry.Register(registrations[i]), CHIP_NO_ERROR); |
| } |
| |
| // test that getters work |
| for (ClusterId cluster = 0; cluster < kClusterTestCount; cluster++) |
| { |
| for (EndpointId ep = 0; ep < kEndpointTestCount; ep++) |
| { |
| if (items[cluster].GetPath().mEndpointId == ep) |
| { |
| ASSERT_EQ(registry.Get({ ep, cluster }), &items[cluster]); |
| } |
| else |
| { |
| ASSERT_EQ(registry.Get({ ep, cluster }), nullptr); |
| } |
| } |
| } |
| |
| // clear endpoints. Stress test, unregister in different ways (bulk vs individual) |
| if (test % 2 == 1) |
| { |
| // shuffle unregister |
| std::vector<size_t> unregister_order; |
| unregister_order.reserve(kClusterTestCount); |
| for (size_t i = 0; i < kClusterTestCount; i++) |
| { |
| unregister_order.push_back(i); |
| } |
| |
| std::default_random_engine eng(static_cast<std::default_random_engine::result_type>(rand())); |
| std::shuffle(unregister_order.begin(), unregister_order.end(), eng); |
| |
| // unregister |
| for (auto cluster : unregister_order) |
| { |
| // item MUST exist and be accessible |
| ASSERT_EQ(registry.Get(items[cluster].GetPath()), &items[cluster]); |
| ASSERT_EQ(registry.Unregister(&items[cluster]), CHIP_NO_ERROR); |
| |
| // once unregistered, it is not there anymore |
| ASSERT_EQ(registry.Get(items[cluster].GetPath()), nullptr); |
| ASSERT_EQ(registry.Unregister(&items[cluster]), CHIP_ERROR_NOT_FOUND); |
| } |
| |
| // all endpoints should be clear |
| for (ClusterId cluster = 0; cluster < kClusterTestCount; cluster++) |
| { |
| for (EndpointId ep = 0; ep < kEndpointTestCount; ep++) |
| { |
| ASSERT_EQ(registry.Get({ ep, cluster }), nullptr); |
| } |
| } |
| } |
| else |
| { |
| // bulk unregister |
| for (EndpointId ep = 0; ep < kEndpointTestCount; ep++) |
| { |
| registry.UnregisterAllFromEndpoint(ep); |
| } |
| } |
| } |
| } |
| |
| TEST_F(TestServerClusterInterfaceRegistry, ClustersOnEndpoint) |
| { |
| std::vector<FakeServerClusterInterface> items; |
| std::vector<ServerClusterRegistration> registrations; |
| |
| static constexpr ClusterId kClusterTestCount = 200; |
| static constexpr EndpointId kEndpointTestCount = 10; |
| |
| static_assert(kInvalidClusterId > kClusterTestCount, "Tests assume all clusters IDs [0...] are valid"); |
| |
| items.reserve(kClusterTestCount); |
| for (ClusterId i = 0; i < kClusterTestCount; i++) |
| { |
| items.emplace_back(static_cast<EndpointId>(i % kEndpointTestCount), i); |
| } |
| for (ClusterId i = 0; i < kClusterTestCount; i++) |
| { |
| registrations.emplace_back(items[i]); |
| } |
| |
| ServerClusterInterfaceRegistry registry; |
| |
| // place the clusters on the respecitve endpoints |
| for (ClusterId i = 0; i < kClusterTestCount; i++) |
| { |
| ASSERT_EQ(registry.Register(registrations[i]), CHIP_NO_ERROR); |
| } |
| |
| // this IS implementation defined: we always register at "HEAD" so the listing is in |
| // INVERSE order of registering. |
| for (EndpointId ep = 0; ep < kEndpointTestCount; ep++) |
| { |
| // Move to the end since we iterate in reverse order |
| ClusterId expectedClusterId = ep + kEndpointTestCount * (kClusterTestCount / kEndpointTestCount); |
| if (expectedClusterId >= kClusterTestCount) |
| { |
| expectedClusterId -= kEndpointTestCount; |
| } |
| |
| // ensure that iteration happens exactly as we expect: reverse order and complete |
| for (const auto & clusterId : registry.ClustersOnEndpoint(ep)) |
| { |
| ASSERT_LT(expectedClusterId, kClusterTestCount); |
| |
| ServerClusterInterface * cluster = registry.Get({ ep, clusterId }); |
| ASSERT_NE(cluster, nullptr); |
| ASSERT_TRUE(cluster->PathsContains(ConcreteClusterPath(ep, expectedClusterId))); |
| expectedClusterId -= kEndpointTestCount; // next expected/registered cluster |
| } |
| |
| // Iterated through all : we overflowed and got a large number |
| ASSERT_GE(expectedClusterId, kClusterTestCount); |
| } |
| |
| // invalid index works and iteration on empty lists is ok |
| auto clusters = registry.ClustersOnEndpoint(kEndpointTestCount + 1); |
| ASSERT_EQ(clusters.begin(), clusters.end()); |
| } |
| |
| TEST_F(TestServerClusterInterfaceRegistry, Context) |
| { |
| FakeServerClusterInterface cluster1(kEp1, kCluster1); |
| FakeServerClusterInterface cluster2(kEp1, kCluster2); |
| FakeServerClusterInterface cluster3(kEp2, kCluster3); |
| |
| ServerClusterRegistration registration1(cluster1); |
| ServerClusterRegistration registration2(cluster2); |
| ServerClusterRegistration registration3(cluster3); |
| |
| { |
| ServerClusterInterfaceRegistry registry; |
| EXPECT_FALSE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_FALSE(cluster3.HasContext()); |
| |
| // registry is NOT initialized |
| EXPECT_EQ(registry.Register(registration1), CHIP_NO_ERROR); |
| EXPECT_FALSE(cluster1.HasContext()); |
| |
| // set up the registry |
| TestServerClusterContext context; |
| EXPECT_EQ(registry.SetContext(context.Create()), CHIP_NO_ERROR); |
| |
| EXPECT_TRUE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_FALSE(cluster3.HasContext()); |
| |
| // adding clusters automatically adds the context |
| EXPECT_EQ(registry.Register(registration2), CHIP_NO_ERROR); |
| EXPECT_TRUE(cluster2.HasContext()); |
| |
| // clearing the context clears all clusters |
| registry.ClearContext(); |
| EXPECT_FALSE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_FALSE(cluster3.HasContext()); |
| |
| EXPECT_EQ(registry.SetContext(context.Create()), CHIP_NO_ERROR); |
| EXPECT_TRUE(cluster1.HasContext()); |
| EXPECT_TRUE(cluster2.HasContext()); |
| EXPECT_FALSE(cluster3.HasContext()); |
| |
| EXPECT_EQ(registry.Register(registration3), CHIP_NO_ERROR); |
| EXPECT_TRUE(cluster3.HasContext()); |
| |
| // removing clears the context/shuts clusters down |
| EXPECT_EQ(registry.Unregister(&cluster2), CHIP_NO_ERROR); |
| EXPECT_TRUE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_TRUE(cluster3.HasContext()); |
| |
| // re-setting context works |
| EXPECT_EQ(registry.SetContext(context.Create()), CHIP_NO_ERROR); |
| EXPECT_TRUE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_TRUE(cluster3.HasContext()); |
| |
| // also not valid, but different |
| TestServerClusterContext otherContext; |
| |
| EXPECT_EQ(registry.SetContext(otherContext.Create()), CHIP_NO_ERROR); |
| EXPECT_TRUE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_TRUE(cluster3.HasContext()); |
| |
| // Removing an entire endpoint clears the context for clusters (shuts them down) |
| registry.UnregisterAllFromEndpoint(kEp1); |
| EXPECT_FALSE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_TRUE(cluster3.HasContext()); |
| } |
| |
| // destructor clears the context |
| EXPECT_FALSE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| EXPECT_FALSE(cluster3.HasContext()); |
| } |
| |
| TEST_F(TestServerClusterInterfaceRegistry, MultiPathRegistration) |
| { |
| const std::array<ConcreteClusterPath, 4> kTestPaths{ { |
| { 15, 100 }, |
| { 15, 88 }, |
| { 15, 20 }, |
| { 15, 33 }, |
| } }; |
| MultiPathCluster cluster(kTestPaths); |
| ServerClusterRegistration registration(cluster); |
| |
| ServerClusterInterfaceRegistry registry; |
| ASSERT_EQ(registry.Register(registration), CHIP_NO_ERROR); |
| |
| for (auto & p : kTestPaths) |
| { |
| ASSERT_EQ(registry.Get(p), &cluster); |
| } |
| |
| // some things not there... |
| ASSERT_EQ(registry.Get({ 1, 20 }), nullptr); |
| ASSERT_EQ(registry.Get({ 1, 200 }), nullptr); |
| ASSERT_EQ(registry.Get({ 3, 200 }), nullptr); |
| ASSERT_EQ(registry.Get({ 4, 33 }), nullptr); |
| |
| // Verify listing works |
| ServerClusterInterfaceRegistry::ClustersList clusters = registry.ClustersOnEndpoint(15); |
| auto it = clusters.begin(); |
| |
| for (auto & p : kTestPaths) |
| { |
| ASSERT_NE(it, clusters.end()); |
| ASSERT_EQ(*it, p.mClusterId); |
| ++it; |
| } |
| ASSERT_EQ(it, clusters.end()); |
| |
| ASSERT_EQ(registry.Unregister(&cluster), CHIP_NO_ERROR); |
| for (auto & p : kTestPaths) |
| { |
| ASSERT_EQ(registry.Get(p), nullptr); |
| } |
| } |
| |
| TEST_F(TestServerClusterInterfaceRegistry, RejectDifferentEndpointPaths) |
| { |
| { |
| const std::array<ConcreteClusterPath, 2> kTestPaths{ { |
| { 1, 100 }, |
| { 2, 88 }, |
| } }; |
| MultiPathCluster cluster(kTestPaths); |
| ServerClusterRegistration registration(cluster); |
| |
| ServerClusterInterfaceRegistry registry; |
| ASSERT_EQ(registry.Register(registration), CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| |
| { |
| const std::array<ConcreteClusterPath, 3> kTestPaths{ { |
| { 1, 100 }, |
| { 1, 200 }, |
| { 3, 100 }, |
| } }; |
| MultiPathCluster cluster(kTestPaths); |
| ServerClusterRegistration registration(cluster); |
| |
| ServerClusterInterfaceRegistry registry; |
| ASSERT_EQ(registry.Register(registration), CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| } |
| |
| TEST_F(TestServerClusterInterfaceRegistry, StartupErrors) |
| { |
| FakeServerClusterInterface cluster1(kEp1, kCluster1); |
| CannotStartUpCluster cluster2(kEp2, kCluster2); |
| |
| ServerClusterRegistration registration1(cluster1); |
| ServerClusterRegistration registration2(cluster2); |
| |
| { |
| ServerClusterInterfaceRegistry registry; |
| EXPECT_FALSE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| |
| // register without context works because startup not called yet |
| EXPECT_EQ(registry.Register(registration1), CHIP_NO_ERROR); |
| EXPECT_EQ(registry.Register(registration2), CHIP_NO_ERROR); |
| |
| EXPECT_FALSE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| |
| TestServerClusterContext context; |
| EXPECT_EQ(registry.SetContext(context.Create()), CHIP_ERROR_HAD_FAILURES); |
| EXPECT_TRUE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| |
| registry.ClearContext(); |
| EXPECT_FALSE(cluster1.HasContext()); |
| EXPECT_FALSE(cluster2.HasContext()); |
| } |
| } |
| |
| TEST_F(TestServerClusterInterfaceRegistry, LazyRegistrationTest) |
| { |
| LazyRegisteredServerCluster<FakeServerClusterInterface> obj; |
| |
| EXPECT_FALSE(obj.IsConstructed()); |
| |
| obj.Create(kEp1, kCluster1); |
| EXPECT_TRUE(obj.IsConstructed()); |
| EXPECT_EQ(obj.Cluster().GetPath(), ConcreteClusterPath(kEp1, kCluster1)); |
| EXPECT_EQ(obj.Registration().serverClusterInterface, &obj.Cluster()); |
| obj.Destroy(); |
| EXPECT_FALSE(obj.IsConstructed()); |
| |
| obj.Create(kEp2, kCluster3); |
| EXPECT_TRUE(obj.IsConstructed()); |
| EXPECT_EQ(obj.Cluster().GetPath(), ConcreteClusterPath(kEp2, kCluster3)); |
| EXPECT_EQ(obj.Registration().serverClusterInterface, &obj.Cluster()); |
| obj.Destroy(); |
| EXPECT_FALSE(obj.IsConstructed()); |
| } |