Skip to content

Commit

Permalink
Allow type-safe calls to be used in InOrder (#78)
Browse files Browse the repository at this point in the history
This modifies `InOrder` to receive variadic `any` instead of `*Call`,
allowing users to use this function with type-safe generated Calls.

This implementation uses a type assertion to check if the provided
arguments are `*Call`s or reflection to get the `*Call` when the
arguments wrap one (generated code). If neither of the two cases are
fullfiled then `InOrder` panics.

Fix #70.

---------

Co-authored-by: Sung Yoon Whang <[email protected]>
  • Loading branch information
EstebanOlmedo and sywhang authored Sep 13, 2023
1 parent 837f20a commit dde4646
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 1 deletion.
39 changes: 38 additions & 1 deletion gomock/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,12 +435,49 @@ func (c *Call) call() []func([]any) []any {
}

// InOrder declares that the given calls should occur in order.
func InOrder(calls ...*Call) {
// It panics if the type of any of the arguments isn't *Call or a generated
// mock with an embedded *Call.
func InOrder(args ...any) {
calls := make([]*Call, 0, len(args))
for i := 0; i < len(args); i++ {
if call := getCall(args[i]); call != nil {
calls = append(calls, call)
continue
}
panic(fmt.Sprintf(
"invalid argument at position %d of type %T, InOrder expects *gomock.Call or generated mock types with an embedded *gomock.Call",
i,
args[i],
))
}
for i := 1; i < len(calls); i++ {
calls[i].After(calls[i-1])
}
}

// getCall checks if the parameter is a *Call or a generated struct
// that wraps a *Call and returns the *Call pointer - if neither, it returns nil.
func getCall(arg any) *Call {
if call, ok := arg.(*Call); ok {
return call
}
t := reflect.ValueOf(arg)
if t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface {
return nil
}
t = t.Elem()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.CanInterface() {
continue
}
if call, ok := f.Interface().(*Call); ok {
return call
}
}
return nil
}

func setSlice(arg any, v reflect.Value) {
va := reflect.ValueOf(arg)
for i := 0; i < v.Len(); i++ {
Expand Down
33 changes: 33 additions & 0 deletions gomock/call_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ type b struct {
foo string
}

type c struct {
*Call
}

func (testObj b) Foo() string {
return testObj.foo
}
Expand Down Expand Up @@ -616,3 +620,32 @@ func TestCall_DoAndReturn(t *testing.T) {
})
}
}

func TestInOrder(t *testing.T) {
t.Run("process only *Call or its wrappers", func(t *testing.T) {
tr1 := &mockTestReporter{}
tr2 := &mockTestReporter{}
c1 := &Call{t: tr1}
c2 := &c{Call: &Call{t: tr2}}
InOrder(c1, c2)
if len(c2.preReqs) != 1 {
t.Fatalf("expected 1 preReq in c2, found %d", len(c2.preReqs))
}
if len(c1.preReqs) != 0 {
t.Fatalf("expected 0 preReq in c1, found %d", len(c1.preReqs))
}
})
t.Run("panic when the argument isn't a *Call or has one embeded", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("expected InOrder to panic")
}
}()
tr := &mockTestReporter{}
c := &Call{t: tr}
a := &a{
name: "Foo",
}
InOrder(c, a)
})
}
14 changes: 14 additions & 0 deletions mockgen/internal/tests/typed_inorder/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package typed_inorder

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

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

import (
"fmt"
"testing"

"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().GetSound().Return("Woof!"),
)
_, err := Interact(mockAnimal, "burguir")
if err != nil {
t.Fatalf("sad")
}
}
114 changes: 114 additions & 0 deletions mockgen/internal/tests/typed_inorder/mock.go

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

0 comments on commit dde4646

Please sign in to comment.