Skip to content

fix: Equals mutation-test harness for IR types; fix Listener and GatewayExtension equality#14248

Merged
andy-fong merged 8 commits into
kgateway-dev:mainfrom
puertomontt:advisor/002-equals-mutation-harness-v2
Jun 23, 2026
Merged

fix: Equals mutation-test harness for IR types; fix Listener and GatewayExtension equality#14248
andy-fong merged 8 commits into
kgateway-dev:mainfrom
puertomontt:advisor/002-equals-mutation-harness-v2

Conversation

@puertomontt

@puertomontt puertomontt commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Description

IR types consumed by KRT collections must implement Equals() covering all fields; a missed field can silently suppress updates and leave stale Envoy config. Today nothing enforces coverage when a field is added to an existing IR type.

This PR has two parts:

1. Equals mutation-test harness. Adds a generic harness (test/testutils/equalstest) that verifies every exported field of an IR type is covered by a mutation case proving Equals() detects the change. Any uncovered, non-exempt field fails the test, so adding a field without updating Equals becomes a build-time failure instead of a silent stale-config bug. The harness supports struct and pointer-to-struct type parameters via reflect.TypeFor, flattens embedded structs one level for coverage checks, and is applied to seven core IR types: PolicyAtt, AttachedPolicies, Gateway, Listener, ListenerSet, BackendRefIR, and GatewayExtension.

2. Equals fixes the harness and follow-up review drove out.

  • GatewayExtension.Equals now includes the embedded ObjectSource comparison.
  • Listener.Equals no longer uses whole-struct reflect.DeepEqual, which compared the Parent client.Object by content and made parent metadata churn (for example, resourceVersion bumps from status writes) dirty every listener. It now compares the embedded gwv1.Listener, AttachedPolicies, and PolicyAncestorRef explicitly, and compares Parent with versionEquals. That keeps status-only parent writes from triggering unnecessary re-translation while still detecting meaningful parent version changes if Listener.Equals is called directly in the future.

Change Type

/kind fix
/kind cleanup

Changelog

Fixed GatewayExtension equality to include the object source, and Listener equality to ignore parent object metadata churn (e.g. resourceVersion bumps from status writes), preventing missed updates and spurious recomputation.

Additional Notes

The harness completeness check is exercised through its real code path in equalstest_internal_test.go, so a regression in field-coverage detection also fails tests. The Listener parent comparison follows the versionEquals approach discussed in #14265.

Copilot AI review requested due to automatic review settings June 13, 2026 00:31
@gateway-bot gateway-bot added kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. kind/fix Categorizes issue or PR as related to a bug. release-note labels Jun 13, 2026
@puertomontt puertomontt added the work in progress Indicates that a PR should not merge because it is a work in progress label Jun 13, 2026

Copilot AI left a comment

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.

Pull request overview

This PR introduces a generic mutation-test harness to enforce that IR Equals() implementations detect changes to every exported field (preventing missed KRT updates/stale Envoy config), applies it to several core IR types, and fixes GatewayExtension.Equals to include ObjectSource in comparisons.

Changes:

  • Added test/testutils/equalstest harness to validate Equals() field coverage via mutation cases plus a reflection-based completeness check.
  • Added harness self-tests (black-box + white-box) to ensure the completeness logic works as intended.
  • Added IR-level harness tests for PolicyAtt, AttachedPolicies, Gateway, ListenerSet, BackendRefIR, and GatewayExtension, and fixed GatewayExtension.Equals to compare ObjectSource.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/testutils/equalstest/equalstest.go Adds the generic mutation/completeness test harness for Equals() implementations.
test/testutils/equalstest/equalstest_test.go Black-box self-tests verifying harness behavior (correct equals, exemptions, fixture sanity).
test/testutils/equalstest/equalstest_internal_test.go White-box tests for embedded-field flattening/completeness helper behavior.
pkg/pluginsdk/ir/gatewayextension.go Fixes GatewayExtension.Equals to include ObjectSource comparison.
pkg/pluginsdk/ir/equality_harness_test.go Applies the harness to core IR types to enforce Equals() coverage going forward.

Comment thread test/testutils/equalstest/equalstest.go Outdated
Adds a generic test harness (test/testutils/equalstest) that verifies
every exported field of an IR type is covered by a mutation case that
proves Equals() detects the change. Any uncovered, non-exempt field
fails the test, so adding a field without updating Equals becomes a
build-time failure instead of a silent stale-config bug.

Applies the harness to six core IR types: PolicyAtt, AttachedPolicies,
Gateway, ListenerSet, BackendRefIR, and GatewayExtension.

Also fixes GatewayExtension.Equals to include the embedded ObjectSource
comparison, which was previously missing (the harness-driven case
exposed this gap before the fix was applied).

