Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.

Commit 0cdccf5

Browse files
authored
feat add InAnyOrder matcher (#546)
Adds a new matcher for comparing slices/arrays elements without the need for the elements to be in a particular order.
1 parent e303461 commit 0cdccf5

File tree

2 files changed

+222
-0
lines changed

2 files changed

+222
-0
lines changed

Diff for: gomock/matchers.go

+73
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,70 @@ func (m lenMatcher) String() string {
207207
return fmt.Sprintf("has length %d", m.i)
208208
}
209209

210+
type inAnyOrderMatcher struct {
211+
x interface{}
212+
}
213+
214+
func (m inAnyOrderMatcher) Matches(x interface{}) bool {
215+
given, ok := m.prepareValue(x)
216+
if !ok {
217+
return false
218+
}
219+
wanted, ok := m.prepareValue(m.x)
220+
if !ok {
221+
return false
222+
}
223+
224+
if given.Len() != wanted.Len() {
225+
return false
226+
}
227+
228+
usedFromGiven := make([]bool, given.Len())
229+
foundFromWanted := make([]bool, wanted.Len())
230+
for i := 0; i < wanted.Len(); i++ {
231+
wantedMatcher := Eq(wanted.Index(i).Interface())
232+
for j := 0; j < given.Len(); j++ {
233+
if usedFromGiven[j] {
234+
continue
235+
}
236+
if wantedMatcher.Matches(given.Index(j).Interface()) {
237+
foundFromWanted[i] = true
238+
usedFromGiven[j] = true
239+
break
240+
}
241+
}
242+
}
243+
244+
missingFromWanted := 0
245+
for _, found := range foundFromWanted {
246+
if !found {
247+
missingFromWanted++
248+
}
249+
}
250+
extraInGiven := 0
251+
for _, used := range usedFromGiven {
252+
if !used {
253+
extraInGiven++
254+
}
255+
}
256+
257+
return extraInGiven == 0 && missingFromWanted == 0
258+
}
259+
260+
func (m inAnyOrderMatcher) prepareValue(x interface{}) (reflect.Value, bool) {
261+
xValue := reflect.ValueOf(x)
262+
switch xValue.Kind() {
263+
case reflect.Slice, reflect.Array:
264+
return xValue, true
265+
default:
266+
return reflect.Value{}, false
267+
}
268+
}
269+
270+
func (m inAnyOrderMatcher) String() string {
271+
return fmt.Sprintf("has the same elements as %v", m.x)
272+
}
273+
210274
// Constructors
211275

212276
// All returns a composite Matcher that returns true if and only all of the
@@ -266,3 +330,12 @@ func AssignableToTypeOf(x interface{}) Matcher {
266330
}
267331
return assignableToTypeOfMatcher{reflect.TypeOf(x)}
268332
}
333+
334+
// InAnyOrder is a Matcher that returns true for collections of the same elements ignoring the order.
335+
//
336+
// Example usage:
337+
// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 3, 2}) // returns true
338+
// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 2}) // returns false
339+
func InAnyOrder(x interface{}) Matcher {
340+
return inAnyOrderMatcher{x}
341+
}

Diff for: gomock/matchers_test.go

