-
Notifications
You must be signed in to change notification settings - Fork 78
proof of concept of the more type-safe way of defining the predicates. #1073
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c751223
9d62475
ad47f65
6205908
4dfd0ce
ddd3eb0
bfc25b6
097f981
55109b4
50e68f2
8fd7da8
7e24fa9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package assertions | ||
|
|
||
| // Assertion is a test function that is meant to test some object. | ||
| type Assertion[T any] interface { | ||
| Test(t AssertT, obj T) | ||
| } | ||
|
|
||
| // Assertions is just a list of assertions provided for convenience. It is meant to be embedded into structs. | ||
| type Assertions[T any] []Assertion[T] | ||
|
|
||
| // AssertionFunc converts a function into an assertion. | ||
| type AssertionFunc[T any] func(t AssertT, obj T) | ||
|
|
||
| // Append is just an alias for Go's built-in append. | ||
| func Append[Type any](assertionList Assertions[Type], assertions ...Assertion[Type]) Assertions[Type] { | ||
| assertionList = append(assertionList, assertions...) | ||
| return assertionList | ||
| } | ||
|
|
||
| // AppendGeneric is a variant of append that can also cast assertions on some super-type into assertions | ||
| // on some type. This can be useful when one has some assertions that work on an super-type of some type and | ||
| // you want to append it to a list of assertions on the type itself. | ||
| func AppendGeneric[SuperType any, Type any](assertionList Assertions[Type], assertions ...Assertion[SuperType]) Assertions[Type] { | ||
| for _, a := range assertions { | ||
| assertionList = append(assertionList, CastAssertion[SuperType, Type](a)) | ||
| } | ||
| return assertionList | ||
| } | ||
|
|
||
| // AppendConverted is a convenience function to first lift all the assertions to the "To" type and then append them to the provided list. | ||
| func AppendConverted[From any, To any](conversion func(To) (From, bool), assertionList Assertions[To], assertions ...Assertion[From]) Assertions[To] { | ||
| return Append(assertionList, ConvertAll(conversion, assertions...)...) | ||
| } | ||
|
|
||
| // AppendFunc is a convenience function that is able to take in the assertions as simple functions. | ||
| func AppendFunc[T any](assertionList Assertions[T], fn ...AssertionFunc[T]) Assertions[T] { | ||
| for _, f := range fn { | ||
| assertionList = append(assertionList, f) | ||
| } | ||
| return assertionList | ||
| } | ||
|
|
||
| // Test runs the test by all assertions in the list. | ||
| func (as Assertions[T]) Test(t AssertT, obj T) { | ||
| t.Helper() | ||
| for _, a := range as { | ||
| a.Test(t, obj) | ||
| } | ||
| } | ||
|
|
||
| func (f AssertionFunc[T]) Test(t AssertT, obj T) { | ||
| t.Helper() | ||
| f(t, obj) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package conditions | ||
|
|
||
| import ( | ||
| toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" | ||
| "github.com/codeready-toolchain/toolchain-common/pkg/condition" | ||
| "github.com/codeready-toolchain/toolchain-e2e/testsupport/assertions" | ||
| "github.com/stretchr/testify/assert" | ||
| corev1 "k8s.io/api/core/v1" | ||
| ) | ||
|
|
||
| type ConditionAssertions struct { | ||
| assertions.Assertions[[]toolchainv1alpha1.Condition] | ||
| } | ||
|
Comment on lines
+11
to
+13
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, now I understand why you used the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having the This function then enables a nice "traversal" in the spaceprovisionerconfig assertions: func (spc *SpaceProvisionerConfigAssertions) Conditions(cas *conditions.ConditionAssertions) *SpaceProvisionerConfigAssertions {
spc.Assertions = assertions.AppendConverted(getConditions, spc.Assertions, cas.Assertions...)
return spc
}
func getConditions(spc *toolchainv1alpha1.SpaceProvisionerConfig) ([]toolchainv1alpha1.Condition, bool) {
return spc.Status.Conditions, true
}Of course, we could implement something similar if we had specialized assertions for conditions that wouldn't share anything with the I like the cohesion that
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I mentioned above - we already talked about it in one of our sync calls and we agreed on focusing only on the wait logic in toolchain-e2e repo. |
||
|
|
||
| func With() *ConditionAssertions { | ||
| return &ConditionAssertions{} | ||
| } | ||
|
|
||
| func (cas *ConditionAssertions) Type(typ toolchainv1alpha1.ConditionType) *ConditionAssertions { | ||
| cas.Assertions = assertions.AppendFunc(cas.Assertions, func(t assertions.AssertT, conds []toolchainv1alpha1.Condition) { | ||
| t.Helper() | ||
| _, found := condition.FindConditionByType(conds, typ) | ||
| assert.True(t, found, "didn't find a condition with the type '%v'", typ) | ||
| }) | ||
| return cas | ||
| } | ||
|
|
||
| func (cas *ConditionAssertions) Status(typ toolchainv1alpha1.ConditionType, status corev1.ConditionStatus) *ConditionAssertions { | ||
| cas.Assertions = assertions.AppendFunc(cas.Assertions, func(t assertions.AssertT, conds []toolchainv1alpha1.Condition) { | ||
| t.Helper() | ||
| cond, found := condition.FindConditionByType(conds, typ) | ||
| assert.True(t, found, "didn't find a condition with the type '%v'", typ) | ||
| assert.Equal(t, status, cond.Status, "condition of type '%v' doesn't have the expected status", typ) | ||
| }) | ||
| return cas | ||
| } | ||
|
|
||
| func (cas *ConditionAssertions) StatusAndReason(typ toolchainv1alpha1.ConditionType, status corev1.ConditionStatus, reason string) *ConditionAssertions { | ||
| cas.Assertions = assertions.AppendFunc(cas.Assertions, func(t assertions.AssertT, conds []toolchainv1alpha1.Condition) { | ||
| t.Helper() | ||
| cond, found := condition.FindConditionByType(conds, typ) | ||
| assert.True(t, found, "didn't find a condition with the type '%v'", typ) | ||
| assert.Equal(t, status, cond.Status, "condition of type '%v' doesn't have the expected status", typ) | ||
| assert.Equal(t, reason, cond.Reason, "condition of type '%v' doesn't have the expected reason", typ) | ||
| }) | ||
| return cas | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package assertions | ||
|
|
||
| // CastAssertion can be used to convert a generic assertion on, say, client.Object, into | ||
| // an assertion on a concrete subtype. Note that the conversion is not guaranteed to | ||
| // pass by the type system and can fail at runtime. | ||
| func CastAssertion[SuperType any, Type any](a Assertion[SuperType]) Assertion[Type] { | ||
|
Comment on lines
+3
to
+6
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to the other comments, by replacing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the my comment from above also applies here. The simplistic generics of Go force a lot of this "cruft", too. This function is only used in |
||
| // we cannot pass "cast[SuperType]" as a function pointer, so we need this aid | ||
| conversion := func(o Type) (SuperType, bool) { | ||
| return cast[SuperType](o) | ||
| } | ||
|
|
||
| return Convert(conversion, a) | ||
| } | ||
|
|
||
| // Convert converts from one assertion type to another by converting the tested value. | ||
| // It respectes the ObjectNameAssertion and ObjectNamespaceAssertion so that assertions | ||
| // can still be used to identify the object after conversion. | ||
| // The provided accessor can be fallible, returning false on the failure to convert the object. | ||
| func Convert[From any, To any](accessor func(To) (From, bool), assertion Assertion[From]) Assertion[To] { | ||
| if _, ok := assertion.(ObjectNameAssertion); ok { | ||
| return &convertedObjectName[From, To]{convertedAssertion: convertedAssertion[From, To]{accessor: accessor, assertion: assertion}} | ||
| } else if _, ok := assertion.(ObjectNamespaceAssertion); ok { | ||
| return &convertedObjectNamespace[From, To]{convertedAssertion: convertedAssertion[From, To]{accessor: accessor, assertion: assertion}} | ||
| } else { | ||
| return &convertedAssertion[From, To]{accessor: accessor, assertion: assertion} | ||
| } | ||
| } | ||
|
|
||
| // ConvertAll performs Convert on all the provided assertions. | ||
| func ConvertAll[From any, To any](accessor func(To) (From, bool), assertions ...Assertion[From]) Assertions[To] { | ||
| tos := make(Assertions[To], len(assertions)) | ||
| for i, a := range assertions { | ||
| tos[i] = Convert(accessor, a) | ||
| } | ||
| return tos | ||
| } | ||
|
|
||
| // cast casts the obj into T. This is strangely required in cases where you want to cast | ||
| // object that is typed using a type parameter into a type specified by another type parameter. | ||
| // The compiler rejects such casts but doesn't complain if the cast is done using | ||
| // an indirection using this function. | ||
| func cast[T any](obj any) (T, bool) { | ||
| ret, ok := obj.(T) | ||
| return ret, ok | ||
| } | ||
|
|
||
| type convertedAssertion[From any, To any] struct { | ||
| assertion Assertion[From] | ||
| accessor func(To) (From, bool) | ||
| } | ||
|
|
||
| func (ca *convertedAssertion[From, To]) Test(t AssertT, obj To) { | ||
| t.Helper() | ||
| o, ok := ca.accessor(obj) | ||
| if !ok { | ||
| t.Errorf("invalid conversion") | ||
| return | ||
| } | ||
| ca.assertion.Test(t, o) | ||
| } | ||
|
|
||
| type convertedObjectName[From any, To any] struct { | ||
| convertedAssertion[From, To] | ||
| } | ||
|
|
||
| func (con *convertedObjectName[From, To]) Name() string { | ||
| return con.assertion.(ObjectNameAssertion).Name() | ||
| } | ||
|
|
||
| type convertedObjectNamespace[From any, To any] struct { | ||
| convertedAssertion[From, To] | ||
| } | ||
|
|
||
| func (con *convertedObjectNamespace[From, To]) Namespace() string { | ||
| return con.assertion.(ObjectNamespaceAssertion).Namespace() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package metadata | ||
|
|
||
| import ( | ||
| "github.com/codeready-toolchain/toolchain-e2e/testsupport/assertions" | ||
| "github.com/stretchr/testify/assert" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| ) | ||
|
|
||
| // MetadataAssertions is a set of assertions on the metadata of any client.Object. | ||
| type MetadataAssertions struct { | ||
| assertions.Assertions[client.Object] | ||
| } | ||
|
|
||
| // With is a "readable" constructor of MetadataAssertions. It is meant to be used | ||
| // to construct the MetadataAssertions instance so that the call reads like an English | ||
| // sentence: "metadata.With().Name().Namespace()..." | ||
| func With() *MetadataAssertions { | ||
| return &MetadataAssertions{} | ||
| } | ||
|
|
||
| // objectName is a special impl of an assertion on object name that also implements | ||
| // the assertions.ObjectNameAssertion so that it can be used in await methods to | ||
| // identify the object. | ||
| type objectName struct { | ||
| name string | ||
| } | ||
|
|
||
| // objectName is a special impl of an assertion on object name that also implements | ||
| // the assertions.ObjectNamespaceAssertion so that it can be used in await methods to | ||
| // identify the object. | ||
| type objectNamespace struct { | ||
| namespace string | ||
| } | ||
|
|
||
| // Name adds an assertion on the objects name being equal to the provided value. | ||
| // The assertion also implements the assertions.ObjectNameAssertion so that it can be | ||
| // transparently used to identify the object during the assertions.Await calls. | ||
| func (ma *MetadataAssertions) Name(name string) *MetadataAssertions { | ||
| ma.Assertions = assertions.Append(ma.Assertions, &objectName{name: name}) | ||
| return ma | ||
| } | ||
|
|
||
| // Name adds an assertion on the objects namespace being equal to the provided value. | ||
| // The assertion also implements the assertions.ObjectNamespaceAssertion so that it can be | ||
| // transparently used to identify the object during the assertions.Await calls. | ||
| func (ma *MetadataAssertions) Namespace(ns string) *MetadataAssertions { | ||
| ma.Assertions = assertions.Append(ma.Assertions, &objectNamespace{namespace: ns}) | ||
| return ma | ||
| } | ||
|
|
||
| // Label adds an assertion for the presence of the label on the object. | ||
| func (ma *MetadataAssertions) Label(name string) *MetadataAssertions { | ||
| ma.Assertions = assertions.AppendFunc(ma.Assertions, func(t assertions.AssertT, obj client.Object) { | ||
| t.Helper() | ||
| assert.Contains(t, obj.GetLabels(), name, "no label called '%s' found on the object", name) | ||
| }) | ||
| return ma | ||
| } | ||
|
|
||
| func (ma *MetadataAssertions) NoLabel(name string) *MetadataAssertions { | ||
| ma.Assertions = assertions.AppendFunc(ma.Assertions, func(t assertions.AssertT, obj client.Object) { | ||
| t.Helper() | ||
| assert.NotContains(t, obj.GetLabels(), name, "a label called '%s' found on the object but none expected", name) | ||
| }) | ||
| return ma | ||
| } | ||
|
|
||
| func (a *objectName) Test(t assertions.AssertT, obj client.Object) { | ||
| t.Helper() | ||
| assert.Equal(t, a.name, obj.GetName(), "object name doesn't match") | ||
| } | ||
|
|
||
| func (a *objectName) Name() string { | ||
| return a.name | ||
| } | ||
|
|
||
| func (a *objectNamespace) Test(t assertions.AssertT, obj client.Object) { | ||
| t.Helper() | ||
| assert.Equal(t, a.namespace, obj.GetNamespace(), "object namespace doesn't match") | ||
| } | ||
|
|
||
| func (a *objectNamespace) Namespace() string { | ||
| return a.namespace | ||
| } | ||
|
|
||
| var ( | ||
| _ assertions.Assertion[client.Object] = (*objectName)(nil) | ||
| _ assertions.Assertion[client.Object] = (*objectNamespace)(nil) | ||
| _ assertions.ObjectNameAssertion = (*objectName)(nil) | ||
| _ assertions.ObjectNamespaceAssertion = (*objectNamespace)(nil) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using
anylooks to complicate the things a bit more (there is the need to cast the assertions).I think that we can rely on the fact that this API should be used only for
client.Objecttype - this assumption could simplify the code a bit.Let's keep in mind that we want to improve the wait functions for k8s resources, we don't want to implement a new assertion library for any object type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the restriction to
client.Objecthere is completely arbitrary - the API as it is doesn't need theTto be aclient.Objectso we shouldn't restrict it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't say that it's arbitrary. The API is gonna be used for resources from OpenShift clusters and all of them should implement
client.Objectinterface. Restricting the API to this type makes the logic simpler.