| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- // 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;
- }
|