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