From ddab2cfc9cfb8c99a8ca480bd47dba56a18acf66 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Tue, 23 Jun 2026 18:26:56 +0300 Subject: [PATCH] Fix EqualExportedValues on recursive values --- assert/assertions.go | 36 ++++++++++++++++++++++++++++++++---- assert/assertions_test.go | 20 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index 1419e4776..5d61af1ae 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -83,6 +83,16 @@ func ObjectsAreEqual(expected, actual interface{}) bool { // copyExportedFields iterates downward through nested data structures and creates a copy // that only contains the exported struct fields. func copyExportedFields(expected interface{}) interface{} { + return copyExportedFieldsWithVisited(expected, make(map[copyExportedFieldsVisit]reflect.Value)) +} + +type copyExportedFieldsVisit struct { + typ reflect.Type + ptr uintptr + length int +} + +func copyExportedFieldsWithVisited(expected interface{}, visited map[copyExportedFieldsVisit]reflect.Value) interface{} { if isNil(expected) { return expected } @@ -102,15 +112,21 @@ func copyExportedFields(expected interface{}) interface{} { if isNil(fieldValue) || isNil(fieldValue.Interface()) { continue } - newValue := copyExportedFields(fieldValue.Interface()) + newValue := copyExportedFieldsWithVisited(fieldValue.Interface(), visited) result.Field(i).Set(reflect.ValueOf(newValue)) } } return result.Interface() case reflect.Ptr: + visit := copyExportedFieldsVisit{typ: expectedType, ptr: expectedValue.Pointer()} + if result, ok := visited[visit]; ok { + return result.Interface() + } + result := reflect.New(expectedType.Elem()) - unexportedRemoved := copyExportedFields(expectedValue.Elem().Interface()) + visited[visit] = result + unexportedRemoved := copyExportedFieldsWithVisited(expectedValue.Elem().Interface(), visited) result.Elem().Set(reflect.ValueOf(unexportedRemoved)) return result.Interface() @@ -119,23 +135,35 @@ func copyExportedFields(expected interface{}) interface{} { if expectedKind == reflect.Array { result = reflect.New(reflect.ArrayOf(expectedValue.Len(), expectedType.Elem())).Elem() } else { + visit := copyExportedFieldsVisit{typ: expectedType, ptr: expectedValue.Pointer(), length: expectedValue.Len()} + if result, ok := visited[visit]; ok { + return result.Interface() + } + result = reflect.MakeSlice(expectedType, expectedValue.Len(), expectedValue.Len()) + visited[visit] = result } for i := 0; i < expectedValue.Len(); i++ { index := expectedValue.Index(i) if isNil(index) { continue } - unexportedRemoved := copyExportedFields(index.Interface()) + unexportedRemoved := copyExportedFieldsWithVisited(index.Interface(), visited) result.Index(i).Set(reflect.ValueOf(unexportedRemoved)) } return result.Interface() case reflect.Map: + visit := copyExportedFieldsVisit{typ: expectedType, ptr: expectedValue.Pointer()} + if result, ok := visited[visit]; ok { + return result.Interface() + } + result := reflect.MakeMap(expectedType) + visited[visit] = result for _, k := range expectedValue.MapKeys() { index := expectedValue.MapIndex(k) - unexportedRemoved := copyExportedFields(index.Interface()) + unexportedRemoved := copyExportedFieldsWithVisited(index.Interface(), visited) result.SetMapIndex(k, reflect.ValueOf(unexportedRemoved)) } return result.Interface() diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 11642e096..835a62bb2 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -372,6 +372,26 @@ func TestCopyExportedFields(t *testing.T) { } } +func TestEqualExportedValuesRecursiveStruct(t *testing.T) { + t.Parallel() + + type Node struct { + Value int + Self *Node + unexported string + } + + expected := &Node{Value: 1, unexported: "expected"} + expected.Self = expected + + actual := &Node{Value: 1, unexported: "actual"} + actual.Self = actual + + if !EqualExportedValues(t, expected, actual) { + t.Error("expected EqualExportedValues to handle recursive values") + } +} + func TestEqualExportedValues(t *testing.T) { t.Parallel()