From bf57a5dedcb0437159f3e6104660b3c219daab4e Mon Sep 17 00:00:00 2001 From: Emil Stanchev Date: Tue, 22 Aug 2017 22:37:30 +0200 Subject: [PATCH] ElementsMatch array/slice assertion ignoring order An assertion that compares the elements of the slices/arrays disregarding the order, i.e. it checks whether each element in the first slice/array appears the same number of times in it as in the second slice/array. This name seemed like it would be easy to find. Possible alternatives for the name: - ContainsSameElements - IsPermutation (C++: http://en.cppreference.com/w/cpp/algorithm/is_permutation) - MatchArray (rspec: http://www.rubydoc.info/github/rspec/rspec-expectations/RSpec/Matchers:match_array) - EqualSorted - Other ideas? This implementaiton is O(N^2), while sorting both lists first would be O(nlogn). However, this one doesn't need to copy the lists, so it is simpler and doesn't require additional memory and time for the copies. I realize this was deemed as out of scope https://github.com/stretchr/testify/issues/275 but I decided to give it a shot as I needed it also. --- assert/assertions.go | 56 +++++++++++++++++++++++++++++++++++++++ assert/assertions_test.go | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/assert/assertions.go b/assert/assertions.go index 209ff84..4865443 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -728,6 +728,62 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) } +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2])) +// +// Returns whether the assertion was successful (true) or not (false). +func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if listA == nil || listB == nil { + return listA == listB + } + + aKind := reflect.TypeOf(listA).Kind() + bKind := reflect.TypeOf(listB).Kind() + + if aKind != reflect.Array && aKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...) + } + + if bKind != reflect.Array && bKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...) + } + + aValue := reflect.ValueOf(listA) + bValue := reflect.ValueOf(listB) + + if aValue.Len() != bValue.Len() { + return false + } + + // Mark indexes in bValue that we already used + visited := make([]bool, bValue.Len()) + + for i := 0; i < aValue.Len(); i++ { + element := aValue.Index(i).Interface() + + found := false + for j := 0; j < bValue.Len(); j++ { + if visited[j] { + continue + } + + if ObjectsAreEqual(bValue.Index(j).Interface(), element) { + visited[j] = true + found = true + break + } + } + if !found { + return false + } + } + + return true +} + // Condition uses a Comparison to assert a complex condition. func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { result := comp() diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 353e97b..ca56a57 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -612,6 +612,57 @@ func Test_includeElement(t *testing.T) { False(t, found) } +func TestElementsMatch(t *testing.T) { + mockT := new(testing.T) + + if !ElementsMatch(mockT, nil, nil) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []int{}, []int{}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []int{1}, []int{1}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []int{1, 1}, []int{1, 1}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []int{1, 2}, []int{1, 2}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []int{1, 2}, []int{2, 1}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, [2]int{1, 2}, [2]int{2, 1}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []string{"hello", "world"}, []string{"world", "hello"}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []string{"hello", "hello"}, []string{"hello", "hello"}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, []string{"hello", "hello", "world"}, []string{"hello", "world", "hello"}) { + t.Error("ElementsMatch should return true") + } + if !ElementsMatch(mockT, [3]string{"hello", "hello", "world"}, [3]string{"hello", "world", "hello"}) { + t.Error("ElementsMatch should return true") + } + + if ElementsMatch(mockT, []int{}, nil) { + t.Error("ElementsMatch should return false") + } + if ElementsMatch(mockT, []int{1}, []int{1, 1}) { + t.Error("ElementsMatch should return false") + } + if ElementsMatch(mockT, []int{1, 2}, []int{2, 2}) { + t.Error("ElementsMatch should return false") + } + if ElementsMatch(mockT, []string{"hello", "hello"}, []string{"hello"}) { + t.Error("ElementsMatch should return false") + } +} + func TestCondition(t *testing.T) { mockT := new(testing.T)