Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

493 lines
15 KiB

Rework settings (#2282) * 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 again
4 years ago
Rework settings (#2282) * 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 again
4 years ago
Rework settings (#2282) * 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 again
4 years ago
Rework settings (#2282) * 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 again
4 years ago
  1. #include <unity.h>
  2. #include <Arduino.h>
  3. #pragma GCC diagnostic warning "-Wall"
  4. #pragma GCC diagnostic warning "-Wextra"
  5. #pragma GCC diagnostic warning "-Wstrict-aliasing"
  6. #pragma GCC diagnostic warning "-Wpointer-arith"
  7. #pragma GCC diagnostic warning "-Wstrict-overflow=5"
  8. #include <settings_embedis.h>
  9. #include <array>
  10. #include <algorithm>
  11. #include <numeric>
  12. namespace settings {
  13. namespace embedis {
  14. // TODO: either
  15. // - throw exception and print backtrace to signify which method caused the out-of-bounds read or write
  16. // - use a custom macro to supply (instance?) object with the correct line number
  17. template <typename T>
  18. struct StaticArrayStorage {
  19. explicit StaticArrayStorage(T& blob) :
  20. _blob(blob),
  21. _size(blob.size())
  22. {}
  23. uint8_t read(size_t index) {
  24. TEST_ASSERT_LESS_THAN(_size, index);
  25. return _blob[index];
  26. }
  27. void write(size_t index, uint8_t value) {
  28. TEST_ASSERT_LESS_THAN(_size, index);
  29. _blob[index] = value;
  30. }
  31. void commit() {
  32. }
  33. T& _blob;
  34. const size_t _size;
  35. };
  36. } // namespace embedis
  37. } // namespace settings
  38. template <size_t Size>
  39. struct StorageHandler {
  40. using array_type = std::array<uint8_t, Size>;
  41. using storage_type = settings::embedis::StaticArrayStorage<array_type>;
  42. using kvs_type = settings::embedis::KeyValueStore<storage_type>;
  43. StorageHandler() :
  44. kvs(std::move(storage_type{blob}), 0, Size)
  45. {
  46. blob.fill(0xff);
  47. }
  48. array_type blob;
  49. kvs_type kvs;
  50. };
  51. // generate stuff depending on the mode
  52. // - Indexed: key1:val1, key2:val2, ...
  53. // - IncreasingLength: k:v, kk:vv, ...
  54. struct TestSequentialKvGenerator {
  55. using kv = std::pair<String, String>;
  56. enum class Mode {
  57. Indexed,
  58. IncreasingLength
  59. };
  60. TestSequentialKvGenerator() = default;
  61. explicit TestSequentialKvGenerator(Mode mode) :
  62. _mode(mode)
  63. {}
  64. const kv& next() {
  65. auto index = _index++;
  66. _current.first = "";
  67. _current.second = "";
  68. switch (_mode) {
  69. case Mode::Indexed:
  70. _current.first = String("key") + String(index);
  71. _current.second = String("val") + String(index);
  72. break;
  73. case Mode::IncreasingLength: {
  74. size_t sizes = _index;
  75. _current.first.reserve(sizes);
  76. _current.second.reserve(sizes);
  77. do {
  78. _current.first += "k";
  79. _current.second += "v";
  80. } while (--sizes);
  81. break;
  82. }
  83. }
  84. TEST_ASSERT(_last.first != _current.first);
  85. TEST_ASSERT(_last.second != _current.second);
  86. return (_last = _current);
  87. }
  88. std::vector<kv> make(size_t size) {;
  89. std::vector<kv> res;
  90. for (size_t index = 0; index < size; ++index) {
  91. res.push_back(next());
  92. }
  93. return res;
  94. }
  95. kv _current;
  96. kv _last;
  97. Mode _mode { Mode::Indexed };
  98. size_t _index { 0 };
  99. };
  100. // ----------------------------------------------------------------------------
  101. using TestStorageHandler = StorageHandler<1024>;
  102. template <typename T>
  103. void check_kv(T& instance, const String& key, const String& value) {
  104. auto result = instance.kvs.get(key);
  105. TEST_ASSERT_MESSAGE(static_cast<bool>(result), key.c_str());
  106. TEST_ASSERT(result.value.length());
  107. TEST_ASSERT_EQUAL_STRING(value.c_str(), result.value.c_str());
  108. };
  109. void test_sizes() {
  110. // empty storage is still manageble, it just does not work :)
  111. {
  112. StorageHandler<0> empty;
  113. TEST_ASSERT_EQUAL(0, empty.kvs.count());
  114. TEST_ASSERT_FALSE(empty.kvs.set("cannot", "happen"));
  115. TEST_ASSERT_FALSE(static_cast<bool>(empty.kvs.get("cannot")));
  116. }
  117. // some hard-coded estimates to notify us about internal changes
  118. {
  119. StorageHandler<16> instance;
  120. TEST_ASSERT_EQUAL(0, instance.kvs.count());
  121. TEST_ASSERT_EQUAL(16, instance.kvs.available());
  122. TEST_ASSERT_EQUAL(0, settings::embedis::estimate("", "123456"));
  123. TEST_ASSERT_EQUAL(16, settings::embedis::estimate("123456", "123456"));
  124. TEST_ASSERT_EQUAL(10, settings::embedis::estimate("123", "123"));
  125. TEST_ASSERT_EQUAL(7, settings::embedis::estimate("345", ""));
  126. TEST_ASSERT_EQUAL(0, settings::embedis::estimate("", ""));
  127. TEST_ASSERT_EQUAL(5, settings::embedis::estimate("1", ""));
  128. }
  129. }
  130. void test_longkey() {
  131. TestStorageHandler instance;
  132. const auto estimate = instance.kvs.size() - 6;
  133. String key;
  134. key.reserve(estimate);
  135. for (size_t n = 0; n < estimate; ++n) {
  136. key += 'a';
  137. }
  138. TEST_ASSERT(instance.kvs.set(key, ""));
  139. auto result = instance.kvs.get(key);
  140. TEST_ASSERT(static_cast<bool>(result));
  141. }
  142. void test_perseverance() {
  143. // ensure we can handle setting the same key
  144. using storage_type = StorageHandler<128>;
  145. using blob_type = decltype(std::declval<storage_type>().blob);
  146. // xxx: implementation detail?
  147. // can we avoid blob modification when value is the same as the existing one
  148. {
  149. storage_type instance;
  150. blob_type original(instance.blob);
  151. TEST_ASSERT(instance.kvs.set("key", "value"));
  152. TEST_ASSERT(instance.kvs.set("another", "keyvalue"));
  153. TEST_ASSERT(original != instance.blob);
  154. blob_type snapshot(instance.blob);
  155. TEST_ASSERT(instance.kvs.set("key", "value"));
  156. TEST_ASSERT(snapshot == instance.blob);
  157. }
  158. // xxx: pointless implementation detail?
  159. // can we re-use existing 'value' storage and avoid data-shift
  160. {
  161. storage_type instance;
  162. blob_type original(instance.blob);
  163. // insert in a specific order, change middle
  164. TEST_ASSERT(instance.kvs.set("aaa", "bbb"));
  165. TEST_ASSERT(instance.kvs.set("cccc", "dd"));
  166. TEST_ASSERT(instance.kvs.set("ee", "fffff"));
  167. TEST_ASSERT(instance.kvs.set("cccc", "ff"));
  168. TEST_ASSERT(original != instance.blob);
  169. blob_type before(instance.blob);
  170. // purge, insert again with updated values
  171. TEST_ASSERT(instance.kvs.del("aaa"));
  172. TEST_ASSERT(instance.kvs.del("cccc"));
  173. TEST_ASSERT(instance.kvs.del("ee"));
  174. TEST_ASSERT(instance.kvs.set("aaa", "bbb"));
  175. TEST_ASSERT(instance.kvs.set("cccc", "ff"));
  176. TEST_ASSERT(instance.kvs.set("ee", "fffff"));
  177. blob_type after(instance.blob);
  178. TEST_ASSERT(original != before);
  179. TEST_ASSERT(original != after);
  180. TEST_ASSERT(before == after);
  181. }
  182. }
  183. template <size_t Size>
  184. struct test_overflow_runner {
  185. void operator ()() const {
  186. StorageHandler<Size> instance;
  187. TEST_ASSERT(instance.kvs.set("a", "b"));
  188. TEST_ASSERT(instance.kvs.set("c", "d"));
  189. TEST_ASSERT_EQUAL(2, instance.kvs.count());
  190. TEST_ASSERT_FALSE(instance.kvs.set("e", "f"));
  191. TEST_ASSERT(instance.kvs.del("a"));
  192. TEST_ASSERT_EQUAL(1, instance.kvs.count());
  193. TEST_ASSERT(instance.kvs.set("e", "f"));
  194. TEST_ASSERT_EQUAL(2, instance.kvs.count());
  195. check_kv(instance, "e", "f");
  196. check_kv(instance, "c", "d");
  197. }
  198. };
  199. void test_overflow() {
  200. // slightly more that available, but we cannot fit the key
  201. test_overflow_runner<16>();
  202. // no more space
  203. test_overflow_runner<12>();
  204. }
  205. void test_small_gaps() {
  206. // ensure we can intemix empty and non-empty values
  207. TestStorageHandler instance;
  208. TEST_ASSERT(instance.kvs.set("key", "value"));
  209. TEST_ASSERT(instance.kvs.set("empty", ""));
  210. TEST_ASSERT(instance.kvs.set("empty_again", ""));
  211. TEST_ASSERT(instance.kvs.set("finally", "avalue"));
  212. auto check_empty = [&instance](const String& key) {
  213. auto result = instance.kvs.get(key);
  214. TEST_ASSERT(static_cast<bool>(result));
  215. TEST_ASSERT_FALSE(result.value.length());
  216. };
  217. check_empty("empty_again");
  218. check_empty("empty");
  219. check_empty("empty_again");
  220. check_empty("empty");
  221. auto check_value = [&instance](const String& key, const String& value) {
  222. auto result = instance.kvs.get(key);
  223. TEST_ASSERT(static_cast<bool>(result));
  224. TEST_ASSERT(result.value.length());
  225. TEST_ASSERT_EQUAL_STRING(value.c_str(), result.value.c_str());
  226. };
  227. check_value("finally", "avalue");
  228. check_value("key", "value");
  229. }
  230. void test_remove_randomized() {
  231. // ensure we can remove keys in any order
  232. // 8 seems like a good number to stop on, 9 will spend ~10seconds
  233. // TODO: seems like a good start benchmarking read / write performance?
  234. constexpr size_t KeysNumber = 8;
  235. TestSequentialKvGenerator generator(TestSequentialKvGenerator::Mode::IncreasingLength);
  236. auto kvs = generator.make(KeysNumber);
  237. // generate indexes array to allow us to reference keys at random
  238. TestStorageHandler instance;
  239. std::array<size_t, KeysNumber> indexes;
  240. std::iota(indexes.begin(), indexes.end(), 0);
  241. // - insert keys sequentially
  242. // - remove keys based on the order provided by next_permutation()
  243. size_t index = 0;
  244. do {
  245. TEST_ASSERT_EQUAL(0, instance.kvs.count());
  246. for (auto& kv : kvs) {
  247. TEST_ASSERT(instance.kvs.set(kv.first, kv.second));
  248. }
  249. for (auto index : indexes) {
  250. auto key = kvs[index].first;
  251. TEST_ASSERT(static_cast<bool>(instance.kvs.get(key)));
  252. TEST_ASSERT(instance.kvs.del(key));
  253. TEST_ASSERT_FALSE(static_cast<bool>(instance.kvs.get(key)));
  254. }
  255. index++;
  256. } while (std::next_permutation(indexes.begin(), indexes.end()));
  257. String message("- keys: ");
  258. message += KeysNumber;
  259. message += ", permutations: ";
  260. message += index;
  261. TEST_MESSAGE(message.c_str());
  262. }
  263. void test_basic() {
  264. TestStorageHandler instance;
  265. constexpr size_t KeysNumber = 5;
  266. // ensure insert works
  267. TestSequentialKvGenerator generator;
  268. auto kvs = generator.make(KeysNumber);
  269. for (auto& kv : kvs) {
  270. instance.kvs.set(kv.first, kv.second);
  271. }
  272. // and we can retrieve keys back
  273. for (auto& kv : kvs) {
  274. auto result = instance.kvs.get(kv.first);
  275. TEST_ASSERT(static_cast<bool>(result));
  276. TEST_ASSERT_EQUAL_STRING(kv.second.c_str(), result.value.c_str());
  277. }
  278. }
  279. void test_storage() {
  280. constexpr size_t Size = 32;
  281. StorageHandler<Size> instance;
  282. // empty keys are invalid
  283. TEST_ASSERT_FALSE(instance.kvs.set("", "value1"));
  284. TEST_ASSERT_FALSE(instance.kvs.del(""));
  285. // ...and both keys are not yet set
  286. TEST_ASSERT_FALSE(instance.kvs.del("key1"));
  287. TEST_ASSERT_FALSE(instance.kvs.del("key2"));
  288. // some different ways to set keys
  289. TEST_ASSERT(instance.kvs.set("key1", "value0"));
  290. TEST_ASSERT_EQUAL(1, instance.kvs.count());
  291. TEST_ASSERT(instance.kvs.set("key1", "value1"));
  292. TEST_ASSERT_EQUAL(1, instance.kvs.count());
  293. TEST_ASSERT(instance.kvs.set("key2", "value_old"));
  294. TEST_ASSERT_EQUAL(2, instance.kvs.count());
  295. TEST_ASSERT(instance.kvs.set("key2", "value2"));
  296. TEST_ASSERT_EQUAL(2, instance.kvs.count());
  297. auto kvsize = settings::embedis::estimate("key1", "value1");
  298. TEST_ASSERT_EQUAL((Size - (2 * kvsize)), instance.kvs.available());
  299. // checking keys one by one by using a separate kvs object,
  300. // working on the same underlying data-store
  301. using storage_type = decltype(instance)::storage_type;
  302. using kvs_type = decltype(instance)::kvs_type;
  303. // - ensure we can operate with storage offsets
  304. // - test for internal length optimization that will overwrite the key in-place
  305. // - make sure we did not break the storage above
  306. // storage_type accepts reference to the blob, so we can seamlessly use the same
  307. // underlying data storage and share it between kvs instances
  308. {
  309. kvs_type slice(storage_type(instance.blob), (Size - kvsize), Size);
  310. TEST_ASSERT_EQUAL(1, slice.count());
  311. TEST_ASSERT_EQUAL(kvsize, slice.size());
  312. TEST_ASSERT_EQUAL(0, slice.available());
  313. auto result = slice.get("key1");
  314. TEST_ASSERT(static_cast<bool>(result));
  315. TEST_ASSERT_EQUAL_STRING("value1", result.value.c_str());
  316. }
  317. // ensure that right offset also works
  318. {
  319. kvs_type slice(storage_type(instance.blob), 0, (Size - kvsize));
  320. TEST_ASSERT_EQUAL(1, slice.count());
  321. TEST_ASSERT_EQUAL((Size - kvsize), slice.size());
  322. TEST_ASSERT_EQUAL((Size - kvsize - kvsize), slice.available());
  323. auto result = slice.get("key2");
  324. TEST_ASSERT(static_cast<bool>(result));
  325. TEST_ASSERT_EQUAL_STRING("value2", result.value.c_str());
  326. }
  327. // ensure offset does not introduce offset bugs
  328. // for instance, test in-place key overwrite by moving left boundary 2 bytes to the right
  329. {
  330. const auto available = instance.kvs.available();
  331. const auto offset = 2;
  332. TEST_ASSERT_GREATER_OR_EQUAL(offset, available);
  333. kvs_type slice(storage_type(instance.blob), offset, Size);
  334. TEST_ASSERT_EQUAL(2, slice.count());
  335. auto key1 = slice.get("key1");
  336. TEST_ASSERT(static_cast<bool>(key1));
  337. String updated(key1.value);
  338. for (size_t index = 0; index < key1.value.length(); ++index) {
  339. updated[index] = 'A';
  340. }
  341. TEST_ASSERT(slice.set("key1", updated));
  342. TEST_ASSERT(slice.set("key2", updated));
  343. TEST_ASSERT_EQUAL(2, slice.count());
  344. auto check_key1 = slice.get("key1");
  345. TEST_ASSERT(static_cast<bool>(check_key1));
  346. TEST_ASSERT_EQUAL_STRING(updated.c_str(), check_key1.value.c_str());
  347. auto check_key2 = slice.get("key2");
  348. TEST_ASSERT(static_cast<bool>(check_key2));
  349. TEST_ASSERT_EQUAL_STRING(updated.c_str(), check_key2.value.c_str());
  350. TEST_ASSERT_EQUAL(available - offset, slice.available());
  351. }
  352. }
  353. void test_keys_iterator() {
  354. constexpr size_t Size = 32;
  355. StorageHandler<Size> instance;
  356. TEST_ASSERT_EQUAL(Size, instance.kvs.available());
  357. TEST_ASSERT_EQUAL(Size, instance.kvs.size());
  358. TEST_ASSERT(instance.kvs.set("key", "value"));
  359. TEST_ASSERT(instance.kvs.set("another", "thing"));
  360. // ensure we get the same order of keys when iterating via foreach
  361. std::vector<String> keys;
  362. instance.kvs.foreach([&keys](decltype(instance)::kvs_type::KeyValueResult&& kv) {
  363. keys.push_back(kv.key.read());
  364. });
  365. TEST_ASSERT(instance.kvs.keys() == keys);
  366. TEST_ASSERT_EQUAL(2, keys.size());
  367. TEST_ASSERT_EQUAL(2, instance.kvs.count());
  368. TEST_ASSERT_EQUAL_STRING("key", keys[0].c_str());
  369. TEST_ASSERT_EQUAL_STRING("another", keys[1].c_str());
  370. }
  371. int main(int argc, char** argv) {
  372. UNITY_BEGIN();
  373. RUN_TEST(test_storage);
  374. RUN_TEST(test_keys_iterator);
  375. RUN_TEST(test_basic);
  376. RUN_TEST(test_remove_randomized);
  377. RUN_TEST(test_small_gaps);
  378. RUN_TEST(test_overflow);
  379. RUN_TEST(test_perseverance);
  380. RUN_TEST(test_longkey);
  381. RUN_TEST(test_sizes);
  382. return UNITY_END();
  383. }