// v2.0 storage engine — unit tests for LmdbEnv (Task 1.2). // // Task 1.3 will extend this file with WriteTxn / ReadTxn / LmdbDbi coverage. #include #include #include #include #include #include #include #include #include #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 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(k.data())}; MDB_val vv{blob.size(), const_cast(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(key.data())}; MDB_val vv{0, nullptr}; rc = mdb_get(txn, dbi, &kv, &vv); if (rc == MDB_SUCCESS) { out_value.assign(static_cast(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; }