Listener is intentionally excluded per plan 002 scope; plan 003 will
add Listener coverage once Listener.Equals is rewritten.

Signed-off-by: omar <[email protected]>
@puertomontt
puertomontt force-pushed the advisor/002-equals-mutation-harness-v2 branch from 2d9a241 to 1916ac6 Compare June 13, 2026 01:24
reflect.TypeOf(*new(T)) returns nil for interface type parameters
(nil-pointer panic on Kind()) and rejects pointer-to-struct types.
reflect.TypeFor[T]() resolves the static type directly; dereference
pointers one level so *Struct IR types can use the harness.

Signed-off-by: omar <[email protected]>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread test/testutils/equalstest/equalstest.go
Comment thread test/testutils/equalstest/equalstest.go
Comment thread pkg/pluginsdk/ir/equality_harness_test.go Outdated
Comment on lines +583 to +586
[]string{"Name", "Hostname", "Port", "Protocol", "TLS", "AllowedRoutes"}, // embedded gwv1.Listener fields covered by "Listener" case
// Suppress the gk field from the AttachedPolicies map closure; the
// "AttachedPolicies" case above exercises it at the struct level.
)
Typed informers leave TypeMeta empty, so a Gateway parent built with
TypeMeta set (e.g. in tests) would never compare equal to the same
parent from an informer, causing permanent spurious recomputation.
Normalize empty GVKs to wellknown.GatewayGVK before comparing.
ListenerSet is not normalized since the same Go type serves both the
standard and legacy XListenerSet GVKs.

Signed-off-by: omar <[email protected]>
The file header claimed Listener coverage was deferred, but this branch
rewrites Listener.Equals and adds TestHarnessListenerEquals.

Signed-off-by: omar <[email protected]>
@puertomontt puertomontt changed the title test: add Equals mutation-test harness for IR types fix: Equals mutation-test harness for IR types; fix Listener and GatewayExtension equality Jun 13, 2026
@puertomontt puertomontt removed the work in progress Indicates that a PR should not merge because it is a work in progress label Jun 14, 2026
Comment thread pkg/pluginsdk/ir/backend.go Outdated
func (c Listener) Equals(in Listener) bool {
return reflect.DeepEqual(c, in)
return reflect.DeepEqual(c.Listener, in.Listener) && // plain gwv1 API struct, no protos
parentRefEquals(c.Parent, in.Parent) &&

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.

I have a similar fix for this in https://github.com/kgateway-dev/kgateway/pull/14265/changes#diff-8613fbbe4c4f3ee612be6e867ac7fdd6464fb168ca0ffc40bdcdad28db7fd385R425 where I decided to repeat the versionEqual() that the parent already did just in case Listener is added to it's own KRT collection in the future and the Equals() is still correct. The parentRefEquals here will not detect value changes in the parent if this function is not called from the parent's Equals(). Currently, this function only get called from it's parent's Equals() though.

Replace the identity-based (GVK + namespace/name) parent comparison with the
versionEquals approach from PR kgateway-dev#14265 so status-only writes to the parent
Gateway/ListenerSet (which bump resourceVersion but not generation) do not
trigger spurious re-translations.

Removes the now-unused parentRefEquals/parentGVK helpers and updates the
equality harness tests to match the new semantics.

Signed-off-by: omar <[email protected]>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

baseHarnessGateway,
func(a, b Gateway) bool { return a.Equals(b) },
cases,
[]string{"Group", "Kind", "Namespace", "Name"}, // embedded ObjectSource fields covered by "ObjectSource" case
baseHarnessListenerSet,
func(a, b ListenerSet) bool { return a.Equals(b) },
cases,
[]string{"Group", "Kind", "Namespace", "Name"}, // embedded ObjectSource fields covered by "ObjectSource" case
Comment on lines +488 to +489
[]string{"Group", "Kind", "Namespace", "Name"}, // embedded ObjectSource fields
)
baseHarnessFullListener,
func(a, b Listener) bool { return a.Equals(b) },
cases,
[]string{"Name", "Hostname", "Port", "Protocol", "TLS", "AllowedRoutes"}, // embedded gwv1.Listener fields covered by "Listener" case
Comment on lines +425 to +426
// TestHarnessGatewayExtensionEquals drives the ObjectSource fix in Step 4.
// The "ObjectSource" mutation case will FAIL until Step 4 adds the comparison.
Comment on lines +588 to +589
// Suppress the gk field from the AttachedPolicies map closure; the
// "AttachedPolicies" case above exercises it at the struct level.
@puertomontt
puertomontt added this pull request to the merge queue Jun 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/cleanup Categorizes issue or PR as related to cleaning up code, process, or technical debt. kind/fix Categorizes issue or PR as related to a bug. release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gateway object status updates trigger reconcilation

4 participants