fix: Make CyclicDependencyError edges stable

- Add sorting for Edges & Verticies
- Improve tests that contain CyclicDependencyError
This commit is contained in:
Karl Isenberg 2022-01-19 15:24:39 -08:00
parent 8cfbb396a7
commit 6abffa8abe
3 changed files with 386 additions and 107 deletions

View File

@ -18,6 +18,8 @@ import (
"sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/inventory" "sigs.k8s.io/cli-utils/pkg/inventory"
"sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/object/graph"
"sigs.k8s.io/cli-utils/pkg/object/validation"
"sigs.k8s.io/cli-utils/pkg/testutil" "sigs.k8s.io/cli-utils/pkg/testutil"
) )
@ -107,12 +109,11 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
applyObjs []*unstructured.Unstructured applyObjs []*unstructured.Unstructured
options Options options Options
expectedTasks []taskrunner.Task expectedTasks []taskrunner.Task
isError bool expectedError error
}{ }{
"no resources, no tasks": { "no resources, no tasks": {
applyObjs: []*unstructured.Unstructured{}, applyObjs: []*unstructured.Unstructured{},
expectedTasks: []taskrunner.Task{}, expectedTasks: []taskrunner.Task{},
isError: false,
}, },
"single resource, one apply task, one wait task": { "single resource, one apply task, one wait task": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -134,7 +135,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"multiple resource with no timeout": { "multiple resource with no timeout": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -159,7 +159,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"multiple resources with reconcile timeout": { "multiple resources with reconcile timeout": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -187,7 +186,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"multiple resources with reconcile timeout and dryrun": { "multiple resources with reconcile timeout and dryrun": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -208,7 +206,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
}, },
}, },
}, },
isError: false,
}, },
"multiple resources with reconcile timeout and server-dryrun": { "multiple resources with reconcile timeout and server-dryrun": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -229,7 +226,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
}, },
}, },
}, },
isError: false,
}, },
"multiple resources including CRD": { "multiple resources including CRD": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -269,7 +265,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"no wait with CRDs if it is a dryrun": { "no wait with CRDs if it is a dryrun": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -296,7 +291,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
}, },
}, },
}, },
isError: false,
}, },
"resources in namespace creates multiple apply tasks": { "resources in namespace creates multiple apply tasks": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -336,7 +330,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"deployment depends on secret creates multiple tasks": { "deployment depends on secret creates multiple tasks": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -362,7 +355,8 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
&task.ApplyTask{ &task.ApplyTask{
TaskName: "apply-1", TaskName: "apply-1",
Objects: []*unstructured.Unstructured{ Objects: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["deployment"]), testutil.Unstructured(t, resources["deployment"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
}, },
}, },
taskrunner.NewWaitTask( taskrunner.NewWaitTask(
@ -374,7 +368,6 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"cyclic dependency returns error": { "cyclic dependency returns error": {
applyObjs: []*unstructured.Unstructured{ applyObjs: []*unstructured.Unstructured{
@ -384,7 +377,22 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))),
}, },
expectedTasks: []taskrunner.Task{}, expectedTasks: []taskrunner.Task{},
isError: true, expectedError: validation.NewError(
graph.CyclicDependencyError{
Edges: []graph.Edge{
{
From: testutil.ToIdentifier(t, resources["secret"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
{
From: testutil.ToIdentifier(t, resources["deployment"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
testutil.ToIdentifier(t, resources["secret"]),
testutil.ToIdentifier(t, resources["deployment"]),
),
}, },
} }
@ -403,11 +411,11 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
[]mutator.Interface{}, []mutator.Interface{},
tc.options, tc.options,
).Build() ).Build()
if tc.isError { if tc.expectedError != nil {
assert.NotNil(t, err, "expected error, but received none") assert.EqualError(t, err, tc.expectedError.Error())
return return
} }
assert.Nil(t, err, "unexpected error received") assert.NoError(t, err)
assert.Equal(t, len(tc.expectedTasks), len(tq.tasks)) assert.Equal(t, len(tc.expectedTasks), len(tq.tasks))
for i, expTask := range tc.expectedTasks { for i, expTask := range tc.expectedTasks {
actualTask := tq.tasks[i] actualTask := tq.tasks[i]
@ -417,18 +425,11 @@ func TestTaskQueueBuilder_AppendApplyWaitTasks(t *testing.T) {
switch expTsk := expTask.(type) { switch expTsk := expTask.(type) {
case *task.ApplyTask: case *task.ApplyTask:
actApplyTask := toApplyTask(t, actualTask) actApplyTask := toApplyTask(t, actualTask)
assert.Equal(t, len(expTsk.Objects), len(actApplyTask.Objects)) testutil.AssertEqual(t, expTsk.Objects, actApplyTask.Objects, "ApplyTask mismatch")
// Order is NOT important for objects stored within task.
verifyObjSets(t, expTsk.Objects, actApplyTask.Objects)
case *taskrunner.WaitTask: case *taskrunner.WaitTask:
actWaitTask := toWaitTask(t, actualTask) actWaitTask := toWaitTask(t, actualTask)
assert.Equal(t, len(expTsk.Ids), len(actWaitTask.Ids)) testutil.AssertEqual(t, expTsk.Ids, actWaitTask.Ids)
// Order is NOT important for ids stored within task. assert.Equal(t, taskrunner.AllCurrent, actWaitTask.Condition, "WaitTask mismatch")
if !expTsk.Ids.Equal(actWaitTask.Ids) {
t.Errorf("expected wait ids (%v), got (%v)",
expTsk.Ids, actWaitTask.Ids)
}
assert.Equal(t, taskrunner.AllCurrent, actWaitTask.Condition)
} }
} }
}) })
@ -440,13 +441,12 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
pruneObjs []*unstructured.Unstructured pruneObjs []*unstructured.Unstructured
options Options options Options
expectedTasks []taskrunner.Task expectedTasks []taskrunner.Task
isError bool expectedError error
}{ }{
"no resources, no tasks": { "no resources, no tasks": {
pruneObjs: []*unstructured.Unstructured{}, pruneObjs: []*unstructured.Unstructured{},
options: Options{Prune: true}, options: Options{Prune: true},
expectedTasks: []taskrunner.Task{}, expectedTasks: []taskrunner.Task{},
isError: false,
}, },
"single resource, one prune task, one wait task": { "single resource, one prune task, one wait task": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -469,7 +469,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"multiple resources, one prune task, one wait task": { "multiple resources, one prune task, one wait task": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -495,7 +494,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"dependent resources, two prune tasks, two wait tasks": { "dependent resources, two prune tasks, two wait tasks": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -509,7 +507,8 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
&task.PruneTask{ &task.PruneTask{
TaskName: "prune-0", TaskName: "prune-0",
Objects: []*unstructured.Unstructured{ Objects: []*unstructured.Unstructured{
testutil.Unstructured(t, resources["pod"]), testutil.Unstructured(t, resources["pod"],
testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))),
}, },
}, },
taskrunner.NewWaitTask( taskrunner.NewWaitTask(
@ -535,7 +534,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"single resource with prune timeout has wait task": { "single resource with prune timeout has wait task": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -562,7 +560,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"multiple resources with prune timeout and server-dryrun": { "multiple resources with prune timeout and server-dryrun": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -584,7 +581,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
}, },
}, },
}, },
isError: false,
}, },
"multiple resources including CRD": { "multiple resources including CRD": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -626,7 +622,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"no wait with CRDs if it is a dryrun": { "no wait with CRDs if it is a dryrun": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -654,7 +649,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
}, },
}, },
}, },
isError: false,
}, },
"resources in namespace creates multiple apply tasks": { "resources in namespace creates multiple apply tasks": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -695,7 +689,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
testutil.NewFakeRESTMapper(), testutil.NewFakeRESTMapper(),
), ),
}, },
isError: false,
}, },
"cyclic dependency returns error": { "cyclic dependency returns error": {
pruneObjs: []*unstructured.Unstructured{ pruneObjs: []*unstructured.Unstructured{
@ -706,7 +699,22 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
}, },
options: Options{Prune: true}, options: Options{Prune: true},
expectedTasks: []taskrunner.Task{}, expectedTasks: []taskrunner.Task{},
isError: true, expectedError: validation.NewError(
graph.CyclicDependencyError{
Edges: []graph.Edge{
{
From: testutil.ToIdentifier(t, resources["secret"]),
To: testutil.ToIdentifier(t, resources["deployment"]),
},
{
From: testutil.ToIdentifier(t, resources["deployment"]),
To: testutil.ToIdentifier(t, resources["secret"]),
},
},
},
testutil.ToIdentifier(t, resources["secret"]),
testutil.ToIdentifier(t, resources["deployment"]),
),
}, },
} }
@ -721,11 +729,11 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
} }
emptyPruneFilters := []filter.ValidationFilter{} emptyPruneFilters := []filter.ValidationFilter{}
tq, err := tqb.AppendPruneWaitTasks(tc.pruneObjs, emptyPruneFilters, tc.options).Build() tq, err := tqb.AppendPruneWaitTasks(tc.pruneObjs, emptyPruneFilters, tc.options).Build()
if tc.isError { if tc.expectedError != nil {
assert.NotNil(t, err, "expected error, but received none") assert.EqualError(t, err, tc.expectedError.Error())
return return
} }
assert.Nil(t, err, "unexpected error received") assert.NoError(t, err)
assert.Equal(t, len(tc.expectedTasks), len(tq.tasks)) assert.Equal(t, len(tc.expectedTasks), len(tq.tasks))
for i, expTask := range tc.expectedTasks { for i, expTask := range tc.expectedTasks {
actualTask := tq.tasks[i] actualTask := tq.tasks[i]
@ -735,16 +743,11 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
switch expTsk := expTask.(type) { switch expTsk := expTask.(type) {
case *task.PruneTask: case *task.PruneTask:
actPruneTask := toPruneTask(t, actualTask) actPruneTask := toPruneTask(t, actualTask)
assert.Equal(t, len(expTsk.Objects), len(actPruneTask.Objects)) testutil.AssertEqual(t, expTsk.Objects, actPruneTask.Objects, "PruneTask mismatch")
verifyObjSets(t, expTsk.Objects, actPruneTask.Objects)
case *taskrunner.WaitTask: case *taskrunner.WaitTask:
actWaitTask := toWaitTask(t, actualTask) actWaitTask := toWaitTask(t, actualTask)
assert.Equal(t, len(expTsk.Ids), len(actWaitTask.Ids)) testutil.AssertEqual(t, expTsk.Ids, actWaitTask.Ids)
if !expTsk.Ids.Equal(actWaitTask.Ids) { assert.Equal(t, taskrunner.AllNotFound, actWaitTask.Condition, "WaitTask mismatch")
t.Errorf("expected wait ids (%v), got (%v)",
expTsk.Ids, actWaitTask.Ids)
}
assert.Equal(t, taskrunner.AllNotFound, actWaitTask.Condition)
// Validate the prune wait timeout. // Validate the prune wait timeout.
assert.Equal(t, tc.options.PruneTimeout, actualTask.(*taskrunner.WaitTask).Timeout) assert.Equal(t, tc.options.PruneTimeout, actualTask.(*taskrunner.WaitTask).Timeout)
} }
@ -753,32 +756,6 @@ func TestTaskQueueBuilder_AppendPruneWaitTasks(t *testing.T) {
} }
} }
// verifyObjSets ensures the slice of expected objects is the same as
// the actual slice of objects. Order is NOT important.
func verifyObjSets(t *testing.T, expected []*unstructured.Unstructured, actual []*unstructured.Unstructured) {
if len(expected) != len(actual) {
t.Fatalf("expected set size (%d), got (%d)", len(expected), len(actual))
}
for _, obj := range expected {
if !containsObj(actual, obj) {
t.Fatalf("expected object (%v) not in found", obj)
}
}
}
// containsObj returns true if the passed object is within the passed
// slice of objects; false otherwise.
func containsObj(objs []*unstructured.Unstructured, obj *unstructured.Unstructured) bool {
ids := object.UnstructuredSetToObjMetadataSet(objs)
id := object.UnstructuredToObjMetadata(obj)
for _, i := range ids {
if i == id {
return true
}
}
return false
}
func toWaitTask(t *testing.T, task taskrunner.Task) *taskrunner.WaitTask { func toWaitTask(t *testing.T, task taskrunner.Task) *taskrunner.WaitTask {
switch tsk := task.(type) { switch tsk := task.(type) {
case *taskrunner.WaitTask: case *taskrunner.WaitTask:

View File

@ -9,10 +9,12 @@ package graph
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"sort"
"sigs.k8s.io/cli-utils/pkg/multierror" "sigs.k8s.io/cli-utils/pkg/multierror"
"sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/object/validation" "sigs.k8s.io/cli-utils/pkg/object/validation"
"sigs.k8s.io/cli-utils/pkg/ordering"
) )
// Graph is contains a directed set of edges, implemented as // Graph is contains a directed set of edges, implemented as
@ -45,7 +47,7 @@ func (g *Graph) AddVertex(v object.ObjMetadata) {
} }
} }
// GetVertices returns an unsorted set of unique vertices in the graph. // GetVertices returns a sorted set of unique vertices in the graph.
func (g *Graph) GetVertices() object.ObjMetadataSet { func (g *Graph) GetVertices() object.ObjMetadataSet {
keys := make(object.ObjMetadataSet, len(g.edges)) keys := make(object.ObjMetadataSet, len(g.edges))
i := 0 i := 0
@ -53,6 +55,7 @@ func (g *Graph) GetVertices() object.ObjMetadataSet {
keys[i] = k keys[i] = k
i++ i++
} }
sort.Sort(ordering.SortableMetas(keys))
return keys return keys
} }
@ -74,8 +77,7 @@ func (g *Graph) AddEdge(from object.ObjMetadata, to object.ObjMetadata) {
} }
} }
// GetEdges returns the slice of vertex pairs which are // GetEdges returns a sorted slice of directed graph edges (vertex pairs).
// the directed edges of the graph.
func (g *Graph) GetEdges() []Edge { func (g *Graph) GetEdges() []Edge {
edges := []Edge{} edges := []Edge{}
for from, toList := range g.edges { for from, toList := range g.edges {
@ -84,6 +86,7 @@ func (g *Graph) GetEdges() []Edge {
edges = append(edges, edge) edges = append(edges, edge)
} }
} }
sort.Sort(SortableEdges(edges))
return edges return edges
} }
@ -164,3 +167,30 @@ func (cde CyclicDependencyError) Error() string {
} }
return errorBuf.String() return errorBuf.String()
} }
// SortableEdges sorts a list of edges alphanumerically by From and then To.
type SortableEdges []Edge
var _ sort.Interface = SortableEdges{}
func (a SortableEdges) Len() int { return len(a) }
func (a SortableEdges) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SortableEdges) Less(i, j int) bool {
if a[i].From != a[j].From {
return metaIsLessThan(a[i].From, a[j].From)
}
return metaIsLessThan(a[i].To, a[j].To)
}
func metaIsLessThan(i, j object.ObjMetadata) bool {
if i.GroupKind.Group != j.GroupKind.Group {
return i.GroupKind.Group < j.GroupKind.Group
}
if i.GroupKind.Kind != j.GroupKind.Kind {
return i.GroupKind.Kind < j.GroupKind.Kind
}
if i.Namespace != j.Namespace {
return i.Namespace < j.Namespace
}
return i.Name < j.Name
}

