pw_kvs: Use key's current state in RelocateEntry
When moving tombstone entries for deleted keys, RelocateEntry would
cause them to become present again.
Expand key_value_store_map_test.cc and add a test that covers this case
specifically.
Change-Id: I846cb155bffb25198377e04752cace971555d32b
diff --git a/pw_kvs/key_value_store_map_test.cc b/pw_kvs/key_value_store_map_test.cc
index 7c3dda5..c976db2 100644
--- a/pw_kvs/key_value_store_map_test.cc
+++ b/pw_kvs/key_value_store_map_test.cc
@@ -12,10 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <cstdlib>
#include <random>
+#include <set>
#include <string>
#include <string_view>
#include <unordered_map>
+#include <unordered_set>
#define DUMP_KVS_CONTENTS 0
@@ -27,6 +30,7 @@
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/in_memory_fake_flash.h"
#include "pw_kvs/key_value_store.h"
+#include "pw_log/log.h"
#include "pw_span/span.h"
namespace pw::kvs {
@@ -48,6 +52,18 @@
size_t partition_alignment;
};
+template <typename T>
+std::set<T> difference(const std::set<T> lhs, const std::set<T> rhs) {
+ std::set<T> diff;
+ std::set_difference(lhs.begin(),
+ lhs.end(),
+ rhs.begin(),
+ rhs.end(),
+ std::inserter(diff, diff.begin()));
+
+ return diff;
+}
+
template <const TestParameters& kParams>
class KvsTester {
public:
@@ -69,43 +85,9 @@
}
}
- ~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
+ ~KvsTester() { CompareContents(); }
- 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) {
+ void Test_RandomValidInputs(int iterations) {
std::mt19937 random(6006411);
std::uniform_int_distribution<unsigned> distro;
auto random_int = [&] { return distro(random); };
@@ -142,7 +124,7 @@
}
}
- void TestPut() {
+ void Test_Put() {
Put("base_key", "base_value");
for (int i = 0; i < 100; ++i) {
Put("other_key", std::to_string(i));
@@ -152,10 +134,86 @@
}
}
+ void Test_PutAndDelete_RelocateDeletedEntriesShouldStayDeleted() {
+ for (int i = 0; i < 100; ++i) {
+ std::string str = "key_" + std::to_string(i);
+ Put(str, std::string(kMaxValueLength, '?'));
+ Delete(str);
+ }
+ }
+
private:
+ void CompareContents() {
+#if DUMP_KVS_CONTENTS
+ std::set<std::string> map_keys, kvs_keys;
+
+ 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';
+ map_keys.insert(key);
+ }
+ std::cout << "\\===============================================/\n";
+
+ std::cout << "/==============================================\\\n";
+ std::cout << "KVS ACTUAL CONTENTS\n";
+ std::cout << "------------------------------------------------\n";
+ std::cout << "Entries: " << kvs_.size() << '\n';
+ std::cout << "------------------------------------------------\n";
+ for (const auto& item : kvs_) {
+ std::cout << item.key() << " = " << item.ValueSize().size() << " B\n";
+ kvs_keys.insert(std::string(item.key()));
+ }
+ std::cout << "\\===============================================/\n";
+
+ auto missing_from_kvs = difference(map_keys, kvs_keys);
+
+ if (!missing_from_kvs.empty()) {
+ std::cout << "MISSING FROM KVS: " << missing_from_kvs.size() << '\n';
+ for (auto& key : missing_from_kvs) {
+ std::cout << key << '\n';
+ }
+ }
+
+ auto missing_from_map = difference(kvs_keys, map_keys);
+ if (!missing_from_map.empty()) {
+ std::cout << "MISSING FROM MAP: " << missing_from_map.size() << '\n';
+ for (auto& key : missing_from_map) {
+ std::cout << key << '\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()));
+ if (map_entry == map_.end()) {
+ PW_LOG_CRITICAL("Entry %s missing from map", item.key().data());
+ } else 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());
+ }
+
// 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);
+ StartOperation("Put", key);
+ EXPECT_LE(value.size(), kMaxValueLength);
Status result = kvs_.Put(key, as_bytes(span(value)));
@@ -163,24 +221,66 @@
EXPECT_EQ(Status::INVALID_ARGUMENT, result);
} else if (map_.size() == KeyValueStore::kMaxEntries) {
EXPECT_EQ(Status::RESOURCE_EXHAUSTED, result);
- } else {
- ASSERT_EQ(Status::OK, result);
+ } else if (result == Status::RESOURCE_EXHAUSTED) {
+ EXPECT_FALSE(map_.empty());
+ } else if (result.ok()) {
map_[key] = value;
+ deleted_.erase(key);
+ } else {
+ PW_LOG_CRITICAL("Put: unhandled result %s", result.str());
+ std::abort();
+ }
+
+ FinishOperation("Put", result, key);
+
+ if (kvs_.size() != map_.size()) {
+ PW_LOG_CRITICAL("Put: size mismatch; expected %zu, actual %zu",
+ map_.size(),
+ kvs_.size());
+ std::abort();
}
}
// Deletes a key from the KVS if it is present.
void Delete(const std::string& key) {
+ StartOperation("Delete", 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);
+ } else if (result.ok()) {
map_.erase(key);
+
+ if (deleted_.count(key) > 0u) {
+ PW_LOG_CRITICAL("Deleted key that was already deleted %s", key.c_str());
+ std::abort();
+ }
+
+ deleted_.insert(key);
+ } else {
+ PW_LOG_CRITICAL("Delete: unhandled result %s", result.str());
+ std::abort();
}
+ FinishOperation("Delete", result, key);
+ }
+
+ void StartOperation(const std::string& operation, const std::string& key) {
+ count_ += 1;
+ PW_LOG_CRITICAL(
+ "[%3u] START %s for '%s'", count_, operation.c_str(), key.c_str());
+ }
+
+ void FinishOperation(const std::string& operation,
+ Status result,
+ const std::string& key) {
+ PW_LOG_CRITICAL("[%3u] FINISH %s <%s> for '%s'",
+ count_,
+ operation.c_str(),
+ result.str(),
+ key.c_str());
}
bool empty() const { return map_.empty(); }
@@ -196,6 +296,8 @@
KeyValueStore kvs_;
std::unordered_map<std::string, std::string> map_;
+ std::unordered_set<std::string> deleted_;
+ unsigned count_ = 0;
};
template <const TestParameters& kParams>
@@ -204,6 +306,9 @@
InMemoryFakeFlash<kParams.sector_size, kParams.sector_count>(
kParams.sector_alignment);
+#define _TEST(fixture, test, ...) \
+ TEST_F(fixture, test) { tester_.Test_##test(__VA_ARGS__); }
+
// Defines a test fixture that runs all tests against a flash with the specified
// parameters.
#define RUN_TESTS_WITH_PARAMETERS(name, ...) \
@@ -215,11 +320,9 @@
}; \
\
/* 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); \
- } \
+ _TEST(name, Put); \
+ _TEST(name, PutAndDelete_RelocateDeletedEntriesShouldStayDeleted); \
+ _TEST(name, RandomValidInputs, 1000) \
static_assert(true, "Don't forget a semicolon!")
RUN_TESTS_WITH_PARAMETERS(Basic,