diff --git a/pkg/controller/garbagecollector/dump.go b/pkg/controller/garbagecollector/dump.go index d9f4cc5d25c..1943a7f1fa3 100644 --- a/pkg/controller/garbagecollector/dump.go +++ b/pkg/controller/garbagecollector/dump.go @@ -17,22 +17,20 @@ limitations under the License. package garbagecollector import ( + "bytes" "fmt" + "io" "net/http" + "sort" "strings" - "gonum.org/v1/gonum/graph" - "gonum.org/v1/gonum/graph/encoding" - "gonum.org/v1/gonum/graph/encoding/dot" - "gonum.org/v1/gonum/graph/simple" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) -type gonumVertex struct { +type dotVertex struct { uid types.UID gvk schema.GroupVersionKind namespace string @@ -41,14 +39,25 @@ type gonumVertex struct { beingDeleted bool deletingDependents bool virtual bool - vertexID int64 } -func (v *gonumVertex) ID() int64 { - return v.vertexID +func (v *dotVertex) MarshalDOT(w io.Writer) error { + attrs := v.Attributes() + if _, err := fmt.Fprintf(w, " %q [\n", v.uid); err != nil { + return err + } + for _, a := range attrs { + if _, err := fmt.Fprintf(w, " %s=%q\n", a.Key, a.Value); err != nil { + return err + } + } + if _, err := fmt.Fprintf(w, " ];\n"); err != nil { + return err + } + return nil } -func (v *gonumVertex) String() string { +func (v *dotVertex) String() string { kind := v.gvk.Kind + "." + v.gvk.Version if len(v.gvk.Group) > 0 { kind = kind + "." + v.gvk.Group @@ -72,7 +81,12 @@ func (v *gonumVertex) String() string { return fmt.Sprintf(`%s/%s[%s]-%v%s%s%s%s`, kind, v.name, v.namespace, v.uid, missing, deleting, deletingDependents, virtual) } -func (v *gonumVertex) Attributes() []encoding.Attribute { +type attribute struct { + Key string + Value string +} + +func (v *dotVertex) Attributes() []attribute { kubectlString := v.gvk.Kind + "." + v.gvk.Version if len(v.gvk.Group) > 0 { kubectlString = kubectlString + "." + v.gvk.Group @@ -106,30 +120,30 @@ namespace=%v label = label + conditionString + "\n" } - return []encoding.Attribute{ - {Key: "label", Value: fmt.Sprintf(`"%v"`, label)}, + return []attribute{ + {Key: "label", Value: label}, // these place metadata in the correct location, but don't conform to any normal attribute for rendering - {Key: "group", Value: fmt.Sprintf(`"%v"`, v.gvk.Group)}, - {Key: "version", Value: fmt.Sprintf(`"%v"`, v.gvk.Version)}, - {Key: "kind", Value: fmt.Sprintf(`"%v"`, v.gvk.Kind)}, - {Key: "namespace", Value: fmt.Sprintf(`"%v"`, v.namespace)}, - {Key: "name", Value: fmt.Sprintf(`"%v"`, v.name)}, - {Key: "uid", Value: fmt.Sprintf(`"%v"`, v.uid)}, - {Key: "missing", Value: fmt.Sprintf(`"%v"`, v.missingFromGraph)}, - {Key: "beingDeleted", Value: fmt.Sprintf(`"%v"`, v.beingDeleted)}, - {Key: "deletingDependents", Value: fmt.Sprintf(`"%v"`, v.deletingDependents)}, - {Key: "virtual", Value: fmt.Sprintf(`"%v"`, v.virtual)}, + {Key: "group", Value: v.gvk.Group}, + {Key: "version", Value: v.gvk.Version}, + {Key: "kind", Value: v.gvk.Kind}, + {Key: "namespace", Value: v.namespace}, + {Key: "name", Value: v.name}, + {Key: "uid", Value: string(v.uid)}, + {Key: "missing", Value: fmt.Sprintf(`%v`, v.missingFromGraph)}, + {Key: "beingDeleted", Value: fmt.Sprintf(`%v`, v.beingDeleted)}, + {Key: "deletingDependents", Value: fmt.Sprintf(`%v`, v.deletingDependents)}, + {Key: "virtual", Value: fmt.Sprintf(`%v`, v.virtual)}, } } -// NewGonumVertex creates a new gonumVertex. -func NewGonumVertex(node *node, nodeID int64) *gonumVertex { +// NewDOTVertex creates a new dotVertex. +func NewDOTVertex(node *node) *dotVertex { gv, err := schema.ParseGroupVersion(node.identity.APIVersion) if err != nil { // this indicates a bad data serialization that should be prevented during storage of the API utilruntime.HandleError(err) } - return &gonumVertex{ + return &dotVertex{ uid: node.identity.UID, gvk: gv.WithKind(node.identity.Kind), namespace: node.identity.Namespace, @@ -137,36 +151,46 @@ func NewGonumVertex(node *node, nodeID int64) *gonumVertex { beingDeleted: node.beingDeleted, deletingDependents: node.deletingDependents, virtual: node.virtual, - vertexID: nodeID, } } -// NewMissingGonumVertex creates a new gonumVertex. -func NewMissingGonumVertex(ownerRef metav1.OwnerReference, nodeID int64) *gonumVertex { +// NewMissingdotVertex creates a new dotVertex. +func NewMissingdotVertex(ownerRef metav1.OwnerReference) *dotVertex { gv, err := schema.ParseGroupVersion(ownerRef.APIVersion) if err != nil { // this indicates a bad data serialization that should be prevented during storage of the API utilruntime.HandleError(err) } - return &gonumVertex{ + return &dotVertex{ uid: ownerRef.UID, gvk: gv.WithKind(ownerRef.Kind), name: ownerRef.Name, missingFromGraph: true, - vertexID: nodeID, } } -func (m *concurrentUIDToNode) ToGonumGraph() graph.Directed { +func (m *concurrentUIDToNode) ToDOTNodesAndEdges() ([]*dotVertex, []dotEdge) { m.uidToNodeLock.Lock() defer m.uidToNodeLock.Unlock() - return toGonumGraph(m.uidToNode) + return toDOTNodesAndEdges(m.uidToNode) } -func toGonumGraph(uidToNode map[types.UID]*node) graph.Directed { - uidToVertex := map[types.UID]*gonumVertex{} - graphBuilder := simple.NewDirectedGraph() +type dotEdge struct { + F types.UID + T types.UID +} + +func (e dotEdge) MarshalDOT(w io.Writer) error { + _, err := fmt.Fprintf(w, " %q -> %q;\n", e.F, e.T) + return err +} + +func toDOTNodesAndEdges(uidToNode map[types.UID]*node) ([]*dotVertex, []dotEdge) { + nodes := []*dotVertex{} + edges := []dotEdge{} + + uidToVertex := map[types.UID]*dotVertex{} // add the vertices first, then edges. That avoids having to deal with missing refs. for _, node := range uidToNode { @@ -174,38 +198,42 @@ func toGonumGraph(uidToNode map[types.UID]*node) graph.Directed { if len(node.dependents) == 0 && len(node.owners) == 0 { continue } - vertex := NewGonumVertex(node, graphBuilder.NewNode().ID()) + vertex := NewDOTVertex(node) uidToVertex[node.identity.UID] = vertex - graphBuilder.AddNode(vertex) + nodes = append(nodes, vertex) } for _, node := range uidToNode { currVertex := uidToVertex[node.identity.UID] for _, ownerRef := range node.owners { currOwnerVertex, ok := uidToVertex[ownerRef.UID] if !ok { - currOwnerVertex = NewMissingGonumVertex(ownerRef, graphBuilder.NewNode().ID()) + currOwnerVertex = NewMissingdotVertex(ownerRef) uidToVertex[node.identity.UID] = currOwnerVertex - graphBuilder.AddNode(currOwnerVertex) + nodes = append(nodes, currOwnerVertex) } - graphBuilder.SetEdge(simple.Edge{ - F: currVertex, - T: currOwnerVertex, - }) - + edges = append(edges, dotEdge{F: currVertex.uid, T: currOwnerVertex.uid}) } } - return graphBuilder + sort.SliceStable(nodes, func(i, j int) bool { return nodes[i].uid < nodes[j].uid }) + sort.SliceStable(edges, func(i, j int) bool { + if edges[i].F != edges[j].F { + return edges[i].F < edges[j].F + } + return edges[i].T < edges[j].T + }) + + return nodes, edges } -func (m *concurrentUIDToNode) ToGonumGraphForObj(uids ...types.UID) graph.Directed { +func (m *concurrentUIDToNode) ToDOTNodesAndEdgesForObj(uids ...types.UID) ([]*dotVertex, []dotEdge) { m.uidToNodeLock.Lock() defer m.uidToNodeLock.Unlock() - return toGonumGraphForObj(m.uidToNode, uids...) + return toDOTNodesAndEdgesForObj(m.uidToNode, uids...) } -func toGonumGraphForObj(uidToNode map[types.UID]*node, uids ...types.UID) graph.Directed { +func toDOTNodesAndEdgesForObj(uidToNode map[types.UID]*node, uids ...types.UID) ([]*dotVertex, []dotEdge) { uidsToCheck := append([]types.UID{}, uids...) interestingNodes := map[types.UID]*node{} @@ -241,7 +269,7 @@ func toGonumGraphForObj(uidToNode map[types.UID]*node, uids ...types.UID) graph. } } - return toGonumGraph(interestingNodes) + return toDOTNodesAndEdges(interestingNodes) } // NewDebugHandler creates a new debugHTTPHandler. @@ -253,32 +281,64 @@ type debugHTTPHandler struct { controller *GarbageCollector } +func marshalDOT(w io.Writer, nodes []*dotVertex, edges []dotEdge) error { + if _, err := w.Write([]byte("strict digraph full {\n")); err != nil { + return err + } + if len(nodes) > 0 { + if _, err := w.Write([]byte(" // Node definitions.\n")); err != nil { + return err + } + for _, node := range nodes { + if err := node.MarshalDOT(w); err != nil { + return err + } + } + } + if len(edges) > 0 { + if _, err := w.Write([]byte(" // Edge definitions.\n")); err != nil { + return err + } + for _, edge := range edges { + if err := edge.MarshalDOT(w); err != nil { + return err + } + } + } + if _, err := w.Write([]byte("}\n")); err != nil { + return err + } + return nil +} + func (h *debugHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if req.URL.Path != "/graph" { http.Error(w, "", http.StatusNotFound) return } - var graph graph.Directed + var nodes []*dotVertex + var edges []dotEdge if uidStrings := req.URL.Query()["uid"]; len(uidStrings) > 0 { uids := []types.UID{} for _, uidString := range uidStrings { uids = append(uids, types.UID(uidString)) } - graph = h.controller.dependencyGraphBuilder.uidToNode.ToGonumGraphForObj(uids...) + nodes, edges = h.controller.dependencyGraphBuilder.uidToNode.ToDOTNodesAndEdgesForObj(uids...) } else { - graph = h.controller.dependencyGraphBuilder.uidToNode.ToGonumGraph() + nodes, edges = h.controller.dependencyGraphBuilder.uidToNode.ToDOTNodesAndEdges() } - data, err := dot.Marshal(graph, "full", "", " ") - if err != nil { + b := bytes.NewBuffer(nil) + if err := marshalDOT(b, nodes, edges); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "text/vnd.graphviz") w.Header().Set("X-Content-Type-Options", "nosniff") - w.Write(data) + w.Write(b.Bytes()) w.WriteHeader(http.StatusOK) } diff --git a/pkg/controller/garbagecollector/dump_test.go b/pkg/controller/garbagecollector/dump_test.go index f3bd43f672d..a2a148d2029 100644 --- a/pkg/controller/garbagecollector/dump_test.go +++ b/pkg/controller/garbagecollector/dump_test.go @@ -17,12 +17,13 @@ limitations under the License. package garbagecollector import ( - "sort" + "bytes" + "os" + "path/filepath" "testing" "github.com/davecgh/go-spew/spew" - "gonum.org/v1/gonum/graph" - "gonum.org/v1/gonum/graph/simple" + "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -116,11 +117,12 @@ var ( } ) -func TestToGonumGraph(t *testing.T) { +func TestToDOTGraph(t *testing.T) { tests := []struct { - name string - uidToNode map[types.UID]*node - expect graph.Directed + name string + uidToNode map[types.UID]*node + expectNodes []*dotVertex + expectEdges []dotEdge }{ { name: "simple", @@ -129,24 +131,15 @@ func TestToGonumGraph(t *testing.T) { types.UID("bravo"): bravoNode(), types.UID("charlie"): charlieNode(), }, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - alphaVertex := NewGonumVertex(alphaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(alphaVertex) - bravoVertex := NewGonumVertex(bravoNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(bravoVertex) - charlieVertex := NewGonumVertex(charlieNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(charlieVertex) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: bravoVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: charlieVertex, - }) - return graphBuilder - }(), + 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 @@ -154,24 +147,15 @@ func TestToGonumGraph(t *testing.T) { types.UID("alpha"): alphaNode(), types.UID("charlie"): charlieNode(), }, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - alphaVertex := NewGonumVertex(alphaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(alphaVertex) - bravoVertex := NewGonumVertex(bravoNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(bravoVertex) - charlieVertex := NewGonumVertex(charlieNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(charlieVertex) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: bravoVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: charlieVertex, - }) - return graphBuilder - }(), + 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", @@ -181,24 +165,15 @@ func TestToGonumGraph(t *testing.T) { types.UID("charlie"): charlieNode(), types.UID("echo"): echoNode(), }, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - alphaVertex := NewGonumVertex(alphaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(alphaVertex) - bravoVertex := NewGonumVertex(bravoNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(bravoVertex) - charlieVertex := NewGonumVertex(charlieNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(charlieVertex) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: bravoVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: charlieVertex, - }) - return graphBuilder - }(), + 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", @@ -210,59 +185,38 @@ func TestToGonumGraph(t *testing.T) { types.UID("foxtrot"): foxtrotNode(), types.UID("golf"): golfNode(), }, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - alphaVertex := NewGonumVertex(alphaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(alphaVertex) - bravoVertex := NewGonumVertex(bravoNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(bravoVertex) - charlieVertex := NewGonumVertex(charlieNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(charlieVertex) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: bravoVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: charlieVertex, - }) - - deltaVertex := NewGonumVertex(deltaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(deltaVertex) - foxtrotVertex := NewGonumVertex(foxtrotNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(foxtrotVertex) - golfVertex := NewGonumVertex(golfNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(golfVertex) - graphBuilder.SetEdge(simple.Edge{ - F: deltaVertex, - T: foxtrotVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: foxtrotVertex, - T: golfVertex, - }) - - return graphBuilder - }(), + 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) { - actual := toGonumGraph(test.uidToNode) - - compareGraphs(test.expect, actual, t) + actualNodes, actualEdges := toDOTNodesAndEdges(test.uidToNode) + compareGraphs(test.expectNodes, actualNodes, test.expectEdges, actualEdges, t) }) } - } -func TestToGonumGraphObj(t *testing.T) { +func TestToDOTGraphObj(t *testing.T) { tests := []struct { - name string - uidToNode map[types.UID]*node - uids []types.UID - expect graph.Directed + name string + uidToNode map[types.UID]*node + uids []types.UID + expectNodes []*dotVertex + expectEdges []dotEdge }{ { name: "simple", @@ -272,24 +226,15 @@ func TestToGonumGraphObj(t *testing.T) { types.UID("charlie"): charlieNode(), }, uids: []types.UID{types.UID("bravo")}, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - alphaVertex := NewGonumVertex(alphaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(alphaVertex) - bravoVertex := NewGonumVertex(bravoNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(bravoVertex) - charlieVertex := NewGonumVertex(charlieNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(charlieVertex) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: bravoVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: charlieVertex, - }) - return graphBuilder - }(), + 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 @@ -297,11 +242,9 @@ func TestToGonumGraphObj(t *testing.T) { types.UID("alpha"): alphaNode(), types.UID("charlie"): charlieNode(), }, - uids: []types.UID{types.UID("bravo")}, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - return graphBuilder - }(), + uids: []types.UID{types.UID("bravo")}, + expectNodes: []*dotVertex{}, + expectEdges: []dotEdge{}, }, { name: "drop-no-ref", @@ -311,11 +254,9 @@ func TestToGonumGraphObj(t *testing.T) { types.UID("charlie"): charlieNode(), types.UID("echo"): echoNode(), }, - uids: []types.UID{types.UID("echo")}, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - return graphBuilder - }(), + uids: []types.UID{types.UID("echo")}, + expectNodes: []*dotVertex{}, + expectEdges: []dotEdge{}, }, { name: "two-chains-from-owner", @@ -328,25 +269,15 @@ func TestToGonumGraphObj(t *testing.T) { types.UID("golf"): golfNode(), }, uids: []types.UID{types.UID("golf")}, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - deltaVertex := NewGonumVertex(deltaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(deltaVertex) - foxtrotVertex := NewGonumVertex(foxtrotNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(foxtrotVertex) - golfVertex := NewGonumVertex(golfNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(golfVertex) - graphBuilder.SetEdge(simple.Edge{ - F: deltaVertex, - T: foxtrotVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: foxtrotVertex, - T: golfVertex, - }) - - return graphBuilder - }(), + 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", @@ -359,25 +290,15 @@ func TestToGonumGraphObj(t *testing.T) { types.UID("golf"): golfNode(), }, uids: []types.UID{types.UID("delta")}, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - deltaVertex := NewGonumVertex(deltaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(deltaVertex) - foxtrotVertex := NewGonumVertex(foxtrotNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(foxtrotVertex) - golfVertex := NewGonumVertex(golfNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(golfVertex) - graphBuilder.SetEdge(simple.Edge{ - F: deltaVertex, - T: foxtrotVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: foxtrotVertex, - T: golfVertex, - }) - - return graphBuilder - }(), + 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", @@ -390,98 +311,125 @@ func TestToGonumGraphObj(t *testing.T) { types.UID("golf"): golfNode(), }, uids: []types.UID{types.UID("delta"), types.UID("charlie")}, - expect: func() graph.Directed { - graphBuilder := simple.NewDirectedGraph() - alphaVertex := NewGonumVertex(alphaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(alphaVertex) - bravoVertex := NewGonumVertex(bravoNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(bravoVertex) - charlieVertex := NewGonumVertex(charlieNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(charlieVertex) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: bravoVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: alphaVertex, - T: charlieVertex, - }) - - deltaVertex := NewGonumVertex(deltaNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(deltaVertex) - foxtrotVertex := NewGonumVertex(foxtrotNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(foxtrotVertex) - golfVertex := NewGonumVertex(golfNode(), graphBuilder.NewNode().ID()) - graphBuilder.AddNode(golfVertex) - graphBuilder.SetEdge(simple.Edge{ - F: deltaVertex, - T: foxtrotVertex, - }) - graphBuilder.SetEdge(simple.Edge{ - F: foxtrotVertex, - T: golfVertex, - }) - - return graphBuilder - }(), + 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) { - actual := toGonumGraphForObj(test.uidToNode, test.uids...) - - compareGraphs(test.expect, actual, t) + actualNodes, actualEdges := toDOTNodesAndEdgesForObj(test.uidToNode, test.uids...) + compareGraphs(test.expectNodes, actualNodes, test.expectEdges, actualEdges, t) }) } } -func compareGraphs(expected, actual graph.Directed, t *testing.T) { - // sort the edges by from ID, then to ID - // (the slices we get back are from map iteration, where order is not guaranteed) - expectedNodes := expected.Nodes().(graph.NodeSlicer).NodeSlice() - actualNodes := actual.Nodes().(graph.NodeSlicer).NodeSlice() - sort.Sort(gonumByUID(expectedNodes)) - sort.Sort(gonumByUID(actualNodes)) - +func compareGraphs(expectedNodes, actualNodes []*dotVertex, expectedEdges, actualEdges []dotEdge, t *testing.T) { if len(expectedNodes) != len(actualNodes) { - t.Fatal(spew.Sdump(actual)) + t.Fatal(spew.Sdump(actualNodes)) } - for i := range expectedNodes { - currExpected := *expectedNodes[i].(*gonumVertex) - currActual := *actualNodes[i].(*gonumVertex) + currExpected := expectedNodes[i] + currActual := actualNodes[i] if currExpected.uid != currActual.uid { t.Errorf("expected %v, got %v", spew.Sdump(currExpected), spew.Sdump(currActual)) } - - expectedFrom := append([]graph.Node{}, expected.From(expectedNodes[i].ID()).(graph.NodeSlicer).NodeSlice()...) - actualFrom := append([]graph.Node{}, actual.From(actualNodes[i].ID()).(graph.NodeSlicer).NodeSlice()...) - sort.Sort(gonumByUID(expectedFrom)) - sort.Sort(gonumByUID(actualFrom)) - if len(expectedFrom) != len(actualFrom) { - t.Errorf("%q: expected %v, got %v", currExpected.uid, spew.Sdump(expectedFrom), spew.Sdump(actualFrom)) - } - for i := range expectedFrom { - currExpectedFrom := *expectedFrom[i].(*gonumVertex) - currActualFrom := *actualFrom[i].(*gonumVertex) - if currExpectedFrom.uid != currActualFrom.uid { - t.Errorf("expected %v, got %v", spew.Sdump(currExpectedFrom), spew.Sdump(currActualFrom)) - } + } + if len(expectedEdges) != len(actualEdges) { + t.Fatal(spew.Sdump(actualEdges)) + } + for i := range expectedEdges { + currExpected := expectedEdges[i] + currActual := actualEdges[i] + if currExpected != currActual { + t.Errorf("expected %v, got %v", spew.Sdump(currExpected), spew.Sdump(currActual)) } } } -type gonumByUID []graph.Node +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)}, + }, + }, + } -func (s gonumByUID) Len() int { return len(s) } -func (s gonumByUID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + 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) + } -func (s gonumByUID) Less(i, j int) bool { - lhs := s[i].(*gonumVertex) - lhsUID := string(lhs.uid) - rhs := s[j].(*gonumVertex) - rhsUID := string(rhs.uid) - - return lhsUID < rhsUID + 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)) + } + }) + } } diff --git a/pkg/controller/garbagecollector/testdata/empty.dot b/pkg/controller/garbagecollector/testdata/empty.dot new file mode 100644 index 00000000000..c608f8ca1ea --- /dev/null +++ b/pkg/controller/garbagecollector/testdata/empty.dot @@ -0,0 +1,2 @@ +strict digraph full { +} diff --git a/pkg/controller/garbagecollector/testdata/escaping.dot b/pkg/controller/garbagecollector/testdata/escaping.dot new file mode 100644 index 00000000000..e811945938c --- /dev/null +++ b/pkg/controller/garbagecollector/testdata/escaping.dot @@ -0,0 +1,31 @@ +strict digraph full { + // Node definitions. + "ref1-[]\"\\Iñtërnâtiônàlizætiøn,🐹" [ + label="uid=ref1-[]\"\\Iñtërnâtiônàlizætiøn,🐹\nnamespace=ref1ns\nref1kind-Iñtërnâtiônàlizætiøn,🐹.version.ref1group/ref1name-Iñtërnâtiônàlizætiøn,🐹\n" + group="ref1group" + version="version" + kind="ref1kind-Iñtërnâtiônàlizætiøn,🐹" + namespace="ref1ns" + name="ref1name-Iñtërnâtiônàlizætiøn,🐹" + uid="ref1-[]\"\\Iñtërnâtiônàlizætiøn,🐹" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + "ref2-" [ + label="uid=ref2-\nnamespace=ref2ns\nref2kind-.version.ref2group/ref2name-\n" + group="ref2group" + version="version" + kind="ref2kind-" + namespace="ref2ns" + name="ref2name-" + uid="ref2-" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + // Edge definitions. + "ref1-[]\"\\Iñtërnâtiônàlizætiøn,🐹" -> "ref2-"; +} diff --git a/pkg/controller/garbagecollector/testdata/simple.dot b/pkg/controller/garbagecollector/testdata/simple.dot new file mode 100644 index 00000000000..046c27a22b1 --- /dev/null +++ b/pkg/controller/garbagecollector/testdata/simple.dot @@ -0,0 +1,86 @@ +strict digraph full { + // Node definitions. + "alpha" [ + label="uid=alpha\nnamespace=\n./\n" + group="" + version="" + kind="" + namespace="" + name="" + uid="alpha" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + "bravo" [ + label="uid=bravo\nnamespace=\n./\n" + group="" + version="" + kind="" + namespace="" + name="" + uid="bravo" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + "charlie" [ + label="uid=charlie\nnamespace=\n./\n" + group="" + version="" + kind="" + namespace="" + name="" + uid="charlie" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + "delta" [ + label="uid=delta\nnamespace=\n./\n" + group="" + version="" + kind="" + namespace="" + name="" + uid="delta" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + "foxtrot" [ + label="uid=foxtrot\nnamespace=\n./\n" + group="" + version="" + kind="" + namespace="" + name="" + uid="foxtrot" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + "golf" [ + label="uid=golf\nnamespace=\n./\n" + group="" + version="" + kind="" + namespace="" + name="" + uid="golf" + missing="false" + beingDeleted="false" + deletingDependents="false" + virtual="false" + ]; + // Edge definitions. + "alpha" -> "bravo"; + "alpha" -> "charlie"; + "delta" -> "foxtrot"; + "foxtrot" -> "golf"; +}