From 4623ebd9ffb4b3cd638666fd9449da2cbdaa6275 Mon Sep 17 00:00:00 2001 From: David Eads Date: Wed, 25 Jul 2018 15:45:47 -0400 Subject: [PATCH] add gonum graph representation of GC graph --- pkg/controller/garbagecollector/BUILD | 13 +- pkg/controller/garbagecollector/dump.go | 279 +++++++++++ pkg/controller/garbagecollector/dump_test.go | 487 +++++++++++++++++++ 3 files changed, 778 insertions(+), 1 deletion(-) create mode 100644 pkg/controller/garbagecollector/dump.go create mode 100644 pkg/controller/garbagecollector/dump_test.go diff --git a/pkg/controller/garbagecollector/BUILD b/pkg/controller/garbagecollector/BUILD index dd3818fe3b2..00fcc6433ce 100644 --- a/pkg/controller/garbagecollector/BUILD +++ b/pkg/controller/garbagecollector/BUILD @@ -9,6 +9,7 @@ load( go_library( name = "go_default_library", srcs = [ + "dump.go", "errors.go", "garbagecollector.go", "graph.go", @@ -44,12 +45,19 @@ go_library( "//staging/src/k8s.io/client-go/util/workqueue:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/groupcache/lru:go_default_library", + "//vendor/gonum.org/v1/gonum/graph:go_default_library", + "//vendor/gonum.org/v1/gonum/graph/encoding:go_default_library", + "//vendor/gonum.org/v1/gonum/graph/encoding/dot:go_default_library", + "//vendor/gonum.org/v1/gonum/graph/simple:go_default_library", ], ) go_test( name = "go_default_test", - srcs = ["garbagecollector_test.go"], + srcs = [ + "dump_test.go", + "garbagecollector_test.go", + ], embed = [":go_default_library"], deps = [ "//pkg/api/legacyscheme:go_default_library", @@ -70,7 +78,10 @@ go_test( "//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/util/workqueue:go_default_library", + "//vendor/github.com/davecgh/go-spew/spew:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", + "//vendor/gonum.org/v1/gonum/graph:go_default_library", + "//vendor/gonum.org/v1/gonum/graph/simple:go_default_library", ], ) diff --git a/pkg/controller/garbagecollector/dump.go b/pkg/controller/garbagecollector/dump.go new file mode 100644 index 00000000000..37c59b16385 --- /dev/null +++ b/pkg/controller/garbagecollector/dump.go @@ -0,0 +1,279 @@ +/* +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 ( + "fmt" + "net/http" + "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 { + uid types.UID + gvk schema.GroupVersionKind + namespace string + name string + missingFromGraph bool + beingDeleted bool + deletingDependents bool + virtual bool + vertexID int64 +} + +func (v *gonumVertex) ID() int64 { + return v.vertexID +} + +func (v *gonumVertex) String() string { + kind := v.gvk.Kind + "." + v.gvk.Version + if len(v.gvk.Group) > 0 { + kind = kind + "." + v.gvk.Group + } + missing := "" + if v.missingFromGraph { + missing = "(missing)" + } + deleting := "" + if v.beingDeleted { + deleting = "(deleting)" + } + deletingDependents := "" + if v.deletingDependents { + deleting = "(deletingDependents)" + } + virtual := "" + if v.virtual { + virtual = "(virtual)" + } + 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 { + kubectlString := v.gvk.Kind + "." + v.gvk.Version + if len(v.gvk.Group) > 0 { + kubectlString = kubectlString + "." + v.gvk.Group + } + kubectlString = kubectlString + "/" + v.name + + label := fmt.Sprintf(`uid=%v +namespace=%v +%v +`, + v.uid, + v.namespace, + kubectlString, + ) + + conditionStrings := []string{} + if v.beingDeleted { + conditionStrings = append(conditionStrings, "beingDeleted") + } + if v.deletingDependents { + conditionStrings = append(conditionStrings, "deletingDependents") + } + if v.virtual { + conditionStrings = append(conditionStrings, "virtual") + } + if v.missingFromGraph { + conditionStrings = append(conditionStrings, "missingFromGraph") + } + conditionString := strings.Join(conditionStrings, ",") + if len(conditionString) > 0 { + label = label + conditionString + "\n" + } + + return []encoding.Attribute{ + {Key: "label", Value: fmt.Sprintf(`"%v"`, 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)}, + } +} + +func NewGonumVertex(node *node, nodeID int64) *gonumVertex { + 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{ + uid: node.identity.UID, + gvk: gv.WithKind(node.identity.Kind), + namespace: node.identity.Namespace, + name: node.identity.Name, + beingDeleted: node.beingDeleted, + deletingDependents: node.deletingDependents, + virtual: node.virtual, + vertexID: nodeID, + } +} + +func NewMissingGonumVertex(ownerRef metav1.OwnerReference, nodeID int64) *gonumVertex { + 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{ + uid: ownerRef.UID, + gvk: gv.WithKind(ownerRef.Kind), + name: ownerRef.Name, + missingFromGraph: true, + vertexID: nodeID, + } +} + +func (m *concurrentUIDToNode) ToGonumGraph() graph.Directed { + m.uidToNodeLock.Lock() + defer m.uidToNodeLock.Unlock() + + return toGonumGraph(m.uidToNode) +} + +func toGonumGraph(uidToNode map[types.UID]*node) graph.Directed { + uidToVertex := map[types.UID]*gonumVertex{} + graphBuilder := simple.NewDirectedGraph() + + // add the vertices first, then edges. That avoids having to deal with missing refs. + for _, node := range uidToNode { + // skip adding objects that don't have owner references and aren't referred to. + if len(node.dependents) == 0 && len(node.owners) == 0 { + continue + } + vertex := NewGonumVertex(node, graphBuilder.NewNode().ID()) + uidToVertex[node.identity.UID] = vertex + graphBuilder.AddNode(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()) + uidToVertex[node.identity.UID] = currOwnerVertex + graphBuilder.AddNode(currOwnerVertex) + } + graphBuilder.SetEdge(simple.Edge{ + F: currVertex, + T: currOwnerVertex, + }) + + } + } + + return graphBuilder +} + +func (m *concurrentUIDToNode) ToGonumGraphForObj(uids ...types.UID) graph.Directed { + m.uidToNodeLock.Lock() + defer m.uidToNodeLock.Unlock() + + return toGonumGraphForObj(m.uidToNode, uids...) +} + +func toGonumGraphForObj(uidToNode map[types.UID]*node, uids ...types.UID) graph.Directed { + uidsToCheck := append([]types.UID{}, uids...) + interestingNodes := map[types.UID]*node{} + + // build the set of nodes to inspect first, then use the normal construction on the subset + for i := 0; i < len(uidsToCheck); i++ { + uid := uidsToCheck[i] + // if we've already been observed, there was a bug, but skip it so we don't loop forever + if _, ok := interestingNodes[uid]; ok { + continue + } + node, ok := uidToNode[uid] + // if there is no node for the UID, skip over it. We may add it to the list multiple times + // but we won't loop forever and hopefully the condition doesn't happen very often + if !ok { + continue + } + + interestingNodes[node.identity.UID] = node + + for _, ownerRef := range node.owners { + // if we've already inspected this UID, don't add it to be inspected again + if _, ok := interestingNodes[ownerRef.UID]; ok { + continue + } + uidsToCheck = append(uidsToCheck, ownerRef.UID) + } + for dependent := range node.dependents { + // if we've already inspected this UID, don't add it to be inspected again + if _, ok := interestingNodes[dependent.identity.UID]; ok { + continue + } + uidsToCheck = append(uidsToCheck, dependent.identity.UID) + } + } + + return toGonumGraph(interestingNodes) +} + +func NewDebugHandler(controller *GarbageCollector) http.Handler { + return &debugHTTPHandler{controller: controller} +} + +type debugHTTPHandler struct { + controller *GarbageCollector +} + +func (h *debugHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/graph" { + w.WriteHeader(http.StatusNotFound) + return + } + + var graph graph.Directed + 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...) + + } else { + graph = h.controller.dependencyGraphBuilder.uidToNode.ToGonumGraph() + } + + data, err := dot.Marshal(graph, "full", "", " ", false) + if err != nil { + w.Write([]byte(err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(data) + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/controller/garbagecollector/dump_test.go b/pkg/controller/garbagecollector/dump_test.go new file mode 100644 index 00000000000..fae9b533d29 --- /dev/null +++ b/pkg/controller/garbagecollector/dump_test.go @@ -0,0 +1,487 @@ +/* +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 ( + "sort" + "testing" + + "github.com/davecgh/go-spew/spew" + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/graph/simple" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +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 TestToGonumGraph(t *testing.T) { + tests := []struct { + name string + uidToNode map[types.UID]*node + expect graph.Directed + }{ + { + name: "simple", + uidToNode: map[types.UID]*node{ + types.UID("alpha"): alphaNode(), + 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 + }(), + }, + { + name: "missing", // synthetic vertex created + uidToNode: map[types.UID]*node{ + 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 + }(), + }, + { + 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(), + }, + 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 + }(), + }, + { + 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(), + }, + 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 + }(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := toGonumGraph(test.uidToNode) + + compareGraphs(test.expect, actual, t) + }) + } + +} + +func TestToGonumGraphObj(t *testing.T) { + tests := []struct { + name string + uidToNode map[types.UID]*node + uids []types.UID + expect graph.Directed + }{ + { + 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")}, + 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 + }(), + }, + { + name: "missing", // synthetic vertex created + uidToNode: map[types.UID]*node{ + types.UID("alpha"): alphaNode(), + types.UID("charlie"): charlieNode(), + }, + uids: []types.UID{types.UID("bravo")}, + expect: func() graph.Directed { + graphBuilder := simple.NewDirectedGraph() + return graphBuilder + }(), + }, + { + 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")}, + expect: func() graph.Directed { + graphBuilder := simple.NewDirectedGraph() + return graphBuilder + }(), + }, + { + 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")}, + 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 + }(), + }, + { + 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")}, + 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 + }(), + }, + { + 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")}, + 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 + }(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := toGonumGraphForObj(test.uidToNode, test.uids...) + + compareGraphs(test.expect, actual, 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() + actualNodes := actual.Nodes() + sort.Sort(gonumByUID(expectedNodes)) + sort.Sort(gonumByUID(actualNodes)) + + if len(expectedNodes) != len(actualNodes) { + t.Fatal(spew.Sdump(actual)) + } + + for i := range expectedNodes { + currExpected := *expectedNodes[i].(*gonumVertex) + currActual := *actualNodes[i].(*gonumVertex) + 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())...) + actualFrom := append([]graph.Node{}, actual.From(actualNodes[i].ID())...) + 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)) + } + } + } +} + +type gonumByUID []graph.Node + +func (s gonumByUID) Len() int { return len(s) } +func (s gonumByUID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +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 +}