View File

@ -6,10 +6,14 @@
package graph package graph
import ( import (
"sort"
"testing" "testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/object/validation"
"sigs.k8s.io/cli-utils/pkg/testutil"
) )
var ( var (
@ -36,61 +40,86 @@ func TestObjectGraphSort(t *testing.T) {
vertices object.ObjMetadataSet vertices object.ObjMetadataSet
edges []Edge edges []Edge
expected []object.ObjMetadataSet expected []object.ObjMetadataSet
isError bool expectedError error
}{ }{
"one edge": { "one edge": {
vertices: object.ObjMetadataSet{o1, o2}, vertices: object.ObjMetadataSet{o1, o2},
edges: []Edge{e1}, edges: []Edge{e1},
expected: []object.ObjMetadataSet{{o2}, {o1}}, expected: []object.ObjMetadataSet{{o2}, {o1}},
isError: false,
}, },
"two edges": { "two edges": {
vertices: object.ObjMetadataSet{o1, o2, o3}, vertices: object.ObjMetadataSet{o1, o2, o3},
edges: []Edge{e1, e2}, edges: []Edge{e1, e2},
expected: []object.ObjMetadataSet{{o3}, {o2}, {o1}}, expected: []object.ObjMetadataSet{{o3}, {o2}, {o1}},
isError: false,
}, },
"three edges": { "three edges": {
vertices: object.ObjMetadataSet{o1, o2, o3}, vertices: object.ObjMetadataSet{o1, o2, o3},
edges: []Edge{e1, e3, e2}, edges: []Edge{e1, e3, e2},
expected: []object.ObjMetadataSet{{o3}, {o2}, {o1}}, expected: []object.ObjMetadataSet{{o3}, {o2}, {o1}},
isError: false,
}, },
"four edges": { "four edges": {
vertices: object.ObjMetadataSet{o1, o2, o3, o4}, vertices: object.ObjMetadataSet{o1, o2, o3, o4},
edges: []Edge{e1, e2, e4, e5}, edges: []Edge{e1, e2, e4, e5},
expected: []object.ObjMetadataSet{{o4}, {o3}, {o2}, {o1}}, expected: []object.ObjMetadataSet{{o4}, {o3}, {o2}, {o1}},
isError: false,
}, },
"five edges": { "five edges": {
vertices: object.ObjMetadataSet{o1, o2, o3, o4}, vertices: object.ObjMetadataSet{o1, o2, o3, o4},
edges: []Edge{e5, e1, e3, e2, e4}, edges: []Edge{e5, e1, e3, e2, e4},
expected: []object.ObjMetadataSet{{o4}, {o3}, {o2}, {o1}}, expected: []object.ObjMetadataSet{{o4}, {o3}, {o2}, {o1}},
isError: false,
}, },
"no edges means all in the same first set": { "no edges means all in the same first set": {
vertices: object.ObjMetadataSet{o1, o2, o3, o4}, vertices: object.ObjMetadataSet{o1, o2, o3, o4},
edges: []Edge{}, edges: []Edge{},
expected: []object.ObjMetadataSet{{o4, o3, o2, o1}}, expected: []object.ObjMetadataSet{{o4, o3, o2, o1}},
isError: false,
}, },
"multiple objects in first set": { "multiple objects in first set": {
vertices: object.ObjMetadataSet{o1, o2, o3, o4, o5}, vertices: object.ObjMetadataSet{o1, o2, o3, o4, o5},
edges: []Edge{e1, e2, e5, e8}, edges: []Edge{e1, e2, e5, e8},
expected: []object.ObjMetadataSet{{o5, o3}, {o4}, {o2}, {o1}}, expected: []object.ObjMetadataSet{{o5, o3}, {o4}, {o2}, {o1}},
isError: false,
}, },
"simple cycle in graph is an error": { "simple cycle in graph is an error": {
vertices: object.ObjMetadataSet{o1, o2}, vertices: object.ObjMetadataSet{o1, o2},
edges: []Edge{e1, e6}, edges: []Edge{e1, e6},
expected: []object.ObjMetadataSet{}, expected: []object.ObjMetadataSet{},
isError: true, expectedError: validation.NewError(
CyclicDependencyError{
Edges: []Edge{
{
From: o1,
To: o2,
},
{
From: o2,
To: o1,
},
},
},
o1, o2,
),
}, },
"multi-edge cycle in graph is an error": { "multi-edge cycle in graph is an error": {
vertices: object.ObjMetadataSet{o1, o2, o3}, vertices: object.ObjMetadataSet{o1, o2, o3},
edges: []Edge{e1, e2, e7}, edges: []Edge{e1, e2, e7},
expected: []object.ObjMetadataSet{}, expected: []object.ObjMetadataSet{},
isError: true, expectedError: validation.NewError(
CyclicDependencyError{
Edges: []Edge{
{
From: o1,
To: o2,
},
{
From: o2,
To: o3,
},
{
From: o3,
To: o1,
},
},
},
o1, o2, o3,
),
}, },
} }
@ -104,23 +133,266 @@ func TestObjectGraphSort(t *testing.T) {
g.AddEdge(edge.From, edge.To) g.AddEdge(edge.From, edge.To)
} }
actual, err := g.Sort() actual, err := g.Sort()
if err == nil && tc.isError { if tc.expectedError != nil {
t.Fatalf("expected error, but received none") assert.EqualError(t, tc.expectedError, err.Error())
} return
if err != nil && !tc.isError {
t.Errorf("unexpected error: %s", err)
}
if !tc.isError {
if len(actual) != len(tc.expected) {
t.Errorf("expected (%s), got (%s)", tc.expected, actual)
}
for i, actualSet := range actual {
expectedSet := tc.expected[i]
if !expectedSet.Equal(actualSet) {
t.Errorf("expected sorted objects (%s), got (%s)", tc.expected, actual)
}
}
} }
assert.NoError(t, err)
testutil.AssertEqual(t, tc.expected, actual)
})
}
}
func TestEdgeSort(t *testing.T) {
testCases := map[string]struct {
edges []Edge
expected []Edge
}{
"one edge": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
},
},
"two edges no change": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
},
},
"two edges same from": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj3"},
},
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj3"},
},
},
},
"two edges": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
},
},
"two edges by name": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj3", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
{
From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
{
From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj3", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
},
},
"three edges": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj3"},
To: object.ObjMetadata{Name: "obj4"},
},
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
{
From: object.ObjMetadata{Name: "obj3"},
To: object.ObjMetadata{Name: "obj4"},
},
},
},
"two edges cycle": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
{
From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
{
From: object.ObjMetadata{Name: "obj2", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
To: object.ObjMetadata{Name: "obj1", Namespace: "ns1", GroupKind: schema.GroupKind{Kind: "Pod"}},
},
},
},
"three edges cycle": {
edges: []Edge{
{
From: object.ObjMetadata{Name: "obj3"},
To: object.ObjMetadata{Name: "obj1"},
},
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
},
expected: []Edge{
{
From: object.ObjMetadata{Name: "obj1"},
To: object.ObjMetadata{Name: "obj2"},
},
{
From: object.ObjMetadata{Name: "obj2"},
To: object.ObjMetadata{Name: "obj3"},
},
{
From: object.ObjMetadata{Name: "obj3"},
To: object.ObjMetadata{Name: "obj1"},
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
sort.Sort(SortableEdges(tc.edges))
assert.Equal(t, tc.expected, tc.edges)
})
}
}
func TestCyclicDependencyErrorString(t *testing.T) {
testCases := map[string]struct {
cycle CyclicDependencyError
expectedString string
}{
"two object cycle": {
cycle: CyclicDependencyError{
Edges: []Edge{
{
From: o1,
To: o2,
},
{
From: o2,
To: o1,
},
},
},
expectedString: `cyclic dependency:
- /obj1 -> /obj2
- /obj2 -> /obj1
`,
},
"three object cycle": {
cycle: CyclicDependencyError{
Edges: []Edge{
{
From: o1,
To: o2,
},
{
From: o2,
To: o3,
},
{
From: o3,
To: o1,
},
},
},
expectedString: `cyclic dependency:
- /obj1 -> /obj2
- /obj2 -> /obj3
- /obj3 -> /obj1
`,
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
assert.Equal(t, tc.expectedString, tc.cycle.Error())
}) })
} }
} }