Skip to content

Commit 19ae601

Browse files
committed
accept async functions with testing.TB variant instead of Gomega
In Kubernetes' ktesting package the following assertion is valid: tCtx.Eventually(func(tCtx TContext)).Should(Succeed()) That works because tCtx.Eventually wraps the function such that failures reported via tCtx.Error/Errorf/Fatal/Fatalf are visible to the matcher. Without this change, the "Success matcher only support a single error value, or function with Gomega as its first parameter" warning is triggered. Instead of building in specific support for ktesting, the approach is to check for a first parameter which has any (not necessarily all!) of the testing.TB methods for reporting failures. That is considered a valid alternative to Gomega. There's now a potential false negative in the linter when passing such a function directly to gomega.Eventually/Consistently. That'll fail at runtime because gomega cannot call it, so such a false negative isn't too bad (fails reliably and obviously). The warning message doesn't get changed because it would be confusing for most users to call out some vague alternative (vague because the linter has no details) that most users won't be able to use.
1 parent 9534693 commit 19ae601

5 files changed

Lines changed: 64 additions & 8 deletions

File tree

internal/expression/actual/actualarg.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
FuncSigArgType
3030
ErrFuncActualArgType
3131
GomegaParamArgType
32+
TBParamArgType
3233
MultiRetsArgType
3334
ErrorMethodArgType
3435

