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/MultigroupCodec.cpp | 145 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 test/MultigroupCodec.cpp (limited to 'test/MultigroupCodec.cpp') diff --git a/test/MultigroupCodec.cpp b/test/MultigroupCodec.cpp new file mode 100644 index 0000000..25c8b53 --- /dev/null +++ b/test/MultigroupCodec.cpp @@ -0,0 +1,145 @@ +#include + +#include "InMemoryBackend.h" +#include "MultigroupCodec.h" + +#include +#include + +// --------------------------------------------------------------------------- +// Multigroup snapshot — Merkle tree mapping relation name → relation root. +// +// The codec is a thin facade over Merkle; 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 in_order = { + {"alpha", mk_root('a')}, + {"bravo", mk_root('b')}, + {"charlie", mk_root('c')}, + }; + std::vector 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 base = {{"foo", mk_root('a')}}; + std::vector different_root = {{"foo", mk_root('b')}}; + std::vector different_name = {{"bar", mk_root('a')}}; + std::vector 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 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 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 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 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 empty; + REQUIRE(nt::MultigroupCodec::Build(backend, empty).empty()); + REQUIRE(nt::MultigroupCodec::List(backend, "").empty()); +} -- cgit v1.2.3