Skip to content

Commit

Permalink
feat add InAnyOrder matcher (golang#546)
Browse files Browse the repository at this point in the history
Adds a new matcher for comparing slices/arrays
elements without the need for the elements to be
in a particular order.
  • Loading branch information
vvkh authored May 17, 2021
1 parent e303461 commit 0cdccf5
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 0 deletions.
73 changes: 73 additions & 0 deletions gomock/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,70 @@ func (m lenMatcher) String() string {
return fmt.Sprintf("has length %d", m.i)
}

type inAnyOrderMatcher struct {
x interface{}
}

func (m inAnyOrderMatcher) Matches(x interface{}) bool {
given, ok := m.prepareValue(x)
if !ok {
return false
}
wanted, ok := m.prepareValue(m.x)
if !ok {
return false
}

if given.Len() != wanted.Len() {
return false
}

usedFromGiven := make([]bool, given.Len())
foundFromWanted := make([]bool, wanted.Len())
for i := 0; i < wanted.Len(); i++ {
wantedMatcher := Eq(wanted.Index(i).Interface())
for j := 0; j < given.Len(); j++ {
if usedFromGiven[j] {
continue
}
if wantedMatcher.Matches(given.Index(j).Interface()) {
foundFromWanted[i] = true
usedFromGiven[j] = true
break
}
}
}

missingFromWanted := 0
for _, found := range foundFromWanted {
if !found {
missingFromWanted++
}
}
extraInGiven := 0
for _, used := range usedFromGiven {
if !used {
extraInGiven++
}
}

return extraInGiven == 0 && missingFromWanted == 0
}

func (m inAnyOrderMatcher) prepareValue(x interface{}) (reflect.Value, bool) {
xValue := reflect.ValueOf(x)
switch xValue.Kind() {
case reflect.Slice, reflect.Array:
return xValue, true
default:
return reflect.Value{}, false
}
}

func (m inAnyOrderMatcher) String() string {
return fmt.Sprintf("has the same elements as %v", m.x)
}

// Constructors

// All returns a composite Matcher that returns true if and only all of the
Expand Down Expand Up @@ -266,3 +330,12 @@ func AssignableToTypeOf(x interface{}) Matcher {
}
return assignableToTypeOfMatcher{reflect.TypeOf(x)}
}

// InAnyOrder is a Matcher that returns true for collections of the same elements ignoring the order.
//
// Example usage:
// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 3, 2}) // returns true
// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 2}) // returns false
func InAnyOrder(x interface{}) Matcher {
return inAnyOrderMatcher{x}
}
149 changes: 149 additions & 0 deletions gomock/matchers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,152 @@ func TestAssignableToTypeOfMatcher(t *testing.T) {
t.Errorf(`AssignableToTypeOf(context.Context) should not match ctxWithValue`)
}
}

func TestInAnyOrder(t *testing.T) {
tests := []struct {
name string
wanted interface{}
given interface{}
wantMatch bool
}{
{
name: "match for equal slices",
wanted: []int{1, 2, 3},
given: []int{1, 2, 3},
wantMatch: true,
},
{
name: "match for slices with same elements of different order",
wanted: []int{1, 2, 3},
given: []int{1, 3, 2},
wantMatch: true,
},
{
name: "not match for slices with different elements",
wanted: []int{1, 2, 3},
given: []int{1, 2, 4},
wantMatch: false,
},
{
name: "not match for slices with missing elements",
wanted: []int{1, 2, 3},
given: []int{1, 2},
wantMatch: false,
},
{
name: "not match for slices with extra elements",
wanted: []int{1, 2, 3},
given: []int{1, 2, 3, 4},
wantMatch: false,
},
{
name: "match for empty slices",
wanted: []int{},
given: []int{},
wantMatch: true,
},
{
name: "not match for equal slices of different types",
wanted: []float64{1, 2, 3},
given: []int{1, 2, 3},
wantMatch: false,
},
{
name: "match for equal arrays",
wanted: [3]int{1, 2, 3},
given: [3]int{1, 2, 3},
wantMatch: true,
},
{
name: "match for equal arrays of different order",
wanted: [3]int{1, 2, 3},
given: [3]int{1, 3, 2},
wantMatch: true,
},
{
name: "not match for arrays of different elements",
wanted: [3]int{1, 2, 3},
given: [3]int{1, 2, 4},
wantMatch: false,
},
{
name: "not match for arrays with extra elements",
wanted: [3]int{1, 2, 3},
given: [4]int{1, 2, 3, 4},
wantMatch: false,
},
{
name: "not match for arrays with missing elements",
wanted: [3]int{1, 2, 3},
given: [2]int{1, 2},
wantMatch: false,
},
{
name: "not match for equal strings", // matcher shouldn't treat strings as collections
wanted: "123",
given: "123",
wantMatch: false,
},
{
name: "not match if x type is not iterable",
wanted: 123,
given: []int{123},
wantMatch: false,
},
{
name: "not match if in type is not iterable",
wanted: []int{123},
given: 123,
wantMatch: false,
},
{
name: "not match if both are not iterable",
wanted: 123,
given: 123,
wantMatch: false,
},
{
name: "match for equal slices with unhashable elements",
wanted: [][]int{{1}, {1, 2}, {1, 2, 3}},
given: [][]int{{1}, {1, 2}, {1, 2, 3}},
wantMatch: true,
},
{
name: "match for equal slices with unhashable elements of different order",
wanted: [][]int{{1}, {1, 2, 3}, {1, 2}},
given: [][]int{{1}, {1, 2}, {1, 2, 3}},
wantMatch: true,
},
{
name: "not match for different slices with unhashable elements",
wanted: [][]int{{1}, {1, 2, 3}, {1, 2}},
given: [][]int{{1}, {1, 2, 4}, {1, 3}},
wantMatch: false,
},
{
name: "not match for unhashable missing elements",
wanted: [][]int{{1}, {1, 2}, {1, 2, 3}},
given: [][]int{{1}, {1, 2}},
wantMatch: false,
},
{
name: "not match for unhashable extra elements",
wanted: [][]int{{1}, {1, 2}},
given: [][]int{{1}, {1, 2}, {1, 2, 3}},
wantMatch: false,
},
{
name: "match for equal slices of assignable types",
wanted: [][]string{{"a", "b"}},
given: []A{{"a", "b"}},
wantMatch: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := gomock.InAnyOrder(tt.wanted).Matches(tt.given); got != tt.wantMatch {
t.Errorf("got = %v, wantMatch %v", got, tt.wantMatch)
}
})
}
}

0 comments on commit 0cdccf5

Please sign in to comment.