diff --git a/gomock/controller.go b/gomock/controller.go index 9d17a2f..af41a0f 100644 --- a/gomock/controller.go +++ b/gomock/controller.go @@ -206,7 +206,11 @@ func (ctrl *Controller) Call(receiver any, method string, args ...any) []any { // and this line changes, i.e. this code is wrapped in another anonymous function. // 0 is us, 1 is controller.Call(), 2 is the generated mock, and 3 is the user's test. origin := callerInfo(3) - ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err) + stringArgs := make([]string, len(args)) + for i, arg := range args { + stringArgs[i] = getString(arg) + } + ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, stringArgs, origin, err) } // Two things happen here: diff --git a/gomock/controller_test.go b/gomock/controller_test.go index 2489226..a1b786d 100644 --- a/gomock/controller_test.go +++ b/gomock/controller_test.go @@ -203,6 +203,22 @@ func TestNoRecordedMatchingMethodNameForAReceiver(t *testing.T) { }) } +func TestNoStringerDeadlockOnError(t *testing.T) { + reporter, ctrl := createFixtures(t) + subject := new(Subject) + mockFoo := NewMockFoo(ctrl) + var _ fmt.Stringer = mockFoo + + ctrl.RecordCall(subject, "FooMethod", mockFoo) + reporter.assertFatal(func() { + ctrl.Call(subject, "NotRecordedMethod", mockFoo) + }, "Unexpected call to", "there are no expected calls of the method \"NotRecordedMethod\" for that receiver") + reporter.assertFatal(func() { + // The expected call wasn't made. + ctrl.Finish() + }) +} + // This tests that a call with an arguments of some primitive type matches a recorded call. func TestExpectedMethodCall(t *testing.T) { reporter, ctrl := createFixtures(t) diff --git a/gomock/matchers.go b/gomock/matchers.go index c172550..d0590d0 100644 --- a/gomock/matchers.go +++ b/gomock/matchers.go @@ -133,7 +133,7 @@ func (e eqMatcher) Matches(x any) bool { } func (e eqMatcher) String() string { - return fmt.Sprintf("is equal to %v (%T)", e.x, e.x) + return fmt.Sprintf("is equal to %s (%T)", getString(e.x), e.x) } type nilMatcher struct{} diff --git a/gomock/mock_test.go b/gomock/mock_test.go index 9037af1..aa632a4 100644 --- a/gomock/mock_test.go +++ b/gomock/mock_test.go @@ -38,6 +38,11 @@ func (m *MockFoo) EXPECT() *MockFooMockRecorder { return m.recorder } +// ISGOMOCK indicates that this struct is a gomock mock. +func (m *MockFoo) ISGOMOCK() struct{} { + return struct{}{} +} + // Bar mocks base method. func (m *MockFoo) Bar(arg0 string) string { m.ctrl.T.Helper() @@ -51,3 +56,17 @@ func (mr *MockFooMockRecorder) Bar(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bar", reflect.TypeOf((*MockFoo)(nil).Bar), arg0) } + +// String mocks base method. +func (m *MockFoo) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String. +func (mr *MockFooMockRecorder) String() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockFoo)(nil).String)) +} diff --git a/gomock/string.go b/gomock/string.go new file mode 100644 index 0000000..6daf917 --- /dev/null +++ b/gomock/string.go @@ -0,0 +1,25 @@ +package gomock + +import "fmt" + +type mockInstance interface { + ISGOMOCK() struct{} +} +type mockedStringer interface { + fmt.Stringer + mockInstance +} + +// getString is a safe way to convert a value to a string for printing results +// If the value is a a mock, getString avoids calling the mocked String() method, +// which avoids potential deadlocks +func getString(x any) string { + switch v := x.(type) { + case mockedStringer: + return fmt.Sprintf("%T", v) + case fmt.Stringer: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/mockgen/mockgen.go b/mockgen/mockgen.go index df7d85f..0fac86b 100644 --- a/mockgen/mockgen.go +++ b/mockgen/mockgen.go @@ -502,6 +502,14 @@ func (g *generator) GenerateMockInterface(intf *model.Interface, outputPackagePa g.out() g.p("}") + // XXX: possible name collision here if someone has ISGOMOCK in their interface. + g.p("// ISGOMOCK indicates that this struct is a gomock mock.") + g.p("func (m *%v%v) ISGOMOCK() struct{} {", mockType, shortTp) + g.in() + g.p("return struct{}{}") + g.out() + g.p("}") + g.GenerateMockMethods(mockType, intf, outputPackagePath, longTp, shortTp, *typed) return nil