aboutsummaryrefslogblamecommitdiffstats
path: root/test/EphemeralRelation.cpp
blob: 18456c246e7323732d730a8f4fd166da776ae95e (plain) (tree)







































































































































































                                                                                                  
#include <catch2/catch_test_macros.hpp>

#include "EphemeralRelation.h"
#include "InMemoryBackend.h"
#include "LifecycleManager.h"
#include "ObjectManager.h"

#include <memory>

// Ephemeral relations — writer path, tiered ownership, and GC cascade.
//
// nt::Ephemeral::Register lands an EPHEMERAL_RELATION in the object manager,
// pins the base relations it depends on, and (for the Named tier) takes a
// session-ownership pin so the entry outlives its cursor. Collection unpins
// the base relations via LifecycleManager::CascadeEphemeral.

namespace {
    using Card = nt::ObjectManager::ephemeral_object_type::Cardinality;

    std::unique_ptr<nt::ObjectManager::object_type> make_relation_type() {
        auto t = std::make_unique<nt::ObjectManager::object_type>();
        t->label = RELATION;
        t->methods = {OPEN, CLOSE};
        return t;
    }

    // Registers a stored base relation at the given registry path and sets its
    // merkle_root. Takes one baseline pin to model the owning multigroup
    // snapshot, so the relation survives an ephemeral releasing its own pin —
    // the reference_count then returns to 1 rather than dropping to 0 and being
    // collected. Returns the slash-joined logical path for use as a dependency.
    std::string make_base(nt::ObjectManager& om, nt::LifecycleManager& lm,
                          const std::vector<std::string>& path, const std::string& root) {
        auto rel = std::make_unique<nt::ObjectManager::Relation>();
        rel->merkle_root = root;
        om.Register(path, std::move(rel), make_relation_type());
        lm.Pin(om.Find(path)); // baseline pin: the snapshot that owns this relation
        std::string joined;
        for (const auto& seg : path) {
            if (!joined.empty())
                joined.push_back('/');
            joined += seg;
        }
        return joined;
    }

    // A trivial generator: yields nothing. Lifetime tests do not scan.
    nt::ObjectManager::ephemeral_object_type::Generator empty_generator() {
        return [](const std::vector<std::string>&, std::size_t, std::size_t) {
            return std::vector<nt::Tuple>{};
        };
    }
} // namespace

TEST_CASE("ComposeRoot is deterministic and order-independent", "[ephemeral][hash]") {
    const std::vector<std::pair<std::string, std::string>> schema_a = {{"left", "natural"},
                                                                       {"right", "natural"}};
    const std::vector<std::pair<std::string, std::string>> schema_b = {{"right", "natural"},
                                                                       {"left", "natural"}};

    const std::string h1 = nt::Ephemeral::ComposeRoot("eq", schema_a, {"aaa", "bbb"});
    const std::string h2 = nt::Ephemeral::ComposeRoot("eq", schema_b, {"bbb", "aaa"});
    REQUIRE(h1 == h2);                  // schema + base-root order must not matter
    REQUIRE(h1.size() == 64);

    // Identity, schema, or base roots each change the digest.
    REQUIRE(nt::Ephemeral::ComposeRoot("lt", schema_a, {"aaa", "bbb"}) != h1);
    REQUIRE(nt::Ephemeral::ComposeRoot("eq", schema_a, {"aaa", "ccc"}) != h1);
    REQUIRE(nt::Ephemeral::ComposeRoot("eq", {{"left", "natural"}}, {"aaa", "bbb"}) != h1);
}

TEST_CASE("Scratch tier pins deps but is not session-owned", "[ephemeral][scratch]") {
    nt::InMemoryBackend store;
    nt::ObjectManager om;
    nt::LifecycleManager lm(om, store);

    const std::string dep =
        make_base(om, lm, {"system", "snapshots", "h", "relations", "r1"}, "root1");

    auto* eph = nt::Ephemeral::Register(om, lm, "sess", nt::Ephemeral::Tier::Scratch, "",
                                        empty_generator(), Card::Finite, "select",
                                        {{"a", "abstract"}}, {dep});
    REQUIRE(eph != nullptr);

    // Registered under the session's scratch subdir, keyed by composed root.
    REQUIRE(eph->head->path[0] == "system");
    REQUIRE(eph->head->path[3] == "scratch");

    // Base relation gains the ephemeral's pin atop its snapshot baseline (1+1);
    // the ephemeral itself carries no ownership pin.
    REQUIRE(om.Find({"system", "snapshots", "h", "relations", "r1"})->head->reference_count == 2);
    REQUIRE(eph->head->reference_count == 0);
}

