From dde4646ed4a3c4541f4d44303f07a16359d207ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Raymundo=20Olmedo=20Ram=C3=ADrez?= <43709368+EstebanOlmedo@users.noreply.github.com> Date: Wed, 13 Sep 2023 11:24:51 -0600 Subject: [PATCH] Allow type-safe calls to be used in InOrder (#78) 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 --- gomock/call.go | 39 +++++- gomock/call_test.go | 33 +++++ mockgen/internal/tests/typed_inorder/input.go | 14 +++ .../tests/typed_inorder/input_test.go | 27 +++++ mockgen/internal/tests/typed_inorder/mock.go | 114 ++++++++++++++++++ 5 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 mockgen/internal/tests/typed_inorder/input.go create mode 100644 mockgen/internal/tests/typed_inorder/input_test.go create mode 100644 mockgen/internal/tests/typed_inorder/mock.go diff --git a/gomock/call.go b/gomock/call.go index b8a06ac..e1ea826 100644 --- a/gomock/call.go +++ b/gomock/call.go @@ -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++ { diff --git a/gomock/call_test.go b/gomock/call_test.go index 1e7d034..489956a 100644 --- a/gomock/call_test.go +++ b/gomock/call_test.go @@ -38,6 +38,10 @@ type b struct { foo string } +type c struct { + *Call +} + func (testObj b) Foo() string { return testObj.foo } @@ -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) + }) +} diff --git a/mockgen/internal/tests/typed_inorder/input.go b/mockgen/internal/tests/typed_inorder/input.go new file mode 100644 index 0000000..a1ed291 --- /dev/null +++ b/mockgen/internal/tests/typed_inorder/input.go @@ -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 +} diff --git a/mockgen/internal/tests/typed_inorder/input_test.go b/mockgen/internal/tests/typed_inorder/input_test.go new file mode 100644 index 0000000..94fc5ed --- /dev/null +++ b/mockgen/internal/tests/typed_inorder/input_test.go @@ -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") + } +} diff --git a/mockgen/internal/tests/typed_inorder/mock.go b/mockgen/internal/tests/typed_inorder/mock.go new file mode 100644 index 0000000..39de997 --- /dev/null +++ b/mockgen/internal/tests/typed_inorder/mock.go @@ -0,0 +1,114 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: input.go +// +// Generated by this command: +// +// mockgen -package typed_inorder -source=input.go -destination=mock.go -typed +// +// Package typed_inorder is a generated GoMock package. +package typed_inorder + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockAnimal is a mock of Animal interface. +type MockAnimal struct { + ctrl *gomock.Controller + recorder *MockAnimalMockRecorder +} + +// MockAnimalMockRecorder is the mock recorder for MockAnimal. +type MockAnimalMockRecorder struct { + mock *MockAnimal +} + +// NewMockAnimal creates a new mock instance. +func NewMockAnimal(ctrl *gomock.Controller) *MockAnimal { + mock := &MockAnimal{ctrl: ctrl} + mock.recorder = &MockAnimalMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAnimal) EXPECT() *MockAnimalMockRecorder { + return m.recorder +} + +// Feed mocks base method. +func (m *MockAnimal) Feed(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Feed", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Feed indicates an expected call of Feed. +func (mr *MockAnimalMockRecorder) Feed(arg0 any) *AnimalFeedCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Feed", reflect.TypeOf((*MockAnimal)(nil).Feed), arg0) + return &AnimalFeedCall{Call: call} +} + +// AnimalFeedCall wrap *gomock.Call +type AnimalFeedCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *AnimalFeedCall) Return(arg0 error) *AnimalFeedCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *AnimalFeedCall) Do(f func(string) error) *AnimalFeedCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *AnimalFeedCall) DoAndReturn(f func(string) error) *AnimalFeedCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetSound mocks base method. +func (m *MockAnimal) GetSound() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSound") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetSound indicates an expected call of GetSound. +func (mr *MockAnimalMockRecorder) GetSound() *AnimalGetSoundCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSound", reflect.TypeOf((*MockAnimal)(nil).GetSound)) + return &AnimalGetSoundCall{Call: call} +} + +// AnimalGetSoundCall wrap *gomock.Call +type AnimalGetSoundCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *AnimalGetSoundCall) Return(arg0 string) *AnimalGetSoundCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *AnimalGetSoundCall) Do(f func() string) *AnimalGetSoundCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *AnimalGetSoundCall) DoAndReturn(f func() string) *AnimalGetSoundCall { + c.Call = c.Call.DoAndReturn(f) + return c +}