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

Support protobuf equality #758

Closed
evie404 opened this issue Apr 27, 2019 · 18 comments
Closed

Support protobuf equality #758

evie404 opened this issue Apr 27, 2019 · 18 comments

Comments

@evie404
Copy link

evie404 commented Apr 27, 2019

There's a subtle issue when using assertions with protobuf structs, because protobuf structs contain internal fields that should be ignored during assertions.

An XXX_sizecache field that is part of the internal implementation of the table-driven serializer. The presence of this field can break tests that use reflect.DeepEqual to compare some received messages with some golden message. It is recommended that proto.Equal be used instead for this situation.

I think it'd be nice for testify to support this. Particularly, we can add this line in ObjectsAreEqual to check if both objects are protobuf messages, and if so, proto.Equal as suggested.

	expProto, ok := expected.(proto.Message)
	if ok {
		actProto, ok := actual.(proto.Message)
		if ok {
			// if both are protobuf messages, use `proto.Equal`
			return proto.Equal(expProto, actProto)
		}
	}

The problem would be introducing a dependency on golang/protobuf, which I'm not sure would be acceptable.

If not, one way to address this is using build constraints. We can split the ObjectsAreEqual method into a file equal.go that is compiled by default. And with a build constraint, we can toggle whether to include the protobuf support:

equal.go:

// +build !protobuf

equal_protos.go:

// +build protobuf

This way, we can include support for protobuf without introducing the dependency for everyone.

@evie404
Copy link
Author

evie404 commented May 10, 2019

we've been using a fork in case interested parties want to try it out: vsco@d23661d

@vincentbernat
Copy link

Until #535 is implemented, this would be a nice workaround to have.

@glesica
Copy link
Collaborator

glesica commented Dec 23, 2019

It'd be great to see a PR to resolve #535, but barring that, I'm not sure we want to introduce a protobuf dependency. A build constraint might be OK, though. @boyan-soubachov thoughts?

@boyan-soubachov
Copy link
Collaborator

boyan-soubachov commented Dec 23, 2019

@glesica , I would agree that this is a bit too use-case specific and would want to avoid having protobuf-specific checks & code.

I am, however completely on-board with moving to go-cmp. I am afraid, however that it would be a big project and almost certainly necessitate a /v2

@glesica
Copy link
Collaborator

glesica commented Dec 23, 2019

OK cool, let's try to pull that into the next release then, even if it doesn't actually go out with the next release it will keep it from getting lost.

@dsnet
Copy link

dsnet commented Jan 21, 2020

