-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathhotspots_test.go
More file actions
299 lines (276 loc) · 10 KB
/
Copy pathhotspots_test.go
File metadata and controls
299 lines (276 loc) · 10 KB
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
package hotspots
import (
"context"
"fmt"
"strings"
"testing"
"github.com/enola-labs/enola/internal/facts"
)
// makeStore builds a hub symbol with the given fan-out (calls to fresh targets)
// and fan-in (callers calling the hub), plus the target/caller symbols.
func makeStore(hub string, fanIn, fanOut int) *facts.Store {
s := facts.NewStore()
calls := make([]facts.Relation, 0, fanOut)
for i := 0; i < fanOut; i++ {
tgt := fmt.Sprintf("dep/t%d.Fn", i)
calls = append(calls, facts.Relation{Kind: facts.RelCalls, Target: tgt})
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: tgt, File: "dep/t.go"})
}
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: hub, File: "core/hub.go", Relations: calls})
for i := 0; i < fanIn; i++ {
caller := fmt.Sprintf("caller/c%d.Fn", i)
s.Add(facts.Fact{
Kind: facts.KindSymbol,
Name: caller,
File: "caller/c.go",
Relations: []facts.Relation{{Kind: facts.RelCalls, Target: hub}},
})
}
s.BuildGraph()
return s
}
func TestExplain_NoGraph(t *testing.T) {
s := facts.NewStore()
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: "a.B"})
insights, err := New().Explain(context.Background(), s)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(insights) != 0 {
t.Errorf("expected 0 insights when graph is nil, got %d", len(insights))
}
}
func TestExplain_DetectsHotspot(t *testing.T) {
store := makeStore("core.Hub", 5, 5) // score 25, clear outlier vs the 0-score neighbors
insights, err := New().Explain(context.Background(), store)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(insights) != 1 {
t.Fatalf("expected 1 hotspot insight, got %d: %+v", len(insights), insights)
}
in := insights[0]
if !strings.Contains(in.Title, "core.Hub") {
t.Errorf("title %q should name the hub", in.Title)
}
if in.Evidence[0].Symbol != "core.Hub" {
t.Errorf("first evidence should be the hub, got %q", in.Evidence[0].Symbol)
}
}
// TestExplain_DedupReopenedSymbol: a constant reopened across many files yields
// one symbol fact per file, all sharing a Name and therefore identical in/out
// degree. Report the name once instead of once per reopening.
func TestExplain_DedupReopenedSymbol(t *testing.T) {
store := makeStore("core.Hub", 5, 5)
// Simulate the same constant reopened in 3 more files (no extra edges).
for i := 0; i < 3; i++ {
store.Add(facts.Fact{Kind: facts.KindSymbol, Name: "core.Hub", File: fmt.Sprintf("reopen/%d.go", i)})
}
store.BuildGraph()
insights, err := New().Explain(context.Background(), store)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(insights) != 1 {
t.Fatalf("reopened symbol should yield 1 hotspot insight, got %d: %+v", len(insights), insights)
}
}
// TestExplain_RubyBaseClassExcluded: a Rails base class (.rb) with high fan-in AND
// fan-out is still excluded from hotspots (its inbound degree is inheritance), while
// a same-degree domain type is reported.
func TestExplain_RubyBaseClassExcluded(t *testing.T) {
s := facts.NewStore()
addHub := func(name, file string) {
calls := make([]facts.Relation, 0, 4)
for i := 0; i < 4; i++ {
tgt := fmt.Sprintf("%s_dep%d.Fn", name, i)
calls = append(calls, facts.Relation{Kind: facts.RelCalls, Target: tgt})
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: tgt, File: "dep.rb"})
}
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: name, File: file, Relations: calls})
for i := 0; i < 4; i++ {
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: fmt.Sprintf("%s_caller%d.Fn", name, i),
File: "caller.rb", Relations: []facts.Relation{{Kind: facts.RelCalls, Target: name}}})
}
}
addHub("NotifierBase", "app/notifiers/notifier_base.rb")
addHub("User", "app/models/user.rb")
s.BuildGraph()
insights, err := New().Explain(context.Background(), s)
if err != nil {
t.Fatalf("Explain: %v", err)
}
var sawBase, sawUser bool
for _, in := range insights {
if strings.Contains(in.Title, "NotifierBase") {
sawBase = true
}
if strings.Contains(in.Title, "User") {
sawUser = true
}
}
if sawBase {
t.Errorf("framework base class should be excluded from hotspots")
}
if !sawUser {
t.Errorf("real domain hotspot User should still be reported")
}
}
func TestExplain_BelowDegreeFloor(t *testing.T) {
// High fan-out but fan-in below minDegree -> not a pinch point.
store := makeStore("core.Hub", 1, 10)
insights, err := New().Explain(context.Background(), store)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(insights) != 0 {
t.Errorf("expected 0 insights when one side is below the degree floor, got %d", len(insights))
}
}
// TestExplain_DegreeBoundary: exactly minDegree on both sides qualifies (it's a
// clear outlier against the zero-score neighbors); one side at minDegree-1 does not.
func TestExplain_DegreeBoundary(t *testing.T) {
at := makeStore("core.Hub", minDegree, minDegree)
got, err := New().Explain(context.Background(), at)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(got) != 1 {
t.Errorf("in==out==minDegree (%d) should be a hotspot, got %d insights", minDegree, len(got))
}
below := makeStore("core.Hub", minDegree-1, 10)
got, err = New().Explain(context.Background(), below)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(got) != 0 {
t.Errorf("fan-in == minDegree-1 (%d) should not qualify, got %d", minDegree-1, len(got))
}
}
// TestExplain_NeighborsCappedAndSorted: evidence is the hub plus up to
// maxNeighbors in-callers and maxNeighbors out-callees, each sorted.
func TestExplain_NeighborsCappedAndSorted(t *testing.T) {
store := makeStore("core.Hub", 8, 8) // more neighbors than the cap on each side
insights, err := New().Explain(context.Background(), store)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(insights) != 1 {
t.Fatalf("expected 1 insight, got %d", len(insights))
}
ev := insights[0].Evidence
// hub + up to maxNeighbors in + up to maxNeighbors out.
if len(ev) > 1+2*maxNeighbors {
t.Errorf("neighbor evidence not capped: got %d, want <= %d", len(ev), 1+2*maxNeighbors)
}
in, out := 0, 0
for _, e := range ev[1:] {
switch {
case strings.HasPrefix(e.Detail, "calls into"):
in++
case strings.HasPrefix(e.Detail, "called by"):
out++
}
}
if in > maxNeighbors || out > maxNeighbors {
t.Errorf("per-side cap exceeded: in=%d out=%d, cap=%d", in, out, maxNeighbors)
}
}
// TestExplain_ExcludesTestRefFanIn: a pinch point's fan-in counts only
// architectural (symbol) callers. test_ref/file_ref facts carry RelCalls edges
// into the hub but are not symbols; counting them inflates the centrality score
// and the outlier distribution (GAP-XL-15).
func TestExplain_ExcludesTestRefFanIn(t *testing.T) {
s := facts.NewStore()
const hub = "core.Hub"
// Fan-out: hub calls 5 targets.
calls := make([]facts.Relation, 0, 5)
for i := 0; i < 5; i++ {
tgt := fmt.Sprintf("dep/t%d.Fn", i)
calls = append(calls, facts.Relation{Kind: facts.RelCalls, Target: tgt})
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: tgt, File: "dep/t.go"})
}
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: hub, File: "core/hub.go", Relations: calls})
// 4 real symbol callers (fan-in) — clears minDegree on symbols alone.
for i := 0; i < 4; i++ {
s.Add(facts.Fact{
Kind: facts.KindSymbol, Name: fmt.Sprintf("caller/c%d.Fn", i),
File: "caller/c.go",
Relations: []facts.Relation{{Kind: facts.RelCalls, Target: hub}},
})
}
// 3 reference-only callers that must not count toward fan-in.
for i := 0; i < 2; i++ {
s.Add(facts.Fact{
Kind: facts.KindTestRef, Name: fmt.Sprintf("spec/c%d_spec.rb", i),
File: fmt.Sprintf("spec/c%d_spec.rb", i),
Relations: []facts.Relation{{Kind: facts.RelCalls, Target: hub}},
})
}
s.Add(facts.Fact{
Kind: facts.KindFileRef, Name: "config/init.rb", File: "config/init.rb",
Relations: []facts.Relation{{Kind: facts.RelCalls, Target: hub}},
})
s.BuildGraph()
insights, err := New().Explain(context.Background(), s)
if err != nil {
t.Fatalf("Explain: %v", err)
}
var hubInsight *facts.Insight
for i := range insights {
if strings.Contains(insights[i].Title, hub) {
hubInsight = &insights[i]
break
}
}
if hubInsight == nil {
t.Fatalf("hub %q not reported as a hotspot; got %v", hub, insights)
}
// Fan-in must be 4 (symbols), not 7 (symbols + 2 test_ref + 1 file_ref).
if !strings.Contains(hubInsight.Title, "fan-in 4") {
t.Errorf("fan-in should exclude reference-only facts; title = %q, want fan-in 4", hubInsight.Title)
}
if strings.Contains(hubInsight.Title, "fan-in 7") {
t.Errorf("fan-in wrongly includes test_ref/file_ref facts; title = %q", hubInsight.Title)
}
// No spec file or initializer should appear as an in-caller in the evidence.
for _, ev := range hubInsight.Evidence[1:] {
if strings.HasPrefix(ev.Symbol, "spec/") || strings.HasPrefix(ev.Symbol, "config/") {
t.Errorf("reference-only fact %q leaked into hotspot evidence", ev.Symbol)
}
}
}
// TestExplain_OrderedByScore: the higher fanIn×fanOut hotspot ranks first.
func TestExplain_OrderedByScore(t *testing.T) {
s := facts.NewStore()
// Big: 6x6 = 36; Small: 3x3 = 9. Give each disjoint callers/callees.
addHub := func(name, dir string, in, out int) {
calls := make([]facts.Relation, 0, out)
for i := 0; i < out; i++ {
tgt := fmt.Sprintf("%s/t%d.Fn", dir, i)
calls = append(calls, facts.Relation{Kind: facts.RelCalls, Target: tgt})
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: tgt, File: dir + "/t.go"})
}
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: name, File: dir + "/h.go", Relations: calls})
for i := 0; i < in; i++ {
s.Add(facts.Fact{Kind: facts.KindSymbol, Name: fmt.Sprintf("%s/c%d.Fn", dir, i), File: dir + "/c.go",
Relations: []facts.Relation{{Kind: facts.RelCalls, Target: name}}})
}
}
addHub("big.Hub", "big", 6, 6)
addHub("small.Hub", "small", 3, 3)
s.BuildGraph()
insights, err := New().Explain(context.Background(), s)
if err != nil {
t.Fatalf("Explain: %v", err)
}
if len(insights) < 1 || !strings.Contains(insights[0].Title, "big.Hub") {
t.Errorf("highest-score hotspot should rank first, got %v", func() []string {
out := make([]string, len(insights))
for i, in := range insights {
out[i] = in.Title
}
return out
}())
}
}