Merge pull request #3936 from hashicorp/f-remove-depgraph
Remove unused depgraph package
This commit is contained in:
commit
48e1e96078
|
@ -1,46 +0,0 @@
|
||||||
package depgraph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/digraph"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Dependency is used to create a directed edge between two nouns.
|
|
||||||
// One noun may depend on another and provide version constraints
|
|
||||||
// that cannot be violated
|
|
||||||
type Dependency struct {
|
|
||||||
Name string
|
|
||||||
Meta interface{}
|
|
||||||
Constraints []Constraint
|
|
||||||
Source *Noun
|
|
||||||
Target *Noun
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constraint is used by dependencies to allow arbitrary constraints
|
|
||||||
// between nouns
|
|
||||||
type Constraint interface {
|
|
||||||
Satisfied(head, tail *Noun) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Head returns the source, or dependent noun
|
|
||||||
func (d *Dependency) Head() digraph.Node {
|
|
||||||
return d.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tail returns the target, or depended upon noun
|
|
||||||
func (d *Dependency) Tail() digraph.Node {
|
|
||||||
return d.Target
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dependency) GoString() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"*Dependency{Name: %s, Source: %s, Target: %s}",
|
|
||||||
d.Name,
|
|
||||||
d.Source.Name,
|
|
||||||
d.Target.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dependency) String() string {
|
|
||||||
return d.Name
|
|
||||||
}
|
|
|
@ -1,379 +0,0 @@
|
||||||
// The depgraph package is used to create and model a dependency graph
|
|
||||||
// of nouns. Each noun can represent a service, server, application,
|
|
||||||
// network switch, etc. Nouns can depend on other nouns, and provide
|
|
||||||
// versioning constraints. Nouns can also have various meta data that
|
|
||||||
// may be relevant to their construction or configuration.
|
|
||||||
package depgraph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/digraph"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WalkFunc is the type used for the callback for Walk.
|
|
||||||
type WalkFunc func(*Noun) error
|
|
||||||
|
|
||||||
// Graph is used to represent a dependency graph.
|
|
||||||
type Graph struct {
|
|
||||||
Name string
|
|
||||||
Meta interface{}
|
|
||||||
Nouns []*Noun
|
|
||||||
Root *Noun
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateError implements the Error interface but provides
|
|
||||||
// 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
|
|
||||||
MissingRoot bool
|
|
||||||
|
|
||||||
// Unreachable are nodes that could not be reached from
|
|
||||||
// the root noun.
|
|
||||||
Unreachable []*Noun
|
|
||||||
|
|
||||||
// Cycles are groups of strongly connected nodes, which
|
|
||||||
// form a cycle. This is disallowed.
|
|
||||||
Cycles [][]*Noun
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ValidateError) Error() string {
|
|
||||||
var msgs []string
|
|
||||||
|
|
||||||
if v.MissingRoot {
|
|
||||||
msgs = append(msgs, "The graph has no single root")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range v.Unreachable {
|
|
||||||
msgs = append(msgs, fmt.Sprintf(
|
|
||||||
"Unreachable node: %s", n.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range v.Cycles {
|
|
||||||
cycleNodes := make([]string, len(c))
|
|
||||||
for i, n := range c {
|
|
||||||
cycleNodes[i] = n.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs = append(msgs, fmt.Sprintf(
|
|
||||||
"Cycle: %s", strings.Join(cycleNodes, " -> ")))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, m := range msgs {
|
|
||||||
msgs[i] = fmt.Sprintf("* %s", m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"The dependency graph is not valid:\n\n%s",
|
|
||||||
strings.Join(msgs, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// Ensure we have a root
|
|
||||||
if g.Root == nil {
|
|
||||||
return fmt.Errorf("Graph must be validated before checking constraint violations")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a constraint error
|
|
||||||
cErr := &ConstraintError{}
|
|
||||||
|
|
||||||
// Walk from the root
|
|
||||||
digraph.DepthFirstWalk(g.Root, func(n digraph.Node) bool {
|
|
||||||
noun := n.(*Noun)
|
|
||||||
for _, dep := range noun.Deps {
|
|
||||||
target := dep.Target
|
|
||||||
for _, constraint := range dep.Constraints {
|
|
||||||
ok, err := constraint.Satisfied(noun, target)
|
|
||||||
if ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
violation := &Violation{
|
|
||||||
Source: noun,
|
|
||||||
Target: target,
|
|
||||||
Dependency: dep,
|
|
||||||
Constraint: constraint,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
cErr.Violations = append(cErr.Violations, violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if cErr.Violations != nil {
|
|
||||||
return cErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Noun returns the noun with the given name, or nil if it cannot be found.
|
|
||||||
func (g *Graph) Noun(name string) *Noun {
|
|
||||||
for _, n := range g.Nouns {
|
|
||||||
if n.Name == name {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String generates a little ASCII string of the graph, useful in
|
|
||||||
// debugging output.
|
|
||||||
func (g *Graph) String() string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
// Alphabetize the output based on the noun name
|
|
||||||
keys := make([]string, 0, len(g.Nouns))
|
|
||||||
mapping := make(map[string]*Noun)
|
|
||||||
for _, n := range g.Nouns {
|
|
||||||
mapping[n.Name] = n
|
|
||||||
keys = append(keys, n.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
if g.Root != nil {
|
|
||||||
buf.WriteString(fmt.Sprintf("root: %s\n", g.Root.Name))
|
|
||||||
} else {
|
|
||||||
buf.WriteString("root: <unknown>\n")
|
|
||||||
}
|
|
||||||
for _, k := range keys {
|
|
||||||
n := mapping[k]
|
|
||||||
buf.WriteString(fmt.Sprintf("%s\n", n.Name))
|
|
||||||
|
|
||||||
// Alphabetize the dependency names
|
|
||||||
depKeys := make([]string, 0, len(n.Deps))
|
|
||||||
depMapping := make(map[string]*Dependency)
|
|
||||||
for _, d := range n.Deps {
|
|
||||||
depMapping[d.Target.Name] = d
|
|
||||||
depKeys = append(depKeys, d.Target.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(depKeys)
|
|
||||||
|
|
||||||
for _, k := range depKeys {
|
|
||||||
dep := depMapping[k]
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
|
||||||
" %s -> %s\n",
|
|
||||||
dep.Source,
|
|
||||||
dep.Target))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for loops to yourself
|
|
||||||
for _, n := range g.Nouns {
|
|
||||||
for _, d := range n.Deps {
|
|
||||||
if d.Source == d.Target {
|
|
||||||
vErr.Cycles = append(vErr.Cycles, []*Noun{n})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the detailed error
|
|
||||||
if vErr.MissingRoot || vErr.Unreachable != nil || vErr.Cycles != nil {
|
|
||||||
return vErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk will walk the tree depth-first (dependency first) and call
|
|
||||||
// the callback.
|
|
||||||
//
|
|
||||||
// The callbacks will be called in parallel, so if you need non-parallelism,
|
|
||||||
// then introduce a lock in your callback.
|
|
||||||
func (g *Graph) Walk(fn WalkFunc) error {
|
|
||||||
// Set so we don't callback for a single noun multiple times
|
|
||||||
var seenMapL sync.RWMutex
|
|
||||||
seenMap := make(map[*Noun]chan struct{})
|
|
||||||
seenMap[g.Root] = make(chan struct{})
|
|
||||||
|
|
||||||
// Keep track of what nodes errored.
|
|
||||||
var errMapL sync.RWMutex
|
|
||||||
errMap := make(map[*Noun]struct{})
|
|
||||||
|
|
||||||
// Build the list of things to visit
|
|
||||||
tovisit := make([]*Noun, 1, len(g.Nouns))
|
|
||||||
tovisit[0] = g.Root
|
|
||||||
|
|
||||||
// Spawn off all our goroutines to walk the tree
|
|
||||||
errCh := make(chan error)
|
|
||||||
for len(tovisit) > 0 {
|
|
||||||
// Grab the current thing to use
|
|
||||||
n := len(tovisit)
|
|
||||||
current := tovisit[n-1]
|
|
||||||
tovisit = tovisit[:n-1]
|
|
||||||
|
|
||||||
// Go through each dependency and run that first
|
|
||||||
for _, dep := range current.Deps {
|
|
||||||
if _, ok := seenMap[dep.Target]; !ok {
|
|
||||||
seenMapL.Lock()
|
|
||||||
seenMap[dep.Target] = make(chan struct{})
|
|
||||||
seenMapL.Unlock()
|
|
||||||
tovisit = append(tovisit, dep.Target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn off a goroutine to execute our callback once
|
|
||||||
// all our dependencies are satisfied.
|
|
||||||
go func(current *Noun) {
|
|
||||||
seenMapL.RLock()
|
|
||||||
closeCh := seenMap[current]
|
|
||||||
seenMapL.RUnlock()
|
|
||||||
|
|
||||||
defer close(closeCh)
|
|
||||||
|
|
||||||
// Wait for all our dependencies
|
|
||||||
for _, dep := range current.Deps {
|
|
||||||
seenMapL.RLock()
|
|
||||||
ch := seenMap[dep.Target]
|
|
||||||
seenMapL.RUnlock()
|
|
||||||
|
|
||||||
// Wait for the dep to be run
|
|
||||||
<-ch
|
|
||||||
|
|
||||||
// Check if any dependencies errored. If so,
|
|
||||||
// then return right away, we won't walk it.
|
|
||||||
errMapL.RLock()
|
|
||||||
_, errOk := errMap[dep.Target]
|
|
||||||
errMapL.RUnlock()
|
|
||||||
if errOk {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call our callback!
|
|
||||||
if err := fn(current); err != nil {
|
|
||||||
errMapL.Lock()
|
|
||||||
errMap[current] = struct{}{}
|
|
||||||
errMapL.Unlock()
|
|
||||||
|
|
||||||
errCh <- err
|
|
||||||
}
|
|
||||||
}(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aggregate channel that is closed when all goroutines finish
|
|
||||||
doneCh := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(doneCh)
|
|
||||||
|
|
||||||
for _, ch := range seenMap {
|
|
||||||
<-ch
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for finish OR an error
|
|
||||||
select {
|
|
||||||
case <-doneCh:
|
|
||||||
return nil
|
|
||||||
case err := <-errCh:
|
|
||||||
// Drain the error channel
|
|
||||||
go func() {
|
|
||||||
for _ = range errCh {
|
|
||||||
// Nothing
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for the goroutines to end
|
|
||||||
<-doneCh
|
|
||||||
close(errCh)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DependsOn returns the set of nouns that have a
|
|
||||||
// dependency on a given noun. This can be used to find
|
|
||||||
// the incoming edges to a noun.
|
|
||||||
func (g *Graph) DependsOn(n *Noun) []*Noun {
|
|
||||||
var incoming []*Noun
|
|
||||||
OUTER:
|
|
||||||
for _, other := range g.Nouns {
|
|
||||||
if other == n {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, d := range other.Deps {
|
|
||||||
if d.Target == n {
|
|
||||||
incoming = append(incoming, other)
|
|
||||||
continue OUTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return incoming
|
|
||||||
}
|
|
|
@ -1,467 +0,0 @@
|
||||||
package depgraph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseNouns is used to parse a string in the format of:
|
|
||||||
// a -> b ; edge name
|
|
||||||
// b -> c
|
|
||||||
// Into a series of nouns and dependencies
|
|
||||||
func ParseNouns(s string) map[string]*Noun {
|
|
||||||
lines := strings.Split(s, "\n")
|
|
||||||
nodes := make(map[string]*Noun)
|
|
||||||
for _, line := range lines {
|
|
||||||
var edgeName string
|
|
||||||
if idx := strings.Index(line, ";"); idx >= 0 {
|
|
||||||
edgeName = strings.Trim(line[idx+1:], " \t\r\n")
|
|
||||||
line = line[:idx]
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(line, "->", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
head_name := strings.Trim(parts[0], " \t\r\n")
|
|
||||||
tail_name := strings.Trim(parts[1], " \t\r\n")
|
|
||||||
head := nodes[head_name]
|
|
||||||
if head == nil {
|
|
||||||
head = &Noun{Name: head_name}
|
|
||||||
nodes[head_name] = head
|
|
||||||
}
|
|
||||||
tail := nodes[tail_name]
|
|
||||||
if tail == nil {
|
|
||||||
tail = &Noun{Name: tail_name}
|
|
||||||
nodes[tail_name] = tail
|
|
||||||
}
|
|
||||||
edge := &Dependency{
|
|
||||||
Name: edgeName,
|
|
||||||
Source: head,
|
|
||||||
Target: tail,
|
|
||||||
}
|
|
||||||
head.Deps = append(head.Deps, edge)
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func NounMapToList(m map[string]*Noun) []*Noun {
|
|
||||||
list := make([]*Noun, 0, len(m))
|
|
||||||
for _, n := range m {
|
|
||||||
list = append(list, n)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_Noun(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
a -> c
|
|
||||||
b -> d
|
|
||||||
b -> e
|
|
||||||
c -> d
|
|
||||||
c -> e`)
|
|
||||||
|
|
||||||
g := &Graph{
|
|
||||||
Name: "Test",
|
|
||||||
Nouns: NounMapToList(nodes),
|
|
||||||
}
|
|
||||||
|
|
||||||
n := g.Noun("a")
|
|
||||||
if n == nil {
|
|
||||||
t.Fatal("should not be nil")
|
|
||||||
}
|
|
||||||
if n.Name != "a" {
|
|
||||||
t.Fatalf("bad: %#v", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_String(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
a -> c
|
|
||||||
b -> d
|
|
||||||
b -> e
|
|
||||||
c -> d
|
|
||||||
c -> e`)
|
|
||||||
|
|
||||||
g := &Graph{
|
|
||||||
Name: "Test",
|
|
||||||
Nouns: NounMapToList(nodes),
|
|
||||||
Root: nodes["a"],
|
|
||||||
}
|
|
||||||
actual := g.String()
|
|
||||||
|
|
||||||
expected := `
|
|
||||||
root: a
|
|
||||||
a
|
|
||||||
a -> b
|
|
||||||
a -> c
|
|
||||||
b
|
|
||||||
b -> d
|
|
||||||
b -> e
|
|
||||||
c
|
|
||||||
c -> d
|
|
||||||
c -> e
|
|
||||||
d
|
|
||||||
e
|
|
||||||
`
|
|
||||||
|
|
||||||
actual = strings.TrimSpace(actual)
|
|
||||||
expected = strings.TrimSpace(expected)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("bad:\n%s\n!=\n%s", actual, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
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}
|
|
||||||
err := g.Validate()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected err")
|
|
||||||
}
|
|
||||||
|
|
||||||
vErr, ok := err.(*ValidateError)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected validate error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(vErr.Cycles) != 1 {
|
|
||||||
t.Fatalf("expected cycles")
|
|
||||||
}
|
|
||||||
|
|
||||||
cycle := vErr.Cycles[0]
|
|
||||||
cycleNodes := make([]string, len(cycle))
|
|
||||||
for i, c := range cycle {
|
|
||||||
cycleNodes[i] = c.Name
|
|
||||||
}
|
|
||||||
sort.Strings(cycleNodes)
|
|
||||||
|
|
||||||
if cycleNodes[0] != "b" {
|
|
||||||
t.Fatalf("bad: %v", cycle)
|
|
||||||
}
|
|
||||||
if cycleNodes[1] != "d" {
|
|
||||||
t.Fatalf("bad: %v", cycle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_Validate_MultiRoot(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
c -> d`)
|
|
||||||
list := NounMapToList(nodes)
|
|
||||||
|
|
||||||
g := &Graph{Name: "Test", Nouns: list}
|
|
||||||
err := g.Validate()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected err")
|
|
||||||
}
|
|
||||||
|
|
||||||
vErr, ok := err.(*ValidateError)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected validate error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !vErr.MissingRoot {
|
|
||||||
t.Fatalf("expected missing root")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_Validate_NoRoot(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
b -> a`)
|
|
||||||
list := NounMapToList(nodes)
|
|
||||||
|
|
||||||
g := &Graph{Name: "Test", Nouns: list}
|
|
||||||
err := g.Validate()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected err")
|
|
||||||
}
|
|
||||||
|
|
||||||
vErr, ok := err.(*ValidateError)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected validate error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !vErr.MissingRoot {
|
|
||||||
t.Fatalf("expected missing root")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_Validate_Unreachable(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
a -> c
|
|
||||||
b -> d
|
|
||||||
x -> x`)
|
|
||||||
list := NounMapToList(nodes)
|
|
||||||
|
|
||||||
g := &Graph{Name: "Test", Nouns: list}
|
|
||||||
err := g.Validate()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected err")
|
|
||||||
}
|
|
||||||
|
|
||||||
vErr, ok := err.(*ValidateError)
|
|
||||||
if !ok {
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type VersionMeta int
|
|
||||||
type VersionConstraint struct {
|
|
||||||
Min int
|
|
||||||
Max int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *VersionConstraint) Satisfied(head, tail *Noun) (bool, error) {
|
|
||||||
vers := int(tail.Meta.(VersionMeta))
|
|
||||||
if vers < v.Min {
|
|
||||||
return false, fmt.Errorf("version %d below minimum %d",
|
|
||||||
vers, v.Min)
|
|
||||||
} else if vers > v.Max {
|
|
||||||
return false, fmt.Errorf("version %d above maximum %d",
|
|
||||||
vers, v.Max)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *VersionConstraint) String() string {
|
|
||||||
return "version"
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_ConstraintViolation(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
a -> c
|
|
||||||
b -> d
|
|
||||||
b -> e
|
|
||||||
c -> d
|
|
||||||
c -> e`)
|
|
||||||
list := NounMapToList(nodes)
|
|
||||||
|
|
||||||
// Add a version constraint
|
|
||||||
vers := &VersionConstraint{1, 3}
|
|
||||||
|
|
||||||
// Introduce some constraints
|
|
||||||
depB := nodes["a"].Deps[0]
|
|
||||||
depB.Constraints = []Constraint{vers}
|
|
||||||
depC := nodes["a"].Deps[1]
|
|
||||||
depC.Constraints = []Constraint{vers}
|
|
||||||
|
|
||||||
// Add some versions
|
|
||||||
nodes["b"].Meta = VersionMeta(0)
|
|
||||||
nodes["c"].Meta = VersionMeta(4)
|
|
||||||
|
|
||||||
g := &Graph{Name: "Test", Nouns: list}
|
|
||||||
err := g.Validate()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = g.CheckConstraints()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected err")
|
|
||||||
}
|
|
||||||
|
|
||||||
cErr, ok := err.(*ConstraintError)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected constraint error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cErr.Violations) != 2 {
|
|
||||||
t.Fatalf("expected 2 violations: %v", cErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cErr.Violations[0].Error() != "Constraint version between a and b violated: version 0 below minimum 1" {
|
|
||||||
t.Fatalf("err: %v", cErr.Violations[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if cErr.Violations[1].Error() != "Constraint version between a and c violated: version 4 above maximum 3" {
|
|
||||||
t.Fatalf("err: %v", cErr.Violations[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_Constraint_NoViolation(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
a -> c
|
|
||||||
b -> d
|
|
||||||
b -> e
|
|
||||||
c -> d
|
|
||||||
c -> e`)
|
|
||||||
list := NounMapToList(nodes)
|
|
||||||
|
|
||||||
// Add a version constraint
|
|
||||||
vers := &VersionConstraint{1, 3}
|
|
||||||
|
|
||||||
// Introduce some constraints
|
|
||||||
depB := nodes["a"].Deps[0]
|
|
||||||
depB.Constraints = []Constraint{vers}
|
|
||||||
depC := nodes["a"].Deps[1]
|
|
||||||
depC.Constraints = []Constraint{vers}
|
|
||||||
|
|
||||||
// Add some versions
|
|
||||||
nodes["b"].Meta = VersionMeta(2)
|
|
||||||
nodes["c"].Meta = VersionMeta(3)
|
|
||||||
|
|
||||||
g := &Graph{Name: "Test", Nouns: list}
|
|
||||||
err := g.Validate()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = g.CheckConstraints()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraphWalk(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}
|
|
||||||
if err := g.Validate(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var namesLock sync.Mutex
|
|
||||||
names := make([]string, 0, 0)
|
|
||||||
err := g.Walk(func(n *Noun) error {
|
|
||||||
namesLock.Lock()
|
|
||||||
defer namesLock.Unlock()
|
|
||||||
names = append(names, n.Name)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := [][]string{
|
|
||||||
{"e", "d", "c", "b", "a"},
|
|
||||||
{"e", "d", "b", "c", "a"},
|
|
||||||
{"d", "e", "c", "b", "a"},
|
|
||||||
{"d", "e", "b", "c", "a"},
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, expect := range expected {
|
|
||||||
if reflect.DeepEqual(expect, names) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Fatalf("bad: %#v", names)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraphWalk_error(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
b -> c
|
|
||||||
a -> d
|
|
||||||
a -> e
|
|
||||||
e -> f
|
|
||||||
f -> g
|
|
||||||
g -> h`)
|
|
||||||
list := NounMapToList(nodes)
|
|
||||||
g := &Graph{Name: "Test", Nouns: list}
|
|
||||||
if err := g.Validate(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We repeat this a lot because sometimes timing causes
|
|
||||||
// a false positive.
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
var lock sync.Mutex
|
|
||||||
var walked []string
|
|
||||||
err := g.Walk(func(n *Noun) error {
|
|
||||||
lock.Lock()
|
|
||||||
defer lock.Unlock()
|
|
||||||
|
|
||||||
walked = append(walked, n.Name)
|
|
||||||
|
|
||||||
if n.Name == "b" {
|
|
||||||
return fmt.Errorf("foo")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("should error")
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(walked)
|
|
||||||
|
|
||||||
expected := []string{"b", "c", "d", "e", "f", "g", "h"}
|
|
||||||
if !reflect.DeepEqual(walked, expected) {
|
|
||||||
t.Fatalf("bad: %#v", walked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGraph_DependsOn(t *testing.T) {
|
|
||||||
nodes := ParseNouns(`a -> b
|
|
||||||
a -> c
|
|
||||||
b -> d
|
|
||||||
b -> e
|
|
||||||
c -> d
|
|
||||||
c -> e`)
|
|
||||||
|
|
||||||
g := &Graph{
|
|
||||||
Name: "Test",
|
|
||||||
Nouns: NounMapToList(nodes),
|
|
||||||
}
|
|
||||||
|
|
||||||
dNoun := g.Noun("d")
|
|
||||||
incoming := g.DependsOn(dNoun)
|
|
||||||
|
|
||||||
if len(incoming) != 2 {
|
|
||||||
t.Fatalf("bad: %#v", incoming)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasB, hasC bool
|
|
||||||
for _, in := range incoming {
|
|
||||||
switch in.Name {
|
|
||||||
case "b":
|
|
||||||
hasB = true
|
|
||||||
case "c":
|
|
||||||
hasC = true
|
|
||||||
default:
|
|
||||||
t.Fatalf("Bad: %#v", in)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasB || !hasC {
|
|
||||||
t.Fatalf("missing incoming edge")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package depgraph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/digraph"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Nouns are the key structure of the dependency graph. They can
|
|
||||||
// be used to represent all objects in the graph. They are linked
|
|
||||||
// by depedencies.
|
|
||||||
type Noun struct {
|
|
||||||
Name string // Opaque name
|
|
||||||
Meta interface{}
|
|
||||||
Deps []*Dependency
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges returns the out-going edges of a Noun
|
|
||||||
func (n *Noun) Edges() []digraph.Edge {
|
|
||||||
edges := make([]digraph.Edge, len(n.Deps))
|
|
||||||
for idx, dep := range n.Deps {
|
|
||||||
edges[idx] = dep
|
|
||||||
}
|
|
||||||
return edges
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Noun) GoString() string {
|
|
||||||
return fmt.Sprintf("*%#v", *n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Noun) String() string {
|
|
||||||
return n.Name
|
|
||||||
}
|
|
Loading…
Reference in New Issue