Skip to content

Commit

Permalink
Add wrapper interface for gomock.Call
Browse files Browse the repository at this point in the history
Currently when type-safe return values are used, one can't use
`InOrder` neither `After` with them as they expect
`*gomock.Call` as parameters. This introduces a new interface called
`WrapperCall` to support usage for this methods when typed
flag is used.

This interface helps to have compile-time error/warnings instead of
runtime errors with a reflection based solution.

Fixes (#70)
  • Loading branch information
EstebanOlmedo committed Sep 5, 2023
1 parent 837f20a commit 0d99752
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 14 deletions.
22 changes: 15 additions & 7 deletions gomock/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import (
"strings"
)

type WrapperCall interface {
After(WrapperCall) WrapperCall
isPreReq(WrapperCall) bool
satisfied() bool
}

// Call represents an expected call to a mock.
type Call struct {
t TestHelper // for triggering test failures on invalid call setup
Expand All @@ -31,7 +37,7 @@ type Call struct {
args []Matcher // the args
origin string // file and line number of call setup

preReqs []*Call // prerequisite calls
preReqs []WrapperCall // prerequisite calls

// Expectations
minCalls, maxCalls int
Expand Down Expand Up @@ -75,8 +81,10 @@ func newCall(t TestHelper, receiver any, method string, methodType reflect.Type,
}
return rets
}}
return &Call{t: t, receiver: receiver, method: method, methodType: methodType,
args: mArgs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions}
return &Call{
t: t, receiver: receiver, method: method, methodType: methodType,
args: mArgs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions,
}
}

// AnyTimes allows the expectation to be called 0 or more times
Expand Down Expand Up @@ -278,7 +286,7 @@ func (c *Call) SetArg(n int, value any) *Call {
}

