blob: 7c3dda574f60a948dc8f70555d5f777f23fce236 [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.
#include <random>
#include <string>
#include <string_view>
#include <unordered_map>
#define DUMP_KVS_CONTENTS 0
#if DUMP_KVS_CONTENTS
#include <iostream>
#endif // DUMP_KVS_CONTENTS
#include "gtest/gtest.h"
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/in_memory_fake_flash.h"
#include "pw_kvs/key_value_store.h"
#include "pw_span/span.h"
namespace pw::kvs {
namespace {
using std::byte;
constexpr std::string_view kChars =
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789";
struct TestParameters {
size_t sector_size;
size_t sector_count;
size_t sector_alignment;
size_t partition_start_sector;
size_t partition_sector_count;
size_t partition_alignment;
};
template <const TestParameters& kParams>
class KvsTester {
public:
static constexpr EntryHeaderFormat kFormat{.magic = 0xBAD'C0D3,
.checksum = nullptr};
KvsTester()
: partition_(&flash_,
kParams.partition_start_sector,
kParams.partition_sector_count,
kParams.partition_alignment),
kvs_(&partition_, kFormat) {
EXPECT_EQ(Status::OK, partition_.Erase());
Status result = kvs_.Init();
EXPECT_EQ(Status::OK, result);
if (!result.ok()) {
std::abort();
}
}
~KvsTester() {
#if DUMP_KVS_CONTENTS
std::cout << "/==============================================\\\n";
std::cout << "KVS EXPECTED CONTENTS\n";
std::cout << "------------------------------------------------\n";
std::cout << "Entries: " << map_.size() << '\n';
std::cout << "------------------------------------------------\n";
for (const auto& [key, value] : map_) {
std::cout << key << " = " << value << '\n';
}
std::cout << "\\===============================================/\n";
#endif // DUMP_KVS_CONTENTS
EXPECT_EQ(map_.size(), kvs_.size());
size_t count = 0;
for (auto& item : kvs_) {
count += 1;
auto map_entry = map_.find(std::string(item.key()));
EXPECT_NE(map_entry, map_.end());
if (map_entry != map_.end()) {
EXPECT_EQ(map_entry->first, item.key());
char value[kMaxValueLength + 1] = {};
EXPECT_EQ(Status::OK,
item.Get(as_writable_bytes(span(value))).status());
EXPECT_EQ(map_entry->second, std::string(value));
}
}
EXPECT_EQ(count, map_.size());
}
void TestRandomValidInputs(int iterations) {
std::mt19937 random(6006411);
std::uniform_int_distribution<unsigned> distro;
auto random_int = [&] { return distro(random); };
auto random_string = [&](size_t length) {
std::string value;
for (size_t i = 0; i < length; ++i) {
value.push_back(kChars[random_int() % kChars.size()]);
}
return value;
};
for (int i = 0; i < iterations; ++i) {
// One out of 4 times, delete a key.
if (random_int() % 4 == 0) {
// Either delete a non-existent key or delete an existing one.
if (empty() || random_int() % 8 == 0) {
Delete("not_a_key" + std::to_string(random_int()));
} else {
Delete(RandomKey());
}
} else {
std::string key;
// Either add a new key or replace an existing one.
if (empty() || random_int() % 2 == 0) {
key = random_string(random_int() % KeyValueStore::kMaxKeyLength);
} else {
key = RandomKey();
}
Put(key, random_string(random_int() % kMaxValueLength));
}
}
}
void TestPut() {
Put("base_key", "base_value");
for (int i = 0; i < 100; ++i) {
Put("other_key", std::to_string(i));
}
for (int i = 0; i < 100; ++i) {
Put("key_" + std::to_string(i), std::to_string(i));
}
}
private:
// Adds a key to the KVS, if there is room for it.
void Put(const std::string& key, const std::string& value) {
ASSERT_LE(value.size(), kMaxValueLength);
Status result = kvs_.Put(key, as_bytes(span(value)));
if (key.empty() || key.size() > KeyValueStore::kMaxKeyLength) {
EXPECT_EQ(Status::INVALID_ARGUMENT, result);
} else if (map_.size() == KeyValueStore::kMaxEntries) {
EXPECT_EQ(Status::RESOURCE_EXHAUSTED, result);
} else {
ASSERT_EQ(Status::OK, result);
map_[key] = value;
}
}
// Deletes a key from the KVS if it is present.
void Delete(const std::string& key) {
Status result = kvs_.Delete(key);
if (key.empty() || key.size() >= KeyValueStore::kMaxKeyLength) {
EXPECT_EQ(Status::INVALID_ARGUMENT, result);
} else if (map_.count(key) == 0) {
EXPECT_EQ(Status::NOT_FOUND, result);
} else {
ASSERT_EQ(Status::OK, result);
map_.erase(key);
}
}
bool empty() const { return map_.empty(); }
std::string RandomKey() const {
return map_.empty() ? "" : map_.begin()->second;
}
static constexpr size_t kMaxValueLength = 64;
static InMemoryFakeFlash<kParams.sector_size, kParams.sector_count> flash_;
FlashPartition partition_;
KeyValueStore kvs_;
std::unordered_map<std::string, std::string> map_;
};
template <const TestParameters& kParams>
InMemoryFakeFlash<kParams.sector_size, kParams.sector_count>
KvsTester<kParams>::flash_ =
InMemoryFakeFlash<kParams.sector_size, kParams.sector_count>(
kParams.sector_alignment);
// Defines a test fixture that runs all tests against a flash with the specified
// parameters.
#define RUN_TESTS_WITH_PARAMETERS(name, ...) \
class name : public ::testing::Test { \
protected: \
static constexpr TestParameters kParams = {__VA_ARGS__}; \
\
KvsTester<kParams> tester_; \
}; \
\
/* Run each test defined in the KvsTester class with these parameters. */ \
\
TEST_F(name, Put) { tester_.TestPut(); } \
TEST_F(name, DISABLED_RandomValidInputs) { /* TODO: Get this passing! */ \
tester_.TestRandomValidInputs(1000); \
} \
static_assert(true, "Don't forget a semicolon!")
RUN_TESTS_WITH_PARAMETERS(Basic,
.sector_size = 4 * 1024,
.sector_count = 4,
.sector_alignment = 16,
.partition_start_sector = 0,
.partition_sector_count = 4,
.partition_alignment = 16);
// TODO: This test suite causes an infinite loop.
RUN_TESTS_WITH_PARAMETERS(DISABLED_NonPowerOf2Alignment,
.sector_size = 1000,
.sector_count = 4,
.sector_alignment = 10,
.partition_start_sector = 0,
.partition_sector_count = 4,
.partition_alignment = 100);
// TODO: This test suite fails to initialize.
RUN_TESTS_WITH_PARAMETERS(DISABLED_Unaligned,
.sector_size = 1026,
.sector_count = 3,
.sector_alignment = 10,
.partition_start_sector = 1,
.partition_sector_count = 2,
.partition_alignment = 9);
} // namespace
} // namespace pw::kvs