* wip based on early draft. todo benchmarking * fixup eraser, assume keys are unique * fix cursor copy, test removal at random * small benchmark via permutations. todo lambdas and novirtual * fix empty condition / reset * overwrite optimizations, fix move offsets overflows * ...erase using 0xff instead of 0 * test the theory with code, different length kv were bugged * try to check for out-of-bounds writes / reads * style * trying to fix mover again * clarify length, defend against reading len on edge * fix uncommited rewind change * prove space assumptions * more concise traces, fix move condition (agrh!!!) * slightly more internal knowledge (estimates API?) * make sure cursor is only valid within the range * ensure 0 does not blow things * go back up * cursor comments * comments * rewrite writes through cursor * in del too * estimate kv storage requirements, return available size * move raw erase / move into a method, allow ::set to avoid scanning storage twice * refactor naming, use in code * amend storage slicing test * fix crash handler offsets, cleanup configuration * start -> begin * eeprom readiness * dependencies * unused * SPI_FLASH constants for older Core * vtables -> templates * less include dependencies * gcov help, move estimate outside of the class * writer position can never match, use begin + offset * tweak save_crash to trigger only once in a serious crash * doh, header function should be inline * foreach api, tweak structs for public api * use test helper class * when not using foreach, move cursor reset closer to the loop using read_kv * coverage comments, fix typo in tests decltype * ensure set() does not break with offset * make codacy happy againmcspr-patch-1
@ -0,0 +1,674 @@ | |||||
/* | |||||
Part of the SETTINGS MODULE | |||||
Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com> | |||||
Reimplementation of the Embedis storage format: | |||||
- https://github.com/thingSoC/embedis | |||||
*/ | |||||
#pragma once | |||||
#include <Arduino.h> | |||||
#include <algorithm> | |||||
#include <memory> | |||||
#include <vector> | |||||
#include "libs/TypeChecks.h" | |||||
namespace settings { | |||||
namespace embedis { | |||||
// 'optional' type for byte range | |||||
struct ValueResult { | |||||
operator bool() { | |||||
return result; | |||||
} | |||||
bool result { false }; | |||||
String value; | |||||
}; | |||||
// We can't save empty keys but can save empty values as 0x00 0x00 0x00 0x00 | |||||
// total sum is: | |||||
// - 2 bytes gap at the end (will be re-used by the next value length byte) | |||||
// - 4 bytes to store length of 2 values (stored as big-endian) | |||||
// - N bytes of values themselves | |||||
inline size_t estimate(const String& key, const String& value) { | |||||
if (!key.length()) { | |||||
return 0; | |||||
} | |||||
const auto key_len = key.length(); | |||||
const auto value_len = value.length(); | |||||
return (4 + key_len + ((value_len > 0) ? value_len : 2)); | |||||
} | |||||
// Note: KeyValueStore is templated to avoid having to provide RawStorageBase via virtual inheritance. | |||||
template <typename RawStorageBase> | |||||
class KeyValueStore { | |||||
// ----------------------------------------------------------------------------------- | |||||
// Notice: we can only use sfinae checks with the current compiler version | |||||
// TODO: provide actual benchmark comparison with 'lambda'-list-as-vtable (old Embedis style) | |||||
// and vtable approach (write(), read() and commit() as pure virtual) | |||||
// TODO: consider overrides for bulk operations like move (see ::del method) | |||||
template <typename T> | |||||
using storage_can_write_t = decltype(std::declval<T>().write( | |||||
std::declval<uint16_t>(), std::declval<uint8_t>())); | |||||
template <typename T> | |||||
using storage_can_write = is_detected<storage_can_write_t, T>; | |||||
template <typename T> | |||||
using storage_can_read_t = decltype(std::declval<T>().read(std::declval<uint16_t>())); | |||||
template <typename T> | |||||
using storage_can_read = is_detected<storage_can_read_t, T>; | |||||
template <typename T> | |||||
using storage_can_commit_t = decltype(std::declval<T>().commit()); | |||||
template <typename T> | |||||
using storage_can_commit = is_detected<storage_can_commit_t, T>; | |||||
static_assert( | |||||
(storage_can_commit<RawStorageBase>{} && | |||||
storage_can_read<RawStorageBase>{} && | |||||
storage_can_write<RawStorageBase>{}), | |||||
"Storage class must implement read(index), write(index, byte) and commit()" | |||||
); | |||||
// ----------------------------------------------------------------------------------- | |||||
protected: | |||||
// Tracking state of the parser inside of _raw_read() | |||||
enum class State { | |||||
Begin, | |||||
End, | |||||
LenByte1, | |||||
LenByte2, | |||||
EmptyValue, | |||||
Value | |||||
}; | |||||
// Pointer to the region of data that we are using | |||||
// | |||||
// XXX: It does not matter right now, but we **will** overflow position when using sizes >= (2^16) - 1 | |||||
// Note: Implementation is also in the header b/c c++ won't allow us | |||||
// to have a plain member (not a ptr or ref) of unknown size. | |||||
// Note: There was a considiration to implement this as 'stashing iterator' to be compatible with stl algorithms. | |||||
// In such implementation, we would store intermediate index and allow the user to receive a `value_proxy`, | |||||
// temporary returned by `value_proxy& operator*()' that is bound to Cursor instance. | |||||
// This **will** cause problems with 'reverse_iterator' or anything like it, as it expects reference to | |||||
// outlive the iterator object (specifically, result of `return *--tmp`, where `tmp` is created inside of a function block) | |||||
struct Cursor { | |||||
Cursor(RawStorageBase& storage, uint16_t position_, uint16_t begin_, uint16_t end_) : | |||||
position(position_), | |||||
begin(begin_), | |||||
end(end_), | |||||
_storage(storage) | |||||
{} | |||||
Cursor(RawStorageBase& storage, uint16_t begin_, uint16_t end_) : | |||||
Cursor(storage, 0, begin_, end_) | |||||
{} | |||||
explicit Cursor(RawStorageBase& storage) : | |||||
Cursor(storage, 0, 0, 0) | |||||
{} | |||||
static Cursor merge(RawStorageBase& storage, const Cursor& key, const Cursor& value) { | |||||
return Cursor(storage, key.begin, value.end); | |||||
} | |||||
static Cursor fromEnd(RawStorageBase& storage, uint16_t begin, uint16_t end) { | |||||
return Cursor(storage, end - begin, begin, end); | |||||
} | |||||
Cursor() = delete; | |||||
void reset(uint16_t begin_, uint16_t end_) { | |||||
position = 0; | |||||
begin = begin_; | |||||
end = end_; | |||||
} | |||||
uint8_t read() { | |||||
return _storage.read(begin + position); | |||||
} | |||||
void write(uint8_t value) { | |||||
_storage.write(begin + position, value); | |||||
} | |||||
void resetBeginning() { | |||||
position = 0; | |||||
} | |||||
void resetEnd() { | |||||
position = end - begin; | |||||
} | |||||
size_t size() { | |||||
return (end - begin); | |||||
} | |||||
bool inRange(uint16_t position_) { | |||||
return (position_ < (end - begin)); | |||||
} | |||||
operator bool() { | |||||
return inRange(position); | |||||
} | |||||
uint8_t operator[](size_t position_) const { | |||||
return _storage.read(begin + position_); | |||||
} | |||||
bool operator ==(const Cursor& other) { | |||||
return (begin == other.begin) && (end == other.end); | |||||
} | |||||
bool operator !=(const Cursor& other) { | |||||
return !(*this == other); | |||||
} | |||||
Cursor& operator++() { | |||||
++position; | |||||
return *this; | |||||
} | |||||
Cursor operator++(int) { | |||||
Cursor other(*this); | |||||
++*this; | |||||
return other; | |||||
} | |||||
Cursor& operator--() { | |||||
--position; | |||||
return *this; | |||||
} | |||||
Cursor operator--(int) { | |||||
Cursor other(*this); | |||||
--*this; | |||||
return other; | |||||
} | |||||
uint16_t position; | |||||
uint16_t begin; | |||||
uint16_t end; | |||||
private: | |||||
RawStorageBase& _storage; | |||||
}; | |||||
public: | |||||
// Store value location in a more reasonable forward-iterator-style manner | |||||
// Allows us to skip string creation when just searching for specific values | |||||
// XXX: be cautious that cursor positions **will** break when underlying storage changes | |||||
struct ReadResult { | |||||
friend class KeyValueStore<RawStorageBase>; | |||||
ReadResult(const Cursor& cursor_) : | |||||
length(0), | |||||
cursor(cursor_), | |||||
result(false) | |||||
{} | |||||
ReadResult(RawStorageBase& storage) : | |||||
length(0), | |||||
cursor(storage), | |||||
result(false) | |||||
{} | |||||
operator bool() { | |||||
return result; | |||||
} | |||||
String read() { | |||||
String out; | |||||
out.reserve(length); | |||||
if (!length) { | |||||
return out; | |||||
} | |||||
decltype(length) index = 0; | |||||
cursor.resetBeginning(); | |||||
while (index < length) { | |||||
out += static_cast<char>(cursor.read()); | |||||
++cursor; | |||||
++index; | |||||
} | |||||
return out; | |||||
} | |||||
uint16_t length; | |||||
private: | |||||
Cursor cursor; | |||||
bool result; | |||||
}; | |||||
// Internal storage consists of sequences of <byte-range><length> | |||||
struct KeyValueResult { | |||||
operator bool() { | |||||
return (key) && (value) && (key.length > 0); | |||||
} | |||||
bool operator !() { | |||||
return !(static_cast<bool>(*this)); | |||||
} | |||||
template <typename T = ReadResult> | |||||
KeyValueResult(T&& key_, T&& value_) : | |||||
key(std::forward<T>(key_)), | |||||
value(std::forward<T>(value_)) | |||||
{} | |||||
KeyValueResult(RawStorageBase& storage) : | |||||
key(storage), | |||||
value(storage) | |||||
{} | |||||
ReadResult key; | |||||
ReadResult value; | |||||
}; | |||||
// one and only possible constructor, simply move the class object into the | |||||
// member variable to avoid forcing the user of the API to keep 2 objects alive. | |||||
KeyValueStore(RawStorageBase&& storage, uint16_t begin, uint16_t end) : | |||||
_storage(std::move(storage)), | |||||
_cursor(_storage, begin, end), | |||||
_state(State::Begin) | |||||
{} | |||||
// Try to find the matching key. Datastructure that we use does not specify | |||||
// any value 'type' inside of it. We expect 'key' to be the first non-empty string, | |||||
// 'value' can be empty. | |||||
ValueResult get(const String& key) { | |||||
return _get(key, true); | |||||
} | |||||
bool has(const String& key) { | |||||
return static_cast<bool>(_get(key, false)); | |||||
} | |||||
// We going be using this pattern all the time here, because we need 2 consecutive **valid** ranges | |||||
// TODO: expose _read_kv() and _cursor_reset_end() so we can have 'break' here? | |||||
// perhaps as a wrapper object, allow something like next() and seekBegin() | |||||
template <typename CallbackType> | |||||
void foreach(CallbackType callback) { | |||||
_cursor_reset_end(); | |||||
do { | |||||
auto kv = _read_kv(); | |||||
if (!kv) { | |||||
break; | |||||
} | |||||
callback(std::move(kv)); | |||||
} while (_state != State::End); | |||||
} | |||||
// read every key into a vector | |||||
std::vector<String> keys() { | |||||
std::vector<String> out; | |||||
out.reserve(count()); | |||||
foreach([&](KeyValueResult&& kv) { | |||||
out.push_back(kv.key.read()); | |||||
}); | |||||
return out; | |||||
} | |||||
// set or update key with value contents. ensure 'key' isn't empty, 'value' can be empty | |||||
bool set(const String& key, const String& value) { | |||||
// ref. 'estimate()' implementation in regards to the storage calculation | |||||
auto need = estimate(key, value); | |||||
if (!need) { | |||||
return false; | |||||
} | |||||
auto key_len = key.length(); | |||||
auto value_len = value.length(); | |||||
Cursor to_erase(_storage); | |||||
bool need_erase = false; | |||||
auto start_pos = _cursor_reset_end(); | |||||
do { | |||||
auto kv = _read_kv(); | |||||
if (!kv) { | |||||
break; | |||||
} | |||||
start_pos = kv.value.cursor.begin; | |||||
// in the very special case we can match the existing key | |||||
if ((kv.key.length == key_len) && (kv.key.read() == key)) { | |||||
if (kv.value.length == value.length()) { | |||||
if (kv.value.read() == value) { | |||||
return true; | |||||
} | |||||
start_pos = kv.key.cursor.end; | |||||
break; | |||||
} | |||||
// but we may need to write over it, when contents are different | |||||
to_erase.reset(kv.value.cursor.begin, kv.key.cursor.end); | |||||
need_erase = true; | |||||
} | |||||
} while (_state != State::End); | |||||
if (need_erase) { | |||||
_raw_erase(start_pos, to_erase); | |||||
start_pos += to_erase.size(); | |||||
} | |||||
// we should only insert when possition is still within possible size | |||||
if (start_pos && (start_pos >= need)) { | |||||
auto writer = Cursor::fromEnd(_storage, start_pos - need, start_pos); | |||||
// put the length of the value as 2 bytes and then write the data | |||||
(--writer).write(key_len & 0xff); | |||||
(--writer).write((key_len >> 8) & 0xff); | |||||
while (key_len--) { | |||||
(--writer).write(key[key_len]); | |||||
} | |||||
(--writer).write(value_len & 0xff); | |||||
(--writer).write((value_len >> 8) & 0xff); | |||||
if (value_len) { | |||||
while (value_len--) { | |||||
(--writer).write(value[value_len]); | |||||
} | |||||
} else { | |||||
(--writer).write(0); | |||||
(--writer).write(0); | |||||
} | |||||
// we also need to pad the space *after* the value | |||||
// but, only when we still have some space left | |||||
if ((start_pos - need) >= 2) { | |||||
_cursor_set_position(writer.begin - _cursor.begin); | |||||
auto next_kv = _read_kv(); | |||||
if (!next_kv) { | |||||
auto padding = Cursor::fromEnd(_storage, writer.begin - 2, writer.begin); | |||||
(--padding).write(0); | |||||
(--padding).write(0); | |||||
} | |||||
} | |||||
_storage.commit(); | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
// remove key from the storage. will check that 'key' argument isn't empty | |||||
bool del(const String& key) { | |||||
size_t key_len = key.length(); | |||||
if (!key_len) { | |||||
return false; | |||||
} | |||||
// Removes key from the storage by overwriting the key with left-most data | |||||
size_t start_pos = _cursor_reset_end() - 1; | |||||
auto to_erase = Cursor::fromEnd(_storage, _cursor.begin, _cursor.end); | |||||
// we should only compare strings of equal length. | |||||
// when matching, record { value ... key } range + 4 bytes for length | |||||
// continue searching for the leftmost boundary | |||||
foreach([&](KeyValueResult&& kv) { | |||||
start_pos = kv.value.cursor.begin; | |||||
if (!to_erase && (kv.key.length == key_len) && (kv.key.read() == key)) { | |||||
to_erase.reset(kv.value.cursor.begin, kv.key.cursor.end); | |||||
} | |||||
}); | |||||
if (!to_erase) { | |||||
return false; | |||||
} | |||||
_raw_erase(start_pos, to_erase); | |||||
return true; | |||||
} | |||||
// Simply count key-value pairs that we could parse | |||||
size_t count() { | |||||
size_t result = 0; | |||||
foreach([&result](KeyValueResult&&) { | |||||
++result; | |||||
}); | |||||
return result; | |||||
} | |||||
// Do exactly the same thing as 'keys' does, but return the amount | |||||
// of bytes to the left of the last kv | |||||
size_t available() { | |||||
size_t result = _cursor.size(); | |||||
foreach([&result](KeyValueResult&& kv) { | |||||
result -= kv.key.cursor.size(); | |||||
result -= kv.value.cursor.size(); | |||||
}); | |||||
return result; | |||||
} | |||||
// How much bytes can be used is directly read from the cursor, based on begin and end values | |||||
size_t size() { | |||||
return _cursor.size(); | |||||
} | |||||
protected: | |||||
// Try to find the matching key. Datastructure that we use does not specify | |||||
// any value 'type' inside of it. We expect 'key' to be the first non-empty string, | |||||
// 'value' can be empty. | |||||
// To implement has(), allow to skip reading the value | |||||
ValueResult _get(const String& key, bool read_value) { | |||||
ValueResult out; | |||||
auto len = key.length(); | |||||
_cursor_reset_end(); | |||||
do { | |||||
auto kv = _read_kv(); | |||||
if (!kv) { | |||||
break; | |||||
} | |||||
// no point in comparing keys when length does not match | |||||
// (and we also don't want to allocate the string) | |||||
if (kv.key.length != len) { | |||||
continue; | |||||
} | |||||
auto key_result = kv.key.read(); | |||||
if (key_result == key) { | |||||
if (read_value) { | |||||
out.value = kv.value.read(); | |||||
} | |||||
out.result = true; | |||||
break; | |||||
} | |||||
} while (_state != State::End); | |||||
return out; | |||||
} | |||||
// Place cursor at the `end` and resets the parser to expect length byte | |||||
uint16_t _cursor_reset_end() { | |||||
_cursor.resetEnd(); | |||||
_state = State::Begin; | |||||
return _cursor.end; | |||||
} | |||||
uint16_t _cursor_set_position(uint16_t position) { | |||||
_state = State::Begin; | |||||
_cursor.position = position; | |||||
return _cursor.position; | |||||
} | |||||
// implementation quirk is that `Cursor::operator=` won't work because of the `RawStorageBase&` member | |||||
// right now, just construct in place and assume that compiler will inline things | |||||
// on one hand, we can implement it. but, we can't specifically | |||||
KeyValueResult _read_kv() { | |||||
auto key = _raw_read(); | |||||
if (!key || !key.length) { | |||||
return {_storage}; | |||||
} | |||||
auto value = _raw_read(); | |||||
return {key, value}; | |||||
}; | |||||
void _raw_erase(size_t start_pos, Cursor& to_erase) { | |||||
// we either end up to the left or to the right of the boundary | |||||
if (start_pos < to_erase.begin) { | |||||
auto from = Cursor::fromEnd(_storage, start_pos, to_erase.begin); | |||||
auto to = Cursor::fromEnd(_storage, start_pos + to_erase.size(), to_erase.end); | |||||
while (--from && --to) { | |||||
to.write(from.read()); | |||||
from.write(0xff); | |||||
}; | |||||
} else { | |||||
// just null the length bytes, since we at the last key | |||||
to_erase.resetEnd(); | |||||
(--to_erase).write(0); | |||||
(--to_erase).write(0); | |||||
} | |||||
_storage.commit(); | |||||
} | |||||
// Returns Cursor to the region that holds the data | |||||
// Result object does not hold any data, we need to explicitly request read() | |||||
// | |||||
// Cursor object is always expected to point to something, e.g. minimum: | |||||
// 0x01 0x00 0x01 | |||||
// data len2 len1 | |||||
// Position will be 0, end will be 4. Total length is 3, data length is 1 | |||||
// | |||||
// Note the distinction between real length and data length. For example, | |||||
// special-case for empty 'value' (as 'key' can never be empty and will be rejected): | |||||
// 0x00 0x00 0x00 0x00 | |||||
// data data len2 len1 | |||||
// Position will be 0, end will be 5. Total length is 4, data length is 0 | |||||
ReadResult _raw_read() { | |||||
uint16_t len = 0; | |||||
ReadResult out(_storage); | |||||
do { | |||||
// storage is written right-to-left, cursor is always decreasing | |||||
switch (_state) { | |||||
case State::Begin: | |||||
if (_cursor.position > 2) { | |||||
--_cursor; | |||||
_state = State::LenByte1; | |||||
} else { | |||||
_state = State::End; | |||||
} | |||||
continue; | |||||
// len is 16 bit uint (bigendian) | |||||
// special case is 0, which is valid and should be returned when encountered | |||||
// another special case is 0xffff, meaning we just hit an empty space | |||||
case State::LenByte1: | |||||
len = _cursor.read(); | |||||
--_cursor; | |||||
_state = State::LenByte2; | |||||
break; | |||||
case State::LenByte2: | |||||
{ | |||||
uint8_t len2 = _cursor.read(); | |||||
--_cursor; | |||||
if ((0 == len) && (0 == len2)) { | |||||
len = 2; | |||||
_state = State::EmptyValue; | |||||
} else if ((0xff == len) && (0xff == len2)) { | |||||
_state = State::End; | |||||
} else { | |||||
len |= len2 << 8; | |||||
_state = State::Value; | |||||
} | |||||
break; | |||||
} | |||||
case State::EmptyValue: | |||||
case State::Value: { | |||||
uint16_t left = len; | |||||
// ensure we don't go out-of-bounds | |||||
switch (_state) { | |||||
case State::Value: | |||||
while (_cursor && --left) { | |||||
--_cursor; | |||||
} | |||||
break; | |||||
// ...and only read 0's | |||||
case State::EmptyValue: | |||||
while (_cursor && (_cursor.read() == 0) && --left) { | |||||
--_cursor; | |||||
} | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
if (left) { | |||||
_state = State::End; | |||||
break; | |||||
} | |||||
// set the resulting cursor as [pos:len+2) | |||||
out.result = true; | |||||
out.length = (_state == State::EmptyValue) ? 0 : len; | |||||
out.cursor.reset( | |||||
_cursor.begin + _cursor.position, | |||||
_cursor.begin + _cursor.position + len + 2 | |||||
); | |||||
_state = State::Begin; | |||||
goto return_result; | |||||
} | |||||
case State::End: | |||||
default: | |||||
break; | |||||
} | |||||
} while (_state != State::End); | |||||
return_result: | |||||
return out; | |||||
} | |||||
RawStorageBase _storage; | |||||
Cursor _cursor; | |||||
State _state { State::Begin }; | |||||
}; | |||||
} // namespace embedis | |||||
} // namespace settings |
@ -0,0 +1,487 @@ | |||||
#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 { | |||||
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.value.length()); | |||||
TEST_ASSERT_EQUAL_STRING(value.c_str(), result.value.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(9, settings::embedis::estimate("345", "")); | |||||
} | |||||
} | |||||
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.value.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.value.length()); | |||||
TEST_ASSERT_EQUAL_STRING(value.c_str(), result.value.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(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.value.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.value.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.value.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.value); | |||||
for (size_t index = 0; index < key1.value.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.value.c_str()); | |||||
auto check_key2 = slice.get("key2"); | |||||
TEST_ASSERT(static_cast<bool>(check_key2)); | |||||
TEST_ASSERT_EQUAL_STRING(updated.c_str(), check_key2.value.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(); | |||||
} |