TEST_CASE("Scratch ephemeral is collected by counters and unpins its base",
          "[ephemeral][scratch][gc]") {
    nt::InMemoryBackend store;
    nt::ObjectManager om;
    nt::LifecycleManager lm(om, store);

    const std::string dep =
        make_base(om, lm, {"system", "snapshots", "h", "relations", "r1"}, "root1");
    auto* eph = nt::Ephemeral::Register(om, lm, "sess", nt::Ephemeral::Tier::Scratch, "",
                                        empty_generator(), Card::Finite, "select",
                                        {{"a", "abstract"}}, {dep});
    const auto eph_path = eph->head->path;

    // A cursor opens (Monitor) then closes (Unmonitor) over the scratch result.
    lm.Monitor(eph);
    REQUIRE(om.Find(eph_path) != nullptr);
    lm.Unmonitor(eph);

    // Gone by counters, and the cascade released the ephemeral's pin on the
    // base, returning it to its snapshot baseline (still alive at 1).
    REQUIRE(om.Find(eph_path) == nullptr);
    REQUIRE(om.Find({"system", "snapshots", "h", "relations", "r1"})->head->reference_count == 1);
}

TEST_CASE("Named ephemeral survives its cursor and is freed at session release",
          "[ephemeral][named][gc]") {
    nt::InMemoryBackend store;
    nt::ObjectManager om;
    nt::LifecycleManager lm(om, store);

    const std::string dep =
        make_base(om, lm, {"system", "snapshots", "h", "relations", "r1"}, "root1");
    auto* eph = nt::Ephemeral::Register(om, lm, "sess", nt::Ephemeral::Tier::Named, "myview",
                                        empty_generator(), Card::Finite, "join",
                                        {{"a", "abstract"}}, {dep});
    REQUIRE(eph != nullptr);
    REQUIRE(eph->head->path[3] == "ephemeral");
    REQUIRE(eph->head->path[4] == "myview");
    REQUIRE(eph->head->reference_count == 1); // session-ownership pin

    const auto eph_path = eph->head->path;

    // A cursor opens and closes; the session pin keeps the named view alive.
    lm.Monitor(eph);
    lm.Unmonitor(eph);
    REQUIRE(om.Find(eph_path) != nullptr);

    // Session close releases it; the base relation's pin cascades free.
    const std::size_t freed = nt::Ephemeral::ReleaseSession(om, lm, "sess");
    REQUIRE(freed == 1);
    REQUIRE(om.Find(eph_path) == nullptr);
    REQUIRE(om.Find({"system", "snapshots", "h", "relations", "r1"})->head->reference_count == 1);
}

TEST_CASE("Identical scratch results dedup and do not double-pin", "[ephemeral][dedup]") {
    nt::InMemoryBackend store;
    nt::ObjectManager om;
    nt::LifecycleManager lm(om, store);

    const std::string dep =
        make_base(om, lm, {"system", "snapshots", "h", "relations", "r1"}, "root1");

    auto* first = nt::Ephemeral::Register(om, lm, "sess", nt::Ephemeral::Tier::Scratch, "",
                                          empty_generator(), Card::Finite, "select",
                                          {{"a", "abstract"}}, {dep});
    auto* second = nt::Ephemeral::Register(om, lm, "sess", nt::Ephemeral::Tier::Scratch, "",
                                           empty_generator(), Card::Finite, "select",
                                           {{"a", "abstract"}}, {dep});

    REQUIRE(first == second); // same composed root → same entry
    // The ephemeral pinned the base exactly once (atop the baseline), not once
    // per Register call.
    REQUIRE(om.Find({"system", "snapshots", "h", "relations", "r1"})->head->reference_count == 2);
}