From baefe7efc0cd394102c4696b0c13b2edd538e8d1 Mon Sep 17 00:00:00 2001 From: "David A. Eilertsen" Date: Mon, 1 Mar 2021 22:05:13 +0100 Subject: [PATCH 1/4] match unordered arrays --- match.go | 67 ++++++++++++++++++++++++++++++++++++++++++--------- match_test.go | 46 +++++++++++++++++++++++++++++++---- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/match.go b/match.go index f8a8eb3..325aff8 100644 --- a/match.go +++ b/match.go @@ -35,22 +35,17 @@ func Match(expected, actual []byte, placeholders ...Placeholder) error { return err } + d, _ := json.Marshal(act) + fmt.Printf("actual remarshal: %s\n", string(d)) + return isEqual(exp, act, "", placeholders...) } func isEqual(expected, actual interface{}, key string, ph ...Placeholder) error { - if ea, aa, ok := isArray(expected, actual); ok { - l, err := areLenEqual(ea, aa) - if err != nil { + if ea, aa, ok := isArray(expected, actual, ph); ok { + if err := matchArray(ea, aa, ph); err != nil { return errUnderKey(err, key) } - - for i := 0; i < l; i++ { - if err := isEqual(ea[i], aa[i], key, ph...); err != nil { - return err - } - } - return nil } @@ -97,7 +92,7 @@ func isEqualValue(expected, actual interface{}, ph []Placeholder) error { return nil } -func isArray(i, j interface{}) ([]interface{}, []interface{}, bool) { +func isArray(i, j interface{}, ph []Placeholder) ([]interface{}, []interface{}, bool) { ia, ok := i.([]interface{}) if !ok { return []interface{}{}, []interface{}{}, false @@ -111,6 +106,50 @@ func isArray(i, j interface{}) ([]interface{}, []interface{}, bool) { return ia, ja, true } +func matchArray(listA, listB interface{}, ph []Placeholder) error { + if isEmpty(listA) && isEmpty(listB) { + return nil + } + + aValue := reflect.ValueOf(listA) + bValue := reflect.ValueOf(listB) + + aLen := aValue.Len() + bLen := bValue.Len() + + if aLen != bLen { + return fmt.Errorf("mismatch array length %d and %d: %w", aLen, bLen, ErrArrayLengths) + } + + visited := make([]bool, bLen) + for i := 0; i < aLen; i++ { + var err error + element := aValue.Index(i).Interface() + found := false + for j := 0; j < bLen; j++ { + if visited[j] { + continue + } + fmt.Printf("comparing %#v and %#v\n", bValue.Index(j).Interface(), element) + err = isEqual(bValue.Index(j).Interface(), element, "", ph...) + fmt.Printf(" compared %#v and %#v, is was %#v\n", bValue.Index(j).Interface(), element, err) + if err == nil { + visited[j] = true + found = true + break + } + } + if !found { + return fmt.Errorf( + "element %s appears more times in %s than in %s: %w", + marshalToJSON(element), + marshalToJSON(aValue.Interface()), + marshalToJSON(bValue.Interface()), err) + } + } + return nil +} + func isObject(i, j interface{}) (map[string]interface{}, map[string]interface{}, bool) { io, ok := i.(map[string]interface{}) if !ok { @@ -169,3 +208,9 @@ func errUnderKey(err error, key string) error { return err } + +func marshalToJSON(i interface{}) string { + d, _ := json.Marshal(i) + fmt.Printf("trying to marshal %#v, got %q\n", i, string(d)) + return string(d) +} diff --git a/match_test.go b/match_test.go index 1083cd0..4325a55 100644 --- a/match_test.go +++ b/match_test.go @@ -24,7 +24,7 @@ func TestNotEqualJSONWithAdditionalArrayItem(t *testing.T) { if err := Match(expected, actual); err == nil { t.Error("expected an error, but nil was returned") } else if err.Error() != expectedErrMsg { - t.Errorf("expected error message %s to be, but got %s", expectedErrMsg, err) + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) } } @@ -32,13 +32,13 @@ func TestNotEqualJSONWithMismatchValue(t *testing.T) { var ( expected = mustReadFile(t, "test/stubs/match/expected.json") actual = mustReadFile(t, "test/stubs/match/mismatch_value.json") - expectedErrMsg = `value 2 and "hex": values are not equal` + expectedErrMsg = "mismatch under key arr_2: element [{},[2]] appears more times in [\"A\",1,[{},[2]]] than in [\"A\",1,[{},[\"hex\"]]]" ) if err := Match(expected, actual); err == nil { t.Error("expected an error, but nil was returned") } else if err.Error() != expectedErrMsg { - t.Errorf("expected error message %s to be, but got %s", expectedErrMsg, err) + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) } } @@ -102,13 +102,13 @@ func TestEqualWithArrayIntegerMismatch(t *testing.T) { var ( expected = `{"id": "1","count":[1,2,3,4,5,6]}` actual = `{"id": "1","count":[1,2,3,4,"s",6]}` - expectedErrMsg = `value 5 and "s": values are not equal` + expectedErrMsg = "mismatch under key count: element 5 appears more times in [1,2,3,4,5,6] than in [1,2,3,4,\"s\",6]" ) if err := Match([]byte(expected), []byte(actual), WithTimeLayout("$TIME_RFC3339", time.RFC3339)); err == nil { t.Error("expected a mismatch on 'count'") } else if err.Error() != expectedErrMsg { - t.Errorf("expected error message %s to be, but got %s", expectedErrMsg, err) + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) } } @@ -130,3 +130,39 @@ func mustReadFile(t *testing.T, filename string) []byte { return out } + +func TestEqualWithUnorderedArrayWhenIsEqualWithPrimitiveDataTypes(t *testing.T) { + var ( + expected = `{"id": "1","count":[1,2,["a","c","b"]]}` + actual = `{"id": "1","count":[2,1,["a","b","c"]]}` + ) + + if err := Match([]byte(expected), []byte(actual)); err != nil { + t.Errorf("unexpected mismatch: %s", err) + } +} + +func TestEqualWithUnorderedArrayWhenIsEqualWithStruct(t *testing.T) { + var ( + expected = `{"id": "1","count":[1,2,["a","c",{"flag":true}]]}` + actual = `{"id": "1","count":[2,1,["a",{"flag":true},"c"]]}` + ) + + if err := Match([]byte(expected), []byte(actual)); err != nil { + t.Errorf("unexpected mismatch: %s", err) + } +} + +func TestEqualWithUnorderedArrayWhenIsNotEqual(t *testing.T) { + var ( + expected = `{"id": "1","count":[1,2,["a","c","e"]]}` + actual = `{"id": "1","count":[2,1,["a","b","c"]]}` + expectedErrMsg = "mismatch under key count: element [\"a\",\"c\",\"e\"] appears more times in [1,2,[\"a\",\"c\",\"e\"]] than in [2,1,[\"a\",\"b\",\"c\"]]" + ) + + if err := Match([]byte(expected), []byte(actual)); err == nil { + t.Error("expected an error, but nil was returned") + } else if err.Error() != expectedErrMsg { + t.Errorf("expected error message to be %q, but got %q", expectedErrMsg, err) + } +} From 503353b9c913e793c882857fc4ae4cdbfcfa2cdb Mon Sep 17 00:00:00 2001 From: "David A. Eilertsen" Date: Mon, 1 Mar 2021 22:48:41 +0100 Subject: [PATCH 2/4] remove debug lines --- match.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/match.go b/match.go index 325aff8..d3a5f38 100644 --- a/match.go +++ b/match.go @@ -35,9 +35,6 @@ func Match(expected, actual []byte, placeholders ...Placeholder) error { return err } - d, _ := json.Marshal(act) - fmt.Printf("actual remarshal: %s\n", string(d)) - return isEqual(exp, act, "", placeholders...) } @@ -130,10 +127,7 @@ func matchArray(listA, listB interface{}, ph []Placeholder) error { if visited[j] { continue } - fmt.Printf("comparing %#v and %#v\n", bValue.Index(j).Interface(), element) - err = isEqual(bValue.Index(j).Interface(), element, "", ph...) - fmt.Printf(" compared %#v and %#v, is was %#v\n", bValue.Index(j).Interface(), element, err) - if err == nil { + if err = isEqual(bValue.Index(j).Interface(), element, "", ph...); err == nil { visited[j] = true found = true break @@ -211,6 +205,5 @@ func errUnderKey(err error, key string) error { func marshalToJSON(i interface{}) string { d, _ := json.Marshal(i) - fmt.Printf("trying to marshal %#v, got %q\n", i, string(d)) return string(d) } From abffb0912b3cb239f8d8fad31c38c2be23e32818 Mon Sep 17 00:00:00 2001 From: "David A. Eilertsen" Date: Mon, 1 Mar 2021 22:50:23 +0100 Subject: [PATCH 3/4] cleanup --- match.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/match.go b/match.go index d3a5f38..38d6c4f 100644 --- a/match.go +++ b/match.go @@ -108,11 +108,13 @@ func matchArray(listA, listB interface{}, ph []Placeholder) error { return nil } - aValue := reflect.ValueOf(listA) - bValue := reflect.ValueOf(listB) + var ( + aValue = reflect.ValueOf(listA) + bValue = reflect.ValueOf(listB) - aLen := aValue.Len() - bLen := bValue.Len() + aLen = aValue.Len() + bLen = bValue.Len() + ) if aLen != bLen { return fmt.Errorf("mismatch array length %d and %d: %w", aLen, bLen, ErrArrayLengths) @@ -157,15 +159,6 @@ func isObject(i, j interface{}) (map[string]interface{}, map[string]interface{}, return io, jo, true } -func areLenEqual(i, j []interface{}) (int, error) { - il, jl := len(i), len(j) - if il != jl { - return 0, fmt.Errorf("mismatch array length %d and %d: %w", il, jl, ErrArrayLengths) - } - - return il, nil -} - type keyMatcher struct { expected, actual bool } From 918b8079c19176c31675f2d574c092074b5d5326 Mon Sep 17 00:00:00 2001 From: "David A. Eilertsen" Date: Mon, 1 Mar 2021 23:11:35 +0100 Subject: [PATCH 4/4] change order of iteration --- README.md | 2 +- match.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 890c2a0..2c1c15f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.com/davidae/jm.svg "Travis CI status")](https://travis-ci.com/davidae/jm) A small package to match JSONs with the addition of using placeholders for possible unknown values. -It should work with any type of valid JSON - however nested and tangled it may be. This package uses only +It should work with any type of valid JSON - however nested, unordered, and tangled it may be. This package uses only golang's standard library, no dependencies. # Installation diff --git a/match.go b/match.go index 38d6c4f..ae6a5aa 100644 --- a/match.go +++ b/match.go @@ -121,15 +121,15 @@ func matchArray(listA, listB interface{}, ph []Placeholder) error { } visited := make([]bool, bLen) - for i := 0; i < aLen; i++ { + for i := 0; i < bLen; i++ { var err error - element := aValue.Index(i).Interface() + element := bValue.Index(i).Interface() found := false - for j := 0; j < bLen; j++ { + for j := 0; j < aLen; j++ { if visited[j] { continue } - if err = isEqual(bValue.Index(j).Interface(), element, "", ph...); err == nil { + if err := isEqual(aValue.Index(j).Interface(), element, "", ph...); err == nil { visited[j] = true found = true break