// isPreReq returns true if other is a direct or indirect prerequisite to c.
func (c *Call) isPreReq(other *Call) bool {
func (c *Call) isPreReq(other WrapperCall) bool {
for _, preReq := range c.preReqs {
if other == preReq || preReq.isPreReq(other) {
return true
Expand All @@ -288,7 +296,7 @@ func (c *Call) isPreReq(other *Call) bool {
}

// After declares that the call may only match after preReq has been exhausted.
func (c *Call) After(preReq *Call) *Call {
func (c *Call) After(preReq WrapperCall) WrapperCall {
c.t.Helper()

if c == preReq {
Expand Down Expand Up @@ -423,7 +431,7 @@ func (c *Call) matches(args []any) error {

// dropPrereqs tells the expected Call to not re-check prerequisite calls any
// longer, and to return its current set.
func (c *Call) dropPrereqs() (preReqs []*Call) {
func (c *Call) dropPrereqs() (preReqs []WrapperCall) {
preReqs = c.preReqs
c.preReqs = nil
return
Expand All @@ -435,7 +443,7 @@ func (c *Call) call() []func([]any) []any {
}

// InOrder declares that the given calls should occur in order.
func InOrder(calls ...*Call) {
func InOrder(calls ...WrapperCall) {
for i := 1; i < len(calls); i++ {
calls[i].After(calls[i-1])
}
Expand Down
5 changes: 4 additions & 1 deletion gomock/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,10 @@ func (ctrl *Controller) Call(receiver any, method string, args ...any) []any {
// * and the prerequite calls are no longer expected, so remove them.
preReqCalls := expected.dropPrereqs()
for _, preReqCall := range preReqCalls {
ctrl.expectedCalls.Remove(preReqCall)
// This is always true for generated code
if prq, ok := preReqCall.(*Call); ok {
ctrl.expectedCalls.Remove(prq)
}
}

actions := expected.call()
Expand Down
4 changes: 4 additions & 0 deletions mockgen/internal/tests/typed_after_in_order/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# `InOrder` and `*gomock.Call.After()` with typed generated code

From #70, this tests that `InOrder` and `*gomock.Call.After` work with code
generated usign the `typed` flag.
15 changes: 15 additions & 0 deletions mockgen/internal/tests/typed_after_in_order/inorder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package inorder


//go:generate mockgen -package inorder -source=inorder.go -destination=mock.go -typed
type Animal interface {
GetNoise() string
Feed(string) error
}

func Interact(a Animal, food string) (string, error) {
if err := a.Feed(food); err != nil {
return "", err
}
return a.GetNoise(), nil
}
26 changes: 26 additions & 0 deletions mockgen/internal/tests/typed_after_in_order/inorder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package inorder

import (
"testing"
"fmt"
"go.uber.org/mock/gomock"
)

func TestInteract(t * testing.T) {
ctrl := gomock.NewController(t)

mockAnimal := NewMockAnimal(ctrl)
gomock.InOrder(
mockAnimal.EXPECT().Feed("burguir").DoAndReturn(func(s string) error {
if s != "chocolate" {
return nil
}
return fmt.Errorf("Dogs can't eat chocolate!")
}),
mockAnimal.EXPECT().GetNoise().Return("Woof!"),
)
_, err := Interact(mockAnimal, "burguir")
if err != nil {
t.Fatalf("sad")
}
}
114 changes: 114 additions & 0 deletions mockgen/internal/tests/typed_after_in_order/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions mockgen/mockgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ func (g *generator) GenerateMockRecorderMethod(intf *model.Interface, mockType s
}
if typed {
g.p(`call := %s.mock.ctrl.RecordCallWithMethodType(%s.mock, "%s", reflect.TypeOf((*%s%s)(nil).%s)%s)`, idRecv, idRecv, m.Name, mockType, shortTp, m.Name, callArgs)
g.p(`return &%s%sCall%s{Call: call}`, intf.Name, m.Name, shortTp)
g.p(`return &%s%sCall%s{WrapperCall: call}`, intf.Name, m.Name, shortTp)
} else {
g.p(`return %s.mock.ctrl.RecordCallWithMethodType(%s.mock, "%s", reflect.TypeOf((*%s%s)(nil).%s)%s)`, idRecv, idRecv, m.Name, mockType, shortTp, m.Name, callArgs)
}
Expand Down Expand Up @@ -665,10 +665,10 @@ func (g *generator) GenerateMockReturnCallMethod(intf *model.Interface, m *model

recvStructName := intf.Name + m.Name

g.p("// %s%sCall wrap *gomock.Call", intf.Name, m.Name)
g.p("// %s%sCall wrap *gomock.Call as a gomock.WrapperCall", intf.Name, m.Name)
g.p("type %s%sCall%s struct{", intf.Name, m.Name, longTp)
g.in()
g.p("*gomock.Call")
g.p("gomock.WrapperCall")
g.out()
g.p("}")

Expand All @@ -679,23 +679,23 @@ func (g *generator) GenerateMockReturnCallMethod(intf *model.Interface, m *model
if len(retNames) > 0 {
retArgs = strings.Join(retNames, ", ")
}
g.p(`%s.Call = %v.Call.Return(%v)`, idRecv, idRecv, retArgs)
g.p(`%s.WrapperCall = %v.WrapperCall.(*gomock.Call).Return(%v)`, idRecv, idRecv, retArgs)
g.p("return %s", idRecv)
g.out()
g.p("}")

g.p("// Do rewrite *gomock.Call.Do")
g.p("func (%s *%sCall%s) Do(f func(%v)%v) *%sCall%s {", idRecv, recvStructName, shortTp, argString, retString, recvStructName, shortTp)
g.in()
g.p(`%s.Call = %v.Call.Do(f)`, idRecv, idRecv)
g.p(`%s.WrapperCall = %v.WrapperCall.(*gomock.Call).Do(f)`, idRecv, idRecv)
g.p("return %s", idRecv)
g.out()
g.p("}")

g.p("// DoAndReturn rewrite *gomock.Call.DoAndReturn")
g.p("func (%s *%sCall%s) DoAndReturn(f func(%v)%v) *%sCall%s {", idRecv, recvStructName, shortTp, argString, retString, recvStructName, shortTp)
g.in()
g.p(`%s.Call = %v.Call.DoAndReturn(f)`, idRecv, idRecv)
g.p(`%s.WrapperCall = %v.WrapperCall.(*gomock.Call).DoAndReturn(f)`, idRecv, idRecv)
g.p("return %s", idRecv)
g.out()
g.p("}")
Expand Down

0 comments on commit 0d99752

Please sign in to comment.