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

EqualExportedValues: Support nested structs and time.Time fields #1373

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion assert/assertion_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions assert/assertion_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
61 changes: 47 additions & 14 deletions assert/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions assert/assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions require/require.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions require/require_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down