diff --git a/dag/dag.go b/dag/dag.go index c41255d1fc..f2716257b8 100644 --- a/dag/dag.go +++ b/dag/dag.go @@ -37,6 +37,26 @@ func (g *AcyclicGraph) Root() (Vertex, error) { return roots[0], nil } +// Validate validates the DAG. A DAG is valid if it has a single root +// with no cycles. +func (g *AcyclicGraph) Validate() error { + if _, err := g.Root(); err != nil { + return err + } + + var cycles [][]Vertex + for _, cycle := range StronglyConnected(&g.Graph) { + if len(cycle) > 1 { + cycles = append(cycles, cycle) + } + } + if len(cycles) > 0 { + return fmt.Errorf("cycles: %#v", cycles) + } + + return nil +} + // Walk walks the graph, calling your callback as each node is visited. // This will walk nodes in parallel if it can. func (g *AcyclicGraph) Walk(cb WalkFunc) error { diff --git a/dag/dag_test.go b/dag/dag_test.go index 2bbea81e41..e607c7ba1b 100644 --- a/dag/dag_test.go +++ b/dag/dag_test.go @@ -47,6 +47,34 @@ func TestAcyclicGraphRoot_multiple(t *testing.T) { } } +func TestAcyclicGraphValidate(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + g.Connect(BasicEdge(3, 1)) + + if err := g.Validate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestAcyclicGraphValidate_cycle(t *testing.T) { + var g AcyclicGraph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(3, 2)) + g.Connect(BasicEdge(3, 1)) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 1)) + + if err := g.Validate(); err == nil { + t.Fatal("should error") + } +} + func TestAcyclicGraphWalk(t *testing.T) { var g AcyclicGraph g.Add(1) diff --git a/dag/tarjan.go b/dag/tarjan.go index b5d5b5c8a2..3475dda25c 100644 --- a/dag/tarjan.go +++ b/dag/tarjan.go @@ -58,7 +58,7 @@ func strongConnect(g *Graph, v Vertex, data *tarjanData) *tarjanVertex { if tv.Lowlink == index { vs := make([]Vertex, 0, 2) - for i := len(data.stack) - 1; ; i-- { + for i := len(data.stack) - 1; i >= 0; i-- { v := data.stack[i] data.stack[i] = nil data.stack = data.stack[:i]