2018-01-29 19:44:18 +00:00
|
|
|
package gstruct
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
"runtime/debug"
|
|
|
|
|
|
|
|
"github.com/onsi/gomega/format"
|
|
|
|
errorsutil "github.com/onsi/gomega/gstruct/errors"
|
|
|
|
"github.com/onsi/gomega/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
//MatchAllElements succeeds if every element of a slice matches the element matcher it maps to
|
|
|
|
//through the id function, and every element matcher is matched.
|
2018-09-05 15:36:14 +00:00
|
|
|
// idFn := func(element interface{}) string {
|
|
|
|
// return fmt.Sprintf("%v", element)
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// Expect([]string{"a", "b"}).To(MatchAllElements(idFn, Elements{
|
|
|
|
// "a": Equal("a"),
|
|
|
|
// "b": Equal("b"),
|
|
|
|
// }))
|
2018-01-29 19:44:18 +00:00
|
|
|
func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher {
|
|
|
|
return &ElementsMatcher{
|
|
|
|
Identifier: identifier,
|
|
|
|
Elements: elements,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//MatchElements succeeds if each element of a slice matches the element matcher it maps to
|
|
|
|
//through the id function. It can ignore extra elements and/or missing elements.
|
2018-09-05 15:36:14 +00:00
|
|
|
// idFn := func(element interface{}) string {
|
|
|
|
// return fmt.Sprintf("%v", element)
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// Expect([]string{"a", "b", "c"}).To(MatchElements(idFn, IgnoreExtras, Elements{
|
|
|
|
// "a": Equal("a"),
|
|
|
|
// "b": Equal("b"),
|
|
|
|
// }))
|
|
|
|
// Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing, Elements{
|
|
|
|
// "a": Equal("a"),
|
|
|
|
// "b": Equal("b"),
|
|
|
|
// "c": Equal("c"),
|
|
|
|
// "d": Equal("d"),
|
|
|
|
// }))
|
2018-01-29 19:44:18 +00:00
|
|
|
func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher {
|
|
|
|
return &ElementsMatcher{
|
|
|
|
Identifier: identifier,
|
|
|
|
Elements: elements,
|
|
|
|
IgnoreExtras: options&IgnoreExtras != 0,
|
|
|
|
IgnoreMissing: options&IgnoreMissing != 0,
|
|
|
|
AllowDuplicates: options&AllowDuplicates != 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped
|
|
|
|
// by the Identifier function.
|
|
|
|
// TODO: Extend this to work with arrays & maps (map the key) as well.
|
|
|
|
type ElementsMatcher struct {
|
|
|
|
// Matchers for each element.
|
|
|
|
Elements Elements
|
|
|
|
// Function mapping an element to the string key identifying its matcher.
|
|
|
|
Identifier Identifier
|
|
|
|
|
|
|
|
// Whether to ignore extra elements or consider it an error.
|
|
|
|
IgnoreExtras bool
|
|
|
|
// Whether to ignore missing elements or consider it an error.
|
|
|
|
IgnoreMissing bool
|
|
|
|
// Whether to key duplicates when matching IDs.
|
|
|
|
AllowDuplicates bool
|
|
|
|
|
|
|
|
// State.
|
|
|
|
failures []error
|
|
|
|
}
|
|
|
|
|
|
|
|
// Element ID to matcher.
|
|
|
|
type Elements map[string]types.GomegaMatcher
|
|
|
|
|
|
|
|
// Function for identifying (mapping) elements.
|
|
|
|
type Identifier func(element interface{}) string
|
|
|
|
|
|
|
|
func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) {
|
|
|
|
if reflect.TypeOf(actual).Kind() != reflect.Slice {
|
|
|
|
return false, fmt.Errorf("%v is type %T, expected slice", actual, actual)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.failures = m.matchElements(actual)
|
|
|
|
if len(m.failures) > 0 {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) {
|
|
|
|
// Provide more useful error messages in the case of a panic.
|
|
|
|
defer func() {
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack()))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
val := reflect.ValueOf(actual)
|
|
|
|
elements := map[string]bool{}
|
|
|
|
for i := 0; i < val.Len(); i++ {
|
|
|
|
element := val.Index(i).Interface()
|
|
|
|
id := m.Identifier(element)
|
|
|
|
if elements[id] {
|
|
|
|
if !m.AllowDuplicates {
|
|
|
|
errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elements[id] = true
|
|
|
|
|
|
|
|
matcher, expected := m.Elements[id]
|
|
|
|
if !expected {
|
|
|
|
if !m.IgnoreExtras {
|
|
|
|
errs = append(errs, fmt.Errorf("unexpected element %s", id))
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
match, err := matcher.Match(element)
|
|
|
|
if match {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if err == nil {
|
|
|
|
if nesting, ok := matcher.(errorsutil.NestingMatcher); ok {
|
|
|
|
err = errorsutil.AggregateError(nesting.Failures())
|
|
|
|
} else {
|
|
|
|
err = errors.New(matcher.FailureMessage(element))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err))
|
|
|
|
}
|
|
|
|
|
|
|
|
for id := range m.Elements {
|
|
|
|
if !elements[id] && !m.IgnoreMissing {
|
|
|
|
errs = append(errs, fmt.Errorf("missing expected element %s", id))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return errs
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) {
|
|
|
|
failure := errorsutil.AggregateError(m.failures)
|
|
|
|
return format.Message(actual, fmt.Sprintf("to match elements: %v", failure))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
|
|
|
|
return format.Message(actual, "not to match elements")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *ElementsMatcher) Failures() []error {
|
|
|
|
return m.failures
|
|
|
|
}
|