diff options
Diffstat (limited to '')
| -rw-r--r-- | test/CursorManager.cpp | 160 |
1 files changed, 160 insertions, 0 deletions
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 <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 }; + }; +} + +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); +} |
