structmatcher/structmatcher.go (128 lines of code) (raw):

package structmatcher /* * This file contains test structs and functions used in unit tests via dependency injection. */ import ( "fmt" "reflect" "strings" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" ) /* * If fields are to be filtered in or out, set shouldFilter to true; filterInclude is true to * include fields or false to exclude fields, and filterFields contains the field names to filter on. * To filter on a field "fieldname" in struct "structname", pass in "fieldname". * To filter on a field "fieldname" in a nested struct under field "structfield", pass in "structfield.fieldname". * This function assumes structs will only ever be nested one level deep. */ func StructMatcher(expected, actual interface{}, shouldFilter bool, filterInclude bool, filterFields ...string) []string { return structMatcher(reflect.ValueOf(expected), reflect.ValueOf(actual), "", shouldFilter, filterInclude, filterFields...) } func structMatcher(expected, actual reflect.Value, fieldPath string, shouldFilter bool, filterInclude bool, filterFields ...string) []string { // Add field names for the top-level struct to a filter map, and split off nested field names to pass down to nested structs filterMap := make(map[string]bool) nestedFilterFields := make([]string, 0) for i := 0; i < len(filterFields); i++ { fieldNames := strings.Split(filterFields[i], ".") if len(fieldNames) == 2 { nestedFilterFields = append(nestedFilterFields, fieldNames[1]) // If we include a nested struct field, we also need to include the nested struct if filterInclude { filterMap[fieldNames[0]] = true } } else { filterMap[filterFields[i]] = true } } expectedStruct := reflect.Indirect(expected) actualStruct := reflect.Indirect(actual) mismatches := []string{} mismatches = append(mismatches, InterceptGomegaFailures(func() { structCanInterface := true for i := 0; i < expectedStruct.NumField(); i++ { expectedField := reflect.Indirect(expectedStruct.Field(i)) actualField := reflect.Indirect(actualStruct.Field(i)) fieldName := actualStruct.Type().Field(i).Name // If we're including, skip this field if the name doesn't match; if we're excluding, skip if it does match if shouldFilter && ((filterInclude && !filterMap[fieldName]) || (!filterInclude && filterMap[fieldName])) { continue } actualFieldIsNonemptySlice := actualField.Kind() == reflect.Slice && !actualField.IsNil() && actualField.Len() > 0 expectedFieldIsNonemptySlice := expectedField.Kind() == reflect.Slice && !expectedField.IsNil() && expectedField.Len() > 0 fieldIsStructSlice := actualFieldIsNonemptySlice && expectedFieldIsNonemptySlice && actualField.Len() == expectedField.Len() && actualField.Index(0).Kind() == reflect.Struct expectedFieldIsNilPtr := expectedStruct.Field(i).Kind() == reflect.Ptr && expectedStruct.Field(i).IsNil() actualFieldIsNilPtr := actualStruct.Field(i).Kind() == reflect.Ptr && actualStruct.Field(i).IsNil() if fieldIsStructSlice { for j := 0; j < actualField.Len(); j++ { expectedStructField := expectedStruct.Field(i).Index(j) actualStructField := actualStruct.Field(i).Index(j) subFieldPath := fmt.Sprintf("%s%s[%d].", fieldPath, fieldName, j) mismatches = append(mismatches, structMatcher(expectedStructField, actualStructField, subFieldPath, shouldFilter, filterInclude, nestedFilterFields...)...) } } else if actualFieldIsNilPtr != expectedFieldIsNilPtr { expectedValue := expectedStruct.Field(i).Interface() actualValue := actualStruct.Field(i).Interface() Expect(actualValue).To(Equal(expectedValue), "Mismatch on field %s%s", fieldPath, fieldName) } else if expectedStruct.Field(i).CanInterface() { if actualField.Kind() == reflect.Struct { expectedStructField := expectedStruct.Field(i) actualStructField := actualStruct.Field(i) subFieldPath := fmt.Sprintf("%s%s.", fieldPath, fieldName) mismatches = append(mismatches, structMatcher(expectedStructField, actualStructField, subFieldPath, shouldFilter, filterInclude, nestedFilterFields...)...) } else { expectedValue := expectedStruct.Field(i).Interface() actualValue := actualStruct.Field(i).Interface() Expect(actualValue).To(Equal(expectedValue), "Mismatch on field %s%s", fieldPath, fieldName) } } else { structCanInterface = false } } if !structCanInterface { extra := []interface{}{ "Mismatch on unexported field within top level struct", } if fieldPath != "" { structName := fieldPath[0 : len(fieldPath)-1] // remove trailing dot. extra = []interface{}{ "Mismatch on unexported field within %s", structName, } } Expect(actualStruct.Interface()).To(Equal(expectedStruct.Interface()), extra...) } })...) return mismatches } // Deprecated: Use structmatcher.MatchStruct() GomegaMatcher func ExpectStructsToMatch(expected interface{}, actual interface{}) { Expect(actual).To(MatchStruct(expected)) } // Deprecated: Use structmatcher.MatchStruct().ExcludingFields() GomegaMatcher func ExpectStructsToMatchExcluding(expected interface{}, actual interface{}, excludeFields ...string) { Expect(actual).To(MatchStruct(expected).ExcludingFields(excludeFields...)) } // Deprecated: Use structmatcher.MatchStruct().IncludingFields() GomegaMatcher func ExpectStructsToMatchIncluding(expected interface{}, actual interface{}, includeFields ...string) { Expect(actual).To(MatchStruct(expected).IncludingFields(includeFields...)) } type Matcher struct { expected interface{} includingFields []string excludingFields []string mismatches []string } var _ types.GomegaMatcher = &Matcher{} func MatchStruct(expected interface{}) *Matcher { return &Matcher{ expected: expected, } } func (m *Matcher) Match(actual interface{}) (success bool, err error) { if m.includingFields != nil { m.mismatches = StructMatcher(m.expected, actual, true, true, m.includingFields...) } else if m.excludingFields != nil { m.mismatches = StructMatcher(m.expected, actual, true, false, m.excludingFields...) } else { m.mismatches = StructMatcher(m.expected, actual, false, false) } return len(m.mismatches) == 0, nil } func (m *Matcher) FailureMessage(actual interface{}) (message string) { return "Expected structs to match but:\n" + strings.Join(m.mismatches, "\n") } func (m *Matcher) NegatedFailureMessage(actual interface{}) (message string) { return "Expected structs not to match, but they did" } func (m *Matcher) IncludingFields(fields ...string) *Matcher { m.includingFields = fields return m } func (m *Matcher) ExcludingFields(fields ...string) *Matcher { m.excludingFields = fields return m }