#include <unity.h>
|
|
#include <Arduino.h>
|
|
|
|
#pragma GCC diagnostic warning "-Wall"
|
|
#pragma GCC diagnostic warning "-Wextra"
|
|
#pragma GCC diagnostic warning "-Wstrict-aliasing"
|
|
#pragma GCC diagnostic warning "-Wpointer-arith"
|
|
#pragma GCC diagnostic warning "-Wstrict-overflow=5"
|
|
|
|
#include <settings_embedis.h>
|
|
|
|
#include <array>
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
|
|
namespace settings {
|
|
namespace embedis {
|
|
|
|
// TODO: either
|
|
// - throw exception and print backtrace to signify which method caused the out-of-bounds read or write
|
|
// - use a custom macro to supply (instance?) object with the correct line number
|
|
template <typename T>
|
|
struct StaticArrayStorage {
|
|
|
|
explicit StaticArrayStorage(T& blob) :
|
|
_blob(blob),
|
|
_size(blob.size())
|
|
{}
|
|
|
|
uint8_t read(size_t index) {
|
|
TEST_ASSERT_LESS_THAN(_size, index);
|
|
return _blob[index];
|
|
}
|
|
|
|
void write(size_t index, uint8_t value) {
|
|
TEST_ASSERT_LESS_THAN(_size, index);
|
|
_blob[index] = value;
|
|
}
|
|
|
|
void commit() {
|
|
}
|
|
|
|
T& _blob;
|
|
const size_t _size;
|
|
|
|
};
|
|
|
|
} // namespace embedis
|
|
} // namespace settings
|
|
|
|
template <size_t Size>
|
|
struct StorageHandler {
|
|
using array_type = std::array<uint8_t, Size>;
|
|
using storage_type = settings::embedis::StaticArrayStorage<array_type>;
|
|
using kvs_type = settings::embedis::KeyValueStore<storage_type>;
|
|
|
|
StorageHandler() :
|
|
kvs(std::move(storage_type{blob}), 0, Size)
|
|
{
|
|
blob.fill(0xff);
|
|
}
|
|
|
|
array_type blob;
|
|
kvs_type kvs;
|
|
};
|
|
|
|
// generate stuff depending on the mode
|
|
// - Indexed: key1:val1, key2:val2, ...
|
|
// - IncreasingLength: k:v, kk:vv, ...
|
|
struct TestSequentialKvGenerator {
|
|
|
|
using kv = std::pair<String, String>;
|
|
enum class Mode {
|
|
Indexed,
|
|
IncreasingLength
|
|
};
|
|
|
|
TestSequentialKvGenerator() = default;
|
|
explicit TestSequentialKvGenerator(Mode mode) :
|
|
_mode(mode)
|
|
{}
|
|
|
|
const kv& next() {
|
|
auto index = _index++;
|
|
|
|
_current.first = "";
|
|
_current.second = "";
|
|
|
|
switch (_mode) {
|
|
case Mode::Indexed:
|
|
_current.first = String("key") + String(index);
|
|
_current.second = String("val") + String(index);
|
|
break;
|
|
case Mode::IncreasingLength: {
|
|
size_t sizes = _index;
|
|
_current.first.reserve(sizes);
|
|
_current.second.reserve(sizes);
|
|
|
|
do {
|
|
_current.first += "k";
|
|
_current.second += "v";
|
|
} while (--sizes);
|
|
break;
|
|
}
|
|
}
|
|
TEST_ASSERT(_last.first != _current.first);
|
|
TEST_ASSERT(_last.second != _current.second);
|
|
|
|
return (_last = _current);
|
|
}
|
|
|
|
std::vector<kv> make(size_t size) {;
|
|
std::vector<kv> res;
|
|
for (size_t index = 0; index < size; ++index) {
|
|
res.push_back(next());
|
|
}
|
|
return res;
|
|
}
|
|
|
|
kv _current;
|
|
kv _last;
|
|
|
|
Mode _mode { Mode::Indexed };
|
|
size_t _index { 0 };
|
|
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
using TestStorageHandler = StorageHandler<1024>;
|
|
|
|
template <typename T>
|
|
void check_kv(T& instance, const String& key, const String& value) {
|
|
auto result = instance.kvs.get(key);
|
|
TEST_ASSERT_MESSAGE(static_cast<bool>(result), key.c_str());
|
|
TEST_ASSERT(result.length());
|
|
TEST_ASSERT_EQUAL_STRING(value.c_str(), result.c_str());
|
|
};
|
|
|
|
void test_sizes() {
|
|
|
|
// empty storage is still manageble, it just does not work :)
|
|
{
|
|
StorageHandler<0> empty;
|
|
TEST_ASSERT_EQUAL(0, empty.kvs.count());
|
|
TEST_ASSERT_FALSE(empty.kvs.set("cannot", "happen"));
|
|
TEST_ASSERT_FALSE(static_cast<bool>(empty.kvs.get("cannot")));
|
|
}
|
|
|
|
// some hard-coded estimates to notify us about internal changes
|
|
{
|
|
StorageHandler<16> instance;
|
|
TEST_ASSERT_EQUAL(0, instance.kvs.count());
|
|
TEST_ASSERT_EQUAL(16, instance.kvs.available());
|
|
TEST_ASSERT_EQUAL(0, settings::embedis::estimate("", "123456"));
|
|
TEST_ASSERT_EQUAL(16, settings::embedis::estimate("123456", "123456"));
|
|
TEST_ASSERT_EQUAL(10, settings::embedis::estimate("123", "123"));
|
|
TEST_ASSERT_EQUAL(7, settings::embedis::estimate("345", ""));
|
|
TEST_ASSERT_EQUAL(0, settings::embedis::estimate("", ""));
|
|
TEST_ASSERT_EQUAL(5, settings::embedis::estimate("1", ""));
|
|
}
|
|
|
|
}
|
|
|
|
void test_longkey() {
|
|
|
|
TestStorageHandler instance;
|
|
const auto estimate = instance.kvs.size() - 6;
|
|
|
|
String key;
|
|
key.reserve(estimate);
|
|
for (size_t n = 0; n < estimate; ++n) {
|
|
key += 'a';
|
|
}
|
|
|
|
TEST_ASSERT(instance.kvs.set(key, ""));
|
|
auto result = instance.kvs.get(key);
|
|
TEST_ASSERT(static_cast<bool>(result));
|
|
|
|
}
|
|
|
|
void test_perseverance() {
|
|
|
|
// ensure we can handle setting the same key
|
|
using storage_type = StorageHandler<128>;
|
|
using blob_type = decltype(std::declval<storage_type>().blob);
|
|
|
|
// xxx: implementation detail?
|
|
// can we avoid blob modification when value is the same as the existing one
|
|
{
|
|
storage_type instance;
|
|
blob_type original(instance.blob);
|
|
|
|
TEST_ASSERT(instance.kvs.set("key", "value"));
|
|
TEST_ASSERT(instance.kvs.set("another", "keyvalue"));
|
|
TEST_ASSERT(original != instance.blob);
|
|
blob_type snapshot(instance.blob);
|
|
|
|
TEST_ASSERT(instance.kvs.set("key", "value"));
|
|
TEST_ASSERT(snapshot == instance.blob);
|
|
}
|
|
|
|
// xxx: pointless implementation detail?
|
|
// can we re-use existing 'value' storage and avoid data-shift
|
|
{
|
|
storage_type instance;
|
|
blob_type original(instance.blob);
|
|
|
|
// insert in a specific order, change middle
|
|
TEST_ASSERT(instance.kvs.set("aaa", "bbb"));
|
|
TEST_ASSERT(instance.kvs.set("cccc", "dd"));
|
|
TEST_ASSERT(instance.kvs.set("ee", "fffff"));
|
|
TEST_ASSERT(instance.kvs.set("cccc", "ff"));
|
|
TEST_ASSERT(original != instance.blob);
|
|
blob_type before(instance.blob);
|
|
|
|
// purge, insert again with updated values
|
|
TEST_ASSERT(instance.kvs.del("aaa"));
|
|
TEST_ASSERT(instance.kvs.del("cccc"));
|
|
TEST_ASSERT(instance.kvs.del("ee"));
|
|
|
|
TEST_ASSERT(instance.kvs.set("aaa", "bbb"));
|
|
TEST_ASSERT(instance.kvs.set("cccc", "ff"));
|
|
TEST_ASSERT(instance.kvs.set("ee", "fffff"));
|
|
blob_type after(instance.blob);
|
|
|
|
TEST_ASSERT(original != before);
|
|
TEST_ASSERT(original != after);
|
|
TEST_ASSERT(before == after);
|
|
}
|
|
}
|
|
|
|
template <size_t Size>
|
|
struct test_overflow_runner {
|
|
void operator ()() const {
|
|
StorageHandler<Size> instance;
|
|
|
|
TEST_ASSERT(instance.kvs.set("a", "b"));
|
|
TEST_ASSERT(instance.kvs.set("c", "d"));
|
|
|
|
TEST_ASSERT_EQUAL(2, instance.kvs.count());
|
|
TEST_ASSERT_FALSE(instance.kvs.set("e", "f"));
|
|
|
|
TEST_ASSERT(instance.kvs.del("a"));
|
|
|
|
TEST_ASSERT_EQUAL(1, instance.kvs.count());
|
|
TEST_ASSERT(instance.kvs.set("e", "f"));
|
|
|
|
TEST_ASSERT_EQUAL(2, instance.kvs.count());
|
|
|
|
check_kv(instance, "e", "f");
|
|
check_kv(instance, "c", "d");
|
|
}
|
|
};
|
|
|
|
void test_overflow() {
|
|
// slightly more that available, but we cannot fit the key
|
|
test_overflow_runner<16>();
|
|
|
|
// no more space
|
|
test_overflow_runner<12>();
|
|
}
|
|
|
|
void test_small_gaps() {
|
|
|
|
// ensure we can intemix empty and non-empty values
|
|
TestStorageHandler instance;
|
|
|
|
TEST_ASSERT(instance.kvs.set("key", "value"));
|
|
TEST_ASSERT(instance.kvs.set("empty", ""));
|
|
TEST_ASSERT(instance.kvs.set("empty_again", ""));
|
|
TEST_ASSERT(instance.kvs.set("finally", "avalue"));
|
|
|
|
auto check_empty = [&instance](const String& key) {
|
|
auto result = instance.kvs.get(key);
|
|
TEST_ASSERT(static_cast<bool>(result));
|
|
TEST_ASSERT_FALSE(result.length());
|
|
};
|
|
|
|
check_empty("empty_again");
|
|
check_empty("empty");
|
|
check_empty("empty_again");
|
|
check_empty("empty");
|
|
|
|
auto check_value = [&instance](const String& key, const String& value) {
|
|
auto result = instance.kvs.get(key);
|
|
TEST_ASSERT(static_cast<bool>(result));
|
|
TEST_ASSERT(result.length());
|
|
TEST_ASSERT_EQUAL_STRING(value.c_str(), result.c_str());
|
|
};
|
|
|
|
check_value("finally", "avalue");
|
|
check_value("key", "value");
|
|
|
|
}
|
|
|
|
void test_remove_randomized() {
|
|
|
|
// ensure we can remove keys in any order
|
|
// 8 seems like a good number to stop on, 9 will spend ~10seconds
|
|
// TODO: seems like a good start benchmarking read / write performance?
|
|
constexpr size_t KeysNumber = 8;
|
|
|
|
TestSequentialKvGenerator generator(TestSequentialKvGenerator::Mode::IncreasingLength);
|
|
auto kvs = generator.make(KeysNumber);
|
|
|
|
// generate indexes array to allow us to reference keys at random
|
|
TestStorageHandler instance;
|
|
std::array<size_t, KeysNumber> indexes;
|
|
std::iota(indexes.begin(), indexes.end(), 0);
|
|
|
|
// - insert keys sequentially
|
|
// - remove keys based on the order provided by next_permutation()
|
|
size_t index = 0;
|
|
do {
|
|
TEST_ASSERT_EQUAL(0, instance.kvs.count());
|
|
|
|
for (auto& kv : kvs) {
|
|
TEST_ASSERT(instance.kvs.set(kv.first, kv.second));
|
|
}
|
|
|
|
for (auto index : indexes) {
|
|
auto key = kvs[index].first;
|
|
TEST_ASSERT(static_cast<bool>(instance.kvs.get(key)));
|
|
TEST_ASSERT(instance.kvs.del(key));
|
|
TEST_ASSERT_FALSE(static_cast<bool>(instance.kvs.get(key)));
|
|
}
|
|
|
|
index++;
|
|
} while (std::next_permutation(indexes.begin(), indexes.end()));
|
|
|
|
String message("- keys: ");
|
|
message += KeysNumber;
|
|
message += ", permutations: ";
|
|
message += index;
|
|
TEST_MESSAGE(message.c_str());
|
|
|
|
}
|
|
|
|
void test_basic() {
|
|
TestStorageHandler instance;
|
|
|
|
constexpr size_t KeysNumber = 5;
|
|
|
|
// ensure insert works
|
|
TestSequentialKvGenerator generator;
|
|
auto kvs = generator.make(KeysNumber);
|
|
|
|
for (auto& kv : kvs) {
|
|
instance.kvs.set(kv.first, kv.second);
|
|
}
|
|
|
|
// and we can retrieve keys back
|
|
for (auto& kv : kvs) {
|
|
auto result = instance.kvs.get(kv.first);
|
|
TEST_ASSERT(static_cast<bool>(result));
|
|
TEST_ASSERT_EQUAL_STRING(kv.second.c_str(), result.c_str());
|
|
}
|
|
|
|
}
|
|
|
|
void test_storage() {
|
|
|
|
constexpr size_t Size = 32;
|
|
StorageHandler<Size> instance;
|
|
|
|
// empty keys are invalid
|
|
TEST_ASSERT_FALSE(instance.kvs.set("", "value1"));
|
|
TEST_ASSERT_FALSE(instance.kvs.del(""));
|
|
|
|
// ...and both keys are not yet set
|
|
TEST_ASSERT_FALSE(instance.kvs.del("key1"));
|
|
TEST_ASSERT_FALSE(instance.kvs.del("key2"));
|
|
|
|
// some different ways to set keys
|
|
TEST_ASSERT(instance.kvs.set("key1", "value0"));
|
|
TEST_ASSERT_EQUAL(1, instance.kvs.count());
|
|
TEST_ASSERT(instance.kvs.set("key1", "value1"));
|
|
TEST_ASSERT_EQUAL(1, instance.kvs.count());
|
|
|
|
TEST_ASSERT(instance.kvs.set("key2", "value_old"));
|
|
TEST_ASSERT_EQUAL(2, instance.kvs.count());
|
|
TEST_ASSERT(instance.kvs.set("key2", "value2"));
|
|
TEST_ASSERT_EQUAL(2, instance.kvs.count());
|
|
|
|
auto kvsize = settings::embedis::estimate("key1", "value1");
|
|
TEST_ASSERT_EQUAL((Size - (2 * kvsize)), instance.kvs.available());
|
|
|
|
// checking keys one by one by using a separate kvs object,
|
|
// working on the same underlying data-store
|
|
using storage_type = decltype(instance)::storage_type;
|
|
using kvs_type = decltype(instance)::kvs_type;
|
|
|
|
// - ensure we can operate with storage offsets
|
|
// - test for internal length optimization that will overwrite the key in-place
|
|
// - make sure we did not break the storage above
|
|
// storage_type accepts reference to the blob, so we can seamlessly use the same
|
|
// underlying data storage and share it between kvs instances
|
|
{
|
|
kvs_type slice(storage_type(instance.blob), (Size - kvsize), Size);
|
|
TEST_ASSERT_EQUAL(1, slice.count());
|
|
TEST_ASSERT_EQUAL(kvsize, slice.size());
|
|
TEST_ASSERT_EQUAL(0, slice.available());
|
|
auto result = slice.get("key1");
|
|
TEST_ASSERT(static_cast<bool>(result));
|
|
TEST_ASSERT_EQUAL_STRING("value1", result.c_str());
|
|
}
|
|
|
|
// ensure that right offset also works
|
|
{
|
|
kvs_type slice(storage_type(instance.blob), 0, (Size - kvsize));
|
|
TEST_ASSERT_EQUAL(1, slice.count());
|
|
TEST_ASSERT_EQUAL((Size - kvsize), slice.size());
|
|
TEST_ASSERT_EQUAL((Size - kvsize - kvsize), slice.available());
|
|
auto result = slice.get("key2");
|
|
TEST_ASSERT(static_cast<bool>(result));
|
|
TEST_ASSERT_EQUAL_STRING("value2", result.c_str());
|
|
}
|
|
|
|
// ensure offset does not introduce offset bugs
|
|
// for instance, test in-place key overwrite by moving left boundary 2 bytes to the right
|
|
{
|
|
const auto available = instance.kvs.available();
|
|
const auto offset = 2;
|
|
|
|
TEST_ASSERT_GREATER_OR_EQUAL(offset, available);
|
|
|
|
kvs_type slice(storage_type(instance.blob), offset, Size);
|
|
TEST_ASSERT_EQUAL(2, slice.count());
|
|
|
|
auto key1 = slice.get("key1");
|
|
TEST_ASSERT(static_cast<bool>(key1));
|
|
|
|
String updated(key1.ref());
|
|
for (size_t index = 0; index < key1.length(); ++index) {
|
|
updated[index] = 'A';
|
|
}
|
|
|
|
TEST_ASSERT(slice.set("key1", updated));
|
|
TEST_ASSERT(slice.set("key2", updated));
|
|
|
|
TEST_ASSERT_EQUAL(2, slice.count());
|
|
|
|
auto check_key1 = slice.get("key1");
|
|
TEST_ASSERT(static_cast<bool>(check_key1));
|
|
TEST_ASSERT_EQUAL_STRING(updated.c_str(), check_key1.c_str());
|
|
|
|
auto check_key2 = slice.get("key2");
|
|
TEST_ASSERT(static_cast<bool>(check_key2));
|
|
TEST_ASSERT_EQUAL_STRING(updated.c_str(), check_key2.c_str());
|
|
|
|
TEST_ASSERT_EQUAL(available - offset, slice.available());
|
|
}
|
|
|
|
}
|
|
|
|
void test_keys_iterator() {
|
|
|
|
constexpr size_t Size = 32;
|
|
StorageHandler<Size> instance;
|
|
|
|
TEST_ASSERT_EQUAL(Size, instance.kvs.available());
|
|
TEST_ASSERT_EQUAL(Size, instance.kvs.size());
|
|
TEST_ASSERT(instance.kvs.set("key", "value"));
|
|
TEST_ASSERT(instance.kvs.set("another", "thing"));
|
|
|
|
// ensure we get the same order of keys when iterating via foreach
|
|
std::vector<String> keys;
|
|
instance.kvs.foreach([&keys](decltype(instance)::kvs_type::KeyValueResult&& kv) {
|
|
keys.push_back(kv.key.read());
|
|
});
|
|
|
|
TEST_ASSERT(instance.kvs.keys() == keys);
|
|
TEST_ASSERT_EQUAL(2, keys.size());
|
|
TEST_ASSERT_EQUAL(2, instance.kvs.count());
|
|
TEST_ASSERT_EQUAL_STRING("key", keys[0].c_str());
|
|
TEST_ASSERT_EQUAL_STRING("another", keys[1].c_str());
|
|
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_storage);
|
|
RUN_TEST(test_keys_iterator);
|
|
RUN_TEST(test_basic);
|
|
RUN_TEST(test_remove_randomized);
|
|
RUN_TEST(test_small_gaps);
|
|
RUN_TEST(test_overflow);
|
|
RUN_TEST(test_perseverance);
|
|
RUN_TEST(test_longkey);
|
|
RUN_TEST(test_sizes);
|
|
return UNITY_END();
|
|
}
|