diff options
| author | Marcos Magueta <maguetamarcos@gmail.com> | 2026-05-18 12:50:25 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-18 12:50:25 -0300 |
| commit | af8759a9c1d965a3220024d1857aea219d62d898 (patch) | |
| tree | 9cf14af855cac3674f508ec001bd04e262a524ae /test/MultigroupCodec.cpp | |
| parent | 0fc76b99abbff1d3cef8e2f9fcd56f3ae4f23a7a (diff) | |
| download | RNT-af8759a9c1d965a3220024d1857aea219d62d898.tar.gz RNT-af8759a9c1d965a3220024d1857aea219d62d898.zip | |
Branching and object registry rework (#11)
Diffstat (limited to '')
| -rw-r--r-- | test/MultigroupCodec.cpp | 145 |
1 files changed, 145 insertions, 0 deletions
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 <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()); +} |
