Skip to content

change require.protoEquals to support compare options#9937

Merged
fretz12 merged 10 commits intotemporalio:mainfrom
fretz12:fredtzeng/proto-equals-ignore
Apr 24, 2026
Merged

change require.protoEquals to support compare options#9937
fretz12 merged 10 commits intotemporalio:mainfrom
fretz12:fredtzeng/proto-equals-ignore

Conversation

@fretz12
Copy link
Copy Markdown
Contributor

@fretz12 fretz12 commented Apr 13, 2026

What changed?

Added ProtoEqualIgnoreFields to protoassert and protorequire packages. Refactored 3 call sites in standalone_activity_test.go to use the new helper.

Why?

Comparing proto messages while ignoring specific fields required a verbose pattern (cmp.Diff + protocmp.Transform() + protocmp.IgnoreFields() + require.Empty). The new option extends ProtoEqual with a clean call site that infers the message type automatically. The Option func type and config struct are designed to support additional comparison options in the future without changing the ProtoEqual signature.

How did you test it?

  • built
  • run locally and tested manually
  • covered by existing tests
  • added new unit test(s)
  • added new functional test(s)

Comment thread common/testing/protoassert/assert.go Outdated
Comment thread common/testing/protorequire/require.go Outdated

// ProtoEqualIgnoreFields compares two proto messages for equality, ignoring the specified fields on the given message
// type. Fields are specified by their proto name (snake_case).
func ProtoEqualIgnoreFields(t require.TestingT, a proto.Message, b proto.Message, msgType proto.Message, fields ...protoreflect.Name) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Food for thought.
There's some other interesting options we can potentially expose:

protocmp.SortRepeatedFields
protocmp.IgnoreEnums
cmpopts.EquateApproxTime
Also another approach is to make this a really generic function:
func ProtoEqualWithOptions(t assert.TestingT, a proto.Message, b proto.Message, opts ...cmp.Option) bool

and it can be used as such:
protorequire.ProtoEqualWithOptions(t, expected, actual, protocmp.IgnoreFields(&activitypb.ActivityExecutionInfo{}, "state_transition_count"), cmpopts.EquateApproxTime(5*time.Second), )

However, that leaks proto/cmp abstractions making the helper harder to use. I decided to make the helper focused on ignore fields, as that's our majority use case and the interface is very clean. I propose for other useful options, like EquateApproxTime, we create separate helpers. But open to thoughts

@fretz12 fretz12 marked this pull request as ready for review April 13, 2026 22:58
@fretz12 fretz12 requested review from a team as code owners April 13, 2026 22:58
@stephanos stephanos self-requested a review April 14, 2026 01:08
fretz12 added 3 commits April 22, 2026 14:17
Replace the standalone ProtoEqualIgnoreFields function with an options pattern on ProtoEqual.
The new IgnoreFields option infers the proto message type automatically, making call sites cleaner and the API extensible to future comparison options.
@fretz12 fretz12 requested a review from long-nt-tran April 23, 2026 16:42
}

// Option configures how proto comparison behaves.
type Option func(msg proto.Message, cfg *config)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

By not exporting config all of the future Option definitions have to be here, which removes the ability to have a one-off Option for a specific test. Thoughts on exporting?

Copy link
Copy Markdown
Contributor Author

@fretz12 fretz12 Apr 23, 2026

Choose a reason for hiding this comment

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

I could see that case. Ideally, I don't want to leak these configs. What if we do an exported config helper:

func WithCmpOptions(opts ...cmp.Option) Option {
	return func(_ proto.Message, cfg *config) {
		cfg.cmpOpts = append(cfg.cmpOpts, opts...)
	}
}

And then call it like:

protorequire.ProtoEqual(t, a, b,
      protorequire.WithCmpOptions(protocmp.SortRepeatedFields(msg, "items")),                                                                                            
  )   

wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 on leaking less than more, I think it's also ok to punt on how to expose one-off option to the future as well when there's a use case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agree with @long-nt-tran , we can defer to when we have a usecase

t.Fatal("expected comparison to fail when not all differing fields are ignored")
}
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like the negative test's expected failure is leaking as the suite failure. We might need a mock for require.testingT interface, i.e.,

type mockT struct {
    failed bool
}

func (m *mockT) Errorf(format string, args ...interface{}) {
    m.failed = true
}

func (m *mockT) FailNow() {
    m.failed = true
}

then this test can be

	t.Run("partial ignore still fails", func(t *testing.T) {
		mt := &mockT{}
		protorequire.ProtoEqual(mt, a, b,
			protorequire.IgnoreFields(
				"status",
			),
		)
		if !mt.failed {
			t.Fatal("expected comparison to fail when not all differing fields are ignored")
		}
	})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good call.. changed

Copy link
Copy Markdown
Contributor

@long-nt-tran long-nt-tran left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for adding this!

}

// Option configures how proto comparison behaves.
type Option func(msg proto.Message, cfg *config)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 on leaking less than more, I think it's also ok to punt on how to expose one-off option to the future as well when there's a use case

@fretz12 fretz12 changed the title add protorequire.ProtoEqualIgnoreFields helper change require.protoEquals to support compare options Apr 23, 2026
@fretz12 fretz12 merged commit 5e825f1 into temporalio:main Apr 24, 2026
47 checks passed
@fretz12 fretz12 deleted the fredtzeng/proto-equals-ignore branch April 24, 2026 19:19
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.

3 participants