blob: 08d5ce569f2fb240e010138195e7c56d14bd9769 [file]
/*
* 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 <pw_unit_test/framework.h>
#include <ranges>
#include <app/CommandHandler.h>
#include <app/InteractionModelEngine.h>
#include <app/clusters/av-analysis-server/AvAnalysisCluster.h>
#include <app/data-model-provider/MetadataTypes.h>
#include <app/data-model/Decode.h>
#include <app/server-cluster/DefaultServerCluster.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <app/server-cluster/testing/ValidateGlobalAttributes.h>
#include <clusters/AvAnalysis/Attributes.h>
#include <clusters/AvAnalysis/Commands.h>
#include <clusters/AvAnalysis/Enums.h>
#include <clusters/AvAnalysis/Metadata.h>
#include <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/ReadOnlyBuffer.h>
namespace {
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::AvAnalysis;
using namespace Protocols::InteractionModel;
using namespace chip::Testing;
using chip::Testing::IsAcceptedCommandsListEqualTo;
using chip::Testing::IsAttributesListEqualTo;
static constexpr chip::EndpointId kTestEndpointId = 1;
static constexpr uint8_t kTestMaxZones = 5;
static constexpr uint8_t kTestMaxAnalysisStreams = 8;
// Test ambient contexts
// Define the list of semantic tags for the endpoint
const std::vector<app::Clusters::Descriptor::Structs::SemanticTagStruct::Type> testAmbientContexts = {
{ std::nullopt, static_cast<uint8_t>(0x49), static_cast<uint8_t>(0x0B),
MakeOptional(chip::app::DataModel::Nullable<chip::CharSpan>("Object.Package"_span)) },
{ std::nullopt, static_cast<uint8_t>(0x4B), static_cast<uint8_t>(0x08),
MakeOptional(chip::app::DataModel::Nullable<chip::CharSpan>("Activity.Delivery"_span)) },
{ std::nullopt, static_cast<uint8_t>(0x4B), static_cast<uint8_t>(0x09),
MakeOptional(chip::app::DataModel::Nullable<chip::CharSpan>("Activity.Retrieval"_span)) }
};
// Minimal mock delegate for testing
class MockAvAnalysisDelegate : public AvAnalysisDelegate
{
public:
void ShutdownApp() {}
Protocols::InteractionModel::Status EstablishAnalysisStream() { return Status::Success; }
Protocols::InteractionModel::Status ActivateAnalysisStream() { return Status::Success; }
Protocols::InteractionModel::Status DeactivateAnalysisStream() { return Status::Success; }
Protocols::InteractionModel::Status RemoveAnalysisStream() { return Status::Success; }
CHIP_ERROR VerifyZoneIDsAreValid(DataModel::DecodableList<uint16_t> aZoneIDs) { return CHIP_NO_ERROR; }
bool CanAddContextTriggers() { return true; }
void ActiveAmbientContextTriggersUpdated() {}
CHIP_ERROR PersistentAttributesLoadedCallback() { return CHIP_NO_ERROR; }
};
struct TestRemoteAvAnalysisCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
TestRemoteAvAnalysisCluster() :
mServer(kTestEndpointId, chip::BitFlags<Feature>(Feature::kRemoteContextDetection, Feature::kPerZoneContextDetection),
testAmbientContexts, DataModel::MakeNullable(kTestMaxZones)),
mClusterTester(mServer)
{}
void SetUp() override
{
mServer.SetDelegate(&mMockDelegate);
EXPECT_EQ(mServer.Startup(mClusterTester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_EQ(mServer.Init(), CHIP_NO_ERROR);
EXPECT_EQ(mServer.SetMaxAnalysisStreamCount(kTestMaxAnalysisStreams), CHIP_NO_ERROR);
}
void TearDown() override { mServer.Shutdown(ClusterShutdownType::kClusterShutdown); }
MockAvAnalysisDelegate mMockDelegate;
AvAnalysisCluster mServer;
ClusterTester mClusterTester;
};
TEST_F(TestRemoteAvAnalysisCluster, TestAttributes)
{
ASSERT_TRUE(IsAttributesListEqualTo(mServer,
{
Attributes::SupportedAmbientContexts::kMetadataEntry,
Attributes::ActiveAmbientContextTriggers::kMetadataEntry,
Attributes::MaxAnalysisStreamCount::kMetadataEntry,
Attributes::CurrentAnalysisStreamCount::kMetadataEntry,
Attributes::AnalysisStreams::kMetadataEntry,
Attributes::TrackingEnabled::kMetadataEntry,
}));
}
TEST_F(TestRemoteAvAnalysisCluster, TestCommands)
{
ASSERT_TRUE(IsAcceptedCommandsListEqualTo(mServer,
{
Commands::EnableContextTriggers::kMetadataEntry,
Commands::DisableContextTriggers::kMetadataEntry,
Commands::EstablishAnalysisStream::kMetadataEntry,
Commands::ActivateAnalysisStream::kMetadataEntry,
Commands::DeactivateAnalysisStream::kMetadataEntry,
Commands::RemoveAnalysisStream::kMetadataEntry,
}));
}
TEST_F(TestRemoteAvAnalysisCluster, ReadAllAttributesWithClusterTesterTest)
{
Attributes::SupportedAmbientContexts::TypeInfo::DecodableType aSupportedAmbientContexts;
ASSERT_EQ(mClusterTester.ReadAttribute(Attributes::SupportedAmbientContexts::Id, aSupportedAmbientContexts), CHIP_NO_ERROR);
// Verify that the entries in the DecodableList match the entries used in construction of the instance by
// creating a vactor of the values then comparing the two vectors
std::vector<app::Clusters::Descriptor::Structs::SemanticTagStruct::Type> readContexts;
auto aContextIterator = aSupportedAmbientContexts.begin();
while (aContextIterator.Next())
{
readContexts.push_back(aContextIterator.GetValue());
}
// No == exists for the Struct, and creating one fails due to the Struct structure, check value by value
bool are_equal =
std::equal(testAmbientContexts.begin(), testAmbientContexts.end(), readContexts.begin(), readContexts.end(),
[](const auto & p1, const auto & p2) { return p1.namespaceID == p2.namespaceID && p1.tag == p2.tag; });
ASSERT_TRUE(are_equal);
// On startup there should be no active triggers
Attributes::ActiveAmbientContextTriggers::TypeInfo::DecodableType aActiveContextTriggers;
size_t triggersSize;
ASSERT_EQ(mClusterTester.ReadAttribute(Attributes::ActiveAmbientContextTriggers::Id, aActiveContextTriggers), CHIP_NO_ERROR);
TEMPORARY_RETURN_IGNORED aActiveContextTriggers.ComputeSize(&triggersSize);
ASSERT_EQ(triggersSize, static_cast<size_t>(0));
uint8_t aMaxAnalysisStreamCount = 0;
ASSERT_EQ(mClusterTester.ReadAttribute(Attributes::MaxAnalysisStreamCount::Id, aMaxAnalysisStreamCount), CHIP_NO_ERROR);
ASSERT_EQ(kTestMaxAnalysisStreams, aMaxAnalysisStreamCount);
uint8_t aCurrentAnalysisStreamCount = 0;
ASSERT_EQ(mClusterTester.ReadAttribute(Attributes::CurrentAnalysisStreamCount::Id, aCurrentAnalysisStreamCount), CHIP_NO_ERROR);
ASSERT_EQ(0, aCurrentAnalysisStreamCount);
// On startup there should be no analysis streams
Attributes::AnalysisStreams::TypeInfo::DecodableType aAnalysisStreams;
size_t streamsSize;
ASSERT_EQ(mClusterTester.ReadAttribute(Attributes::AnalysisStreams::Id, aAnalysisStreams), CHIP_NO_ERROR);
TEMPORARY_RETURN_IGNORED aAnalysisStreams.ComputeSize(&streamsSize);
ASSERT_EQ(streamsSize, static_cast<size_t>(0));
bool trackingEnabled = false;
ASSERT_EQ(mClusterTester.ReadAttribute(Attributes::TrackingEnabled::Id, trackingEnabled), CHIP_NO_ERROR);
ASSERT_FALSE(trackingEnabled);
}
TEST_F(TestRemoteAvAnalysisCluster, ExecuteEnableContextTriggersCommandTest)
{
Testing::MockCommandHandler commandHandler;
commandHandler.SetFabricIndex(1);
ConcreteCommandPath kCommandPath{ 1, Clusters::AvAnalysis::Id, Commands::EnableContextTriggers::Id };
Commands::EnableContextTriggers::DecodableType commandData;
auto response = mServer.GetLogic().HandleEnableContextTriggers(commandHandler, kCommandPath, commandData);
if (response.has_value())
{
ASSERT_TRUE(response.value().IsSuccess());
}
else
{
// Fail the test case
FAIL();
}
}
TEST_F(TestRemoteAvAnalysisCluster, ExecuteDisableContextTriggersCommandTest)
{
Testing::MockCommandHandler commandHandler;
commandHandler.SetFabricIndex(1);
ConcreteCommandPath kCommandPath{ 1, Clusters::AvAnalysis::Id, Commands::DisableContextTriggers::Id };
Commands::DisableContextTriggers::DecodableType commandData;
auto response = mServer.GetLogic().HandleDisableContextTriggers(commandHandler, kCommandPath, commandData);
// The response should contain an ActionReturnStatus
if (response.has_value())
{
ASSERT_TRUE(response.value().IsSuccess());
}
else
{
FAIL();
}
}
TEST_F(TestRemoteAvAnalysisCluster, ExecuteEstablishAnalysisStreamCommandTest)
{
Testing::MockCommandHandler commandHandler;
commandHandler.SetFabricIndex(1);
ConcreteCommandPath kCommandPath{ 1, Clusters::AvAnalysis::Id, Commands::EstablishAnalysisStream::Id };
Commands::EstablishAnalysisStream::DecodableType commandData;
auto response = mServer.GetLogic().HandleEstablishAnalysisStream(commandHandler, kCommandPath, commandData);
// The response should contain an ActionReturnStatus
if (response.has_value())
{
ASSERT_TRUE(response.value().IsSuccess());
}
else
{
FAIL();
}
}
TEST_F(TestRemoteAvAnalysisCluster, ExecuteActivateAnalysisStreamCommandTest)
{
Testing::MockCommandHandler commandHandler;
commandHandler.SetFabricIndex(1);
ConcreteCommandPath kCommandPath{ 1, Clusters::AvAnalysis::Id, Commands::ActivateAnalysisStream::Id };
Commands::ActivateAnalysisStream::DecodableType commandData;
auto response = mServer.GetLogic().HandleActivateAnalysisStream(commandHandler, kCommandPath, commandData);
// The response should contain an ActionReturnStatus
if (response.has_value())
{
ASSERT_TRUE(response.value().IsSuccess());
}
else
{
FAIL();
}
}
TEST_F(TestRemoteAvAnalysisCluster, ExecuteDeactivateAnalysisStreamCommandTest)
{
Testing::MockCommandHandler commandHandler;
commandHandler.SetFabricIndex(1);
ConcreteCommandPath kCommandPath{ 1, Clusters::AvAnalysis::Id, Commands::DeactivateAnalysisStream::Id };
Commands::DeactivateAnalysisStream::DecodableType commandData;
auto response = mServer.GetLogic().HandleDeactivateAnalysisStream(commandHandler, kCommandPath, commandData);
// The response should contain an ActionReturnStatus
if (response.has_value())
{
ASSERT_TRUE(response.value().IsSuccess());
}
else
{
FAIL();
}
}
TEST_F(TestRemoteAvAnalysisCluster, ExecuteRemoveAnalysisStreamCommandTest)
{
Testing::MockCommandHandler commandHandler;
commandHandler.SetFabricIndex(1);
ConcreteCommandPath kCommandPath{ 1, Clusters::AvAnalysis::Id, Commands::RemoveAnalysisStream::Id };
Commands::RemoveAnalysisStream::DecodableType commandData;
auto response = mServer.GetLogic().HandleRemoveAnalysisStream(commandHandler, kCommandPath, commandData);
// The response should contain an ActionReturnStatus
if (response.has_value())
{
ASSERT_TRUE(response.value().IsSuccess());
}
else
{
FAIL();
}
}
} // namespace