aboutsummaryrefslogblamecommitdiffstats
path: root/test/MultigroupCodec.cpp
blob: 25c8b53850f362329a8efb9560a2e9c4393001a0 (plain) (tree)
















































































































































                                                                                             
#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());
}