Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions assert/assertion_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,17 @@ func JSONEqf(t TestingT, expected string, actual string, msg string, args ...int
return JSONEq(t, expected, actual, append([]interface{}{msg}, args...)...)
}

// JsonContentsMatchf asserts that two JSON strings (or []byte) decode to values
// that ObjectsMatch. Object key order and array element order are ignored.
//
// assert.JsonContentsMatchf(t, `{"a":[1,2]}`, `{"a":[2,1]}`, "error message %s", "formatted")
func JsonContentsMatchf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return JsonContentsMatch(t, expected, actual, append([]interface{}{msg}, args...)...)
}

// Lenf asserts that the specified object has specific length.
// Lenf also fails if the object has a type that len() not accept.
//
Expand Down Expand Up @@ -735,6 +746,23 @@ func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) bool {
return NotZero(t, i, append([]interface{}{msg}, args...)...)
}

// ObjectsMatchf asserts that expected and actual are deeply equal while treating
// every slice and array as an unordered multiset (the same rule as ElementsMatch),
// recursing into structs and maps. This is useful for comparing values that
// contain slices whose element order is not meaningful (issue #806).
//
// type T struct{ Names []string }
// assert.ObjectsMatchf(t, T{Names: []string{"Joe", "Rick"}}, T{Names: []string{"Rick", "Joe"}}, "error message %s", "formatted")
//
// []byte values are compared as ordered byte strings, not as unordered lists of
// bytes. Unexported struct fields are ignored.
func ObjectsMatchf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return ObjectsMatch(t, expected, actual, append([]interface{}{msg}, args...)...)
}

// Panicsf asserts that the code inside the specified PanicTestFunc panics.
//
// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted")
Expand Down
56 changes: 56 additions & 0 deletions assert/assertion_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,28 @@ func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ..
return JSONEqf(a.t, expected, actual, msg, args...)
}

// JsonContentsMatch asserts that two JSON strings (or []byte) decode to values
// that ObjectsMatch. Object key order and array element order are ignored.
//
// a.JsonContentsMatch(`{"a":[1,2]}`, `{"a":[2,1]}`)
func (a *Assertions) JsonContentsMatch(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return JsonContentsMatch(a.t, expected, actual, msgAndArgs...)
}

// JsonContentsMatchf asserts that two JSON strings (or []byte) decode to values
// that ObjectsMatch. Object key order and array element order are ignored.
//
// a.JsonContentsMatchf(`{"a":[1,2]}`, `{"a":[2,1]}`, "error message %s", "formatted")
func (a *Assertions) JsonContentsMatchf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return JsonContentsMatchf(a.t, expected, actual, msg, args...)
}

// Len asserts that the specified object has specific length.
// Len also fails if the object has a type that len() not accept.
//
Expand Down Expand Up @@ -1462,6 +1484,40 @@ func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) bo
return NotZerof(a.t, i, msg, args...)
}

// ObjectsMatch asserts that expected and actual are deeply equal while treating
// every slice and array as an unordered multiset (the same rule as ElementsMatch),
// recursing into structs and maps. This is useful for comparing values that
// contain slices whose element order is not meaningful (issue #806).
//
// type T struct{ Names []string }
// a.ObjectsMatch(T{Names: []string{"Joe", "Rick"}}, T{Names: []string{"Rick", "Joe"}})
//
// []byte values are compared as ordered byte strings, not as unordered lists of
// bytes. Unexported struct fields are ignored.
func (a *Assertions) ObjectsMatch(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ObjectsMatch(a.t, expected, actual, msgAndArgs...)
}

// ObjectsMatchf asserts that expected and actual are deeply equal while treating
// every slice and array as an unordered multiset (the same rule as ElementsMatch),
// recursing into structs and maps. This is useful for comparing values that
// contain slices whose element order is not meaningful (issue #806).
//
// type T struct{ Names []string }
// a.ObjectsMatchf(T{Names: []string{"Joe", "Rick"}}, T{Names: []string{"Rick", "Joe"}}, "error message %s", "formatted")
//
// []byte values are compared as ordered byte strings, not as unordered lists of
// bytes. Unexported struct fields are ignored.
func (a *Assertions) ObjectsMatchf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return ObjectsMatchf(a.t, expected, actual, msg, args...)
}

// Panics asserts that the code inside the specified PanicTestFunc panics.
//
// a.Panics(func(){ GoCrazy() })
Expand Down
151 changes: 151 additions & 0 deletions assert/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,157 @@ func formatListDiff(listA, listB interface{}, extraA, extraB []interface{}) stri
return msg.String()
}

// ObjectsMatch asserts that expected and actual are deeply equal while treating
// every slice and array as an unordered multiset (the same rule as ElementsMatch),
// recursing into structs and maps. This is useful for comparing values that
// contain slices whose element order is not meaningful (issue #806).
//
// type T struct{ Names []string }
// assert.ObjectsMatch(t, T{Names: []string{"Joe", "Rick"}}, T{Names: []string{"Rick", "Joe"}})
//
// []byte values are compared as ordered byte strings, not as unordered lists of
// bytes. Unexported struct fields are ignored.
func ObjectsMatch(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if objectsMatch(expected, actual) {
return true
}
return Fail(t, fmt.Sprintf("Not match: %#v, %#v", expected, actual), msgAndArgs...)
}

