terraform/depgraph/graph_test.go

468 lines
8.3 KiB
Go

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")
}
}