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

fix: compare functional option names for indirect calls #1626

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

arjun-1
Copy link

@arjun-1 arjun-1 commented Jul 23, 2024

Summary

Closes Functional Options testing broken for indirect calls #1380. The fix is consists of not comparing the the function names, as it is already asserted when comparing expectedValues and actualValues

Changes

  • Adds test Test_Mock_AssertExpectationsFunctionalOptionsTypeIndirectly which would fail without the fix
  • Adds test Test_Mock_AssertExpectationsFunctionalOptionsType_Diff to examine behavior of slightly different functional options in expected vs actual.
  • Changes the funcName implementation to only compare the relevant last part, if applicable. This aids in reading the diff that is produced when the function names don't. match (for go1.20 or higher)

Motivation

The choice was made to omit the comparison of function names completely, instead of comparing only the relevant last part. The reason is that go1.19 and lower don't give enough any meaningful comparable part in the function name. Nevertheless the most relevant part of the function name is included in the diff result. Without it, the diff yielded in Test_Mock_AssertExpectationsFunctionalOptionsType_Diff would have resulted in (go1.20):

mock.Test_Mock_AssertExpectationsFunctionalOptionsType_Diff.OpNum &{num:0 str:1} != mock.Test_Mock_AssertExpectationsFunctionalOptionsType_Diff.OpNum &{num:1 str:}

The current implementation instead yields a diff:

OpStr &{num:0 str:1} != OpNum &{num:1 str:}

Though there could be added benefit in displaying the full location of the function, I can revert that part if desired.

Related issues

Closes #1380

@arjun-1
Copy link
Author

arjun-1 commented Jul 23, 2024

Looks like the approach to compare a relevant part only fails for older Go versions, I'll rethink that
Done

@arjun-1 arjun-1 marked this pull request as draft July 23, 2024 14:36
@arjun-1 arjun-1 marked this pull request as ready for review July 23, 2024 15:01
@arjun-1
Copy link
Author

arjun-1 commented Aug 5, 2024

Hey @dolmen I believe you were involved in reviewing related change #1381. Anything specific required for this review?

@pmokeev
Copy link

pmokeev commented Aug 30, 2024

@dolmen can you have a look at this PR? I've also encountered the same problem

@dolmen dolmen added pkg-mock Any issues related to Mock bug labels Oct 4, 2024
Copy link
Collaborator

@brackendawson brackendawson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR would regress this highly contrived test:

package kata_test

import (
	"testing"

	"github.com/stretchr/testify/mock"
)

type MyStruct struct {
	A []byte
}

type Opt func(*MyStruct)

func WithString(s string) Opt {
	return func(m *MyStruct) {
		m.A = []byte(s)
	}
}

func WithBytes(b []byte) Opt {
	return func(m *MyStruct) {
		m.A = b
	}
}

type myMockFactory struct {
	mock.Mock
}

func (m *myMockFactory) New(opts ...Opt) *MyStruct {
	return m.Called(opts).Get(0).(*MyStruct)
}

func TestIfy(t *testing.T) {
	mockFactory := &myMockFactory{}
	mockFactory.Test(t)
	mockFactory.On("New", mock.FunctionalOptions(WithBytes([]byte("this")))).Return(&MyStruct{})
	mockFactory.New(WithString("this"))
}

The existing function name checking doesn't sit well with me either because it stripped the package name off. Can we use reflect.Value.Pointer to check that it's the same function? The docs suggest that multiple functions can share a code pointer but I don't know in what cases that happens. If it's not practical lets drop to testing only the behaviour as you propose.

On the diff output, we should show what we compare, which in the current state of this PR is just the behaviour, so I'd like the diff to look somewhat like:

Diff: 0: FAIL:  &MyStruct{A:1 B:0} != &MyStruct{A:0 B:1} 

@brackendawson
Copy link
Collaborator

Considering the diff some more, the function name could make a mistake more obvious to the user. But I'd want some clearer separation between the function name and the bahaviour because it's quite hard to grok, how about:

Diff: 0: FAIL:  WithB(*MyMock) -> &MyMock{A:0 B:1} != WithA(*MyMock) -> &MyMock{A:1 B:0}

@brackendawson
Copy link
Collaborator

brackendawson commented Oct 7, 2024

The docstring for FunctionalOptions should also be changed to state that only the behaviour of the functional option argument is compared, and that functional options with identical outcomes compare as equal. Currently it says:

FunctionalOptions returns an [FunctionalOptionsArgument] object containing
the expected functional-options to check for.

For example:

  Assert(t, FunctionalOptions(foo.Opt1("strValue"), foo.Opt2(613)))

@arjun-1 arjun-1 force-pushed the fix-functional-options-diff-indirect-calls branch from 2b8f363 to 55bac84 Compare October 24, 2024 09:30
@arjun-1
Copy link
Author

arjun-1 commented Oct 27, 2024

@brackendawson I agree with you that the regression you mention is not ideal. Originally I removed the function name comparison (only focussing on behavior) to make the tests (fixing the bug of this issue) pass for older Go versions older than 1.20. The function name retrieved via FuncForPC yields false unexpected calls for these versions. For example

github.com/stretchr/testify/mock.Test_Mock_AssertExpectationsFunctionalOptionsType_Indirectly.func1
github.com/stretchr/testify/mock.TheExampleMethodFunctionalOptionsIndirect.func1

I investigated the solution of using Pointer() directly on the reflected value. The pointer value is again falsely different, as I believe it points to the inner closed anonymous function (func1).

I explored a different comparison using FileLine. It's imperfect as two different function values on the same line would not be detected as equal. But it is the only solution that prevents the regression you mention (see Test_Mock_AssertExpectationsFunctionalOptionsType_Diff_Func) and fixes the original issue (Test_Mock_AssertExpectationsFunctionalOptionsType_Indirectly) for all Go versions.

The result is now a comparison based on function file line, and if that is the same a comparison based on function behavior. Let me know if the docstring for FunctionalOptions should still be updated.

Copy link
Collaborator

@brackendawson brackendawson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pulling on all those threads. I'm happy to take a function matching strategy that falsely matches different functions with the same behaviour defined on the same line because the original match also wasn't complete, it considered the same function defined in different packages to be equal.

  • Your proposed change fixes the described bug, anonymous functions now match. They are actually quite a common code pattern to use.
  • Different functions with the same behaviour still fail (unless they are defined on the same line).
  • Different functions defined in different packages with the same name and the same behaviour now fail. This is faithful to our function's promise and is such a contrived example that I'm not concerned by the change to behaviour.

I'm confident that this is a safe enough way to resolve the issue. Thanks for all your work here. I'm also going to give other maintainers some time to review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug pkg-mock Any issues related to Mock
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Functional Options testing broken for indirect calls
4 participants