blob: 3deda0dc2045e75aba435a1d4a02bf810b012d8f [file]
/*
*
* Copyright (c) 2026 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/clusters/service-area-server/ServiceAreaCluster.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <clusters/ServiceArea/Attributes.h>
#include <clusters/ServiceArea/Metadata.h>
#include <clusters/shared/Attributes.h>
#include <lib/core/CHIPError.h>
#include <lib/support/CHIPMem.h>
#include <pw_unit_test/framework.h>
#include <vector>
#include <cstring>
namespace chip {
namespace app {
namespace Clusters {
namespace ServiceArea {
namespace Testing {
using namespace chip::app::Clusters::Globals;
using namespace chip::app::Clusters::ServiceArea;
using namespace chip::Protocols::InteractionModel;
namespace {
constexpr EndpointId kTestEndpoint = 1;
class InMemoryStorageDelegate : public StorageDelegate
{
public:
uint32_t GetNumberOfSupportedAreas() override { return static_cast<uint32_t>(mSupportedAreas.size()); }
bool GetSupportedAreaByIndex(uint32_t listIndex, AreaStructureWrapper & aSupportedArea) override
{
if (listIndex >= mSupportedAreas.size())
{
return false;
}
aSupportedArea = mSupportedAreas[listIndex];
return true;
}
bool AddSupportedAreaRaw(const AreaStructureWrapper & newArea, uint32_t & listIndex) override
{
if (mSupportedAreas.size() >= kMaxNumSupportedAreas)
{
return false;
}
mSupportedAreas.push_back(newArea);
listIndex = static_cast<uint32_t>(mSupportedAreas.size() - 1);
return true;
}
bool ModifySupportedAreaRaw(uint32_t listIndex, const AreaStructureWrapper & modifiedArea) override
{
if (listIndex >= mSupportedAreas.size() || modifiedArea.areaID != mSupportedAreas[listIndex].areaID)
{
return false;
}
mSupportedAreas[listIndex] = modifiedArea;
return true;
}
bool ClearSupportedAreasRaw() override
{
if (mSupportedAreas.empty())
{
return false;
}
mSupportedAreas.clear();
return true;
}
bool RemoveSupportedAreaRaw(uint32_t areaId) override
{
for (auto it = mSupportedAreas.begin(); it != mSupportedAreas.end(); ++it)
{
if (it->areaID == areaId)
{
mSupportedAreas.erase(it);
return true;
}
}
return false;
}
uint32_t GetNumberOfSupportedMaps() override { return static_cast<uint32_t>(mSupportedMaps.size()); }
bool GetSupportedMapByIndex(uint32_t listIndex, MapStructureWrapper & aSupportedMap) override
{
if (listIndex >= mSupportedMaps.size())
{
return false;
}
aSupportedMap = mSupportedMaps[listIndex];
return true;
}
bool AddSupportedMapRaw(const MapStructureWrapper & newMap, uint32_t & listIndex) override
{
if (mSupportedMaps.size() >= kMaxNumSupportedMaps)
{
return false;
}
mSupportedMaps.push_back(newMap);
listIndex = static_cast<uint32_t>(mSupportedMaps.size() - 1);
return true;
}
bool ModifySupportedMapRaw(uint32_t listIndex, const MapStructureWrapper & modifiedMap) override
{
if (listIndex >= mSupportedMaps.size() || modifiedMap.mapID != mSupportedMaps[listIndex].mapID)
{
return false;
}
mSupportedMaps[listIndex] = modifiedMap;
return true;
}
bool ClearSupportedMapsRaw() override
{
if (mSupportedMaps.empty())
{
return false;
}
mSupportedMaps.clear();
return true;
}
bool RemoveSupportedMapRaw(uint32_t mapId) override
{
for (auto it = mSupportedMaps.begin(); it != mSupportedMaps.end(); ++it)
{
if (it->mapID == mapId)
{
mSupportedMaps.erase(it);
return true;
}
}
return false;
}
uint32_t GetNumberOfSelectedAreas() override { return static_cast<uint32_t>(mSelectedAreas.size()); }
bool GetSelectedAreaByIndex(uint32_t listIndex, uint32_t & selectedArea) override
{
if (listIndex >= mSelectedAreas.size())
{
return false;
}
selectedArea = mSelectedAreas[listIndex];
return true;
}
bool AddSelectedAreaRaw(uint32_t aAreaId, uint32_t & listIndex) override
{
if (mSelectedAreas.size() >= kMaxNumSelectedAreas)
{
return false;
}
mSelectedAreas.push_back(aAreaId);
listIndex = static_cast<uint32_t>(mSelectedAreas.size() - 1);
return true;
}
bool ClearSelectedAreasRaw() override
{
if (mSelectedAreas.empty())
{
return false;
}
mSelectedAreas.clear();
return true;
}
bool RemoveSelectedAreasRaw(uint32_t areaId) override
{
for (auto it = mSelectedAreas.begin(); it != mSelectedAreas.end(); ++it)
{
if (*it == areaId)
{
mSelectedAreas.erase(it);
return true;
}
}
return false;
}
uint32_t GetNumberOfProgressElements() override { return static_cast<uint32_t>(mProgress.size()); }
bool GetProgressElementByIndex(uint32_t listIndex, Structs::ProgressStruct::Type & aProgressElement) override
{
if (listIndex >= mProgress.size())
{
return false;
}
aProgressElement = mProgress[listIndex];
return true;
}
bool AddProgressElementRaw(const Structs::ProgressStruct::Type & newProgressElement, uint32_t & listIndex) override
{
if (mProgress.size() >= kMaxNumProgressElements)
{
return false;
}
mProgress.push_back(newProgressElement);
listIndex = static_cast<uint32_t>(mProgress.size() - 1);
return true;
}
bool ModifyProgressElementRaw(uint32_t listIndex, const Structs::ProgressStruct::Type & modifiedProgressElement) override
{
if (listIndex >= mProgress.size() || modifiedProgressElement.areaID != mProgress[listIndex].areaID)
{
return false;
}
mProgress[listIndex] = modifiedProgressElement;
return true;
}
bool ClearProgressRaw() override
{
if (mProgress.empty())
{
return false;
}
mProgress.clear();
return true;
}
bool RemoveProgressElementRaw(uint32_t areaId) override
{
for (auto it = mProgress.begin(); it != mProgress.end(); ++it)
{
if (it->areaID == areaId)
{
mProgress.erase(it);
return true;
}
}
return false;
}
std::vector<AreaStructureWrapper> mSupportedAreas;
std::vector<MapStructureWrapper> mSupportedMaps;
std::vector<uint32_t> mSelectedAreas;
std::vector<Structs::ProgressStruct::Type> mProgress;
};
class MockDelegate : public Delegate
{
public:
bool IsSetSelectedAreasAllowed(MutableCharSpan & statusText) override
{
if (!mSetSelectedAreasAllowed)
{
CopyCharSpanToMutableCharSpanWithTruncation(mInvalidModeText, statusText);
}
return mSetSelectedAreasAllowed;
}
bool IsValidSelectAreasSet(const Span<const uint32_t> & selectedAreas, SelectAreasStatus & locationStatus,
MutableCharSpan & statusText) override
{
locationStatus = mSelectAreasSetStatus;
if (locationStatus != SelectAreasStatus::kSuccess)
{
CopyCharSpanToMutableCharSpanWithTruncation(mSelectAreasSetStatusText, statusText);
return false;
}
(void) selectedAreas;
return true;
}
bool HandleSkipArea(uint32_t skippedArea, MutableCharSpan & skipStatusText) override
{
if (!mSkipAreaAllowed)
{
CopyCharSpanToMutableCharSpanWithTruncation(mSkipAreaStatusText, skipStatusText);
return false;
}
mLastSkippedArea = skippedArea;
return true;
}
bool IsSupportedAreasChangeAllowed() override { return mSupportedAreasChangeAllowed; }
bool IsSupportedMapChangeAllowed() override { return mSupportedMapChangeAllowed; }
bool mSetSelectedAreasAllowed = true;
bool mSkipAreaAllowed = true;
bool mSupportedAreasChangeAllowed = true;
bool mSupportedMapChangeAllowed = true;
SelectAreasStatus mSelectAreasSetStatus = SelectAreasStatus::kSuccess;
CharSpan mInvalidModeText = "device not idle"_span;
CharSpan mSelectAreasSetStatusText = "invalid set"_span;
CharSpan mSkipAreaStatusText = "cannot skip in current mode"_span;
uint32_t mLastSkippedArea = 0;
void Reset()
{
mSetSelectedAreasAllowed = true;
mSkipAreaAllowed = true;
mSupportedAreasChangeAllowed = true;
mSupportedMapChangeAllowed = true;
mSelectAreasSetStatus = SelectAreasStatus::kSuccess;
mInvalidModeText = "device not idle"_span;
mSelectAreasSetStatusText = "invalid set"_span;
mSkipAreaStatusText = "cannot skip in current mode"_span;
mLastSkippedArea = 0;
}
};
struct ClusterConfig
{
BitMask<Feature> features;
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
};
ServiceAreaCluster::OptionalAttributeSet AllOptionalAttributes()
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::SupportedMaps::Id>();
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::EstimatedEndTime::Id>();
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
return optionalAttributes;
}
ClusterConfig ClusterConfigWithCurrentArea()
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
return { .optionalAttributes = optionalAttributes };
}
ClusterConfig ClusterConfigWithProgress()
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
return { .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes };
}
AreaStructureWrapper MakeNamedArea(uint32_t areaId, const char * name)
{
AreaStructureWrapper area;
CharSpan nameSpan(name, strlen(name));
area.SetAreaId(areaId)
.SetMapId(DataModel::NullNullable)
.SetLocationInfo(nameSpan, DataModel::NullNullable, DataModel::NullNullable);
return area;
}
AreaStructureWrapper
MakeLandmarkOnlyArea(uint32_t areaId, Globals::LandmarkTag landmark,
const DataModel::Nullable<Globals::RelativePositionTag> & position = DataModel::NullNullable)
{
AreaStructureWrapper area;
area.SetAreaId(areaId).SetMapId(DataModel::NullNullable).SetLocationInfoNull().SetLandmarkInfo(landmark, position);
return area;
}
AreaStructureWrapper MakeFloorArea(uint32_t areaId, int16_t floor)
{
AreaStructureWrapper area;
area.SetAreaId(areaId)
.SetMapId(DataModel::NullNullable)
.SetLocationInfo(""_span, DataModel::MakeNullable(floor), DataModel::NullNullable);
return area;
}
AreaStructureWrapper MakeMappedArea(uint32_t areaId, uint32_t mapId, const char * name)
{
AreaStructureWrapper area;
CharSpan nameSpan(name, strlen(name));
area.SetAreaId(areaId)
.SetMapId(DataModel::MakeNullable(mapId))
.SetLocationInfo(nameSpan, DataModel::NullNullable, DataModel::NullNullable);
return area;
}
bool AddSupportedArea(ServiceAreaCluster & cluster, AreaStructureWrapper area)
{
return cluster.AddSupportedArea(area);
}
bool ModifySupportedArea(ServiceAreaCluster & cluster, AreaStructureWrapper area)
{
return cluster.ModifySupportedArea(area);
}
bool AddSelectedArea(ServiceAreaCluster & cluster, uint32_t areaId)
{
return cluster.AddSelectedArea(areaId);
}
Commands::SelectAreas::Type MakeSelectAreasRequest(std::initializer_list<uint32_t> areaIds)
{
Commands::SelectAreas::Type request;
static uint32_t sAreaIdBuffer[kMaxNumSelectedAreas];
size_t index = 0;
for (uint32_t id : areaIds)
{
sAreaIdBuffer[index++] = id;
}
request.newAreas = DataModel::List<const uint32_t>(sAreaIdBuffer, index);
return request;
}
std::vector<uint32_t> ReadSelectedAreasList(chip::Testing::ClusterTester & tester)
{
std::vector<uint32_t> result;
DataModel::DecodableList<uint32_t> selected;
if (!tester.ReadAttribute(Attributes::SelectedAreas::Id, selected).IsSuccess())
{
return result;
}
auto it = selected.begin();
while (it.Next())
{
result.push_back(it.GetValue());
}
return result;
}
size_t CountSupportedAreas(chip::Testing::ClusterTester & tester)
{
DataModel::DecodableList<Structs::AreaStruct::DecodableType> areas;
if (!tester.ReadAttribute(Attributes::SupportedAreas::Id, areas).IsSuccess())
{
return 0;
}
size_t count = 0;
auto it = areas.begin();
while (it.Next())
{
++count;
}
return count;
}
std::vector<uint32_t> ReadSupportedAreaIds(chip::Testing::ClusterTester & tester)
{
std::vector<uint32_t> result;
DataModel::DecodableList<Structs::AreaStruct::DecodableType> areas;
if (!tester.ReadAttribute(Attributes::SupportedAreas::Id, areas).IsSuccess())
{
return result;
}
auto it = areas.begin();
while (it.Next())
{
result.push_back(it.GetValue().areaID);
}
return result;
}
bool Contains(const std::vector<uint32_t> & ids, uint32_t id)
{
for (uint32_t entry : ids)
{
if (entry == id)
{
return true;
}
}
return false;
}
size_t CountSupportedMaps(chip::Testing::ClusterTester & tester)
{
DataModel::DecodableList<Structs::MapStruct::DecodableType> maps;
if (!tester.ReadAttribute(Attributes::SupportedMaps::Id, maps).IsSuccess())
{
return 0;
}
size_t count = 0;
auto it = maps.begin();
while (it.Next())
{
++count;
}
return count;
}
std::optional<Structs::ProgressStruct::DecodableType> FindProgressForArea(chip::Testing::ClusterTester & tester, uint32_t areaId)
{
DataModel::DecodableList<Structs::ProgressStruct::DecodableType> progressList;
if (!tester.ReadAttribute(Attributes::Progress::Id, progressList).IsSuccess())
{
return std::nullopt;
}
auto it = progressList.begin();
while (it.Next())
{
if (it.GetValue().areaID == areaId)
{
return it.GetValue();
}
}
return std::nullopt;
}
// Helpers to access invoke results and progress elements without unchecked optional access.
template <typename ResponseType, typename StatusEnum>
std::optional<StatusEnum> InvokeResponseStatus(const chip::Testing::ClusterTester::InvokeResult<ResponseType> & result)
{
VerifyOrReturnValue(result.response.has_value(), std::nullopt);
return result.response->status;
}
std::optional<SelectAreasStatus>
SelectAreasResponseStatus(const chip::Testing::ClusterTester::InvokeResult<Commands::SelectAreasResponse::DecodableType> & result)
{
return InvokeResponseStatus<Commands::SelectAreasResponse::DecodableType, SelectAreasStatus>(result);
}
std::optional<SkipAreaStatus>
SkipAreaResponseStatus(const chip::Testing::ClusterTester::InvokeResult<Commands::SkipAreaResponse::DecodableType> & result)
{
return InvokeResponseStatus<Commands::SkipAreaResponse::DecodableType, SkipAreaStatus>(result);
}
template <typename ResponseType>
bool InvokeResponseHasNonEmptyStatusText(const chip::Testing::ClusterTester::InvokeResult<ResponseType> & result)
{
VerifyOrReturnValue(result.response.has_value(), false);
return !result.response->statusText.empty();
}
std::optional<OperationalStatusEnum> ProgressElementStatus(const std::optional<Structs::ProgressStruct::DecodableType> & progress)
{
VerifyOrReturnValue(progress.has_value(), std::nullopt);
return progress->status;
}
std::optional<bool> ProgressElementHasTotalOperationalTime(const std::optional<Structs::ProgressStruct::DecodableType> & progress)
{
VerifyOrReturnValue(progress.has_value(), std::nullopt);
return progress->totalOperationalTime.HasValue();
}
std::optional<uint32_t>
ProgressElementTotalOperationalTimeSeconds(const std::optional<Structs::ProgressStruct::DecodableType> & progress)
{
VerifyOrReturnValue(progress.has_value(), std::nullopt);
VerifyOrReturnValue(progress->totalOperationalTime.HasValue(), std::nullopt);
return progress->totalOperationalTime.Value().Value();
}
bool DirtyListContainsAttribute(chip::Testing::ClusterTester & tester, AttributeId attributeId)
{
for (const auto & path : tester.GetDirtyList())
{
if (path.mAttributeId == attributeId)
{
return true;
}
}
return false;
}
class ServiceAreaClusterTest : public ::testing::Test
{
public:
void SetUp() override
{
ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR);
CreateCluster({});
}
void TearDown() override
{
if (mCluster)
{
mCluster->Shutdown(ClusterShutdownType::kClusterShutdown);
}
mTester.reset();
mCluster.reset();
chip::Platform::MemoryShutdown();
}
protected:
void CreateCluster(const ClusterConfig & config)
{
if (mCluster)
{
mCluster->Shutdown(ClusterShutdownType::kClusterShutdown);
}
mStorage = InMemoryStorageDelegate{};
mDelegate.Reset();
mCluster =
std::make_unique<ServiceAreaCluster>(kTestEndpoint, mStorage, mDelegate, config.features, config.optionalAttributes);
mTester = std::make_unique<chip::Testing::ClusterTester>(*mCluster);
ASSERT_EQ(mCluster->Startup(mTester->GetServerClusterContext()), CHIP_NO_ERROR);
}
void SeedBasicAreas()
{
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeNamedArea(0, "yellow bedroom")));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeNamedArea(1, "orange bedroom")));
}
InMemoryStorageDelegate mStorage;
MockDelegate mDelegate;
std::unique_ptr<ServiceAreaCluster> mCluster;
std::unique_ptr<chip::Testing::ClusterTester> mTester;
};
} // namespace
//*****************************************************************************
// cluster metadata
TEST_F(ServiceAreaClusterTest, ClusterRevisionMatchesSpec)
{
uint16_t revision = 0;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::ClusterRevision::Id, revision).IsSuccess());
EXPECT_EQ(revision, static_cast<uint16_t>(kRevision));
}
TEST_F(ServiceAreaClusterTest, FeatureMapReflectsEnabledFeatures)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kSelectWhileRunning, Feature::kProgressReporting, Feature::kMaps) });
uint32_t featureMap = 0;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::FeatureMap::Id, featureMap).IsSuccess());
EXPECT_EQ(featureMap, 0x07u);
}
TEST_F(ServiceAreaClusterTest, MandatoryAttributesAreAdvertised)
{
ReadOnlyBufferBuilder<DataModel::AttributeEntry> builder;
EXPECT_EQ(mCluster->Attributes(mCluster->GetPaths()[0], builder), CHIP_NO_ERROR);
auto list = builder.TakeBuffer();
auto HasAttr = [&](AttributeId id) {
for (const auto & entry : list)
{
if (entry.attributeId == id)
{
return true;
}
}
return false;
};
EXPECT_TRUE(HasAttr(Attributes::ClusterRevision::Id));
EXPECT_TRUE(HasAttr(Attributes::FeatureMap::Id));
EXPECT_TRUE(HasAttr(Attributes::SupportedAreas::Id));
EXPECT_TRUE(HasAttr(Attributes::SelectedAreas::Id));
EXPECT_FALSE(HasAttr(Attributes::SupportedMaps::Id));
EXPECT_FALSE(HasAttr(Attributes::CurrentArea::Id));
EXPECT_FALSE(HasAttr(Attributes::EstimatedEndTime::Id));
EXPECT_FALSE(HasAttr(Attributes::Progress::Id));
}
TEST_F(ServiceAreaClusterTest, OptionalAttributesAreAdvertisedWhenEnabled)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps, Feature::kProgressReporting),
.optionalAttributes = AllOptionalAttributes() });
ReadOnlyBufferBuilder<DataModel::AttributeEntry> builder;
EXPECT_EQ(mCluster->Attributes(mCluster->GetPaths()[0], builder), CHIP_NO_ERROR);
auto list = builder.TakeBuffer();
auto HasAttr = [&](AttributeId id) {
for (const auto & entry : list)
{
if (entry.attributeId == id)
{
return true;
}
}
return false;
};
EXPECT_TRUE(HasAttr(Attributes::SupportedMaps::Id));
EXPECT_TRUE(HasAttr(Attributes::CurrentArea::Id));
EXPECT_TRUE(HasAttr(Attributes::EstimatedEndTime::Id));
EXPECT_TRUE(HasAttr(Attributes::Progress::Id));
}
TEST_F(ServiceAreaClusterTest, SelectAreasCommandIsAlwaysAccepted)
{
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> builder;
EXPECT_EQ(mCluster->AcceptedCommands(mCluster->GetPaths()[0], builder), CHIP_NO_ERROR);
auto list = builder.TakeBuffer();
bool hasSelectAreas = false;
bool hasSkipArea = false;
for (const auto & entry : list)
{
if (entry.commandId == Commands::SelectAreas::Id)
{
hasSelectAreas = true;
}
if (entry.commandId == Commands::SkipArea::Id)
{
hasSkipArea = true;
}
}
EXPECT_TRUE(hasSelectAreas);
EXPECT_FALSE(hasSkipArea);
}
TEST_F(ServiceAreaClusterTest, SkipAreaCommandIsAcceptedWhenCurrentAreaIsEnabled)
{
CreateCluster(ClusterConfigWithCurrentArea());
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> builder;
EXPECT_EQ(mCluster->AcceptedCommands(mCluster->GetPaths()[0], builder), CHIP_NO_ERROR);
auto list = builder.TakeBuffer();
bool hasSelectAreas = false;
bool hasSkipArea = false;
for (const auto & entry : list)
{
if (entry.commandId == Commands::SelectAreas::Id)
{
hasSelectAreas = true;
}
if (entry.commandId == Commands::SkipArea::Id)
{
hasSkipArea = true;
}
}
EXPECT_TRUE(hasSelectAreas);
EXPECT_TRUE(hasSkipArea);
}
TEST_F(ServiceAreaClusterTest, SkipAreaCommandIsAcceptedWhenProgressIsEnabled)
{
CreateCluster(ClusterConfigWithProgress());
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> builder;
EXPECT_EQ(mCluster->AcceptedCommands(mCluster->GetPaths()[0], builder), CHIP_NO_ERROR);
auto list = builder.TakeBuffer();
bool hasSelectAreas = false;
bool hasSkipArea = false;
for (const auto & entry : list)
{
if (entry.commandId == Commands::SelectAreas::Id)
{
hasSelectAreas = true;
}
if (entry.commandId == Commands::SkipArea::Id)
{
hasSkipArea = true;
}
}
EXPECT_TRUE(hasSelectAreas);
EXPECT_TRUE(hasSkipArea);
}
//*****************************************************************************
// SupportedAreas attribute
TEST_F(ServiceAreaClusterTest, SupportedAreasStartsEmpty)
{
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
}
TEST_F(ServiceAreaClusterTest, MeaningfulAreaDescriptionsAppearInSupportedAreas)
{
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeNamedArea(0, "blue room")));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeFloorArea(1, 2)));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeLandmarkOnlyArea(2, LandmarkTag::kTable)));
EXPECT_EQ(CountSupportedAreas(*mTester), 3u);
}
TEST_F(ServiceAreaClusterTest, InvalidAreaDescriptionsDoNotAppearInSupportedAreas)
{
AreaStructureWrapper missingDescription;
missingDescription.SetAreaId(0).SetMapId(DataModel::NullNullable).SetLocationInfoNull().SetLandmarkInfoNull();
AddSupportedArea(*mCluster, missingDescription);
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
AreaStructureWrapper emptySemantics;
emptySemantics.SetAreaId(1)
.SetMapId(DataModel::NullNullable)
.SetLocationInfo(""_span, DataModel::NullNullable, DataModel::NullNullable)
.SetLandmarkInfoNull();
AddSupportedArea(*mCluster, emptySemantics);
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
}
TEST_F(ServiceAreaClusterTest, DuplicateAreaIdsAreNotAddedToSupportedAreas)
{
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeNamedArea(0, "room a")));
AddSupportedArea(*mCluster, MakeNamedArea(0, "room b"));
EXPECT_EQ(CountSupportedAreas(*mTester), 1u);
}
TEST_F(ServiceAreaClusterTest, DuplicateAreaInfoIsNotAddedWhenMapsAreEmpty)
{
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeNamedArea(0, "hallway")));
AddSupportedArea(*mCluster, MakeNamedArea(1, "hallway"));
EXPECT_EQ(CountSupportedAreas(*mTester), 1u);
}
TEST_F(ServiceAreaClusterTest, SameAreaInfoOnDifferentMapsIsAllowed)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "floor one"_span));
ASSERT_TRUE(mCluster->AddSupportedMap(2, "floor two"_span));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(5, 1, "hallway")));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(3, 2, "hallway")));
EXPECT_EQ(CountSupportedAreas(*mTester), 2u);
}
TEST_F(ServiceAreaClusterTest, AreasWithoutMapsMustHaveNullMapId)
{
AddSupportedArea(*mCluster, MakeMappedArea(0, 1, "room"));
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
}
TEST_F(ServiceAreaClusterTest, AreasMustReferenceKnownMapsWhenMapsExist)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "main floor"_span));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(0, 1, "kitchen")));
AddSupportedArea(*mCluster, MakeMappedArea(1, 99, "garage"));
EXPECT_EQ(CountSupportedAreas(*mTester), 1u);
}
TEST_F(ServiceAreaClusterTest, RemovingSupportedAreaUpdatesSelectedAreasAndCurrentArea)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(AddSelectedArea(*mCluster, static_cast<uint32_t>(0)));
ASSERT_TRUE(AddSelectedArea(*mCluster, static_cast<uint32_t>(1)));
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(1))));
ASSERT_TRUE(mCluster->RemoveSupportedArea(1));
auto selected = ReadSelectedAreasList(*mTester);
ASSERT_EQ(selected.size(), 1u);
EXPECT_EQ(selected[0], 0u);
DataModel::Nullable<uint32_t> currentArea;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::CurrentArea::Id, currentArea).IsSuccess());
EXPECT_TRUE(currentArea.IsNull());
}
TEST_F(ServiceAreaClusterTest, ClearingSupportedAreasClearsDependentAttributes)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(AddSelectedArea(*mCluster, static_cast<uint32_t>(0)));
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(0))));
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
ASSERT_TRUE(mCluster->ClearSupportedAreas());
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
DataModel::Nullable<uint32_t> currentArea;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::CurrentArea::Id, currentArea).IsSuccess());
EXPECT_TRUE(currentArea.IsNull());
DataModel::DecodableList<Structs::ProgressStruct::DecodableType> progress;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::Progress::Id, progress).IsSuccess());
EXPECT_FALSE(progress.begin().Next());
}
TEST_F(ServiceAreaClusterTest, SupportedAreasListIsUnchangedWhenDelegateDisallowsUpdates)
{
mDelegate.mSupportedAreasChangeAllowed = false;
AddSupportedArea(*mCluster, MakeNamedArea(0, "room"));
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
}
TEST_F(ServiceAreaClusterTest, ModifyingSupportedAreaUpdatesReadableAreaInfo)
{
SeedBasicAreas();
ASSERT_TRUE(ModifySupportedArea(*mCluster, MakeNamedArea(0, "renamed bedroom")));
DataModel::DecodableList<Structs::AreaStruct::DecodableType> areas;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::SupportedAreas::Id, areas).IsSuccess());
auto it = areas.begin();
ASSERT_TRUE(it.Next());
EXPECT_TRUE(it.GetValue().areaInfo.locationInfo.Value().locationName.data_equal("renamed bedroom"_span));
}
TEST_F(ServiceAreaClusterTest, RemovingSupportedAreaRemovesOrphanedProgress)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
ASSERT_TRUE(mCluster->AddPendingProgressElement(1));
ASSERT_TRUE(mCluster->RemoveSupportedArea(1));
auto progressForArea0 = FindProgressForArea(*mTester, 0);
auto progressForArea1 = FindProgressForArea(*mTester, 1);
ASSERT_TRUE(progressForArea0.has_value());
EXPECT_FALSE(progressForArea1.has_value());
}
//*****************************************************************************
// SupportedMaps attribute
TEST_F(ServiceAreaClusterTest, SupportedMapsIsUnsupportedWithoutMapsFeature)
{
DataModel::DecodableList<Structs::MapStruct::DecodableType> maps;
EXPECT_EQ(mTester->ReadAttribute(Attributes::SupportedMaps::Id, maps).GetStatusCode(),
ClusterStatusCode(Status::UnsupportedAttribute));
}
TEST_F(ServiceAreaClusterTest, DuplicateMapIdsOrNamesAreNotAdded)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "Main Floor"_span));
mCluster->AddSupportedMap(1, "Duplicate Id"_span);
mCluster->AddSupportedMap(2, "Main Floor"_span);
mCluster->AddSupportedMap(3, ""_span);
EXPECT_EQ(CountSupportedMaps(*mTester), 1u);
}
TEST_F(ServiceAreaClusterTest, RemovingMapRemovesItsAreas)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "floor one"_span));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(0, 1, "kitchen")));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(1, 1, "bedroom")));
ASSERT_TRUE(mCluster->RemoveSupportedMap(1));
EXPECT_EQ(CountSupportedMaps(*mTester), 0u);
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
}
// Regression test for the RemoveSupportedMap packed-buffer index bug.
//
// RemoveSupportedMap collects the areaIDs whose mapID matches the removed map
// into a temporary stack buffer, then removes each. The write into that buffer
// must use the packed cursor (count of matches so far), not the source cursor
// (index over all supported areas). When more than one map exists and the areas
// are interleaved, writing at the source cursor scatters the matches to their
// source slots; the subsequent reduce_size(<match count>) then keeps only the
// front of the buffer, so any match whose source index is >= the match count is
// dropped (never removed, and its MapID keeps pointing at the deleted map), and
// the unwritten gaps feed uninitialized stack values into RemoveSupportedAreaRaw.
//
// Two maps are needed so that removing one leaves GetNumberOfSupportedMaps() > 0
// and the buggy loop runs (the single-map case above short-circuits through
// ClearSupportedAreas). The list leads with a map-2 area so the first map-1 match
// lands past index 0: map-1 areas 100/101/102 sit at source indices 1, 3, 4. With
// 3 matches, the matches at source index >= 3 (areas 101 and 102) were dropped by
// the old code. After the fix all three map-1 areas are removed and the two map-2
// areas are kept.
TEST_F(ServiceAreaClusterTest, RemoveSupportedMapRemovesAllMatchingAreasWhenSparse)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "floor one"_span));
ASSERT_TRUE(mCluster->AddSupportedMap(2, "floor two"_span));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(200, 2, "two hundred"))); // source index 0: non-match
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(100, 1, "one hundred"))); // source index 1: match
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(201, 2, "two oh one"))); // source index 2: non-match
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(101, 1, "one oh one"))); // source index 3: match, dropped by bug
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(102, 1, "one oh two"))); // source index 4: match, dropped by bug
ASSERT_TRUE(mCluster->RemoveSupportedMap(1));
std::vector<uint32_t> remaining = ReadSupportedAreaIds(*mTester);
// Every area that referenced the removed map must be gone; 101 and 102 are the
// ones the old code left behind.
EXPECT_FALSE(Contains(remaining, 100u));
EXPECT_FALSE(Contains(remaining, 101u));
EXPECT_FALSE(Contains(remaining, 102u));
// Areas belonging to the surviving map must be untouched.
EXPECT_TRUE(Contains(remaining, 200u));
EXPECT_TRUE(Contains(remaining, 201u));
EXPECT_EQ(remaining.size(), 2u);
EXPECT_EQ(CountSupportedMaps(*mTester), 1u);
}
TEST_F(ServiceAreaClusterTest, ClearingSupportedMapsClearsSupportedAreas)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "floor one"_span));
ASSERT_TRUE(AddSupportedArea(*mCluster, MakeMappedArea(0, 1, "kitchen")));
ASSERT_TRUE(mCluster->ClearSupportedMaps());
EXPECT_EQ(CountSupportedMaps(*mTester), 0u);
EXPECT_EQ(CountSupportedAreas(*mTester), 0u);
}
TEST_F(ServiceAreaClusterTest, RenamedMapIsVisibleOnRead)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "old name"_span));
ASSERT_TRUE(mCluster->RenameSupportedMap(1, "new name"_span));
DataModel::DecodableList<Structs::MapStruct::DecodableType> maps;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::SupportedMaps::Id, maps).IsSuccess());
auto it = maps.begin();
ASSERT_TRUE(it.Next());
EXPECT_TRUE(it.GetValue().name.data_equal("new name"_span));
}
TEST_F(ServiceAreaClusterTest, SupportedMapsListIsUnchangedWhenDelegateDisallowsClear)
{
CreateCluster({ .features = BitMask<Feature>(Feature::kMaps), .optionalAttributes = AllOptionalAttributes() });
ASSERT_TRUE(mCluster->AddSupportedMap(1, "floor"_span));
mDelegate.mSupportedMapChangeAllowed = false;
mCluster->ClearSupportedMaps();
EXPECT_EQ(CountSupportedMaps(*mTester), 1u);
}
//*****************************************************************************
// SelectedAreas attribute
TEST_F(ServiceAreaClusterTest, SelectedAreasStartsEmpty)
{
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
}
TEST_F(ServiceAreaClusterTest, SelectedAreasOnlyContainsSupportedAreaIds)
{
AddSelectedArea(*mCluster, static_cast<uint32_t>(0));
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
SeedBasicAreas();
ASSERT_TRUE(AddSelectedArea(*mCluster, static_cast<uint32_t>(0)));
auto selected = ReadSelectedAreasList(*mTester);
ASSERT_EQ(selected.size(), 1u);
EXPECT_EQ(selected[0], 0u);
}
TEST_F(ServiceAreaClusterTest, SelectedAreasDoesNotContainDuplicates)
{
SeedBasicAreas();
ASSERT_TRUE(AddSelectedArea(*mCluster, static_cast<uint32_t>(0)));
AddSelectedArea(*mCluster, static_cast<uint32_t>(0));
auto selected = ReadSelectedAreasList(*mTester);
ASSERT_EQ(selected.size(), 1u);
EXPECT_EQ(selected[0], 0u);
}
TEST_F(ServiceAreaClusterTest, SelectedAreasIsUnchangedWhenDelegateDisallowsSelection)
{
SeedBasicAreas();
mDelegate.mSetSelectedAreasAllowed = false;
AddSelectedArea(*mCluster, static_cast<uint32_t>(0));
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
}
//*****************************************************************************
// CurrentArea and EstimatedEndTime attributes
TEST_F(ServiceAreaClusterTest, CurrentAreaStartsNull)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
DataModel::Nullable<uint32_t> currentArea;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::CurrentArea::Id, currentArea).IsSuccess());
EXPECT_TRUE(currentArea.IsNull());
}
TEST_F(ServiceAreaClusterTest, CurrentAreaOnlyReflectsSupportedAreas)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(99)));
DataModel::Nullable<uint32_t> currentArea;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::CurrentArea::Id, currentArea).IsSuccess());
EXPECT_TRUE(currentArea.IsNull());
SeedBasicAreas();
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(0))));
EXPECT_TRUE(mTester->ReadAttribute(Attributes::CurrentArea::Id, currentArea).IsSuccess());
EXPECT_FALSE(currentArea.IsNull());
EXPECT_EQ(currentArea.Value(), 0u);
}
TEST_F(ServiceAreaClusterTest, ClearingCurrentAreaClearsEstimatedEndTime)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::EstimatedEndTime::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(0))));
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(1000))));
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::NullNullable));
DataModel::Nullable<uint32_t> estimatedEndTime;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::EstimatedEndTime::Id, estimatedEndTime).IsSuccess());
EXPECT_TRUE(estimatedEndTime.IsNull());
}
TEST_F(ServiceAreaClusterTest, EstimatedEndTimeRemainsNullWhileCurrentAreaIsNull)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::EstimatedEndTime::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(100)));
DataModel::Nullable<uint32_t> estimatedEndTime;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::EstimatedEndTime::Id, estimatedEndTime).IsSuccess());
EXPECT_TRUE(estimatedEndTime.IsNull());
}
TEST_F(ServiceAreaClusterTest, EstimatedEndTimeChangeToNullIsReported)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::EstimatedEndTime::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(0))));
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(500))));
mTester->GetDirtyList().clear();
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::NullNullable));
EXPECT_TRUE(DirtyListContainsAttribute(*mTester, Attributes::EstimatedEndTime::Id));
}
TEST_F(ServiceAreaClusterTest, EstimatedEndTimeChangeToZeroIsReported)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::EstimatedEndTime::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(0))));
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(500))));
mTester->GetDirtyList().clear();
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(0))));
EXPECT_TRUE(DirtyListContainsAttribute(*mTester, Attributes::EstimatedEndTime::Id));
}
TEST_F(ServiceAreaClusterTest, EstimatedEndTimeDecreaseIsReported)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::EstimatedEndTime::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(0))));
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(500))));
mTester->GetDirtyList().clear();
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(400))));
EXPECT_TRUE(DirtyListContainsAttribute(*mTester, Attributes::EstimatedEndTime::Id));
}
TEST_F(ServiceAreaClusterTest, EstimatedEndTimeIncreaseIsNotReported)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::CurrentArea::Id>();
optionalAttributes.Set<ServiceArea::Attributes::EstimatedEndTime::Id>();
CreateCluster({ .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->SetCurrentArea(DataModel::MakeNullable(static_cast<uint32_t>(0))));
ASSERT_TRUE(mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(500))));
mTester->GetDirtyList().clear();
mCluster->SetEstimatedEndTime(DataModel::MakeNullable(static_cast<uint32_t>(600)));
EXPECT_FALSE(DirtyListContainsAttribute(*mTester, Attributes::EstimatedEndTime::Id));
}
//*****************************************************************************
// Progress attribute
TEST_F(ServiceAreaClusterTest, ProgressIsUnsupportedWithoutOptionalAttribute)
{
DataModel::DecodableList<Structs::ProgressStruct::DecodableType> progress;
EXPECT_EQ(mTester->ReadAttribute(Attributes::Progress::Id, progress).GetStatusCode(),
ClusterStatusCode(Status::UnsupportedAttribute));
}
TEST_F(ServiceAreaClusterTest, NewProgressStartsPendingWithNullTotalOperationalTime)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
auto progress = FindProgressForArea(*mTester, 0);
EXPECT_EQ(ProgressElementStatus(progress), OperationalStatusEnum::kPending);
EXPECT_EQ(ProgressElementHasTotalOperationalTime(progress), false);
}
TEST_F(ServiceAreaClusterTest, ProgressForUnsupportedAreaDoesNotAppear)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
mCluster->AddPendingProgressElement(0);
EXPECT_FALSE(FindProgressForArea(*mTester, 0).has_value());
}
TEST_F(ServiceAreaClusterTest, LeavingCompletedOrOperatingClearsTotalOperationalTime)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
ASSERT_TRUE(mCluster->SetProgressStatus(0, OperationalStatusEnum::kCompleted));
ASSERT_TRUE(mCluster->SetProgressTotalOperationalTime(0, DataModel::MakeNullable(static_cast<uint32_t>(120))));
ASSERT_TRUE(mCluster->SetProgressStatus(0, OperationalStatusEnum::kOperating));
auto progress = FindProgressForArea(*mTester, 0);
EXPECT_EQ(ProgressElementHasTotalOperationalTime(progress), false);
}
TEST_F(ServiceAreaClusterTest, TotalOperationalTimeIsStoredForCompletedAreas)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
ASSERT_TRUE(mCluster->SetProgressStatus(0, OperationalStatusEnum::kCompleted));
ASSERT_TRUE(mCluster->SetProgressTotalOperationalTime(0, DataModel::MakeNullable(static_cast<uint32_t>(300))));
auto progress = FindProgressForArea(*mTester, 0);
EXPECT_EQ(ProgressElementHasTotalOperationalTime(progress), true);
EXPECT_EQ(ProgressElementTotalOperationalTimeSeconds(progress), 300u);
}
TEST_F(ServiceAreaClusterTest, TotalOperationalTimeIsNotStoredWhileAreaIsPending)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
mCluster->SetProgressTotalOperationalTime(0, DataModel::MakeNullable(static_cast<uint32_t>(60)));
auto progress = FindProgressForArea(*mTester, 0);
EXPECT_EQ(ProgressElementHasTotalOperationalTime(progress), false);
}
TEST_F(ServiceAreaClusterTest, ProgressContainsAtMostOneEntryPerArea)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
mCluster->AddPendingProgressElement(0);
DataModel::DecodableList<Structs::ProgressStruct::DecodableType> progressList;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::Progress::Id, progressList).IsSuccess());
size_t count = 0;
auto it = progressList.begin();
while (it.Next())
{
++count;
}
EXPECT_EQ(count, 1u);
}
TEST_F(ServiceAreaClusterTest, ClearingProgressEmptiesProgressAttribute)
{
ServiceAreaCluster::OptionalAttributeSet optionalAttributes;
optionalAttributes.Set<ServiceArea::Attributes::Progress::Id>();
CreateCluster({ .features = BitMask<Feature>(Feature::kProgressReporting), .optionalAttributes = optionalAttributes });
SeedBasicAreas();
ASSERT_TRUE(mCluster->AddPendingProgressElement(0));
ASSERT_TRUE(mCluster->ClearProgress());
DataModel::DecodableList<Structs::ProgressStruct::DecodableType> progress;
EXPECT_TRUE(mTester->ReadAttribute(Attributes::Progress::Id, progress).IsSuccess());
EXPECT_FALSE(progress.begin().Next());
}
//*****************************************************************************
// SelectAreas command
TEST_F(ServiceAreaClusterTest, SelectAreasUpdatesSelectedAreasAttribute)
{
SeedBasicAreas();
auto result = mTester->Invoke(MakeSelectAreasRequest({ 0, 1 }));
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SelectAreasResponseStatus(result), SelectAreasStatus::kSuccess);
auto selected = ReadSelectedAreasList(*mTester);
ASSERT_EQ(selected.size(), 2u);
EXPECT_EQ(selected[0], 0u);
EXPECT_EQ(selected[1], 1u);
}
TEST_F(ServiceAreaClusterTest, SelectAreasWithEmptyListRemovesAreaConstraint)
{
SeedBasicAreas();
ASSERT_TRUE(AddSelectedArea(*mCluster, static_cast<uint32_t>(0)));
auto result = mTester->Invoke(MakeSelectAreasRequest({}));
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SelectAreasResponseStatus(result), SelectAreasStatus::kSuccess);
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
}
TEST_F(ServiceAreaClusterTest, SelectAreasWithUnknownAreaLeavesSelectedAreasUnchanged)
{
SeedBasicAreas();
auto result = mTester->Invoke(MakeSelectAreasRequest({ 99 }));
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SelectAreasResponseStatus(result), SelectAreasStatus::kUnsupportedArea);
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
}
TEST_F(ServiceAreaClusterTest, SelectAreasWhileDeviceModeDisallowsSelectionLeavesSelectedAreasUnchanged)
{
SeedBasicAreas();
mDelegate.mSetSelectedAreasAllowed = false;
auto result = mTester->Invoke(MakeSelectAreasRequest({ 0 }));
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SelectAreasResponseStatus(result), SelectAreasStatus::kInvalidInMode);
EXPECT_TRUE(InvokeResponseHasNonEmptyStatusText(result));
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
}
TEST_F(ServiceAreaClusterTest, SelectAreasWithInvalidSetLeavesSelectedAreasUnchanged)
{
SeedBasicAreas();
mDelegate.mSelectAreasSetStatus = SelectAreasStatus::kInvalidSet;
auto result = mTester->Invoke(MakeSelectAreasRequest({ 0, 1 }));
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SelectAreasResponseStatus(result), SelectAreasStatus::kInvalidSet);
EXPECT_TRUE(ReadSelectedAreasList(*mTester).empty());
}
TEST_F(ServiceAreaClusterTest, SelectAreasIgnoresDuplicateEntriesInNewAreas)
{
SeedBasicAreas();
auto result = mTester->Invoke(MakeSelectAreasRequest({ 1, 0, 0, 1 }));
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SelectAreasResponseStatus(result), SelectAreasStatus::kSuccess);
auto selected = ReadSelectedAreasList(*mTester);
ASSERT_EQ(selected.size(), 2u);
EXPECT_EQ(selected[0], 1u);
EXPECT_EQ(selected[1], 0u);
}
TEST_F(ServiceAreaClusterTest, SelectAreasMatchingCurrentSelectionSucceedsWithoutChangingSelectedAreas)
{
SeedBasicAreas();
ASSERT_TRUE(mTester->Invoke(MakeSelectAreasRequest({ 0, 1 })).IsSuccess());
auto before = ReadSelectedAreasList(*mTester);
auto result = mTester->Invoke(MakeSelectAreasRequest({ 0, 1 }));
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SelectAreasResponseStatus(result), SelectAreasStatus::kSuccess);
EXPECT_EQ(ReadSelectedAreasList(*mTester), before);
}
//*****************************************************************************
// SkipArea command
TEST_F(ServiceAreaClusterTest, SkipAreaIsUnsupportedWithoutCurrentAreaOrProgress)
{
SeedBasicAreas();
ASSERT_TRUE(mTester->Invoke(MakeSelectAreasRequest({ 0 })).IsSuccess());
Commands::SkipArea::Type request{ .skippedArea = 0 };
auto result = mTester->Invoke(request);
EXPECT_EQ(result.GetStatusCode(), ClusterStatusCode(Status::UnsupportedCommand));
}
TEST_F(ServiceAreaClusterTest, SkipAreaWithoutSelectedAreasReturnsInvalidAreaList)
{
CreateCluster(ClusterConfigWithCurrentArea());
SeedBasicAreas();
Commands::SkipArea::Type request{ .skippedArea = 0 };
auto result = mTester->Invoke(request);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SkipAreaResponseStatus(result), SkipAreaStatus::kInvalidAreaList);
}
TEST_F(ServiceAreaClusterTest, SkipAreaWithUnknownAreaReturnsInvalidSkippedArea)
{
CreateCluster(ClusterConfigWithCurrentArea());
SeedBasicAreas();
ASSERT_TRUE(mTester->Invoke(MakeSelectAreasRequest({ 0 })).IsSuccess());
Commands::SkipArea::Type request{ .skippedArea = 99 };
auto result = mTester->Invoke(request);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SkipAreaResponseStatus(result), SkipAreaStatus::kInvalidSkippedArea);
}
TEST_F(ServiceAreaClusterTest, SkipAreaWhileDeviceModeDisallowsSkipReturnsInvalidInMode)
{
CreateCluster(ClusterConfigWithCurrentArea());
SeedBasicAreas();
ASSERT_TRUE(mTester->Invoke(MakeSelectAreasRequest({ 0 })).IsSuccess());
mDelegate.mSkipAreaAllowed = false;
Commands::SkipArea::Type request{ .skippedArea = 0 };
auto result = mTester->Invoke(request);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SkipAreaResponseStatus(result), SkipAreaStatus::kInvalidInMode);
EXPECT_TRUE(InvokeResponseHasNonEmptyStatusText(result));
}
TEST_F(ServiceAreaClusterTest, SkipAreaSuccessInvokesDeviceDelegate)
{
CreateCluster(ClusterConfigWithCurrentArea());
SeedBasicAreas();
ASSERT_TRUE(mTester->Invoke(MakeSelectAreasRequest({ 0, 1 })).IsSuccess());
Commands::SkipArea::Type request{ .skippedArea = 0 };
auto result = mTester->Invoke(request);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SkipAreaResponseStatus(result), SkipAreaStatus::kSuccess);
EXPECT_EQ(mDelegate.mLastSkippedArea, 0u);
}
TEST_F(ServiceAreaClusterTest, SkipAreaIsRejectedAfterUnconstrainedSelection)
{
CreateCluster(ClusterConfigWithCurrentArea());
SeedBasicAreas();
ASSERT_TRUE(mTester->Invoke(MakeSelectAreasRequest({})).IsSuccess());
Commands::SkipArea::Type request{ .skippedArea = 0 };
auto result = mTester->Invoke(request);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(SkipAreaResponseStatus(result), SkipAreaStatus::kInvalidAreaList);
}
} // namespace Testing
} // namespace ServiceArea
} // namespace Clusters
} // namespace app
} // namespace chip