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

Feature/525 return findings for ContainElement #527

Merged
merged 2 commits into from Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/index.md
Expand Up @@ -886,6 +886,13 @@ succeeds if the capacity of `ACTUAL` is `INT`. `ACTUAL` must be of type `array`,
Ω(ACTUAL).Should(ContainElement(ELEMENT))
```

or

```go
Ω(ACTUAL).Should(ContainElement(ELEMENT, <Pointer>))
```


succeeds if `ACTUAL` contains an element that equals `ELEMENT`. `ACTUAL` must be an `array`, `slice`, or `map` -- anything else is an error. For `map`s `ContainElement` searches through the map's values (not keys!).

By default `ContainElement()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s elements and `ELEMENT`. You can change this, however, by passing `ContainElement` a `GomegaMatcher`. For example, to check that a slice of strings has an element that matches a substring:
Expand All @@ -894,6 +901,29 @@ By default `ContainElement()` uses the `Equal()` matcher under the hood to asser
Ω([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubstring("Bar")))
```

In addition, there are occasions when you need to grab (all) matching contained elements, for instance, to make several assertions against the matching contained elements. To do this, you can ask the `ContainElement` matcher for the matching contained elements by passing it a pointer to a variable of the appropriate type. If multiple matching contained elements are expected, then a pointer to either a slice or a map should be passed (but not a pointer to an array), otherwise a pointer to a scalar (non-slice, non-map):

```go
var findings []string
Ω([]string{"foo", "foobar", "bar"}).Should(ContainElement(ContainSubstring("foo"), &findings))

var finding string
Ω([]string{"foo", "foobar", "bar"}).Should(ContainElement("foobar", &finding))
```

The `ContainElement` matcher will fail with a descriptive error message in case of multiple matches when the pointer references a scalar type.

In case of maps, the matching contained elements will be returned with their keys in the map referenced by the pointer.

```go
var findings map[int]string
Ω(map[int]string{
1: "bar",
2: "foobar",
3: "foo",
}).Should(ContainElement(ContainSubstring("foo"), &findings))
```

#### ContainElements(element ...interface{})

```go
Expand Down
22 changes: 16 additions & 6 deletions matchers.go
Expand Up @@ -256,16 +256,26 @@ func BeZero() types.GomegaMatcher {
return &matchers.BeZeroMatcher{}
}

//ContainElement succeeds if actual contains the passed in element.
//By default ContainElement() uses Equal() to perform the match, however a
//matcher can be passed in instead:
//ContainElement succeeds if actual contains the passed in element. By default
//ContainElement() uses Equal() to perform the match, however a matcher can be
//passed in instead:
// Expect([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubstring("Bar")))
//
//Actual must be an array, slice or map.
//For maps, ContainElement searches through the map's values.
func ContainElement(element interface{}) types.GomegaMatcher {
//Actual must be an array, slice or map. For maps, ContainElement searches
//through the map's values.
//
//If you want to have a copy of the matching element(s) found you can pass a
//pointer to a variable of the appropriate type. If the variable isn't a slice
//or map, then exactly one match will be expected and returned. If the variable
//is a slice or map, then at least one match is expected and all matches will be
//stored in the variable.
//
// var findings []string
// Expect([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubString("Bar", &findings)))
func ContainElement(element interface{}, result ...interface{}) types.GomegaMatcher {
return &matchers.ContainElementMatcher{
Element: element,
Result: result,
}
}

Expand Down
120 changes: 117 additions & 3 deletions matchers/contain_element_matcher.go
Expand Up @@ -3,6 +3,7 @@
package matchers

import (
"errors"
"fmt"
"reflect"

Expand All @@ -11,44 +12,157 @@ import (

type ContainElementMatcher struct {
Element interface{}
Result []interface{}
}

func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, err error) {
if !isArrayOrSlice(actual) && !isMap(actual) {
return false, fmt.Errorf("ContainElement matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1))
}

var actualT reflect.Type
var result reflect.Value
switch l := len(matcher.Result); {
case l > 1:
return false, errors.New("ContainElement matcher expects at most a single optional pointer to store its findings at")
case l == 1:
if reflect.ValueOf(matcher.Result[0]).Kind() != reflect.Ptr {
return false, fmt.Errorf("ContainElement matcher expects a non-nil pointer to store its findings at. Got\n%s",
format.Object(matcher.Result[0], 1))
}
actualT = reflect.TypeOf(actual)
resultReference := matcher.Result[0]
result = reflect.ValueOf(resultReference).Elem() // what ResultReference points to, to stash away our findings
switch result.Kind() {
case reflect.Array:
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
reflect.SliceOf(actualT.Elem()).String(), result.Type().String())
case reflect.Slice:
if !isArrayOrSlice(actual) {
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
reflect.MapOf(actualT.Key(), actualT.Elem()).String(), result.Type().String())
}
if !actualT.Elem().AssignableTo(result.Type().Elem()) {
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
actualT.String(), result.Type().String())
}
case reflect.Map:
if !isMap(actual) {
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
actualT.String(), result.Type().String())
}
if !actualT.AssignableTo(result.Type()) {
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
actualT.String(), result.Type().String())
}
default:
if !actualT.Elem().AssignableTo(result.Type()) {
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
actualT.Elem().String(), result.Type().String())
}
}
}

elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher)
if !elementIsMatcher {
elemMatcher = &EqualMatcher{Expected: matcher.Element}
}

value := reflect.ValueOf(actual)
var valueAt func(int) interface{}

var getFindings func() reflect.Value
var foundAt func(int)

if isMap(actual) {
keys := value.MapKeys()
valueAt = func(i int) interface{} {
return value.MapIndex(keys[i]).Interface()
}
if result.Kind() != reflect.Invalid {
fm := reflect.MakeMap(actualT)
getFindings = func() reflect.Value {
return fm
}
foundAt = func(i int) {
fm.SetMapIndex(keys[i], value.MapIndex(keys[i]))
}
}
} else {
valueAt = func(i int) interface{} {
return value.Index(i).Interface()
}
if result.Kind() != reflect.Invalid {
var f reflect.Value
if result.Kind() == reflect.Slice {
f = reflect.MakeSlice(result.Type(), 0, 0)
} else {
f = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0)
}
getFindings = func() reflect.Value {
return f
}
foundAt = func(i int) {
f = reflect.Append(f, value.Index(i))
}
}
}

var lastError error
for i := 0; i < value.Len(); i++ {
success, err := elemMatcher.Match(valueAt(i))
elem := valueAt(i)
success, err := elemMatcher.Match(elem)
if err != nil {
lastError = err
continue
}
if success {
return true, nil
if result.Kind() == reflect.Invalid {
return true, nil
}
foundAt(i)
}
}

return false, lastError
// when the expectation isn't interested in the findings except for success
// or non-success, then we're done here and return the last matcher error
// seen, if any, as well as non-success.
if result.Kind() == reflect.Invalid {
return false, lastError
}

// pick up any findings the test is interested in as it specified a non-nil
// result reference. However, the expection always is that there are at
// least one or multiple findings. So, if a result is expected, but we had
// no findings, then this is an error.
findings := getFindings()
if findings.Len() == 0 {
return false, lastError
}

// there's just a single finding and the result is neither a slice nor a map
// (so it's a scalar): pick the one and only finding and return it in the
// place the reference points to.
if findings.Len() == 1 && !isArrayOrSlice(result.Interface()) && !isMap(result.Interface()) {
if isMap(actual) {
miter := findings.MapRange()
miter.Next()
result.Set(miter.Value())
} else {
result.Set(findings.Index(0))
}
return true, nil
}

// at least one or even multiple findings and a the result references a
// slice or a map, so all we need to do is to store our findings where the
// reference points to.
if !findings.Type().AssignableTo(result.Type()) {
return false, fmt.Errorf("ContainElement cannot return multiple findings. Need *%s, got *%s",
findings.Type().String(), result.Type().String())
}
result.Set(findings)
return true, nil
}

func (matcher *ContainElementMatcher) FailureMessage(actual interface{}) (message string) {
Expand Down