blob: 88548e27b02c9ef0ab9cfba0468037a5d488fbb9 [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 "lib/core/TLVWriter.h"
#include <pw_unit_test/framework.h>
#include <app/AttributeValueDecoder.h>
#include <app/ConcreteAttributePath.h>
#include <app/data-model-provider/tests/WriteTesting.h>
#include <app/persistence/AttributePersistence.h>
#include <app/persistence/DefaultAttributePersistenceProvider.h>
#include <app/persistence/String.h>
#include <clusters/TimeFormatLocalization/Enums.h>
#include <clusters/TimeFormatLocalization/EnumsCheck.h>
#include <lib/core/CHIPError.h>
#include <lib/core/StringBuilderAdapters.h>
#include <lib/support/DefaultStorageKeyAllocator.h>
#include <lib/support/Span.h>
#include <lib/support/TestPersistentStorageDelegate.h>
#include <unistd.h>
namespace {
using namespace chip;
using namespace chip::app;
using namespace chip::Testing;
TEST(TestAttributePersistence, TestLoadAndDecodeAndStoreNativeEndian)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
const ConcreteAttributePath wrongPath(1, 2, 4);
constexpr uint32_t kValueToStore = 42;
constexpr uint32_t kOtherValue = 99;
// Store a fake value
{
const uint32_t value = kValueToStore;
EXPECT_EQ(storageDelegate.SyncSetKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
&value, sizeof(value)),
CHIP_NO_ERROR);
}
// Test loading a value
{
uint32_t valueRead = 0;
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, kOtherValue));
ASSERT_EQ(valueRead, kValueToStore);
}
// Test loading a non-existent value
{
uint32_t valueRead = 0;
ASSERT_FALSE(persistence.LoadNativeEndianValue(wrongPath, valueRead, kOtherValue));
ASSERT_EQ(valueRead, kOtherValue);
}
// Test loading a removed value
{
EXPECT_EQ(storageDelegate.SyncDeleteKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName()),
CHIP_NO_ERROR);
uint32_t valueRead = 0;
ASSERT_FALSE(persistence.LoadNativeEndianValue(path, valueRead, kOtherValue));
ASSERT_EQ(valueRead, kOtherValue);
}
}
TEST(TestAttributePersistence, TestNativeRawValueViaDecoder)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
const ConcreteAttributePath wrongPath(1, 2, 4);
constexpr uint32_t kValueToStore = 0x12345678;
constexpr uint32_t kOtherValue = 0x99887766;
uint32_t valueRead = 0;
// Store a value using an encoder (these are a PAIN to create, so use data model provider helpers)
{
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(kValueToStore);
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, valueRead), CHIP_NO_ERROR);
EXPECT_EQ(valueRead, kValueToStore);
}
{
valueRead = 0;
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, kOtherValue));
ASSERT_EQ(valueRead, kValueToStore);
}
// Try to read non-compatible types (note that size-wise compatible types will work ... wrongly (like u32 and float))
// this extra check is best-effort
{
uint16_t smallValue = 0;
const uint16_t kOther = 123u;
ASSERT_FALSE(persistence.LoadNativeEndianValue(path, smallValue, kOther));
ASSERT_EQ(smallValue, kOther);
}
{
uint64_t largeValue = 0;
const uint64_t kOther = 0x1122334455667788ull;
ASSERT_FALSE(persistence.LoadNativeEndianValue(path, largeValue, kOther));
ASSERT_EQ(largeValue, kOther);
}
}
TEST(TestAttributePersistence, TestStrings)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
const ConcreteAttributePath wrongPath(1, 2, 4);
{
Storage::String<8> testString;
ASSERT_TRUE(testString.SetContent("foo"_span));
ASSERT_EQ(persistence.StoreString(path, testString), CHIP_NO_ERROR);
}
{
Storage::String<16> readString;
ASSERT_TRUE(persistence.LoadString(path, readString));
ASSERT_TRUE(readString.Content().data_equal("foo"_span));
ASSERT_STREQ(readString.c_str(), "foo");
}
// fits exactly. Load should succeed
{
Storage::String<3> readString;
ASSERT_TRUE(persistence.LoadString(path, readString));
ASSERT_TRUE(readString.Content().data_equal("foo"_span));
ASSERT_STREQ(readString.c_str(), "foo");
}
// no space: data is cleared on load error
{
Storage::String<2> readString;
ASSERT_FALSE(persistence.LoadString(path, readString));
ASSERT_TRUE(readString.Content().empty());
ASSERT_TRUE(readString.SetContent("xy"_span));
ASSERT_FALSE(readString.Content().empty());
ASSERT_FALSE(persistence.LoadString(path, readString));
ASSERT_TRUE(readString.Content().empty());
ASSERT_STREQ(readString.c_str(), "");
}
// wrong path: data is cleared on load error
{
Storage::String<16> readString;
ASSERT_TRUE(readString.SetContent("xy"_span));
ASSERT_FALSE(readString.Content().empty());
ASSERT_FALSE(persistence.LoadString(wrongPath, readString));
ASSERT_TRUE(readString.Content().empty());
ASSERT_STREQ(readString.c_str(), "");
}
// empty string can be stored and loaded
{
Storage::String<8> testString;
Storage::String<16> readString;
ASSERT_TRUE(testString.SetContent(""_span));
ASSERT_EQ(persistence.StoreString(path, testString), CHIP_NO_ERROR);
ASSERT_TRUE(readString.SetContent("some value"_span));
ASSERT_TRUE(persistence.LoadString(path, readString));
ASSERT_TRUE(readString.Content().empty());
ASSERT_STREQ(readString.c_str(), "");
}
}
TEST(TestAttributePersistence, TestEnumHandling)
{
// Using TimeFormatLocalization enums for these tests.
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::TimeFormatLocalization;
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
CalendarTypeEnum valueRead = CalendarTypeEnum::kUnknownEnumValue;
// Test storing and loading a valid enum value
{
const ConcreteAttributePath path(1, 2, 3);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(CalendarTypeEnum::kGregorian);
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, valueRead), CHIP_NO_ERROR);
EXPECT_EQ(valueRead, CalendarTypeEnum::kGregorian);
// Test loading the stored enum value
valueRead = CalendarTypeEnum::kUnknownEnumValue;
EXPECT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, CalendarTypeEnum::kPersian));
EXPECT_EQ(valueRead, CalendarTypeEnum::kGregorian);
}
// Test attempting to store an unknown enum value
{
const ConcreteAttributePath path(3, 2, 1);
const uint8_t testUnknownValue = static_cast<uint8_t>(CalendarTypeEnum::kUnknownEnumValue) + 1;
ASSERT_EQ(EnsureKnownEnumValue(static_cast<CalendarTypeEnum>(testUnknownValue)), CalendarTypeEnum::kUnknownEnumValue);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(testUnknownValue);
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, valueRead), CHIP_IM_GLOBAL_STATUS(ConstraintError));
}
}
TEST(TestAttributePersistence, TestNoOpOnSameValueArithmetic)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
constexpr uint32_t kInitialValue = 42;
// Store an initial value
uint32_t currentValue = kInitialValue;
{
currentValue = 0;
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(kInitialValue);
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
EXPECT_EQ(currentValue, kInitialValue);
}
// Attempt to store the same value - should return kWriteSuccessNoOp
{
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(kInitialValue);
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_TRUE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue, kInitialValue); // Value should remain unchanged
}
// Verify the value is still loadable and unchanged
{
uint32_t loadedValue = 0;
EXPECT_TRUE(persistence.LoadNativeEndianValue(path, loadedValue, static_cast<uint32_t>(0)));
EXPECT_EQ(loadedValue, kInitialValue);
}
}
TEST(TestAttributePersistence, TestNoOpOnSameValueEnum)
{
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::TimeFormatLocalization;
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
CalendarTypeEnum currentValue = CalendarTypeEnum::kUnknownEnumValue;
// Store an initial enum value
{
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(CalendarTypeEnum::kGregorian);
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
EXPECT_EQ(currentValue, CalendarTypeEnum::kGregorian);
}
// Attempt to store the same enum value - should return kWriteSuccessNoOp
{
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(CalendarTypeEnum::kGregorian);
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_TRUE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue, CalendarTypeEnum::kGregorian);
}
// Verify the value is still loadable and unchanged
{
CalendarTypeEnum loadedValue = CalendarTypeEnum::kUnknownEnumValue;
EXPECT_TRUE(persistence.LoadNativeEndianValue(path, loadedValue, CalendarTypeEnum::kPersian));
EXPECT_EQ(loadedValue, CalendarTypeEnum::kGregorian);
}
}
TEST(TestAttributePersistence, TestWriteOnDifferentValueEnum)
{
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::TimeFormatLocalization;
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
CalendarTypeEnum currentValue = CalendarTypeEnum::kUnknownEnumValue;
// Store an initial enum value
{
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(CalendarTypeEnum::kGregorian);
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
EXPECT_EQ(currentValue, CalendarTypeEnum::kGregorian);
}
// Store a different enum value - should perform actual write
{
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(CalendarTypeEnum::kBuddhist);
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_FALSE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue, CalendarTypeEnum::kBuddhist);
}
// Verify the new value is persisted
{
CalendarTypeEnum loadedValue = CalendarTypeEnum::kUnknownEnumValue;
EXPECT_TRUE(persistence.LoadNativeEndianValue(path, loadedValue, CalendarTypeEnum::kPersian));
EXPECT_EQ(loadedValue, CalendarTypeEnum::kBuddhist);
}
}
TEST(TestAttributePersistence, TestInvalidPascalLengthStored)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
// This string is invalid as stored
{
uint8_t buffer[] = { 10, 'h', 'e', 'l', 'l', 'o' }; // length 10, but only 5 chars
EXPECT_EQ(storageDelegate.SyncSetKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
buffer, sizeof(buffer)),
CHIP_NO_ERROR);
}
// Load into a buffer that COULD contain the string, but
// stored string is invalid
{
Storage::String<16> readString;
ASSERT_TRUE(readString.SetContent("some value"_span));
ASSERT_FALSE(persistence.LoadString(path, readString));
ASSERT_TRUE(readString.Content().empty());
}
}
TEST(TestAttributePersistence, TestLoadNativeEndianValueNullable)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
const ConcreteAttributePath wrongPath(1, 2, 4);
constexpr uint32_t kValueToStore = 42;
// Store a non-null value directly
{
typename NumericAttributeTraits<uint32_t>::StorageType storageReadValue;
NumericAttributeTraits<uint32_t>::WorkingToStorage(kValueToStore, storageReadValue);
EXPECT_EQ(storageDelegate.SyncSetKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
&storageReadValue, sizeof(storageReadValue)),
CHIP_NO_ERROR);
}
// Test loading a non-null value into Nullable
{
DataModel::Nullable<uint32_t> valueRead;
DataModel::Nullable<uint32_t> defaultValue = DataModel::MakeNullable<uint32_t>(99);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, defaultValue));
ASSERT_FALSE(valueRead.IsNull());
ASSERT_EQ(valueRead.Value(), kValueToStore);
}
// Store a null value
{
typename NumericAttributeTraits<uint32_t>::StorageType nullValue;
NumericAttributeTraits<uint32_t>::SetNull(nullValue);
EXPECT_EQ(storageDelegate.SyncSetKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
&nullValue, sizeof(nullValue)),
CHIP_NO_ERROR);
}
// Test loading a null value
{
DataModel::Nullable<uint32_t> valueRead;
DataModel::Nullable<uint32_t> defaultValue = DataModel::MakeNullable<uint32_t>(99);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, defaultValue));
ASSERT_TRUE(valueRead.IsNull());
}
// Test loading from non-existent path with non-null default
{
DataModel::Nullable<uint32_t> valueRead;
DataModel::Nullable<uint32_t> defaultValue = DataModel::MakeNullable<uint32_t>(123);
ASSERT_FALSE(persistence.LoadNativeEndianValue(wrongPath, valueRead, defaultValue));
ASSERT_FALSE(valueRead.IsNull());
ASSERT_EQ(valueRead.Value(), 123u);
}
// Test loading from non-existent path with null default
{
DataModel::Nullable<uint32_t> valueRead;
DataModel::Nullable<uint32_t> defaultValue = DataModel::NullNullable;
ASSERT_FALSE(persistence.LoadNativeEndianValue(wrongPath, valueRead, defaultValue));
ASSERT_TRUE(valueRead.IsNull());
}
}
TEST(TestAttributePersistence, TestDecodeAndStoreNativeEndianValueNullable)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
constexpr uint32_t kValueToStore = 0x12345678;
// Store a non-null value via decoder
{
DataModel::Nullable<uint32_t> currentValue;
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(kValueToStore));
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
ASSERT_FALSE(currentValue.IsNull());
EXPECT_EQ(currentValue.Value(), kValueToStore);
}
// Verify the value can be loaded back
{
DataModel::Nullable<uint32_t> valueRead;
DataModel::Nullable<uint32_t> errorRead;
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, errorRead));
ASSERT_FALSE(valueRead.IsNull());
ASSERT_EQ(valueRead.Value(), kValueToStore);
}
// Store a null value via decoder
{
DataModel::Nullable<uint32_t> currentValue = DataModel::MakeNullable(kValueToStore);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<uint32_t>());
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
ASSERT_TRUE(currentValue.IsNull());
}
// Verify the null value can be loaded back
{
DataModel::Nullable<uint32_t> valueRead = DataModel::MakeNullable<uint32_t>(999);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, DataModel::MakeNullable<uint32_t>(0)));
ASSERT_TRUE(valueRead.IsNull());
}
}
TEST(TestAttributePersistence, TestNoOpOnSameValueNullable)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
constexpr uint32_t kInitialValue = 42;
// Test no-op for same non-null value
{
DataModel::Nullable<uint32_t> currentValue;
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(kInitialValue));
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
}
{
DataModel::Nullable<uint32_t> currentValue = DataModel::MakeNullable(kInitialValue);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(kInitialValue));
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_TRUE(status.IsNoOpSuccess());
}
// Test no-op for same null value
{
DataModel::Nullable<uint32_t> currentValue = DataModel::MakeNullable(kInitialValue);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<uint32_t>());
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
ASSERT_TRUE(currentValue.IsNull());
}
{
DataModel::Nullable<uint32_t> currentValue; // null
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<uint32_t>());
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_TRUE(status.IsNoOpSuccess());
}
}
TEST(TestAttributePersistence, TestWriteOnDifferentValueNullable)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
constexpr uint32_t kInitialValue = 42;
constexpr uint32_t kNewValue = 99;
// Store initial non-null value
{
DataModel::Nullable<uint32_t> currentValue;
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(kInitialValue));
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
EXPECT_EQ(currentValue.Value(), kInitialValue);
}
// Store a different non-null value - should write
{
DataModel::Nullable<uint32_t> currentValue = DataModel::MakeNullable(kInitialValue);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(kNewValue));
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_FALSE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue.Value(), kNewValue);
}
// Store null - should write
{
DataModel::Nullable<uint32_t> currentValue = DataModel::MakeNullable(kNewValue);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<uint32_t>());
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_FALSE(status.IsNoOpSuccess());
EXPECT_TRUE(currentValue.IsNull());
}
// Store non-null after null - should write
{
DataModel::Nullable<uint32_t> currentValue; // null
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(kInitialValue));
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_FALSE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue.Value(), kInitialValue);
}
}
TEST(TestAttributePersistence, TestLoadNativeEndianValueNullableEnum)
{
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::TimeFormatLocalization;
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
const ConcreteAttributePath wrongPath(1, 2, 4);
// Store a non-null enum value directly
{
typename NumericAttributeTraits<CalendarTypeEnum>::StorageType storageValue;
NumericAttributeTraits<CalendarTypeEnum>::WorkingToStorage(CalendarTypeEnum::kGregorian, storageValue);
EXPECT_EQ(storageDelegate.SyncSetKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
&storageValue, sizeof(storageValue)),
CHIP_NO_ERROR);
}
// Test loading a non-null enum value into Nullable
{
DataModel::Nullable<CalendarTypeEnum> valueRead;
DataModel::Nullable<CalendarTypeEnum> defaultValue = DataModel::MakeNullable(CalendarTypeEnum::kPersian);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, defaultValue));
ASSERT_FALSE(valueRead.IsNull());
ASSERT_EQ(valueRead.Value(), CalendarTypeEnum::kGregorian);
}
// Store a null value
{
typename NumericAttributeTraits<CalendarTypeEnum>::StorageType nullValue;
NumericAttributeTraits<CalendarTypeEnum>::SetNull(nullValue);
EXPECT_EQ(storageDelegate.SyncSetKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
&nullValue, sizeof(nullValue)),
CHIP_NO_ERROR);
}
// Test loading a null value
{
DataModel::Nullable<CalendarTypeEnum> valueRead;
DataModel::Nullable<CalendarTypeEnum> defaultValue = DataModel::MakeNullable(CalendarTypeEnum::kPersian);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, defaultValue));
ASSERT_TRUE(valueRead.IsNull());
}
// Test loading from non-existent path with non-null default
{
DataModel::Nullable<CalendarTypeEnum> valueRead;
DataModel::Nullable<CalendarTypeEnum> defaultValue = DataModel::MakeNullable(CalendarTypeEnum::kBuddhist);
ASSERT_FALSE(persistence.LoadNativeEndianValue(wrongPath, valueRead, defaultValue));
ASSERT_FALSE(valueRead.IsNull());
ASSERT_EQ(valueRead.Value(), CalendarTypeEnum::kBuddhist);
}
// Test loading from non-existent path with null default
{
DataModel::Nullable<CalendarTypeEnum> valueRead;
DataModel::Nullable<CalendarTypeEnum> defaultValue = DataModel::NullNullable;
ASSERT_FALSE(persistence.LoadNativeEndianValue(wrongPath, valueRead, defaultValue));
ASSERT_TRUE(valueRead.IsNull());
}
}
TEST(TestAttributePersistence, TestDecodeAndStoreNativeEndianValueNullableEnum)
{
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::TimeFormatLocalization;
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
// Store a non-null valid enum value via decoder
{
DataModel::Nullable<CalendarTypeEnum> currentValue;
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(CalendarTypeEnum::kGregorian));
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
ASSERT_FALSE(currentValue.IsNull());
EXPECT_EQ(currentValue.Value(), CalendarTypeEnum::kGregorian);
}
// Verify the value can be loaded back
{
DataModel::Nullable<CalendarTypeEnum> valueRead;
DataModel::Nullable<CalendarTypeEnum> defaultValue = DataModel::MakeNullable(CalendarTypeEnum::kPersian);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, defaultValue));
ASSERT_FALSE(valueRead.IsNull());
ASSERT_EQ(valueRead.Value(), CalendarTypeEnum::kGregorian);
}
// Store a null value via decoder
{
DataModel::Nullable<CalendarTypeEnum> currentValue = DataModel::MakeNullable(CalendarTypeEnum::kGregorian);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<CalendarTypeEnum>());
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
ASSERT_TRUE(currentValue.IsNull());
}
// Verify the null value can be loaded back
{
DataModel::Nullable<CalendarTypeEnum> valueRead = DataModel::MakeNullable(CalendarTypeEnum::kBuddhist);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, DataModel::MakeNullable(CalendarTypeEnum::kPersian)));
ASSERT_TRUE(valueRead.IsNull());
}
// Test that kUnknownEnumValue wrapped in Nullable is rejected
{
const ConcreteAttributePath path2(4, 5, 6);
const uint8_t testUnknownValue = static_cast<uint8_t>(CalendarTypeEnum::kUnknownEnumValue) + 1;
ASSERT_EQ(EnsureKnownEnumValue(static_cast<CalendarTypeEnum>(testUnknownValue)), CalendarTypeEnum::kUnknownEnumValue);
DataModel::Nullable<CalendarTypeEnum> currentValue;
WriteOperation writeOp(path2);
AttributeValueDecoder decoder = writeOp.DecoderFor(testUnknownValue);
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path2, decoder, currentValue),
CHIP_IM_GLOBAL_STATUS(ConstraintError));
}
// Test that null bypasses kUnknownEnumValue check (null is valid)
{
const ConcreteAttributePath path3(7, 8, 9);
DataModel::Nullable<CalendarTypeEnum> currentValue = DataModel::MakeNullable(CalendarTypeEnum::kGregorian);
WriteOperation writeOp(path3);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<CalendarTypeEnum>());
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path3, decoder, currentValue), CHIP_NO_ERROR);
ASSERT_TRUE(currentValue.IsNull());
}
}
TEST(TestAttributePersistence, TestNoOpOnSameValueNullableEnum)
{
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::TimeFormatLocalization;
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
// Test no-op for same non-null enum value
{
DataModel::Nullable<CalendarTypeEnum> currentValue;
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(CalendarTypeEnum::kGregorian));
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
EXPECT_EQ(currentValue.Value(), CalendarTypeEnum::kGregorian);
}
{
DataModel::Nullable<CalendarTypeEnum> currentValue = DataModel::MakeNullable(CalendarTypeEnum::kGregorian);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(CalendarTypeEnum::kGregorian));
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_TRUE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue.Value(), CalendarTypeEnum::kGregorian);
}
// Test no-op for same null value
{
DataModel::Nullable<CalendarTypeEnum> currentValue = DataModel::MakeNullable(CalendarTypeEnum::kGregorian);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<CalendarTypeEnum>());
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
ASSERT_TRUE(currentValue.IsNull());
}
{
DataModel::Nullable<CalendarTypeEnum> currentValue; // null
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<CalendarTypeEnum>());
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_TRUE(status.IsNoOpSuccess());
ASSERT_TRUE(currentValue.IsNull());
}
}
TEST(TestAttributePersistence, TestWriteOnDifferentValueNullableEnum)
{
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::TimeFormatLocalization;
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
// Store initial non-null enum value
{
DataModel::Nullable<CalendarTypeEnum> currentValue;
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(CalendarTypeEnum::kGregorian));
EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue), CHIP_NO_ERROR);
EXPECT_EQ(currentValue.Value(), CalendarTypeEnum::kGregorian);
}
// Store a different non-null enum value - should write
{
DataModel::Nullable<CalendarTypeEnum> currentValue = DataModel::MakeNullable(CalendarTypeEnum::kGregorian);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(CalendarTypeEnum::kBuddhist));
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_FALSE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue.Value(), CalendarTypeEnum::kBuddhist);
}
// Verify the new value is persisted
{
DataModel::Nullable<CalendarTypeEnum> loadedValue;
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, loadedValue, DataModel::MakeNullable(CalendarTypeEnum::kPersian)));
ASSERT_FALSE(loadedValue.IsNull());
ASSERT_EQ(loadedValue.Value(), CalendarTypeEnum::kBuddhist);
}
// Store null - should write
{
DataModel::Nullable<CalendarTypeEnum> currentValue = DataModel::MakeNullable(CalendarTypeEnum::kBuddhist);
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::Nullable<CalendarTypeEnum>());
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_FALSE(status.IsNoOpSuccess());
EXPECT_TRUE(currentValue.IsNull());
}
// Verify null is persisted
{
DataModel::Nullable<CalendarTypeEnum> loadedValue = DataModel::MakeNullable(CalendarTypeEnum::kGregorian);
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, loadedValue, DataModel::MakeNullable(CalendarTypeEnum::kPersian)));
ASSERT_TRUE(loadedValue.IsNull());
}
// Store non-null after null - should write
{
DataModel::Nullable<CalendarTypeEnum> currentValue; // null
WriteOperation writeOp(path);
AttributeValueDecoder decoder = writeOp.DecoderFor(DataModel::MakeNullable(CalendarTypeEnum::kCoptic));
DataModel::ActionReturnStatus status = persistence.DecodeAndStoreNativeEndianValue(path, decoder, currentValue);
EXPECT_TRUE(status.IsSuccess());
EXPECT_FALSE(status.IsNoOpSuccess());
EXPECT_EQ(currentValue.Value(), CalendarTypeEnum::kCoptic);
}
// Verify the final value is persisted
{
DataModel::Nullable<CalendarTypeEnum> loadedValue;
ASSERT_TRUE(persistence.LoadNativeEndianValue(path, loadedValue, DataModel::MakeNullable(CalendarTypeEnum::kPersian)));
ASSERT_FALSE(loadedValue.IsNull());
ASSERT_EQ(loadedValue.Value(), CalendarTypeEnum::kCoptic);
}
}
// A fake encodable TLV structure, for testing purposes only.
struct TestTLVStruct
{
uint32_t a = 0;
bool b = false;
bool operator==(const TestTLVStruct & other) const { return a == other.a && b == other.b; }
CHIP_ERROR Encode(TLV::TLVWriter & writer, TLV::Tag tag) const
{
TLV::TLVType outer;
ReturnErrorOnFailure(writer.StartContainer(tag, TLV::kTLVType_Structure, outer));
ReturnErrorOnFailure(writer.Put(TLV::ContextTag(1), a));
ReturnErrorOnFailure(writer.Put(TLV::ContextTag(2), b));
return writer.EndContainer(outer);
}
CHIP_ERROR Decode(TLV::TLVReader & reader)
{
TLV::TLVType outer;
ReturnErrorOnFailure(reader.EnterContainer(outer));
// Format of structure during Encode is known, so we force ordering.
ReturnErrorOnFailure(reader.Next());
VerifyOrReturnError(reader.GetTag() == TLV::ContextTag(1), CHIP_ERROR_INVALID_ARGUMENT);
ReturnErrorOnFailure(reader.Get(a));
ReturnErrorOnFailure(reader.Next());
VerifyOrReturnError(reader.GetTag() == TLV::ContextTag(2), CHIP_ERROR_INVALID_ARGUMENT);
ReturnErrorOnFailure(reader.Get(b));
VerifyOrReturnError(reader.Next() == CHIP_ERROR_END_OF_TLV, CHIP_ERROR_INVALID_ARGUMENT);
return reader.ExitContainer(outer);
}
};
TEST(TestAttributePersistence, TestStoreAndLoadTLV)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath kPath(1, 2, 3);
const ConcreteAttributePath kOtherInvalidPath(1, 2, 4);
constexpr size_t kBufferSize = 128;
TestTLVStruct valueToStore{ .a = 12345, .b = true };
TestTLVStruct valueToStore2{ .a = 67890, .b = false };
// Test StoreTLV with external buffer
{
uint8_t buffer[kBufferSize];
MutableByteSpan span(buffer);
EXPECT_EQ(persistence.StoreTLV(kPath, valueToStore, span), CHIP_NO_ERROR);
}
// Test LoadTLV with external buffer
{
uint8_t buffer[kBufferSize];
MutableByteSpan span(buffer);
TestTLVStruct loadedValue;
EXPECT_EQ(persistence.LoadTLV(kPath, loadedValue, span), CHIP_NO_ERROR);
EXPECT_EQ(loadedValue, valueToStore);
}
// Test LoadTLV from wrong path
{
uint8_t buffer[kBufferSize];
MutableByteSpan span(buffer);
TestTLVStruct loadedValue;
EXPECT_NE(persistence.LoadTLV(kOtherInvalidPath, loadedValue, span), CHIP_NO_ERROR);
}
// Test StoreTLV with stack allocation (convenience overload)
{
EXPECT_EQ(persistence.StoreTLV<kBufferSize>(kPath, valueToStore2), CHIP_NO_ERROR);
}
// Test StoreTLV with too small buffer
{
uint8_t buffer[4]; // Too small for TestTLVStruct
MutableByteSpan span(buffer);
EXPECT_EQ(persistence.StoreTLV(kPath, valueToStore, span), CHIP_ERROR_BUFFER_TOO_SMALL);
}
}
TEST(TestAttributePersistence, TestAttributePersistenceTLVValidation)
{
TestPersistentStorageDelegate storageDelegate;
DefaultAttributePersistenceProvider ramProvider;
ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
AttributePersistence persistence(ramProvider);
const ConcreteAttributePath path(1, 2, 3);
// Helper to write raw bytes to storage
auto writeRaw = [&](const ByteSpan & data) {
return storageDelegate.SyncSetKeyValue(
DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(), data.data(),
static_cast<uint16_t>(data.size()));
};
// 1. Not a structure (Just an integer)
{
uint8_t buffer[128];
TLV::TLVWriter writer;
writer.Init(buffer);
ASSERT_EQ(writer.Put(TLV::AnonymousTag(), (uint32_t) 100), CHIP_NO_ERROR);
ASSERT_EQ(writer.Finalize(), CHIP_NO_ERROR);
ASSERT_EQ(writeRaw(ByteSpan(buffer, writer.GetLengthWritten())), CHIP_NO_ERROR);
uint32_t value = 0;
uint8_t readBuffer[128];
// Should fail because it expects a Structure
EXPECT_EQ(persistence.LoadTLV(path, value, MutableByteSpan(readBuffer)), CHIP_ERROR_WRONG_TLV_TYPE);
}
// 2. Empty Structure
{
uint8_t buffer[128];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType container;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, container), CHIP_NO_ERROR);
ASSERT_EQ(writer.EndContainer(container), CHIP_NO_ERROR);
ASSERT_EQ(writer.Finalize(), CHIP_NO_ERROR);
ASSERT_EQ(writeRaw(ByteSpan(buffer, writer.GetLengthWritten())), CHIP_NO_ERROR);
uint32_t value = 0;
uint8_t readBuffer[128];
// Should fail because it expects an element inside
EXPECT_EQ(persistence.LoadTLV(path, value, MutableByteSpan(readBuffer)), CHIP_END_OF_TLV);
}
// 3. Structure with Wrong Element Tag (Context Tag 2 instead of 1)
{
uint8_t buffer[128];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType container;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, container), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::ContextTag(2), (uint32_t) 100), CHIP_NO_ERROR);
ASSERT_EQ(writer.EndContainer(container), CHIP_NO_ERROR);
ASSERT_EQ(writer.Finalize(), CHIP_NO_ERROR);
ASSERT_EQ(writeRaw(ByteSpan(buffer, writer.GetLengthWritten())), CHIP_NO_ERROR);
uint32_t value = 0;
uint8_t readBuffer[128];
// Should fail on tag check
EXPECT_EQ(persistence.LoadTLV(path, value, MutableByteSpan(readBuffer)), CHIP_ERROR_INVALID_ARGUMENT);
}
// 4. Structure with Extra Element
{
uint8_t buffer[128];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType container;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, container), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::ContextTag(1), (uint32_t) 100), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::ContextTag(2), (uint32_t) 200), CHIP_NO_ERROR); // Extra
ASSERT_EQ(writer.EndContainer(container), CHIP_NO_ERROR);
ASSERT_EQ(writer.Finalize(), CHIP_NO_ERROR);
ASSERT_EQ(writeRaw(ByteSpan(buffer, writer.GetLengthWritten())), CHIP_NO_ERROR);
uint32_t value = 0;
uint8_t readBuffer[128];
// Should fail on VerifyEndOfContainer inside container
EXPECT_EQ(persistence.LoadTLV(path, value, MutableByteSpan(readBuffer)), CHIP_ERROR_UNEXPECTED_TLV_ELEMENT);
}
// 5. Trailing Data after Structure
{
uint8_t buffer[128];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType container;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, container), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::ContextTag(1), (uint32_t) 100), CHIP_NO_ERROR);
ASSERT_EQ(writer.EndContainer(container), CHIP_NO_ERROR);
ASSERT_EQ(writer.Put(TLV::AnonymousTag(), (uint32_t) 200), CHIP_NO_ERROR); // Trailing
ASSERT_EQ(writer.Finalize(), CHIP_NO_ERROR);
ASSERT_EQ(writeRaw(ByteSpan(buffer, writer.GetLengthWritten())), CHIP_NO_ERROR);
uint32_t value = 0;
uint8_t readBuffer[128];
// Should fail on VerifyEndOfContainer outside container
EXPECT_EQ(persistence.LoadTLV(path, value, MutableByteSpan(readBuffer)), CHIP_ERROR_UNEXPECTED_TLV_ELEMENT);
}
}
} // namespace