aboutsummaryrefslogtreecommitdiffstats
path: root/test/LifecycleManager.cpp
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-05-18 12:50:25 -0300
committerGitHub <noreply@github.com>2026-05-18 12:50:25 -0300
commitaf8759a9c1d965a3220024d1857aea219d62d898 (patch)
tree9cf14af855cac3674f508ec001bd04e262a524ae /test/LifecycleManager.cpp
parent0fc76b99abbff1d3cef8e2f9fcd56f3ae4f23a7a (diff)
downloadRNT-af8759a9c1d965a3220024d1857aea219d62d898.tar.gz
RNT-af8759a9c1d965a3220024d1857aea219d62d898.zip
Branching and object registry rework (#11)
Diffstat (limited to 'test/LifecycleManager.cpp')
-rw-r--r--test/LifecycleManager.cpp185
1 files changed, 185 insertions, 0 deletions
diff --git a/test/LifecycleManager.cpp b/test/LifecycleManager.cpp
new file mode 100644
index 0000000..aa75e14
--- /dev/null
+++ b/test/LifecycleManager.cpp
@@ -0,0 +1,185 @@
+#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;
+ }
+}
+
+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);
+}