216 lines
6.2 KiB
Go
216 lines
6.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestScheduleDependenciesOrdersReadyNodes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
keys []string
|
|
edges [][2]string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "independent branches",
|
|
keys: []string{"branch-c", "branch-a", "branch-b"},
|
|
want: []string{"branch-a", "branch-b", "branch-c"},
|
|
},
|
|
{
|
|
name: "unequal chains",
|
|
keys: []string{"chain-d", "chain-c", "chain-b", "chain-a"},
|
|
edges: [][2]string{
|
|
{"chain-a", "chain-b"},
|
|
{"chain-b", "chain-c"},
|
|
{"chain-c", "chain-d"},
|
|
},
|
|
want: []string{"chain-a", "chain-b", "chain-c", "chain-d"},
|
|
},
|
|
{
|
|
name: "multiple dependencies",
|
|
keys: []string{"application", "right", "base", "left"},
|
|
edges: [][2]string{
|
|
{"base", "left"}, {"base", "right"},
|
|
{"left", "application"}, {"right", "application"},
|
|
},
|
|
want: []string{"base", "left", "right", "application"},
|
|
},
|
|
{
|
|
name: "shared dependency at equal depths",
|
|
keys: []string{"sink", "right", "root", "left"},
|
|
edges: [][2]string{
|
|
{"root", "left"}, {"root", "right"},
|
|
{"left", "sink"}, {"right", "sink"},
|
|
},
|
|
want: []string{"root", "left", "right", "sink"},
|
|
},
|
|
{
|
|
name: "shared dependency at unequal depths",
|
|
keys: []string{"sink", "deep", "side", "middle", "root"},
|
|
edges: [][2]string{
|
|
{"root", "middle"}, {"middle", "deep"}, {"root", "side"},
|
|
{"deep", "sink"}, {"side", "sink"},
|
|
},
|
|
want: []string{"root", "middle", "deep", "side", "sink"},
|
|
},
|
|
{
|
|
name: "newly ready small key precedes older large key",
|
|
keys: []string{"z-ready", "a-new", "b-ready"},
|
|
edges: [][2]string{{"b-ready", "a-new"}},
|
|
want: []string{"b-ready", "a-new", "z-ready"},
|
|
},
|
|
{
|
|
name: "duplicate edges count once",
|
|
keys: []string{"dependent", "prerequisite"},
|
|
edges: [][2]string{
|
|
{"prerequisite", "dependent"},
|
|
{"prerequisite", "dependent"},
|
|
},
|
|
want: []string{"prerequisite", "dependent"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
keys, edges, indexes := dependencyGraph(test.keys, test.edges)
|
|
order, cycle := scheduleDependencies(keys, edges)
|
|
if len(cycle) != 0 {
|
|
t.Fatalf("unexpected cycle: %v", dependencyNames(cycle, indexes))
|
|
}
|
|
if got := dependencyNames(order, indexes); !reflect.DeepEqual(got, test.want) {
|
|
t.Fatalf("schedule = %v, want %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScheduleDependenciesUsesTupleKeys(t *testing.T) {
|
|
keys := []dependencyKey{
|
|
{"a", "z"},
|
|
{"a"},
|
|
{"a", "aa"},
|
|
{"A"},
|
|
}
|
|
order, cycle := scheduleDependencies(keys, nil)
|
|
if len(cycle) != 0 {
|
|
t.Fatalf("unexpected cycle: %v", cycle)
|
|
}
|
|
want := []int{3, 1, 2, 0}
|
|
if !reflect.DeepEqual(order, want) {
|
|
t.Fatalf("schedule = %v, want %v", order, want)
|
|
}
|
|
}
|
|
|
|
func TestScheduleDependenciesReportsActualCanonicalCycles(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
keys []string
|
|
edges [][2]string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "self cycle",
|
|
keys: []string{"self"},
|
|
edges: [][2]string{{"self", "self"}},
|
|
want: []string{"self", "self"},
|
|
},
|
|
{
|
|
name: "multi-node cycle excludes blocked dependent",
|
|
keys: []string{"blocked", "cycle-b", "cycle-a"},
|
|
edges: [][2]string{
|
|
{"cycle-a", "cycle-b"},
|
|
{"cycle-b", "cycle-a"},
|
|
{"cycle-b", "blocked"},
|
|
},
|
|
want: []string{"cycle-a", "cycle-b", "cycle-a"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
keys, edges, indexes := dependencyGraph(test.keys, test.edges)
|
|
order, cycle := scheduleDependencies(keys, edges)
|
|
if len(order) != 0 {
|
|
t.Fatalf("unexpected schedule: %v", dependencyNames(order, indexes))
|
|
}
|
|
if got := dependencyNames(cycle, indexes); !reflect.DeepEqual(got, test.want) {
|
|
t.Fatalf("cycle = %v, want %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScheduleDependenciesIsStableAcrossInsertionPermutations(t *testing.T) {
|
|
edges := [][2]string{
|
|
{"base", "left"}, {"base", "right"},
|
|
{"left", "sink"}, {"right", "sink"},
|
|
}
|
|
permutations := [][]string{
|
|
{"sink", "right", "base", "left"},
|
|
{"left", "base", "sink", "right"},
|
|
{"right", "left", "base", "sink"},
|
|
}
|
|
for i, permutation := range permutations {
|
|
keys, permutedEdges, indexes := dependencyGraph(permutation, edges)
|
|
if i%2 != 0 {
|
|
reverseDependencyEdges(permutedEdges)
|
|
}
|
|
order, cycle := scheduleDependencies(keys, permutedEdges)
|
|
if len(cycle) != 0 {
|
|
t.Fatalf("permutation %d returned cycle %v", i, dependencyNames(cycle, indexes))
|
|
}
|
|
if got, want := dependencyNames(order, indexes), []string{"base", "left", "right", "sink"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("permutation %d schedule = %v, want %v", i, got, want)
|
|
}
|
|
}
|
|
|
|
cycleEdges := [][2]string{{"b", "a"}, {"a", "b"}, {"b", "blocked"}}
|
|
cyclePermutations := [][]string{{"blocked", "b", "a"}, {"a", "blocked", "b"}, {"b", "a", "blocked"}}
|
|
for i, permutation := range cyclePermutations {
|
|
keys, permutedEdges, indexes := dependencyGraph(permutation, cycleEdges)
|
|
if i%2 != 0 {
|
|
reverseDependencyEdges(permutedEdges)
|
|
}
|
|
order, cycle := scheduleDependencies(keys, permutedEdges)
|
|
if len(order) != 0 {
|
|
t.Fatalf("cycle permutation %d returned schedule %v", i, dependencyNames(order, indexes))
|
|
}
|
|
if got, want := dependencyNames(cycle, indexes), []string{"a", "b", "a"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("cycle permutation %d returned %v, want %v", i, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func dependencyGraph(nodeNames []string, namedEdges [][2]string) ([]dependencyKey, []dependencyEdge, map[int]string) {
|
|
keys := make([]dependencyKey, len(nodeNames))
|
|
indexes := make(map[string]int, len(nodeNames))
|
|
for i, name := range nodeNames {
|
|
keys[i] = dependencyKey{name}
|
|
indexes[name] = i
|
|
}
|
|
edges := make([]dependencyEdge, len(namedEdges))
|
|
for i, edge := range namedEdges {
|
|
edges[i] = dependencyEdge{prerequisite: indexes[edge[0]], dependent: indexes[edge[1]]}
|
|
}
|
|
byIndex := make(map[int]string, len(nodeNames))
|
|
for name, index := range indexes {
|
|
byIndex[index] = name
|
|
}
|
|
return keys, edges, byIndex
|
|
}
|
|
|
|
func dependencyNames(nodes []int, names map[int]string) []string {
|
|
result := make([]string, len(nodes))
|
|
for i, node := range nodes {
|
|
result[i] = names[node]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func reverseDependencyEdges(edges []dependencyEdge) {
|
|
for i, j := 0, len(edges)-1; i < j; i, j = i+1, j-1 {
|
|
edges[i], edges[j] = edges[j], edges[i]
|
|
}
|
|
}
|