| /* |
| Copyright 2018 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package garbagecollector |
| |
| import ( |
| "bytes" |
| "os" |
| "path/filepath" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/types" |
| "k8s.io/apimachinery/pkg/util/dump" |
| ) |
| |
| var ( |
| alphaNode = func() *node { |
| return &node{ |
| identity: objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("alpha"), |
| }, |
| }, |
| owners: []metav1.OwnerReference{ |
| {UID: types.UID("bravo")}, |
| {UID: types.UID("charlie")}, |
| }, |
| } |
| } |
| bravoNode = func() *node { |
| return &node{ |
| identity: objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("bravo"), |
| }, |
| }, |
| dependents: map[*node]struct{}{ |
| alphaNode(): {}, |
| }, |
| } |
| } |
| charlieNode = func() *node { |
| return &node{ |
| identity: objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("charlie"), |
| }, |
| }, |
| dependents: map[*node]struct{}{ |
| alphaNode(): {}, |
| }, |
| } |
| } |
| deltaNode = func() *node { |
| return &node{ |
| identity: objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("delta"), |
| }, |
| }, |
| owners: []metav1.OwnerReference{ |
| {UID: types.UID("foxtrot")}, |
| }, |
| } |
| } |
| echoNode = func() *node { |
| return &node{ |
| identity: objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("echo"), |
| }, |
| }, |
| } |
| } |
| foxtrotNode = func() *node { |
| return &node{ |
| identity: objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("foxtrot"), |
| }, |
| }, |
| owners: []metav1.OwnerReference{ |
| {UID: types.UID("golf")}, |
| }, |
| dependents: map[*node]struct{}{ |
| deltaNode(): {}, |
| }, |
| } |
| } |
| golfNode = func() *node { |
| return &node{ |
| identity: objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("golf"), |
| }, |
| }, |
| dependents: map[*node]struct{}{ |
| foxtrotNode(): {}, |
| }, |
| } |
| } |
| ) |
| |
| func TestToDOTGraph(t *testing.T) { |
| tests := []struct { |
| name string |
| uidToNode map[types.UID]*node |
| expectNodes []*dotVertex |
| expectEdges []dotEdge |
| }{ |
| { |
| name: "simple", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| }, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(alphaNode()), |
| NewDOTVertex(bravoNode()), |
| NewDOTVertex(charlieNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("alpha"), T: types.UID("bravo")}, |
| {F: types.UID("alpha"), T: types.UID("charlie")}, |
| }, |
| }, |
| { |
| name: "missing", // synthetic vertex created |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("charlie"): charlieNode(), |
| }, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(alphaNode()), |
| NewDOTVertex(bravoNode()), |
| NewDOTVertex(charlieNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("alpha"), T: types.UID("bravo")}, |
| {F: types.UID("alpha"), T: types.UID("charlie")}, |
| }, |
| }, |
| { |
| name: "drop-no-ref", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| types.UID("echo"): echoNode(), |
| }, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(alphaNode()), |
| NewDOTVertex(bravoNode()), |
| NewDOTVertex(charlieNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("alpha"), T: types.UID("bravo")}, |
| {F: types.UID("alpha"), T: types.UID("charlie")}, |
| }, |
| }, |
| { |
| name: "two-chains", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| types.UID("delta"): deltaNode(), |
| types.UID("foxtrot"): foxtrotNode(), |
| types.UID("golf"): golfNode(), |
| }, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(alphaNode()), |
| NewDOTVertex(bravoNode()), |
| NewDOTVertex(charlieNode()), |
| NewDOTVertex(deltaNode()), |
| NewDOTVertex(foxtrotNode()), |
| NewDOTVertex(golfNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("alpha"), T: types.UID("bravo")}, |
| {F: types.UID("alpha"), T: types.UID("charlie")}, |
| {F: types.UID("delta"), T: types.UID("foxtrot")}, |
| {F: types.UID("foxtrot"), T: types.UID("golf")}, |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| actualNodes, actualEdges := toDOTNodesAndEdges(test.uidToNode) |
| compareGraphs(test.expectNodes, actualNodes, test.expectEdges, actualEdges, t) |
| }) |
| } |
| } |
| |
| func TestToDOTGraphObj(t *testing.T) { |
| tests := []struct { |
| name string |
| uidToNode map[types.UID]*node |
| uids []types.UID |
| expectNodes []*dotVertex |
| expectEdges []dotEdge |
| }{ |
| { |
| name: "simple", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| }, |
| uids: []types.UID{types.UID("bravo")}, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(alphaNode()), |
| NewDOTVertex(bravoNode()), |
| NewDOTVertex(charlieNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("alpha"), T: types.UID("bravo")}, |
| {F: types.UID("alpha"), T: types.UID("charlie")}, |
| }, |
| }, |
| { |
| name: "missing", // synthetic vertex created |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("charlie"): charlieNode(), |
| }, |
| uids: []types.UID{types.UID("bravo")}, |
| expectNodes: []*dotVertex{}, |
| expectEdges: []dotEdge{}, |
| }, |
| { |
| name: "drop-no-ref", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| types.UID("echo"): echoNode(), |
| }, |
| uids: []types.UID{types.UID("echo")}, |
| expectNodes: []*dotVertex{}, |
| expectEdges: []dotEdge{}, |
| }, |
| { |
| name: "two-chains-from-owner", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| types.UID("delta"): deltaNode(), |
| types.UID("foxtrot"): foxtrotNode(), |
| types.UID("golf"): golfNode(), |
| }, |
| uids: []types.UID{types.UID("golf")}, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(deltaNode()), |
| NewDOTVertex(foxtrotNode()), |
| NewDOTVertex(golfNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("delta"), T: types.UID("foxtrot")}, |
| {F: types.UID("foxtrot"), T: types.UID("golf")}, |
| }, |
| }, |
| { |
| name: "two-chains-from-child", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| types.UID("delta"): deltaNode(), |
| types.UID("foxtrot"): foxtrotNode(), |
| types.UID("golf"): golfNode(), |
| }, |
| uids: []types.UID{types.UID("delta")}, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(deltaNode()), |
| NewDOTVertex(foxtrotNode()), |
| NewDOTVertex(golfNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("delta"), T: types.UID("foxtrot")}, |
| {F: types.UID("foxtrot"), T: types.UID("golf")}, |
| }, |
| }, |
| { |
| name: "two-chains-choose-both", |
| uidToNode: map[types.UID]*node{ |
| types.UID("alpha"): alphaNode(), |
| types.UID("bravo"): bravoNode(), |
| types.UID("charlie"): charlieNode(), |
| types.UID("delta"): deltaNode(), |
| types.UID("foxtrot"): foxtrotNode(), |
| types.UID("golf"): golfNode(), |
| }, |
| uids: []types.UID{types.UID("delta"), types.UID("charlie")}, |
| expectNodes: []*dotVertex{ |
| NewDOTVertex(alphaNode()), |
| NewDOTVertex(bravoNode()), |
| NewDOTVertex(charlieNode()), |
| NewDOTVertex(deltaNode()), |
| NewDOTVertex(foxtrotNode()), |
| NewDOTVertex(golfNode()), |
| }, |
| expectEdges: []dotEdge{ |
| {F: types.UID("alpha"), T: types.UID("bravo")}, |
| {F: types.UID("alpha"), T: types.UID("charlie")}, |
| {F: types.UID("delta"), T: types.UID("foxtrot")}, |
| {F: types.UID("foxtrot"), T: types.UID("golf")}, |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| actualNodes, actualEdges := toDOTNodesAndEdgesForObj(test.uidToNode, test.uids...) |
| compareGraphs(test.expectNodes, actualNodes, test.expectEdges, actualEdges, t) |
| }) |
| } |
| } |
| |
| func compareGraphs(expectedNodes, actualNodes []*dotVertex, expectedEdges, actualEdges []dotEdge, t *testing.T) { |
| if len(expectedNodes) != len(actualNodes) { |
| t.Fatal(dump.Pretty(actualNodes)) |
| } |
| for i := range expectedNodes { |
| currExpected := expectedNodes[i] |
| currActual := actualNodes[i] |
| if currExpected.uid != currActual.uid { |
| t.Errorf("expected %v, got %v", dump.Pretty(currExpected), dump.Pretty(currActual)) |
| } |
| } |
| if len(expectedEdges) != len(actualEdges) { |
| t.Fatal(dump.Pretty(actualEdges)) |
| } |
| for i := range expectedEdges { |
| currExpected := expectedEdges[i] |
| currActual := actualEdges[i] |
| if currExpected != currActual { |
| t.Errorf("expected %v, got %v", dump.Pretty(currExpected), dump.Pretty(currActual)) |
| } |
| } |
| } |
| |
| func TestMarshalDOT(t *testing.T) { |
| ref1 := objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("ref1-[]\"\\Iñtërnâtiônàlizætiøn,🐹"), |
| Name: "ref1name-Iñtërnâtiônàlizætiøn,🐹", |
| Kind: "ref1kind-Iñtërnâtiônàlizætiøn,🐹", |
| APIVersion: "ref1group/version", |
| }, |
| Namespace: "ref1ns", |
| } |
| ref2 := objectReference{ |
| OwnerReference: metav1.OwnerReference{ |
| UID: types.UID("ref2-"), |
| Name: "ref2name-", |
| Kind: "ref2kind-", |
| APIVersion: "ref2group/version", |
| }, |
| Namespace: "ref2ns", |
| } |
| testcases := []struct { |
| file string |
| nodes []*dotVertex |
| edges []dotEdge |
| }{ |
| { |
| file: "empty.dot", |
| }, |
| { |
| file: "simple.dot", |
| nodes: []*dotVertex{ |
| NewDOTVertex(alphaNode()), |
| NewDOTVertex(bravoNode()), |
| NewDOTVertex(charlieNode()), |
| NewDOTVertex(deltaNode()), |
| NewDOTVertex(foxtrotNode()), |
| NewDOTVertex(golfNode()), |
| }, |
| edges: []dotEdge{ |
| {F: types.UID("alpha"), T: types.UID("bravo")}, |
| {F: types.UID("alpha"), T: types.UID("charlie")}, |
| {F: types.UID("delta"), T: types.UID("foxtrot")}, |
| {F: types.UID("foxtrot"), T: types.UID("golf")}, |
| }, |
| }, |
| { |
| file: "escaping.dot", |
| nodes: []*dotVertex{ |
| NewDOTVertex(makeNode(ref1, withOwners(ref2))), |
| NewDOTVertex(makeNode(ref2)), |
| }, |
| edges: []dotEdge{ |
| {F: types.UID(ref1.UID), T: types.UID(ref2.UID)}, |
| }, |
| }, |
| } |
| |
| for _, tc := range testcases { |
| t.Run(tc.file, func(t *testing.T) { |
| goldenData, err := os.ReadFile(filepath.Join("testdata", tc.file)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| b := bytes.NewBuffer(nil) |
| if err := marshalDOT(b, tc.nodes, tc.edges); err != nil { |
| t.Fatal(err) |
| } |
| |
| if e, a := string(goldenData), string(b.Bytes()); cmp.Diff(e, a) != "" { |
| t.Logf("got\n%s", string(a)) |
| t.Fatalf("unexpected diff:\n%s", cmp.Diff(e, a)) |
| } |
| }) |
| } |
| } |