aboutsummaryrefslogtreecommitdiffstats
path: root/test/MultigroupCodec.cpp
blob: b04c405e7fa3e50c964e9e5f7b33392951f94ac0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#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());
}