(I'm the author of both cmp and one of the primary authors of Go protobufs)

Google heavily uses protobufs and I have been asked many times for cmp to provide native support for them. However, I've denied such requests because adding support for them would require a dependency on protobufs. Furthermore, it introduces a dangerous slippery slope: if protobufs can have special treatment, can other Go types have special treatment as well? What about Cap'n Proto or Thrift? Specialized support for any type sets precedence for adding special handling of other types resulting in cmp having a massive dependency tree. Presumably this same argument applies to the assert project.

BTW, for cmp to better support protobufs, there is a protocmp package that is specifically designed to support protobuf messages with cmp.

@boyan-soubachov
Copy link
Collaborator

(I'm the author of both cmp and one of the primary authors of Go protobufs)

Google heavily uses protobufs and I have been asked many times for cmp to provide native support for them. However, I've denied such requests because adding support for them would require a dependency on protobufs. Furthermore, it introduces a dangerous slippery slope: if protobufs can have special treatment, can other Go types have special treatment as well? What about Cap'n Proto or Thrift? Specialized support for any type sets precedence for adding special handling of other types resulting in cmp having a massive dependency tree. Presumably this same argument applies to the assert project.

BTW, for cmp to better support protobufs, there is a protocmp package that is specifically designed to support protobuf messages with cmp.

Fair point. I've had some more time to think about this one as well and it seems like makes more sense for testify to remain a simple/thin functionality package rather than aiming to cater to all use cases. I definitely agree with supporting what the community wants and if there is an overwhelming voice for adding protobufs, then we should do it.

Thank you for the input, @dsnet

@ocavue
Copy link

ocavue commented Jun 17, 2020

I'm using embedding type to "inherit" assert.Assertions with my extra method to support protobuf equality.

Define a new internal package called expect (to not conflict with the name assert) and write our custom assertion method:

package expect

import (
	"fmt"

	"github.com/stretchr/testify/assert"
	"google.golang.org/protobuf/proto"
)

// Expectation is a embedding type for assert.Assertions
type Expectation struct {
	*assert.Assertions
}

// New makes a new Expectation object for the specified TestingT.
func New(t assert.TestingT) *Expectation {
	assert := assert.New(t)
	return &Expectation{assert}
}

// ProtoEqual asserts that the specified protobuf messages are equal.
func (a *Expectation) ProtoEqual(expected, actual proto.Message) bool {
	return a.True(
		proto.Equal(expected, actual),
		fmt.Sprintf("These two protobuf messages are not equal:\nexpected: %v\nactual:  %v", expected, actual),
	)
}

Then in the test files, I can use both the origin assert functions from github.com/stretchr/testify/assert as well as my custom assert function.

func TestBaseWorkflow(t *testing.T) {
	expect := expect.New(t)
	expect.NotEqual(1, 2)  // 
	expect.ProtoEqual(&pb.Pod{}, &pb.Pod{})  // 
}

@bogdandrutu
Copy link
Contributor

bogdandrutu commented Aug 12, 2020

@boyan-soubachov I think having a small extensibility to Equal like what cmp-go does, would make a lot of sense:

func Diff(x, y interface{}, opts ...Option) string

type Option interface {
	// filter applies all filters and returns the option that remains.
	// Each option may only read s.curPath and call s.callTTBFunc.
	//
	// An Options is returned only if multiple comparers or transformers
	// can apply simultaneously and will only contain values of those types
	// or sub-Options containing values of those types.
	filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption
}

Maybe the interface will be different, similar to ObjectsAreEqual would make everyone happy:

  • Reduce dependency on custom libraries like proto
  • Allow users to write their own custom Equaler for things like proto.
assert.CustomEqual(t, want, got, MyCustomProtoEqualer)

@torkelrogstad
Copy link
Contributor

A solution to this would be to allow people to provide custom equality and serialization options.

Could look like this:

        // applied to assert.New constructor
	assert := assert.New(t, assert.WithEqualCheck(func(expected, actual interface{}) (result, ok bool) {
		expectedProto, ok := expected.(proto.Message)
		if !ok {
			return false, false
		}

		actualProto, ok := actual.(proto.Message)
		if !ok {
			return false, false
		}

		return proto.Equal(expectedProto, actualProto), true
	})
	
        // with package level option, if not using the assert.New constructor
	assert.AddEqualCheck(func(expected, actual interface{}) (result, ok bool) {
		expectedProto, ok := expected.(proto.Message)
		if !ok {
			return false, false
		}

		actualProto, ok := actual.(proto.Message)
		if !ok {
			return false, false
		}

		return proto.Equal(expectedProto, actualProto), true
	})

The first boolean result is the equality check, and the second is if it "should count", i.e. can we bail on comparisons or should we continue with other comparisons.

This would let people pull in their own dependencies, while still leveraging this great library.

@FarhanSajid1
Copy link

Any updates to this?

dahuang-purestorage added a commit to libopenstorage/openstorage that referenced this issue Oct 27, 2022
OSD-dev was failing for a few reason. First the test is expecting to use Go
1.16, so we updated the osd-dev image to use 1.16. Secondly, there is issue
with mock comparing structs generated by protobuf. The new protobuf version
has extra hidden fields that causes comparison to fail even if the values
are the same. Similar issues in stretchr/testify#758.

For the fix, we move all of the mock request using protobuf to use
mock.Any() as the parameters to bypass the comparison. This should be
discussed with other contributors if there are other possibilities.

Testing: local testing passes this issue
Signed-off-by: dahuang <[email protected]>
@Seklfreak
Copy link

I believe by now EqualExportedValues does the trick? https://pkg.go.dev/github.com/stretchr/testify/assert#EqualExportedValues

@stephenpmurray
Copy link

I believe by now EqualExportedValues does the trick? https://pkg.go.dev/github.com/stretchr/testify/assert#EqualExportedValues

Good point. Unfortunately, this doesn't work for pointers.

Perhaps the feature request now is to make this function handle pointers?

@redachl
Copy link

redachl commented Feb 22, 2024

I believe by now EqualExportedValues does the trick? https://pkg.go.dev/github.com/stretchr/testify/assert#EqualExportedValues

Good point. Unfortunately, this doesn't work for pointers.

Perhaps the feature request now is to make this function handle pointers?

I just opened a pull request for this. Thanks for the suggestion :).

@redachl
Copy link

redachl commented Feb 22, 2024

Sorry just closed the PR.
Actually the PR #1517 already allows EqualExportedValues to accept pointers, and it was approved 🎉

Let's wait for the PR to be merged

@brackendawson
Copy link
Collaborator

I've merged that change and you should expect it in testify v1.8.5. Does EqaulExpertedValues fully resolve this Issue or are there some comparisons that protobuf users still can't make?

@redachl
Copy link

redachl commented Apr 9, 2024

I've merged that change and you should expect it in testify v1.8.5. Does EqaulExpertedValues fully resolve this Issue or are there some comparisons that protobuf users still can't make?

Thanks for the change it's really useful!

Something unrelated to protobuf is still missing: Equal and EqualValues can be run on slices, but EqualExportedValues doesn't run on slices. Is there a specific reason for this?

Let me know if I should create another issue.

@brackendawson
Copy link
Collaborator

Yes, create a new issue.

I'm going to mark this as completed. Please let me know if protobuf equality is still an issue.

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 a pull request may close this issue.