diff options
| author | Estevan Castilho <tevo@riajuu.net> | 2026-07-02 00:31:57 +0000 |
|---|---|---|
| committer | Estevan Castilho <tevo@riajuu.net> | 2026-07-02 00:31:57 +0000 |
| commit | ab70e5461b98ddf2b56268c53c171cab89157027 (patch) | |
| tree | 3e57218edacc8e1366e0d1a86bdb922af7e51dca /test | |
| parent | a4f6637601548a7158869ad155f29db50b151823 (diff) | |
| download | RNT-ab70e5461b98ddf2b56268c53c171cab89157027.tar.gz RNT-ab70e5461b98ddf2b56268c53c171cab89157027.zip | |
Nuke the old code
Diffstat (limited to 'test')
| -rw-r--r-- | test/C_API.cpp | 415 | ||||
| -rw-r--r-- | test/CursorManager.cpp | 152 | ||||
| -rw-r--r-- | test/LifecycleManager.cpp | 169 | ||||
| -rw-r--r-- | test/MultigroupCodec.cpp | 130 | ||||
| -rw-r--r-- | test/NamespaceReferenceManager.cpp | 177 | ||||
| -rw-r--r-- | test/ObjectManager.cpp | 68 | ||||
| -rw-r--r-- | test/VM.cpp | 254 |
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); -} |
