Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt cmp.Diff for showing unmatched arguments #154

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module go.uber.org/mock
go 1.20

require (
github.com/google/go-cmp v0.6.0
golang.org/x/mod v0.15.0
golang.org/x/tools v0.18.0
)
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
Expand Down
48 changes: 38 additions & 10 deletions gomock/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"reflect"
"strconv"
"strings"

"github.com/google/go-cmp/cmp"
)

// Call represents an expected call to a mock.
Expand All @@ -42,11 +44,13 @@ type Call struct {
// can set the return values by returning a non-nil slice. Actions run in the
// order they are created.
actions []func([]any) []any

cmpOpts cmp.Options // comparison options
}

// newCall creates a *Call. It requires the method type in order to support
// unexported methods.
func newCall(t TestHelper, receiver any, method string, methodType reflect.Type, args ...any) *Call {
func newCall(t TestHelper, receiver any, method string, methodType reflect.Type, cmpOpts cmp.Options, args ...any) *Call {
t.Helper()

// TODO: check arity, types.
Expand Down Expand Up @@ -76,7 +80,8 @@ 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}
args: mArgs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions,
cmpOpts: cmpOpts}
}

// AnyTimes allows the expectation to be called 0 or more times
Expand Down Expand Up @@ -317,6 +322,30 @@ func (c *Call) String() string {
return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin)
}

func (c *Call) matchError(m Matcher, arg any) error {
if g, ok := m.(GotFormatter); ok {
return fmt.Errorf(
"\nGot: %v\nWant: %v",
g.Got(arg), m,
)
}
if d, ok := m.(Differ); ok {
diff := d.Diff(arg, c.cmpOpts...)
// Recover if the diff is empty, implying the match failed on ignored fields.
if diff == "" {
return nil
}
return fmt.Errorf(
"\nDiff (-want +got): %s",
diff,
)
}
return fmt.Errorf(
"\nGot: %v\nWant: %v",
formatGottenArg(m, arg), m,
)
}

// Tests if the given call matches the expected call.
// If yes, returns nil. If no, returns error with message explaining why it does not match.
func (c *Call) matches(args []any) error {
Expand All @@ -327,11 +356,9 @@ func (c *Call) matches(args []any) error {
}

for i, m := range c.args {
if !m.Matches(args[i]) {
return fmt.Errorf(
"expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v",
c.origin, i, formatGottenArg(m, args[i]), m,
)
arg := args[i]
if !m.Matches(arg) {
return fmt.Errorf("expected call at %s doesn't match the argument at index %d: %w", c.origin, i, c.matchError(m, arg))
}
}
} else {
Expand All @@ -349,11 +376,12 @@ func (c *Call) matches(args []any) error {
}

for i, m := range c.args {
arg := args[i]
if i < c.methodType.NumIn()-1 {
// Non-variadic args
if !m.Matches(args[i]) {
return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v",
c.origin, strconv.Itoa(i), formatGottenArg(m, args[i]), m)
if !m.Matches(arg) {
return fmt.Errorf("expected call at %s doesn't match the argument at index %d: %w",
c.origin, i, c.matchError(m, args[i]))
}
continue
}
Expand Down
8 changes: 4 additions & 4 deletions gomock/callset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestCallSetAdd(t *testing.T) {

numCalls := 10
for i := 0; i < numCalls; i++ {
cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func)))
cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func), nil))
}

call, err := cs.FindMatch(receiver, method, []any{})
Expand All @@ -47,13 +47,13 @@ func TestCallSetAdd_WhenOverridable_ClearsPreviousExpectedAndExhausted(t *testin
var receiver any = "TestReceiver"
cs := newOverridableCallSet()

cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func)))
cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func), nil))
numExpectedCalls := len(cs.expected[callSetKey{receiver, method}])
if numExpectedCalls != 1 {
t.Fatalf("Expected 1 expected call in callset, got %d", numExpectedCalls)
}

cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func)))
cs.Add(newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func), nil))
newNumExpectedCalls := len(cs.expected[callSetKey{receiver, method}])
if newNumExpectedCalls != 1 {
t.Fatalf("Expected 1 expected call in callset, got %d", newNumExpectedCalls)
Expand Down Expand Up @@ -100,7 +100,7 @@ func TestCallSetFindMatch(t *testing.T) {
method := "TestMethod"
args := []any{}

c1 := newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func))
c1 := newCall(t, receiver, method, reflect.TypeOf(receiverType{}.Func), nil)
cs.exhausted = map[callSetKey][]*Call{
{receiver: receiver, fname: method}: {c1},
}
Expand Down
19 changes: 18 additions & 1 deletion gomock/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"reflect"
"runtime"
"sync"

"github.com/google/go-cmp/cmp"
)

// A TestReporter is something that can be used to report test failures. It
Expand Down Expand Up @@ -76,6 +78,7 @@ type Controller struct {
mu sync.Mutex
expectedCalls *callSet
finished bool
cmpOpts cmp.Options
}

// NewController returns a new Controller. It is the preferred way to create a Controller.
Expand Down Expand Up @@ -121,6 +124,20 @@ func (o overridableExpectationsOption) apply(ctrl *Controller) {
ctrl.expectedCalls = newOverridableCallSet()
}

type cmpOptions struct {
opts []cmp.Option
}

