blob: e914c39e19fa04e1313aa43b51fe13f24e7b8a1e [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.
#define DUMP_KVS_STATE_TO_FILE 0
#define USE_MEMORY_BUFFER 1
#define PW_LOG_USE_ULTRA_SHORT_NAMES 1
#include "pw_kvs/key_value_store.h"
#include <array>
#include <cstdio>
#include <cstring>
#if DUMP_KVS_STATE_TO_FILE
#include <vector>
#endif // DUMP_KVS_STATE_TO_FILE
#include "gtest/gtest.h"
#include "pw_checksum/ccitt_crc16.h"
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/internal/entry.h"
#include "pw_kvs_private/byte_utils.h"
#include "pw_kvs_private/macros.h"
#include "pw_log/log.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_string/string_builder.h"
#if USE_MEMORY_BUFFER
#include "pw_kvs/in_memory_fake_flash.h"
#endif // USE_MEMORY_BUFFER
namespace pw::kvs {
namespace {
using internal::EntryHeader;
using std::byte;
constexpr size_t kMaxEntries = 256;
constexpr size_t kMaxUsableSectors = 256;
// Test the functions in byte_utils.h. Create a byte array with AsBytes and
// ByteStr and check that its contents are correct.
constexpr std::array<char, 2> kTestArray = {'a', 'b'};
constexpr auto kAsBytesTest = AsBytes(
'a', uint16_t(1), uint8_t(23), kTestArray, ByteStr("c"), uint64_t(-1));
static_assert(kAsBytesTest.size() == 15);
static_assert(kAsBytesTest[0] == std::byte{'a'});
static_assert(kAsBytesTest[1] == std::byte{1});
static_assert(kAsBytesTest[2] == std::byte{0});
static_assert(kAsBytesTest[3] == std::byte{23});
static_assert(kAsBytesTest[4] == std::byte{'a'});
static_assert(kAsBytesTest[5] == std::byte{'b'});
static_assert(kAsBytesTest[6] == std::byte{'c'});
static_assert(kAsBytesTest[7] == std::byte{0xff});
static_assert(kAsBytesTest[8] == std::byte{0xff});
static_assert(kAsBytesTest[9] == std::byte{0xff});
static_assert(kAsBytesTest[10] == std::byte{0xff});
static_assert(kAsBytesTest[11] == std::byte{0xff});
static_assert(kAsBytesTest[12] == std::byte{0xff});
static_assert(kAsBytesTest[13] == std::byte{0xff});
static_assert(kAsBytesTest[14] == std::byte{0xff});
// Test that the ConvertsToSpan trait correctly idenitifies types that convert
// to span.
static_assert(!ConvertsToSpan<int>());
static_assert(!ConvertsToSpan<void>());
static_assert(!ConvertsToSpan<std::byte>());
static_assert(!ConvertsToSpan<std::byte*>());
static_assert(ConvertsToSpan<std::array<int, 5>>());
static_assert(ConvertsToSpan<decltype("Hello!")>());
static_assert(ConvertsToSpan<std::string_view>());
static_assert(ConvertsToSpan<std::string_view&>());
static_assert(ConvertsToSpan<std::string_view&&>());
static_assert(ConvertsToSpan<const std::string_view>());
static_assert(ConvertsToSpan<const std::string_view&>());
static_assert(ConvertsToSpan<const std::string_view&&>());
static_assert(ConvertsToSpan<span<int>>());
static_assert(ConvertsToSpan<span<byte>>());
static_assert(ConvertsToSpan<span<const int*>>());
static_assert(ConvertsToSpan<span<bool>&&>());
static_assert(ConvertsToSpan<const span<bool>&>());
static_assert(ConvertsToSpan<span<bool>&&>());
// This is a self contained flash unit with both memory and a single partition.
template <uint32_t sector_size_bytes, uint16_t sector_count>
struct FlashWithPartitionFake {
// Default to 16 byte alignment, which is common in practice.
FlashWithPartitionFake() : FlashWithPartitionFake(16) {}
FlashWithPartitionFake(size_t alignment_bytes)
: memory(alignment_bytes), partition(&memory, 0, memory.sector_count()) {}
FakeFlashBuffer<sector_size_bytes, sector_count> memory;
FlashPartition partition;
public:
#if DUMP_KVS_STATE_TO_FILE
Status Dump(const char* filename) {
std::FILE* out_file = std::fopen(filename, "w+");
if (out_file == nullptr) {
PW_LOG_ERROR("Failed to dump to %s", filename);
return Status::DATA_LOSS;
}
std::vector<std::byte> out_vec(memory.size_bytes());
Status status =
memory.Read(0, pw::span<std::byte>(out_vec.data(), out_vec.size()));
if (status != Status::OK) {
fclose(out_file);
return status;
}
size_t written =
std::fwrite(out_vec.data(), 1, memory.size_bytes(), out_file);
if (written != memory.size_bytes()) {
PW_LOG_ERROR("Failed to dump to %s, written=%u",
filename,
static_cast<unsigned>(written));
status = Status::DATA_LOSS;
} else {
PW_LOG_INFO("Dumped to %s", filename);
status = Status::OK;
}
fclose(out_file);
return status;
}
#else
Status Dump(const char*) { return Status::OK; }
#endif // DUMP_KVS_STATE_TO_FILE
};
typedef FlashWithPartitionFake<4 * 128 /*sector size*/, 6 /*sectors*/> Flash;
#if USE_MEMORY_BUFFER
// Although it might be useful to test other configurations, some tests require
// at least 3 sectors; therfore it should have this when checked in.
FakeFlashBuffer<4 * 1024, 4> test_flash(
16); // 4 x 4k sectors, 16 byte alignment
FlashPartition test_partition(&test_flash, 0, test_flash.sector_count());
FakeFlashBuffer<1024, 60> large_test_flash(8);
FlashPartition large_test_partition(&large_test_flash,
0,
large_test_flash.sector_count());
#else // TODO: Test with real flash
FlashPartition& test_partition = FlashExternalTestPartition();
#endif // USE_MEMORY_BUFFER
std::array<byte, 512> buffer;
constexpr std::array<const char*, 3> keys{"TestKey1", "Key2", "TestKey3"};
ChecksumCrc16 checksum;
constexpr EntryFormat format{.magic = 0xBAD'C0D3, .checksum = &checksum};
size_t RoundUpForAlignment(size_t size) {
return AlignUp(size, test_partition.alignment_bytes());
}
// This class gives attributes of KVS that we are testing against
class KvsAttributes {
public:
KvsAttributes(size_t key_size, size_t data_size)
: chunk_header_size_(RoundUpForAlignment(sizeof(EntryHeader))),
data_size_(RoundUpForAlignment(data_size)),
key_size_(RoundUpForAlignment(key_size)),
erase_size_(chunk_header_size_ + key_size_),
min_put_size_(
RoundUpForAlignment(chunk_header_size_ + key_size_ + data_size_)) {}
size_t ChunkHeaderSize() { return chunk_header_size_; }
size_t DataSize() { return data_size_; }
size_t KeySize() { return key_size_; }
size_t EraseSize() { return erase_size_; }
size_t MinPutSize() { return min_put_size_; }
private:
const size_t chunk_header_size_;
const size_t data_size_;
const size_t key_size_;
const size_t erase_size_;
const size_t min_put_size_;
};
class EmptyInitializedKvs : public ::testing::Test {
protected:
EmptyInitializedKvs() : kvs_(&test_partition, format) {
test_partition.Erase();
ASSERT_EQ(Status::OK, kvs_.Init());
}
// Intention of this is to put and erase key-val to fill up sectors. It's a
// helper function in testing how KVS handles cases where flash sector is full
// or near full.
void FillKvs(const char* key, size_t size_to_fill) {
constexpr size_t kTestDataSize = 8;
KvsAttributes kvs_attr(std::strlen(key), kTestDataSize);
const size_t kMaxPutSize =
buffer.size() + kvs_attr.ChunkHeaderSize() + kvs_attr.KeySize();
ASSERT_GE(size_to_fill, kvs_attr.MinPutSize() + kvs_attr.EraseSize());
// Saving enough space to perform erase after loop
size_to_fill -= kvs_attr.EraseSize();
// We start with possible small chunk to prevent too small of a Kvs.Put() at
// the end.
size_t chunk_len =
std::max(kvs_attr.MinPutSize(), size_to_fill % buffer.size());
std::memset(buffer.data(), 0, buffer.size());
while (size_to_fill > 0) {
// Changing buffer value so put actually does something
buffer[0] = static_cast<byte>(static_cast<uint8_t>(buffer[0]) + 1);
ASSERT_EQ(Status::OK,
kvs_.Put(key,
span(buffer.data(),
chunk_len - kvs_attr.ChunkHeaderSize() -
kvs_attr.KeySize())));
size_to_fill -= chunk_len;
chunk_len = std::min(size_to_fill, kMaxPutSize);
}
ASSERT_EQ(Status::OK, kvs_.Delete(key));
}
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
};
} // namespace
TEST_F(EmptyInitializedKvs, Put_SameKeySameValueRepeatedly_AlignedEntries) {
std::array<char, 8> value{'v', 'a', 'l', 'u', 'e', '6', '7', '\0'};
for (int i = 0; i < 1000; ++i) {
ASSERT_EQ(Status::OK, kvs_.Put("The Key!", as_bytes(span(value))));
}
}
TEST_F(EmptyInitializedKvs, Put_SameKeySameValueRepeatedly_UnalignedEntries) {
std::array<char, 7> value{'v', 'a', 'l', 'u', 'e', '6', '\0'};
for (int i = 0; i < 1000; ++i) {
ASSERT_EQ(Status::OK, kvs_.Put("The Key!", as_bytes(span(value))));
}
}
TEST_F(EmptyInitializedKvs, Put_SameKeyDifferentValuesRepeatedly) {
std::array<char, 10> value{'v', 'a', 'l', 'u', 'e', '6', '7', '8', '9', '\0'};
for (int i = 0; i < 100; ++i) {
for (unsigned size = 0; size < value.size(); ++size) {
ASSERT_EQ(Status::OK, kvs_.Put("The Key!", i));
}
}
}
TEST_F(EmptyInitializedKvs, Put_MaxValueSize) {
size_t max_value_size =
test_partition.sector_size_bytes() - sizeof(EntryHeader) - 1;
// Use the large_test_flash as a big chunk of data for the Put statement.
ASSERT_GT(sizeof(large_test_flash), max_value_size + 2 * sizeof(EntryHeader));
auto big_data = as_bytes(span(&large_test_flash, 1));
EXPECT_EQ(Status::OK, kvs_.Put("K", big_data.subspan(0, max_value_size)));
// Larger than maximum is rejected.
EXPECT_EQ(Status::INVALID_ARGUMENT,
kvs_.Put("K", big_data.subspan(0, max_value_size + 1)));
EXPECT_EQ(Status::INVALID_ARGUMENT, kvs_.Put("K", big_data));
}
TEST_F(EmptyInitializedKvs, Get_Simple) {
ASSERT_EQ(Status::OK, kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[16];
auto result = kvs_.Get("Charles", as_writable_bytes(span(value)));
EXPECT_EQ(Status::OK, result.status());
EXPECT_EQ(sizeof("Mingus"), result.size());
EXPECT_STREQ("Mingus", value);
}
TEST_F(EmptyInitializedKvs, Get_WithOffset) {
ASSERT_EQ(Status::OK, kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[16];
auto result = kvs_.Get("Charles", as_writable_bytes(span(value)), 4);
EXPECT_EQ(Status::OK, result.status());
EXPECT_EQ(sizeof("Mingus") - 4, result.size());
EXPECT_STREQ("us", value);
}
TEST_F(EmptyInitializedKvs, Get_WithOffset_FillBuffer) {
ASSERT_EQ(Status::OK, kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[4] = {};
auto result = kvs_.Get("Charles", as_writable_bytes(span(value, 3)), 1);
EXPECT_EQ(Status::RESOURCE_EXHAUSTED, result.status());
EXPECT_EQ(3u, result.size());
EXPECT_STREQ("ing", value);
}
TEST_F(EmptyInitializedKvs, Get_WithOffset_PastEnd) {
ASSERT_EQ(Status::OK, kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[16];
auto result =
kvs_.Get("Charles", as_writable_bytes(span(value)), sizeof("Mingus") + 1);
EXPECT_EQ(Status::OUT_OF_RANGE, result.status());
EXPECT_EQ(0u, result.size());
}
TEST_F(EmptyInitializedKvs, Delete_GetDeletedKey_ReturnsNotFound) {
ASSERT_EQ(Status::OK, kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(Status::OK, kvs_.Delete("kEy"));
EXPECT_EQ(Status::NOT_FOUND, kvs_.Get("kEy", {}).status());
EXPECT_EQ(Status::NOT_FOUND, kvs_.ValueSize("kEy").status());
}
TEST_F(EmptyInitializedKvs, Delete_AddBackKey_PersistsAfterInitialization) {
ASSERT_EQ(Status::OK, kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(Status::OK, kvs_.Delete("kEy"));
EXPECT_EQ(Status::OK, kvs_.Put("kEy", as_bytes(span("45678"))));
char data[6] = {};
ASSERT_EQ(Status::OK, kvs_.Get("kEy", &data));
EXPECT_STREQ(data, "45678");
// Ensure that the re-added key is still present after reinitialization.
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> new_kvs(&test_partition,
format);
ASSERT_EQ(Status::OK, new_kvs.Init());
EXPECT_EQ(Status::OK, new_kvs.Put("kEy", as_bytes(span("45678"))));
char new_data[6] = {};
EXPECT_EQ(Status::OK, new_kvs.Get("kEy", &new_data));
EXPECT_STREQ(data, "45678");
}
TEST_F(EmptyInitializedKvs, Delete_AllItems_KvsIsEmpty) {
ASSERT_EQ(Status::OK, kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(Status::OK, kvs_.Delete("kEy"));
EXPECT_EQ(0u, kvs_.size());
EXPECT_TRUE(kvs_.empty());
}
TEST_F(EmptyInitializedKvs, Collision_WithPresentKey) {
// Both hash to 0x19df36f0.
constexpr std::string_view key1 = "D4";
constexpr std::string_view key2 = "dFU6S";
ASSERT_EQ(Status::OK, kvs_.Put(key1, 1000));
EXPECT_EQ(Status::ALREADY_EXISTS, kvs_.Put(key2, 999));
int value = 0;
EXPECT_EQ(Status::OK, kvs_.Get(key1, &value));
EXPECT_EQ(1000, value);
EXPECT_EQ(Status::NOT_FOUND, kvs_.Get(key2, &value));
EXPECT_EQ(Status::NOT_FOUND, kvs_.ValueSize(key2).status());
EXPECT_EQ(Status::NOT_FOUND, kvs_.Delete(key2));
}
TEST_F(EmptyInitializedKvs, Collision_WithDeletedKey) {
// Both hash to 0x4060f732.
constexpr std::string_view key1 = "1U2";
constexpr std::string_view key2 = "ahj9d";
ASSERT_EQ(Status::OK, kvs_.Put(key1, 1000));
ASSERT_EQ(Status::OK, kvs_.Delete(key1));
// key2 collides with key1's tombstone.
EXPECT_EQ(Status::ALREADY_EXISTS, kvs_.Put(key2, 999));
int value = 0;
EXPECT_EQ(Status::NOT_FOUND, kvs_.Get(key1, &value));
EXPECT_EQ(Status::NOT_FOUND, kvs_.Get(key2, &value));
EXPECT_EQ(Status::NOT_FOUND, kvs_.ValueSize(key2).status());
EXPECT_EQ(Status::NOT_FOUND, kvs_.Delete(key2));
}
TEST_F(EmptyInitializedKvs, Iteration_Empty_ByReference) {
for (const KeyValueStore::Item& entry : kvs_) {
FAIL(); // The KVS is empty; this shouldn't execute.
static_cast<void>(entry);
}
}
TEST_F(EmptyInitializedKvs, Iteration_Empty_ByValue) {
for (KeyValueStore::Item entry : kvs_) {
FAIL(); // The KVS is empty; this shouldn't execute.
static_cast<void>(entry);
}
}
TEST_F(EmptyInitializedKvs, Iteration_OneItem) {
ASSERT_EQ(Status::OK, kvs_.Put("kEy", as_bytes(span("123"))));
for (KeyValueStore::Item entry : kvs_) {
EXPECT_STREQ(entry.key(), "kEy"); // Make sure null-terminated.
char buffer[sizeof("123")] = {};
EXPECT_EQ(Status::OK, entry.Get(&buffer));
EXPECT_STREQ("123", buffer);
}
}
TEST_F(EmptyInitializedKvs, Iteration_GetWithOffset) {
ASSERT_EQ(Status::OK, kvs_.Put("key", as_bytes(span("not bad!"))));
for (KeyValueStore::Item entry : kvs_) {
char buffer[5];
auto result = entry.Get(as_writable_bytes(span(buffer)), 4);
EXPECT_EQ(Status::OK, result.status());
EXPECT_EQ(5u, result.size());
EXPECT_STREQ("bad!", buffer);
}
}
TEST_F(EmptyInitializedKvs, Iteration_EmptyAfterDeletion) {
ASSERT_EQ(Status::OK, kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(Status::OK, kvs_.Delete("kEy"));
for (KeyValueStore::Item entry : kvs_) {
static_cast<void>(entry);
FAIL();
}
}
TEST_F(EmptyInitializedKvs, FuzzTest) {
if (test_partition.sector_size_bytes() < 4 * 1024 ||
test_partition.sector_count() < 4) {
PW_LOG_INFO("Sectors too small, skipping test.");
return; // TODO: Test could be generalized
}
const char* key1 = "Buf1";
const char* key2 = "Buf2";
const size_t kLargestBufSize = 3 * 1024;
static byte buf1[kLargestBufSize];
static byte buf2[kLargestBufSize];
std::memset(buf1, 1, sizeof(buf1));
std::memset(buf2, 2, sizeof(buf2));
// Start with things in KVS
ASSERT_EQ(Status::OK, kvs_.Put(key1, buf1));
ASSERT_EQ(Status::OK, kvs_.Put(key2, buf2));
for (size_t j = 0; j < keys.size(); j++) {
ASSERT_EQ(Status::OK, kvs_.Put(keys[j], j));
}
for (size_t i = 0; i < 100; i++) {
// Vary two sizes
size_t size1 = (kLargestBufSize) / (i + 1);
size_t size2 = (kLargestBufSize) / (100 - i);
for (size_t j = 0; j < 50; j++) {
// Rewrite a single key many times, can fill up a sector
ASSERT_EQ(Status::OK, kvs_.Put("some_data", j));
}
// Delete and re-add everything
ASSERT_EQ(Status::OK, kvs_.Delete(key1));
ASSERT_EQ(Status::OK, kvs_.Put(key1, span(buf1, size1)));
ASSERT_EQ(Status::OK, kvs_.Delete(key2));
ASSERT_EQ(Status::OK, kvs_.Put(key2, span(buf2, size2)));
for (size_t j = 0; j < keys.size(); j++) {
ASSERT_EQ(Status::OK, kvs_.Delete(keys[j]));
ASSERT_EQ(Status::OK, kvs_.Put(keys[j], j));
}
// Re-enable and verify
ASSERT_EQ(Status::OK, kvs_.Init());
static byte buf[4 * 1024];
ASSERT_EQ(Status::OK, kvs_.Get(key1, span(buf, size1)).status());
ASSERT_EQ(std::memcmp(buf, buf1, size1), 0);
ASSERT_EQ(Status::OK, kvs_.Get(key2, span(buf, size2)).status());
ASSERT_EQ(std::memcmp(buf2, buf2, size2), 0);
for (size_t j = 0; j < keys.size(); j++) {
size_t ret = 1000;
ASSERT_EQ(Status::OK, kvs_.Get(keys[j], &ret));
ASSERT_EQ(ret, j);
}
}
}
TEST_F(EmptyInitializedKvs, Basic) {
// Add some data
uint8_t value1 = 0xDA;
ASSERT_EQ(Status::OK,
kvs_.Put(keys[0], as_bytes(span(&value1, sizeof(value1)))));
uint32_t value2 = 0xBAD0301f;
ASSERT_EQ(Status::OK, kvs_.Put(keys[1], value2));
// Verify data
uint32_t test2;
EXPECT_EQ(Status::OK, kvs_.Get(keys[1], &test2));
uint8_t test1;
ASSERT_EQ(Status::OK, kvs_.Get(keys[0], &test1));
EXPECT_EQ(test1, value1);
EXPECT_EQ(test2, value2);
// Delete a key
EXPECT_EQ(Status::OK, kvs_.Delete(keys[0]));
// Verify it was erased
EXPECT_EQ(kvs_.Get(keys[0], &test1), Status::NOT_FOUND);
test2 = 0;
ASSERT_EQ(
Status::OK,
kvs_.Get(keys[1], span(reinterpret_cast<byte*>(&test2), sizeof(test2)))
.status());
EXPECT_EQ(test2, value2);
// Delete other key
kvs_.Delete(keys[1]);
// Verify it was erased
EXPECT_EQ(kvs_.size(), 0u);
}
#define ASSERT_OK(expr) ASSERT_EQ(Status::OK, expr)
#define EXPECT_OK(expr) EXPECT_EQ(Status::OK, expr)
TEST(InMemoryKvs, WriteOneKeyMultipleTimes) {
// Create and erase the fake flash. It will persist across reloads.
Flash flash;
ASSERT_OK(flash.partition.Erase());
int num_reloads = 2;
for (int reload = 0; reload < num_reloads; ++reload) {
DBG("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
DBG("xxx xxxx");
DBG("xxx Reload %2d xxxx", reload);
DBG("xxx xxxx");
DBG("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
// Create and initialize the KVS.
constexpr EntryFormat format{.magic = 0xBAD'C0D3, .checksum = nullptr};
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs(&flash.partition,
format);
ASSERT_OK(kvs.Init());
// Write the same entry many times.
const char* key = "abcd";
const size_t num_writes = 99;
uint32_t written_value;
EXPECT_EQ(kvs.size(), (reload == 0) ? 0 : 1u);
for (uint32_t i = 0; i < num_writes; ++i) {
DBG("PUT #%zu for key %s with value %zu", size_t(i), key, size_t(i));
written_value = i + 0xfc; // Prevent accidental pass with zero.
EXPECT_OK(kvs.Put(key, written_value));
EXPECT_EQ(kvs.size(), 1u);
}
// Verify that we can read the value back.
DBG("GET final value for key: %s", key);
uint32_t actual_value;
EXPECT_OK(kvs.Get(key, &actual_value));
EXPECT_EQ(actual_value, written_value);
char fname_buf[64] = {'\0'};
snprintf(&fname_buf[0],
sizeof(fname_buf),
"WriteOneKeyMultipleTimes_%d.bin",
reload);
flash.Dump(fname_buf);
}
}
TEST(InMemoryKvs, WritingMultipleKeysIncreasesSize) {
// Create and erase the fake flash.
Flash flash;
ASSERT_OK(flash.partition.Erase());
// Create and initialize the KVS.
constexpr EntryFormat format{.magic = 0xBAD'C0D3, .checksum = nullptr};
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs(&flash.partition,
format);
ASSERT_OK(kvs.Init());
// Write the same entry many times.
const size_t num_writes = 10;
EXPECT_EQ(kvs.size(), 0u);
for (size_t i = 0; i < num_writes; ++i) {
StringBuffer<150> key;
key << "key_" << i;
DBG("PUT #%zu for key %s with value %zu", i, key.c_str(), i);
size_t value = i + 77; // Prevent accidental pass with zero.
EXPECT_OK(kvs.Put(key.view(), value));
EXPECT_EQ(kvs.size(), i + 1);
}
flash.Dump("WritingMultipleKeysIncreasesSize.bin");
}
TEST(InMemoryKvs, WriteAndReadOneKey) {
// Create and erase the fake flash.
Flash flash;
ASSERT_OK(flash.partition.Erase());
// Create and initialize the KVS.
constexpr EntryFormat format{.magic = 0xBAD'C0D3, .checksum = nullptr};
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs(&flash.partition,
format);
ASSERT_OK(kvs.Init());
// Add two entries with different keys and values.
const char* key = "Key1";
DBG("PUT value for key: %s", key);
uint8_t written_value = 0xDA;
ASSERT_OK(kvs.Put(key, written_value));
EXPECT_EQ(kvs.size(), 1u);
DBG("GET value for key: %s", key);
uint8_t actual_value;
ASSERT_OK(kvs.Get(key, &actual_value));
EXPECT_EQ(actual_value, written_value);
EXPECT_EQ(kvs.size(), 1u);
}
TEST(InMemoryKvs, Basic) {
const char* key1 = "Key1";
const char* key2 = "Key2";
// Create and erase the fake flash.
Flash flash;
ASSERT_EQ(Status::OK, flash.partition.Erase());
// Create and initialize the KVS.
constexpr EntryFormat format{.magic = 0xBAD'C0D3, .checksum = nullptr};
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs(&flash.partition,
format);
ASSERT_OK(kvs.Init());
// Add two entries with different keys and values.
uint8_t value1 = 0xDA;
ASSERT_OK(kvs.Put(key1, as_bytes(span(&value1, sizeof(value1)))));
EXPECT_EQ(kvs.size(), 1u);
uint32_t value2 = 0xBAD0301f;
ASSERT_OK(kvs.Put(key2, value2));
EXPECT_EQ(kvs.size(), 2u);
// Verify data
uint32_t test2;
EXPECT_OK(kvs.Get(key2, &test2));
uint8_t test1;
ASSERT_OK(kvs.Get(key1, &test1));
EXPECT_EQ(test1, value1);
EXPECT_EQ(test2, value2);
EXPECT_EQ(kvs.size(), 2u);
}
TEST_F(EmptyInitializedKvs, MaxKeyLength) {
// Add some data
char key[16] = "123456789abcdef"; // key length 15 (without \0)
int value = 1;
ASSERT_EQ(Status::OK, kvs_.Put(key, value));
// Verify data
int test = 0;
ASSERT_EQ(Status::OK, kvs_.Get(key, &test));
EXPECT_EQ(test, value);
// Delete a key
EXPECT_EQ(Status::OK, kvs_.Delete(key));
// Verify it was erased
EXPECT_EQ(kvs_.Get(key, &test), Status::NOT_FOUND);
}
TEST_F(EmptyInitializedKvs, LargeBuffers) {
// Note this assumes that no other keys larger then key0
static_assert(sizeof(keys[0]) >= sizeof(keys[1]) &&
sizeof(keys[0]) >= sizeof(keys[2]));
KvsAttributes kvs_attr(std::strlen(keys[0]), buffer.size());
// Verify the data will fit in this test partition. This checks that all the
// keys chunks will fit and a header for each sector will fit. It requires 1
// empty sector also.
const size_t kMinSize = kvs_attr.MinPutSize() * keys.size();
const size_t kAvailSectorSpace =
test_partition.sector_size_bytes() * (test_partition.sector_count() - 1);
if (kAvailSectorSpace < kMinSize) {
PW_LOG_INFO("KVS too small, skipping test.");
return;
}
// Add and verify
for (unsigned add_idx = 0; add_idx < keys.size(); add_idx++) {
std::memset(buffer.data(), add_idx, buffer.size());
ASSERT_EQ(Status::OK, kvs_.Put(keys[add_idx], buffer));
EXPECT_EQ(kvs_.size(), add_idx + 1);
for (unsigned verify_idx = 0; verify_idx <= add_idx; verify_idx++) {
std::memset(buffer.data(), 0, buffer.size());
ASSERT_EQ(Status::OK, kvs_.Get(keys[verify_idx], buffer).status());
for (unsigned i = 0; i < buffer.size(); i++) {
EXPECT_EQ(static_cast<unsigned>(buffer[i]), verify_idx);
}
}
}
// Erase and verify
for (unsigned erase_idx = 0; erase_idx < keys.size(); erase_idx++) {
ASSERT_EQ(Status::OK, kvs_.Delete(keys[erase_idx]));
EXPECT_EQ(kvs_.size(), keys.size() - erase_idx - 1);
for (unsigned verify_idx = 0; verify_idx < keys.size(); verify_idx++) {
std::memset(buffer.data(), 0, buffer.size());
if (verify_idx <= erase_idx) {
ASSERT_EQ(kvs_.Get(keys[verify_idx], buffer).status(),
Status::NOT_FOUND);
} else {
ASSERT_EQ(Status::OK, kvs_.Get(keys[verify_idx], buffer).status());
for (uint32_t i = 0; i < buffer.size(); i++) {
EXPECT_EQ(buffer[i], static_cast<byte>(verify_idx));
}
}
}
}
}
TEST_F(EmptyInitializedKvs, Enable) {
KvsAttributes kvs_attr(std::strlen(keys[0]), buffer.size());
// Verify the data will fit in this test partition. This checks that all the
// keys chunks will fit and a header for each sector will fit. It requires 1
// empty sector also.
const size_t kMinSize = kvs_attr.MinPutSize() * keys.size();
const size_t kAvailSectorSpace =
test_partition.sector_size_bytes() * (test_partition.sector_count() - 1);
if (kAvailSectorSpace < kMinSize) {
PW_LOG_INFO("KVS too small, skipping test.");
return;
}
// Add some items
for (unsigned add_idx = 0; add_idx < keys.size(); add_idx++) {
std::memset(buffer.data(), add_idx, buffer.size());
ASSERT_EQ(Status::OK, kvs_.Put(keys[add_idx], buffer));
EXPECT_EQ(kvs_.size(), add_idx + 1);
}
// Enable different KVS which should be able to properly setup the same map
// from what is stored in flash.
static KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_local(
&test_partition, format);
ASSERT_EQ(Status::OK, kvs_local.Init());
EXPECT_EQ(kvs_local.size(), keys.size());
// Ensure adding to new KVS works
uint8_t value = 0xDA;
const char* key = "new_key";
ASSERT_EQ(Status::OK, kvs_local.Put(key, value));
uint8_t test;
ASSERT_EQ(Status::OK, kvs_local.Get(key, &test));
EXPECT_EQ(value, test);
EXPECT_EQ(kvs_local.size(), keys.size() + 1);
// Verify previous data
for (unsigned verify_idx = 0; verify_idx < keys.size(); verify_idx++) {
std::memset(buffer.data(), 0, buffer.size());
ASSERT_EQ(Status::OK, kvs_local.Get(keys[verify_idx], buffer).status());
for (uint32_t i = 0; i < buffer.size(); i++) {
EXPECT_EQ(static_cast<unsigned>(buffer[i]), verify_idx);
}
}
}
TEST_F(EmptyInitializedKvs, MultiSector) {
// Calculate number of elements to ensure multiple sectors are required.
uint16_t add_count = (test_partition.sector_size_bytes() / buffer.size()) + 1;
if (kvs_.max_size() < add_count) {
PW_LOG_INFO("Sector size too large, skipping test.");
return; // this chip has very large sectors, test won't work
}
if (test_partition.sector_count() < 3) {
PW_LOG_INFO("Not enough sectors, skipping test.");
return; // need at least 3 sectors for multi-sector test
}
char key[20];
for (unsigned add_idx = 0; add_idx < add_count; add_idx++) {
std::memset(buffer.data(), add_idx, buffer.size());
snprintf(key, sizeof(key), "key_%u", add_idx);
ASSERT_EQ(Status::OK, kvs_.Put(key, buffer));
EXPECT_EQ(kvs_.size(), add_idx + 1);
}
for (unsigned verify_idx = 0; verify_idx < add_count; verify_idx++) {
std::memset(buffer.data(), 0, buffer.size());
snprintf(key, sizeof(key), "key_%u", verify_idx);
ASSERT_EQ(Status::OK, kvs_.Get(key, buffer).status());
for (uint32_t i = 0; i < buffer.size(); i++) {
EXPECT_EQ(static_cast<unsigned>(buffer[i]), verify_idx);
}
}
// Check erase
for (unsigned erase_idx = 0; erase_idx < add_count; erase_idx++) {
snprintf(key, sizeof(key), "key_%u", erase_idx);
ASSERT_EQ(Status::OK, kvs_.Delete(key));
EXPECT_EQ(kvs_.size(), add_count - erase_idx - 1);
}
}
TEST_F(EmptyInitializedKvs, RewriteValue) {
// Write first value
const uint8_t kValue1 = 0xDA;
const uint8_t kValue2 = 0x12;
const char* key = "the_key";
ASSERT_EQ(Status::OK, kvs_.Put(key, as_bytes(span(&kValue1, 1))));
// Verify
uint8_t value;
ASSERT_EQ(Status::OK,
kvs_.Get(key, as_writable_bytes(span(&value, 1))).status());
EXPECT_EQ(kValue1, value);
// Write new value for key
ASSERT_EQ(Status::OK, kvs_.Put(key, as_bytes(span(&kValue2, 1))));
// Verify
ASSERT_EQ(Status::OK,
kvs_.Get(key, as_writable_bytes(span(&value, 1))).status());
EXPECT_EQ(kValue2, value);
// Verify only 1 element exists
EXPECT_EQ(kvs_.size(), 1u);
}
TEST_F(EmptyInitializedKvs, RepeatingValueWithOtherData) {
std::byte set_buf[150];
std::byte get_buf[sizeof(set_buf)];
for (size_t set_index = 0; set_index < sizeof(set_buf); set_index++) {
set_buf[set_index] = static_cast<std::byte>(set_index);
}
StatusWithSize result;
// Test setting the same entry 10 times but varying the amount of data
// that is already in env before each test
for (size_t test_iteration = 0; test_iteration < sizeof(set_buf);
test_iteration++) {
// TOD0: Add KVS erase
// Add a constant unchanging entry so that the updates are not
// the only entries in the env. The size of this initial entry
// we vary between no bytes to sizeof(set_buf).
ASSERT_EQ(Status::OK,
kvs_.Put("const_entry", span(set_buf, test_iteration)));
// The value we read back should be the last value we set
std::memset(get_buf, 0, sizeof(get_buf));
result = kvs_.Get("const_entry", span(get_buf));
ASSERT_EQ(Status::OK, result.status());
ASSERT_EQ(result.size(), test_iteration);
for (size_t j = 0; j < test_iteration; j++) {
EXPECT_EQ(set_buf[j], get_buf[j]);
}
// Update the test entry 5 times
static_assert(sizeof(std::byte) == sizeof(uint8_t));
uint8_t set_entry_buf[]{1, 2, 3, 4, 5, 6, 7, 8};
std::byte* set_entry = reinterpret_cast<std::byte*>(set_entry_buf);
std::byte get_entry_buf[sizeof(set_entry_buf)];
for (size_t i = 0; i < 5; i++) {
set_entry[0] = static_cast<std::byte>(i);
ASSERT_EQ(Status::OK,
kvs_.Put("test_entry", span(set_entry, sizeof(set_entry_buf))));
std::memset(get_entry_buf, 0, sizeof(get_entry_buf));
result = kvs_.Get("test_entry", span(get_entry_buf));
ASSERT_TRUE(result.ok());
ASSERT_EQ(result.size(), sizeof(get_entry_buf));
for (uint32_t j = 0; j < sizeof(set_entry_buf); j++) {
EXPECT_EQ(set_entry[j], get_entry_buf[j]);
}
}
// Check that the const entry is still present and has the right value
std::memset(get_buf, 0, sizeof(get_buf));
result = kvs_.Get("const_entry", span(get_buf));
ASSERT_TRUE(result.ok());
ASSERT_EQ(result.size(), test_iteration);
for (size_t j = 0; j < test_iteration; j++) {
EXPECT_EQ(set_buf[j], get_buf[j]);
}
}
}
TEST_F(EmptyInitializedKvs, OffsetRead) {
const char* key = "the_key";
constexpr size_t kReadSize = 16; // needs to be a multiple of alignment
constexpr size_t kTestBufferSize = kReadSize * 10;
ASSERT_GT(buffer.size(), kTestBufferSize);
ASSERT_LE(kTestBufferSize, 0xFFu);
// Write the entire buffer
for (size_t i = 0; i < kTestBufferSize; i++) {
buffer[i] = byte(i);
}
ASSERT_EQ(Status::OK, kvs_.Put(key, span(buffer.data(), kTestBufferSize)));
EXPECT_EQ(kvs_.size(), 1u);
// Read in small chunks and verify
for (unsigned i = 0; i < kTestBufferSize / kReadSize; i++) {
std::memset(buffer.data(), 0, buffer.size());
StatusWithSize result =
kvs_.Get(key, span(buffer.data(), kReadSize), i * kReadSize);
ASSERT_EQ(kReadSize, result.size());
// Only last iteration is OK since all remaining data was read.
if (i == kTestBufferSize / kReadSize - 1) {
ASSERT_EQ(Status::OK, result.status());
} else { // RESOURCE_EXHAUSTED, since there is still data to read.
ASSERT_EQ(Status::RESOURCE_EXHAUSTED, result.status());
}
for (unsigned j = 0; j < kReadSize; j++) {
ASSERT_EQ(static_cast<unsigned>(buffer[j]), j + i * kReadSize);
}
}
}
TEST_F(EmptyInitializedKvs, MultipleRewrite) {
// Calculate number of elements to ensure multiple sectors are required.
unsigned add_count = (test_partition.sector_size_bytes() / buffer.size()) + 1;
const char* key = "the_key";
constexpr uint8_t kGoodVal = 0x60;
constexpr uint8_t kBadVal = 0xBA;
std::memset(buffer.data(), kBadVal, buffer.size());
for (unsigned add_idx = 0; add_idx < add_count; add_idx++) {
if (add_idx == add_count - 1) { // last value
std::memset(buffer.data(), kGoodVal, buffer.size());
}
ASSERT_EQ(Status::OK, kvs_.Put(key, buffer));
EXPECT_EQ(kvs_.size(), 1u);
}
// Verify
std::memset(buffer.data(), 0, buffer.size());
ASSERT_EQ(Status::OK, kvs_.Get(key, buffer).status());
for (uint32_t i = 0; i < buffer.size(); i++) {
ASSERT_EQ(buffer[i], static_cast<byte>(kGoodVal));
}
}
TEST_F(EmptyInitializedKvs, FillSector) {
ASSERT_EQ(std::strlen(keys[0]), 8U); // Easier for alignment
ASSERT_EQ(std::strlen(keys[2]), 8U); // Easier for alignment
constexpr size_t kTestDataSize = 8;
KvsAttributes kvs_attr(std::strlen(keys[2]), kTestDataSize);
int bytes_remaining = test_partition.sector_size_bytes();
constexpr byte kKey0Pattern = byte{0xBA};
std::memset(
buffer.data(), static_cast<int>(kKey0Pattern), kvs_attr.DataSize());
ASSERT_EQ(Status::OK,
kvs_.Put(keys[0], span(buffer.data(), kvs_attr.DataSize())));
bytes_remaining -= kvs_attr.MinPutSize();
std::memset(buffer.data(), 1, kvs_attr.DataSize());
ASSERT_EQ(Status::OK,
kvs_.Put(keys[2], span(buffer.data(), kvs_attr.DataSize())));
bytes_remaining -= kvs_attr.MinPutSize();
EXPECT_EQ(kvs_.size(), 2u);
ASSERT_EQ(Status::OK, kvs_.Delete(keys[2]));
bytes_remaining -= kvs_attr.EraseSize();
EXPECT_EQ(kvs_.size(), 1u);
// Intentionally adding erase size to trigger sector cleanup
bytes_remaining += kvs_attr.EraseSize();
FillKvs(keys[2], bytes_remaining);
// Verify key[0]
std::memset(buffer.data(), 0, kvs_attr.DataSize());
ASSERT_EQ(
Status::OK,
kvs_.Get(keys[0], span(buffer.data(), kvs_attr.DataSize())).status());
for (uint32_t i = 0; i < kvs_attr.DataSize(); i++) {
EXPECT_EQ(buffer[i], kKey0Pattern);
}
}
TEST_F(EmptyInitializedKvs, Interleaved) {
const uint8_t kValue1 = 0xDA;
const uint8_t kValue2 = 0x12;
uint8_t value;
ASSERT_EQ(Status::OK, kvs_.Put(keys[0], kValue1));
EXPECT_EQ(kvs_.size(), 1u);
ASSERT_EQ(Status::OK, kvs_.Delete(keys[0]));
EXPECT_EQ(kvs_.Get(keys[0], &value), Status::NOT_FOUND);
ASSERT_EQ(Status::OK, kvs_.Put(keys[1], as_bytes(span(&kValue1, 1))));
ASSERT_EQ(Status::OK, kvs_.Put(keys[2], kValue2));
ASSERT_EQ(Status::OK, kvs_.Delete(keys[1]));
EXPECT_EQ(Status::OK, kvs_.Get(keys[2], &value));
EXPECT_EQ(kValue2, value);
EXPECT_EQ(kvs_.size(), 1u);
}
TEST_F(EmptyInitializedKvs, DeleteAndReinitialize) {
// Write value
const uint8_t kValue = 0xDA;
ASSERT_EQ(Status::OK, kvs_.Put(keys[0], kValue));
ASSERT_EQ(Status::OK, kvs_.Delete(keys[0]));
uint8_t value;
ASSERT_EQ(kvs_.Get(keys[0], &value), Status::NOT_FOUND);
// Reset KVS, ensure captured at enable
ASSERT_EQ(Status::OK, kvs_.Init());
ASSERT_EQ(kvs_.Get(keys[0], &value), Status::NOT_FOUND);
}
TEST_F(EmptyInitializedKvs, TemplatedPutAndGet) {
// Store a value with the convenience method.
const uint32_t kValue = 0x12345678;
ASSERT_EQ(Status::OK, kvs_.Put(keys[0], kValue));
// Read it back with the other convenience method.
uint32_t value;
ASSERT_EQ(Status::OK, kvs_.Get(keys[0], &value));
ASSERT_EQ(kValue, value);
// Make sure we cannot get something where size isn't what we expect
const uint8_t kSmallValue = 0xBA;
uint8_t small_value = kSmallValue;
ASSERT_EQ(kvs_.Get(keys[0], &small_value), Status::INVALID_ARGUMENT);
ASSERT_EQ(small_value, kSmallValue);
}
// This test is derived from bug that was discovered. Testing this corner case
// relies on creating a new key-value just under the size that is left over in
// the sector.
TEST_F(EmptyInitializedKvs, FillSector2) {
if (test_partition.sector_count() < 3) {
PW_LOG_INFO("Not enough sectors, skipping test.");
return; // need at least 3 sectors
}
// Start of by filling flash sector to near full
constexpr int kHalfBufferSize = buffer.size() / 2;
const int kSizeToFill = test_partition.sector_size_bytes() - kHalfBufferSize;
constexpr size_t kTestDataSize = 8;
KvsAttributes kvs_attr(std::strlen(keys[2]), kTestDataSize);
FillKvs(keys[2], kSizeToFill);
// Find out how much space is remaining for new key-value and confirm it
// makes sense.
size_t new_keyvalue_size = 0;
size_t alignment = test_partition.alignment_bytes();
// Starts on second sector since it will try to keep first sector free
FlashPartition::Address read_address =
2 * test_partition.sector_size_bytes() - alignment;
for (; read_address > 0; read_address -= alignment) {
bool is_erased = false;
ASSERT_EQ(
Status::OK,
test_partition.IsRegionErased(read_address, alignment, &is_erased));
if (is_erased) {
new_keyvalue_size += alignment;
} else {
break;
}
}
size_t expected_remaining = test_partition.sector_size_bytes() - kSizeToFill;
ASSERT_EQ(new_keyvalue_size, expected_remaining);
const char* kNewKey = "NewKey";
constexpr size_t kValueLessThanChunkHeaderSize = 2;
constexpr auto kTestPattern = byte{0xBA};
new_keyvalue_size -= kValueLessThanChunkHeaderSize;
std::memset(buffer.data(), static_cast<int>(kTestPattern), new_keyvalue_size);
ASSERT_EQ(Status::OK,
kvs_.Put(kNewKey, span(buffer.data(), new_keyvalue_size)));
// In failed corner case, adding new key is deceptively successful. It isn't
// until KVS is disabled and reenabled that issue can be detected.
ASSERT_EQ(Status::OK, kvs_.Init());
// Might as well check that new key-value is what we expect it to be
ASSERT_EQ(Status::OK,
kvs_.Get(kNewKey, span(buffer.data(), new_keyvalue_size)).status());
for (size_t i = 0; i < new_keyvalue_size; i++) {
EXPECT_EQ(buffer[i], kTestPattern);
}
}
TEST_F(EmptyInitializedKvs, ValueSize_Positive) {
constexpr auto kData = AsBytes('h', 'i', '!');
ASSERT_EQ(Status::OK, kvs_.Put("TheKey", kData));
auto result = kvs_.ValueSize("TheKey");
EXPECT_EQ(Status::OK, result.status());
EXPECT_EQ(kData.size(), result.size());
}
TEST_F(EmptyInitializedKvs, ValueSize_Zero) {
ASSERT_EQ(Status::OK, kvs_.Put("TheKey", as_bytes(span("123", 3))));
auto result = kvs_.ValueSize("TheKey");
EXPECT_EQ(Status::OK, result.status());
EXPECT_EQ(3u, result.size());
}
TEST_F(EmptyInitializedKvs, ValueSize_InvalidKey) {
EXPECT_EQ(Status::INVALID_ARGUMENT, kvs_.ValueSize("").status());
}
TEST_F(EmptyInitializedKvs, ValueSize_MissingKey) {
EXPECT_EQ(Status::NOT_FOUND, kvs_.ValueSize("Not in there").status());
}
TEST_F(EmptyInitializedKvs, ValueSize_DeletedKey) {
ASSERT_EQ(Status::OK, kvs_.Put("TheKey", as_bytes(span("123", 3))));
ASSERT_EQ(Status::OK, kvs_.Delete("TheKey"));
EXPECT_EQ(Status::NOT_FOUND, kvs_.ValueSize("TheKey").status());
}
#if USE_MEMORY_BUFFER
class LargeEmptyInitializedKvs : public ::testing::Test {
protected:
LargeEmptyInitializedKvs() : kvs_(&large_test_partition, format) {
ASSERT_EQ(
Status::OK,
large_test_partition.Erase(0, large_test_partition.sector_count()));
ASSERT_EQ(Status::OK, kvs_.Init());
}
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
};
TEST_F(LargeEmptyInitializedKvs, Basic) {
const uint8_t kValue1 = 0xDA;
const uint8_t kValue2 = 0x12;
uint8_t value;
ASSERT_EQ(Status::OK, kvs_.Put(keys[0], kValue1));
EXPECT_EQ(kvs_.size(), 1u);
ASSERT_EQ(Status::OK, kvs_.Delete(keys[0]));
EXPECT_EQ(kvs_.Get(keys[0], &value), Status::NOT_FOUND);
ASSERT_EQ(Status::OK, kvs_.Put(keys[1], kValue1));
ASSERT_EQ(Status::OK, kvs_.Put(keys[2], kValue2));
ASSERT_EQ(Status::OK, kvs_.Delete(keys[1]));
EXPECT_EQ(Status::OK, kvs_.Get(keys[2], &value));
EXPECT_EQ(kValue2, value);
ASSERT_EQ(kvs_.Get(keys[1], &value), Status::NOT_FOUND);
EXPECT_EQ(kvs_.size(), 1u);
}
#endif // USE_MEMORY_BUFFER
TEST_F(EmptyInitializedKvs, CallingEraseTwice_NothingWrittenToFlash) {
const uint8_t kValue = 0xDA;
ASSERT_EQ(Status::OK, kvs_.Put(keys[0], kValue));
ASSERT_EQ(Status::OK, kvs_.Delete(keys[0]));
// Compare before / after checksums to verify that nothing was written.
const uint16_t crc = checksum::CcittCrc16(test_flash.buffer());
EXPECT_EQ(kvs_.Delete(keys[0]), Status::NOT_FOUND);
EXPECT_EQ(crc, checksum::CcittCrc16(test_flash.buffer()));
}
} // namespace pw::kvs