1
16
17 package garbagecollector
18
19 import (
20 "bytes"
21 "os"
22 "path/filepath"
23 "testing"
24
25 "github.com/google/go-cmp/cmp"
26
27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28 "k8s.io/apimachinery/pkg/types"
29 "k8s.io/apimachinery/pkg/util/dump"
30 )
31
32 var (
33 alphaNode = func() *node {
34 return &node{
35 identity: objectReference{
36 OwnerReference: metav1.OwnerReference{
37 UID: types.UID("alpha"),
38 },
39 },
40 owners: []metav1.OwnerReference{
41 {UID: types.UID("bravo")},
42 {UID: types.UID("charlie")},
43 },
44 }
45 }
46 bravoNode = func() *node {
47 return &node{
48 identity: objectReference{
49 OwnerReference: metav1.OwnerReference{
50 UID: types.UID("bravo"),
51 },
52 },
53 dependents: map[*node]struct{}{
54 alphaNode(): {},
55 },
56 }
57 }
58 charlieNode = func() *node {
59 return &node{
60 identity: objectReference{
61 OwnerReference: metav1.OwnerReference{
62 UID: types.UID("charlie"),
63 },
64 },
65 dependents: map[*node]struct{}{
66 alphaNode(): {},
67 },
68 }
69 }
70 deltaNode = func() *node {
71 return &node{
72 identity: objectReference{
73 OwnerReference: metav1.OwnerReference{
74 UID: types.UID("delta"),
75 },
76 },
77 owners: []metav1.OwnerReference{
78 {UID: types.UID("foxtrot")},
79 },
80 }
81 }
82 echoNode = func() *node {
83 return &node{
84 identity: objectReference{
85 OwnerReference: metav1.OwnerReference{
86 UID: types.UID("echo"),
87 },
88 },
89 }
90 }
91 foxtrotNode = func() *node {
92 return &node{
93 identity: objectReference{
94 OwnerReference: metav1.OwnerReference{
95 UID: types.UID("foxtrot"),
96 },
97 },
98 owners: []metav1.OwnerReference{
99 {UID: types.UID("golf")},
100 },
101 dependents: map[*node]struct{}{
102 deltaNode(): {},
103 },
104 }
105 }
106 golfNode = func() *node {
107 return &node{
108 identity: objectReference{
109 OwnerReference: metav1.OwnerReference{
110 UID: types.UID("golf"),
111 },
112 },
113 dependents: map[*node]struct{}{
114 foxtrotNode(): {},
115 },
116 }
117 }
118 )
119
120 func TestToDOTGraph(t *testing.T) {
121 tests := []struct {
122 name string
123 uidToNode map[types.UID]*node
124 expectNodes []*dotVertex
125 expectEdges []dotEdge
126 }{
127 {
128 name: "simple",
129 uidToNode: map[types.UID]*node{
130 types.UID("alpha"): alphaNode(),
131 types.UID("bravo"): bravoNode(),
132 types.UID("charlie"): charlieNode(),
133 },
134 expectNodes: []*dotVertex{
135 NewDOTVertex(alphaNode()),
136 NewDOTVertex(bravoNode()),
137 NewDOTVertex(charlieNode()),
138 },
139 expectEdges: []dotEdge{
140 {F: types.UID("alpha"), T: types.UID("bravo")},
141 {F: types.UID("alpha"), T: types.UID("charlie")},
142 },
143 },
144 {
145 name: "missing",
146 uidToNode: map[types.UID]*node{
147 types.UID("alpha"): alphaNode(),
148 types.UID("charlie"): charlieNode(),
149 },
150 expectNodes: []*dotVertex{
151 NewDOTVertex(alphaNode()),
152 NewDOTVertex(bravoNode()),
153 NewDOTVertex(charlieNode()),
154 },
155 expectEdges: []dotEdge{
156 {F: types.UID("alpha"), T: types.UID("bravo")},
157 {F: types.UID("alpha"), T: types.UID("charlie")},
158 },
159 },
160 {
161 name: "drop-no-ref",
162 uidToNode: map[types.UID]*node{
163 types.UID("alpha"): alphaNode(),
164 types.UID("bravo"): bravoNode(),
165 types.UID("charlie"): charlieNode(),
166 types.UID("echo"): echoNode(),
167 },
168 expectNodes: []*dotVertex{
169 NewDOTVertex(alphaNode()),
170 NewDOTVertex(bravoNode()),
171 NewDOTVertex(charlieNode()),
172 },
173 expectEdges: []dotEdge{
174 {F: types.UID("alpha"), T: types.UID("bravo")},
175 {F: types.UID("alpha"), T: types.UID("charlie")},
176 },
177 },
178 {
179 name: "two-chains",
180 uidToNode: map[types.UID]*node{
181 types.UID("alpha"): alphaNode(),
182 types.UID("bravo"): bravoNode(),
183 types.UID("charlie"): charlieNode(),
184 types.UID("delta"): deltaNode(),
185 types.UID("foxtrot"): foxtrotNode(),
186 types.UID("golf"): golfNode(),
187 },
188 expectNodes: []*dotVertex{
189 NewDOTVertex(alphaNode()),
190 NewDOTVertex(bravoNode()),
191 NewDOTVertex(charlieNode()),
192 NewDOTVertex(deltaNode()),
193 NewDOTVertex(foxtrotNode()),
194 NewDOTVertex(golfNode()),
195 },
196 expectEdges: []dotEdge{
197 {F: types.UID("alpha"), T: types.UID("bravo")},
198 {F: types.UID("alpha"), T: types.UID("charlie")},
199 {F: types.UID("delta"), T: types.UID("foxtrot")},
200 {F: types.UID("foxtrot"), T: types.UID("golf")},
201 },
202 },
203 }
204
205 for _, test := range tests {
206 t.Run(test.name, func(t *testing.T) {
207 actualNodes, actualEdges := toDOTNodesAndEdges(test.uidToNode)
208 compareGraphs(test.expectNodes, actualNodes, test.expectEdges, actualEdges, t)
209 })
210 }
211 }
212
213 func TestToDOTGraphObj(t *testing.T) {
214 tests := []struct {
215 name string
216 uidToNode map[types.UID]*node
217 uids []types.UID
218 expectNodes []*dotVertex
219 expectEdges []dotEdge
220 }{
221 {
222 name: "simple",
223 uidToNode: map[types.UID]*node{
224 types.UID("alpha"): alphaNode(),
225 types.UID("bravo"): bravoNode(),
226 types.UID("charlie"): charlieNode(),
227 },
228 uids: []types.UID{types.UID("bravo")},
229 expectNodes: []*dotVertex{
230 NewDOTVertex(alphaNode()),
231 NewDOTVertex(bravoNode()),
232 NewDOTVertex(charlieNode()),
233 },
234 expectEdges: []dotEdge{
235 {F: types.UID("alpha"), T: types.UID("bravo")},
236 {F: types.UID("alpha"), T: types.UID("charlie")},
237 },
238 },
239 {
240 name: "missing",
241 uidToNode: map[types.UID]*node{
242 types.UID("alpha"): alphaNode(),
243 types.UID("charlie"): charlieNode(),
244 },
245 uids: []types.UID{types.UID("bravo")},
246 expectNodes: []*dotVertex{},
247 expectEdges: []dotEdge{},
248 },
249 {
250 name: "drop-no-ref",
251 uidToNode: map[types.UID]*node{
252 types.UID("alpha"): alphaNode(),
253 types.UID("bravo"): bravoNode(),
254 types.UID("charlie"): charlieNode(),
255 types.UID("echo"): echoNode(),
256 },
257 uids: []types.UID{types.UID("echo")},
258 expectNodes: []*dotVertex{},
259 expectEdges: []dotEdge{},
260 },
261 {
262 name: "two-chains-from-owner",
263 uidToNode: map[types.UID]*node{
264 types.UID("alpha"): alphaNode(),
265 types.UID("bravo"): bravoNode(),
266 types.UID("charlie"): charlieNode(),
267 types.UID("delta"): deltaNode(),
268 types.UID("foxtrot"): foxtrotNode(),
269 types.UID("golf"): golfNode(),
270 },
271 uids: []types.UID{types.UID("golf")},
272 expectNodes: []*dotVertex{
273 NewDOTVertex(deltaNode()),
274 NewDOTVertex(foxtrotNode()),
275 NewDOTVertex(golfNode()),
276 },
277 expectEdges: []dotEdge{
278 {F: types.UID("delta"), T: types.UID("foxtrot")},
279 {F: types.UID("foxtrot"), T: types.UID("golf")},
280 },
281 },
282 {
283 name: "two-chains-from-child",
284 uidToNode: map[types.UID]*node{
285 types.UID("alpha"): alphaNode(),
286 types.UID("bravo"): bravoNode(),
287 types.UID("charlie"): charlieNode(),
288 types.UID("delta"): deltaNode(),
289 types.UID("foxtrot"): foxtrotNode(),
290 types.UID("golf"): golfNode(),
291 },
292 uids: []types.UID{types.UID("delta")},
293 expectNodes: []*dotVertex{
294 NewDOTVertex(deltaNode()),
295 NewDOTVertex(foxtrotNode()),
296 NewDOTVertex(golfNode()),
297 },
298 expectEdges: []dotEdge{
299 {F: types.UID("delta"), T: types.UID("foxtrot")},
300 {F: types.UID("foxtrot"), T: types.UID("golf")},
301 },
302 },
303 {
304 name: "two-chains-choose-both",
305 uidToNode: map[types.UID]*node{
306 types.UID("alpha"): alphaNode(),
307 types.UID("bravo"): bravoNode(),
308 types.UID("charlie"): charlieNode(),
309 types.UID("delta"): deltaNode(),
310 types.UID("foxtrot"): foxtrotNode(),
311 types.UID("golf"): golfNode(),
312 },
313 uids: []types.UID{types.UID("delta"), types.UID("charlie")},
314 expectNodes: []*dotVertex{
315 NewDOTVertex(alphaNode()),
316 NewDOTVertex(bravoNode()),
317 NewDOTVertex(charlieNode()),
318 NewDOTVertex(deltaNode()),
319 NewDOTVertex(foxtrotNode()),
320 NewDOTVertex(golfNode()),
321 },
322 expectEdges: []dotEdge{
323 {F: types.UID("alpha"), T: types.UID("bravo")},
324 {F: types.UID("alpha"), T: types.UID("charlie")},
325 {F: types.UID("delta"), T: types.UID("foxtrot")},
326 {F: types.UID("foxtrot"), T: types.UID("golf")},
327 },
328 },
329 }
330
331 for _, test := range tests {
332 t.Run(test.name, func(t *testing.T) {
333 actualNodes, actualEdges := toDOTNodesAndEdgesForObj(test.uidToNode, test.uids...)
334 compareGraphs(test.expectNodes, actualNodes, test.expectEdges, actualEdges, t)
335 })
336 }
337 }
338
339 func compareGraphs(expectedNodes, actualNodes []*dotVertex, expectedEdges, actualEdges []dotEdge, t *testing.T) {
340 if len(expectedNodes) != len(actualNodes) {
341 t.Fatal(dump.Pretty(actualNodes))
342 }
343 for i := range expectedNodes {
344 currExpected := expectedNodes[i]
345 currActual := actualNodes[i]
346 if currExpected.uid != currActual.uid {
347 t.Errorf("expected %v, got %v", dump.Pretty(currExpected), dump.Pretty(currActual))
348 }
349 }
350 if len(expectedEdges) != len(actualEdges) {
351 t.Fatal(dump.Pretty(actualEdges))
352 }
353 for i := range expectedEdges {
354 currExpected := expectedEdges[i]
355 currActual := actualEdges[i]
356 if currExpected != currActual {
357 t.Errorf("expected %v, got %v", dump.Pretty(currExpected), dump.Pretty(currActual))
358 }
359 }
360 }
361
362 func TestMarshalDOT(t *testing.T) {
363 ref1 := objectReference{
364 OwnerReference: metav1.OwnerReference{
365 UID: types.UID("ref1-[]\"\\Iñtërnâtiônàlizætiøn,🐹"),
366 Name: "ref1name-Iñtërnâtiônàlizætiøn,🐹",
367 Kind: "ref1kind-Iñtërnâtiônàlizætiøn,🐹",
368 APIVersion: "ref1group/version",
369 },
370 Namespace: "ref1ns",
371 }
372 ref2 := objectReference{
373 OwnerReference: metav1.OwnerReference{
374 UID: types.UID("ref2-"),
375 Name: "ref2name-",
376 Kind: "ref2kind-",
377 APIVersion: "ref2group/version",
378 },
379 Namespace: "ref2ns",
380 }
381 testcases := []struct {
382 file string
383 nodes []*dotVertex
384 edges []dotEdge
385 }{
386 {
387 file: "empty.dot",
388 },
389 {
390 file: "simple.dot",
391 nodes: []*dotVertex{
392 NewDOTVertex(alphaNode()),
393 NewDOTVertex(bravoNode()),
394 NewDOTVertex(charlieNode()),
395 NewDOTVertex(deltaNode()),
396 NewDOTVertex(foxtrotNode()),
397 NewDOTVertex(golfNode()),
398 },
399 edges: []dotEdge{
400 {F: types.UID("alpha"), T: types.UID("bravo")},
401 {F: types.UID("alpha"), T: types.UID("charlie")},
402 {F: types.UID("delta"), T: types.UID("foxtrot")},
403 {F: types.UID("foxtrot"), T: types.UID("golf")},
404 },
405 },
406 {
407 file: "escaping.dot",
408 nodes: []*dotVertex{
409 NewDOTVertex(makeNode(ref1, withOwners(ref2))),
410 NewDOTVertex(makeNode(ref2)),
411 },
412 edges: []dotEdge{
413 {F: types.UID(ref1.UID), T: types.UID(ref2.UID)},
414 },
415 },
416 }
417
418 for _, tc := range testcases {
419 t.Run(tc.file, func(t *testing.T) {
420 goldenData, err := os.ReadFile(filepath.Join("testdata", tc.file))
421 if err != nil {
422 t.Fatal(err)
423 }
424 b := bytes.NewBuffer(nil)
425 if err := marshalDOT(b, tc.nodes, tc.edges); err != nil {
426 t.Fatal(err)
427 }
428
429 if e, a := string(goldenData), string(b.Bytes()); cmp.Diff(e, a) != "" {
430 t.Logf("got\n%s", string(a))
431 t.Fatalf("unexpected diff:\n%s", cmp.Diff(e, a))
432 }
433 })
434 }
435 }
436
View as plain text