blob: dbaf44b347b04e063b98cf707a9f44d0532b5bc7 [file] [log] [blame]
// Copyright 2020 The Pigweed 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
//
// https://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.
// Tests that directly work with the KVS's binary format and flash layer.
#include <string_view>
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/fake_flash_memory.h"
#include "pw_kvs/format.h"
#include "pw_kvs/internal/hash.h"
#include "pw_kvs/key_value_store.h"
namespace pw::kvs {
namespace {
using std::byte;
using std::string_view;
constexpr size_t kMaxEntries = 256;
constexpr size_t kMaxUsableSectors = 256;
constexpr uint32_t SimpleChecksum(std::span<const byte> data, uint32_t state) {
for (byte b : data) {
state += uint32_t(b);
}
return state;
}
template <typename State>
class ChecksumFunction final : public ChecksumAlgorithm {
public:
ChecksumFunction(State (&algorithm)(std::span<const byte>, State))
: ChecksumAlgorithm(std::as_bytes(std::span(&state_, 1))),
algorithm_(algorithm) {}
void Reset() override { state_ = {}; }
void Update(std::span<const byte> data) override {
state_ = algorithm_(data, state_);
}
private:
State state_;
State (&algorithm_)(std::span<const byte>, State);
};
ChecksumFunction<uint32_t> default_checksum(SimpleChecksum);
// Returns a buffer containing the necessary padding for an entry.
template <size_t kAlignmentBytes, size_t kKeyLength, size_t kValueSize = 0>
constexpr auto EntryPadding() {
constexpr size_t content =
sizeof(internal::EntryHeader) + kKeyLength + kValueSize;
return std::array<byte, Padding(content, kAlignmentBytes)>{};
}
// Creates a buffer containing a valid entry at compile time.
template <uint32_t (*kChecksum)(std::span<const byte>,
uint32_t) = &SimpleChecksum,
size_t kAlignmentBytes = sizeof(internal::EntryHeader),
size_t kKeyLengthWithNull,
size_t kValueSize>
constexpr auto MakeValidEntry(uint32_t magic,
uint32_t id,
const char (&key)[kKeyLengthWithNull],
const std::array<byte, kValueSize>& value) {
constexpr size_t kKeyLength = kKeyLengthWithNull - 1;
auto data =
bytes::Concat(magic,
uint32_t(0),
uint8_t(kAlignmentBytes / 16 - 1),
uint8_t(kKeyLength),
uint16_t(kValueSize),
id,
bytes::String(key),
std::span(value),
EntryPadding<kAlignmentBytes, kKeyLength, kValueSize>());
// Calculate the checksum
uint32_t checksum = kChecksum(data, 0);
for (size_t i = 0; i < sizeof(checksum); ++i) {
data[4 + i] = byte(checksum & 0xff);
checksum >>= 8;
}
return data;
}
// Creates a buffer containing a deleted entry at compile time.
template <uint32_t (*kChecksum)(std::span<const byte>,
uint32_t) = &SimpleChecksum,
size_t kAlignmentBytes = sizeof(internal::EntryHeader),
size_t kKeyLengthWithNull>
constexpr auto MakeDeletedEntry(uint32_t magic,
uint32_t id,
const char (&key)[kKeyLengthWithNull]) {
constexpr size_t kKeyLength = kKeyLengthWithNull - 1;
auto data = bytes::Concat(magic,
uint32_t(0),
uint8_t(kAlignmentBytes / 16 - 1),
uint8_t(kKeyLength),
uint16_t(0xFFFF),
id,
bytes::String(key),
EntryPadding<kAlignmentBytes, kKeyLength>());
// Calculate the checksum
uint32_t checksum = kChecksum(data, 0);
for (size_t i = 0; i < sizeof(checksum); ++i) {
data[4 + i] = byte(checksum & 0xff);
checksum >>= 8;
}
return data;
}
// For KVS magic value always use a random 32 bit integer rather than a
// human readable 4 bytes. See pw_kvs/format.h for more information.
constexpr uint32_t kMagic = 0x5ab2f0b5;
constexpr Options kNoGcOptions{
.gc_on_write = GargbageCollectOnWrite::kDisabled,
.recovery = ErrorRecovery::kManual,
.verify_on_read = true,
.verify_on_write = true,
};
constexpr Options kRecoveryNoGcOptions{
.gc_on_write = GargbageCollectOnWrite::kDisabled,
.recovery = ErrorRecovery::kLazy,
.verify_on_read = true,
.verify_on_write = true,
};
constexpr Options kRecoveryLazyGcOptions{
.gc_on_write = GargbageCollectOnWrite::kOneSector,
.recovery = ErrorRecovery::kLazy,
.verify_on_read = true,
.verify_on_write = true,
};
constexpr auto kEntry1 =
MakeValidEntry(kMagic, 1, "key1", bytes::String("value1"));
constexpr auto kEntry2 =
MakeValidEntry(kMagic, 3, "k2", bytes::String("value2"));
constexpr auto kEntry3 =
MakeValidEntry(kMagic, 4, "k3y", bytes::String("value3"));
constexpr auto kEntry4 =
MakeValidEntry(kMagic, 5, "4k", bytes::String("value4"));
constexpr auto kEmpty32Bytes = bytes::Initialized<32>(0xff);
static_assert(sizeof(kEmpty32Bytes) == 32);
EntryFormat default_format = {.magic = kMagic, .checksum = &default_checksum};
class KvsErrorHandling : public ::testing::Test {
protected:
KvsErrorHandling()
: flash_(internal::Entry::kMinAlignmentBytes),
partition_(&flash_),
kvs_(&partition_, default_format, kNoGcOptions) {}
void InitFlashTo(std::span<const byte> contents) {
partition_.Erase()
.IgnoreError(); // TODO(pwbug/387): Handle Status properly
std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
}
FakeFlashMemoryBuffer<512, 4> flash_;
FlashPartition partition_;
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
};
TEST_F(KvsErrorHandling, Init_Ok) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
EXPECT_EQ(OkStatus(), kvs_.Init());
byte buffer[64];
EXPECT_EQ(OkStatus(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
}
TEST_F(KvsErrorHandling, Init_DuplicateEntries_ReturnsDataLossButReadsEntry) {
InitFlashTo(bytes::Concat(kEntry1, kEntry1));
EXPECT_EQ(Status::DataLoss(), kvs_.Init());
byte buffer[64];
EXPECT_EQ(OkStatus(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(Status::NotFound(), kvs_.Get("k2", buffer).status());
}
TEST_F(KvsErrorHandling, Init_CorruptEntry_FindsSubsequentValidEntry) {
// Corrupt each byte in the first entry once.
for (size_t i = 0; i < kEntry1.size(); ++i) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
flash_.buffer()[i] = byte(int(flash_.buffer()[i]) + 1);
ASSERT_EQ(Status::DataLoss(), kvs_.Init());
byte buffer[64];
ASSERT_EQ(Status::NotFound(), kvs_.Get("key1", buffer).status());
ASSERT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
auto stats = kvs_.GetStorageStats();
// One valid entry.
ASSERT_EQ(32u, stats.in_use_bytes);
// Rest of space is reclaimable as the sector is corrupt.
ASSERT_EQ(480u, stats.reclaimable_bytes);
}
}
TEST_F(KvsErrorHandling, Init_CorruptEntry_CorrectlyAccountsForSectorSize) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2, kEntry3, kEntry4));
// Corrupt the first and third entries.
flash_.buffer()[9] = byte(0xef);
flash_.buffer()[67] = byte(0xef);
ASSERT_EQ(Status::DataLoss(), kvs_.Init());
EXPECT_EQ(2u, kvs_.size());
byte buffer[64];
EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
EXPECT_EQ(Status::NotFound(), kvs_.Get("k3y", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("4k", buffer).status());
auto stats = kvs_.GetStorageStats();
ASSERT_EQ(64u, stats.in_use_bytes);
ASSERT_EQ(448u, stats.reclaimable_bytes);
ASSERT_EQ(1024u, stats.writable_bytes);
}
TEST_F(KvsErrorHandling, Init_ReadError_InitializedWithSingleEntryError) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
flash_.InjectReadError(
FlashError::InRange(Status::Unauthenticated(), kEntry1.size()));
EXPECT_EQ(Status::DataLoss(), kvs_.Init());
EXPECT_FALSE(kvs_.initialized());
}
TEST_F(KvsErrorHandling, Init_CorruptSectors_ShouldBeUnwritable) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
// Corrupt 3 of the 4 512-byte flash sectors. Corrupt sectors should be
// unwritable, and the KVS must maintain one empty sector at all times.
// As GC on write is disabled through KVS options, writes should no longer be
// possible due to lack of space.
flash_.buffer()[1] = byte(0xef);
flash_.buffer()[513] = byte(0xef);
flash_.buffer()[1025] = byte(0xef);
ASSERT_EQ(Status::DataLoss(), kvs_.Init());
EXPECT_EQ(Status::FailedPrecondition(),
kvs_.Put("hello", bytes::String("world")));
EXPECT_EQ(Status::FailedPrecondition(), kvs_.Put("a", bytes::String("b")));
// Existing valid entries should still be readable.
EXPECT_EQ(1u, kvs_.size());
byte buffer[64];
EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(32u, stats.in_use_bytes);
EXPECT_EQ(480u + 2 * 512u, stats.reclaimable_bytes);
EXPECT_EQ(0u, stats.writable_bytes);
}
TEST_F(KvsErrorHandling, Init_CorruptSectors_ShouldRecoverOne) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
// Corrupt all of the 4 512-byte flash sectors. Leave the pre-init entries
// intact. The KVS should be unavailable because recovery is set to full
// manual, and it does not have the required one empty sector at all times.
flash_.buffer()[64] = byte(0xef);
flash_.buffer()[513] = byte(0xef);
flash_.buffer()[1025] = byte(0xef);
flash_.buffer()[1537] = byte(0xef);
ASSERT_EQ(Status::DataLoss(), kvs_.Init());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(64u, stats.in_use_bytes);
EXPECT_EQ(4 * 512u - 64u, stats.reclaimable_bytes);
EXPECT_EQ(0u, stats.writable_bytes);
}
// Currently disabled due to KVS failing the test. KVS fails due to Init bailing
// out when it sees a small patch of "erased" looking flash space, which could
// result in missing keys that are actually written after a write error in
// flash.
TEST_F(KvsErrorHandling, DISABLED_Init_OkWithWriteErrorOnFlash) {
InitFlashTo(bytes::Concat(kEntry1, kEmpty32Bytes, kEntry2));
EXPECT_EQ(Status::DataLoss(), kvs_.Init());
byte buffer[64];
EXPECT_EQ(2u, kvs_.size());
EXPECT_EQ(true, kvs_.error_detected());
EXPECT_EQ(OkStatus(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(64u, stats.in_use_bytes);
EXPECT_EQ(512u - 64u, stats.reclaimable_bytes);
EXPECT_EQ(2 * 512u, stats.writable_bytes);
}
TEST_F(KvsErrorHandling, Init_CorruptKey_RevertsToPreviousVersion) {
constexpr auto kVersion7 =
MakeValidEntry(kMagic, 7, "my_key", bytes::String("version 7"));
constexpr auto kVersion8 =
MakeValidEntry(kMagic, 8, "my_key", bytes::String("version 8"));
InitFlashTo(bytes::Concat(kVersion7, kVersion8));
// Corrupt a byte of entry version 8 (addresses 32-63).
flash_.buffer()[34] = byte(0xef);
ASSERT_EQ(Status::DataLoss(), kvs_.Init());
char buffer[64] = {};
EXPECT_EQ(1u, kvs_.size());
auto result = kvs_.Get("my_key", std::as_writable_bytes(std::span(buffer)));
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(sizeof("version 7") - 1, result.size());
EXPECT_STREQ("version 7", buffer);
EXPECT_EQ(32u, kvs_.GetStorageStats().in_use_bytes);
}
// The Put_WriteFailure_EntryNotAddedButBytesMarkedWritten test is run with both
// the KvsErrorRecovery and KvsErrorHandling test fixtures (different KVS
// configurations).
TEST_F(KvsErrorHandling, Put_WriteFailure_EntryNotAddedButBytesMarkedWritten) {
ASSERT_EQ(OkStatus(), kvs_.Init());
flash_.InjectWriteError(FlashError::Unconditional(Status::Unavailable(), 1));
EXPECT_EQ(Status::Unavailable(), kvs_.Put("key1", bytes::String("value1")));
EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", std::span<byte>()).status());
ASSERT_TRUE(kvs_.empty());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, 0u);
EXPECT_EQ(stats.reclaimable_bytes, 512u);
EXPECT_EQ(stats.writable_bytes, 512u * 2);
// The bytes were marked used, so a new key should not overlap with the bytes
// from the failed Put.
EXPECT_EQ(OkStatus(), kvs_.Put("key1", bytes::String("value1")));
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (32u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 512u);
EXPECT_EQ(stats.writable_bytes, 512u * 2 - (32 * kvs_.redundancy()));
}
class KvsErrorRecovery : public ::testing::Test {
protected:
KvsErrorRecovery()
: flash_(internal::Entry::kMinAlignmentBytes),
partition_(&flash_),
kvs_(&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kRecoveryNoGcOptions) {}
void InitFlashTo(std::span<const byte> contents) {
partition_.Erase()
.IgnoreError(); // TODO(pwbug/387): Handle Status properly
std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
}
FakeFlashMemoryBuffer<512, 4> flash_;
FlashPartition partition_;
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
};
TEST_F(KvsErrorRecovery, Init_Ok) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
EXPECT_EQ(OkStatus(), kvs_.Init());
byte buffer[64];
EXPECT_EQ(OkStatus(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
}
TEST_F(KvsErrorRecovery, Init_DuplicateEntries_RecoversDuringInit) {
InitFlashTo(bytes::Concat(kEntry1, kEntry1));
EXPECT_EQ(OkStatus(), kvs_.Init());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.corrupt_sectors_recovered, 1u);
byte buffer[64];
EXPECT_EQ(OkStatus(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(Status::NotFound(), kvs_.Get("k2", buffer).status());
}
TEST_F(KvsErrorRecovery, Init_CorruptEntry_FindsSubsequentValidEntry) {
// Corrupt each byte in the first entry once.
for (size_t i = 0; i < kEntry1.size(); ++i) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
flash_.buffer()[i] = byte(int(flash_.buffer()[i]) + 1);
ASSERT_EQ(OkStatus(), kvs_.Init());
byte buffer[64];
ASSERT_EQ(Status::NotFound(), kvs_.Get("key1", buffer).status());
ASSERT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
auto stats = kvs_.GetStorageStats();
// One valid entry.
ASSERT_EQ(32u, stats.in_use_bytes);
// The sector with corruption should have been recovered.
ASSERT_EQ(0u, stats.reclaimable_bytes);
ASSERT_EQ(i + 1u, stats.corrupt_sectors_recovered);
}
}
TEST_F(KvsErrorRecovery, Init_CorruptEntry_CorrectlyAccountsForSectorSize) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2, kEntry3, kEntry4));
// Corrupt the first and third entries.
flash_.buffer()[9] = byte(0xef);
flash_.buffer()[67] = byte(0xef);
ASSERT_EQ(OkStatus(), kvs_.Init());
EXPECT_EQ(2u, kvs_.size());
byte buffer[64];
EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
EXPECT_EQ(Status::NotFound(), kvs_.Get("k3y", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("4k", buffer).status());
auto stats = kvs_.GetStorageStats();
ASSERT_EQ(64u, stats.in_use_bytes);
ASSERT_EQ(0u, stats.reclaimable_bytes);
ASSERT_EQ(1472u, stats.writable_bytes);
ASSERT_EQ(1u, stats.corrupt_sectors_recovered);
}
TEST_F(KvsErrorRecovery, Init_ReadError_InitializedWithSingleEntryError) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
flash_.InjectReadError(
FlashError::InRange(Status::Unauthenticated(), kEntry1.size()));
EXPECT_EQ(OkStatus(), kvs_.Init());
EXPECT_TRUE(kvs_.initialized());
auto stats = kvs_.GetStorageStats();
ASSERT_EQ(32u, stats.in_use_bytes);
ASSERT_EQ(0u, stats.reclaimable_bytes);
ASSERT_EQ(3 * 512u - 32u, stats.writable_bytes);
ASSERT_EQ(1u, stats.corrupt_sectors_recovered);
ASSERT_EQ(0u, stats.missing_redundant_entries_recovered);
}
TEST_F(KvsErrorRecovery, Init_CorruptSectors_ShouldBeUnwritable) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
// Corrupt 3 of the 4 512-byte flash sectors. Corrupt sectors should be
// recovered via garbage collection.
flash_.buffer()[1] = byte(0xef);
flash_.buffer()[513] = byte(0xef);
flash_.buffer()[1025] = byte(0xef);
ASSERT_EQ(OkStatus(), kvs_.Init());
EXPECT_EQ(OkStatus(), kvs_.Put("hello", bytes::String("world")));
EXPECT_EQ(OkStatus(), kvs_.Put("a", bytes::String("b")));
// Existing valid entries should still be readable.
EXPECT_EQ(3u, kvs_.size());
byte buffer[64];
EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(96u, stats.in_use_bytes);
EXPECT_EQ(0u, stats.reclaimable_bytes);
EXPECT_EQ(1440u, stats.writable_bytes);
EXPECT_EQ(3u, stats.corrupt_sectors_recovered);
}
TEST_F(KvsErrorRecovery, Init_CorruptSectors_ShouldRecoverOne) {
InitFlashTo(bytes::Concat(kEntry1, kEntry2));
// Corrupt all of the 4 512-byte flash sectors. Leave the pre-init entries
// intact. As part of recovery all corrupt sectors should get garbage
// collected.
flash_.buffer()[64] = byte(0xef);
flash_.buffer()[513] = byte(0xef);
flash_.buffer()[1025] = byte(0xef);
flash_.buffer()[1537] = byte(0xef);
ASSERT_EQ(OkStatus(), kvs_.Init());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(64u, stats.in_use_bytes);
EXPECT_EQ(0u, stats.reclaimable_bytes);
EXPECT_EQ(3 * 512u - 64u, stats.writable_bytes);
EXPECT_EQ(4u, stats.corrupt_sectors_recovered);
}
// Currently disabled due to KVS failing the test. KVS fails due to Init bailing
// out when it sees a small patch of "erased" looking flash space, which could
// result in missing keys that are actually written after a write error in
// flash.
TEST_F(KvsErrorRecovery, DISABLED_Init_OkWithWriteErrorOnFlash) {
InitFlashTo(bytes::Concat(kEntry1, kEmpty32Bytes, kEntry2));
EXPECT_EQ(OkStatus(), kvs_.Init());
byte buffer[64];
EXPECT_EQ(2u, kvs_.size());
EXPECT_EQ(false, kvs_.error_detected());
EXPECT_EQ(OkStatus(), kvs_.Get("key1", buffer).status());
EXPECT_EQ(OkStatus(), kvs_.Get("k2", buffer).status());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(64u, stats.in_use_bytes);
EXPECT_EQ(0u, stats.reclaimable_bytes);
EXPECT_EQ(3 * 512u - 64u, stats.writable_bytes);
EXPECT_EQ(1u, stats.corrupt_sectors_recovered);
EXPECT_EQ(0u, stats.missing_redundant_entries_recovered);
}
TEST_F(KvsErrorRecovery, Init_CorruptKey_RevertsToPreviousVersion) {
constexpr auto kVersion7 =
MakeValidEntry(kMagic, 7, "my_key", bytes::String("version 7"));
constexpr auto kVersion8 =
MakeValidEntry(kMagic, 8, "my_key", bytes::String("version 8"));
InitFlashTo(bytes::Concat(kVersion7, kVersion8));
// Corrupt a byte of entry version 8 (addresses 32-63).
flash_.buffer()[34] = byte(0xef);
ASSERT_EQ(OkStatus(), kvs_.Init());
char buffer[64] = {};
EXPECT_EQ(1u, kvs_.size());
auto result = kvs_.Get("my_key", std::as_writable_bytes(std::span(buffer)));
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(sizeof("version 7") - 1, result.size());
EXPECT_STREQ("version 7", buffer);
EXPECT_EQ(32u, kvs_.GetStorageStats().in_use_bytes);
}
// The Put_WriteFailure_EntryNotAddedButBytesMarkedWritten test is run with both
// the KvsErrorRecovery and KvsErrorHandling test fixtures (different KVS
// configurations).
TEST_F(KvsErrorRecovery, Put_WriteFailure_EntryNotAddedButBytesMarkedWritten) {
ASSERT_EQ(OkStatus(), kvs_.Init());
flash_.InjectWriteError(FlashError::Unconditional(Status::Unavailable(), 1));
EXPECT_EQ(Status::Unavailable(), kvs_.Put("key1", bytes::String("value1")));
EXPECT_EQ(true, kvs_.error_detected());
EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", std::span<byte>()).status());
ASSERT_TRUE(kvs_.empty());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, 0u);
EXPECT_EQ(stats.reclaimable_bytes, 512u);
EXPECT_EQ(stats.writable_bytes, 512u * 2);
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
// The bytes were marked used, so a new key should not overlap with the bytes
// from the failed Put.
EXPECT_EQ(OkStatus(), kvs_.Put("key1", bytes::String("value1")));
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (32u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 512u);
EXPECT_EQ(stats.writable_bytes, 512u * 2 - (32 * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
}
// For KVS magic value always use a random 32 bit integer rather than a
// human readable 4 bytes. See pw_kvs/format.h for more information.
constexpr uint32_t kAltMagic = 0x41a2db83;
constexpr uint32_t AltChecksum(std::span<const byte> data, uint32_t state) {
for (byte b : data) {
state = (state << 8) | uint32_t(byte(state >> 24) ^ b);
}
return state;
}
ChecksumFunction<uint32_t> alt_checksum(AltChecksum);
constexpr auto kAltEntry =
MakeValidEntry<AltChecksum>(kAltMagic, 32, "A Key", bytes::String("XD"));
constexpr uint32_t NoChecksum(std::span<const byte>, uint32_t) { return 0; }
// For KVS magic value always use a random 32 bit integer rather than a
// human readable 4 bytes. See pw_kvs/format.h for more information.
constexpr uint32_t kNoChecksumMagic = 0xd49ba138;
constexpr auto kNoChecksumEntry = MakeValidEntry<NoChecksum>(
kNoChecksumMagic, 64, "kee", bytes::String("O_o"));
constexpr auto kDeletedEntry =
MakeDeletedEntry<AltChecksum>(kAltMagic, 128, "gone");
class InitializedRedundantMultiMagicKvs : public ::testing::Test {
protected:
static constexpr auto kInitialContents = bytes::Concat(
kNoChecksumEntry, kEntry1, kAltEntry, kEntry2, kEntry3, kDeletedEntry);
InitializedRedundantMultiMagicKvs()
: flash_(internal::Entry::kMinAlignmentBytes),
partition_(&flash_),
kvs_(&partition_,
{{
{.magic = kMagic, .checksum = &default_checksum},
{.magic = kAltMagic, .checksum = &alt_checksum},
{.magic = kNoChecksumMagic, .checksum = nullptr},
}},
kRecoveryNoGcOptions) {
partition_.Erase()
.IgnoreError(); // TODO(pwbug/387): Handle Status properly
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
EXPECT_EQ(OkStatus(), kvs_.Init());
}
FakeFlashMemoryBuffer<512, 4, 3> flash_;
FlashPartition partition_;
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 2, 3> kvs_;
};
#define ASSERT_CONTAINS_ENTRY(key, str_value) \
do { \
char val[sizeof(str_value)] = {}; \
StatusWithSize stat = \
kvs_.Get(key, std::as_writable_bytes(std::span(val))); \
ASSERT_EQ(OkStatus(), stat.status()); \
ASSERT_EQ(sizeof(str_value) - 1, stat.size()); \
ASSERT_STREQ(str_value, val); \
} while (0)
TEST_F(InitializedRedundantMultiMagicKvs, AllEntriesArePresent) {
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("A Key", "XD");
ASSERT_CONTAINS_ENTRY("kee", "O_o");
}
TEST_F(InitializedRedundantMultiMagicKvs, RecoversLossOfFirstSector) {
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 512u * 3 - (192 * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
EXPECT_EQ(OkStatus(), partition_.Erase(0, 1));
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("A Key", "XD");
ASSERT_CONTAINS_ENTRY("kee", "O_o");
EXPECT_EQ(true, kvs_.error_detected());
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 320u);
EXPECT_EQ(stats.writable_bytes, 512u * 2 - (192 * (kvs_.redundancy() - 1)));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
EXPECT_EQ(OkStatus(), kvs_.FullMaintenance());
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 512u * 3 - (192 * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 6u);
}
TEST_F(InitializedRedundantMultiMagicKvs, RecoversLossOfSecondSector) {
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 512u * 3 - (192 * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
EXPECT_EQ(OkStatus(), partition_.Erase(partition_.sector_size_bytes(), 1));
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("A Key", "XD");
ASSERT_CONTAINS_ENTRY("kee", "O_o");
EXPECT_EQ(false, kvs_.error_detected());
EXPECT_EQ(OkStatus(), kvs_.Init());
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 512u * 3 - (192 * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
}
TEST_F(InitializedRedundantMultiMagicKvs, SingleReadErrors) {
// Inject 2 read errors, so the first read attempt fully fails.
flash_.InjectReadError(FlashError::Unconditional(Status::Internal(), 2));
flash_.InjectReadError(FlashError::Unconditional(Status::Internal(), 1, 7));
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("A Key", "XD");
ASSERT_CONTAINS_ENTRY("kee", "O_o");
EXPECT_EQ(true, kvs_.error_detected());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 320u);
EXPECT_EQ(stats.writable_bytes, 512u * 2 - (192 * (kvs_.redundancy() - 1)));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
}
TEST_F(InitializedRedundantMultiMagicKvs, SingleWriteError) {
flash_.InjectWriteError(FlashError::Unconditional(Status::Internal(), 1, 1));
EXPECT_EQ(Status::Internal(), kvs_.Put("new key", bytes::String("abcd?")));
EXPECT_EQ(true, kvs_.error_detected());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, 32 + (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 320u);
EXPECT_EQ(stats.writable_bytes,
512u * 2 - 32 - (192 * (kvs_.redundancy() - 1)));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
char val[20] = {};
EXPECT_EQ(
OkStatus(),
kvs_.Get("new key", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(OkStatus(), kvs_.FullMaintenance());
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (224u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 512u * 3 - (224 * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
EXPECT_EQ(
OkStatus(),
kvs_.Get("new key", std::as_writable_bytes(std::span(val))).status());
}
TEST_F(InitializedRedundantMultiMagicKvs, DataLossAfterLosingBothCopies) {
EXPECT_EQ(OkStatus(), partition_.Erase(0, 2));
char val[20] = {};
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("key1", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("k2", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("k3y", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("A Key", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("kee", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(true, kvs_.error_detected());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (192u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 2 * 320u);
EXPECT_EQ(stats.writable_bytes, 512u);
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
}
TEST_F(InitializedRedundantMultiMagicKvs, PutNewEntry_UsesFirstFormat) {
EXPECT_EQ(OkStatus(), kvs_.Put("new key", bytes::String("abcd?")));
constexpr auto kNewEntry =
MakeValidEntry(kMagic, 129, "new key", bytes::String("abcd?"));
EXPECT_EQ(0,
std::memcmp(kNewEntry.data(),
flash_.buffer().data() + kInitialContents.size(),
kNewEntry.size()));
ASSERT_CONTAINS_ENTRY("new key", "abcd?");
}
TEST_F(InitializedRedundantMultiMagicKvs, PutExistingEntry_UsesFirstFormat) {
EXPECT_EQ(OkStatus(), kvs_.Put("A Key", bytes::String("New value!")));
constexpr auto kNewEntry =
MakeValidEntry(kMagic, 129, "A Key", bytes::String("New value!"));
EXPECT_EQ(0,
std::memcmp(kNewEntry.data(),
flash_.buffer().data() + kInitialContents.size(),
kNewEntry.size()));
ASSERT_CONTAINS_ENTRY("A Key", "New value!");
}
#define ASSERT_KVS_CONTAINS_ENTRY(kvs, key, str_value) \
do { \
char val[sizeof(str_value)] = {}; \
StatusWithSize stat = \
kvs.Get(key, std::as_writable_bytes(std::span(val))); \
ASSERT_EQ(OkStatus(), stat.status()); \
ASSERT_EQ(sizeof(str_value) - 1, stat.size()); \
ASSERT_STREQ(str_value, val); \
} while (0)
TEST_F(InitializedRedundantMultiMagicKvs, UpdateEntryFormat) {
ASSERT_EQ(OkStatus(), kvs_.FullMaintenance());
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 2, 1> local_kvs(
&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kNoGcOptions);
ASSERT_EQ(OkStatus(), local_kvs.Init());
EXPECT_EQ(false, local_kvs.error_detected());
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "key1", "value1");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k2", "value2");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k3y", "value3");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "A Key", "XD");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "kee", "O_o");
}
class InitializedMultiMagicKvs : public ::testing::Test {
protected:
static constexpr auto kInitialContents =
bytes::Concat(kNoChecksumEntry, kEntry1, kAltEntry, kEntry2, kEntry3);
InitializedMultiMagicKvs()
: flash_(internal::Entry::kMinAlignmentBytes),
partition_(&flash_),
kvs_(&partition_,
{{
{.magic = kMagic, .checksum = &default_checksum},
{.magic = kAltMagic, .checksum = &alt_checksum},
{.magic = kNoChecksumMagic, .checksum = nullptr},
}},
kRecoveryNoGcOptions) {
partition_.Erase()
.IgnoreError(); // TODO(pwbug/387): Handle Status properly
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
EXPECT_EQ(OkStatus(), kvs_.Init());
}
FakeFlashMemoryBuffer<512, 4, 3> flash_;
FlashPartition partition_;
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 1, 3> kvs_;
};
// Similar to test for InitializedRedundantMultiMagicKvs. Doing similar test
// with different KVS configuration.
TEST_F(InitializedMultiMagicKvs, AllEntriesArePresent) {
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("A Key", "XD");
ASSERT_CONTAINS_ENTRY("kee", "O_o");
}
// Similar to test for InitializedRedundantMultiMagicKvs. Doing similar test
// with different KVS configuration.
TEST_F(InitializedMultiMagicKvs, UpdateEntryFormat) {
ASSERT_EQ(OkStatus(), kvs_.FullMaintenance());
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 1, 1> local_kvs(
&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kNoGcOptions);
ASSERT_EQ(OkStatus(), local_kvs.Init());
EXPECT_EQ(false, local_kvs.error_detected());
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "key1", "value1");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k2", "value2");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k3y", "value3");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "A Key", "XD");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "kee", "O_o");
}
class InitializedRedundantLazyRecoveryKvs : public ::testing::Test {
protected:
static constexpr auto kInitialContents =
bytes::Concat(kEntry1, kEntry2, kEntry3, kEntry4);
InitializedRedundantLazyRecoveryKvs()
: flash_(internal::Entry::kMinAlignmentBytes),
partition_(&flash_),
kvs_(&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kRecoveryLazyGcOptions) {
partition_.Erase()
.IgnoreError(); // TODO(pwbug/387): Handle Status properly
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
EXPECT_EQ(OkStatus(), kvs_.Init());
}
FakeFlashMemoryBuffer<512, 4, 3> flash_;
FlashPartition partition_;
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 2> kvs_;
};
TEST_F(InitializedRedundantLazyRecoveryKvs, WriteAfterDataLoss) {
EXPECT_EQ(OkStatus(), partition_.Erase(0, 4));
char val[20] = {};
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("key1", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("k2", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("k3y", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(Status::DataLoss(),
kvs_.Get("4k", std::as_writable_bytes(std::span(val))).status());
EXPECT_EQ(true, kvs_.error_detected());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (128u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 2 * 384u);
EXPECT_EQ(stats.writable_bytes, 512u);
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
ASSERT_EQ(Status::DataLoss(), kvs_.Put("key1", 1000));
EXPECT_EQ(OkStatus(), kvs_.FullMaintenance());
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, 0u);
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 3 * 512u);
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
}
TEST_F(InitializedRedundantLazyRecoveryKvs, TwoSectorsCorruptWithGoodEntries) {
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("4k", "value4");
EXPECT_EQ(false, kvs_.error_detected());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (128u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 3 * 512u - (128u * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
// Corrupt all the keys, alternating which copy gets corrupted.
flash_.buffer()[0x10] = byte(0xef);
flash_.buffer()[0x230] = byte(0xef);
flash_.buffer()[0x50] = byte(0xef);
flash_.buffer()[0x270] = byte(0xef);
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("4k", "value4");
EXPECT_EQ(OkStatus(), kvs_.FullMaintenance());
stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (128u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 3 * 512u - (128u * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 2u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 4u);
}
class InitializedLazyRecoveryKvs : public ::testing::Test {
protected:
static constexpr auto kInitialContents =
bytes::Concat(kEntry1, kEntry2, kEntry3, kEntry4);
InitializedLazyRecoveryKvs()
: flash_(internal::Entry::kMinAlignmentBytes),
partition_(&flash_),
kvs_(&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kRecoveryLazyGcOptions) {
partition_.Erase()
.IgnoreError(); // TODO(pwbug/387): Handle Status properly
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
EXPECT_EQ(OkStatus(), kvs_.Init());
}
FakeFlashMemoryBuffer<512, 8> flash_;
FlashPartition partition_;
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
};
// Test a KVS with a number of entries, several sectors that are nearly full
// of stale (reclaimable) space, and not enough writable (free) space to add a
// redundant copy for any of the entries. Tests that the add redundancy step of
// repair is able to use garbage collection to free up space needed for the new
// copies.
TEST_F(InitializedLazyRecoveryKvs, AddRedundancyToKvsFullOfStaleData) {
// Verify the pre-initialized key are present in the KVS.
ASSERT_CONTAINS_ENTRY("key1", "value1");
ASSERT_CONTAINS_ENTRY("k2", "value2");
ASSERT_CONTAINS_ENTRY("k3y", "value3");
ASSERT_CONTAINS_ENTRY("4k", "value4");
EXPECT_EQ(false, kvs_.error_detected());
auto stats = kvs_.GetStorageStats();
EXPECT_EQ(stats.in_use_bytes, (128u * kvs_.redundancy()));
EXPECT_EQ(stats.reclaimable_bytes, 0u);
EXPECT_EQ(stats.writable_bytes, 7 * 512u - (128u * kvs_.redundancy()));
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
// Block of data to use for entry value. Sized to 470 so the total entry
// results in the 512 byte sector having 16 bytes remaining.
uint8_t test_data[470] = {1, 2, 3, 4, 5, 6};
// Add a near-sector size key entry to fill the KVS with a valid large entry
// and stale data. Modify the value in between Puts so it actually writes
// (identical value writes are skipped).
EXPECT_EQ(OkStatus(), kvs_.Put("big_key", test_data));
test_data[0]++;
EXPECT_EQ(OkStatus(), kvs_.Put("big_key", test_data));
test_data[0]++;
EXPECT_EQ(OkStatus(), kvs_.Put("big_key", test_data));
test_data[0]++;
EXPECT_EQ(OkStatus(), kvs_.Put("big_key", test_data));
test_data[0]++;
EXPECT_EQ(OkStatus(), kvs_.Put("big_key", test_data));
test_data[0]++;
EXPECT_EQ(OkStatus(), kvs_.Put("big_key", test_data));
// Instantiate a new KVS with redundancy of 2. This KVS should add an extra
// copy of each valid key as part of the init process. Because there is not
// enough writable space, the adding redundancy will need to garbage collect
// two sectors.
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 2> local_kvs(
&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kRecoveryLazyGcOptions);
ASSERT_EQ(OkStatus(), local_kvs.Init());
// Verify no errors found in the new KVS and all the entries are present.
EXPECT_EQ(false, local_kvs.error_detected());
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "key1", "value1");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k2", "value2");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "k3y", "value3");
ASSERT_KVS_CONTAINS_ENTRY(local_kvs, "4k", "value4");
StatusWithSize big_key_size = local_kvs.ValueSize("big_key");
EXPECT_EQ(OkStatus(), big_key_size.status());
EXPECT_EQ(sizeof(test_data), big_key_size.size());
// Verify that storage stats of the new redundant KVS match expected values.
stats = local_kvs.GetStorageStats();
// Expected in-use bytes is size of (pre-init keys + big key) * redundancy.
EXPECT_EQ(stats.in_use_bytes, ((128u + 496u) * local_kvs.redundancy()));
// Expected reclaimable space is number of stale entries remaining for big_key
// (3 after GC to add redundancy) * total sizeof big_key entry (496 bytes).
EXPECT_EQ(stats.reclaimable_bytes, 496u * 3u);
// Expected writable bytes is total writable size (512 * 7) - valid bytes (in
// use) - reclaimable bytes.
EXPECT_EQ(stats.writable_bytes, 848u);
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
}
} // namespace
} // namespace pw::kvs