// JsonContentsMatch asserts that two JSON strings (or []byte) decode to values
// that ObjectsMatch. Object key order and array element order are ignored.
//
// assert.JsonContentsMatch(t, `{"a":[1,2]}`, `{"a":[2,1]}`)
func JsonContentsMatch(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
expVal, err := jsonDecode(expected)
if err != nil {
return Fail(t, fmt.Sprintf("expected is not valid JSON: %v", err), msgAndArgs...)
}
actVal, err := jsonDecode(actual)
if err != nil {
return Fail(t, fmt.Sprintf("actual is not valid JSON: %v", err), msgAndArgs...)
}
if objectsMatch(expVal, actVal) {
return true
}
return Fail(t, fmt.Sprintf("JSON contents do not match:\nexpected: %#v\nactual : %#v", expVal, actVal), msgAndArgs...)
}

func jsonDecode(v interface{}) (interface{}, error) {
var b []byte
switch x := v.(type) {
case string:
b = []byte(x)
case []byte:
b = x
case json.RawMessage:
b = x
default:
return nil, fmt.Errorf("unsupported type %T", v)
}
var out interface{}
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return out, nil
}

func objectsMatch(expected, actual interface{}) bool {
if expected == nil || actual == nil {
return expected == actual
}
return valuesMatch(reflect.ValueOf(expected), reflect.ValueOf(actual))
}

func valuesMatch(a, b reflect.Value) bool {
for a.Kind() == reflect.Ptr || a.Kind() == reflect.Interface {
if a.IsNil() {
return !b.IsValid() || ((b.Kind() == reflect.Ptr || b.Kind() == reflect.Interface) && b.IsNil())
}
a = a.Elem()
}
for b.Kind() == reflect.Ptr || b.Kind() == reflect.Interface {
if b.IsNil() {
return false
}
b = b.Elem()
}
if !a.IsValid() || !b.IsValid() {
return a.IsValid() == b.IsValid()
}
if a.Kind() != b.Kind() {
return ObjectsAreEqual(a.Interface(), b.Interface())
}

switch a.Kind() {
case reflect.Slice, reflect.Array:
if a.Kind() == reflect.Slice && a.Type().Elem().Kind() == reflect.Uint8 {
if b.Kind() != reflect.Slice || b.Type().Elem().Kind() != reflect.Uint8 {
return false
}
return bytes.Equal(a.Bytes(), b.Bytes())
}
return matchUnordered(a, b)
case reflect.Map:
if a.Len() != b.Len() {
return false
}
for _, key := range a.MapKeys() {
av := a.MapIndex(key)
bv := b.MapIndex(key)
if !bv.IsValid() || !valuesMatch(av, bv) {
return false
}
}
return true
case reflect.Struct:
if a.Type() != b.Type() {
return false
}
for i := 0; i < a.NumField(); i++ {
if !a.Type().Field(i).IsExported() {
continue
}
if !valuesMatch(a.Field(i), b.Field(i)) {
return false
}
}
return true
default:
return ObjectsAreEqual(a.Interface(), b.Interface())
}
}

func matchUnordered(a, b reflect.Value) bool {
if a.Len() != b.Len() {
return false
}
visited := make([]bool, b.Len())
for i := 0; i < a.Len(); i++ {
found := false
for j := 0; j < b.Len(); j++ {
if visited[j] {
continue
}
if valuesMatch(a.Index(i), b.Index(j)) {
visited[j] = true
found = true
break
}
}
if !found {
return false
}
}
return true
}

// NotElementsMatch asserts that the specified listA(array, slice...) is NOT 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 not match.
Expand Down
58 changes: 58 additions & 0 deletions assert/objects_match_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package assert

import (
"testing"
)

func TestObjectsMatchStructsWithUnorderedSlices(t *testing.T) {
type Test struct {
Names []string
}
c1 := Test{Names: []string{"Joe", "Rick"}}
c2 := Test{Names: []string{"Rick", "Joe"}}
c3 := Test{Names: []string{"Joe", "Bob"}}

mockT := new(testing.T)
if !ObjectsMatch(mockT, c1, c2) {
t.Fatal("expected match for same elements different order")
}
if ObjectsMatch(mockT, c1, c3) {
t.Fatal("expected mismatch for different elements")
}
}

func TestObjectsMatchNested(t *testing.T) {
type Inner struct {
Tags []string
}
type Outer struct {
Items []Inner
}
a := Outer{Items: []Inner{{Tags: []string{"x", "y"}}, {Tags: []string{"a"}}}}
b := Outer{Items: []Inner{{Tags: []string{"a"}}, {Tags: []string{"y", "x"}}}}
mockT := new(testing.T)
if !ObjectsMatch(mockT, a, b) {
t.Fatal("expected nested unordered match")
}
}

func TestJsonContentsMatch(t *testing.T) {
expected := `{"participants":["Joe","Rick"],"event":"Birthday party"}`
actual := `{"event":"Birthday party","participants":["Rick","Joe"]}`
mockT := new(testing.T)
if !JsonContentsMatch(mockT, expected, actual) {
t.Fatal("expected JSON contents match")
}
if JsonContentsMatch(mockT, expected, `{"event":"other"}`) {
t.Fatal("expected mismatch")
}
}

func TestObjectsMatchMaps(t *testing.T) {
a := map[string][]int{"k": {1, 2, 3}}
b := map[string][]int{"k": {3, 1, 2}}
mockT := new(testing.T)
if !ObjectsMatch(mockT, a, b) {
t.Fatal("map slice order should not matter")
}
}
Loading