|
|
@@ -1,24 +1,30 @@
|
|
|
-// v2.0 storage engine — unit tests for LmdbEnv (Task 1.2).
|
|
|
-//
|
|
|
-// Task 1.3 will extend this file with WriteTxn / ReadTxn / LmdbDbi coverage.
|
|
|
+// v2.0 storage engine — unit tests for LmdbEnv (Task 1.2) plus WriteTxn /
|
|
|
+// ReadTxn / LmdbDbi primitives (Task 1.3).
|
|
|
|
|
|
#include <atomic>
|
|
|
#include <cassert>
|
|
|
#include <cstdlib>
|
|
|
#include <filesystem>
|
|
|
#include <iostream>
|
|
|
+#include <optional>
|
|
|
#include <stdexcept>
|
|
|
#include <string>
|
|
|
+#include <string_view>
|
|
|
#include <unistd.h>
|
|
|
|
|
|
#include <lmdb.h>
|
|
|
|
|
|
+#include "storage/lmdb_dbi.hpp"
|
|
|
#include "storage/lmdb_env.hpp"
|
|
|
+#include "storage/lmdb_txn.hpp"
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
+using smartbotic::db::storage::LmdbDbi;
|
|
|
using smartbotic::db::storage::LmdbEnv;
|
|
|
using smartbotic::db::storage::LmdbEnvOpts;
|
|
|
+using smartbotic::db::storage::ReadTxn;
|
|
|
+using smartbotic::db::storage::WriteTxn;
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
@@ -249,15 +255,327 @@ void test_move_constructor_transfers_ownership() {
|
|
|
check(b.path() == td.path, "moved-to env retains path");
|
|
|
}
|
|
|
|
|
|
+// =========================================================================
|
|
|
+// Task 1.3 — WriteTxn / ReadTxn / LmdbDbi tests
|
|
|
+// =========================================================================
|
|
|
+
|
|
|
+void test_write_txn_commit_roundtrip() {
|
|
|
+ TmpDir td("commit-roundtrip");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "alpha", "one");
|
|
|
+ dbi.put(wtxn, "beta", "two");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi dbi(rtxn, "_test");
|
|
|
+ auto a = dbi.get(rtxn, "alpha");
|
|
|
+ check(a.has_value() && std::string(*a) == "one", "alpha = one");
|
|
|
+ auto b = dbi.get(rtxn, "beta");
|
|
|
+ check(b.has_value() && std::string(*b) == "two", "beta = two");
|
|
|
+ auto c = dbi.get(rtxn, "gamma");
|
|
|
+ check(!c.has_value(), "gamma absent -> nullopt");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_write_txn_abort_discards() {
|
|
|
+ TmpDir td("abort-discards");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ // First commit creates the sub-db so the post-abort read txn can open it.
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "seed", "ok");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "alpha", "one");
|
|
|
+ wtxn.abort();
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi dbi(rtxn, "_test");
|
|
|
+ auto a = dbi.get(rtxn, "alpha");
|
|
|
+ check(!a.has_value(), "aborted put leaves no trace");
|
|
|
+ auto s = dbi.get(rtxn, "seed");
|
|
|
+ check(s.has_value(), "seed survived (sanity check that the sub-db is intact)");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_read_during_write() {
|
|
|
+ TmpDir td("read-during-write");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ // Seed: k=v1.
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "k", "v1");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Open a long-lived read txn on the v1 snapshot.
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi rdbi(rtxn, "_test");
|
|
|
+
|
|
|
+ // Update k -> v2 in a separate, committed write txn.
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi wdbi(wtxn, "_test");
|
|
|
+ wdbi.put(wtxn, "k", "v2");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ // The pre-existing read txn still sees v1 (LMDB MVCC snapshot).
|
|
|
+ auto got = rdbi.get(rtxn, "k");
|
|
|
+ check(got.has_value() && std::string(*got) == "v1",
|
|
|
+ "long-lived read txn sees pre-write snapshot value");
|
|
|
+
|
|
|
+ // A fresh read txn sees v2.
|
|
|
+ {
|
|
|
+ ReadTxn r2(env);
|
|
|
+ LmdbDbi d2(r2, "_test");
|
|
|
+ auto g2 = d2.get(r2, "k");
|
|
|
+ check(g2.has_value() && std::string(*g2) == "v2",
|
|
|
+ "fresh read txn sees post-write value");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_multiple_subdbs_isolated() {
|
|
|
+ TmpDir td("multi-subdb");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi a(wtxn, "_a");
|
|
|
+ LmdbDbi b(wtxn, "_b");
|
|
|
+ a.put(wtxn, "k", "from_a");
|
|
|
+ b.put(wtxn, "k", "from_b");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi a(rtxn, "_a");
|
|
|
+ LmdbDbi b(rtxn, "_b");
|
|
|
+ auto ga = a.get(rtxn, "k");
|
|
|
+ auto gb = b.get(rtxn, "k");
|
|
|
+ check(ga.has_value() && std::string(*ga) == "from_a",
|
|
|
+ "sub-db _a returns _a's value for k");
|
|
|
+ check(gb.has_value() && std::string(*gb) == "from_b",
|
|
|
+ "sub-db _b returns _b's value for k");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_del_returns_false_on_missing() {
|
|
|
+ TmpDir td("del-missing");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "present", "x");
|
|
|
+ bool d1 = dbi.del(wtxn, "absent");
|
|
|
+ check(!d1, "del on missing key returns false (not an error)");
|
|
|
+ bool d2 = dbi.del(wtxn, "present");
|
|
|
+ check(d2, "del on present key returns true");
|
|
|
+ bool d3 = dbi.del(wtxn, "present");
|
|
|
+ check(!d3, "second del on now-absent key returns false");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_count_matches_puts() {
|
|
|
+ TmpDir td("count");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ const int N = 100;
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ for (int i = 0; i < N; ++i) {
|
|
|
+ std::string k = "k" + std::to_string(i);
|
|
|
+ dbi.put(wtxn, k, "v");
|
|
|
+ }
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi dbi(rtxn, "_test");
|
|
|
+ check(dbi.count(rtxn) == static_cast<uint64_t>(N),
|
|
|
+ "count() matches puts");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_write_txn_throws_if_committed_twice() {
|
|
|
+ TmpDir td("double-commit");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "k", "v");
|
|
|
+ wtxn.commit();
|
|
|
+
|
|
|
+ bool threw = false;
|
|
|
+ try {
|
|
|
+ wtxn.commit();
|
|
|
+ } catch (const std::runtime_error&) {
|
|
|
+ threw = true;
|
|
|
+ }
|
|
|
+ check(threw, "second commit() throws");
|
|
|
+}
|
|
|
+
|
|
|
+void test_write_txn_destructor_aborts_if_not_finalised() {
|
|
|
+ TmpDir td("dtor-abort");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ // Seed an empty sub-db so the post-scope read txn can open it.
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "seed", "s");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Drop a write txn without committing — destructor must abort, not commit.
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "alpha", "one");
|
|
|
+ // No commit; let the dtor run.
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi dbi(rtxn, "_test");
|
|
|
+ auto a = dbi.get(rtxn, "alpha");
|
|
|
+ check(!a.has_value(), "destructor of uncommitted WriteTxn discards writes");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_commit_after_abort_throws() {
|
|
|
+ TmpDir td("commit-after-abort");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "k", "v");
|
|
|
+ wtxn.abort();
|
|
|
+
|
|
|
+ bool threw = false;
|
|
|
+ try {
|
|
|
+ wtxn.commit();
|
|
|
+ } catch (const std::runtime_error&) {
|
|
|
+ threw = true;
|
|
|
+ }
|
|
|
+ check(threw, "commit() after abort() throws");
|
|
|
+}
|
|
|
+
|
|
|
+void test_write_txn_move_construct() {
|
|
|
+ TmpDir td("write-move");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ auto make_txn = [&]() {
|
|
|
+ WriteTxn t(env);
|
|
|
+ return t; // forces move-construction
|
|
|
+ };
|
|
|
+
|
|
|
+ {
|
|
|
+ WriteTxn moved = make_txn();
|
|
|
+ LmdbDbi dbi(moved, "_test");
|
|
|
+ dbi.put(moved, "k", "from-moved");
|
|
|
+ moved.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi dbi(rtxn, "_test");
|
|
|
+ auto g = dbi.get(rtxn, "k");
|
|
|
+ check(g.has_value() && std::string(*g) == "from-moved",
|
|
|
+ "moved WriteTxn commits successfully");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void test_get_returns_view_into_mmap() {
|
|
|
+ // The contract says get() returns a string_view that lives for the
|
|
|
+ // duration of the txn. Exercise that lifetime: read the value, use
|
|
|
+ // it while the txn is alive, drop the txn.
|
|
|
+ TmpDir td("mmap-view");
|
|
|
+ LmdbEnvOpts opts;
|
|
|
+ opts.path = td.path;
|
|
|
+ LmdbEnv env(opts);
|
|
|
+
|
|
|
+ {
|
|
|
+ WriteTxn wtxn(env);
|
|
|
+ LmdbDbi dbi(wtxn, "_test");
|
|
|
+ dbi.put(wtxn, "k", "hello world");
|
|
|
+ wtxn.commit();
|
|
|
+ }
|
|
|
+
|
|
|
+ ReadTxn rtxn(env);
|
|
|
+ LmdbDbi dbi(rtxn, "_test");
|
|
|
+ auto v = dbi.get(rtxn, "k");
|
|
|
+ check(v.has_value(), "get returns value");
|
|
|
+ check(v->size() == 11, "string_view has expected length");
|
|
|
+ check(std::string(*v) == "hello world", "string_view content matches");
|
|
|
+ // rtxn drops at end of scope; we don't touch v after that.
|
|
|
+}
|
|
|
+
|
|
|
} // namespace
|
|
|
|
|
|
int main() {
|
|
|
+ // Task 1.2 — env.
|
|
|
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();
|
|
|
+
|
|
|
+ // Task 1.3 — txn + dbi.
|
|
|
+ test_write_txn_commit_roundtrip();
|
|
|
+ test_write_txn_abort_discards();
|
|
|
+ test_read_during_write();
|
|
|
+ test_multiple_subdbs_isolated();
|
|
|
+ test_del_returns_false_on_missing();
|
|
|
+ test_count_matches_puts();
|
|
|
+ test_write_txn_throws_if_committed_twice();
|
|
|
+ test_write_txn_destructor_aborts_if_not_finalised();
|
|
|
+ test_commit_after_abort_throws();
|
|
|
+ test_write_txn_move_construct();
|
|
|
+ test_get_returns_view_into_mmap();
|
|
|
+
|
|
|
std::cout << "test_lmdb_env: all passed\n";
|
|
|
return 0;
|
|
|
}
|