+149
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,152 @@ func TestAssignableToTypeOfMatcher(t *testing.T) {
144144
t.Errorf(`AssignableToTypeOf(context.Context) should not match ctxWithValue`)
145145
}
146146
}
147+
148+
func TestInAnyOrder(t *testing.T) {
149+
tests := []struct {
150+
name string
151+
wanted interface{}
152+
given interface{}
153+
wantMatch bool
154+
}{
155+
{
156+
name: "match for equal slices",
157+
wanted: []int{1, 2, 3},
158+
given: []int{1, 2, 3},
159+
wantMatch: true,
160+
},
161+
{
162+
name: "match for slices with same elements of different order",
163+
wanted: []int{1, 2, 3},
164+
given: []int{1, 3, 2},
165+
wantMatch: true,
166+
},
167+
{
168+
name: "not match for slices with different elements",
169+
wanted: []int{1, 2, 3},
170+
given: []int{1, 2, 4},
171+
wantMatch: false,
172+
},
173+
{
174+
name: "not match for slices with missing elements",
175+
wanted: []int{1, 2, 3},
176+
given: []int{1, 2},
177+
wantMatch: false,
178+
},
179+
{
180+
name: "not match for slices with extra elements",
181+
wanted: []int{1, 2, 3},
182+
given: []int{1, 2, 3, 4},
183+
wantMatch: false,
184+
},
185+
{
186+
name: "match for empty slices",
187+
wanted: []int{},
188+
given: []int{},
189+
wantMatch: true,
190+
},
191+
{
192+
name: "not match for equal slices of different types",
193+
wanted: []float64{1, 2, 3},
194+
given: []int{1, 2, 3},
195+
wantMatch: false,
196+
},
197+
{
198+
name: "match for equal arrays",
199+
wanted: [3]int{1, 2, 3},
200+
given: [3]int{1, 2, 3},
201+
wantMatch: true,
202+
},
203+
{
204+
name: "match for equal arrays of different order",
205+
wanted: [3]int{1, 2, 3},
206+
given: [3]int{1, 3, 2},
207+
wantMatch: true,
208+
},
209+
{
210+
name: "not match for arrays of different elements",
211+
wanted: [3]int{1, 2, 3},
212+
given: [3]int{1, 2, 4},
213+
wantMatch: false,
214+
},
215+
{
216+
name: "not match for arrays with extra elements",
217+
wanted: [3]int{1, 2, 3},
218+
given: [4]int{1, 2, 3, 4},
219+
wantMatch: false,
220+
},
221+
{
222+
name: "not match for arrays with missing elements",
223+
wanted: [3]int{1, 2, 3},
224+
given: [2]int{1, 2},
225+
wantMatch: false,
226+
},
227+
{
228+
name: "not match for equal strings", // matcher shouldn't treat strings as collections
229+
wanted: "123",
230+
given: "123",
231+
wantMatch: false,
232+
},
233+
{
234+
name: "not match if x type is not iterable",
235+
wanted: 123,
236+
given: []int{123},
237+
wantMatch: false,
238+
},
239+
{
240+
name: "not match if in type is not iterable",
241+
wanted: []int{123},
242+
given: 123,
243+
wantMatch: false,
244+
},
245+
{
246+
name: "not match if both are not iterable",
247+
wanted: 123,
248+
given: 123,
249+
wantMatch: false,
250+
},
251+
{
252+
name: "match for equal slices with unhashable elements",
253+
wanted: [][]int{{1}, {1, 2}, {1, 2, 3}},
254+
given: [][]int{{1}, {1, 2}, {1, 2, 3}},
255+
wantMatch: true,
256+
},
257+
{
258+
name: "match for equal slices with unhashable elements of different order",
259+
wanted: [][]int{{1}, {1, 2, 3}, {1, 2}},
260+
given: [][]int{{1}, {1, 2}, {1, 2, 3}},
261+
wantMatch: true,
262+
},
263+
{
264+
name: "not match for different slices with unhashable elements",
265+
wanted: [][]int{{1}, {1, 2, 3}, {1, 2}},
266+
given: [][]int{{1}, {1, 2, 4}, {1, 3}},
267+
wantMatch: false,
268+
},
269+
{
270+
name: "not match for unhashable missing elements",
271+
wanted: [][]int{{1}, {1, 2}, {1, 2, 3}},
272+
given: [][]int{{1}, {1, 2}},
273+
wantMatch: false,
274+
},
275+
{
276+
name: "not match for unhashable extra elements",
277+
wanted: [][]int{{1}, {1, 2}},
278+
given: [][]int{{1}, {1, 2}, {1, 2, 3}},
279+
wantMatch: false,
280+
},
281+
{
282+
name: "match for equal slices of assignable types",
283+
wanted: [][]string{{"a", "b"}},
284+
given: []A{{"a", "b"}},
285+
wantMatch: true,
286+
},
287+
}
288+
for _, tt := range tests {
289+
t.Run(tt.name, func(t *testing.T) {
290+
if got := gomock.InAnyOrder(tt.wanted).Matches(tt.given); got != tt.wantMatch {
291+
t.Errorf("got = %v, wantMatch %v", got, tt.wantMatch)
292+
}
293+
})
294+
}
295+
}

0 commit comments

Comments
 (0)