diff --git a/pgraph/pgraph.go b/pgraph/pgraph.go index e0da0e94..102fe9f5 100644 --- a/pgraph/pgraph.go +++ b/pgraph/pgraph.go @@ -167,20 +167,6 @@ func (g *Graph) DeleteEdge(e Edge) { } } -// VertexMatchFn searches for a vertex in the graph and returns the vertex if -// one matches. It uses a user defined function to match. That function must -// return true on match, and an error if anything goes wrong. -func (g *Graph) VertexMatchFn(fn func(Vertex) (bool, error)) (Vertex, error) { - for v := range g.adjacency { - if b, err := fn(v); err != nil { - return nil, errwrap.Wrapf(err, "fn in VertexMatchFn() errored") - } else if b { - return v, nil - } - } - return nil, nil // nothing found -} - // HasVertex returns if the input vertex exists in the graph. func (g *Graph) HasVertex(v Vertex) bool { if _, exists := g.adjacency[v]; exists { @@ -239,8 +225,8 @@ func (vs VertexSlice) Len() int { return len(vs) } func (vs VertexSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } func (vs VertexSlice) Less(i, j int) bool { return vs[i].String() < vs[j].String() } -// VerticesSorted returns a sorted slice of all vertices in the graph -// The order is sorted by String() to avoid the non-determinism in the map type +// VerticesSorted returns a sorted slice of all vertices in the graph. +// The order is sorted by String() to avoid the non-determinism in the map type. func (g *Graph) VerticesSorted() []Vertex { var vertices []Vertex for k := range g.adjacency { @@ -512,6 +498,102 @@ func (g *Graph) Reachability(a, b Vertex) []Vertex { return result } +// VertexMatchFn searches for a vertex in the graph and returns the vertex if +// one matches. It uses a user defined function to match. That function must +// return true on match, and an error if anything goes wrong. +func (g *Graph) VertexMatchFn(fn func(Vertex) (bool, error)) (Vertex, error) { + for v := range g.adjacency { + if b, err := fn(v); err != nil { + return nil, errwrap.Wrapf(err, "fn in VertexMatchFn() errored") + } else if b { + return v, nil + } + } + return nil, nil // nothing found +} + +// GraphCmp compares the topology of this graph to another and returns nil if +// they're equal. It uses a user defined function to compare topologically +// equivalent vertices, and edges. +// FIXME: add more test cases +func (g *Graph) GraphCmp(graph *Graph, vertexCmpFn func(Vertex, Vertex) (bool, error), edgeCmpFn func(Edge, Edge) (bool, error)) error { + n1, n2 := g.NumVertices(), graph.NumVertices() + if n1 != n2 { + return fmt.Errorf("base graph has %d vertices, while input graph has %d", n1, n2) + } + if e1, e2 := g.NumEdges(), graph.NumEdges(); e1 != e2 { + return fmt.Errorf("base graph has %d edges, while input graph has %d", e1, e2) + } + + var m = make(map[Vertex]Vertex) // g to graph vertex correspondence +Loop: + // check vertices + for v1 := range g.Adjacency() { // for each vertex in g + for v2 := range graph.Adjacency() { // does it match in graph ? + b, err := vertexCmpFn(v1, v2) + if err != nil { + return errwrap.Wrapf(err, "could not run vertexCmpFn() properly") + } + // does it match ? + if b { + m[v1] = v2 // store the mapping + continue Loop + } + } + return fmt.Errorf("base graph, has no match in input graph for: %s", v1) + } + // vertices match :) + + // is the mapping the right length? + if n1 := len(m); n1 != n2 { + return fmt.Errorf("mapping only has correspondence of %d, when it should have %d", n1, n2) + } + + // check if mapping is unique (are there duplicates?) + m1 := []Vertex{} + m2 := []Vertex{} + for k, v := range m { + if VertexContains(k, m1) { + return fmt.Errorf("mapping from %s is used more than once to: %s", k, m1) + } + if VertexContains(v, m2) { + return fmt.Errorf("mapping to %s is used more than once from: %s", v, m2) + } + m1 = append(m1, k) + m2 = append(m2, v) + } + + // check edges + for v1 := range g.Adjacency() { // for each vertex in g + v2 := m[v1] // lookup in map to get correspondance + // g.Adjacency()[v1] corresponds to graph.Adjacency()[v2] + if e1, e2 := len(g.Adjacency()[v1]), len(graph.Adjacency()[v2]); e1 != e2 { + return fmt.Errorf("base graph, vertex(%s) has %d edges, while input graph, vertex(%s) has %d", v1, e1, v2, e2) + } + + for vv1, ee1 := range g.Adjacency()[v1] { + vv2 := m[vv1] + ee2 := graph.Adjacency()[v2][vv2] + + // these are edges from v1 -> vv1 via ee1 (graph 1) + // to cmp to edges from v2 -> vv2 via ee2 (graph 2) + + // check: (1) vv1 == vv2 ? (we've already checked this!) + + // check: (2) ee1 == ee2 + b, err := edgeCmpFn(ee1, ee2) + if err != nil { + return errwrap.Wrapf(err, "could not run edgeCmpFn() properly") + } + if !b { + return fmt.Errorf("base graph edge(%s) doesn't match input graph edge(%s)", ee1, ee2) + } + } + } + + return nil // success! +} + // VertexContains is an "in array" function to test for a vertex in a slice of vertices. func VertexContains(needle Vertex, haystack []Vertex) bool { for _, v := range haystack { diff --git a/pgraph/pgraph_test.go b/pgraph/pgraph_test.go index 92987756..86bfcbf6 100644 --- a/pgraph/pgraph_test.go +++ b/pgraph/pgraph_test.go @@ -18,6 +18,7 @@ package pgraph import ( + "fmt" "reflect" "testing" ) @@ -657,3 +658,38 @@ func TestPgraphDelete1(t *testing.T) { t.Errorf("should have 0 vertices instead of: %d", i) } } + +func vertexCmpFn(v1, v2 Vertex) (bool, error) { + if v1.String() == "" || v2.String() == "" { + return false, fmt.Errorf("oops, empty vertex") + } + return v1.String() == v2.String(), nil +} + +func edgeCmpFn(e1, e2 Edge) (bool, error) { + if e1.String() == "" || e2.String() == "" { + return false, fmt.Errorf("oops, empty edge") + } + return e1.String() == e2.String(), nil +} + +func TestPgraphGraphCmp1(t *testing.T) { + g1 := &Graph{} + g2 := &Graph{} + g3 := &Graph{} + g3.AddVertex(NV("v1")) + g4 := &Graph{} + g4.AddVertex(NV("v2")) + + if err := g1.GraphCmp(g2, vertexCmpFn, edgeCmpFn); err != nil { + t.Errorf("should have no error during GraphCmp, but got: %v", err) + } + + if err := g1.GraphCmp(g3, vertexCmpFn, edgeCmpFn); err == nil { + t.Errorf("should have error during GraphCmp, but got nil") + } + + if err := g3.GraphCmp(g4, vertexCmpFn, edgeCmpFn); err == nil { + t.Errorf("should have error during GraphCmp, but got nil") + } +} diff --git a/resources/autogroup_test.go b/resources/autogroup_test.go index 9e7746d9..2f50fe4c 100644 --- a/resources/autogroup_test.go +++ b/resources/autogroup_test.go @@ -117,8 +117,9 @@ func NewNoopResTest(name string) *NoopResTest { return obj } -// GraphCmp compares the topology of two graphs and returns nil if they're equal -// It also compares if grouped element groups are identical +// GraphCmp compares the topology of two graphs and returns nil if they're +// equal. It also compares if grouped element groups are identical. +// TODO: port this to use the pgraph.GraphCmp function instead. func GraphCmp(g1, g2 *pgraph.Graph) error { if n1, n2 := g1.NumVertices(), g2.NumVertices(); n1 != n2 { return fmt.Errorf("graph g1 has %d vertices, while g2 has %d", n1, n2)