k8s.io/kubernetes@v1.29.3/pkg/controller/garbagecollector/dump_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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", // synthetic vertex created 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", // synthetic vertex created 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 }