From af8759a9c1d965a3220024d1857aea219d62d898 Mon Sep 17 00:00:00 2001 From: Marcos Magueta Date: Mon, 18 May 2026 12:50:25 -0300 Subject: Branching and object registry rework (#11) --- test/CursorManager.cpp | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 test/CursorManager.cpp (limited to 'test/CursorManager.cpp') diff --git a/test/CursorManager.cpp b/test/CursorManager.cpp new file mode 100644 index 0000000..8c7de1a --- /dev/null +++ b/test/CursorManager.cpp @@ -0,0 +1,160 @@ +#include + +#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 + +// --------------------------------------------------------------------------- +// Step 6 / cursor-independence follow-up +// +// Two things to verify: +// 1. A cursor opened on a /system/snapshots//relations/ 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 make_type(OBJECT_TYPE label) + { + auto t = std::make_unique(); + t->label = label; + t->methods = { OPEN, CLOSE }; + return t; + } + + std::unique_ptr + make_constant_eph_type(std::vector attrs) + { + auto t = std::make_unique(); + t->label = EPHEMERAL_RELATION; + t->methods = { OPEN, CLOSE }; + t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::Finite; + t->generator = [attrs](const std::vector&, + std::size_t offset, std::size_t limit) + -> std::vector + { + 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 }; + }; +} + +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(), make_type(MULTIGROUP)); + om.Register({"system", "snapshots", "Hcur", "relations", "items"}, + std::make_unique(), 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(), + 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(), + 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); +} -- cgit v1.2.3