causal_test.go raw
1 package causal
2
3 import (
4 "testing"
5
6 "git.mleku.dev/mleku/dendrite/pkg/axiom"
7 )
8
9 type elem struct {
10 tag string
11 val string
12 }
13
14 func (e elem) Type() string { return e.tag }
15 func (e elem) Value() any { return e.val }
16
17 func TestExtractDefines(t *testing.T) {
18 tests := []struct {
19 src string
20 want []string
21 }{
22 {"x := 1", []string{"x"}},
23 {"x, y := foo()", []string{"x", "y"}},
24 {"var z int", []string{"z"}},
25 {"fmt.Println(x)", nil},
26 {"return x + 1", nil},
27 }
28 for _, tt := range tests {
29 defs := ExtractDefines(tt.src)
30 for _, w := range tt.want {
31 if !defs[w] {
32 t.Errorf("ExtractDefines(%q) missing %q", tt.src, w)
33 }
34 }
35 if tt.want == nil && len(defs) > 0 {
36 t.Errorf("ExtractDefines(%q) = %v, want empty", tt.src, defs)
37 }
38 }
39 }
40
41 func TestExtractUses(t *testing.T) {
42 tests := []struct {
43 src string
44 want []string
45 not []string
46 }{
47 {"x := foo()", []string{"foo"}, []string{"x"}},
48 {"fmt.Println(x)", []string{"fmt", "Println", "x"}, nil},
49 {"return x + 1", []string{"x"}, nil},
50 {"y := x + 1", []string{"x"}, []string{"y"}},
51 }
52 for _, tt := range tests {
53 uses := ExtractUses(tt.src)
54 for _, w := range tt.want {
55 if !uses[w] {
56 t.Errorf("ExtractUses(%q) missing %q", tt.src, w)
57 }
58 }
59 for _, n := range tt.not {
60 if uses[n] {
61 t.Errorf("ExtractUses(%q) should not contain %q", tt.src, n)
62 }
63 }
64 }
65 }
66
67 func TestCausalConstraintAdmitsInContext(t *testing.T) {
68 cc := CausalConstraint{ConstraintTag: "assign"}
69
70 // Element that defines x.
71 defX := elem{"assign", "main\x00x := 1"}
72 // Element that uses x.
73 useX := elem{"assign", "main\x00y := x + 1"}
74 // Element with no dependencies.
75 selfContained := elem{"assign", "main\x00a := 42"}
76
77 // With defX as neighbor, useX should be admitted.
78 neighbors := []axiom.Element{defX}
79 if !cc.AdmitsInContext(useX, neighbors) {
80 t.Error("should admit element when neighbor defines used identifier")
81 }
82
83 // Self-contained element should always be admitted.
84 if !cc.AdmitsInContext(selfContained, nil) {
85 t.Error("should admit self-contained element with no neighbors")
86 }
87
88 // Even with no matching neighbors, should still admit (lenient).
89 noMatchNeighbors := []axiom.Element{elem{"assign", "main\x00z := 99"}}
90 if !cc.AdmitsInContext(useX, noMatchNeighbors) {
91 t.Error("should admit element even without matching neighbor (lenient)")
92 }
93
94 // Wrong type should be rejected.
95 wrongType := elem{"return", "main\x00return x"}
96 if cc.AdmitsInContext(wrongType, neighbors) {
97 t.Error("should reject element with wrong type tag")
98 }
99 }
100
101 func TestScopeConstraint(t *testing.T) {
102 sc := ScopeConstraint{ConstraintTag: "assign"}
103
104 defX := elem{"assign", "main\x00x := 1"}
105 useX := elem{"assign", "main\x00y := x + 1"}
106 selfContained := elem{"assign", "main\x00a := 42"}
107 pureConsumer := elem{"expr", "main\x00fmt.Println(x)"}
108
109 // With defining neighbor, use should be admitted.
110 if !sc.AdmitsInContext(useX, []axiom.Element{defX}) {
111 t.Error("should admit when same-scope neighbor defines used identifier")
112 }
113
114 // Self-contained element (defines something) always admitted.
115 if !sc.AdmitsInContext(selfContained, []axiom.Element{defX}) {
116 t.Error("should admit self-contained element")
117 }
118
119 // No neighbors — seed bonding, always admitted.
120 if !sc.AdmitsInContext(useX, nil) {
121 t.Error("should admit with no neighbors (seed bonding)")
122 }
123
124 // Pure consumer with no matching definitions — rejected by ScopeConstraint.
125 scExpr := ScopeConstraint{ConstraintTag: "expr"}
126 unrelated := elem{"assign", "main\x00z := 99"}
127 if scExpr.AdmitsInContext(pureConsumer, []axiom.Element{unrelated}) {
128 t.Error("should reject pure consumer when no neighbor defines used identifiers")
129 }
130
131 // Different parent scope — should not count as scope neighbor.
132 diffScope := elem{"assign", "other\x00x := 1"}
133 if !sc.AdmitsInContext(useX, []axiom.Element{diffScope}) {
134 t.Error("should admit when only neighbors are from different scope (treated as no scope neighbor)")
135 }
136 }
137
138 func TestUniqueDefConstraint(t *testing.T) {
139 uc := UniqueDefConstraint{ConstraintTag: "assign"}
140
141 defX := elem{"assign", "main\x00x := 1"}
142 defXAgain := elem{"assign", "main\x00x := 2"}
143 defY := elem{"assign", "main\x00y := 3"}
144
145 // No conflict — different identifiers.
146 if !uc.AdmitsInContext(defY, []axiom.Element{defX}) {
147 t.Error("should admit when no duplicate definitions")
148 }
149
150 // Conflict — same identifier in same scope.
151 if uc.AdmitsInContext(defXAgain, []axiom.Element{defX}) {
152 t.Error("should reject duplicate definition of same identifier in same scope")
153 }
154
155 // Different scope — no conflict.
156 defXOther := elem{"assign", "other\x00x := 1"}
157 if !uc.AdmitsInContext(defXAgain, []axiom.Element{defXOther}) {
158 t.Error("should admit when duplicate is in different scope")
159 }
160
161 // No definitions — always admitted.
162 nodef := elem{"expr", "main\x00fmt.Println(x)"}
163 ucExpr := UniqueDefConstraint{ConstraintTag: "expr"}
164 if !ucExpr.AdmitsInContext(nodef, []axiom.Element{defX}) {
165 t.Error("should admit element with no definitions")
166 }
167
168 // No neighbors — always admitted.
169 if !uc.AdmitsInContext(defX, nil) {
170 t.Error("should admit with no neighbors")
171 }
172 }
173