diff --git a/pkg/test/assertions/assertions.go b/pkg/test/assertions.go similarity index 87% rename from pkg/test/assertions/assertions.go rename to pkg/test/assertions.go index 160c98e8..17fba819 100644 --- a/pkg/test/assertions/assertions.go +++ b/pkg/test/assertions.go @@ -1,18 +1,16 @@ -package assertions +package test import ( "fmt" "reflect" "strings" - "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" - "sigs.k8s.io/controller-runtime/pkg/client" ) // AssertThat is a helper function that tests that the provided object satisfies given predicate. -// It is a exactly implemented as: +// If it accepted only a single predicate, it would be exactly implemented as: // // assert.True(t, predicate.Matches(object)) // @@ -26,7 +24,7 @@ import ( // // Note that this method accepts multiple predicates and reports any failures in them using // the Explain function. -func AssertThat(t *testing.T, object client.Object, predicates ...Predicate[client.Object]) { +func AssertThat(t T, object any, predicates ...Predicate[any]) { t.Helper() message := assertThat(object, predicates...) if message != "" { @@ -37,7 +35,7 @@ func AssertThat(t *testing.T, object client.Object, predicates ...Predicate[clie // assertThat contains the actual logic of the AssertThat function. This is separated out into // its own testable function because we cannot cannot capture the result of assert.Fail() in // another test. -func assertThat(object client.Object, predicates ...Predicate[client.Object]) string { +func assertThat(object any, predicates ...Predicate[any]) string { results := make([]bool, len(predicates)) failure := false for i, p := range predicates { @@ -70,7 +68,7 @@ func assertThat(object client.Object, predicates ...Predicate[client.Object]) st // // Note that this function doesn't actually check if the predicate matches the object so it can produce // slightly misleading output if called with a predicate that matches given object. -func Explain[T client.Object](predicate Predicate[client.Object], actual T) string { +func Explain[T any](predicate Predicate[any], actual T) string { // this is used for reporting the type of the predicate var reportedPredicateType reflect.Type @@ -86,7 +84,7 @@ func Explain[T client.Object](predicate Predicate[client.Object], actual T) stri predVal = predVal.Elem() } typName := predVal.Type().Name() - if strings.HasPrefix(typName, "cast[") { + if strings.HasPrefix(typName, "cast[") || strings.HasPrefix(typName, "fixingCast[") { // Interestingly, predVal.FieldByName("Inner").Type() returns the type of the field // not the type of the value. So we need to get the actual value using .Interface() // and get the type of that. Also notice, that in order to be able to call .Interface() @@ -101,12 +99,12 @@ func Explain[T client.Object](predicate Predicate[client.Object], actual T) stri } prefix := fmt.Sprintf("predicate '%s' didn't match the object", reportedPredicateType.String()) - fix, ok := predicate.(PredicateMatchFixer[client.Object]) + fix, ok := predicate.(PredicateMatchFixer[any]) if !ok { return prefix } - expected := fix.FixToMatch(actual.DeepCopyObject().(client.Object)) + expected := fix.FixToMatch(actual) diff := cmp.Diff(expected, actual) return fmt.Sprintf("%s because of the following differences (- indicates the expected values, + the actual values):\n%s", prefix, diff) diff --git a/pkg/test/assertions/assertions_test.go b/pkg/test/assertions/assertions_test.go deleted file mode 100644 index 192a6a07..00000000 --- a/pkg/test/assertions/assertions_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package assertions - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func TestExplain(t *testing.T) { - t.Run("with diff", func(t *testing.T) { - // given - actual := &corev1.Secret{} - actual.SetName("actual") - - pred := Has(Name("expected")) - - // when - expl := Explain(pred, actual) - - // then - assert.True(t, strings.HasPrefix(expl, "predicate 'assertions.named' didn't match the object because of the following differences (- indicates the expected values, + the actual values):")) - assert.Contains(t, expl, "-") - assert.Contains(t, expl, "\"expected\"") - assert.Contains(t, expl, "+") - assert.Contains(t, expl, "\"actual\"") - }) - - t.Run("without diff", func(t *testing.T) { - // given - actual := &corev1.Secret{} - actual.SetName("actual") - - pred := &predicateWithoutFixing{} - - // when - expl := Explain(pred, actual) - - // then - assert.Equal(t, "predicate 'assertions.predicateWithoutFixing' didn't match the object", expl) - }) -} - -func TestAssertThat(t *testing.T) { - t.Run("positive case", func(t *testing.T) { - // given - actual := &corev1.ConfigMap{} - actual.SetName("actual") - actual.SetLabels(map[string]string{"k": "v"}) - - // when - message := assertThat(actual, Has(Name("actual")), Has(Labels(map[string]string{"k": "v"}))) - - // then - assert.Empty(t, message) - }) - - t.Run("negative case", func(t *testing.T) { - // given - actual := &corev1.ConfigMap{} - actual.SetName("actual") - actual.SetLabels(map[string]string{"k": "v"}) - - // when - message := assertThat(actual, Has(Name("expected")), Has(Labels(map[string]string{"k": "another value"}))) - - // then - assert.Contains(t, message, "predicate 'assertions.named' didn't match the object because of the following differences") - assert.Contains(t, message, "predicate 'assertions.hasLabels' didn't match the object because of the following differences") - }) -} - -type predicateWithoutFixing struct{} - -var _ Predicate[client.Object] = (*predicateWithoutFixing)(nil) - -func (*predicateWithoutFixing) Matches(obj client.Object) bool { - return false -} diff --git a/pkg/test/assertions_test.go b/pkg/test/assertions_test.go new file mode 100644 index 00000000..6e03cf4d --- /dev/null +++ b/pkg/test/assertions_test.go @@ -0,0 +1,126 @@ +package test + +import ( + "strings" + "testing" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestExplain(t *testing.T) { + t.Run("with diff", func(t *testing.T) { + // given + actual := &corev1.Secret{} + actual.SetName("actual") + + pred := Has(Name("expected")) + + // when + expl := Explain(pred, actual) + + // then + assert.True(t, strings.HasPrefix(expl, "predicate 'test.named' didn't match the object because of the following differences (- indicates the expected values, + the actual values):")) + assert.Contains(t, expl, "-") + assert.Contains(t, expl, "\"expected\"") + assert.Contains(t, expl, "+") + assert.Contains(t, expl, "\"actual\"") + }) + + t.Run("without diff", func(t *testing.T) { + // given + actual := &corev1.Secret{} + actual.SetName("actual") + + pred := Is[client.Object](&predicateWithoutFixing{}) + + // when + expl := Explain(pred, actual) + + // then + assert.Equal(t, "predicate 'test.predicateWithoutFixing' didn't match the object", expl) + }) + + t.Run("with a slice", func(t *testing.T) { + actual := []int{1, 2, 3} + pred := MockPredicate[[]int]{} + pred.MatchesFunc = func(v []int) bool { + return false + } + pred.FixToMatchFunc = func(v []int) []int { + return []int{1, 2} + } + + expl := Explain(Is[[]int](pred), actual) + + assert.True(t, strings.HasPrefix(expl, "predicate 'test.MockPredicate[[]int]' didn't match the object because of the following")) + }) + + t.Run("with conditions", func(t *testing.T) { + actual := []toolchainv1alpha1.Condition{ + { + Type: toolchainv1alpha1.ConditionType("test"), + Status: corev1.ConditionFalse, + Reason: "because", + }, + } + + pred := ConditionThat(toolchainv1alpha1.ConditionType("test"), HasStatus(corev1.ConditionTrue)) + + expl := Explain(Has(pred), actual) + + assert.True(t, strings.HasPrefix(expl, "predicate 'test.conditionsPredicate' didn't match the object because of the following")) + }) +} + +func TestAssertThat(t *testing.T) { + t.Run("positive case", func(t *testing.T) { + // given + actual := &corev1.ConfigMap{} + actual.SetName("actual") + actual.SetLabels(map[string]string{"k": "v"}) + + // when + message := assertThat(actual, Has(Name("actual")), Has(Labels(map[string]string{"k": "v"}))) + + // then + assert.Empty(t, message) + }) + + t.Run("negative case", func(t *testing.T) { + // given + actual := &corev1.ConfigMap{} + actual.SetName("actual") + actual.SetLabels(map[string]string{"k": "v"}) + + // when + message := assertThat(actual, Has(Name("expected")), Has(Labels(map[string]string{"k": "another value"}))) + + // then + assert.Contains(t, message, "predicate 'test.named' didn't match the object because of the following differences") + assert.Contains(t, message, "predicate 'test.hasLabels' didn't match the object because of the following differences") + }) +} + +type predicateWithoutFixing struct{} + +var _ Predicate[client.Object] = (*predicateWithoutFixing)(nil) + +func (*predicateWithoutFixing) Matches(obj client.Object) bool { + return false +} + +type MockPredicate[T any] struct { + MatchesFunc func(v T) bool + FixToMatchFunc func(v T) T +} + +func (p MockPredicate[T]) Matches(v T) bool { + return p.MatchesFunc(v) +} + +func (p MockPredicate[T]) FixToMatch(v T) T { + return p.FixToMatchFunc(v) +} diff --git a/pkg/test/condition.go b/pkg/test/condition.go index 580b7dc1..fa35d6e9 100644 --- a/pkg/test/condition.go +++ b/pkg/test/condition.go @@ -1,15 +1,16 @@ package test import ( - "fmt" "time" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + conditions "github.com/codeready-toolchain/toolchain-common/pkg/condition" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" ) // AssertConditionsMatch asserts that the specified list A of conditions is equal to specified @@ -17,68 +18,375 @@ import ( // because the LastTransitionTime of the actual conditions can be modified but the conditions // still should be treated as matched func AssertConditionsMatch(t T, actual []toolchainv1alpha1.Condition, expected ...toolchainv1alpha1.Condition) { - require.Equal(t, len(expected), len(actual)) - for _, c := range expected { - AssertContainsCondition(t, actual, c) - } + AssertThat(t, actual, Has(AllConditionsLike(expected...))) } // AssertContainsCondition asserts that the specified list of conditions contains the specified condition. // LastTransitionTime is ignored. func AssertContainsCondition(t T, conditions []toolchainv1alpha1.Condition, contains toolchainv1alpha1.Condition) { - for _, c := range conditions { - if c.Type == contains.Type { - assert.Equal(t, contains.Status, c.Status) - assert.Equal(t, contains.Reason, c.Reason) - assert.Equal(t, contains.Message, c.Message) - return - } - } - assert.FailNow(t, fmt.Sprintf("the list of conditions %v doesn't contain the expected condition %v", conditions, contains)) + AssertThat(t, conditions, Has(SomeConditionThat(contains.Type, HasStatus(contains.Status), HasReason(contains.Reason), HasMessage(contains.Message)))) } // AssertConditionsMatchAndRecentTimestamps asserts that the specified list of conditions match AND asserts that the timestamps are recent func AssertConditionsMatchAndRecentTimestamps(t T, actual []toolchainv1alpha1.Condition, expected ...toolchainv1alpha1.Condition) { - AssertConditionsMatch(t, actual, expected...) - AssertTimestampsAreRecent(t, actual) -} - -// AssertTimestampsAreRecent asserts that the timestamps for the provided list of conditions are recent -func AssertTimestampsAreRecent(t T, conditions []toolchainv1alpha1.Condition) { - var secs int64 = 5 - recentTime := metav1.Now().Add(time.Duration(-secs) * time.Second) - for _, c := range conditions { - assert.True(t, c.LastTransitionTime.After(recentTime), "LastTransitionTime was not updated within the last %d seconds", secs) - assert.True(t, (*c.LastUpdatedTime).After(recentTime), "LastUpdatedTime was not updated within the last %d seconds", secs) + cutoff := time.Now().Add(-5 * time.Second) + expectedMap := map[toolchainv1alpha1.ConditionType][]Predicate[toolchainv1alpha1.Condition]{} + for _, c := range expected { + expectedMap[c.Type] = []Predicate[toolchainv1alpha1.Condition]{IsLike(c), HasTransitionTimeLaterThan(cutoff), HasUpdateTimeLaterThan(cutoff)} } + AssertThat(t, actual, Has(AllConditions(expectedMap))) } // ConditionsMatch returns true if the specified list A of conditions is equal to specified // list B of conditions ignoring the order of the elements func ConditionsMatch(actual []toolchainv1alpha1.Condition, expected ...toolchainv1alpha1.Condition) bool { - if len(expected) != len(actual) { + return AllConditionsLike(expected...).Matches(actual) +} + +// ContainsCondition returns true if the specified list of conditions contains the specified condition. +// LastTransitionTime is ignored. +func ContainsCondition(conditions []toolchainv1alpha1.Condition, contains toolchainv1alpha1.Condition) bool { + return SomeConditionThat(contains.Type, IsLike(contains)).Matches(conditions) +} + +func SomeConditionThat(conditionType toolchainv1alpha1.ConditionType, preds ...Predicate[toolchainv1alpha1.Condition]) Predicate[[]toolchainv1alpha1.Condition] { + return &conditionsPredicate{conditionType: conditionType, predicates: preds} +} + +func SomeConditionLike(expected toolchainv1alpha1.Condition) Predicate[[]toolchainv1alpha1.Condition] { + return SomeConditionThat(expected.Type, IsLike(expected)) +} + +func AllConditions(conditions map[toolchainv1alpha1.ConditionType][]Predicate[toolchainv1alpha1.Condition]) Predicate[[]toolchainv1alpha1.Condition] { + return &allConditionsLikePredicate{conditions: conditions} +} + +func AllConditionsLike(expected ...toolchainv1alpha1.Condition) Predicate[[]toolchainv1alpha1.Condition] { + expectedMap := map[toolchainv1alpha1.ConditionType][]Predicate[toolchainv1alpha1.Condition]{} + for _, c := range expected { + expectedMap[c.Type] = []Predicate[toolchainv1alpha1.Condition]{IsLike(c)} + } + return &allConditionsLikePredicate{conditions: expectedMap} +} + +func BridgeToConditions[T client.Object](accessor func(T) *[]toolchainv1alpha1.Condition, pred Predicate[[]toolchainv1alpha1.Condition]) Predicate[T] { + return &bridgePredicate[T]{accessor: accessor, pred: pred} +} + +func IsLike(cond toolchainv1alpha1.Condition) Predicate[toolchainv1alpha1.Condition] { + return &likePredicate{condition: cond} +} + +func IsTrue() Predicate[toolchainv1alpha1.Condition] { + return HasStatus(corev1.ConditionTrue) +} + +func IsNotTrue() Predicate[toolchainv1alpha1.Condition] { + return HasStatusDifferentFrom(corev1.ConditionTrue) +} + +func IsFalse() Predicate[toolchainv1alpha1.Condition] { + return HasStatus(corev1.ConditionFalse) +} + +func IsNotFalse() Predicate[toolchainv1alpha1.Condition] { + return HasStatusDifferentFrom(corev1.ConditionFalse) +} + +func IsUnknown() Predicate[toolchainv1alpha1.Condition] { + return HasStatus(corev1.ConditionUnknown) +} + +func IsNotUnknown() Predicate[toolchainv1alpha1.Condition] { + return HasStatusDifferentFrom(corev1.ConditionUnknown) +} + +func HasStatus(status corev1.ConditionStatus) Predicate[toolchainv1alpha1.Condition] { + return &conditionPredicate{expectedStatus: &status} +} + +func HasStatusDifferentFrom(status corev1.ConditionStatus) Predicate[toolchainv1alpha1.Condition] { + return &conditionPredicate{expectedStatus: &status, negate: true} +} + +func HasReason(reason string) Predicate[toolchainv1alpha1.Condition] { + return &conditionPredicate{expectedReason: pointer.String(reason)} +} + +func HasMessage(reason string) Predicate[toolchainv1alpha1.Condition] { + return &conditionPredicate{expectedMessage: pointer.String(reason)} +} + +func HasRecentTransitionTime() Predicate[toolchainv1alpha1.Condition] { + return HasTransitionTimeLaterThan(time.Now().Add(-5 * time.Second)) +} + +func HasRecentUpdateTime() Predicate[toolchainv1alpha1.Condition] { + return HasUpdateTimeLaterThan(time.Now().Add(-5 * time.Second)) +} + +func HasTransitionTimeLaterThan(t time.Time) Predicate[toolchainv1alpha1.Condition] { + return &recencyPredicate{oldestAllowed: t, transition: true} +} + +func HasUpdateTimeLaterThan(t time.Time) Predicate[toolchainv1alpha1.Condition] { + return &recencyPredicate{oldestAllowed: t, transition: false} +} + +type conditionsPredicate struct { + conditionType toolchainv1alpha1.ConditionType + predicates []Predicate[toolchainv1alpha1.Condition] +} + +// Matches implements Predicate. +func (c *conditionsPredicate) Matches(conds []toolchainv1alpha1.Condition) bool { + condition, found := conditions.FindConditionByType(conds, c.conditionType) + if !found { return false } - for _, c := range expected { - if !ContainsCondition(actual, c) { + + for _, predicate := range c.predicates { + if !predicate.Matches(condition) { return false } } - for _, c := range actual { - if !ContainsCondition(expected, c) { + return true +} + +func (c *conditionsPredicate) FixToMatch(conds []toolchainv1alpha1.Condition) []toolchainv1alpha1.Condition { + var found bool + var index int + var condition toolchainv1alpha1.Condition + + if conds == nil { + conds = []toolchainv1alpha1.Condition{} + } else { + copy := make([]toolchainv1alpha1.Condition, len(conds)) + for i, c := range conds { + copy[i] = c + } + conds = copy + } + + for i, cond := range conds { + if cond.Type == c.conditionType { + found = true + index = i + condition = cond + break + } + } + + if !found { + conds = append(conds, toolchainv1alpha1.Condition{}) + index = len(conds) - 1 + condition.Type = c.conditionType + } + + for _, predicate := range c.predicates { + if p, ok := predicate.(PredicateMatchFixer[toolchainv1alpha1.Condition]); ok { + condition = p.FixToMatch(condition) + } + } + + conds[index] = condition + + return conds +} + +type conditionPredicate struct { + expectedStatus *corev1.ConditionStatus + expectedReason *string + expectedMessage *string + expectedType toolchainv1alpha1.ConditionType + negate bool +} + +func (c *conditionPredicate) Matches(cond toolchainv1alpha1.Condition) bool { + if c.expectedType != "" && cond.Type != c.expectedType { + return c.negate + } + if c.expectedStatus != nil && cond.Status != *c.expectedStatus { + return c.negate + } + if c.expectedReason != nil && cond.Reason != *c.expectedReason { + return c.negate + } + if c.expectedMessage != nil && cond.Message != *c.expectedMessage { + return c.negate + } + + return !c.negate +} + +func (c *conditionPredicate) FixToMatch(cond toolchainv1alpha1.Condition) toolchainv1alpha1.Condition { + if c.expectedType != "" { + if c.negate { + cond.Type = "" + } else { + cond.Type = c.expectedType + } + } + if c.expectedStatus != nil { + if c.negate { + cond.Status = "" + } else { + cond.Status = *c.expectedStatus + } + } + if c.expectedReason != nil { + if c.negate { + cond.Reason = "" + } else { + cond.Reason = *c.expectedReason + } + } + if c.expectedMessage != nil { + if c.negate { + cond.Message = "" + } else { + cond.Message = *c.expectedMessage + } + } + + return cond +} + +type bridgePredicate[T client.Object] struct { + accessor func(T) *[]toolchainv1alpha1.Condition + pred Predicate[[]toolchainv1alpha1.Condition] +} + +func (c *bridgePredicate[T]) Matches(obj T) bool { + conds := c.accessor(obj) + return c.pred.Matches(*conds) +} + +func (c *bridgePredicate[T]) FixToMatch(obj T) T { + if p, ok := c.pred.(PredicateMatchFixer[[]toolchainv1alpha1.Condition]); ok { + obj = obj.DeepCopyObject().(T) + conds := c.accessor(obj) + *conds = p.FixToMatch(*conds) + } + return obj +} + +type recencyPredicate struct { + oldestAllowed time.Time + transition bool +} + +func (r *recencyPredicate) Matches(cond toolchainv1alpha1.Condition) bool { + var condTime time.Time + if r.transition { + condTime = cond.LastTransitionTime.Time + } else if cond.LastUpdatedTime != nil { + condTime = cond.LastUpdatedTime.Time + } else { + // we're looking for a recent update time, but there was no update + return false + } + + return condTime.After(r.oldestAllowed) +} + +func (r *recencyPredicate) FixToMatch(cond toolchainv1alpha1.Condition) toolchainv1alpha1.Condition { + if r.transition { + cond.LastTransitionTime.Time = r.oldestAllowed + } else { + cond.LastUpdatedTime = &metav1.Time{Time: r.oldestAllowed} + } + return cond +} + +type likePredicate struct { + condition toolchainv1alpha1.Condition +} + +func (r *likePredicate) Matches(cond toolchainv1alpha1.Condition) bool { + return r.condition.Type == cond.Type && r.condition.Status == cond.Status && r.condition.Reason == cond.Reason && r.condition.Message == cond.Message +} + +func (r *likePredicate) FixToMatch(cond toolchainv1alpha1.Condition) toolchainv1alpha1.Condition { + cond.Type = r.condition.Type + cond.Status = r.condition.Status + cond.Reason = r.condition.Reason + cond.Message = r.condition.Message + return cond +} + +type allConditionsLikePredicate struct { + conditions map[toolchainv1alpha1.ConditionType][]Predicate[toolchainv1alpha1.Condition] +} + +func (r *allConditionsLikePredicate) Matches(conds []toolchainv1alpha1.Condition) bool { + if len(conds) != len(r.conditions) { + return false + } + + for _, cond := range conds { + preds, ok := r.conditions[cond.Type] + if !ok { return false } + + for _, p := range preds { + if !p.Matches(cond) { + return false + } + } } return true } -// ContainsCondition returns true if the specified list of conditions contains the specified condition. -// LastTransitionTime is ignored. -func ContainsCondition(conditions []toolchainv1alpha1.Condition, contains toolchainv1alpha1.Condition) bool { - for _, c := range conditions { - if c.Type == contains.Type { - return contains.Status == c.Status && contains.Reason == c.Reason && contains.Message == c.Message +func (r *allConditionsLikePredicate) FixToMatch(conds []toolchainv1alpha1.Condition) []toolchainv1alpha1.Condition { + remainingTypes := make(map[toolchainv1alpha1.ConditionType]bool, len(r.conditions)) + for t := range r.conditions { + remainingTypes[t] = true + } + + fixed := []toolchainv1alpha1.Condition{} + + fix := func(cond toolchainv1alpha1.Condition, preds []Predicate[toolchainv1alpha1.Condition]) toolchainv1alpha1.Condition { + for _, p := range preds { + if p, ok := p.(PredicateMatchFixer[toolchainv1alpha1.Condition]); ok { + cond = p.FixToMatch(cond) + } } + return cond } - return false + + for _, cond := range conds { + preds, ok := r.conditions[cond.Type] + if !ok { + // we don't add the condition to the fixed ones, effectively removing it + continue + } + cond = fix(cond, preds) + fixed = append(fixed, cond) + delete(remainingTypes, cond.Type) + } + + for t := range remainingTypes { + preds := r.conditions[t] + cond := toolchainv1alpha1.Condition{} + cond.Type = t + cond = fix(cond, preds) + fixed = append(fixed, cond) + } + + return fixed } + +var ( + _ Predicate[[]toolchainv1alpha1.Condition] = (*conditionsPredicate)(nil) + _ PredicateMatchFixer[[]toolchainv1alpha1.Condition] = (*conditionsPredicate)(nil) + _ Predicate[[]toolchainv1alpha1.Condition] = (*allConditionsLikePredicate)(nil) + _ PredicateMatchFixer[[]toolchainv1alpha1.Condition] = (*allConditionsLikePredicate)(nil) + _ Predicate[toolchainv1alpha1.Condition] = (*conditionPredicate)(nil) + _ PredicateMatchFixer[toolchainv1alpha1.Condition] = (*conditionPredicate)(nil) + _ Predicate[toolchainv1alpha1.Condition] = (*recencyPredicate)(nil) + _ PredicateMatchFixer[toolchainv1alpha1.Condition] = (*recencyPredicate)(nil) + _ Predicate[toolchainv1alpha1.Condition] = (*likePredicate)(nil) + _ PredicateMatchFixer[toolchainv1alpha1.Condition] = (*likePredicate)(nil) +) diff --git a/pkg/test/assertions/predicates.go b/pkg/test/predicates.go similarity index 85% rename from pkg/test/assertions/predicates.go rename to pkg/test/predicates.go index 32a74c74..df11fde6 100644 --- a/pkg/test/assertions/predicates.go +++ b/pkg/test/predicates.go @@ -1,4 +1,4 @@ -package assertions +package test import ( "k8s.io/apimachinery/pkg/types" @@ -36,7 +36,7 @@ import ( // // If you're implementing your own predicate, consider implementing the PredicateMatchFixer, // too, so that you can benefit from improved failure diagnostics offered by Explain function. -type Predicate[T client.Object] interface { +type Predicate[T any] interface { Matches(obj T) bool } @@ -46,8 +46,9 @@ type Predicate[T client.Object] interface { // between it and the non-matching object of the predicate in case of a test failure // for logging purposes. // -// There is no need to copy the provided object. -type PredicateMatchFixer[T client.Object] interface { +// If T is a reference object, it MUST be copied first before making the modifications +// so that the diff can be constructed. +type PredicateMatchFixer[T any] interface { FixToMatch(obj T) T } @@ -56,36 +57,45 @@ type PredicateMatchFixer[T client.Object] interface { // readability of the code by being able to construct expressions like: // // predicates.Is(predicates.Named("whatevs")) -func Is[T client.Object](p Predicate[T]) Predicate[client.Object] { +func Is[T any](p Predicate[T]) Predicate[any] { + if _, ok := p.(PredicateMatchFixer[T]); ok { + return &fixingCast[T]{cast: cast[T]{Inner: p}} + } return &cast[T]{Inner: p} } // Has is just an alias of Is. It is provided for better readability with certain predicate // names. -func Has[T client.Object](p Predicate[T]) Predicate[client.Object] { - return &cast[T]{Inner: p} +func Has[T any](p Predicate[T]) Predicate[any] { + return Is(p) } -type cast[T client.Object] struct { +type cast[T any] struct { // Inner is public so that Explain (in assertions.go) can access it... Inner Predicate[T] } +type fixingCast[T any] struct { + cast[T] +} + var ( - _ Predicate[client.Object] = (*cast[client.Object])(nil) - _ PredicateMatchFixer[client.Object] = (*cast[client.Object])(nil) + _ Predicate[any] = (*cast[any])(nil) + _ Predicate[any] = (*fixingCast[any])(nil) + _ PredicateMatchFixer[any] = (*fixingCast[any])(nil) ) -func (c *cast[T]) Matches(obj client.Object) bool { +func (c *cast[T]) Matches(obj any) bool { return c.Inner.Matches(obj.(T)) } -func (c *cast[T]) FixToMatch(obj client.Object) client.Object { - pf, ok := c.Inner.(PredicateMatchFixer[T]) - if ok { - return pf.FixToMatch(obj.(T)) - } - return obj +func (c *fixingCast[T]) Matches(obj any) bool { + return c.cast.Matches(obj) +} + +func (c *fixingCast[T]) FixToMatch(obj any) any { + pf := c.Inner.(PredicateMatchFixer[T]) + return pf.FixToMatch(obj.(T)) } type named struct { @@ -102,6 +112,7 @@ func (n *named) Matches(obj client.Object) bool { } func (n *named) FixToMatch(obj client.Object) client.Object { + obj = obj.DeepCopyObject().(client.Object) obj.SetName(n.name) return obj } @@ -125,6 +136,7 @@ func (i *inNamespace) Matches(obj client.Object) bool { } func (i *inNamespace) FixToMatch(obj client.Object) client.Object { + obj = obj.DeepCopyObject().(client.Object) obj.SetNamespace(i.namespace) return obj } @@ -148,6 +160,7 @@ func (w *withKey) Matches(obj client.Object) bool { } func (w *withKey) FixToMatch(obj client.Object) client.Object { + obj = obj.DeepCopyObject().(client.Object) obj.SetName(w.Name) obj.SetNamespace(w.Namespace) return obj @@ -182,6 +195,7 @@ func (h *hasLabels) FixToMatch(obj client.Object) client.Object { if len(h.requiredLabels) == 0 { return obj } + obj = obj.DeepCopyObject().(client.Object) objLabels := obj.GetLabels() if objLabels == nil { objLabels = map[string]string{} @@ -224,6 +238,7 @@ func (h *hasAnnotations) FixToMatch(obj client.Object) client.Object { if len(h.requiredAnnotations) == 0 { return obj } + obj = obj.DeepCopyObject().(client.Object) objAnnos := obj.GetAnnotations() if objAnnos == nil { objAnnos = map[string]string{} diff --git a/pkg/test/assertions/predicates_test.go b/pkg/test/predicates_test.go similarity index 99% rename from pkg/test/assertions/predicates_test.go rename to pkg/test/predicates_test.go index 7d4c3958..95b48ae9 100644 --- a/pkg/test/assertions/predicates_test.go +++ b/pkg/test/predicates_test.go @@ -1,4 +1,4 @@ -package assertions +package test import ( "testing" diff --git a/pkg/test/space/space_assertions.go b/pkg/test/space/space_assertions.go index 47259b06..14413a03 100644 --- a/pkg/test/space/space_assertions.go +++ b/pkg/test/space/space_assertions.go @@ -6,12 +6,14 @@ import ( toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/toolchain-common/pkg/hash" "github.com/codeready-toolchain/toolchain-common/pkg/test" + "golang.org/x/exp/slices" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -354,3 +356,192 @@ func (a *Assertion) loadSubSpace() error { } return err } + +/////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////// +/////// Example conversion to predicates ////////////////////////// +/////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////// + +func Finalizer() test.Predicate[*toolchainv1alpha1.Space] { + return &hasFinalizer{} +} + +func NoFinalizer() test.Predicate[*toolchainv1alpha1.Space] { + return &hasFinalizer{negate: true} +} + +func Tier(tierName string) test.Predicate[*toolchainv1alpha1.Space] { + return &hasTier{name: tierName} +} + +func DisableInheritance(disabled bool) test.Predicate[*toolchainv1alpha1.Space] { + return &hasInheritance{disabled: disabled} +} + +func ParentSpace(parentSpaceName string) test.Predicate[*toolchainv1alpha1.Space] { + return &hasParentSpace{name: parentSpaceName} +} + +// predicates on labels and annotations are implemented generically, so we don't have +// to redefine and reimplement them here... + +func MatchingTierLabelForTier(tier *toolchainv1alpha1.NSTemplateTier) test.Predicate[*toolchainv1alpha1.Space] { + return &hasMatchingTierLabel{tier: tier} +} + +func StateLabel(value string) test.Predicate[client.Object] { + return test.Labels(map[string]string{toolchainv1alpha1.SpaceStateLabelKey: value}) +} + +func TargetCluster(cluster string) test.Predicate[*toolchainv1alpha1.Space] { + return &hasTargetCluster{cluster: cluster} +} + +func TargetClusterRoles(roles []string) test.Predicate[*toolchainv1alpha1.Space] { + return &hasTargetClusterRoles{roles: roles} +} + +func StatusTargetCluster(cluster string) test.Predicate[*toolchainv1alpha1.Space] { + return &hasStatusTargetCluster{cluster: cluster} +} + +func ProvisionedNamespaces(provisionedNamespaces []toolchainv1alpha1.SpaceNamespace) test.Predicate[*toolchainv1alpha1.Space] { + return &hasProvisionedNamespaces{namespaces: provisionedNamespaces} +} + +// condition predicates are done generically + +func Conditions(pred test.Predicate[[]toolchainv1alpha1.Condition]) test.Predicate[*toolchainv1alpha1.Space] { + return test.BridgeToConditions(func(s *toolchainv1alpha1.Space) *[]toolchainv1alpha1.Condition { + return &s.Status.Conditions + }, pred) +} + +// impls + +type ( + hasFinalizer struct{ negate bool } + hasTier struct{ name string } + hasInheritance struct{ disabled bool } + hasParentSpace struct{ name string } + hasMatchingTierLabel struct { + tier *toolchainv1alpha1.NSTemplateTier + } + hasTargetCluster struct{ cluster string } + hasTargetClusterRoles struct{ roles []string } + hasStatusTargetCluster struct{ cluster string } + hasProvisionedNamespaces struct { + namespaces []toolchainv1alpha1.SpaceNamespace + } +) + +func (p *hasFinalizer) Matches(s *toolchainv1alpha1.Space) bool { + return slices.Contains(s.Finalizers, toolchainv1alpha1.FinalizerName) +} + +func (p *hasFinalizer) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + if slices.Contains(s.Finalizers, toolchainv1alpha1.FinalizerName) != p.negate { + s = s.DeepCopy() + if p.negate { + s.Finalizers = slices.DeleteFunc(s.Finalizers, func(e string) bool { + return e == toolchainv1alpha1.FinalizerName + }) + } else { + s.Finalizers = append(s.Finalizers, toolchainv1alpha1.FinalizerName) + } + } + return s +} + +func (p *hasTier) Matches(s *toolchainv1alpha1.Space) bool { + return s.Spec.TierName == p.name +} + +func (p *hasTier) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + s.Spec.TierName = p.name + return s +} + +func (p *hasInheritance) Matches(s *toolchainv1alpha1.Space) bool { + return s.Spec.DisableInheritance == p.disabled +} + +func (p *hasInheritance) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + s.Spec.DisableInheritance = p.disabled + return s +} + +func (p *hasParentSpace) Matches(s *toolchainv1alpha1.Space) bool { + return s.Spec.ParentSpace == p.name +} + +func (p *hasParentSpace) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + s.Spec.ParentSpace = p.name + return s +} + +func (p *hasMatchingTierLabel) Matches(s *toolchainv1alpha1.Space) bool { + key, hash := p.labelKeyAndHash() + return s.Labels[key] == hash +} + +func (p *hasMatchingTierLabel) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + key, hash := p.labelKeyAndHash() + if s.Labels == nil { + s.Labels = map[string]string{} + } + s.Labels[key] = hash + return s +} + +func (p *hasMatchingTierLabel) labelKeyAndHash() (string, string) { + key := hash.TemplateTierHashLabelKey(p.tier.Name) + // TODO: ignoring the error is not ideal - maybe we could support failable predicates? + hash, _ := hash.ComputeHashForNSTemplateTier(p.tier) + return key, hash +} + +func (p *hasTargetCluster) Matches(s *toolchainv1alpha1.Space) bool { + return s.Spec.TargetCluster == p.cluster +} + +func (p *hasTargetCluster) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + s.Spec.TargetCluster = p.cluster + return s +} + +func (p *hasTargetClusterRoles) Matches(s *toolchainv1alpha1.Space) bool { + return slices.Equal(s.Spec.TargetClusterRoles, p.roles) +} + +func (p *hasTargetClusterRoles) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + s.Spec.TargetClusterRoles = p.roles + return s +} + +func (p *hasStatusTargetCluster) Matches(s *toolchainv1alpha1.Space) bool { + return s.Status.TargetCluster == p.cluster +} + +func (p *hasStatusTargetCluster) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + s.Status.TargetCluster = p.cluster + return s +} + +func (p *hasProvisionedNamespaces) Matches(s *toolchainv1alpha1.Space) bool { + return slices.Equal(s.Status.ProvisionedNamespaces, p.namespaces) +} + +func (p *hasProvisionedNamespaces) FixToMatch(s *toolchainv1alpha1.Space) *toolchainv1alpha1.Space { + s = s.DeepCopy() + s.Status.ProvisionedNamespaces = p.namespaces + return s +} diff --git a/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions.go b/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions.go index 14197dd6..a10cb4f8 100644 --- a/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions.go +++ b/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions.go @@ -2,87 +2,142 @@ package spaceprovisionerconfig import ( toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" - "github.com/codeready-toolchain/toolchain-common/pkg/condition" - "github.com/codeready-toolchain/toolchain-common/pkg/test/assertions" - corev1 "k8s.io/api/core/v1" + "github.com/codeready-toolchain/toolchain-common/pkg/test" ) type ( - ready struct{} - notReady struct{} - notReadyWithReason struct { - expectedReason string + // readyWithStatusAndReason struct { + // expectedStatus corev1.ConditionStatus + // expectedReason *string + // } + + consumedSpaceCount struct { + expectedSpaceCount int + } + + consumedMemoryUsage struct { + expectedMemoryUsage map[string]int } + + unknownConsumedCapacity struct{} ) var ( - _ assertions.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] = (*ready)(nil) - _ assertions.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] = (*notReady)(nil) - _ assertions.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] = (*notReadyWithReason)(nil) - _ assertions.PredicateMatchFixer[*toolchainv1alpha1.SpaceProvisionerConfig] = (*ready)(nil) - _ assertions.PredicateMatchFixer[*toolchainv1alpha1.SpaceProvisionerConfig] = (*notReady)(nil) - _ assertions.PredicateMatchFixer[*toolchainv1alpha1.SpaceProvisionerConfig] = (*notReadyWithReason)(nil) + // _ test.PredicateMatchFixer[*toolchainv1alpha1.SpaceProvisionerConfig] = (*readyWithStatusAndReason)(nil) + _ test.PredicateMatchFixer[*toolchainv1alpha1.SpaceProvisionerConfig] = (*consumedSpaceCount)(nil) + _ test.PredicateMatchFixer[*toolchainv1alpha1.SpaceProvisionerConfig] = (*consumedMemoryUsage)(nil) + _ test.PredicateMatchFixer[*toolchainv1alpha1.SpaceProvisionerConfig] = (*unknownConsumedCapacity)(nil) + + // _ test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] = (*readyWithStatusAndReason)(nil) + _ test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] = (*consumedSpaceCount)(nil) + _ test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] = (*consumedMemoryUsage)(nil) + _ test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] = (*unknownConsumedCapacity)(nil) + + conditionsAccessor = func(spc *toolchainv1alpha1.SpaceProvisionerConfig) *[]toolchainv1alpha1.Condition { + return &spc.Status.Conditions + } ) -func (*ready) Matches(spc *toolchainv1alpha1.SpaceProvisionerConfig) bool { - return condition.IsTrueWithReason(spc.Status.Conditions, toolchainv1alpha1.ConditionReady, toolchainv1alpha1.SpaceProvisionerConfigValidReason) +// +// func (*readyWithStatusAndReason) Matches(spc *toolchainv1alpha1.SpaceProvisionerConfig) bool { +// return condition.IsTrueWithReason(spc.Status.Conditions, toolchainv1alpha1.ConditionReady, toolchainv1alpha1.SpaceProvisionerConfigValidReason) +// } +// +// func (r *readyWithStatusAndReason) FixToMatch(spc *toolchainv1alpha1.SpaceProvisionerConfig) *toolchainv1alpha1.SpaceProvisionerConfig { +// spc = spc.DeepCopyObject().(*toolchainv1alpha1.SpaceProvisionerConfig) +// cnd, found := condition.FindConditionByType(spc.Status.Conditions, toolchainv1alpha1.ConditionReady) +// if !found { +// spc.Status.Conditions = condition.AddStatusConditions(spc.Status.Conditions, toolchainv1alpha1.Condition{ +// Type: toolchainv1alpha1.ConditionReady, +// Status: corev1.ConditionFalse, +// }) +// } else { +// cnd.Status = corev1.ConditionFalse +// spc.Status.Conditions, _ = condition.AddOrUpdateStatusConditions(spc.Status.Conditions, cnd) +// } +// return spc +// } + +func (p *consumedSpaceCount) Matches(spc *toolchainv1alpha1.SpaceProvisionerConfig) bool { + if spc.Status.ConsumedCapacity == nil { + return false + } + return p.expectedSpaceCount == spc.Status.ConsumedCapacity.SpaceCount } -func (*ready) FixToMatch(spc *toolchainv1alpha1.SpaceProvisionerConfig) *toolchainv1alpha1.SpaceProvisionerConfig { - spc.Status.Conditions, _ = condition.AddOrUpdateStatusConditions(spc.Status.Conditions, toolchainv1alpha1.Condition{ - Type: toolchainv1alpha1.ConditionReady, - Status: corev1.ConditionTrue, - Reason: toolchainv1alpha1.SpaceProvisionerConfigValidReason, - }) +func (p *consumedSpaceCount) FixToMatch(spc *toolchainv1alpha1.SpaceProvisionerConfig) *toolchainv1alpha1.SpaceProvisionerConfig { + spc = spc.DeepCopyObject().(*toolchainv1alpha1.SpaceProvisionerConfig) + if spc.Status.ConsumedCapacity == nil { + spc.Status.ConsumedCapacity = &toolchainv1alpha1.ConsumedCapacity{} + } + spc.Status.ConsumedCapacity.SpaceCount = p.expectedSpaceCount return spc } -func Ready() assertions.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { - return &ready{} +func (p *consumedMemoryUsage) Matches(spc *toolchainv1alpha1.SpaceProvisionerConfig) bool { + if spc.Status.ConsumedCapacity == nil { + return false + } + if len(spc.Status.ConsumedCapacity.MemoryUsagePercentPerNodeRole) != len(p.expectedMemoryUsage) { + return false + } + for k, v := range spc.Status.ConsumedCapacity.MemoryUsagePercentPerNodeRole { + if p.expectedMemoryUsage[k] != v { + return false + } + } + return true } -func (*notReady) Matches(spc *toolchainv1alpha1.SpaceProvisionerConfig) bool { - return condition.IsFalse(spc.Status.Conditions, toolchainv1alpha1.ConditionReady) +func (p *consumedMemoryUsage) FixToMatch(spc *toolchainv1alpha1.SpaceProvisionerConfig) *toolchainv1alpha1.SpaceProvisionerConfig { + spc = spc.DeepCopyObject().(*toolchainv1alpha1.SpaceProvisionerConfig) + if spc.Status.ConsumedCapacity == nil { + spc.Status.ConsumedCapacity = &toolchainv1alpha1.ConsumedCapacity{} + } + spc.Status.ConsumedCapacity.MemoryUsagePercentPerNodeRole = p.expectedMemoryUsage + return spc } -func (*notReady) FixToMatch(spc *toolchainv1alpha1.SpaceProvisionerConfig) *toolchainv1alpha1.SpaceProvisionerConfig { - cnd, found := condition.FindConditionByType(spc.Status.Conditions, toolchainv1alpha1.ConditionReady) - if !found { - spc.Status.Conditions = condition.AddStatusConditions(spc.Status.Conditions, toolchainv1alpha1.Condition{ - Type: toolchainv1alpha1.ConditionReady, - Status: corev1.ConditionFalse, - }) - } else { - cnd.Status = corev1.ConditionFalse - spc.Status.Conditions, _ = condition.AddOrUpdateStatusConditions(spc.Status.Conditions, cnd) - } +func (p *unknownConsumedCapacity) Matches(spc *toolchainv1alpha1.SpaceProvisionerConfig) bool { + return spc.Status.ConsumedCapacity == nil +} + +func (p *unknownConsumedCapacity) FixToMatch(spc *toolchainv1alpha1.SpaceProvisionerConfig) *toolchainv1alpha1.SpaceProvisionerConfig { + spc = spc.DeepCopyObject().(*toolchainv1alpha1.SpaceProvisionerConfig) + spc.Status.ConsumedCapacity = nil return spc } -func NotReady() assertions.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { - return ¬Ready{} +func ReadyConditionThat(pred test.Predicate[[]toolchainv1alpha1.Condition]) test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { + return test.BridgeToConditions(func(spc *toolchainv1alpha1.SpaceProvisionerConfig) *[]toolchainv1alpha1.Condition { + return &spc.Status.Conditions + }, pred) } -func (p *notReadyWithReason) Matches(spc *toolchainv1alpha1.SpaceProvisionerConfig) bool { - return condition.IsFalseWithReason(spc.Status.Conditions, toolchainv1alpha1.ConditionReady, p.expectedReason) +// func Ready() test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { +// return &readyWithStatusAndReason{expectedStatus: corev1.ConditionTrue} +// } +// +// func NotReady() test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { +// return &readyWithStatusAndReason{expectedStatus: corev1.ConditionFalse} +// } +// +// func NotReadyWithReason(reason string) test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { +// return &readyWithStatusAndReason{expectedStatus: corev1.ConditionFalse, expectedReason: &reason} +// } +// +// func ReadyStatusAndReason(status corev1.ConditionStatus, reason string) test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { +// return &readyWithStatusAndReason{expectedStatus: status, expectedReason: &reason} +// } + +func ConsumedSpaceCount(value int) test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { + return &consumedSpaceCount{expectedSpaceCount: value} } -func (p *notReadyWithReason) FixToMatch(spc *toolchainv1alpha1.SpaceProvisionerConfig) *toolchainv1alpha1.SpaceProvisionerConfig { - cnd, found := condition.FindConditionByType(spc.Status.Conditions, toolchainv1alpha1.ConditionReady) - if !found { - spc.Status.Conditions = condition.AddStatusConditions(spc.Status.Conditions, toolchainv1alpha1.Condition{ - Type: toolchainv1alpha1.ConditionReady, - Status: corev1.ConditionFalse, - Reason: p.expectedReason, - }) - } else { - cnd.Status = corev1.ConditionFalse - cnd.Reason = p.expectedReason - spc.Status.Conditions, _ = condition.AddOrUpdateStatusConditions(spc.Status.Conditions, cnd) - } - return spc +func ConsumedMemoryUsage(values map[string]int) test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { + return &consumedMemoryUsage{expectedMemoryUsage: values} } -func NotReadyWithReason(reason string) assertions.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { - return ¬ReadyWithReason{expectedReason: reason} +func UnknownConsumedCapacity() test.Predicate[*toolchainv1alpha1.SpaceProvisionerConfig] { + return &unknownConsumedCapacity{} } diff --git a/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions_test.go b/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions_test.go index eb43e913..2f9d28e7 100644 --- a/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions_test.go +++ b/pkg/test/spaceprovisionerconfig/spaceprovisionerconfig_assertions_test.go @@ -1,166 +1,11 @@ package spaceprovisionerconfig -import ( - "testing" +import "testing" - toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" - "github.com/codeready-toolchain/toolchain-common/pkg/condition" - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" -) - -func TestReadyPredicate(t *testing.T) { - t.Run("matching", func(t *testing.T) { - // given - pred := &ready{} - spc := NewSpaceProvisionerConfig("spc", "default", WithReadyConditionValid()) - - // when & then - assert.True(t, pred.Matches(spc)) - }) - - t.Run("fixer with no conditions", func(t *testing.T) { - // given - pred := &ready{} - spc := NewSpaceProvisionerConfig("spc", "default") - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsTrue(spc.Status.Conditions, toolchainv1alpha1.ConditionReady)) - }) - t.Run("fixer with different conditions", func(t *testing.T) { - // given - pred := &ready{} - spc := NewSpaceProvisionerConfig("spc", "default") - spc.Status.Conditions = []toolchainv1alpha1.Condition{ - { - Type: toolchainv1alpha1.ConditionType("made up"), - Status: corev1.ConditionTrue, - }, - } - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsTrue(spc.Status.Conditions, toolchainv1alpha1.ConditionReady)) - assert.Len(t, spc.Status.Conditions, 2) - }) - t.Run("fixer with wrong condition", func(t *testing.T) { - // given - pred := &ready{} - spc := NewSpaceProvisionerConfig("spc", "default", WithReadyConditionInvalid("because")) - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsTrueWithReason(spc.Status.Conditions, toolchainv1alpha1.ConditionReady, toolchainv1alpha1.SpaceProvisionerConfigValidReason)) - }) +func TestConsumedMemoryCapacityPredicate(t *testing.T) { + t.Skip("TODO implement") } -func TestNotReadyPredicate(t *testing.T) { - t.Run("matching", func(t *testing.T) { - // given - pred := ¬Ready{} - spc := NewSpaceProvisionerConfig("spc", "default", WithReadyConditionInvalid("any reason")) - - // when & then - assert.True(t, pred.Matches(spc)) - }) - - t.Run("fixer with no conditions", func(t *testing.T) { - // given - pred := ¬Ready{} - spc := NewSpaceProvisionerConfig("spc", "default") - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsFalse(spc.Status.Conditions, toolchainv1alpha1.ConditionReady)) - }) - t.Run("fixer with different conditions", func(t *testing.T) { - // given - pred := ¬Ready{} - spc := NewSpaceProvisionerConfig("spc", "default") - spc.Status.Conditions = []toolchainv1alpha1.Condition{ - { - Type: toolchainv1alpha1.ConditionType("made up"), - Status: corev1.ConditionTrue, - }, - } - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsFalse(spc.Status.Conditions, toolchainv1alpha1.ConditionReady)) - assert.Len(t, spc.Status.Conditions, 2) - }) - t.Run("fixer with wrong condition", func(t *testing.T) { - // given - pred := ¬Ready{} - spc := NewSpaceProvisionerConfig("spc", "default", WithReadyConditionValid()) - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsFalse(spc.Status.Conditions, toolchainv1alpha1.ConditionReady)) - }) -} - -func TestNotReadyWithReasonPredicate(t *testing.T) { - t.Run("matching", func(t *testing.T) { - // given - pred := ¬ReadyWithReason{expectedReason: "the right reason"} - spc := NewSpaceProvisionerConfig("spc", "default", WithReadyConditionInvalid("the right reason")) - - // when & then - assert.True(t, pred.Matches(spc)) - }) - - t.Run("fixer with no conditions", func(t *testing.T) { - // given - pred := ¬ReadyWithReason{expectedReason: "the right reason"} - spc := NewSpaceProvisionerConfig("spc", "default") - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsFalseWithReason(spc.Status.Conditions, toolchainv1alpha1.ConditionReady, "the right reason")) - }) - t.Run("fixer with different conditions", func(t *testing.T) { - // given - pred := ¬ReadyWithReason{expectedReason: "the right reason"} - spc := NewSpaceProvisionerConfig("spc", "default") - spc.Status.Conditions = []toolchainv1alpha1.Condition{ - { - Type: toolchainv1alpha1.ConditionType("made up"), - Status: corev1.ConditionTrue, - }, - } - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsFalseWithReason(spc.Status.Conditions, toolchainv1alpha1.ConditionReady, "the right reason")) - assert.Len(t, spc.Status.Conditions, 2) - }) - t.Run("fixer with wrong condition", func(t *testing.T) { - // given - pred := ¬ReadyWithReason{expectedReason: "the right reason"} - spc := NewSpaceProvisionerConfig("spc", "default", WithReadyConditionInvalid("the wrong reason")) - - // when - spc = pred.FixToMatch(spc) - - // then - assert.True(t, condition.IsFalseWithReason(spc.Status.Conditions, toolchainv1alpha1.ConditionReady, "the right reason")) - }) +func TestConsumedSpaceCountPredicate(t *testing.T) { + t.Skip("TODO implement") } diff --git a/pkg/test/testing_t.go b/pkg/test/testing_t.go index 8c72ee3b..f9939997 100644 --- a/pkg/test/testing_t.go +++ b/pkg/test/testing_t.go @@ -2,6 +2,7 @@ package test // T our minimal testing interface for our custom assertions type T interface { + Helper() Log(args ...interface{}) Logf(format string, args ...interface{}) Errorf(format string, args ...interface{}) @@ -24,6 +25,10 @@ type MockT struct { failCount int } +func (t *MockT) Helper() { + // mock test does nothing here +} + func (t *MockT) Log(_ ...interface{}) { t.logfCount++ }