spore_test.go raw
1 package spore
2
3 import (
4 "bytes"
5 "testing"
6
7 "git.mleku.dev/mleku/dendrite/pkg/axiom"
8 "git.mleku.dev/mleku/dendrite/pkg/enzyme"
9 "git.mleku.dev/mleku/dendrite/pkg/lattice"
10 )
11
12 type tagConstraint struct{ tag string }
13
14 func (c tagConstraint) Tag() string { return c.tag }
15 func (c tagConstraint) Admits(e axiom.Element) bool { return e.Type() == c.tag }
16
17 func makeLattice() *lattice.Lattice {
18 l := lattice.New()
19
20 // 10 word sites, 3 punct sites.
21 words := make([]*lattice.Node, 10)
22 for i := range words {
23 words[i] = l.AddNode([]axiom.Constraint{tagConstraint{"word"}})
24 }
25 puncts := make([]*lattice.Node, 3)
26 for i := range puncts {
27 puncts[i] = l.AddNode([]axiom.Constraint{tagConstraint{"punct"}})
28 }
29
30 // Connect.
31 for i := range words {
32 l.Connect(words[i], words[(i+1)%len(words)])
33 }
34 for i := range puncts {
35 l.Connect(puncts[i], puncts[(i+1)%len(puncts)])
36 }
37 l.Connect(words[0], puncts[0])
38
39 // Bond some elements.
40 words[0].Bond(enzyme.Elem("word", "hello"))
41 words[1].Bond(enzyme.Elem("word", "world"))
42 puncts[0].Bond(enzyme.Elem("punct", "!"))
43
44 return l
45 }
46
47 func TestExtract(t *testing.T) {
48 l := makeLattice()
49 s := Extract(l)
50
51 if s.TotalNodes != 13 {
52 t.Errorf("expected 13 nodes, got %d", s.TotalNodes)
53 }
54 if tagCountValue(s.TypeSignature, "word") != 10 {
55 t.Errorf("expected 10 word constraints, got %d", tagCountValue(s.TypeSignature, "word"))
56 }
57 if tagCountValue(s.TypeSignature, "punct") != 3 {
58 t.Errorf("expected 3 punct constraints, got %d", tagCountValue(s.TypeSignature, "punct"))
59 }
60 if tagCountValue(s.ElementTypes, "word") != 2 {
61 t.Errorf("expected 2 word elements, got %d", tagCountValue(s.ElementTypes, "word"))
62 }
63 if s.Occupied != 3 {
64 t.Errorf("expected 3 occupied, got %d", s.Occupied)
65 }
66
67 t.Log(s.String())
68 }
69
70 func TestNucleate(t *testing.T) {
71 l := makeLattice()
72 s := Extract(l)
73
74 // Nucleate a new lattice of size 50.
75 l2 := s.Nucleate(50, func(tag string) axiom.Constraint {
76 return tagConstraint{tag}
77 })
78
79 if l2.Size() < 40 || l2.Size() > 55 {
80 t.Errorf("expected ~50 nodes, got %d", l2.Size())
81 }
82
83 // Check that type proportions are roughly preserved.
84 wordCount := 0
85 punctCount := 0
86 for _, n := range l2.Nodes() {
87 cs := n.Constraints()
88 if len(cs) > 0 {
89 switch cs[0].Tag() {
90 case "word":
91 wordCount++
92 case "punct":
93 punctCount++
94 }
95 }
96 }
97
98 t.Logf("nucleated: %d word, %d punct (total %d)", wordCount, punctCount, l2.Size())
99
100 // Word should dominate (10:3 ratio in source).
101 if wordCount < punctCount {
102 t.Error("word count should be greater than punct count")
103 }
104 }
105
106 func TestSerializeRoundTrip(t *testing.T) {
107 l := makeLattice()
108 s := Extract(l)
109
110 var buf bytes.Buffer
111 _, err := s.WriteTo(&buf)
112 if err != nil {
113 t.Fatal(err)
114 }
115
116 s2, err := ReadSpore(&buf)
117 if err != nil {
118 t.Fatal(err)
119 }
120
121 if s2.TotalNodes != s.TotalNodes {
122 t.Errorf("total nodes mismatch: %d vs %d", s2.TotalNodes, s.TotalNodes)
123 }
124 if tagCountValue(s2.TypeSignature, "word") != tagCountValue(s.TypeSignature, "word") {
125 t.Error("word count mismatch after roundtrip")
126 }
127 }
128
129 func TestLineage(t *testing.T) {
130 l := makeLattice()
131 gen0 := Extract(l)
132
133 if gen0.Generation != 0 {
134 t.Errorf("gen0 should be generation 0, got %d", gen0.Generation)
135 }
136 if gen0.ParentHash != "" {
137 t.Error("gen0 should have no parent hash")
138 }
139
140 h0 := gen0.Hash()
141 if len(h0) != 64 {
142 t.Errorf("hash should be 64 hex chars, got %d", len(h0))
143 }
144
145 // Nucleate daughter, feed it, sporulate with lineage.
146 daughter := gen0.Nucleate(20, func(tag string) axiom.Constraint {
147 return tagConstraint{tag}
148 })
149 daughter.Nodes()[0].Bond(enzyme.Elem("word", "test"))
150
151 gen1 := Extract(daughter, gen0)
152
153 if gen1.Generation != 1 {
154 t.Errorf("gen1 should be generation 1, got %d", gen1.Generation)
155 }
156 if gen1.ParentHash != h0 {
157 t.Error("gen1 parent hash should match gen0 hash")
158 }
159
160 // Second generation.
161 d2 := gen1.Nucleate(30, func(tag string) axiom.Constraint {
162 return tagConstraint{tag}
163 })
164 gen2 := Extract(d2, gen1)
165
166 if gen2.Generation != 2 {
167 t.Errorf("gen2 should be generation 2, got %d", gen2.Generation)
168 }
169 if gen2.ParentHash != gen1.Hash() {
170 t.Error("gen2 parent hash should match gen1 hash")
171 }
172
173 t.Logf("lineage: gen0=%s...", h0[:16])
174 t.Logf(" gen1=%s... (parent=%s...)", gen1.Hash()[:16], gen1.ParentHash[:16])
175 t.Logf(" gen2=%s... (parent=%s...)", gen2.Hash()[:16], gen2.ParentHash[:16])
176 }
177