diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 40cb458f6..ed941faec 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -92,7 +92,8 @@ func EqualErrorf(t TestingT, theError error, errString string, msg string, args // EqualExportedValuesf asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index 86497116a..1d151bd5e 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -157,7 +157,8 @@ func (a *Assertions) EqualErrorf(theError error, errString string, msg string, a // EqualExportedValues asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int @@ -174,7 +175,8 @@ func (a *Assertions) EqualExportedValues(expected interface{}, actual interface{ // EqualExportedValuesf asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int diff --git a/assert/assertions.go b/assert/assertions.go index b362b4a29..498b9e197 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -77,6 +77,8 @@ func ObjectsAreEqual(expected, actual interface{}) bool { // ObjectsExportedFieldsAreEqual determines if the exported (public) fields of two structs are considered equal. // If the two objects are not of the same type, or if either of them are not a struct, they are not considered equal. +// If the structs happen to be time.Time, then they're compared using time.Time.Equal. Recursive data structures are not +// supported. // // This function does no assertion of any kind. func ObjectsExportedFieldsAreEqual(expected, actual interface{}) bool { @@ -86,37 +88,67 @@ func ObjectsExportedFieldsAreEqual(expected, actual interface{}) bool { expectedType := reflect.TypeOf(expected) actualType := reflect.TypeOf(actual) - if expectedType != actualType { return false } - if expectedType.Kind() != reflect.Struct || actualType.Kind() != reflect.Struct { return false } + if expectedTime, ok := expected.(time.Time); ok { + actualTime := actual.(time.Time) + return expectedTime.Equal(actualTime) + } + expectedValue := reflect.ValueOf(expected) actualValue := reflect.ValueOf(actual) for i := 0; i < expectedType.NumField(); i++ { field := expectedType.Field(i) - isExported := field.PkgPath == "" // should use field.IsExported() but it's not available in Go 1.16.5 - if isExported { - var equal bool - if field.Type.Kind() == reflect.Struct { - equal = ObjectsExportedFieldsAreEqual(expectedValue.Field(i).Interface(), actualValue.Field(i).Interface()) - } else { - equal = ObjectsAreEqualValues(expectedValue.Field(i).Interface(), actualValue.Field(i).Interface()) - } + isExported := field.PkgPath == "" // Use field.IsExported() instead if Go version is upgraded to 1.17+ + if !isExported { + continue + } - if !equal { - return false - } + exp := expectedValue.Field(i) + act := actualValue.Field(i) + + if !structFieldsAreEqual(field.Type.Kind(), exp, act) { + return false } } + return true } +// Recursive function used by ObjectsExportedFieldsAreEqual. Compares values depending on their kind, potentially +// dereferencing not-nil pointers and recursing into nested structs. +func structFieldsAreEqual(k reflect.Kind, exp, act reflect.Value) bool { + switch k { + case reflect.Struct: + return ObjectsExportedFieldsAreEqual(exp.Interface(), act.Interface()) + case reflect.Ptr: + if exp.IsNil() || act.IsNil() { + return exp.IsNil() && act.IsNil() + } + if exp.Elem().Kind() == reflect.Struct { + return ObjectsExportedFieldsAreEqual(exp.Elem().Interface(), act.Elem().Interface()) + } + return ObjectsAreEqualValues(exp.Interface(), exp.Interface()) + case reflect.Interface: + expected := exp.Interface() + actual := act.Interface() + if expected == nil || actual == nil { + return expected == actual + } + exp = reflect.ValueOf(expected) + act = reflect.ValueOf(actual) + return structFieldsAreEqual(exp.Kind(), exp, act) + default: + return ObjectsAreEqualValues(exp.Interface(), act.Interface()) + } +} + // ObjectsAreEqualValues gets whether two objects are equal, or if their // values are equal. func ObjectsAreEqualValues(expected, actual interface{}) bool { @@ -517,7 +549,8 @@ func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interfa // EqualExportedValues asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 5eaf2af8d..03d17b42a 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -166,6 +166,10 @@ func TestObjectsExportedFieldsAreEqual(t *testing.T) { foo interface{} } + s := Nested{Exported: "a"} + t1 := time.Date(2023, time.April, 7, 12, 56, 32, 0, time.UTC) + t2 := time.Date(2023, time.April, 7, 7, 56, 32, 0, time.FixedZone("UTC-5", -5*60*60)) + cases := []struct { expected interface{} actual interface{} @@ -176,10 +180,15 @@ func TestObjectsExportedFieldsAreEqual(t *testing.T) { {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, 3}, 4, Nested{5, "a"}}, true}, {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, 3}, 4, Nested{"a", "a"}}, true}, {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, "a"}, 4, Nested{5, 6}}, true}, + {S{1, Nested{2, S{Exported1: "a"}}, 4, Nested{5, 6}}, S{1, Nested{2, S{Exported1: "a"}}, 4, Nested{5, 6}}, true}, + {S{1, Nested{2, &s}, 4, Nested{5, s}}, S{1, Nested{2, &s}, 4, Nested{5, s}}, true}, + {S{t1, Nested{2, t1}, 4, Nested{&t1, 6}}, S{t2, Nested{2, t2}, 4, Nested{&t2, 6}}, true}, {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{"a", Nested{2, 3}, 4, Nested{5, 6}}, false}, {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{"a", 3}, 4, Nested{5, 6}}, false}, {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S2{1}, false}, {1, S{1, Nested{2, 3}, 4, Nested{5, 6}}, false}, + {S{Exported1: &t1}, S{Exported1: nil}, false}, + {S{Exported1: t1}, S{Exported1: t1.Add(time.Microsecond)}, false}, } for _, c := range cases { diff --git a/require/require.go b/require/require.go index a6e1b7c1d..8f525ee27 100644 --- a/require/require.go +++ b/require/require.go @@ -197,7 +197,8 @@ func EqualErrorf(t TestingT, theError error, errString string, msg string, args // EqualExportedValues asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int @@ -217,7 +218,8 @@ func EqualExportedValues(t TestingT, expected interface{}, actual interface{}, m // EqualExportedValuesf asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int diff --git a/require/require_forward.go b/require/require_forward.go index 05b1ec812..2b516b4ac 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -158,7 +158,8 @@ func (a *Assertions) EqualErrorf(theError error, errString string, msg string, a // EqualExportedValues asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int @@ -175,7 +176,8 @@ func (a *Assertions) EqualExportedValues(expected interface{}, actual interface{ // EqualExportedValuesf asserts that the types of two objects are equal and their public // fields are also equal. This is useful for comparing structs that have private fields -// that could potentially differ. +// that could potentially differ. Types of time.Time are compared using time.Time.Equal. +// Recursive data structures are not supported and may result in an infinite loop. // // type S struct { // Exported int