func (o cmpOptions) apply(ctrl *Controller) {
ctrl.cmpOpts = o.opts
}

// WithCmpOpts is a ControllerOption that configures the options to pass to
// cmp.Diff.
func WithCmpOpts(opts ...cmp.Option) cmpOptions {
return cmpOptions{opts: opts}
}

type cancelReporter struct {
t TestHelper
cancel func()
Expand Down Expand Up @@ -181,7 +198,7 @@ func (ctrl *Controller) RecordCall(receiver any, method string, args ...any) *Ca
func (ctrl *Controller) RecordCallWithMethodType(receiver any, method string, methodType reflect.Type, args ...any) *Call {
ctrl.T.Helper()

call := newCall(ctrl.T, receiver, method, methodType, args...)
call := newCall(ctrl.T, receiver, method, methodType, ctrl.cmpOpts, args...)

ctrl.mu.Lock()
defer ctrl.mu.Unlock()
Expand Down
26 changes: 17 additions & 9 deletions gomock/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"strings"
"testing"

"github.com/google/go-cmp/cmp/cmpopts"

"go.uber.org/mock/gomock"
)

Expand Down Expand Up @@ -74,8 +76,11 @@ func (e *ErrorReporter) assertFatal(fn func(), expectedErrMsgs ...string) {
// check the last actualErrMsg, because the previous messages come from previous errors
actualErrMsg := e.log[len(e.log)-1]
for _, expectedErrMsg := range expectedErrMsgs {
if !strings.Contains(actualErrMsg, expectedErrMsg) {
i := strings.Index(actualErrMsg, expectedErrMsg)
if i == -1 {
e.t.Errorf("Error message:\ngot: %q\nwant to contain: %q\n", actualErrMsg, expectedErrMsg)
} else {
actualErrMsg = actualErrMsg[i+len(expectedErrMsg):]
}
}
}
Expand Down Expand Up @@ -149,8 +154,9 @@ func (s *Subject) VariadicMethod(arg int, vararg ...string) {}

// A type purely for ActOnTestStructMethod
type TestStruct struct {
Number int
Message string
Number int
Message string
secretMessage string
}

func (s *Subject) ActOnTestStructMethod(arg TestStruct, arg1 int) int {
Expand All @@ -171,7 +177,9 @@ func createFixtures(t *testing.T) (reporter *ErrorReporter, ctrl *gomock.Control
// Controller. We use it to test that the mock considered tests
// successful or failed.
reporter = NewErrorReporter(t)
ctrl = gomock.NewController(reporter)
ctrl = gomock.NewController(
reporter, gomock.WithCmpOpts(cmpopts.IgnoreUnexported(TestStruct{})),
)
return
}

Expand Down Expand Up @@ -298,13 +306,13 @@ func TestUnexpectedArgValue_FirstArg(t *testing.T) {
// the method argument (of TestStruct type) has 1 unexpected value (for the Message field)
ctrl.Call(subject, "ActOnTestStructMethod", TestStruct{Number: 123, Message: "no message"}, 15)
}, "Unexpected call to", "doesn't match the argument at index 0",
"Got: {123 no message} (gomock_test.TestStruct)\nWant: is equal to {123 hello %s} (gomock_test.TestStruct)")
"Diff (-want +got):", "gomock_test.TestStruct{", "Number: 123", "-", "Message: \"hello %s\",", "+", "Message: \"no message\",", "}")

reporter.assertFatal(func() {
// the method argument (of TestStruct type) has 2 unexpected values (for both fields)
ctrl.Call(subject, "ActOnTestStructMethod", TestStruct{Number: 11, Message: "no message"}, 15)
}, "Unexpected call to", "doesn't match the argument at index 0",
"Got: {11 no message} (gomock_test.TestStruct)\nWant: is equal to {123 hello %s} (gomock_test.TestStruct)")
"Diff (-want +got):", "gomock_test.TestStruct{", "-", "Number: 123,", "+", "Number: 11,", "-", "Message: \"hello %s\",", "+", "Message: \"no message\",", "}")

reporter.assertFatal(func() {
// The expected call wasn't made.
Expand All @@ -323,7 +331,7 @@ func TestUnexpectedArgValue_SecondArg(t *testing.T) {
reporter.assertFatal(func() {
ctrl.Call(subject, "ActOnTestStructMethod", TestStruct{Number: 123, Message: "hello"}, 3)
}, "Unexpected call to", "doesn't match the argument at index 1",
"Got: 3 (int)\nWant: is equal to 15 (int)")
"Diff (-want +got):", "int(", "-", "15,", "+", "3,", ")")

reporter.assertFatal(func() {
// The expected call wasn't made.
Expand Down Expand Up @@ -742,8 +750,8 @@ func TestVariadicNoMatch(t *testing.T) {
ctrl.RecordCall(s, "VariadicMethod", 0)
rep.assertFatal(func() {
ctrl.Call(s, "VariadicMethod", 1)
}, "expected call at", "doesn't match the argument at index 0",
"Got: 1 (int)\nWant: is equal to 0 (int)")
}, "expected call at", "doesn't match the argument at index 0:",
"Diff (-want +got):", "int(", "-", "0,", "+", "1,", ")")
ctrl.Call(s, "VariadicMethod", 0)
}

Expand Down
Loading