From 5e8a21e61529ac2c2846c543fefa1bed6d2fc72c Mon Sep 17 00:00:00 2001 From: Dylan Pulver Date: Thu, 20 Aug 2026 16:25:00 +0300 Subject: [PATCH] mock: do not let a surplus matcher match a missing argument Arguments.Diff used the plain string "(Missing)" as the sentinel for an argument that one side does not have. A surplus mock.Anything, mock.AnythingOfType("string"), mock.IsType(""), or the literal string "(Missing)" therefore matched an argument the caller never passed, so an expectation with more arguments than the method takes passed silently. mock.MatchedBy was handed the sentinel string as if it were a real argument, and mock.FunctionalOptions() panicked on it. The sentinel is now an unexported missingArgument type with no constructor, so no expectation can hold a value equal to it. Diff also reports a difference for a missing argument on either side before any matcher is consulted, since differing arity is never a match. Failure messages are unchanged: missing arguments still print as "(Missing)". --- mock/mock.go | 32 +++++++++++++++--- mock/mock_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/mock/mock.go b/mock/mock.go index 7f4d28d5e..746151e1b 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -952,6 +952,18 @@ func (args Arguments) Is(objects ...interface{}) bool { return true } +// missingArgument is the sentinel [Arguments.Diff] uses for an argument that +// only one side has, either because the call passed fewer arguments than the +// expectation declares or because it passed more. The type is unexported and +// has no constructor, so an expectation can never hold a value equal to it. +type missingArgument struct{} + +// String returns the text used for a missing argument in failure messages. +func (missingArgument) String() string { return "(Missing)" } + +// missing is the only value of type [missingArgument]. +var missing missingArgument + // Diff gets a string describing the differences between the arguments // and the specified objects. // @@ -972,21 +984,33 @@ func (args Arguments) Diff(objects []interface{}) (string, int) { var actualFmt, expectedFmt string if len(objects) <= i { - actual = "(Missing)" - actualFmt = "(Missing)" + actual = missing + actualFmt = missing.String() } else { actual = objects[i] actualFmt = fmt.Sprintf("(%[1]T=%[1]v)", actual) } if len(args) <= i { - expected = "(Missing)" - expectedFmt = "(Missing)" + expected = missing + expectedFmt = missing.String() } else { expected = args[i] expectedFmt = fmt.Sprintf("(%[1]T=%[1]v)", expected) } + // A missing argument on either side means the call and the expectation + // disagree on how many arguments there are, which no matcher can + // reconcile. Report the difference before any matcher is given the + // sentinel to look at. + _, actualMissing := actual.(missingArgument) + _, expectedMissing := expected.(missingArgument) + if actualMissing || expectedMissing { + differences++ + output = fmt.Sprintf("%s\t%d: FAIL: %s != %s\n", output, i, actualFmt, expectedFmt) + continue + } + if matcher, ok := expected.(argumentMatcher); ok { var matches bool func() { diff --git a/mock/mock_test.go b/mock/mock_test.go index 3dc9e0b1e..c30f526c4 100644 --- a/mock/mock_test.go +++ b/mock/mock_test.go @@ -1932,6 +1932,89 @@ func Test_Arguments_Diff_DifferentNumberOfArgs(t *testing.T) { } +func Test_Arguments_Diff_MissingArgument_WithAnythingArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", Anything}) + diff, count := args.Diff([]interface{}{"string"}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(Missing) != (string=mock.Anything)`) +} + +func Test_Arguments_Diff_MissingArgument_WithAnythingOfTypeArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", AnythingOfType("string")}) + diff, count := args.Diff([]interface{}{"string"}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(Missing) != (mock.anythingOfTypeArgument=string)`) +} + +func Test_Arguments_Diff_MissingArgument_WithIsTypeArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", IsType("")}) + _, count := args.Diff([]interface{}{"string"}) + + assert.Equal(t, 1, count) +} + +func Test_Arguments_Diff_MissingArgument_WithArgMatcher(t *testing.T) { + t.Parallel() + + var called bool + matchFn := func(s string) bool { + called = true + return true + } + var args = Arguments([]interface{}{"string", MatchedBy(matchFn)}) + diff, count := args.Diff([]interface{}{"string"}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(Missing) != (mock.argumentMatcher=func(string) bool)`) + assert.False(t, called, "the matcher must not be called for an argument that was never passed") +} + +func Test_Arguments_Diff_MissingArgument_WithFunctionalOptionsArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", FunctionalOptions()}) + _, count := args.Diff([]interface{}{"string"}) + + assert.Equal(t, 1, count) +} + +func Test_Arguments_Diff_MissingArgument_WithLiteralMissingString(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", "(Missing)"}) + diff, count := args.Diff([]interface{}{"string"}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(Missing) != (string=(Missing))`) +} + +func Test_Arguments_Diff_MissingExpectation_WithAnythingArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string"}) + diff, count := args.Diff([]interface{}{"string", Anything}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(string=mock.Anything) != (Missing)`) +} + +func Test_Arguments_Diff_MissingArgument_StillMatchesWhenPresent(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{Anything, AnythingOfType("string"), IsType(0), MatchedBy(func(b bool) bool { return b })}) + _, count := args.Diff([]interface{}{"anything", "string", 123, true}) + + assert.Equal(t, 0, count) +} + func Test_Arguments_Diff_WithAnythingArgument(t *testing.T) { t.Parallel()