diff --git a/depgraph/graph.go b/depgraph/graph.go index 1cf4c15b99..7d30de1afd 100644 --- a/depgraph/graph.go +++ b/depgraph/graph.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform/digraph" ) -// Graph is used to represent the entire dependency graph +// Graph is used to represent a dependency graph. type Graph struct { Name string Meta interface{} @@ -19,58 +19,8 @@ type Graph struct { Root *Noun } -// Validate is used to ensure that a few properties of the graph are not violated: -// 1) There must be a single "root", or source on which nothing depends. -// 2) All nouns in the graph must be reachable from the root -// 3) The graph must be cycle free, meaning there are no cicular dependencies -func (g *Graph) Validate() error { - // Convert to node list - nodes := make([]digraph.Node, len(g.Nouns)) - for i, n := range g.Nouns { - nodes[i] = n - } - - // Create a validate erro - vErr := &ValidateError{} - - // Search for all the sources, if we have only 1, it must be the root - if sources := digraph.Sources(nodes); len(sources) != 1 { - vErr.MissingRoot = true - goto CHECK_CYCLES - } else { - g.Root = sources[0].(*Noun) - } - - // Check reachability - if unreached := digraph.Unreachable(g.Root, nodes); len(unreached) > 0 { - vErr.Unreachable = make([]*Noun, len(unreached)) - for i, u := range unreached { - vErr.Unreachable[i] = u.(*Noun) - } - } - -CHECK_CYCLES: - // Check for cycles - if cycles := digraph.StronglyConnectedComponents(nodes, true); len(cycles) > 0 { - vErr.Cycles = make([][]*Noun, len(cycles)) - for i, cycle := range cycles { - group := make([]*Noun, len(cycle)) - for j, n := range cycle { - group[j] = n.(*Noun) - } - vErr.Cycles[i] = group - } - } - - // Return the detailed error - if vErr.MissingRoot || vErr.Unreachable != nil || vErr.Cycles != nil { - return vErr - } - return nil -} - // ValidateError implements the Error interface but provides -// additional information on a validation error +// additional information on a validation error. type ValidateError struct { // If set, then the graph is missing a single root, on which // there are no depdendencies @@ -89,6 +39,31 @@ func (v *ValidateError) Error() string { return "The depedency graph is not valid" } +// ConstraintError is used to return detailed violation +// information from CheckConstraints +type ConstraintError struct { + Violations []*Violation +} + +func (c *ConstraintError) Error() string { + return fmt.Sprintf("%d constraint violations", len(c.Violations)) +} + +// Violation is used to pass along information about +// a constraint violation +type Violation struct { + Source *Noun + Target *Noun + Dependency *Dependency + Constraint Constraint + Err error +} + +func (v *Violation) Error() string { + return fmt.Sprintf("Constraint %v between %v and %v violated: %v", + v.Constraint, v.Source, v.Target, v.Err) +} + // CheckConstraints walks the graph and ensures that all // user imposed constraints are satisfied. func (g *Graph) CheckConstraints() error { @@ -129,27 +104,52 @@ func (g *Graph) CheckConstraints() error { return nil } -// ConstraintError is used to return detailed violation -// information from CheckConstraints -type ConstraintError struct { - Violations []*Violation -} +// Validate is used to ensure that a few properties of the graph are not violated: +// 1) There must be a single "root", or source on which nothing depends. +// 2) All nouns in the graph must be reachable from the root +// 3) The graph must be cycle free, meaning there are no cicular dependencies +func (g *Graph) Validate() error { + // Convert to node list + nodes := make([]digraph.Node, len(g.Nouns)) + for i, n := range g.Nouns { + nodes[i] = n + } -func (c *ConstraintError) Error() string { - return fmt.Sprintf("%d constraint violations", len(c.Violations)) -} + // Create a validate erro + vErr := &ValidateError{} -// Violation is used to pass along information about -// a constraint violation -type Violation struct { - Source *Noun - Target *Noun - Dependency *Dependency - Constraint Constraint - Err error -} + // Search for all the sources, if we have only 1, it must be the root + if sources := digraph.Sources(nodes); len(sources) != 1 { + vErr.MissingRoot = true + goto CHECK_CYCLES + } else { + g.Root = sources[0].(*Noun) + } -func (v *Violation) Error() string { - return fmt.Sprintf("Constraint %v between %v and %v violated: %v", - v.Constraint, v.Source, v.Target, v.Err) + // Check reachability + if unreached := digraph.Unreachable(g.Root, nodes); len(unreached) > 0 { + vErr.Unreachable = make([]*Noun, len(unreached)) + for i, u := range unreached { + vErr.Unreachable[i] = u.(*Noun) + } + } + +CHECK_CYCLES: + // Check for cycles + if cycles := digraph.StronglyConnectedComponents(nodes, true); len(cycles) > 0 { + vErr.Cycles = make([][]*Noun, len(cycles)) + for i, cycle := range cycles { + group := make([]*Noun, len(cycle)) + for j, n := range cycle { + group[j] = n.(*Noun) + } + vErr.Cycles[i] = group + } + } + + // Return the detailed error + if vErr.MissingRoot || vErr.Unreachable != nil || vErr.Cycles != nil { + return vErr + } + return nil } diff --git a/depgraph/graph_test.go b/depgraph/graph_test.go index f0bbc88898..59d3e72810 100644 --- a/depgraph/graph_test.go +++ b/depgraph/graph_test.go @@ -53,9 +53,26 @@ func NounMapToList(m map[string]*Noun) []*Noun { return list } -func TestGraph_Validate_NoRoot(t *testing.T) { +func TestGraph_Validate(t *testing.T) { nodes := ParseNouns(`a -> b -b -> a`) +a -> c +b -> d +b -> e +c -> d +c -> e`) + list := NounMapToList(nodes) + + g := &Graph{Name: "Test", Nouns: list} + if err := g.Validate(); err != nil { + t.Fatalf("err: %v", err) + } +} + +func TestGraph_Validate_Cycle(t *testing.T) { + nodes := ParseNouns(`a -> b +a -> c +b -> d +d -> b`) list := NounMapToList(nodes) g := &Graph{Name: "Test", Nouns: list} @@ -69,8 +86,16 @@ b -> a`) t.Fatalf("expected validate error") } - if !vErr.MissingRoot { - t.Fatalf("expected missing root") + if len(vErr.Cycles) != 1 { + t.Fatalf("expected cycles") + } + + cycle := vErr.Cycles[0] + if cycle[0].Name != "d" { + t.Fatalf("bad: %v", cycle) + } + if cycle[1].Name != "b" { + t.Fatalf("bad: %v", cycle) } } @@ -95,11 +120,9 @@ c -> d`) } } -func TestGraph_Validate_Unreachable(t *testing.T) { +func TestGraph_Validate_NoRoot(t *testing.T) { nodes := ParseNouns(`a -> b -a -> c -b -> d -x -> x`) +b -> a`) list := NounMapToList(nodes) g := &Graph{Name: "Test", Nouns: list} @@ -113,20 +136,16 @@ x -> x`) t.Fatalf("expected validate error") } - if len(vErr.Unreachable) != 1 { - t.Fatalf("expected unreachable") - } - - if vErr.Unreachable[0].Name != "x" { - t.Fatalf("bad: %v", vErr.Unreachable[0]) + if !vErr.MissingRoot { + t.Fatalf("expected missing root") } } -func TestGraph_Validate_Cycle(t *testing.T) { +func TestGraph_Validate_Unreachable(t *testing.T) { nodes := ParseNouns(`a -> b a -> c b -> d -d -> b`) +x -> x`) list := NounMapToList(nodes) g := &Graph{Name: "Test", Nouns: list} @@ -140,32 +159,12 @@ d -> b`) t.Fatalf("expected validate error") } - if len(vErr.Cycles) != 1 { - t.Fatalf("expected cycles") - } - - cycle := vErr.Cycles[0] - if cycle[0].Name != "d" { - t.Fatalf("bad: %v", cycle) - } - if cycle[1].Name != "b" { - t.Fatalf("bad: %v", cycle) + if len(vErr.Unreachable) != 1 { + t.Fatalf("expected unreachable") } -} -func TestGraph_Validate(t *testing.T) { - nodes := ParseNouns(`a -> b -a -> c -b -> d -b -> e -c -> d -c -> e`) - list := NounMapToList(nodes) - - g := &Graph{Name: "Test", Nouns: list} - err := g.Validate() - if err != nil { - t.Fatalf("err: %v", err) + if vErr.Unreachable[0].Name != "x" { + t.Fatalf("bad: %v", vErr.Unreachable[0]) } }