aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/C_API.cpp415
-rw-r--r--test/CursorManager.cpp152
-rw-r--r--test/LifecycleManager.cpp169
-rw-r--r--test/MultigroupCodec.cpp130
-rw-r--r--test/NamespaceReferenceManager.cpp177
-rw-r--r--test/ObjectManager.cpp68
-rw-r--r--test/VM.cpp254
7 files changed, 0 insertions, 1365 deletions
diff --git a/test/C_API.cpp b/test/C_API.cpp
deleted file mode 100644
index 892ed24..0000000
--- a/test/C_API.cpp
+++ /dev/null
@@ -1,415 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "RNT_C_API.h"
-
-#include <cstring>
-#include <string>
-
-// ---------------------------------------------------------------------------
-// End-to-end C API tests covering step 2 (Branch hash-pointer), step 3+4
-// (copy-on-write writes and resolver wiring through rnt_open_handle), and
-// step 5 (session lifecycle and overrides).
-//
-// The C API holds a single process-global runtime; rnt_init is idempotent.
-// Each test uses unique branch and session identifiers so state from one
-// test does not affect another.
-// ---------------------------------------------------------------------------
-
-namespace {
- struct InitGuard {
- InitGuard() {
- REQUIRE(rnt_init("memory", nullptr) == 0);
- }
- };
-
- std::string take_string(char* p) {
- std::string s = p ? p : "";
- rnt_free_string(p);
- return s;
- }
-} // namespace
-
-// ---------------------------------------------------------------------------
-// Step 2 — Branch hash-pointer model
-// ---------------------------------------------------------------------------
-
-TEST_CASE("rnt_register_branch with empty target creates an unborn branch",
- "[step2][capi][branch]") {
- InitGuard _;
- const char* path = "/system/branches/test_step2_unborn";
- REQUIRE(rnt_register_branch(path, "") == 0);
-
- rnt_handle_t h = rnt_open_handle(path, nullptr);
- REQUIRE(h != nullptr);
-
- char* target = nullptr;
- REQUIRE(rnt_branch_target(h, &target) == 0);
- REQUIRE(take_string(target) == "");
-
- REQUIRE(rnt_close_handle(h) == 0);
-}
-
-TEST_CASE("rnt_register_branch rejects an unregistered target hash", "[step2][capi][branch]") {
- InitGuard _;
- REQUIRE(rnt_register_branch(
- "/system/branches/test_step2_bad_init",
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
-}
-
-TEST_CASE("rnt_branch_advance rejects an unregistered target hash", "[step2][capi][branch]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step2_bad_advance";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
-
- REQUIRE(rnt_branch_advance(
- bpath, "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
-
- // Target unchanged.
- rnt_handle_t h = rnt_open_handle(bpath, nullptr);
- REQUIRE(h != nullptr);
- char* target = nullptr;
- REQUIRE(rnt_branch_target(h, &target) == 0);
- REQUIRE(take_string(target) == "");
- rnt_close_handle(h);
-}
-
-TEST_CASE("Writing a tuple advances the branch to a new snapshot", "[step2][step3][capi][branch]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step2_advance";
- const char* rpath = "/system/branches/test_step2_advance/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- // After register_relation, the branch is bound to the first snapshot.
- rnt_handle_t h = rnt_open_handle(bpath, nullptr);
- REQUIRE(h != nullptr);
- char* first = nullptr;
- REQUIRE(rnt_branch_target(h, &first) == 0);
- const std::string first_hash = take_string(first);
- REQUIRE(first_hash.size() == 64);
- rnt_close_handle(h);
-
- // A tuple write advances the branch again.
- char* tuple_hash = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &tuple_hash) == 0);
- rnt_free_string(tuple_hash);
-
- h = rnt_open_handle(bpath, nullptr);
- REQUIRE(h != nullptr);
- char* second = nullptr;
- REQUIRE(rnt_branch_target(h, &second) == 0);
- const std::string second_hash = take_string(second);
- rnt_close_handle(h);
-
- REQUIRE(second_hash.size() == 64);
- REQUIRE(second_hash != first_hash);
-}
-
-// ---------------------------------------------------------------------------
-// Step 3+4 — Copy-on-write writes and resolver wiring
-// ---------------------------------------------------------------------------
-
-TEST_CASE("link_tuple commits a new snapshot and relation_root reflects it", "[step3][capi][cow]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step3_link";
- const char* rpath = "/system/branches/test_step3_link/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- // Empty relation: root is "".
- char* root = nullptr;
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) == "");
-
- char* tuple_hash = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\nname=alpha\n", &tuple_hash) == 0);
- REQUIRE(tuple_hash != nullptr);
- rnt_free_string(tuple_hash);
-
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- const std::string new_root = take_string(root);
- REQUIRE(new_root.size() == 64); // a real Merkle root now
-}
-
-TEST_CASE("unlink_tuple and clear_relation walk back through new snapshots", "[step3][capi][cow]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step3_unlink";
- const char* rpath = "/system/branches/test_step3_unlink/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- char* th = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
- const std::string tuple_hash = take_string(th);
-
- char* root = nullptr;
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) != "");
-
- REQUIRE(rnt_unlink_tuple(rpath, tuple_hash.c_str()) == 0);
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) == "");
-
- // After re-link and clear, root is empty again.
- REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
- rnt_free_string(th);
- REQUIRE(rnt_clear_relation(rpath) == 0);
- REQUIRE(rnt_relation_root(rpath, &root) == 0);
- REQUIRE(take_string(root) == "");
-}
-
-TEST_CASE("rnt_open_handle resolves branch-relative paths to snapshot relations",
- "[step4][capi][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step4_resolve";
- const char* rpath = "/system/branches/test_step4_resolve/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- char* tuple_hash = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &tuple_hash) == 0);
- rnt_free_string(tuple_hash);
-
- // Opening the branch-relative path resolves to the snapshot-bound entry,
- // and a cursor reads the tuple just written.
- rnt_handle_t h = rnt_open_handle(rpath, nullptr);
- REQUIRE(h != nullptr);
- rnt_cursor_t c = rnt_cursor_open(h);
- REQUIRE(c != nullptr);
-
- char* row = nullptr;
- REQUIRE(rnt_cursor_next(c, &row) == 1);
- REQUIRE(row != nullptr);
- rnt_free_string(row);
-
- // Exhausted on the next call.
- REQUIRE(rnt_cursor_next(c, &row) == 0);
- REQUIRE(row == nullptr);
-
- rnt_cursor_close(c);
- rnt_close_handle(h);
-}
-
-TEST_CASE("Reading an unborn branch's relation fails cleanly", "[step4][capi][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step4_unborn";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
-
- // No relation registered: relation_root miss returns nonzero.
- char* root = nullptr;
- REQUIRE(
- rnt_relation_root("/system/branches/test_step4_unborn/multigroups/public/relations/missing",
- &root) != 0);
-}
-
-// ---------------------------------------------------------------------------
-// Step 5 — Session lifecycle and overrides
-// ---------------------------------------------------------------------------
-
-TEST_CASE("Each rnt_session_open returns a distinct 64-char hex hash", "[step5][capi][session]") {
- InitGuard _;
- char* a = nullptr;
- char* b = nullptr;
- REQUIRE(rnt_session_open(nullptr, &a) == 0);
- REQUIRE(rnt_session_open(nullptr, &b) == 0);
- const std::string ha = take_string(a);
- const std::string hb = take_string(b);
- REQUIRE(ha.size() == 64);
- REQUIRE(hb.size() == 64);
- REQUIRE(ha != hb);
-
- REQUIRE(rnt_session_close(ha.c_str()) == 0);
- REQUIRE(rnt_session_close(hb.c_str()) == 0);
-}
-
-TEST_CASE("rnt_session_close on an unknown hash fails", "[step5][capi][session]") {
- InitGuard _;
- REQUIRE(rnt_session_close("0000000000000000000000000000000000000000000000000000000000000000") !=
- 0);
-}
-
-TEST_CASE("Session override redirects branch-relative reads to a different snapshot",
- "[step5][capi][session][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step5_override";
- const char* rpath = "/system/branches/test_step5_override/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- // Snapshot S1: one tuple.
- char* th = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
- rnt_free_string(th);
-
- // Capture S1's hash and pin it via a session override before the next
- // commit — without the override the next link_tuple would cascade-collect
- // S1 as soon as the branch advances away from it.
- rnt_handle_t bh = rnt_open_handle(bpath, nullptr);
- REQUIRE(bh != nullptr);
- char* target = nullptr;
- REQUIRE(rnt_branch_target(bh, &target) == 0);
- const std::string s1 = take_string(target);
- rnt_close_handle(bh);
-
- char* sess = nullptr;
- REQUIRE(rnt_session_open(nullptr, &sess) == 0);
- const std::string sid = take_string(sess);
- REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_override", s1.c_str()) == 0);
-
- // Advance the branch to S2 — global HEAD moves on; the session override
- // keeps S1 resident.
- REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
- rnt_free_string(th);
-
- // Session view: still S1 (one tuple).
- const std::string spath = "/system/sessions/" + sid +
- "/branches/test_step5_override/multigroups/public/relations/items";
-
- rnt_handle_t h_session = rnt_open_handle(spath.c_str(), nullptr);
- REQUIRE(h_session != nullptr);
- rnt_cursor_t c_session = rnt_cursor_open(h_session);
- REQUIRE(c_session != nullptr);
-
- int rows_under_session = 0;
- char* row = nullptr;
- while (rnt_cursor_next(c_session, &row) == 1) {
- ++rows_under_session;
- rnt_free_string(row);
- }
- REQUIRE(rows_under_session == 1);
-
- rnt_cursor_close(c_session);
- rnt_close_handle(h_session);
-
- // Global view sees both tuples.
- rnt_handle_t h_global = rnt_open_handle(rpath, nullptr);
- REQUIRE(h_global != nullptr);
- rnt_cursor_t c_global = rnt_cursor_open(h_global);
- REQUIRE(c_global != nullptr);
- int rows_global = 0;
- while (rnt_cursor_next(c_global, &row) == 1) {
- ++rows_global;
- rnt_free_string(row);
- }
- REQUIRE(rows_global == 2);
- rnt_cursor_close(c_global);
- rnt_close_handle(h_global);
-
- REQUIRE(rnt_session_close(sid.c_str()) == 0);
-}
-
-TEST_CASE("Clearing a session override falls back to the global branch HEAD",
- "[step5][capi][session][resolver]") {
- InitGuard _;
- const char* bpath = "/system/branches/test_step5_clear";
- const char* rpath = "/system/branches/test_step5_clear/multigroups/public/relations/items";
- REQUIRE(rnt_register_branch(bpath, "") == 0);
- REQUIRE(rnt_register_relation(rpath) == 0);
-
- char* th = nullptr;
- REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
- rnt_free_string(th);
- rnt_handle_t bh = rnt_open_handle(bpath, nullptr);
- REQUIRE(bh != nullptr);
- char* target = nullptr;
- REQUIRE(rnt_branch_target(bh, &target) == 0);
- const std::string s1 = take_string(target);
- rnt_close_handle(bh);
-
- // Same pattern as above: install the override on S1 before advancing.
- char* sess = nullptr;
- REQUIRE(rnt_session_open(nullptr, &sess) == 0);
- const std::string sid = take_string(sess);
- REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_clear", s1.c_str()) == 0);
-
- REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
- rnt_free_string(th);
-
- // Clear the override.
- REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_clear", "") == 0);
-
- // Now the session sees the global view (two tuples).
- const std::string spath =
- "/system/sessions/" + sid + "/branches/test_step5_clear/multigroups/public/relations/items";
- rnt_handle_t h = rnt_open_handle(spath.c_str(), nullptr);
- REQUIRE(h != nullptr);
- rnt_cursor_t c = rnt_cursor_open(h);
- REQUIRE(c != nullptr);
- int rows = 0;
- char* row = nullptr;
- while (rnt_cursor_next(c, &row) == 1) {
- ++rows;
- rnt_free_string(row);
- }
- REQUIRE(rows == 2);
- rnt_cursor_close(c);
- rnt_close_handle(h);
-
- REQUIRE(rnt_session_close(sid.c_str()) == 0);
-}
-
-TEST_CASE("rnt_session_set_branch rejects an unregistered snapshot", "[step5][capi][session]") {
- InitGuard _;
- char* sess = nullptr;
- REQUIRE(rnt_session_open(nullptr, &sess) == 0);
- const std::string sid = take_string(sess);
-
- REQUIRE(rnt_session_set_branch(
- sid.c_str(), "anything",
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
-
- REQUIRE(rnt_session_close(sid.c_str()) == 0);
-}
-
-// ---------------------------------------------------------------------------
-// Multi-mg-per-branch — sibling multigroups under one branch evolve
-// independently. Inserting into one mg must not perturb the other's hash
-// or its enumerable relation set.
-// ---------------------------------------------------------------------------
-
-TEST_CASE("Two multigroups under one branch advance independently", "[capi][multi-mg]") {
- InitGuard _;
- const std::string bpath = "/system/branches/test_multi_mg";
- REQUIRE(rnt_register_branch(bpath.c_str(), "") == 0);
-
- // Register one relation in each of two distinct multigroups.
- REQUIRE(rnt_register_relation((bpath + "/multigroups/warehouse/relations/orders").c_str()) ==
- 0);
- REQUIRE(rnt_register_relation((bpath + "/multigroups/audit/relations/events").c_str()) == 0);
-
- char* mgs_raw = nullptr;
- REQUIRE(rnt_list_branch_multigroups(bpath.c_str(), &mgs_raw) == 0);
- const std::string mgs = take_string(mgs_raw);
- REQUIRE(mgs.find("warehouse\t") != std::string::npos);
- REQUIRE(mgs.find("audit\t") != std::string::npos);
-
- // Insert into warehouse; record audit's root before/after.
- char* audit_before = nullptr;
- REQUIRE(rnt_list_relations((bpath + "/multigroups/audit").c_str(), &audit_before) == 0);
- const std::string audit_before_s = take_string(audit_before);
-
- char* th = nullptr;
- REQUIRE(rnt_link_tuple((bpath + "/multigroups/warehouse/relations/orders").c_str(), "id=1\n",
- &th) == 0);
- rnt_free_string(th);
-
- char* audit_after = nullptr;
- REQUIRE(rnt_list_relations((bpath + "/multigroups/audit").c_str(), &audit_after) == 0);
- const std::string audit_after_s = take_string(audit_after);
-
- // audit's relation set is byte-identical: the warehouse-side mutation
- // touched only the warehouse subtree at the multigroup level.
- REQUIRE(audit_before_s == audit_after_s);
-
- // warehouse/orders is now non-empty.
- char* root = nullptr;
- REQUIRE(rnt_relation_root((bpath + "/multigroups/warehouse/relations/orders").c_str(), &root) ==
- 0);
- REQUIRE(!take_string(root).empty());
-
- // audit/events still empty.
- char* root2 = nullptr;
- REQUIRE(rnt_relation_root((bpath + "/multigroups/audit/relations/events").c_str(), &root2) ==
- 0);
- REQUIRE(take_string(root2).empty());
-}
diff --git a/test/CursorManager.cpp b/test/CursorManager.cpp
deleted file mode 100644
index c823ec5..0000000
--- a/test/CursorManager.cpp
+++ /dev/null
@@ -1,152 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "CursorManager.h"
-#include "HandlerManager.h"
-#include "IdentityManager.h"
-#include "InMemoryBackend.h"
-#include "LifecycleManager.h"
-#include "Merkle.h"
-#include "MultigroupCodec.h"
-#include "NamespaceReferenceManager.h"
-#include "ObjectManager.h"
-#include "PermissionsManager.h"
-#include "TupleCodec.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Step 6 / cursor-independence follow-up
-//
-// Two things to verify:
-// 1. A cursor opened on a /system/snapshots/<H>/relations/<n> relation
-// pins the parent Multigroup for its lifetime; releasing the cursor
-// releases the pin.
-// 2. Closing the handle a cursor was opened from no longer dereferences
-// a freed handle pointer inside Next() — the cursor captures the type
-// label and the ephemeral generator at Open time, so iteration after
-// the handle's death is degenerate but safe.
-// ---------------------------------------------------------------------------
-
-namespace {
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-
- std::unique_ptr<nt::ObjectManager::ephemeral_object_type>
- make_constant_eph_type(std::vector<nt::Attribute> attrs) {
- auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
- t->label = EPHEMERAL_RELATION;
- t->methods = {OPEN, CLOSE};
- t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::Finite;
- t->generator = [attrs](const std::vector<std::string>&, std::size_t offset,
- std::size_t limit) -> std::vector<nt::Tuple> {
- if (offset > 0 || limit == 0)
- return {};
- return {nt::Tuple(attrs)};
- };
- return t;
- }
-
- struct Stack {
- nt::InMemoryBackend backend;
- nt::ObjectManager objects;
- nt::PermissionsManager permissions;
- nt::IdentityManager identities;
- nt::LifecycleManager lifecycles{objects, backend};
- nt::NamespaceReferenceManager references{objects, backend};
- nt::HandlerManager handler{objects, permissions, identities, lifecycles, references};
- nt::CursorManager cursors{backend, &lifecycles, &objects};
- };
-} // namespace
-
-TEST_CASE("Cursor pins the parent snapshot and unpins on close", "[step6][cursor][pin]") {
- Stack s;
-
- // Stand up a minimal snapshot with one relation.
- auto& om = s.objects;
- om.Register({"system", "snapshots", "Hcur"}, std::make_unique<nt::ObjectManager::Multigroup>(),
- make_type(MULTIGROUP));
- om.Register({"system", "snapshots", "Hcur", "relations", "items"},
- std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
-
- auto* mg_entry = om.Find({"system", "snapshots", "Hcur"});
- auto* rel_entry = om.Find({"system", "snapshots", "Hcur", "relations", "items"});
-
- // Parent's pin on the child Relation (mirrors register_snapshot in the C
- // API). An external pin on the Multigroup simulates a branch holding the
- // snapshot — without it, closing the cursor would drop ref_count to 0 and
- // cascade-collect mg_entry mid-test.
- s.lifecycles.Pin(rel_entry);
- s.lifecycles.Pin(mg_entry);
-
- // Open a handle and a cursor on the child Relation.
- int conn = 1;
- auto* h = s.handler.Open({"system", "snapshots", "Hcur", "relations", "items"}, &conn);
- REQUIRE(h != nullptr);
- REQUIRE(mg_entry->head->reference_count == 1); // external pin only
-
- auto* c = s.cursors.Open(h);
- REQUIRE(c != nullptr);
- REQUIRE(c->pinned_snapshot == mg_entry);
- REQUIRE(mg_entry->head->reference_count == 2); // external + cursor pin
-
- s.cursors.Close(c);
- REQUIRE(mg_entry->head->reference_count == 1); // back to external only
-
- s.handler.Close(h);
- s.lifecycles.Unpin(mg_entry); // releases the external pin; cascade-collects.
-}
-
-TEST_CASE("Closing the handle mid-iteration does not crash an ephemeral cursor",
- "[step6][cursor][handle-independence]") {
- Stack s;
- // Register an ephemeral relation; its generator yields one tuple.
- s.objects.Register({"eph"}, std::make_unique<nt::ObjectManager::Relation>(),
- make_constant_eph_type({{"k", "v"}}));
-
- int conn = 1;
- auto* h = s.handler.Open({"eph"}, &conn);
- REQUIRE(h != nullptr);
- auto* c = s.cursors.Open(h);
- REQUIRE(c != nullptr);
-
- // Ephemeral cursors start exhausted; reset to simulate a JOIN probe.
- c->exhausted = false;
- c->args = {};
-
- // Now violate the documented contract: close the handle BEFORE Next().
- // The cursor must still produce its tuple from the captured generator
- // without dereferencing the freed handle.
- s.handler.Close(h);
-
- auto* tuple = s.cursors.Next(c);
- REQUIRE(tuple != nullptr);
- bool found = false;
- for (const auto& a : tuple->attrs())
- if (a.name == "k" && a.value == "v") {
- found = true;
- break;
- }
- REQUIRE(found);
-
- s.cursors.Close(c);
-}
-
-TEST_CASE("Cursor opened against a non-snapshot relation does not pin anything",
- "[step6][cursor][pin]") {
- Stack s;
- s.objects.Register({"plain", "rel"}, std::make_unique<nt::ObjectManager::Relation>(),
- make_type(RELATION));
- int conn = 1;
- auto* h = s.handler.Open({"plain", "rel"}, &conn);
- REQUIRE(h != nullptr);
- auto* c = s.cursors.Open(h);
- REQUIRE(c != nullptr);
- REQUIRE(c->pinned_snapshot == nullptr);
-
- s.cursors.Close(c);
- s.handler.Close(h);
-}
diff --git a/test/LifecycleManager.cpp b/test/LifecycleManager.cpp
deleted file mode 100644
index 3712522..0000000
--- a/test/LifecycleManager.cpp
+++ /dev/null
@@ -1,169 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "InMemoryBackend.h"
-#include "LifecycleManager.h"
-#include "ObjectManager.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Step 6 — Pin/Unpin and the cascade
-//
-// LifecycleManager owns object lifetime. Pin/Unpin adjust reference_count;
-// Monitor/Unmonitor adjust handle_count. Both decrement paths call into
-// TryCollect, which gates GC on the joint condition (both counters zero)
-// and, for MULTIGROUP entries, cascades through CascadeMultigroup to
-// unpin every Relation registered under their snapshot subtree.
-// ---------------------------------------------------------------------------
-
-namespace {
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-} // namespace
-
-TEST_CASE("Pin and Unpin adjust reference_count symmetrically", "[step6][lifecycle][pin]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
- REQUIRE(entry->head->reference_count == 0);
-
- lm.Pin(entry);
- lm.Pin(entry);
- REQUIRE(entry->head->reference_count == 2);
-
- lm.Unpin(entry);
- REQUIRE(entry->head->reference_count == 1);
-
- // Still alive at ref_count=1.
- REQUIRE(om.Find({"r1"}) != nullptr);
-}
-
-TEST_CASE("Pin on nullptr is a no-op", "[step6][lifecycle][pin]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- REQUIRE_NOTHROW(lm.Pin(nullptr));
- REQUIRE_NOTHROW(lm.Unpin(nullptr));
-}
-
-TEST_CASE("Unpin below zero does not underflow", "[step6][lifecycle][pin]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- lm.Unpin(entry);
- REQUIRE(entry->head->reference_count == 0);
-}
-
-TEST_CASE("GC is gated on both counters reaching zero", "[step6][lifecycle][gc]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- lm.Monitor(entry);
- lm.Pin(entry);
-
- // handle_count=1, ref_count=1.
- lm.Unpin(entry); // ref_count → 0, but handle_count still 1.
- REQUIRE(om.Find({"r1"}) != nullptr);
-
- lm.Pin(entry);
- lm.Unmonitor(entry); // handle_count → 0, but ref_count still 1.
- REQUIRE(om.Find({"r1"}) != nullptr);
-
- lm.Unpin(entry); // both zero → GC.
- REQUIRE(om.Find({"r1"}) == nullptr);
-}
-
-TEST_CASE("Unmonitor that brings both counters to zero collects the entry",
- "[step6][lifecycle][gc]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- lm.Monitor(entry);
- REQUIRE(entry->head->handle_count == 1);
- lm.Unmonitor(entry);
- REQUIRE(om.Find({"r1"}) == nullptr);
-}
-
-TEST_CASE("IsEligibleForGC reflects the joint-zero rule", "[step6][lifecycle][gc]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
- om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* entry = om.Find({"r1"});
-
- REQUIRE(lm.IsEligibleForGC(entry));
- lm.Pin(entry);
- REQUIRE_FALSE(lm.IsEligibleForGC(entry));
-}
-
-TEST_CASE("CascadeMultigroup unpins child Relations on snapshot collection",
- "[step6][lifecycle][cascade]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
-
- om.Register({"system", "snapshots", "H1"}, std::make_unique<nt::ObjectManager::Multigroup>(),
- make_type(MULTIGROUP));
- auto* mg_entry = om.Find({"system", "snapshots", "H1"});
-
- for (const auto& n : {std::string{"foo"}, std::string{"bar"}}) {
- om.Register({"system", "snapshots", "H1", "relations", n},
- std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- // The parent Multigroup's pin on each child, mirroring what
- // register_snapshot does in the C API layer.
- lm.Pin(om.Find({"system", "snapshots", "H1", "relations", n}));
- }
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) != nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) != nullptr);
-
- // One external pin on the Multigroup (e.g. a branch). Release it: cascade.
- lm.Pin(mg_entry);
- lm.Unpin(mg_entry);
-
- REQUIRE(om.Find({"system", "snapshots", "H1"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) == nullptr);
-}
-
-TEST_CASE("Cascade survives a child with an open handle", "[step6][lifecycle][cascade]") {
- nt::InMemoryBackend store;
- nt::ObjectManager om;
- nt::LifecycleManager lm(om, store);
-
- om.Register({"system", "snapshots", "H2"}, std::make_unique<nt::ObjectManager::Multigroup>(),
- make_type(MULTIGROUP));
- auto* mg_entry = om.Find({"system", "snapshots", "H2"});
-
- om.Register({"system", "snapshots", "H2", "relations", "live"},
- std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
- auto* child = om.Find({"system", "snapshots", "H2", "relations", "live"});
- lm.Pin(child); // parent's pin
- lm.Monitor(child); // simulate an open handle on the child
-
- // Drop the Multigroup's external ref: cascade unpins child (ref → 0),
- // but handle_count is still 1 — child must survive.
- lm.Pin(mg_entry);
- lm.Unpin(mg_entry);
-
- REQUIRE(om.Find({"system", "snapshots", "H2"}) == nullptr);
- REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) != nullptr);
-
- // Closing the handle now releases the child.
- lm.Unmonitor(child);
- REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) == nullptr);
-}
diff --git a/test/MultigroupCodec.cpp b/test/MultigroupCodec.cpp
deleted file mode 100644
index b04c405..0000000
--- a/test/MultigroupCodec.cpp
+++ /dev/null
@@ -1,130 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "InMemoryBackend.h"
-#include "MultigroupCodec.h"
-
-#include <string>
-#include <vector>
-
-// ---------------------------------------------------------------------------
-// Multigroup snapshot — Merkle<string> tree mapping relation name → relation root.
-//
-// The codec is a thin facade over Merkle<std::string>; these tests confirm
-// the tree is order-independent (it sorts intrinsically), that mutations are
-// path-localised (sibling entries are untouched), and that snapshots can be
-// re-enumerated from a cold backend by hash alone.
-// ---------------------------------------------------------------------------
-
-namespace {
-
- // Pad a short label into a deterministic 64-char hex string so the codec can
- // decode it as a 32-byte payload.
- std::string mk_root(char c, char d = '0') {
- std::string h(64, '0');
- h[0] = c;
- h[1] = d;
- return h;
- }
-
-} // namespace
-
-TEST_CASE("Build is order-independent - tree sorts intrinsically", "[multigroup-codec]") {
- nt::InMemoryBackend a, b;
-
- std::vector<nt::MultigroupCodec::RelationEntry> in_order = {
- {"alpha", mk_root('a')},
- {"bravo", mk_root('b')},
- {"charlie", mk_root('c')},
- };
- std::vector<nt::MultigroupCodec::RelationEntry> shuffled = {
- {"charlie", mk_root('c')},
- {"alpha", mk_root('a')},
- {"bravo", mk_root('b')},
- };
-
- REQUIRE(nt::MultigroupCodec::Build(a, in_order) == nt::MultigroupCodec::Build(b, shuffled));
-}
-
-TEST_CASE("Build distinguishes content changes", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> base = {{"foo", mk_root('a')}};
- std::vector<nt::MultigroupCodec::RelationEntry> different_root = {{"foo", mk_root('b')}};
- std::vector<nt::MultigroupCodec::RelationEntry> different_name = {{"bar", mk_root('a')}};
- std::vector<nt::MultigroupCodec::RelationEntry> extra_member = {{"foo", mk_root('a')},
- {"bar", mk_root('b')}};
-
- const auto h_base = nt::MultigroupCodec::Build(backend, base);
- REQUIRE(h_base != nt::MultigroupCodec::Build(backend, different_root));
- REQUIRE(h_base != nt::MultigroupCodec::Build(backend, different_name));
- REQUIRE(h_base != nt::MultigroupCodec::Build(backend, extra_member));
-}
-
-TEST_CASE("Build then List round-trips entries in key-sorted order", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a', '1')},
- {"bar", mk_root('b', '2')},
- {"baz", mk_root('c', '3')},
- };
- const auto root = nt::MultigroupCodec::Build(backend, entries);
-
- auto listed = nt::MultigroupCodec::List(backend, root);
- REQUIRE(listed.size() == 3);
- REQUIRE(listed[0].first == "bar");
- REQUIRE(listed[1].first == "baz");
- REQUIRE(listed[2].first == "foo");
- REQUIRE(listed[0].second == mk_root('b', '2'));
-}
-
-TEST_CASE("Lookup returns payload by name", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a')},
- {"bar", mk_root('b')},
- };
- const auto root = nt::MultigroupCodec::Build(backend, entries);
-
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "foo") == mk_root('a'));
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "bar") == mk_root('b'));
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "missing").empty());
-}
-
-TEST_CASE("InsertOne mutates one entry; sibling subtree is untouched", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a')},
- {"bar", mk_root('b')},
- };
- const auto root0 = nt::MultigroupCodec::Build(backend, entries);
-
- // Mutate foo only; bar's payload must round-trip unchanged.
- const auto root1 = nt::MultigroupCodec::InsertOne(backend, root0, "foo", mk_root('d'));
- REQUIRE(root1 != root0);
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "foo") == mk_root('d'));
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
-}
-
-TEST_CASE("RemoveOne deletes one entry; sibling preserved", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
-
- std::vector<nt::MultigroupCodec::RelationEntry> entries = {
- {"foo", mk_root('a')},
- {"bar", mk_root('b')},
- };
- const auto root0 = nt::MultigroupCodec::Build(backend, entries);
-
- const auto root1 = nt::MultigroupCodec::RemoveOne(backend, root0, "foo");
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "foo").empty());
- REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
-}
-
-TEST_CASE("Empty multigroup builds to an empty root", "[multigroup-codec]") {
- nt::InMemoryBackend backend;
- std::vector<nt::MultigroupCodec::RelationEntry> empty;
- REQUIRE(nt::MultigroupCodec::Build(backend, empty).empty());
- REQUIRE(nt::MultigroupCodec::List(backend, "").empty());
-}
diff --git a/test/NamespaceReferenceManager.cpp b/test/NamespaceReferenceManager.cpp
deleted file mode 100644
index 90e65e5..0000000
--- a/test/NamespaceReferenceManager.cpp
+++ /dev/null
@@ -1,177 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "InMemoryBackend.h"
-#include "Merkle.h"
-#include "NamespaceReferenceManager.h"
-#include "ObjectManager.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Resolver — two-level reparse through the branch tree.
-//
-// NamespaceReferenceManager::Resolve rewrites
-// /system/branches/<name>/multigroups/<mg>/<sub>...
-// into
-// /system/snapshots/<mg_hash>/<sub>...
-// by reading branch.target_hash (a Merkle<string> root) from storage and
-// looking up <mg>. Session paths consult the per-session override map first
-// and fall back to the global branch when no override is set.
-// ---------------------------------------------------------------------------
-
-namespace {
-
- using Path = std::vector<std::string>;
-
- // Pad a label into a deterministic 64-char hex string so the merkle codec can
- // decode it as a 32-byte payload.
- std::string mk_hash(char c, char d = '0') {
- std::string h(64, '0');
- h[0] = c;
- h[1] = d;
- return h;
- }
-
- std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label) {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = label;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-
- void register_branch(nt::ObjectManager& om, const std::string& name,
- const std::string& target_hash) {
- auto br = std::make_unique<nt::ObjectManager::Branch>();
- br->name = name;
- br->target_hash = target_hash;
- om.Register({"system", "branches", name}, std::move(br), make_type(BRANCH));
- }
-
- void register_session(nt::ObjectManager& om, const std::string& id,
- std::map<std::string, std::string> overrides = {}) {
- auto s = std::make_unique<nt::ObjectManager::Session>();
- s->branch_overrides = std::move(overrides);
- om.Register({"system", "sessions", id}, std::move(s), make_type(SESSION));
- }
-
- // Builds a branch tree with a single (mg_name → mg_hash) entry. Returns the
- // branch-tree root hex.
- std::string build_branch_tree(nt::InMemoryBackend& store, const std::string& mg_name,
- const std::string& mg_hash_hex) {
- return nt::Merkle<std::string>::Insert(store, "", mg_name, nt::hex_to_bin(mg_hash_hex));
- }
-
-} // namespace
-
-TEST_CASE("Branch sub-path rewrites through the branch tree to the mg snapshot", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
-
- const std::string mg_hash = mk_hash('a');
- const std::string tree = build_branch_tree(store, "warehouse", mg_hash);
- register_branch(om, "main", tree);
-
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "branches", "main", "multigroups", "warehouse", "relations",
- "items"}) == Path{"system", "snapshots", mg_hash, "relations", "items"});
-}
-
-TEST_CASE("Resolve on the branch object itself is left unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
- register_branch(om, "main", tree);
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "main"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Resolve on an unborn branch leaves the path unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- register_branch(om, "main", ""); // unborn
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "main", "multigroups", "warehouse", "relations", "items"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Resolve on an absent multigroup leaves the path unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
- register_branch(om, "main", tree);
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "main", "multigroups", "audit", "relations", "events"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Resolve on an unknown branch leaves the path unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "branches", "ghost", "multigroups", "warehouse", "relations", "items"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Non-branch paths pass through unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"relations", "anywhere"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Session override takes precedence over the global branch HEAD", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
-
- const std::string mg_global = mk_hash('a');
- const std::string mg_session = mk_hash('b');
- const std::string tree_global = build_branch_tree(store, "warehouse", mg_global);
- const std::string tree_session = build_branch_tree(store, "warehouse", mg_session);
-
- register_branch(om, "main", tree_global);
- register_session(om, "sess123", {{"main", tree_session}});
-
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main", "multigroups",
- "warehouse", "relations", "items"}) ==
- Path{"system", "snapshots", mg_session, "relations", "items"});
-}
-
-TEST_CASE("Session resolution falls back to global branch when override is absent", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
-
- const std::string mg_global = mk_hash('a');
- const std::string tree_global = build_branch_tree(store, "warehouse", mg_global);
-
- register_branch(om, "main", tree_global);
- register_session(om, "sess123"); // no overrides
-
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main", "multigroups",
- "warehouse", "relations", "items"}) ==
- Path{"system", "snapshots", mg_global, "relations", "items"});
-}
-
-TEST_CASE("Session resolution with neither override nor global branch is unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- register_session(om, "sess123");
-
- nt::NamespaceReferenceManager nrm(om, store);
- const Path p{"system", "sessions", "sess123", "branches", "main",
- "multigroups", "warehouse", "relations", "items"};
- REQUIRE(nrm.Resolve(p) == p);
-}
-
-TEST_CASE("Empty input path is returned unchanged", "[resolve]") {
- nt::ObjectManager om;
- nt::InMemoryBackend store;
- nt::NamespaceReferenceManager nrm(om, store);
- REQUIRE(nrm.Resolve({}).empty());
-}
diff --git a/test/ObjectManager.cpp b/test/ObjectManager.cpp
deleted file mode 100644
index f90de3a..0000000
--- a/test/ObjectManager.cpp
+++ /dev/null
@@ -1,68 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "ObjectManager.h"
-
-#include <memory>
-
-// ---------------------------------------------------------------------------
-// Step 5 — Unregister
-//
-// Sessions are disposable and must be removable from the registry on close.
-// Unregister is the splice-out primitive that rnt_session_close, the
-// LifecycleManager TryCollect path, and any future explicit-delete API all
-// share.
-// ---------------------------------------------------------------------------
-
-namespace {
- std::unique_ptr<nt::ObjectManager::object_type> session_type() {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = SESSION;
- t->disposable = true;
- t->methods = {OPEN, CLOSE};
- return t;
- }
-} // namespace
-
-TEST_CASE("Unregister removes the entry and Find returns nullptr", "[step5][object-manager]") {
- nt::ObjectManager om;
- om.Register({"system", "sessions", "alpha"}, std::make_unique<nt::ObjectManager::Session>(),
- session_type());
- REQUIRE(om.Find({"system", "sessions", "alpha"}) != nullptr);
-
- REQUIRE(om.Unregister({"system", "sessions", "alpha"}) == true);
- REQUIRE(om.Find({"system", "sessions", "alpha"}) == nullptr);
-}
-
-TEST_CASE("Unregister returns false for missing paths", "[step5][object-manager]") {
- nt::ObjectManager om;
- REQUIRE(om.Unregister({"never", "registered"}) == false);
-}
-
-TEST_CASE("Unregister splices a middle entry without disturbing neighbours",
- "[step5][object-manager]") {
- nt::ObjectManager om;
- om.Register({"a"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
- om.Register({"b"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
- om.Register({"c"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
-
- REQUIRE(om.Unregister({"b"}) == true);
-
- REQUIRE(om.Find({"a"}) != nullptr);
- REQUIRE(om.Find({"b"}) == nullptr);
- REQUIRE(om.Find({"c"}) != nullptr);
-}
-
-TEST_CASE("Unregister at the head of the list works", "[step5][object-manager]") {
- nt::ObjectManager om;
- om.Register({"first"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
- om.Register({"second"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
-
- // The most recently registered entry is the list head (LIFO).
- REQUIRE(om.Unregister({"second"}) == true);
- REQUIRE(om.Find({"second"}) == nullptr);
- REQUIRE(om.Find({"first"}) != nullptr);
-
- // And the new head is removable too.
- REQUIRE(om.Unregister({"first"}) == true);
- REQUIRE(om.Find({"first"}) == nullptr);
-}
diff --git a/test/VM.cpp b/test/VM.cpp
deleted file mode 100644
index 7e61da6..0000000
--- a/test/VM.cpp
+++ /dev/null
@@ -1,254 +0,0 @@
-#include <catch2/catch_test_macros.hpp>
-
-#include "CursorManager.h"
-#include "HandlerManager.h"
-#include "IdentityManager.h"
-#include "InMemoryBackend.h"
-#include "LifecycleManager.h"
-#include "Merkle.h"
-#include "NamespaceReferenceManager.h"
-#include "ObjectManager.h"
-#include "PermissionsManager.h"
-#include "TupleCodec.h"
-#include "VM.h"
-
-#include <algorithm>
-#include <string>
-#include <vector>
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-// Stores a tuple in the backend and updates the Relation object's Merkle root.
-static void store_tuple(nt::InMemoryBackend& backend, nt::ObjectManager& objects,
- const std::vector<std::string>& path, std::vector<nt::Attribute> attrs) {
- auto bytes = nt::TupleCodec::Serialize(attrs);
- auto hash = backend.Put(std::move(bytes));
-
- auto* entry = objects.Find(path);
- if (!entry)
- return;
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(entry->object.get());
- if (!rel)
- return;
- auto hash_bin = nt::hex_to_bin(hash);
- rel->merkle_root =
- nt::Merkle<nt::Hash32>::Insert(backend, rel->merkle_root, hash_bin, hash_bin);
-}
-
-static std::unique_ptr<nt::ObjectManager::object_type> make_relation_type() {
- auto t = std::make_unique<nt::ObjectManager::object_type>();
- t->label = RELATION;
- t->disposable = false;
- t->methods = {OPEN, CLOSE};
- return t;
-}
-
-// Generator for eq[A, B]: returns one tuple when A == B, nothing otherwise.
-// Treats both args as integers when parseable, strings otherwise.
-static std::unique_ptr<nt::ObjectManager::ephemeral_object_type> make_eq_type() {
- auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
- t->label = EPHEMERAL_RELATION;
- t->disposable = false;
- t->methods = {OPEN, CLOSE};
- t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::AlephZero;
- t->generator = [](const std::vector<std::string>& args, std::size_t offset,
- std::size_t limit) -> std::vector<nt::Tuple> {
- if (args.size() < 2 || offset > 0 || limit == 0)
- return {};
- if (args[0] != args[1])
- return {};
- return {nt::Tuple({{"left", args[0]}, {"right", args[1]}})};
- };
- return t;
-}
-
-// ---------------------------------------------------------------------------
-// Fixture: shared manager stack
-// ---------------------------------------------------------------------------
-
-struct Fixture {
- nt::InMemoryBackend backend;
- nt::ObjectManager objects;
- nt::PermissionsManager permissions;
- nt::IdentityManager identities;
- nt::LifecycleManager lifecycles{objects, backend};
- nt::NamespaceReferenceManager references{objects, backend};
- nt::HandlerManager handler{objects, permissions, identities, lifecycles, references};
- nt::CursorManager cursors{backend, &lifecycles, &objects};
- int conn = 1;
-};
-
-// ---------------------------------------------------------------------------
-// Tests
-// ---------------------------------------------------------------------------
-
-TEST_CASE("SCAN returns all tuples from a stored relation", "[scan]") {
- Fixture f;
- const std::vector<std::string> path = {"relations", "villager"};
-
- f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, {{"name", "John"}, {"age", "30"}});
- store_tuple(f.backend, f.objects, path, {{"name", "Peter"}, {"age", "25"}});
-
- auto* handle = f.handler.Open(path, &f.conn);
- REQUIRE(handle != nullptr);
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(path)->object.get());
- auto* cursor = f.cursors.Open(handle, rel->merkle_root);
- REQUIRE(cursor != nullptr);
-
- nt::PlanNode plan;
- plan.op = nt::FOL_OPERATION_SCAN;
- plan.scan_cursor = cursor;
-
- nt::VM vm(f.cursors);
- int count = 0;
- while (vm.Next(&plan))
- ++count;
-
- REQUIRE(count == 2);
-
- f.cursors.Close(cursor);
- f.handler.Close(handle);
-}
-
-TEST_CASE("JOIN filters a stored relation with eq ephemeral", "[join]") {
- Fixture f;
- const std::vector<std::string> people_path = {"relations", "people"};
- const std::vector<std::string> eq_path = {"builtins", "eq"};
-
- // Register stored relation: people(name, age)
- f.objects.Register(people_path, std::make_unique<nt::ObjectManager::Relation>(),
- make_relation_type());
-
- store_tuple(f.backend, f.objects, people_path, {{"name", "John"}, {"age", "30"}});
- store_tuple(f.backend, f.objects, people_path, {{"name", "Peter"}, {"age", "25"}});
- store_tuple(f.backend, f.objects, people_path, {{"name", "Jude"}, {"age", "25"}});
-
- // Register ephemeral eq relation
- f.objects.Register(eq_path, std::make_unique<nt::ObjectManager::Relation>(), make_eq_type());
-
- // Open handles and cursors
- auto* people_handle = f.handler.Open(people_path, &f.conn);
- REQUIRE(people_handle != nullptr);
- auto* eq_handle = f.handler.Open(eq_path, &f.conn);
- REQUIRE(eq_handle != nullptr);
-
- auto* people_rel =
- dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(people_path)->object.get());
- auto* people_cursor = f.cursors.Open(people_handle, people_rel->merkle_root);
- REQUIRE(people_cursor != nullptr);
- auto* eq_cursor = f.cursors.Open(eq_handle); // EPHEMERAL: starts exhausted, JOIN resets
- REQUIRE(eq_cursor != nullptr);
-
- // Plan: JOIN( SCAN(people), SCAN(eq[ Var("age"), Const("25") ]) )
- nt::PlanNode right;
- right.op = nt::FOL_OPERATION_SCAN;
- right.scan_cursor = eq_cursor;
- right.scan_args = {nt::PathArg::Var("age"), nt::PathArg::Const("25")};
-
- nt::PlanNode left;
- left.op = nt::FOL_OPERATION_SCAN;
- left.scan_cursor = people_cursor;
-
- nt::PlanNode root;
- root.op = nt::FOL_OPERATION_JOIN;
- root.left = &left;
- root.right = &right;
-
- // Execute and collect matched names
- nt::VM vm(f.cursors);
- std::vector<std::string> matched;
- while (nt::Tuple* t = vm.Next(&root)) {
- for (const auto& attr : t->attrs())
- if (attr.name == "name")
- matched.push_back(attr.value);
- }
-
- // Jude (age=25) and Peter (age=25) should match; Jogn (age=30) should not
- REQUIRE(matched.size() == 2);
- REQUIRE(std::find(matched.begin(), matched.end(), "Jude") != matched.end());
- REQUIRE(std::find(matched.begin(), matched.end(), "Peter") != matched.end());
- REQUIRE(std::find(matched.begin(), matched.end(), "John") == matched.end());
-
- f.cursors.Close(people_cursor);
- f.cursors.Close(eq_cursor);
- f.handler.Close(people_handle);
- f.handler.Close(eq_handle);
-}
-
-TEST_CASE("TAKE limits tuples emitted from a SCAN", "[take]") {
- Fixture f;
- const std::vector<std::string> path = {"relations", "items"};
-
- f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, {{"id", "1"}});
- store_tuple(f.backend, f.objects, path, {{"id", "2"}});
- store_tuple(f.backend, f.objects, path, {{"id", "3"}});
-
- auto* handle = f.handler.Open(path, &f.conn);
- REQUIRE(handle != nullptr);
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(path)->object.get());
- auto* cursor = f.cursors.Open(handle, rel->merkle_root);
- REQUIRE(cursor != nullptr);
-
- nt::PlanNode scan;
- scan.op = nt::FOL_OPERATION_SCAN;
- scan.scan_cursor = cursor;
-
- nt::PlanNode take;
- take.op = nt::FOL_OPERATION_TAKE;
- take.left = &scan;
- take.take_limit = 2;
-
- nt::VM vm(f.cursors);
- int count = 0;
- while (vm.Next(&take))
- ++count;
-
- REQUIRE(count == 2);
-
- f.cursors.Close(cursor);
- f.handler.Close(handle);
-}
-
-TEST_CASE("PROJECT filters tuple attributes by name", "[project]") {
- Fixture f;
- const std::vector<std::string> path = {"relations", "people"};
-
- f.objects.Register(path, std::make_unique<nt::ObjectManager::Relation>(), make_relation_type());
- store_tuple(f.backend, f.objects, path, {{"name", "John"}, {"age", "30"}, {"city", "Lisbon"}});
-
- auto* handle = f.handler.Open(path, &f.conn);
- REQUIRE(handle != nullptr);
- auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(f.objects.Find(path)->object.get());
- auto* cursor = f.cursors.Open(handle, rel->merkle_root);
- REQUIRE(cursor != nullptr);
-
- nt::PlanNode scan;
- scan.op = nt::FOL_OPERATION_SCAN;
- scan.scan_cursor = cursor;
-
- nt::PlanNode project;
- project.op = nt::FOL_OPERATION_PROJECT;
- project.left = &scan;
- project.project_attrs = {"name", "city"};
-
- nt::VM vm(f.cursors);
- nt::Tuple* t = vm.Next(&project);
- REQUIRE(t != nullptr);
-
- std::vector<std::string> names;
- for (const auto& attr : t->attrs())
- names.push_back(attr.name);
-
- REQUIRE(names.size() == 2);
- REQUIRE(std::find(names.begin(), names.end(), "name") != names.end());
- REQUIRE(std::find(names.begin(), names.end(), "city") != names.end());
- REQUIRE(std::find(names.begin(), names.end(), "age") == names.end());
- REQUIRE(vm.Next(&project) == nullptr);
-
- f.cursors.Close(cursor);
- f.handler.Close(handle);
-}