internal/expression/actual/asyncfuncarg.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ func getAsyncFuncArg(sig *gotypes.Signature) ArgPayload {
1717

1818
if sig.Params().Len() > 0 {
1919
arg := sig.Params().At(0).Type()
20-
if gomegainfo.IsGomegaType(arg) && sig.Results().Len() == 0 {
21-
argType |= FuncSigArgType | GomegaParamArgType
20+
if sig.Results().Len() == 0 {
21+
if gomegainfo.IsGomegaType(arg) {
22+
argType |= FuncSigArgType | GomegaParamArgType
23+
}
24+
if implementsTB(arg) {
25+
argType |= FuncSigArgType | TBParamArgType
26+
}
2227
}
2328
}
2429

@@ -36,3 +41,34 @@ type FuncSigArgPayload struct {
3641
func (f FuncSigArgPayload) ArgType() ArgType {
3742
return f.argType
3843
}
44+
45+
// implementsTB checks if the argument type implements any of the methods in testing.TB which
46+
// can be used to report test failures. Such a type is a potential alternative to a Gomega
47+
// parameter in some Gomega wrappers.
48+
func implementsTB(t gotypes.Type) bool {
49+
for _, interfaceType := range tbInterfaces {
50+
if gotypes.Implements(t, interfaceType) {
51+
return true
52+
}
53+
}
54+
return false
55+
}
56+
57+
var tbInterfaces = []*gotypes.Interface{
58+
tbInterface("Error", false),
59+
}
60+
61+
func tbInterface(name string, printf bool) *gotypes.Interface {
62+
var params []*gotypes.Var
63+
if printf {
64+
params = append(params, gotypes.NewVar(0, nil, "", gotypes.Typ[gotypes.String]))
65+
}
66+
params = append(params, gotypes.NewVar(0, nil, "", gotypes.NewSlice(gotypes.Universe.Lookup("any").Type())))
67+
signature := gotypes.NewSignatureType(nil, nil, nil,
68+
gotypes.NewTuple(params...),
69+
gotypes.NewTuple(),
70+
true,
71+
)
72+
method := gotypes.NewFunc(0, nil, name, signature)
73+
return gotypes.NewInterface([]*gotypes.Func{method}, nil)
74+
}

internal/rules/asyncsucceedrule.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ func (AsyncSucceedRule) isApply(gexp *expression.GomegaExpression) bool {
1717
return gexp.IsAsync() &&
1818
gexp.MatcherTypeIs(matcher.SucceedMatcherType) &&
1919
gexp.ActualArgTypeIs(actual.FuncSigArgType) &&
20-
!gexp.ActualArgTypeIs(actual.ErrorTypeArgType|actual.GomegaParamArgType)
20+
!gexp.ActualArgTypeIs(actual.ErrorTypeArgType|actual.GomegaParamArgType|actual.TBParamArgType)
2121
}
2222

2323
func (r AsyncSucceedRule) Apply(gexp *expression.GomegaExpression, _ config.Config, reportBuilder *reports.Builder) bool {
2424
if r.isApply(gexp) {
2525
if gexp.ActualArgTypeIs(actual.MultiRetsArgType) {
2626
reportBuilder.AddIssue(false, "Success matcher does not support multiple values")
2727
} else {
28+
// The message intentionally does not call out "function with a TB implementation" as another alternative because
29+
// that alternative is not valid for generic Gomega - it would be confusing for many users. Users
30+
// of a Gomega wrapper which supports such functions must figure that out themselves.
2831
reportBuilder.AddIssue(false, "Success matcher only support a single error value, or function with Gomega as its first parameter")
2932
}
3033
}

testdata/src/a/eventually/eventuallyFunc.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,14 @@ var _ = Describe("Eventually with function", func() {
6262

6363
Eventually(retFunc(5)).Should(Equal(5)) // valid. retFunc returns a function
6464
ch := make(chan int)
65-
Eventually(ch).Should(BeClosed()) // valid
66-
Eventually(slowInt).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // valid
67-
Eventually(slowInt).WithPolling(time.Second * 2).WithTimeout(time.Millisecond * 100).Should(Equal(42)) // not valid, if validate-async-intervals flag is set
68-
Eventually(slowInt()).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // want `ginkgo-linter: use a function call in Eventually\. This actually checks nothing, because Eventually receives the function returned value, instead of function itself, and this value is never changed\. Consider using .Eventually\(slowInt\)\.WithPolling\(time\.Millisecond \* 100\)\.WithTimeout\(time\.Second \* 2\)\.Should\(Equal\(42\)\). instead`
69-
wrappers.MyEventually(slowInt()).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // want `ginkgo-linter: use a function call in MyEventually\. This actually checks nothing, because MyEventually receives the function returned value, instead of function itself, and this value is never changed\. Consider using .wrappers.MyEventually\(slowInt\)\.WithPolling\(time\.Millisecond \* 100\)\.WithTimeout\(time\.Second \* 2\)\.Should\(Equal\(42\)\). instead`
65+
Eventually(ch).Should(BeClosed()) // valid
66+
Eventually(slowInt).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // valid
67+
Eventually(slowInt).WithPolling(time.Second * 2).WithTimeout(time.Millisecond * 100).Should(Equal(42)) // not valid, if validate-async-intervals flag is set
68+
Eventually(slowInt()).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // want `ginkgo-linter: use a function call in Eventually\. This actually checks nothing, because Eventually receives the function returned value, instead of function itself, and this value is never changed\. Consider using .Eventually\(slowInt\)\.WithPolling\(time\.Millisecond \* 100\)\.WithTimeout\(time\.Second \* 2\)\.Should\(Equal\(42\)\). instead`
69+
wrappers.MyEventually(slowInt()).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // want `ginkgo-linter: use a function call in MyEventually\. This actually checks nothing, because MyEventually receives the function returned value, instead of function itself, and this value is never changed\. Consider using .wrappers.MyEventually\(slowInt\)\.WithPolling\(time\.Millisecond \* 100\)\.WithTimeout\(time\.Second \* 2\)\.Should\(Equal\(42\)\). instead`
70+
Eventually(func() {}).Should(Succeed()) // want `ginkgo-linter: Success matcher only support a single error value, or function with Gomega as its first parameter`
71+
wrappers.MyEventually(func() {}).Should(Succeed()) // want `ginkgo-linter: Success matcher only support a single error value, or function with Gomega as its first parameter`
72+
wrappers.MyEventually(func(tb wrappers.MyTB) {}).Should(Succeed())
7073
Eventually(context.TODO(), slowInt()).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // want `ginkgo-linter: use a function call in Eventually\. This actually checks nothing, because Eventually receives the function returned value, instead of function itself, and this value is never changed\. Consider using .Eventually\(context\.TODO\(\), slowInt\)\.WithPolling\(time\.Millisecond \* 100\)\.WithTimeout\(time\.Second \* 2\)\.Should\(Equal\(42\)\). instead`
7174
ctx := context.TODO()
7275
Eventually(ctx, slowInt()).WithPolling(time.Millisecond * 100).WithTimeout(time.Second * 2).Should(Equal(42)) // want `ginkgo-linter: use a function call in Eventually\. This actually checks nothing, because Eventually receives the function returned value, instead of function itself, and this value is never changed\. Consider using .Eventually\(ctx, slowInt\)\.WithPolling\(time\.Millisecond \* 100\)\.WithTimeout\(time\.Second \* 2\)\.Should\(Equal\(42\)\). instead`

testdata/src/a/wrappers/wrappers.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
package wrappers
22

33
import (
4+
"testing"
5+
46
gtypes "github.com/onsi/gomega/types"
57

68
g "github.com/onsi/gomega"
79
)
810

11+
type MyTB interface {
12+
Error(args ...any)
13+
Errorf(format string, args ...any)
14+
Fatal(args ...any)
15+
Fatalf(format string, args ...any)
16+
}
17+
18+
var _ MyTB = &testing.T{}
19+
var _ MyTB = &testing.B{}
20+
921
var Expect = g.Expect
1022

1123
func MyExpect(value any) g.Assertion {
1224
return g.Expect(value)
1325
}
1426

27+
// value = func(tb MyTB) without return value is okay, failures can be reported via tb.Error/Fatal/Errorf/Fatalf.
1528
func MyEventually(value any) g.AsyncAssertion {
1629
return g.Eventually(value)
1730
}

0 commit comments

Comments
 (0)