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] } }