diff --git a/assert/assertion_format.go b/assert/assertion_format.go index a19a89279..38a437ff4 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -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. // @@ -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") diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index cd2a86061..8055bbfc0 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -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. // @@ -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() }) diff --git a/assert/assertions.go b/assert/assertions.go index 1419e4776..bac3d9508 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -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. diff --git a/assert/objects_match_test.go b/assert/objects_match_test.go new file mode 100644 index 000000000..b89b1729c --- /dev/null +++ b/assert/objects_match_test.go @@ -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") + } +} diff --git a/require/require.go b/require/require.go index 652871f2e..9a96d1f7c 100644 --- a/require/require.go +++ b/require/require.go @@ -1165,6 +1165,34 @@ func JSONEqf(t TestingT, expected string, actual string, msg string, args ...int t.FailNow() } +// JsonContentsMatch asserts that two JSON strings (or []byte) decode to values +// that ObjectsMatch. Object key order and array element order are ignored. +// +// require.JsonContentsMatch(t, `{"a":[1,2]}`, `{"a":[2,1]}`) +func JsonContentsMatch(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JsonContentsMatch(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// JsonContentsMatchf asserts that two JSON strings (or []byte) decode to values +// that ObjectsMatch. Object key order and array element order are ignored. +// +// require.JsonContentsMatchf(t, `{"a":[1,2]}`, `{"a":[2,1]}`, "error message %s", "formatted") +func JsonContentsMatchf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JsonContentsMatchf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + // Len asserts that the specified object has specific length. // Len also fails if the object has a type that len() not accept. // @@ -1819,6 +1847,46 @@ func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) { t.FailNow() } +// 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 } +// require.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 interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ObjectsMatch(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// 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 } +// require.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{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ObjectsMatchf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + // Panics asserts that the code inside the specified PanicTestFunc panics. // // require.Panics(t, func(){ GoCrazy() }) diff --git a/require/require_forward.go b/require/require_forward.go index edac147ef..0213fd710 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -919,6 +919,28 @@ func (a *Assertions) JSONEqf(expected string, actual string, msg string, args .. 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{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + 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{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + 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. // @@ -1435,6 +1457,40 @@ func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) { 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{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + 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{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ObjectsMatchf(a.t, expected, actual, msg, args...) +} + // Panics asserts that the code inside the specified PanicTestFunc panics. // // a.Panics(func(){ GoCrazy() })