diff options
-rw-r--r-- | cmp/cmpopts/struct_filter.go | 17 | ||||
-rw-r--r-- | cmp/cmpopts/util_test.go | 49 |
2 files changed, 57 insertions, 9 deletions
diff --git a/cmp/cmpopts/struct_filter.go b/cmp/cmpopts/struct_filter.go index 97f7079..dae7ced 100644 --- a/cmp/cmpopts/struct_filter.go +++ b/cmp/cmpopts/struct_filter.go @@ -160,14 +160,19 @@ func canonicalName(t reflect.Type, sel string) ([]string, error) { // Find the canonical name for this current field name. // If the field exists in an embedded struct, then it will be expanded. + sf, _ := t.FieldByName(name) if !isExported(name) { - // Disallow unexported fields: - // * To discourage people from actually touching unexported fields - // * FieldByName is buggy (https://golang.org/issue/4876) - return []string{name}, fmt.Errorf("name must be exported") + // Avoid using reflect.Type.FieldByName for unexported fields due to + // buggy behavior with regard to embeddeding and unexported fields. + // See https://golang.org/issue/4876 for details. + sf = reflect.StructField{} + for i := 0; i < t.NumField() && sf.Name == ""; i++ { + if t.Field(i).Name == name { + sf = t.Field(i) + } + } } - sf, ok := t.FieldByName(name) - if !ok { + if sf.Name == "" { return []string{name}, fmt.Errorf("does not exist") } var ss []string diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index d0fd888..37704c8 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -772,6 +772,39 @@ func TestOptions(t *testing.T) { wantEqual: false, reason: "not equal because highest-level field is not ignored: Foo3", }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{})}, + wantEqual: false, + reason: "not equal because unexported fields mismatch", + }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{ + cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{}), + IgnoreFields(ParentStruct{}, "PublicStruct.private", "privateStruct.private", "private"), + }, + wantEqual: true, + reason: "equal because mismatching unexported fields are ignored", + }, { label: "IgnoreTypes", x: []interface{}{5, "same"}, y: []interface{}{6, "same"}, @@ -1193,11 +1226,21 @@ func TestPanic(t *testing.T) { wantPanic: "must be a struct", reason: "the type must be a struct (not pointer to a struct)", }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "privateStruct"), + reason: "privateStruct field permitted since it is the default name of the embedded type", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "Public"), + reason: "Public field permitted since it is a forwarded field that is exported", + }, { label: "IgnoreFields", fnc: IgnoreFields, - args: args(Foo1{}, "unexported"), - wantPanic: "name must be exported", - reason: "unexported fields must not be specified", + args: args(struct{ privateStruct }{}, "private"), + wantPanic: "does not exist", + reason: "private field not permitted since it is a forwarded field that is unexported", }, { label: "IgnoreTypes", fnc: IgnoreTypes, |