Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions testsupport/assertions/assertion.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package assertions

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var _ Assertion[bool] = (AssertionFunc[bool])(nil)

type Assertion[T any] func(t AssertT, obj T)
type Assertion[T any] interface {
Test(t AssertT, obj T)
}

type AssertionFunc[T any] func(t AssertT, obj T)

type EmbeddableAssertions[Self any, T any] struct {
assertions *[]Assertion[T]
Expand All @@ -16,23 +17,36 @@ type WithAssertions[T any] interface {
Assertions() []Assertion[T]
}

type AssertT interface {
assert.TestingT
Helper()
}

type RequireT interface {
require.TestingT
Helper()
func Test[T any, A WithAssertions[T]](t AssertT, obj T, assertions A) {
t.Helper()
testInner(t, obj, assertions, false)
}

func Test[T any, A WithAssertions[T]](t AssertT, obj T, assertions A) {
func testInner[T any, A WithAssertions[T]](t AssertT, obj T, assertions A, suppressLogAround bool) {
t.Helper()
ft := &failureTrackingT{AssertT: t}

if !suppressLogAround {
t.Logf("About to test object %T with assertions", obj)
}

for _, a := range assertions.Assertions() {
a(t, obj)
a.Test(ft, obj)
}

if !suppressLogAround && ft.failed {
format, args := doExplainAfterTestFailure(obj, assertions)
t.Logf(format, args...)
}
}

func doExplainAfterTestFailure[T any, A WithAssertions[T]](obj T, assertions A) (format string, args []any) {
diff := Explain(obj, assertions)
format = "Some of the assertions failed to match the object (see output above). The following diff shows what the object should have looked like:\n%s"
args = []any{diff}
return
}

func (a *EmbeddableAssertions[Self, T]) Self() *Self {
return a.self
}
Expand All @@ -45,3 +59,8 @@ func (a *EmbeddableAssertions[Self, T]) EmbedInto(self *Self, assertions *[]Asse
func (ea *EmbeddableAssertions[Self, T]) AddAssertion(a Assertion[T]) {
*ea.assertions = append(*ea.assertions, a)
}

func (f AssertionFunc[T]) Test(t AssertT, obj T) {
t.Helper()
f(t, obj)
}
27 changes: 20 additions & 7 deletions testsupport/assertions/conditions/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,33 @@ import (
type Assertions[Self any, T any] struct {
assertions.EmbeddableAssertions[Self, T]

accessor func(T) []toolchainv1aplha1.Condition
accessor func(T) *[]toolchainv1aplha1.Condition
}

func (a *Assertions[Self, T]) EmbedInto(self *Self, assertions *[]assertions.Assertion[T], accessor func(T) []toolchainv1aplha1.Condition) {
func (a *Assertions[Self, T]) EmbedInto(self *Self, assertions *[]assertions.Assertion[T], accessor func(T) *[]toolchainv1aplha1.Condition) {
a.EmbeddableAssertions.EmbedInto(self, assertions)
a.accessor = accessor
}

func (a *Assertions[Self, T]) HasConditionWithType(typ toolchainv1aplha1.ConditionType) *Self {
a.AddAssertion(func(t assertions.AssertT, obj T) {
t.Helper()
conds := a.accessor(obj)
_, found := condition.FindConditionByType(conds, typ)
assert.True(t, found, "condition with the type %s not found", typ)
a.AddAssertion(&assertions.AssertAndFixFunc[T]{
Assert: func(t assertions.AssertT, obj T) {
t.Helper()
conds := a.accessor(obj)
_, found := condition.FindConditionByType(*conds, typ)
assert.True(t, found, "condition with the type %s not found", typ)
},
Fix: func(obj T) T {
conds := a.accessor(obj)
if *conds == nil {
*conds = []toolchainv1aplha1.Condition{}
}
*conds, _ = condition.AddOrUpdateStatusConditions(*conds, toolchainv1aplha1.Condition{
Type: toolchainv1aplha1.ConditionReady,
})

return obj
},
})
return a.Self()
}
13 changes: 13 additions & 0 deletions testsupport/assertions/deepcopy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package assertions

type deepCopy[T any] interface {
DeepCopy() T
}

func copyObject[T any](obj any) T {
if dc, ok := obj.(deepCopy[T]); ok {
return dc.DeepCopy()
}
// TODO: should we go into attempting cloning slices and maps?
return obj.(T)
}
60 changes: 60 additions & 0 deletions testsupport/assertions/fix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package assertions

import (
"fmt"
"strings"

"github.com/google/go-cmp/cmp"
)

var (
_ Assertion[bool] = (*AssertAndFixFunc[bool])(nil)
_ AssertionFixer[bool] = (*AssertAndFixFunc[bool])(nil)
)

func Explain[T any, A WithAssertions[T]](obj T, assertions A) string {
cpy := copyObject[T](obj)

nonFixingAssertions := []string{}
nonFixingAssertionsIndices := []int{}
for i, a := range assertions.Assertions() {
if f, ok := a.(AssertionFixer[T]); ok {
f.AdaptToMatch(cpy)
} else {
nonFixingAssertions = append(nonFixingAssertions, fmt.Sprintf("%T", a))
nonFixingAssertionsIndices = append(nonFixingAssertionsIndices, i)
}
}

sb := strings.Builder{}
sb.WriteString(cmp.Diff(obj, cpy))
for i := range nonFixingAssertions {
sb.WriteRune('\n')
sb.WriteString(fmt.Sprintf("the %dth assertion was not able to modify the object to match it", nonFixingAssertionsIndices[i]))
}

return sb.String()
}

type AssertionFixer[T any] interface {
AdaptToMatch(object T) T
}

type AssertAndFixFunc[T any] struct {
Assert func(t AssertT, obj T)
Fix func(obj T) T
}

func (a *AssertAndFixFunc[T]) Test(t AssertT, obj T) {
t.Helper()
if a.Assert != nil {
a.Assert(t, obj)
}
}

func (a *AssertAndFixFunc[T]) AdaptToMatch(object T) T {
if a.Fix != nil {
return a.Fix(object)
}
return object
}
59 changes: 47 additions & 12 deletions testsupport/assertions/object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,68 @@ type Assertions[Self any, T client.Object] struct {
}

func (o *Assertions[Self, T]) HasLabel(label string) *Self {
o.AddAssertion(func(t assertions.AssertT, o T) {
t.Helper()
assert.Contains(t, o.GetLabels(), label, "label '%s' not found", label)
o.AddAssertion(&assertions.AssertAndFixFunc[T]{
Assert: func(t assertions.AssertT, o T) {
t.Helper()
t.Logf("ad-hoc log message from within the HasLabel assertion :)")
assert.Contains(t, o.GetLabels(), label, "label '%s' not found", label)
},
Fix: func(o T) T {
labels := o.GetLabels()
if labels == nil {
labels = map[string]string{}
o.SetLabels(labels)
}
labels[label] = ""
return o
},
})
return o.Self()
}

func (o *Assertions[Self, T]) HasLabelWithValue(label string, value string) *Self {
o.AddAssertion(func(t assertions.AssertT, o T) {
t.Helper()
assert.Equal(t, value, o.GetLabels()[label])
o.AddAssertion(&assertions.AssertAndFixFunc[T]{
Assert: func(t assertions.AssertT, o T) {
t.Helper()
assert.Equal(t, value, o.GetLabels()[label])
},
Fix: func(o T) T {
labels := o.GetLabels()
if labels == nil {
labels = map[string]string{}
o.SetLabels(labels)
}
labels[label] = value
return o
},
})
return o.Self()
}

func (o *Assertions[Self, T]) HasName(name string) *Self {
o.AddAssertion(func(t assertions.AssertT, o T) {
t.Helper()
assert.Equal(t, name, o.GetName())
o.AddAssertion(&assertions.AssertAndFixFunc[T]{
Assert: func(t assertions.AssertT, o T) {
t.Helper()
assert.Equal(t, name, o.GetName())
},
Fix: func(o T) T {
o.SetName(name)
return o
},
})
return o.Self()
}

func (o *Assertions[Self, T]) IsInNamespace(namespace string) *Self {
o.AddAssertion(func(t assertions.AssertT, o T) {
t.Helper()
assert.Equal(t, namespace, o.GetNamespace())
o.AddAssertion(&assertions.AssertAndFixFunc[T]{
Assert: func(t assertions.AssertT, o T) {
t.Helper()
assert.Equal(t, namespace, o.GetNamespace())
},
Fix: func(o T) T {
o.SetNamespace(namespace)
return o
},
})
return o.Self()
}
14 changes: 10 additions & 4 deletions testsupport/assertions/spaceprovisionerconfig/spc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,22 @@ func (a *Assertions) Assertions() []assertions.Assertion[*toolchainv1aplha1.Spac

func That() *Assertions {
instance := &Assertions{assertions: []assertions.Assertion[*toolchainv1aplha1.SpaceProvisionerConfig]{}}
instance.EmbedInto(instance, &instance.assertions, func(spc *toolchainv1aplha1.SpaceProvisionerConfig) []toolchainv1aplha1.Condition {
return spc.Status.Conditions
instance.EmbedInto(instance, &instance.assertions, func(spc *toolchainv1aplha1.SpaceProvisionerConfig) *[]toolchainv1aplha1.Condition {
return &spc.Status.Conditions
})
instance.ObjectAssertions.EmbedInto(instance, &instance.assertions)
return instance
}

func (a *Assertions) ReferencesToolchainCluster(tc string) *Assertions {
a.assertions = append(a.assertions, func(t assertions.AssertT, spc *toolchainv1aplha1.SpaceProvisionerConfig) {
assert.Equal(t, tc, spc.Spec.ToolchainCluster)
a.assertions = append(a.assertions, &assertions.AssertAndFixFunc[*toolchainv1aplha1.SpaceProvisionerConfig]{
Assert: func(t assertions.AssertT, spc *toolchainv1aplha1.SpaceProvisionerConfig) {
assert.Equal(t, tc, spc.Spec.ToolchainCluster)
},
Fix: func(obj *toolchainv1aplha1.SpaceProvisionerConfig) *toolchainv1aplha1.SpaceProvisionerConfig {
obj.Spec.ToolchainCluster = tc
return obj
},
})
return a
}
Expand Down
28 changes: 28 additions & 0 deletions testsupport/assertions/t.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package assertions

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type AssertT interface {
assert.TestingT
Helper()
Logf(format string, args ...any)
}

type RequireT interface {
require.TestingT
Helper()
Logf(format string, args ...any)
}

type failureTrackingT struct {
AssertT
failed bool
}

func (t *failureTrackingT) Errorf(format string, args ...interface{}) {
t.failed = true
t.AssertT.Errorf(format, args...)
}
Loading
Loading