diff --git a/gomock/matchers.go b/gomock/matchers.go index f30f8030..2822fb2c 100644 --- a/gomock/matchers.go +++ b/gomock/matchers.go @@ -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 @@ -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} +} diff --git a/gomock/matchers_test.go b/gomock/matchers_test.go index 42a1c5a8..61bc1993 100644 --- a/gomock/matchers_test.go +++ b/gomock/matchers_test.go @@ -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) + } + }) + } +}