Skip to content

mock: make Arguments.Is handle arity and uncomparable types - #1966

Open
feiiiiii5 wants to merge 1 commit into
stretchr:masterfrom
feiiiiii5:fix/arguments-is-arity
Open

feiiiiii5 wants to merge 1 commit into
stretchr:masterfrom
feiiiiii5:fix/arguments-is-arity

Conversation

@feiiiiii5

Copy link
Copy Markdown

Summary

mock.Arguments.Is panics when fewer objects than arguments are supplied, silently reports a match when more are supplied, and panics on arguments whose type is uncomparable. Get and Diff in the same file already handle all three cases.

Changes

  • Guard on len(objects) != len(args) and return false instead of indexing past the end or ignoring the extras.
  • Compare with assert.ObjectsAreEqual (already imported in this file) instead of != on interface{} values, so a slice, map, or func argument compares instead of panicking.
  • Document the arity behaviour in the doc comment.
  • Two tests in mock/mock_test.go, next to the existing Test_Arguments_Is.

Motivation

// Is gets whether the objects match the arguments specified.
func (args Arguments) Is(objects ...interface{}) bool {
	for i, obj := range args {
		if obj != objects[i] {
			return false
		}
	}
	return true
}

The loop is bounded by len(args) but indexes objects[i], and != on two interface{} values compares dynamic types. On master at 87a7b9d57689f6579db2da795ab8deeab29cb724:

args := mock.Arguments{"string", 123, true}

args.Is("string", 123)
// panic: runtime error: index out of range [1] with length 1

args.Is("string", 123, true, "extra")
// true

args := mock.Arguments{[]int{1, 2}}
args.Is([]int{1, 2})
// panic: runtime error: comparing uncomparable type []int

The middle one is a logic error rather than a style question: supplying more objects than arguments reports a match, for a call the expectation can never have produced.

The rest of the type already handles both halves of this:

How Get and Diff already do it

Get guards the index and reports the arity:

if index+1 > len(args) {
	panic(fmt.Sprintf("assert: arguments: Cannot call Get(%d) because there are %d argument(s).", index, len(args)))
}

Diff has a sentinel whose comment spells out that arity is a difference in both directions:

// 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.
// 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.
if actualMissing || expectedMissing {
	differences++

And Diff compares through assert.ObjectsAreEqual, which is reflect.DeepEqual-based and does not panic on an uncomparable type.

So "arity mismatch is a difference, not a crash" and "compare elements with ObjectsAreEqual" are already this package's contract — Is is the one comparison helper on the type doing neither.

Example usage

mock.Arguments{"a", 1}.Is("a", 1)            // true, unchanged
mock.Arguments{"a", 1}.Is("a", 1, "extra")   // was true, now false
mock.Arguments{"a", 1}.Is("a")               // was a panic, now false
mock.Arguments{[]int{1, 2}}.Is([]int{1, 2})  // was a panic, now true

This changes behaviour in those three cases. Is is not called anywhere inside the library outside its own test, so nothing internal moves; the effect is on external callers who relied on a panic, or on the extra-objects case that was silently wrong. I added the arity note to the doc comment so the new return value is documented rather than implied.

Test plan

Go 1.24.3, macOS. Two tests added to mock/mock_test.go, same package, same idioms. One covers both arity directions and asserts NotPanics; one covers a slice and a map argument and checks both the equal and unequal cases.

Command output

Before the fix, with only the tests added:

--- FAIL: Test_Arguments_IsUncomparableType (0.00s)
    Panic value: runtime error: comparing uncomparable type []int
--- FAIL: Test_Arguments_IsArityMismatch (0.00s)
    Panic value: runtime error: index out of range [2] with length 2
FAIL	github.com/stretchr/testify/mock	0.313s

After:

$ go test -race ./mock/
ok  	github.com/stretchr/testify/mock	1.389s

$ go test -race ./...
ok  	github.com/stretchr/testify/assert/internal/unsafetests	1.315s
ok  	github.com/stretchr/testify/internal/difflib	1.423s
ok  	github.com/stretchr/testify/internal/spew	1.371s
ok  	github.com/stretchr/testify/mock
ok  	github.com/stretchr/testify/require	1.466s
ok  	github.com/stretchr/testify/suite	6.185s

The gates from .github/workflows/main.yml, both empty:

$ gofmt -l .
$ go vet ./...

go generate ./... fails on this checkout with a panic inside _codegen/main.go at parsePackageSource. It fails identically on the unmodified master, so it is a pre-existing Go 1.24 incompatibility in that tool. The generated forwarders only cover exported assertion APIs and Is keeps its signature, so no regeneration is needed either way.

Related issues

None found. I searched open and closed issues and PRs for Arguments.Is, mock Is arity, Is index out of range, and mock Arguments uncomparable; #393 (diffArguments panics with index out of range) and #587 (mock does not enforce arity if parameter is Anything) are both about Diff/Anything and neither covers Is.

Arguments.Is ranged over args but indexed objects, and compared with
!= on interface values:

    for i, obj := range args {
        if obj != objects[i] {
            return false
        }
    }

Three things go wrong, all on master:

  - Fewer objects than arguments panics with "index out of range".
  - More objects than arguments is silently ignored, so
    Arguments{"a", 1}.Is("a", 1, "extra") reports a match for a call
    that could never have matched the expectation.
  - An argument whose dynamic type is uncomparable (a slice, a map, a
    func) panics with "comparing uncomparable type", so such arguments
    could not be compared at all.

Get and Diff in the same file already handle both halves of this.
Get checks the index and panics with a message that reports the
arity. Diff has a missingArgument sentinel whose comment spells out
"either because the call passed fewer arguments than the expectation
declares or because it passed more", and it counts that as a
difference. Diff also compares through assert.ObjectsAreEqual, which
is reflect.DeepEqual-based and never panics on an uncomparable type.

Is was the only comparison helper on the type doing neither. Guard the
length and compare with ObjectsAreEqual, which is already imported.

Signed-off-by: feiiiiii5 <feiiiiii5@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant