test_lmdb_env.cpp 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. // v2.0 storage engine — unit tests for LmdbEnv (Task 1.2).
  2. //
  3. // Task 1.3 will extend this file with WriteTxn / ReadTxn / LmdbDbi coverage.
  4. #include <atomic>
  5. #include <cassert>
  6. #include <cstdlib>
  7. #include <filesystem>
  8. #include <iostream>
  9. #include <stdexcept>
  10. #include <string>
  11. #include <unistd.h>
  12. #include <lmdb.h>
  13. #include "storage/lmdb_env.hpp"
  14. namespace fs = std::filesystem;
  15. using smartbotic::db::storage::LmdbEnv;
  16. using smartbotic::db::storage::LmdbEnvOpts;
  17. namespace {
  18. void check(bool cond, const char* msg) {
  19. if (!cond) {
  20. std::cerr << "FAIL: " << msg << "\n";
  21. std::abort();
  22. }
  23. }
  24. // Per-process, unique tmpdir for test isolation.
  25. std::string make_tmpdir(const char* tag) {
  26. static std::atomic<int> counter{0};
  27. int n = counter.fetch_add(1);
  28. std::string path = "/tmp/lmdb-env-test-" + std::to_string(::getpid()) +
  29. "-" + std::to_string(n) + "-" + tag;
  30. std::error_code ec;
  31. fs::remove_all(path, ec);
  32. return path;
  33. }
  34. struct TmpDir {
  35. std::string path;
  36. explicit TmpDir(const char* tag) : path(make_tmpdir(tag)) {}
  37. ~TmpDir() {
  38. std::error_code ec;
  39. fs::remove_all(path, ec);
  40. }
  41. TmpDir(const TmpDir&) = delete;
  42. TmpDir& operator=(const TmpDir&) = delete;
  43. };
  44. // Helper: populate an LMDB env via raw mdb_* calls. Used by the env-only
  45. // tests below; once Task 1.3 adds WriteTxn / LmdbDbi, the txn-level tests
  46. // will use those wrappers instead.
  47. void raw_populate(const std::string& path, size_t map_size,
  48. int n_entries, size_t entry_size) {
  49. std::error_code ec;
  50. fs::create_directories(path, ec);
  51. if (ec) {
  52. throw std::runtime_error(
  53. "raw_populate: create_directories: " + ec.message());
  54. }
  55. MDB_env* env = nullptr;
  56. if (mdb_env_create(&env) != MDB_SUCCESS) {
  57. throw std::runtime_error("raw_populate: env_create");
  58. }
  59. if (mdb_env_set_mapsize(env, map_size) != MDB_SUCCESS) {
  60. mdb_env_close(env);
  61. throw std::runtime_error("raw_populate: set_mapsize");
  62. }
  63. if (mdb_env_open(env, path.c_str(), 0, 0644) != MDB_SUCCESS) {
  64. mdb_env_close(env);
  65. throw std::runtime_error("raw_populate: env_open");
  66. }
  67. MDB_txn* txn = nullptr;
  68. if (mdb_txn_begin(env, nullptr, 0, &txn) != MDB_SUCCESS) {
  69. mdb_env_close(env);
  70. throw std::runtime_error("raw_populate: txn_begin");
  71. }
  72. MDB_dbi dbi = 0;
  73. if (mdb_dbi_open(txn, nullptr, MDB_CREATE, &dbi) != MDB_SUCCESS) {
  74. mdb_txn_abort(txn);
  75. mdb_env_close(env);
  76. throw std::runtime_error("raw_populate: dbi_open");
  77. }
  78. std::string blob(entry_size, 'x');
  79. for (int i = 0; i < n_entries; ++i) {
  80. std::string k = "k" + std::to_string(i);
  81. MDB_val kv{k.size(), const_cast<char*>(k.data())};
  82. MDB_val vv{blob.size(), const_cast<char*>(blob.data())};
  83. int rc = mdb_put(txn, dbi, &kv, &vv, 0);
  84. if (rc != MDB_SUCCESS) {
  85. mdb_txn_abort(txn);
  86. mdb_env_close(env);
  87. throw std::runtime_error("raw_populate: put");
  88. }
  89. }
  90. if (mdb_txn_commit(txn) != MDB_SUCCESS) {
  91. mdb_env_close(env);
  92. throw std::runtime_error("raw_populate: txn_commit");
  93. }
  94. mdb_env_close(env);
  95. }
  96. void raw_get(const std::string& path, const std::string& key,
  97. std::string& out_value, bool& found) {
  98. found = false;
  99. MDB_env* env = nullptr;
  100. if (mdb_env_create(&env) != MDB_SUCCESS) {
  101. throw std::runtime_error("raw_get: env_create");
  102. }
  103. if (mdb_env_set_mapsize(env, 4ULL << 20) != MDB_SUCCESS) {
  104. mdb_env_close(env);
  105. throw std::runtime_error("raw_get: set_mapsize");
  106. }
  107. if (mdb_env_open(env, path.c_str(), 0, 0644) != MDB_SUCCESS) {
  108. mdb_env_close(env);
  109. throw std::runtime_error("raw_get: env_open");
  110. }
  111. MDB_txn* txn = nullptr;
  112. if (mdb_txn_begin(env, nullptr, MDB_RDONLY, &txn) != MDB_SUCCESS) {
  113. mdb_env_close(env);
  114. throw std::runtime_error("raw_get: txn_begin");
  115. }
  116. MDB_dbi dbi = 0;
  117. int rc = mdb_dbi_open(txn, nullptr, 0, &dbi);
  118. if (rc != MDB_SUCCESS) {
  119. mdb_txn_abort(txn);
  120. mdb_env_close(env);
  121. if (rc == MDB_NOTFOUND) return;
  122. throw std::runtime_error("raw_get: dbi_open");
  123. }
  124. MDB_val kv{key.size(), const_cast<char*>(key.data())};
  125. MDB_val vv{0, nullptr};
  126. rc = mdb_get(txn, dbi, &kv, &vv);
  127. if (rc == MDB_SUCCESS) {
  128. out_value.assign(static_cast<const char*>(vv.mv_data), vv.mv_size);
  129. found = true;
  130. }
  131. mdb_txn_abort(txn);
  132. mdb_env_close(env);
  133. }
  134. // =========================================================================
  135. // Task 1.2 — LmdbEnv tests
  136. // =========================================================================
  137. void test_open_creates_dir() {
  138. TmpDir td("create-dir");
  139. check(!fs::exists(td.path), "precondition: tmpdir absent");
  140. LmdbEnvOpts opts;
  141. opts.path = td.path;
  142. LmdbEnv env(opts);
  143. check(fs::exists(td.path) && fs::is_directory(td.path),
  144. "LmdbEnv constructor created the env directory");
  145. // LMDB also created its data.mdb / lock.mdb in there.
  146. check(fs::exists(td.path + "/data.mdb"), "data.mdb exists after open");
  147. check(fs::exists(td.path + "/lock.mdb"), "lock.mdb exists after open");
  148. }
  149. void test_path_accessor() {
  150. TmpDir td("path-accessor");
  151. LmdbEnvOpts opts;
  152. opts.path = td.path;
  153. LmdbEnv env(opts);
  154. check(env.path() == td.path, "env.path() returns configured path");
  155. check(env.raw() != nullptr, "env.raw() returns non-null MDB_env*");
  156. }
  157. void test_mapsize_too_small_fails() {
  158. TmpDir td("mapsize-small");
  159. // Populate via raw LMDB so the data.mdb file grows past the small mapsize
  160. // we'll try to reopen with.
  161. raw_populate(td.path, 4ULL << 20 /* 4 MiB */, 16, 64 * 1024 /* 64 KiB */);
  162. // Sanity: the populated dir reports nonzero on-disk bytes.
  163. uint64_t bytes = 0;
  164. {
  165. LmdbEnvOpts opts;
  166. opts.path = td.path;
  167. opts.map_size_bytes = 4ULL << 20;
  168. LmdbEnv env(opts);
  169. bytes = env.on_disk_bytes();
  170. }
  171. check(bytes > 0, "populated env has nonzero on_disk_bytes");
  172. // Re-opening with a smaller mapsize than the existing data file must
  173. // fail fast — LMDB rejects mapsize < size-of-data-file.
  174. LmdbEnvOpts opts;
  175. opts.path = td.path;
  176. opts.map_size_bytes = 4096; // 4 KiB — far below data.mdb size.
  177. bool threw = false;
  178. try {
  179. LmdbEnv env(opts);
  180. } catch (const std::runtime_error&) {
  181. threw = true;
  182. }
  183. check(threw, "opening with too-small mapsize fails fast");
  184. }
  185. void test_double_open_same_path_separate_envs() {
  186. TmpDir td("double-open");
  187. LmdbEnvOpts opts;
  188. opts.path = td.path;
  189. // Seed via raw LMDB so we have data to read through the second handle.
  190. raw_populate(td.path, 4ULL << 20, 4, 64);
  191. // LMDB supports multi-process access; two envs on the same path should
  192. // both be openable from the same process too.
  193. LmdbEnv a(opts);
  194. LmdbEnv b(opts);
  195. check(a.raw() != nullptr, "first env handle non-null");
  196. check(b.raw() != nullptr, "second env handle non-null");
  197. check(a.raw() != b.raw(), "two LmdbEnv handles are distinct objects");
  198. // Sanity: both envs see the seeded data via raw_get (which opens a third
  199. // handle internally — proves multi-handle reads work).
  200. std::string v;
  201. bool found = false;
  202. raw_get(td.path, "k0", v, found);
  203. check(found, "raw_get sees seeded key while two LmdbEnv handles are open");
  204. }
  205. void test_on_disk_bytes_nonzero_after_create() {
  206. TmpDir td("on-disk-bytes");
  207. LmdbEnvOpts opts;
  208. opts.path = td.path;
  209. LmdbEnv env(opts);
  210. check(env.on_disk_bytes() > 0,
  211. "fresh env reports nonzero on_disk_bytes (data.mdb/lock.mdb initialised)");
  212. }
  213. void test_move_constructor_transfers_ownership() {
  214. TmpDir td("move-ctor");
  215. LmdbEnvOpts opts;
  216. opts.path = td.path;
  217. LmdbEnv a(opts);
  218. MDB_env* raw_a = a.raw();
  219. check(raw_a != nullptr, "source env has non-null raw before move");
  220. LmdbEnv b(std::move(a));
  221. check(b.raw() == raw_a, "moved-to env owns the original handle");
  222. check(a.raw() == nullptr, "moved-from env has null handle");
  223. check(b.path() == td.path, "moved-to env retains path");
  224. }
  225. } // namespace
  226. int main() {
  227. test_open_creates_dir();
  228. test_path_accessor();
  229. test_mapsize_too_small_fails();
  230. test_double_open_same_path_separate_envs();
  231. test_on_disk_bytes_nonzero_after_create();
  232. test_move_constructor_transfers_ownership();
  233. std::cout << "test_lmdb_env: all passed\n";
  234. return 0;
  235. }