blob: 8be9d2980a991858ca4c6bdcfc4d416d94b701a2 [file] [log] [blame]
/*
* Copyright (c) 2025 Project CHIP Authors
*
* 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/clusters/chime-server/ChimeCluster.h>
#include <clusters/Chime/Metadata.h>
#include <pw_unit_test/framework.h>
#include <app/DefaultSafeAttributePersistenceProvider.h>
#include <app/SafeAttributePersistenceProvider.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <app/server-cluster/testing/AttributeTesting.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <lib/support/ReadOnlyBuffer.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::Chime;
using namespace chip::Testing;
namespace {
constexpr EndpointId kTestEndpointId = 1;
class MockChimeDelegate : public ChimeDelegate
{
public:
struct MockChimeSound
{
uint8_t id;
std::string name;
};
std::vector<MockChimeSound> sounds = { { 1, "Ding Dong" }, { 2, "Ring Ring" } };
bool playChimeSoundCalled = false;
Protocols::InteractionModel::Status playChimeSoundStatus = Protocols::InteractionModel::Status::Success;
CHIP_ERROR getChimeSoundByIndexError = CHIP_NO_ERROR;
CHIP_ERROR GetChimeSoundByIndex(uint8_t chimeIndex, uint8_t & chimeID, MutableCharSpan & name) override
{
if (getChimeSoundByIndexError != CHIP_NO_ERROR)
{
return getChimeSoundByIndexError;
}
if (chimeIndex < sounds.size())
{
chimeID = sounds[chimeIndex].id;
return CopyCharSpanToMutableCharSpan(CharSpan(sounds[chimeIndex].name.c_str(), sounds[chimeIndex].name.length()), name);
}
return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED;
}
CHIP_ERROR GetChimeIDByIndex(uint8_t chimeIndex, uint8_t & chimeID) override
{
if (chimeIndex < sounds.size())
{
chimeID = sounds[chimeIndex].id;
return CHIP_NO_ERROR;
}
return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED;
}
Protocols::InteractionModel::Status PlayChimeSound() override
{
playChimeSoundCalled = true;
return playChimeSoundStatus;
}
};
struct TestChimeCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { Platform::MemoryShutdown(); }
void SetUp() override
{
VerifyOrDie(mPersistenceProvider.Init(&mClusterTester.GetServerClusterContext().storage) == CHIP_NO_ERROR);
app::SetSafeAttributePersistenceProvider(&mPersistenceProvider);
EXPECT_EQ(mCluster.Startup(mClusterTester.GetServerClusterContext()), CHIP_NO_ERROR);
}
void TearDown() override { app::SetSafeAttributePersistenceProvider(nullptr); }
MockChimeDelegate mMockDelegate;
ChimeCluster mCluster{ kTestEndpointId, mMockDelegate };
ClusterTester mClusterTester{ mCluster };
app::DefaultSafeAttributePersistenceProvider mPersistenceProvider;
};
TEST_F(TestChimeCluster, TestAttributesList)
{
ReadOnlyBufferBuilder<DataModel::AttributeEntry> listBuilder;
EXPECT_EQ(mCluster.Attributes(ConcreteClusterPath(kTestEndpointId, Chime::Id), listBuilder), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> expectedListBuilder;
AttributeListBuilder attributeListBuilder(expectedListBuilder);
EXPECT_EQ(attributeListBuilder.Append(Span(Chime::Attributes::kMandatoryMetadata), {}), CHIP_NO_ERROR);
EXPECT_TRUE(EqualAttributeSets(listBuilder.TakeBuffer(), expectedListBuilder.TakeBuffer()));
}
TEST_F(TestChimeCluster, TestAcceptedCommands)
{
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> listBuilder;
EXPECT_EQ(mCluster.AcceptedCommands(ConcreteClusterPath(kTestEndpointId, Chime::Id), listBuilder), CHIP_NO_ERROR);
static constexpr DataModel::AcceptedCommandEntry kExpectedCommands[] = {
Chime::Commands::PlayChimeSound::kMetadataEntry,
};
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> expectedListBuilder;
EXPECT_EQ(expectedListBuilder.ReferenceExisting(kExpectedCommands), CHIP_NO_ERROR);
EXPECT_TRUE(EqualAcceptedCommandSets(listBuilder.TakeBuffer(), expectedListBuilder.TakeBuffer()));
}
TEST_F(TestChimeCluster, TestDelegateErrors)
{
// Test 1: PlayChimeSound delegate failure
mMockDelegate.playChimeSoundStatus = Protocols::InteractionModel::Status::Busy;
auto result = mClusterTester.Invoke<Commands::PlayChimeSound::Type>(Commands::PlayChimeSound::Type());
EXPECT_FALSE(result.IsSuccess());
EXPECT_TRUE(mMockDelegate.playChimeSoundCalled);
if (result.status.has_value())
{
EXPECT_EQ(result.status.value().GetStatusCode().GetStatus(), Protocols::InteractionModel::Status::Busy);
}
// Test 2: GetChimeSoundByIndex delegate failure
mMockDelegate.getChimeSoundByIndexError = CHIP_ERROR_INTERNAL;
Attributes::InstalledChimeSounds::TypeInfo::DecodableType list;
EXPECT_NE(mClusterTester.ReadAttribute(Attributes::InstalledChimeSounds::Id, list), CHIP_NO_ERROR);
}
TEST_F(TestChimeCluster, TestNoOpWrites)
{
// 1. Write Initial Value (Change)
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::SelectedChime::Id, static_cast<uint8_t>(1)), CHIP_NO_ERROR);
auto & dirtyList = mClusterTester.GetDirtyList();
EXPECT_EQ(dirtyList.size(), 1u);
dirtyList.clear();
// 2. Write Same Value (No-Op)
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::SelectedChime::Id, static_cast<uint8_t>(1)), CHIP_NO_ERROR);
EXPECT_EQ(dirtyList.size(), 0u);
// 3. Write Enabled Initial (Change)
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::Enabled::Id, false), CHIP_NO_ERROR);
EXPECT_EQ(dirtyList.size(), 1u);
dirtyList.clear();
// 4. Write Enabled Same (No-Op)
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::Enabled::Id, false), CHIP_NO_ERROR);
EXPECT_EQ(dirtyList.size(), 0u);
}
TEST_F(TestChimeCluster, TestReadClusterRevision)
{
uint16_t clusterRevision = 0;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::ClusterRevision::Id, clusterRevision), CHIP_NO_ERROR);
EXPECT_EQ(clusterRevision, kRevision);
}
TEST_F(TestChimeCluster, TestReadFeatureMap)
{
uint32_t featureMap = 1;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, 0u);
}
TEST_F(TestChimeCluster, TestReadSelectedChime)
{
uint8_t selectedChime = 1;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::SelectedChime::Id, selectedChime), CHIP_NO_ERROR);
EXPECT_EQ(selectedChime, 0);
}
TEST_F(TestChimeCluster, TestReadEnabled)
{
bool enabled = false;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::Enabled::Id, enabled), CHIP_NO_ERROR);
EXPECT_EQ(enabled, true);
}
TEST_F(TestChimeCluster, TestInstalledChimeSoundsLifecycle)
{
// 1. Verify initial content (TestReadInstalledChimeSounds)
Attributes::InstalledChimeSounds::TypeInfo::DecodableType list;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::InstalledChimeSounds::Id, list), CHIP_NO_ERROR);
auto it = list.begin();
ASSERT_TRUE(it.Next());
EXPECT_EQ(it.GetValue().chimeID, 1);
EXPECT_TRUE(it.GetValue().name.data_equal(CharSpan("Ding Dong", 9)));
ASSERT_TRUE(it.Next());
EXPECT_EQ(it.GetValue().chimeID, 2);
EXPECT_TRUE(it.GetValue().name.data_equal(CharSpan("Ring Ring", 9)));
ASSERT_FALSE(it.Next());
// 2. Change content
mMockDelegate.sounds.push_back({ 3, "New Sound" });
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::InstalledChimeSounds::Id, list), CHIP_NO_ERROR);
it = list.begin();
ASSERT_TRUE(it.Next());
EXPECT_EQ(it.GetValue().chimeID, 1);
ASSERT_TRUE(it.Next());
EXPECT_EQ(it.GetValue().chimeID, 2);
ASSERT_TRUE(it.Next());
EXPECT_EQ(it.GetValue().chimeID, 3);
EXPECT_TRUE(it.GetValue().name.data_equal(CharSpan("New Sound", 9)));
ASSERT_FALSE(it.Next());
}
TEST_F(TestChimeCluster, TestWriteAttributes)
{
// Test writing SelectedChime
// Write valid value (1)
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::SelectedChime::Id, static_cast<uint8_t>(1)), CHIP_NO_ERROR);
uint8_t selectedChime = 0;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::SelectedChime::Id, selectedChime), CHIP_NO_ERROR);
EXPECT_EQ(selectedChime, 1);
// Write valid value (2)
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::SelectedChime::Id, static_cast<uint8_t>(2)), CHIP_NO_ERROR);
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::SelectedChime::Id, selectedChime), CHIP_NO_ERROR);
EXPECT_EQ(selectedChime, 2);
// Write invalid value (3) - should fail with NotFound as per spec
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::SelectedChime::Id, static_cast<uint8_t>(3)),
Protocols::InteractionModel::Status::NotFound);
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::SelectedChime::Id, selectedChime), CHIP_NO_ERROR);
EXPECT_EQ(selectedChime, 2); // Should remain unchanged
// Test writing Enabled
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::Enabled::Id, false), CHIP_NO_ERROR);
bool enabled = true;
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::Enabled::Id, enabled), CHIP_NO_ERROR);
EXPECT_EQ(enabled, false);
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::Enabled::Id, true), CHIP_NO_ERROR);
EXPECT_EQ(mClusterTester.ReadAttribute(Attributes::Enabled::Id, enabled), CHIP_NO_ERROR);
EXPECT_EQ(enabled, true);
}
TEST_F(TestChimeCluster, TestPersistence)
{
TestServerClusterContext context;
app::DefaultSafeAttributePersistenceProvider persistenceProvider;
EXPECT_EQ(persistenceProvider.Init(&context.Get().storage), CHIP_NO_ERROR);
app::SetSafeAttributePersistenceProvider(&persistenceProvider);
MockChimeDelegate mockDelegate;
// 1. Initial startup, verify default values
{
ChimeCluster cluster(kTestEndpointId, mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint8_t selectedChime = 1;
EXPECT_EQ(tester.ReadAttribute(Attributes::SelectedChime::Id, selectedChime), CHIP_NO_ERROR);
EXPECT_EQ(selectedChime, 0);
bool enabled = false;
EXPECT_EQ(tester.ReadAttribute(Attributes::Enabled::Id, enabled), CHIP_NO_ERROR);
EXPECT_EQ(enabled, true);
// Modify values
EXPECT_EQ(tester.WriteAttribute(Attributes::SelectedChime::Id, static_cast<uint8_t>(2)), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(Attributes::Enabled::Id, false), CHIP_NO_ERROR);
}
// 2. Restart (create new cluster instance), verify modified values are loaded
{
ChimeCluster cluster(kTestEndpointId, mockDelegate);
EXPECT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR);
chip::Testing::ClusterTester tester(cluster);
uint8_t selectedChime;
EXPECT_EQ(tester.ReadAttribute(Attributes::SelectedChime::Id, selectedChime), CHIP_NO_ERROR);
EXPECT_EQ(selectedChime, 2);
bool enabled;
EXPECT_EQ(tester.ReadAttribute(Attributes::Enabled::Id, enabled), CHIP_NO_ERROR);
EXPECT_EQ(enabled, false);
}
}
TEST_F(TestChimeCluster, TestPlayChimeSound)
{
// 1. Test PlayChimeSound when Enabled is true (default)
auto result = mClusterTester.Invoke<Commands::PlayChimeSound::Type>(Commands::PlayChimeSound::Type());
EXPECT_TRUE(result.IsSuccess());
EXPECT_TRUE(mMockDelegate.playChimeSoundCalled);
// 2. Test PlayChimeSound when Enabled is false
mMockDelegate.playChimeSoundCalled = false;
EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::Enabled::Id, false), CHIP_NO_ERROR);
result = mClusterTester.Invoke<Commands::PlayChimeSound::Type>(Commands::PlayChimeSound::Type());
EXPECT_TRUE(result.IsSuccess());
EXPECT_FALSE(mMockDelegate.playChimeSoundCalled);
}
} // namespace