diff --git a/src/app/server-cluster/ServerClusterInterface.h b/src/app/server-cluster/ServerClusterInterface.h index 086a37cbab529b..53460f763547f6 100644 --- a/src/app/server-cluster/ServerClusterInterface.h +++ b/src/app/server-cluster/ServerClusterInterface.h @@ -43,7 +43,12 @@ class ServerClusterInterface virtual ~ServerClusterInterface() = default; ///////////////////////////////////// Cluster Metadata Support ////////////////////////////////////////////////// - [[nodiscard]] virtual ClusterId GetClusterId() const = 0; + + /// The path where this cluster operates on. + /// + /// This path (endpointid,clusterid) is expected to be fixed once the server + /// cluster interface is in use. + [[nodiscard]] virtual ConcreteClusterPath GetPath() const = 0; /// Gets the data version for this cluster instance. /// diff --git a/src/app/server-cluster/tests/TestDefaultServerCluster.cpp b/src/app/server-cluster/tests/TestDefaultServerCluster.cpp index a44791625d4a5b..299408f7d0ae60 100644 --- a/src/app/server-cluster/tests/TestDefaultServerCluster.cpp +++ b/src/app/server-cluster/tests/TestDefaultServerCluster.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -44,9 +45,9 @@ namespace { class FakeDefaultServerCluster : public DefaultServerCluster { public: - FakeDefaultServerCluster(ClusterId id) : mClusterId(id) {} + FakeDefaultServerCluster(ConcreteClusterPath path) : mPath(path) {} - ClusterId GetClusterId() const override { return mClusterId; } + [[nodiscard]] ConcreteClusterPath GetPath() const override { return mPath; } DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) override @@ -64,14 +65,14 @@ class FakeDefaultServerCluster : public DefaultServerCluster void TestIncreaseDataVersion() { IncreaseDataVersion(); } private: - ClusterId mClusterId; + ConcreteClusterPath mPath; }; } // namespace TEST(TestDefaultServerCluster, TestDataVersion) { - FakeDefaultServerCluster cluster(1); + FakeDefaultServerCluster cluster({ 1, 2 }); DataVersion v1 = cluster.GetDataVersion(); cluster.TestIncreaseDataVersion(); @@ -80,13 +81,13 @@ TEST(TestDefaultServerCluster, TestDataVersion) TEST(TestDefaultServerCluster, TestFlagsDefault) { - FakeDefaultServerCluster cluster(1); + FakeDefaultServerCluster cluster({ 1, 2 }); ASSERT_EQ(cluster.GetClusterFlags().Raw(), 0u); } TEST(TestDefaultServerCluster, AttributesDefault) { - FakeDefaultServerCluster cluster(1); + FakeDefaultServerCluster cluster({ 1, 2 }); DataModel::ListBuilder attributes; @@ -114,7 +115,7 @@ TEST(TestDefaultServerCluster, AttributesDefault) TEST(TestDefaultServerCluster, CommandsDefault) { - FakeDefaultServerCluster cluster(1); + FakeDefaultServerCluster cluster({ 1, 2 }); DataModel::ListBuilder acceptedCommands; ASSERT_EQ(cluster.AcceptedCommands({ 1, 1 }, acceptedCommands), CHIP_NO_ERROR); @@ -127,7 +128,7 @@ TEST(TestDefaultServerCluster, CommandsDefault) TEST(TestDefaultServerCluster, WriteAttributeDefault) { - FakeDefaultServerCluster cluster(1); + FakeDefaultServerCluster cluster({ 1, 2 }); WriteOperation test(0 /* endpoint */, 1 /* cluster */, 1234 /* attribute */); test.SetSubjectDescriptor(kAdminSubjectDescriptor); @@ -140,7 +141,7 @@ TEST(TestDefaultServerCluster, WriteAttributeDefault) TEST(TestDefaultServerCluster, InvokeDefault) { - FakeDefaultServerCluster cluster(1); + FakeDefaultServerCluster cluster({ 1, 2 }); TLV::TLVReader tlvReader; InvokeRequest request; diff --git a/src/data-model-providers/codegen/BUILD.gn b/src/data-model-providers/codegen/BUILD.gn index 3b414c595b9f67..d6517dea8366c0 100644 --- a/src/data-model-providers/codegen/BUILD.gn +++ b/src/data-model-providers/codegen/BUILD.gn @@ -45,3 +45,17 @@ source_set("instance-header") { # generally being unit tests or data_model.gni/data_model.cmake files) sources = [ "Instance.h" ] } + +source_set("registry") { + sources = [ + "ServerClusterInterfaceRegistry.cpp", + "ServerClusterInterfaceRegistry.h", + ] + + public_deps = [ + "${chip_root}/src/app:paths", + "${chip_root}/src/app/server-cluster", + "${chip_root}/src/lib/core:types", + "${chip_root}/src/lib/support", + ] +} diff --git a/src/data-model-providers/codegen/ServerClusterInterfaceRegistry.cpp b/src/data-model-providers/codegen/ServerClusterInterfaceRegistry.cpp new file mode 100644 index 00000000000000..65aad7a53c986a --- /dev/null +++ b/src/data-model-providers/codegen/ServerClusterInterfaceRegistry.cpp @@ -0,0 +1,162 @@ +/* + * 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 + +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { + +ServerClusterInterfaceRegistry::~ServerClusterInterfaceRegistry() +{ + while (mRegistrations != nullptr) + { + ServerClusterRegistration * next = mRegistrations->next; + mRegistrations->next = nullptr; + mRegistrations = next; + } +} + +CHIP_ERROR ServerClusterInterfaceRegistry::Register(ServerClusterRegistration & entry) +{ + // we have no strong way to check if entry is already registered somewhere else, so we use "next" as some + // form of double-check + VerifyOrReturnError(entry.next == nullptr, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(entry.serverClusterInterface != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + ConcreteClusterPath path = entry.serverClusterInterface->GetPath(); + + VerifyOrReturnError(path.HasValidIds(), CHIP_ERROR_INVALID_ARGUMENT); + + // Double-checking for duplicates makes the checks O(n^2) on the total number of registered + // items. We preserve this however we may want to make this optional at some point in time. + VerifyOrReturnError(Get(path) == nullptr, CHIP_ERROR_DUPLICATE_KEY_ID); + + entry.next = mRegistrations; + mRegistrations = &entry; + + return CHIP_NO_ERROR; +} + +ServerClusterInterface * ServerClusterInterfaceRegistry::Unregister(const ConcreteClusterPath & path) +{ + ServerClusterRegistration * prev = nullptr; + ServerClusterRegistration * current = mRegistrations; + + while (current != nullptr) + { + if (current->serverClusterInterface->GetPath() == path) + { + // take the item out of the current list and return it. + ServerClusterRegistration * next = current->next; + + if (prev == nullptr) + { + mRegistrations = next; + } + else + { + prev->next = next; + } + + if (mCachedInterface == current->serverClusterInterface) + { + mCachedInterface = nullptr; + } + + current->next = nullptr; // some clearing + return current->serverClusterInterface; + } + + prev = current; + current = current->next; + } + + // Not found. + return nullptr; +} + +ServerClusterInterfaceRegistry::ClustersList ServerClusterInterfaceRegistry::ClustersOnEndpoint(EndpointId endpointId) +{ + return { mRegistrations, endpointId }; +} + +void ServerClusterInterfaceRegistry::UnregisterAllFromEndpoint(EndpointId endpointId) +{ + ServerClusterRegistration * prev = nullptr; + ServerClusterRegistration * current = mRegistrations; + while (current != nullptr) + { + if (current->serverClusterInterface->GetPath().mEndpointId == endpointId) + { + if (mCachedInterface == current->serverClusterInterface) + { + mCachedInterface = nullptr; + } + if (prev == nullptr) + { + mRegistrations = current->next; + } + else + { + prev->next = current->next; + } + ServerClusterRegistration * actual_next = current->next; + current->next = nullptr; // some clearing + current = actual_next; + } + else + { + prev = current; + current = current->next; + } + } +} + +ServerClusterInterface * ServerClusterInterfaceRegistry::Get(const ConcreteClusterPath & path) +{ + // Check the cache to speed things up + if ((mCachedInterface != nullptr) && (mCachedInterface->GetPath() == path)) + { + return mCachedInterface; + } + + // The cluster searched for is not cached, do a linear search for it + ServerClusterRegistration * current = mRegistrations; + + while (current != nullptr) + { + if (current->serverClusterInterface->GetPath() == path) + { + mCachedInterface = current->serverClusterInterface; + return mCachedInterface; + } + + current = current->next; + } + + // not found + return nullptr; +} + +} // namespace app +} // namespace chip diff --git a/src/data-model-providers/codegen/ServerClusterInterfaceRegistry.h b/src/data-model-providers/codegen/ServerClusterInterfaceRegistry.h new file mode 100644 index 00000000000000..34300eaae35401 --- /dev/null +++ b/src/data-model-providers/codegen/ServerClusterInterfaceRegistry.h @@ -0,0 +1,132 @@ +/* + * 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. + */ +#pragma once + +#include +#include +#include +#include + +namespace chip { +namespace app { + +/// Represents an entry in the server cluster interface registry for +/// a specific interface. +/// +/// A single-linked list element +struct ServerClusterRegistration +{ + // A single-linked list of clusters registered for the given `endpointId` + ServerClusterInterface * serverClusterInterface = nullptr; + ServerClusterRegistration * next = nullptr; + + constexpr ServerClusterRegistration() = default; + constexpr ServerClusterRegistration(ServerClusterInterface * interface, ServerClusterRegistration * next_item = nullptr) : + serverClusterInterface(interface), next(next_item) + {} +}; + +/// Allows registering and retrieving ServerClusterInterface instances for specific cluster paths. +class ServerClusterInterfaceRegistry +{ +public: + /// represents an iterable list of clusters + class ClustersList + { + public: + class Iterator + { + public: + Iterator(ServerClusterRegistration * interface, EndpointId endpoint) : mRegistration(interface), mEndpointId(endpoint) + { + AdvanceUntilMatchingEndpoint(); + } + + Iterator & operator++() + { + if (mRegistration != nullptr) + { + mRegistration = mRegistration->next; + } + AdvanceUntilMatchingEndpoint(); + return *this; + } + bool operator==(const Iterator & other) const { return mRegistration == other.mRegistration; } + bool operator!=(const Iterator & other) const { return mRegistration != other.mRegistration; } + ServerClusterInterface * operator*() { return mRegistration->serverClusterInterface; } + + private: + ServerClusterRegistration * mRegistration; + EndpointId mEndpointId; + + void AdvanceUntilMatchingEndpoint() + { + while ((mRegistration != nullptr) && (mRegistration->serverClusterInterface->GetPath().mEndpointId != mEndpointId)) + { + mRegistration = mRegistration->next; + } + } + }; + + constexpr ClustersList(ServerClusterRegistration * start, EndpointId endpointId) : mStart(start), mEndpointId(endpointId) {} + Iterator begin() { return { mStart, mEndpointId }; } + Iterator end() { return { nullptr, mEndpointId }; } + + private: + ServerClusterRegistration * mStart; + EndpointId mEndpointId; + }; + + ~ServerClusterInterfaceRegistry(); + + /// Add the given entry to the registry. + /// + /// Requirements: + /// - entry MUST NOT be part of any other registration + /// - LIFETIME of entry must outlive the Registry (or entry must be unregistered) + /// + /// There can be only a single registration for a given `endpointId/clusterId` path. + [[nodiscard]] CHIP_ERROR Register(ServerClusterRegistration & entry); + + /// Remove an existing registration for a given endpoint/cluster path. + /// + /// Returns the previous registration if any exists (or nullptr if nothing + /// to unregister) + ServerClusterInterface * Unregister(const ConcreteClusterPath & path); + + /// Return the interface registered for the given cluster path or nullptr if one does not exist + ServerClusterInterface * Get(const ConcreteClusterPath & path); + + /// Provides a list of clusters that are registered for the given endpoint. + /// + /// As ClustersList points inside the internal registrations of the registry, + /// the list is only valid as long as the registry is not modified. + ClustersList ClustersOnEndpoint(EndpointId endpointId); + + /// Unregister all registrations for the given endpoint. + void UnregisterAllFromEndpoint(EndpointId endpointId); + +private: + ServerClusterRegistration * mRegistrations = nullptr; + + // A one-element cache to speed up finding a cluster within an endpoint. + // The endpointId specifies which endpoint the cache belongs to. + ServerClusterInterface * mCachedInterface = nullptr; +}; + +} // namespace app +} // namespace chip diff --git a/src/data-model-providers/codegen/model.gni b/src/data-model-providers/codegen/model.gni index 51305a5fe30b5a..f9a8295fcb8593 100644 --- a/src/data-model-providers/codegen/model.gni +++ b/src/data-model-providers/codegen/model.gni @@ -39,6 +39,8 @@ codegen_data_model_SOURCES = [ codegen_data_model_PUBLIC_DEPS = [ "${chip_root}/src/app/common:attribute-type", "${chip_root}/src/app/data-model-provider", + "${chip_root}/src/app/server-cluster", "${chip_root}/src/data-model-providers/codegen:instance-header", + "${chip_root}/src/data-model-providers/codegen:registry", "${chip_root}/src/app/util/persistence", ] diff --git a/src/data-model-providers/codegen/tests/BUILD.gn b/src/data-model-providers/codegen/tests/BUILD.gn index 57facbee09ec10..a2d4fdec2cbee1 100644 --- a/src/data-model-providers/codegen/tests/BUILD.gn +++ b/src/data-model-providers/codegen/tests/BUILD.gn @@ -54,6 +54,7 @@ chip_test_suite("tests") { test_sources = [ "TestCodegenModelViaMocks.cpp", "TestEmberAttributeDataBuffer.cpp", + "TestServerClusterInterfaceRegistry.cpp", ] cflags = [ "-Wconversion" ] diff --git a/src/data-model-providers/codegen/tests/TestServerClusterInterfaceRegistry.cpp b/src/data-model-providers/codegen/tests/TestServerClusterInterfaceRegistry.cpp new file mode 100644 index 00000000000000..7d6c972db16bac --- /dev/null +++ b/src/data-model-providers/codegen/tests/TestServerClusterInterfaceRegistry.cpp @@ -0,0 +1,322 @@ +/* + * 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 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace chip; +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(EndpointId endpoint, ClusterId cluster) : mPath({ endpoint, cluster }) {} + FakeServerClusterInterface(const ConcreteClusterPath & path) : mPath(path) {} + + [[nodiscard]] ConcreteClusterPath GetPath() const override { return mPath; } + + DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, + AttributeValueEncoder & encoder) override + { + switch (request.path.mAttributeId) + { + case Globals::Attributes::FeatureMap::Id: + return encoder.Encode(0); + case Globals::Attributes::ClusterRevision::Id: + return encoder.Encode(123); + } + return CHIP_ERROR_INVALID_ARGUMENT; + } + +private: + ConcreteClusterPath mPath; +}; + +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 + ServerClusterRegistration registration; + EXPECT_EQ(registry.Register(registration), CHIP_ERROR_INVALID_ARGUMENT); + + // next is not null (meaning registration2 looks like already registered) + ServerClusterRegistration registration2(&cluster1, ®istration); + 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({ kEp2, kCluster2 }), &cluster2); + EXPECT_EQ(registry.Unregister({ kEp2, kCluster2 }), nullptr); + + // 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 items; + std::vector 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(rand() % kEndpointTestCount); + items.emplace_back(endpointId, i); + } + + registrations.reserve(kClusterTestCount); + 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 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(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].GetPath()), &items[cluster]); + + // once unregistered, it is not there anymore + ASSERT_EQ(registry.Get(items[cluster].GetPath()), nullptr); + ASSERT_EQ(registry.Unregister(items[cluster].GetPath()), nullptr); + } + } + else + { + // bulk unregister + for (EndpointId ep = 0; ep < kEndpointTestCount; ep++) + { + registry.UnregisterAllFromEndpoint(ep); + } + } + + // 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); + } + } + } +} + +TEST_F(TestServerClusterInterfaceRegistry, ClustersOnEndpoint) +{ + std::vector items; + std::vector 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(i % kEndpointTestCount), i); + } + registrations.reserve(kClusterTestCount); + 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 (auto cluster : registry.ClustersOnEndpoint(ep)) + { + ASSERT_LT(expectedClusterId, kClusterTestCount); + ASSERT_EQ(cluster->GetPath(), 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()); +}