mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-29 10:21:01 -06:00
ca272b2107
A topological walk was previously only done in Terraform via the concurrent method used for walking the primary dependency graph in core. Sometime however we want a dependency ordering without the overhead of instantiating the concurrent walk with the channel-based edges. Add TopologicalOrder and ReverseTopologicalOrder to obtain a list of nodes which can be used to visit each while ensuring that all dependencies are satisfied.
583 lines
12 KiB
Go
583 lines
12 KiB
Go
package dag
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
|
|
_ "github.com/hashicorp/terraform/internal/logging"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
flag.Parse()
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestAcyclicGraphRoot(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 root, err := g.Root(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
} else if root != 3 {
|
|
t.Fatalf("bad: %#v", root)
|
|
}
|
|
}
|
|
|
|
func TestAcyclicGraphRoot_cycle(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Connect(BasicEdge(1, 2))
|
|
g.Connect(BasicEdge(2, 3))
|
|
g.Connect(BasicEdge(3, 1))
|
|
|
|
if _, err := g.Root(); err == nil {
|
|
t.Fatal("should error")
|
|
}
|
|
}
|
|
|
|
func TestAcyclicGraphRoot_multiple(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Connect(BasicEdge(3, 2))
|
|
|
|
if _, err := g.Root(); err == nil {
|
|
t.Fatal("should error")
|
|
}
|
|
}
|
|
|
|
func TestAyclicGraphTransReduction(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Connect(BasicEdge(1, 2))
|
|
g.Connect(BasicEdge(1, 3))
|
|
g.Connect(BasicEdge(2, 3))
|
|
g.TransitiveReduction()
|
|
|
|
actual := strings.TrimSpace(g.String())
|
|
expected := strings.TrimSpace(testGraphTransReductionStr)
|
|
if actual != expected {
|
|
t.Fatalf("bad: %s", actual)
|
|
}
|
|
}
|
|
|
|
func TestAyclicGraphTransReduction_more(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Add(4)
|
|
g.Connect(BasicEdge(1, 2))
|
|
g.Connect(BasicEdge(1, 3))
|
|
g.Connect(BasicEdge(1, 4))
|
|
g.Connect(BasicEdge(2, 3))
|
|
g.Connect(BasicEdge(2, 4))
|
|
g.Connect(BasicEdge(3, 4))
|
|
g.TransitiveReduction()
|
|
|
|
actual := strings.TrimSpace(g.String())
|
|
expected := strings.TrimSpace(testGraphTransReductionMoreStr)
|
|
if actual != expected {
|
|
t.Fatalf("bad: %s", actual)
|
|
}
|
|
}
|
|
|
|
func TestAyclicGraphTransReduction_multipleRoots(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Add(4)
|
|
g.Connect(BasicEdge(1, 2))
|
|
g.Connect(BasicEdge(1, 3))
|
|
g.Connect(BasicEdge(1, 4))
|
|
g.Connect(BasicEdge(2, 3))
|
|
g.Connect(BasicEdge(2, 4))
|
|
g.Connect(BasicEdge(3, 4))
|
|
|
|
g.Add(5)
|
|
g.Add(6)
|
|
g.Add(7)
|
|
g.Add(8)
|
|
g.Connect(BasicEdge(5, 6))
|
|
g.Connect(BasicEdge(5, 7))
|
|
g.Connect(BasicEdge(5, 8))
|
|
g.Connect(BasicEdge(6, 7))
|
|
g.Connect(BasicEdge(6, 8))
|
|
g.Connect(BasicEdge(7, 8))
|
|
g.TransitiveReduction()
|
|
|
|
actual := strings.TrimSpace(g.String())
|
|
expected := strings.TrimSpace(testGraphTransReductionMultipleRootsStr)
|
|
if actual != expected {
|
|
t.Fatalf("bad: %s", actual)
|
|
}
|
|
}
|
|
|
|
// use this to simulate slow sort operations
|
|
type counter struct {
|
|
Name string
|
|
Calls int64
|
|
}
|
|
|
|
func (s *counter) String() string {
|
|
s.Calls++
|
|
return s.Name
|
|
}
|
|
|
|
// Make sure we can reduce a sizable, fully-connected graph.
|
|
func TestAyclicGraphTransReduction_fullyConnected(t *testing.T) {
|
|
var g AcyclicGraph
|
|
|
|
const nodeCount = 200
|
|
nodes := make([]*counter, nodeCount)
|
|
for i := 0; i < nodeCount; i++ {
|
|
nodes[i] = &counter{Name: strconv.Itoa(i)}
|
|
}
|
|
|
|
// Add them all to the graph
|
|
for _, n := range nodes {
|
|
g.Add(n)
|
|
}
|
|
|
|
// connect them all
|
|
for i := range nodes {
|
|
for j := range nodes {
|
|
if i == j {
|
|
continue
|
|
}
|
|
g.Connect(BasicEdge(nodes[i], nodes[j]))
|
|
}
|
|
}
|
|
|
|
g.TransitiveReduction()
|
|
|
|
vertexNameCalls := int64(0)
|
|
for _, n := range nodes {
|
|
vertexNameCalls += n.Calls
|
|
}
|
|
|
|
switch {
|
|
case vertexNameCalls > 2*nodeCount:
|
|
// Make calling it more the 2x per node fatal.
|
|
// If we were sorting this would give us roughly ln(n)(n^3) calls, or
|
|
// >59000000 calls for 200 vertices.
|
|
t.Fatalf("VertexName called %d times", vertexNameCalls)
|
|
case vertexNameCalls > 0:
|
|
// we don't expect any calls, but a change here isn't necessarily fatal
|
|
t.Logf("WARNING: VertexName called %d times", vertexNameCalls)
|
|
}
|
|
}
|
|
|
|
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 TestAcyclicGraphValidate_cycleSelf(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Connect(BasicEdge(1, 1))
|
|
|
|
if err := g.Validate(); err == nil {
|
|
t.Fatal("should error")
|
|
}
|
|
}
|
|
|
|
func TestAcyclicGraphAncestors(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Add(4)
|
|
g.Add(5)
|
|
g.Connect(BasicEdge(0, 1))
|
|
g.Connect(BasicEdge(1, 2))
|
|
g.Connect(BasicEdge(2, 3))
|
|
g.Connect(BasicEdge(3, 4))
|
|
g.Connect(BasicEdge(4, 5))
|
|
|
|
actual, err := g.Ancestors(2)
|
|
if err != nil {
|
|
t.Fatalf("err: %#v", err)
|
|
}
|
|
|
|
expected := []Vertex{3, 4, 5}
|
|
|
|
if actual.Len() != len(expected) {
|
|
t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected))
|
|
}
|
|
|
|
for _, e := range expected {
|
|
if !actual.Include(e) {
|
|
t.Fatalf("expected: %#v to include: %#v", expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAcyclicGraphDescendents(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Add(4)
|
|
g.Add(5)
|
|
g.Connect(BasicEdge(0, 1))
|
|
g.Connect(BasicEdge(1, 2))
|
|
g.Connect(BasicEdge(2, 3))
|
|
g.Connect(BasicEdge(3, 4))
|
|
g.Connect(BasicEdge(4, 5))
|
|
|
|
actual, err := g.Descendents(2)
|
|
if err != nil {
|
|
t.Fatalf("err: %#v", err)
|
|
}
|
|
|
|
expected := []Vertex{0, 1}
|
|
|
|
if actual.Len() != len(expected) {
|
|
t.Fatalf("bad length! expected %#v to have len %d", actual, len(expected))
|
|
}
|
|
|
|
for _, e := range expected {
|
|
if !actual.Include(e) {
|
|
t.Fatalf("expected: %#v to include: %#v", expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAcyclicGraphWalk(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))
|
|
|
|
var visits []Vertex
|
|
var lock sync.Mutex
|
|
err := g.Walk(func(v Vertex) tfdiags.Diagnostics {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
visits = append(visits, v)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
expected := [][]Vertex{
|
|
{1, 2, 3},
|
|
{2, 1, 3},
|
|
}
|
|
for _, e := range expected {
|
|
if reflect.DeepEqual(visits, e) {
|
|
return
|
|
}
|
|
}
|
|
|
|
t.Fatalf("bad: %#v", visits)
|
|
}
|
|
|
|
func TestAcyclicGraphWalk_error(t *testing.T) {
|
|
var g AcyclicGraph
|
|
g.Add(1)
|
|
g.Add(2)
|
|
g.Add(3)
|
|
g.Add(4)
|
|
g.Connect(BasicEdge(4, 3))
|
|
g.Connect(BasicEdge(3, 2))
|
|
g.Connect(BasicEdge(2, 1))
|
|
|
|
var visits []Vertex
|
|
var lock sync.Mutex
|
|
err := g.Walk(func(v Vertex) tfdiags.Diagnostics {
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
if v == 2 {
|
|
diags = diags.Append(fmt.Errorf("error"))
|
|
return diags
|
|
}
|
|
|
|
visits = append(visits, v)
|
|
return diags
|
|
})
|
|
if err == nil {
|
|
t.Fatal("should error")
|
|
}
|
|
|
|
expected := []Vertex{1}
|
|
if !reflect.DeepEqual(visits, expected) {
|
|
t.Errorf("wrong visits\ngot: %#v\nwant: %#v", visits, expected)
|
|
}
|
|
|
|
}
|
|
|
|
func BenchmarkDAG(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
count := 150
|
|
b.StopTimer()
|
|
g := &AcyclicGraph{}
|
|
|
|
// create 4 layers of fully connected nodes
|
|
// layer A
|
|
for i := 0; i < count; i++ {
|
|
g.Add(fmt.Sprintf("A%d", i))
|
|
}
|
|
|
|
// layer B
|
|
for i := 0; i < count; i++ {
|
|
B := fmt.Sprintf("B%d", i)
|
|
g.Add(B)
|
|
for j := 0; j < count; j++ {
|
|
g.Connect(BasicEdge(B, fmt.Sprintf("A%d", j)))
|
|
}
|
|
}
|
|
|
|
// layer C
|
|
for i := 0; i < count; i++ {
|
|
c := fmt.Sprintf("C%d", i)
|
|
g.Add(c)
|
|
for j := 0; j < count; j++ {
|
|
// connect them to previous layers so we have something that requires reduction
|
|
g.Connect(BasicEdge(c, fmt.Sprintf("A%d", j)))
|
|
g.Connect(BasicEdge(c, fmt.Sprintf("B%d", j)))
|
|
}
|
|
}
|
|
|
|
// layer D
|
|
for i := 0; i < count; i++ {
|
|
d := fmt.Sprintf("D%d", i)
|
|
g.Add(d)
|
|
for j := 0; j < count; j++ {
|
|
g.Connect(BasicEdge(d, fmt.Sprintf("A%d", j)))
|
|
g.Connect(BasicEdge(d, fmt.Sprintf("B%d", j)))
|
|
g.Connect(BasicEdge(d, fmt.Sprintf("C%d", j)))
|
|
}
|
|
}
|
|
|
|
b.StartTimer()
|
|
// Find dependencies for every node
|
|
for _, v := range g.Vertices() {
|
|
_, err := g.Ancestors(v)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// reduce the final graph
|
|
g.TransitiveReduction()
|
|
}
|
|
}
|
|
|
|
func TestAcyclicGraphWalkOrder(t *testing.T) {
|
|
/* Sample dependency graph,
|
|
all edges pointing downwards.
|
|
1 2
|
|
/ \ / \
|
|
3 4 5
|
|
/ \ /
|
|
6 7
|
|
/ | \
|
|
8 9 10
|
|
\ | /
|
|
11
|
|
*/
|
|
|
|
var g AcyclicGraph
|
|
for i := 1; i <= 11; i++ {
|
|
g.Add(i)
|
|
}
|
|
g.Connect(BasicEdge(1, 3))
|
|
g.Connect(BasicEdge(1, 4))
|
|
g.Connect(BasicEdge(2, 4))
|
|
g.Connect(BasicEdge(2, 5))
|
|
g.Connect(BasicEdge(3, 6))
|
|
g.Connect(BasicEdge(4, 7))
|
|
g.Connect(BasicEdge(5, 7))
|
|
g.Connect(BasicEdge(7, 8))
|
|
g.Connect(BasicEdge(7, 9))
|
|
g.Connect(BasicEdge(7, 10))
|
|
g.Connect(BasicEdge(8, 11))
|
|
g.Connect(BasicEdge(9, 11))
|
|
g.Connect(BasicEdge(10, 11))
|
|
|
|
start := make(Set)
|
|
start.Add(2)
|
|
start.Add(1)
|
|
reverse := make(Set)
|
|
reverse.Add(11)
|
|
reverse.Add(6)
|
|
|
|
t.Run("DepthFirst", func(t *testing.T) {
|
|
var visits []vertexAtDepth
|
|
g.walk(depthFirst|downOrder, true, start, func(v Vertex, d int) error {
|
|
visits = append(visits, vertexAtDepth{v, d})
|
|
return nil
|
|
|
|
})
|
|
expect := []vertexAtDepth{
|
|
{2, 0}, {5, 1}, {7, 2}, {9, 3}, {11, 4}, {8, 3}, {10, 3}, {4, 1}, {1, 0}, {3, 1}, {6, 2},
|
|
}
|
|
if !reflect.DeepEqual(visits, expect) {
|
|
t.Errorf("expected visits:\n%v\ngot:\n%v\n", expect, visits)
|
|
}
|
|
})
|
|
t.Run("ReverseDepthFirst", func(t *testing.T) {
|
|
var visits []vertexAtDepth
|
|
g.walk(depthFirst|upOrder, true, reverse, func(v Vertex, d int) error {
|
|
visits = append(visits, vertexAtDepth{v, d})
|
|
return nil
|
|
|
|
})
|
|
expect := []vertexAtDepth{
|
|
{6, 0}, {3, 1}, {1, 2}, {11, 0}, {9, 1}, {7, 2}, {5, 3}, {2, 4}, {4, 3}, {8, 1}, {10, 1},
|
|
}
|
|
if !reflect.DeepEqual(visits, expect) {
|
|
t.Errorf("expected visits:\n%v\ngot:\n%v\n", expect, visits)
|
|
}
|
|
})
|
|
t.Run("BreadthFirst", func(t *testing.T) {
|
|
var visits []vertexAtDepth
|
|
g.walk(breadthFirst|downOrder, true, start, func(v Vertex, d int) error {
|
|
visits = append(visits, vertexAtDepth{v, d})
|
|
return nil
|
|
|
|
})
|
|
expect := []vertexAtDepth{
|
|
{1, 0}, {2, 0}, {3, 1}, {4, 1}, {5, 1}, {6, 2}, {7, 2}, {10, 3}, {8, 3}, {9, 3}, {11, 4},
|
|
}
|
|
if !reflect.DeepEqual(visits, expect) {
|
|
t.Errorf("expected visits:\n%v\ngot:\n%v\n", expect, visits)
|
|
}
|
|
})
|
|
t.Run("ReverseBreadthFirst", func(t *testing.T) {
|
|
var visits []vertexAtDepth
|
|
g.walk(breadthFirst|upOrder, true, reverse, func(v Vertex, d int) error {
|
|
visits = append(visits, vertexAtDepth{v, d})
|
|
return nil
|
|
|
|
})
|
|
expect := []vertexAtDepth{
|
|
{11, 0}, {6, 0}, {10, 1}, {8, 1}, {9, 1}, {3, 1}, {7, 2}, {1, 2}, {4, 3}, {5, 3}, {2, 4},
|
|
}
|
|
if !reflect.DeepEqual(visits, expect) {
|
|
t.Errorf("expected visits:\n%v\ngot:\n%v\n", expect, visits)
|
|
}
|
|
})
|
|
|
|
t.Run("TopologicalOrder", func(t *testing.T) {
|
|
order := g.topoOrder(downOrder)
|
|
|
|
// Validate the order by checking it against the initial graph. We only
|
|
// need to verify that each node has it's direct dependencies
|
|
// satisfied.
|
|
completed := map[Vertex]bool{}
|
|
for _, v := range order {
|
|
deps := g.DownEdges(v)
|
|
for _, dep := range deps {
|
|
if !completed[dep] {
|
|
t.Fatalf("walking node %v, but dependency %v was not yet seen", v, dep)
|
|
}
|
|
}
|
|
completed[v] = true
|
|
}
|
|
})
|
|
t.Run("ReverseTopologicalOrder", func(t *testing.T) {
|
|
order := g.topoOrder(upOrder)
|
|
|
|
// Validate the order by checking it against the initial graph. We only
|
|
// need to verify that each node has it's direct dependencies
|
|
// satisfied.
|
|
completed := map[Vertex]bool{}
|
|
for _, v := range order {
|
|
deps := g.UpEdges(v)
|
|
for _, dep := range deps {
|
|
if !completed[dep] {
|
|
t.Fatalf("walking node %v, but dependency %v was not yet seen", v, dep)
|
|
}
|
|
}
|
|
completed[v] = true
|
|
}
|
|
})
|
|
}
|
|
|
|
const testGraphTransReductionStr = `
|
|
1
|
|
2
|
|
2
|
|
3
|
|
3
|
|
`
|
|
|
|
const testGraphTransReductionMoreStr = `
|
|
1
|
|
2
|
|
2
|
|
3
|
|
3
|
|
4
|
|
4
|
|
`
|
|
|
|
const testGraphTransReductionMultipleRootsStr = `
|
|
1
|
|
2
|
|
2
|
|
3
|
|
3
|
|
4
|
|
4
|
|
5
|
|
6
|
|
6
|
|
7
|
|
7
|
|
8
|
|
8
|
|
`
|