|
|
@@ -0,0 +1,263 @@
|
|
|
+// v2.0 storage engine — unit tests for LmdbEnv (Task 1.2).
|
|
|
+//
|
|
|
+// Task 1.3 will extend this file with WriteTxn / ReadTxn / LmdbDbi coverage.
|
|
|
+
|
|
|
+#include <atomic>
|
|
|
+#include <cassert>
|
|
|
+#include <cstdlib>
|
|
|
+#include <filesystem>
|
|
|
+#include <iostream>
|
|
|
+#include <stdexcept>
|
|
|
+#include <string>
|
|
|
+#include <unistd.h>
|
|
|
+
|
|
|
+#include <lmdb.h>
|
|
|
+
|
|
|
+#include "storage/lmdb_env.hpp"
|
|
|
+
|
|
|
+namespace fs = std::filesystem;
|
|
|
+
|
|
|
+using smartbotic::db::storage::LmdbEnv;
|
|
|
+using smartbotic::db::storage::LmdbEnvOpts;
|
|
|
+
|
|
|
+namespace {
|
|
|
+
|
|
|
+void check(bool cond, const char* msg) {
|
|
|
+ if (!cond) {
|
|
|
+ std::cerr << "FAIL: " << msg << "\n";
|
|
|
+ std::abort();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Per-process, unique tmpdir for test isolation.
|
|
|
+std::string make_tmpdir(const char* tag) {
|
|
|
+ static std::atomic<int> counter{0};
|
|
|
+ int n = counter.fetch_add(1);
|
|
|
+ std::string path = "/tmp/lmdb-env-test-" + std::to_string(::getpid()) +
|
|
|
+ "-" + std::to_string(n) + "-" + tag;
|
|
|
+ std::error_code ec;
|
|
|
+ fs::remove_all(path, ec);
|
|
|
+ return path;
|
|
|
+}
|
|
|
+
|
|
|
+struct TmpDir {
|
|
|
+ std::string path;
|
|
|
+ explicit TmpDir(const char* tag) : path(make_tmpdir(tag)) {}
|
|
|
+ ~TmpDir() {
|
|
|
+ std::error_code ec;
|
|
|
+ fs::remove_all(path, ec);
|
|
|
+ }
|
|
|
+ TmpDir(const TmpDir&) = delete;
|
|
|
+ TmpDir& operator=(const TmpDir&) = delete;
|
|
|
+};
|
|
|
+
|
|
|
+// Helper: populate an LMDB env via raw mdb_* calls. Used by the env-only
|
|
|
+// tests below; once Task 1.3 adds WriteTxn / LmdbDbi, the txn-level tests
|
|
|
+// will use those wrappers instead.
|
|
|
+void raw_populate(const std::string& path, size_t map_size,
|
|
|
+ int n_entries, size_t entry_size) {
|
|
|
+ std::error_code ec;
|
|
|
+ fs::create_directories(path, ec);
|
|
|
+ if (ec) {
|
|
|
+ throw std::runtime_error(
|
|
|
+ "raw_populate: create_directories: " + ec.message());
|
|
|
+ }
|
|
|
+ MDB_env* env = nullptr;
|
|
|
+ if (mdb_env_create(&env) != MDB_SUCCESS) {
|
|
|
+ throw std::runtime_error("raw_populate: env_create");
|
|
|
+ }
|
|
|
+ if (mdb_env_set_mapsize(env, map_size) != MDB_SUCCESS) {
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_populate: set_mapsize");
|
|
|
+ }
|
|
|
+ if (mdb_env_open(env, path.c_str(), 0, 0644) != MDB_SUCCESS) {
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_populate: env_open");
|
|
|
+ }
|
|
|
+ MDB_txn* txn = nullptr;
|
|
|
+ if (mdb_txn_begin(env, nullptr, 0, &txn) != MDB_SUCCESS) {
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_populate: txn_begin");
|
|
|
+ }
|
|
|
+ MDB_dbi dbi = 0;
|
|
|
+ if (mdb_dbi_open(txn, nullptr, MDB_CREATE, &dbi) != MDB_SUCCESS) {
|
|
|
+ mdb_txn_abort(txn);
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_populate: dbi_open");
|
|
|
+ }
|
|
|
+ std::string blob(entry_size, 'x');
|
|
|
+ for (int i = 0; i < n_entries; ++i) {
|
|
|
+ std::string k = "k" + std::to_string(i);
|
|
|
+ MDB_val kv{k.size(), const_cast<char*>(k.data())};
|
|
|
+ MDB_val vv{blob.size(), const_cast<char*>(blob.data())};
|
|
|
+ int rc = mdb_put(txn, dbi, &kv, &vv, 0);
|
|
|
+ if (rc != MDB_SUCCESS) {
|
|
|
+ mdb_txn_abort(txn);
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_populate: put");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (mdb_txn_commit(txn) != MDB_SUCCESS) {
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_populate: txn_commit");
|
|
|
+ }
|
|
|
+ mdb_env_close(env);
|
|
|
+}
|
|
|
+
|
|
|
+void raw_get(const std::string& path, const std::string& key,
|
|
|
+ std::string& out_value, bool& found) {
|
|
|
+ found = false;
|
|
|
+ MDB_env* env = nullptr;
|
|
|
+ if (mdb_env_create(&env) != MDB_SUCCESS) {
|
|
|
+ throw std::runtime_error("raw_get: env_create");
|
|
|
+ }
|
|
|
+ if (mdb_env_set_mapsize(env, 4ULL << 20) != MDB_SUCCESS) {
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_get: set_mapsize");
|
|
|
+ }
|
|
|
+ if (mdb_env_open(env, path.c_str(), 0, 0644) != MDB_SUCCESS) {
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_get: env_open");
|
|
|
+ }
|
|
|
+ MDB_txn* txn = nullptr;
|
|
|
+ if (mdb_txn_begin(env, nullptr, MDB_RDONLY, &txn) != MDB_SUCCESS) {
|
|
|
+ mdb_env_close(env);
|
|
|
+ throw std::runtime_error("raw_get: txn_begin");
|
|
|
+ }
|
|
|
+ MDB_dbi dbi = 0;
|
|
|
+ int rc = mdb_dbi_open(txn, nullptr, 0, &dbi);
|
|
|
+ if (rc != MDB_SUCCESS) {
|
|
|
+ mdb_txn_abort(txn);
|
|
|
+ mdb_env_close(env);
|
|
|
+ if (rc == MDB_NOTFOUND) return;
|
|
|
+ throw std::runtime_error("raw_get: dbi_open");
|
|
|
+ }
|
|
|
+ MDB_val kv{key.size(), const_cast<char*>(key.data())};
|
|
|
+ MDB_val vv{0, nullptr};
|
|
|
+ rc = mdb_get(txn, dbi, &kv, &vv);
|
|
|
+ if (rc == MDB_SUCCESS) {
|
|
|
+ out_value.assign(static_cast<const char*>(vv.mv_data), vv.mv_size);
|
|
|
+ found = true;
|
|
|
+ }
|
|
|
+ mdb_txn_abort(txn);
|
|
|
+ mdb_env_close(env);
|
|
|
+}
|
|
|
+
|
|
|
+// =========================================================================
|
|
|
+// Task 1.2 — LmdbEnv tests
|
|
|
+// =========================================================================
|
|
|
+
|
|
|
+void test_open_creates_dir() {
|
|
|
+ TmpDir td("create-dir");
|
|
|
+ check(!fs::exists(td.path), "precondition: tmpdir absent");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+ check(fs::exists(td.path) && fs::is_directory(td.path),
|
|
|
+ "LmdbEnv constructor created the env directory");
|
|
|
+ // LMDB also created its data.mdb / lock.mdb in there.
|
|
|
+ check(fs::exists(td.path + "/data.mdb"), "data.mdb exists after open");
|
|
|
+ check(fs::exists(td.path + "/lock.mdb"), "lock.mdb exists after open");
|
|
|
+}
|
|
|
+
|
|
|
+void test_path_accessor() {
|
|
|
+ TmpDir td("path-accessor");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+ check(env.path() == td.path, "env.path() returns configured path");
|
|
|
+ check(env.raw() != nullptr, "env.raw() returns non-null MDB_env*");
|
|
|
+}
|
|
|
+
|
|
|
+void test_mapsize_too_small_fails() {
|
|
|
+ TmpDir td("mapsize-small");
|
|
|
+
|
|
|
+ // Populate via raw LMDB so the data.mdb file grows past the small mapsize
|
|
|
+ // we'll try to reopen with.
|
|
|
+ raw_populate(td.path, 4ULL << 20 /* 4 MiB */, 16, 64 * 1024 /* 64 KiB */);
|
|
|
+
|
|
|
+ // Sanity: the populated dir reports nonzero on-disk bytes.
|
|
|
+ uint64_t bytes = 0;
|
|
|
+ {
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ opts.map_size_bytes = 4ULL << 20;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+ bytes = env.on_disk_bytes();
|
|
|
+ }
|
|
|
+ check(bytes > 0, "populated env has nonzero on_disk_bytes");
|
|
|
+
|
|
|
+ // Re-opening with a smaller mapsize than the existing data file must
|
|
|
+ // fail fast — LMDB rejects mapsize < size-of-data-file.
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ opts.map_size_bytes = 4096; // 4 KiB — far below data.mdb size.
|
|
|
+ bool threw = false;
|
|
|
+ try {
|
|
|
+ LmdbEnv env(opts);
|
|
|
+ } catch (const std::runtime_error&) {
|
|
|
+ threw = true;
|
|
|
+ }
|
|
|
+ check(threw, "opening with too-small mapsize fails fast");
|
|
|
+}
|
|
|
+
|
|
|
+void test_double_open_same_path_separate_envs() {
|
|
|
+ TmpDir td("double-open");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+
|
|
|
+ // Seed via raw LMDB so we have data to read through the second handle.
|
|
|
+ raw_populate(td.path, 4ULL << 20, 4, 64);
|
|
|
+
|
|
|
+ // LMDB supports multi-process access; two envs on the same path should
|
|
|
+ // both be openable from the same process too.
|
|
|
+ LmdbEnv a(opts);
|
|
|
+ LmdbEnv b(opts);
|
|
|
+
|
|
|
+ check(a.raw() != nullptr, "first env handle non-null");
|
|
|
+ check(b.raw() != nullptr, "second env handle non-null");
|
|
|
+ check(a.raw() != b.raw(), "two LmdbEnv handles are distinct objects");
|
|
|
+
|
|
|
+ // Sanity: both envs see the seeded data via raw_get (which opens a third
|
|
|
+ // handle internally — proves multi-handle reads work).
|
|
|
+ std::string v;
|
|
|
+ bool found = false;
|
|
|
+ raw_get(td.path, "k0", v, found);
|
|
|
+ check(found, "raw_get sees seeded key while two LmdbEnv handles are open");
|
|
|
+}
|
|
|
+
|
|
|
+void test_on_disk_bytes_nonzero_after_create() {
|
|
|
+ TmpDir td("on-disk-bytes");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+ check(env.on_disk_bytes() > 0,
|
|
|
+ "fresh env reports nonzero on_disk_bytes (data.mdb/lock.mdb initialised)");
|
|
|
+}
|
|
|
+
|
|
|
+void test_move_constructor_transfers_ownership() {
|
|
|
+ TmpDir td("move-ctor");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv a(opts);
|
|
|
+ MDB_env* raw_a = a.raw();
|
|
|
+ check(raw_a != nullptr, "source env has non-null raw before move");
|
|
|
+
|
|
|
+ LmdbEnv b(std::move(a));
|
|
|
+ check(b.raw() == raw_a, "moved-to env owns the original handle");
|
|
|
+ check(a.raw() == nullptr, "moved-from env has null handle");
|
|
|
+ check(b.path() == td.path, "moved-to env retains path");
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace
|
|
|
+
|
|
|
+int main() {
|
|
|
+ test_open_creates_dir();
|
|
|
+ test_path_accessor();
|
|
|
+ test_mapsize_too_small_fails();
|
|
|
+ test_double_open_same_path_separate_envs();
|
|
|
+ test_on_disk_bytes_nonzero_after_create();
|
|
|
+ test_move_constructor_transfers_ownership();
|
|
|
+ std::cout << "test_lmdb_env: all passed\n";
|
|
|
+ return 0;
|
|
|
+}
|