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

Add EqualsWithNaN to testutil #3

Merged
merged 4 commits into from
Dec 6, 2022
Merged

Conversation

saswatamcode
Copy link
Collaborator

This PR adds an EqualsWithNaN function to testutil which allows comparing interfaces with NaN values (nested or otherwise) by considering NaN == NaN. This is useful for comparing values containing NaN in tests like promql.Result.
It uses go-cmp to achieve this.

reflect.DeepEqual considers NaN == NaN for some very specific cases which aren't easy to navigate, so an alternative implementation is necessary here. 🙂

The limitation of EqualsWithNaN is that one would need to pass in special options for unexported struct fields.

@douglascamata
Copy link

reflect.DeepEqual considers NaN == NaN for some very specific cases which aren't easy to navigate, so an alternative implementation is necessary here. 🙂

I think you mean reflect.DeepEqual consider NaN != NaN, right?

@saswatamcode
Copy link
Collaborator Author

saswatamcode commented Oct 26, 2022

I think you mean reflect.DeepEqual consider NaN != NaN, right?

Generally relfect.DeepEqual considers NaN != NaN, but in some specific cases, it might consider NaN == NaN according to an old & long issue thread: golang/go#12025.

But yes, generally it considers NaN != NaN. Adding this method to overcome that. Thanks! 🙂

Copy link
Contributor

@bwplotka bwplotka 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 this, but I would love to be quite careful here.. let's make sure API is right.

go.mod Show resolved Hide resolved
merrors/doc.go Outdated
// return errs.Err()
// }
//
// func CloseAll(closers []io.Closer) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

You wanted to change that?

Copy link
Collaborator Author

@saswatamcode saswatamcode Oct 31, 2022

Choose a reason for hiding this comment

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

So make lint fails on CI and points to merrors/doc.go being modified. But it passes locally. I then tried to format it via IDE, which changed it this way. Pushed to separate commit to see if this fixes it. I think it is gofmt-ing comments?

The same lint fail is on latest commit to main.

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔

// EqualsWithNaN fails the test if exp is not equal to act and ensures NaN == NaN.
// Keep in mind that this implementation cannot check unexported fields in structs on its own
// and needs setting custom options for it.
func EqualsWithNaN(tb testing.TB, exp, act interface{}, opts cmp.Options, v ...interface{}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this functionality is a bit too narrow.

I wished we could do Equals(<things>, ...option). Perhaps we could do WithCmpOpts(opts).Equal ? and there define which comparison to using (reflect or go-cmp) and perhaps go cmp opts?

Alternatively EqualsWithOpts - but I think I like that former better. Or even With(opts).Equal is fine too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ack! Will try to modify

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So I updated this to now be testutil.WithCmpOpts(opts).Equals for go-cmp comparison, and tesutil.Equals for reflect.DeepEqual one. I wanted to preserve testutil.Equals as it's super clean and used everywhere right now, whereas the go-cmp one would only be needed in a few specific cases. WDYT? 🙂

If this looks good, I might add some comment explaining this then.

Copy link
Contributor

@bwplotka bwplotka left a comment

Choose a reason for hiding this comment

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

Nice 💪🏽 Some further thoughts.

merrors/doc.go Outdated
// return errs.Err()
// }
//
// func CloseAll(closers []io.Closer) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔

opts cmp.Options
}

func WithCmpOpts(opts ...cmp.Option) goCmp {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, I think I like this direction, but the details matter.

It's weird that WithCmpOpts feels like those are comparison opts, buts actually those are opts ONLY for specific go-cmp thing. Does it matter if reflect.DeepEqual is used or default opts for go-cmp? - essentially can normal func Equals()... use return WithCmpOpts().Equals..? If not, then perhaps we need something like:

Suggested change
func WithCmpOpts(opts ...cmp.Option) goCmp {
func WithGoCmp(opts ...cmp.Option) goCmp {

Also I wonder if it makes sense to do our struct with Options, so we are not blocked when they are changing things or when we want to add extra thing.

I think I would go with sth like WithCmpOpts(opts ... CmpOption) comparer

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So, I agree, that it'd be much cleaner if Equals could switch go-cmp and reflect.DeepEqual based on opts, but go-cmp with default opts and reflect.DeepEqual have some subtle difference (for eg. private struct member comparisons).

I think WithGoCmp might be the cleanest way to do this without breaking testutil.Equals API.

Signed-off-by: Saswata Mukherjee <[email protected]>
Signed-off-by: Saswata Mukherjee <[email protected]>
Signed-off-by: Saswata Mukherjee <[email protected]>
Copy link
Contributor

@bwplotka bwplotka left a comment

Choose a reason for hiding this comment

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

LGTM, just one thing

opts cmp.Options
}

func WithGoCmp(opts ...cmp.Option) goCmp {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's also mention in the comment that compatibility guarantee for this function arguments is same as go-cmp, so... zero as it is at version 0.x. It's fine, but let's just mention it in comment

Signed-off-by: Saswata Mukherjee <[email protected]>
Copy link
Contributor

@bwplotka bwplotka left a comment

Choose a reason for hiding this comment

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

Thanks!

@bwplotka bwplotka merged commit a9e1cee into efficientgo:main Dec 6, 2022
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.

4 participants