From 2a1c170aa4b11346cfb9c06f02c89a27362ac7d6 Mon Sep 17 00:00:00 2001 From: sebastian-mrozek Date: Thu, 11 Mar 2021 13:44:59 +1300 Subject: [PATCH 1/6] Add test for shuffled array comparison --- .../test/java/io/ebean/test/JsonAssertContainsTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java index 2c25776f9..1d5864a6b 100644 --- a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java +++ b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java @@ -83,4 +83,13 @@ public class JsonAssertContainsTest { assertThat(contains.path("a")).isEqualTo("a"); assertThat(contains.path("b")).isEqualTo("b"); } + + @Test + public void assertContainsArrayShuffled() { + JsonNode array = readNodeFromResource("/contains/array.json"); + JsonNode arrayShuffled = readNodeFromResource("/contains/array-shuffled.json"); + + JsonAssertContains.assertContains(array, arrayShuffled); + JsonAssertContains.assertContains(arrayShuffled, array); + } } From ef3980a3ee5805dda1b7cce19590aee80d73f71c Mon Sep 17 00:00:00 2001 From: sebastian-mrozek Date: Thu, 11 Mar 2021 13:47:46 +1300 Subject: [PATCH 2/6] Add test resources --- ebean-test/src/test/resources/contains/array-shuffled.json | 1 + ebean-test/src/test/resources/contains/array.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 ebean-test/src/test/resources/contains/array-shuffled.json create mode 100644 ebean-test/src/test/resources/contains/array.json diff --git a/ebean-test/src/test/resources/contains/array-shuffled.json b/ebean-test/src/test/resources/contains/array-shuffled.json new file mode 100644 index 000000000..07546c233 --- /dev/null +++ b/ebean-test/src/test/resources/contains/array-shuffled.json @@ -0,0 +1 @@ +[2, 54, 13, 10] \ No newline at end of file diff --git a/ebean-test/src/test/resources/contains/array.json b/ebean-test/src/test/resources/contains/array.json new file mode 100644 index 000000000..547a670c1 --- /dev/null +++ b/ebean-test/src/test/resources/contains/array.json @@ -0,0 +1 @@ +[10, 2, 13, 54] \ No newline at end of file From 4731ee034e3f5ee6c56f1e0bebb81f51060adbcf Mon Sep 17 00:00:00 2001 From: sebastian-mrozek Date: Mon, 15 Mar 2021 12:51:36 +1300 Subject: [PATCH 3/6] Handle searching for matching elements in an array Add unit tests. Refactor internals to allow reusing assertion method for finding matches in an array. --- .../java/io/ebean/test/CompareResult.java | 38 +++++ .../io/ebean/test/JsonAssertContains.java | 150 +++++++++++++----- .../io/ebean/test/JsonAssertContainsTest.java | 57 +++++-- .../array-multi-match-duplicate-props.json | 18 +++ .../resources/contains/array-multi-match.json | 20 +++ .../contains/array-objects-shuffled.json | 14 ++ .../resources/contains/array-objects.json | 11 ++ .../resources/contains/array-shuffled.json | 1 - .../src/test/resources/contains/array.json | 1 - 9 files changed, 256 insertions(+), 54 deletions(-) create mode 100644 ebean-test/src/main/java/io/ebean/test/CompareResult.java create mode 100644 ebean-test/src/test/resources/contains/array-multi-match-duplicate-props.json create mode 100644 ebean-test/src/test/resources/contains/array-multi-match.json create mode 100644 ebean-test/src/test/resources/contains/array-objects-shuffled.json create mode 100644 ebean-test/src/test/resources/contains/array-objects.json delete mode 100644 ebean-test/src/test/resources/contains/array-shuffled.json delete mode 100644 ebean-test/src/test/resources/contains/array.json diff --git a/ebean-test/src/main/java/io/ebean/test/CompareResult.java b/ebean-test/src/main/java/io/ebean/test/CompareResult.java new file mode 100644 index 000000000..1f2602674 --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/CompareResult.java @@ -0,0 +1,38 @@ +package io.ebean.test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class CompareResult { + private final boolean applicable; + private final List errors; + + public static final CompareResult NO_ERRORS = new CompareResult(true, Collections.emptyList()); + public static final CompareResult NOT_APPLICABLE = new CompareResult(false, Collections.emptyList()); + + public static CompareResult error(String error) { + return new CompareResult(true, Collections.singletonList(error)); + } + + public static CompareResult errors(List errors) { + return new CompareResult(true, errors); + } + + private CompareResult(boolean applicable, List errors) { + this.applicable = applicable; + this.errors = new ArrayList<>(errors); + } + + public boolean isApplicable() { + return applicable; + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } + + public List getErrors() { + return errors; + } +} diff --git a/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java b/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java index 25e1e065a..2c3082605 100644 --- a/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java +++ b/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java @@ -3,28 +3,26 @@ package io.ebean.test; import com.fasterxml.jackson.databind.JsonNode; import org.assertj.core.api.Assertions; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; -import java.util.Stack; +import java.util.*; +import java.util.stream.Collectors; /** * Perform traversal of JsonNodes comparing against an expected JsonNode that * typically contains a subset of the data (typically excludes any generated properties * like when modified timestamps etc). */ -class JsonAssertContains { +public class JsonAssertContains { private final Stack path = new Stack<>(); - private final LinkedList errors = new LinkedList<>(); static void assertContains(JsonNode actualJsonNode, JsonNode expectedJsonNode) { new JsonAssertContains().contains(actualJsonNode, expectedJsonNode); } private void contains(JsonNode actualJsonNode, JsonNode expectedJsonNode) { - checkRecursive(null, actualJsonNode, expectedJsonNode); - if (!errors.isEmpty()) { + CompareResult result = checkRecursive(null, actualJsonNode, expectedJsonNode); + if (result.hasErrors()) { + List errors = result.getErrors(); String errorsString = String.join("\n", errors); errorsString += "\nExpected JSON fields: " + expectedJsonNode; errorsString += "\nActual JSON: " + actualJsonNode; @@ -32,55 +30,128 @@ class JsonAssertContains { } } - private void checkRecursive(String name, JsonNode actualJsonNode, JsonNode expectedJsonNode) { + private CompareResult checkRecursive(String name, JsonNode actualJsonNode, JsonNode expectedJsonNode) { if (name != null) { path.push(name); } - if (checkNull(actualJsonNode, expectedJsonNode)) { - if (checkType(actualJsonNode, expectedJsonNode)) { - if (checkArray(actualJsonNode, expectedJsonNode)) { - if (checkObject(actualJsonNode, expectedJsonNode)) { - checkValue(actualJsonNode, expectedJsonNode); - } - } - } + + CompareResult result = checkNull(actualJsonNode, expectedJsonNode); + if (result.isApplicable()) { + return pop(name, result); } + + result = checkType(actualJsonNode, expectedJsonNode); + if (result.isApplicable()) { + return pop(name, result); + } + + result = checkArray(actualJsonNode, expectedJsonNode); + if (result.isApplicable()) { + return pop(name, result); + } + + result = checkObject(actualJsonNode, expectedJsonNode); + if (result.isApplicable()) { + return pop(name, result); + } + + result = checkValue(actualJsonNode, expectedJsonNode); + if (result.isApplicable()) { + return pop(name, result); + } + + return CompareResult.NOT_APPLICABLE; + } + + private CompareResult pop(String name, CompareResult result) { if (name != null) { path.pop(); } + return result; } - private boolean checkNull(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + private CompareResult checkNull(JsonNode actualJsonNode, JsonNode expectedJsonNode) { if (actualJsonNode == null) { - errors.add(String.format("Expected field '%s' to be '%s' but was null", path(), expectedJsonNode)); - return false; + return CompareResult.error(String.format("Expected field '%s' to be '%s' but was null", path(), expectedJsonNode)); } - return true; + return CompareResult.NOT_APPLICABLE; } - private boolean checkType(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + private CompareResult checkType(JsonNode actualJsonNode, JsonNode expectedJsonNode) { if (!expectedJsonNode.getNodeType().equals(actualJsonNode.getNodeType())) { - errors.add(String.format("Expected field '%s' to be of type '%s' but was '%s'", path(), expectedJsonNode.getNodeType(), actualJsonNode.getNodeType())); - return false; + return CompareResult.error(String.format("Expected field '%s' to be of type '%s' but was '%s'", path(), expectedJsonNode.getNodeType(), actualJsonNode.getNodeType())); } - return true; + return CompareResult.NOT_APPLICABLE; } - private boolean checkArray(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + private CompareResult checkArray(JsonNode actualJsonNode, JsonNode expectedJsonNode) { if (!expectedJsonNode.isArray()) { - return true; + return CompareResult.NOT_APPLICABLE; } - for (int i = 0; i < expectedJsonNode.size(); i++) { - checkRecursive("[" + i + "]", actualJsonNode.get(i), expectedJsonNode.get(i)); + + Map> matchingIndexes = findMatchingIndexes(actualJsonNode, expectedJsonNode); + List unmatchedIndexes = listUnmatchedIndexes(expectedJsonNode.size(), matchingIndexes); + List>> remainingEntries = removeMultipleMatches(matchingIndexes); + if (!remainingEntries.isEmpty()) { + unmatchedIndexes.addAll(remainingEntries.stream().map(Map.Entry::getKey).collect(Collectors.toList())); } - // do not continue (object or scalar type check) - return false; + + List errors = unmatchedIndexes.stream() + .map(index -> String.format("Unable to match expected element '%s[%d]' in the actual array", path(), index)) + .collect(Collectors.toList()); + + return CompareResult.errors(errors); } - private boolean checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) { - if (!expectedJsonNode.isObject()) { - return true; + private List listUnmatchedIndexes(int size, Map> matchingIndexes) { + List unmatched = new LinkedList<>(); + for (int i = 0; i < size; i++) { + if (!matchingIndexes.containsKey(i)) { + unmatched.add(i); + } } + return unmatched; + } + + private List>> removeMultipleMatches(Map> matchingIndexes) { + List>> entries = new ArrayList<>(matchingIndexes.entrySet()); + entries.sort(Comparator.comparingInt(entry -> entry.getValue().size())); + ListIterator>> iterator = entries.listIterator(); + + while (iterator.hasNext()) { + Map.Entry> next = iterator.next(); + if (!next.getValue().isEmpty()) { + iterator.remove(); + Integer aMatchingIndex = next.getValue().stream().findFirst().get(); + removeAllMatchingIndexesOf(aMatchingIndex, entries); + } + } + + return entries; + } + + private void removeAllMatchingIndexesOf(Integer aMatchingIndex, List>> matchingIndexes) { + matchingIndexes.forEach(entry -> entry.getValue().remove(aMatchingIndex)); + } + + private Map> findMatchingIndexes(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + Map> matchingElementsIndexes = new HashMap<>(); + for (int e = 0; e < expectedJsonNode.size(); e++) { + for (int a = 0; a < actualJsonNode.size(); a++) { + CompareResult result = checkRecursive("[" + e + "]", actualJsonNode.get(a), expectedJsonNode.get(e)); + if (result.isApplicable() && !result.hasErrors()) { + matchingElementsIndexes.computeIfAbsent(e, key -> new HashSet<>()).add(a); + } + } + } + return matchingElementsIndexes; + } + + private CompareResult checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + if (!expectedJsonNode.isObject()) { + return CompareResult.NOT_APPLICABLE; + } + List errors = new LinkedList<>(); Iterator> expectedFields = expectedJsonNode.fields(); while (expectedFields.hasNext()) { Map.Entry expectedField = expectedFields.next(); @@ -89,17 +160,18 @@ class JsonAssertContains { if (actualNode == null) { errors.add(String.format("Expected field '%s' to be present", path(expectedKey))); } else { - checkRecursive(expectedKey, actualNode, expectedField.getValue()); + CompareResult result = checkRecursive(expectedKey, actualNode, expectedField.getValue()); + errors.addAll(result.getErrors()); } } - // do not continue (scalar type check) - return false; + return CompareResult.errors(errors); } - private void checkValue(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + private CompareResult checkValue(JsonNode actualJsonNode, JsonNode expectedJsonNode) { if (!expectedJsonNode.equals(actualJsonNode)) { - errors.add(String.format("Expected field '%s' to be equal to '%s' but was '%s'", path(), expectedJsonNode, actualJsonNode)); + return CompareResult.error(String.format("Expected field '%s' to be equal to '%s' but was '%s'", path(), expectedJsonNode, actualJsonNode)); } + return CompareResult.NO_ERRORS; } String path(String expectedKey) { diff --git a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java index 1d5864a6b..579bc2eca 100644 --- a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java +++ b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java @@ -1,10 +1,12 @@ package io.ebean.test; import com.fasterxml.jackson.databind.JsonNode; +import org.assertj.core.api.Assertions; import org.junit.Test; import java.util.stream.Stream; +import static io.ebean.test.Json.readNode; import static io.ebean.test.Json.readNodeFromResource; import static org.assertj.core.api.Assertions.assertThat; @@ -31,18 +33,20 @@ public class JsonAssertContainsTest { JsonNode expected = readNodeFromResource("/contains/original-subset-modified.json"); try { JsonAssertContains.assertContains(original, expected); + Assertions.fail("Expected an exception to be thrown"); } catch (AssertionError e) { String exceptionMessage = e.getMessage(); + System.out.println(exceptionMessage); Stream.of("Expected field 'someString1' to be equal to '\"aaaa\"' but was '\"string1\"", "Expected field 'someValue1' to be equal to '99' but was '1'", - "Expected field 'someArray1[0]' to be of type 'STRING' but was 'NUMBER", - "Expected field 'someArray2[0].value1' to be of type 'ARRAY' but was 'NUMBER'", - "Expected field 'someArray2[0].value2' to be of type 'OBJECT' but was 'STRING'", - "Expected field 'someArray2[0].array1[0]' to be '\"1\"' but was null", - "Expected field 'someArray2[0].object1.val5' to be present", - "Expected field 'someArray2[0].object1.val6' to be present", - "Expected field 'someArray2[0].object2' to be of type 'NULL' but was 'OBJECT'", - "Expected field 'someArray2[0].objectNull' to be of type 'OBJECT' but was 'NULL'") + "Unable to match expected element 'someArray1[0]' in the actual array", + "Expected field 'someObject1.value1' to be of type 'ARRAY' but was 'NUMBER'", + "Expected field 'someObject1.value2' to be of type 'OBJECT' but was 'STRING'", + "Unable to match expected element 'someObject1.array1[0]' in the actual array", + "Expected field 'someObject1.object1.val5' to be present", + "Expected field 'someObject1.object1.val6' to be present", + "Expected field 'someObject1.object2' to be of type 'NULL' but was 'OBJECT'", + "Expected field 'someObject1.objectNull' to be of type 'OBJECT' but was 'NULL'") .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); } } @@ -54,6 +58,7 @@ public class JsonAssertContainsTest { JsonNode expected = readNodeFromResource("/contains/check-null-expected.json"); try { JsonAssertContains.assertContains(original, expected); + Assertions.fail("Expected an exception to be thrown"); } catch (AssertionError e) { String exceptionMessage = e.getMessage(); Stream.of("Expected field 'someNull' to be of type 'NULL' but was 'STRING'", @@ -68,6 +73,7 @@ public class JsonAssertContainsTest { JsonNode expected = readNodeFromResource("/contains/check-type-expected.json"); try { JsonAssertContains.assertContains(original, expected); + Assertions.fail("Expected an exception to be thrown"); } catch (AssertionError e) { String exceptionMessage = e.getMessage(); Stream.of("Expected field 'some' to be of type 'NUMBER' but was 'STRING'") @@ -84,12 +90,37 @@ public class JsonAssertContainsTest { assertThat(contains.path("b")).isEqualTo("b"); } - @Test - public void assertContainsArrayShuffled() { - JsonNode array = readNodeFromResource("/contains/array.json"); - JsonNode arrayShuffled = readNodeFromResource("/contains/array-shuffled.json"); - JsonAssertContains.assertContains(array, arrayShuffled); + @Test + public void assertContainsNumbersArrayShuffled() { + JsonNode array = readNode("[2, 54, 13, 10]"); + JsonNode arrayShuffled = readNode("[2, 13, 10, 54]"); + JsonAssertContains.assertContains(arrayShuffled, array); } + + @Test + public void assertContainsObjectsArrayShuffled() { + JsonNode array = readNodeFromResource("/contains/array-objects.json"); + JsonNode arrayShuffled = readNodeFromResource("/contains/array-objects-shuffled.json"); + + JsonAssertContains.assertContains(arrayShuffled, array); + } + + @Test + public void assertArrayElementsNotFound() { + JsonNode original = readNodeFromResource("/contains/array-multi-match.json"); + JsonNode actual = readNodeFromResource("/contains/array-multi-match-duplicate-props.json"); + + try { + JsonAssertContains.assertContains(actual, original); + Assertions.fail("Expected an exception to be thrown"); + } catch (AssertionError e) { + System.out.println(e); + String exceptionMessage = e.getMessage(); + Stream.of("Unable to match expected element '[5]' in the actual array", + "Unable to match expected element '[4]' in the actual array") + .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + } + } } diff --git a/ebean-test/src/test/resources/contains/array-multi-match-duplicate-props.json b/ebean-test/src/test/resources/contains/array-multi-match-duplicate-props.json new file mode 100644 index 000000000..4dbcb1c10 --- /dev/null +++ b/ebean-test/src/test/resources/contains/array-multi-match-duplicate-props.json @@ -0,0 +1,18 @@ +[ + { + "b": 2, + "c": 3 + }, + { + "d": 4, + "e": 5 + }, + { + "a": 1, + "b": 2, + "c": 3 + }, + { + "b": 2 + } +] diff --git a/ebean-test/src/test/resources/contains/array-multi-match.json b/ebean-test/src/test/resources/contains/array-multi-match.json new file mode 100644 index 000000000..0d106dd88 --- /dev/null +++ b/ebean-test/src/test/resources/contains/array-multi-match.json @@ -0,0 +1,20 @@ +[ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + }, + { + "d": 4 + }, + { + "e": 5 + }, + { + "f": 6 + } +] diff --git a/ebean-test/src/test/resources/contains/array-objects-shuffled.json b/ebean-test/src/test/resources/contains/array-objects-shuffled.json new file mode 100644 index 000000000..8c28a8e7a --- /dev/null +++ b/ebean-test/src/test/resources/contains/array-objects-shuffled.json @@ -0,0 +1,14 @@ +[ + { + "id": "tyu", + "c": 3 + }, + { + "id": "zxy", + "a": 1 + }, + { + "id": "123", + "b": 2 + } +] \ No newline at end of file diff --git a/ebean-test/src/test/resources/contains/array-objects.json b/ebean-test/src/test/resources/contains/array-objects.json new file mode 100644 index 000000000..bfc5e4159 --- /dev/null +++ b/ebean-test/src/test/resources/contains/array-objects.json @@ -0,0 +1,11 @@ +[ + { + "a": 1 + }, + { + "b": 2 + }, + { + "c": 3 + } +] diff --git a/ebean-test/src/test/resources/contains/array-shuffled.json b/ebean-test/src/test/resources/contains/array-shuffled.json deleted file mode 100644 index 07546c233..000000000 --- a/ebean-test/src/test/resources/contains/array-shuffled.json +++ /dev/null @@ -1 +0,0 @@ -[2, 54, 13, 10] \ No newline at end of file diff --git a/ebean-test/src/test/resources/contains/array.json b/ebean-test/src/test/resources/contains/array.json deleted file mode 100644 index 547a670c1..000000000 --- a/ebean-test/src/test/resources/contains/array.json +++ /dev/null @@ -1 +0,0 @@ -[10, 2, 13, 54] \ No newline at end of file From 899fdca9958913f07cb5bad2ca778e3a11d21abd Mon Sep 17 00:00:00 2001 From: sebastian-mrozek Date: Mon, 15 Mar 2021 13:04:05 +1300 Subject: [PATCH 4/6] Improve assertions in case json compare does not fail as expected Remove sys out print. --- .../java/io/ebean/test/JsonAssertContainsTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java index 579bc2eca..59d69c00b 100644 --- a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java +++ b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java @@ -33,7 +33,6 @@ public class JsonAssertContainsTest { JsonNode expected = readNodeFromResource("/contains/original-subset-modified.json"); try { JsonAssertContains.assertContains(original, expected); - Assertions.fail("Expected an exception to be thrown"); } catch (AssertionError e) { String exceptionMessage = e.getMessage(); System.out.println(exceptionMessage); @@ -48,7 +47,9 @@ public class JsonAssertContainsTest { "Expected field 'someObject1.object2' to be of type 'NULL' but was 'OBJECT'", "Expected field 'someObject1.objectNull' to be of type 'OBJECT' but was 'NULL'") .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + return; } + Assertions.fail("Expected an exception to be thrown"); } @@ -58,13 +59,14 @@ public class JsonAssertContainsTest { JsonNode expected = readNodeFromResource("/contains/check-null-expected.json"); try { JsonAssertContains.assertContains(original, expected); - Assertions.fail("Expected an exception to be thrown"); } catch (AssertionError e) { String exceptionMessage = e.getMessage(); Stream.of("Expected field 'someNull' to be of type 'NULL' but was 'STRING'", "Expected field 'extra' to be present") .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + return; } + Assertions.fail("Expected an exception to be thrown"); } @Test @@ -73,12 +75,13 @@ public class JsonAssertContainsTest { JsonNode expected = readNodeFromResource("/contains/check-type-expected.json"); try { JsonAssertContains.assertContains(original, expected); - Assertions.fail("Expected an exception to be thrown"); } catch (AssertionError e) { String exceptionMessage = e.getMessage(); Stream.of("Expected field 'some' to be of type 'NUMBER' but was 'STRING'") .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + return; } + Assertions.fail("Expected an exception to be thrown"); } @Test @@ -114,13 +117,14 @@ public class JsonAssertContainsTest { try { JsonAssertContains.assertContains(actual, original); - Assertions.fail("Expected an exception to be thrown"); } catch (AssertionError e) { - System.out.println(e); String exceptionMessage = e.getMessage(); Stream.of("Unable to match expected element '[5]' in the actual array", "Unable to match expected element '[4]' in the actual array") .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + return; } + + Assertions.fail("Expected an exception to be thrown"); } } From e3001e8bdddce41fa97644298a73b9e7368250d7 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Tue, 16 Mar 2021 09:30:54 +1300 Subject: [PATCH 5/6] Fix JsonAssertContains testContainsFails with missing parts of JSON Just add missing parts of test/resources/original.json and original-subset-modified.json --- .../contains/original-subset-modified.json | 16 ++++++++++++++++ .../src/test/resources/contains/original.json | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/ebean-test/src/test/resources/contains/original-subset-modified.json b/ebean-test/src/test/resources/contains/original-subset-modified.json index 107d557d2..3c3e70f3e 100644 --- a/ebean-test/src/test/resources/contains/original-subset-modified.json +++ b/ebean-test/src/test/resources/contains/original-subset-modified.json @@ -3,6 +3,22 @@ "someString2": "string2", "someValue1": 99, "someValue2": 2, + "someObject1": { + "value1": [], + "value2": { + + }, + "array1": [ + 101 + ], + "object1": { + "val5": "val5", + "val6": "val6" + }, + "object2": null, + "objectNull": { + } + }, "someArray1": [ "a" ], diff --git a/ebean-test/src/test/resources/contains/original.json b/ebean-test/src/test/resources/contains/original.json index 063affac4..8f6fe3624 100644 --- a/ebean-test/src/test/resources/contains/original.json +++ b/ebean-test/src/test/resources/contains/original.json @@ -3,6 +3,19 @@ "someString2": "string2", "someValue1": 1, "someValue2": 2, + "someObject1": { + "value1": 42, + "value2": "string3", + "array1": [ + 99 + ], + "object1": { + }, + "object2": { + + }, + "objectNull": null + }, "someArray1": [ 1, 2, From c9968a96629a1ded327e9a517cab3a0074e9c16e Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Tue, 16 Mar 2021 22:03:12 +1300 Subject: [PATCH 6/6] Change JsonAssertContains array matching to match on size --- .../java/io/ebean/test/CompareResult.java | 49 +++---- .../io/ebean/test/JsonAssertContains.java | 121 +++++++++--------- .../io/ebean/test/JsonAssertContainsTest.java | 72 ++++++++++- .../resources/contains/nested-array-both.json | 8 ++ .../contains/nested-array-extraExpected.json | 9 ++ .../contains/nested-array-missExpected.json | 7 + .../contains/nested-array-ordering.json | 8 ++ .../test/resources/contains/nested-array.json | 8 ++ 8 files changed, 195 insertions(+), 87 deletions(-) create mode 100644 ebean-test/src/test/resources/contains/nested-array-both.json create mode 100644 ebean-test/src/test/resources/contains/nested-array-extraExpected.json create mode 100644 ebean-test/src/test/resources/contains/nested-array-missExpected.json create mode 100644 ebean-test/src/test/resources/contains/nested-array-ordering.json create mode 100644 ebean-test/src/test/resources/contains/nested-array.json diff --git a/ebean-test/src/main/java/io/ebean/test/CompareResult.java b/ebean-test/src/main/java/io/ebean/test/CompareResult.java index 1f2602674..b76f872de 100644 --- a/ebean-test/src/main/java/io/ebean/test/CompareResult.java +++ b/ebean-test/src/main/java/io/ebean/test/CompareResult.java @@ -4,35 +4,36 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public final class CompareResult { - private final boolean applicable; - private final List errors; +final class CompareResult { - public static final CompareResult NO_ERRORS = new CompareResult(true, Collections.emptyList()); - public static final CompareResult NOT_APPLICABLE = new CompareResult(false, Collections.emptyList()); + private final boolean applicable; + private final List errors; - public static CompareResult error(String error) { - return new CompareResult(true, Collections.singletonList(error)); - } + static final CompareResult NO_ERRORS = new CompareResult(true, Collections.emptyList()); + static final CompareResult NOT_APPLICABLE = new CompareResult(false, Collections.emptyList()); - public static CompareResult errors(List errors) { - return new CompareResult(true, errors); - } + static CompareResult error(String error) { + return new CompareResult(true, Collections.singletonList(error)); + } - private CompareResult(boolean applicable, List errors) { - this.applicable = applicable; - this.errors = new ArrayList<>(errors); - } + static CompareResult errors(List errors) { + return new CompareResult(true, errors); + } - public boolean isApplicable() { - return applicable; - } + private CompareResult(boolean applicable, List errors) { + this.applicable = applicable; + this.errors = new ArrayList<>(errors); + } - public boolean hasErrors() { - return !errors.isEmpty(); - } + boolean isApplicable() { + return applicable; + } - public List getErrors() { - return errors; - } + boolean hasErrors() { + return !errors.isEmpty(); + } + + List getErrors() { + return errors; + } } diff --git a/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java b/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java index 2c3082605..df89c0b16 100644 --- a/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java +++ b/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java @@ -4,14 +4,13 @@ import com.fasterxml.jackson.databind.JsonNode; import org.assertj.core.api.Assertions; import java.util.*; -import java.util.stream.Collectors; /** * Perform traversal of JsonNodes comparing against an expected JsonNode that * typically contains a subset of the data (typically excludes any generated properties * like when modified timestamps etc). */ -public class JsonAssertContains { +class JsonAssertContains { private final Stack path = new Stack<>(); @@ -88,63 +87,7 @@ public class JsonAssertContains { if (!expectedJsonNode.isArray()) { return CompareResult.NOT_APPLICABLE; } - - Map> matchingIndexes = findMatchingIndexes(actualJsonNode, expectedJsonNode); - List unmatchedIndexes = listUnmatchedIndexes(expectedJsonNode.size(), matchingIndexes); - List>> remainingEntries = removeMultipleMatches(matchingIndexes); - if (!remainingEntries.isEmpty()) { - unmatchedIndexes.addAll(remainingEntries.stream().map(Map.Entry::getKey).collect(Collectors.toList())); - } - - List errors = unmatchedIndexes.stream() - .map(index -> String.format("Unable to match expected element '%s[%d]' in the actual array", path(), index)) - .collect(Collectors.toList()); - - return CompareResult.errors(errors); - } - - private List listUnmatchedIndexes(int size, Map> matchingIndexes) { - List unmatched = new LinkedList<>(); - for (int i = 0; i < size; i++) { - if (!matchingIndexes.containsKey(i)) { - unmatched.add(i); - } - } - return unmatched; - } - - private List>> removeMultipleMatches(Map> matchingIndexes) { - List>> entries = new ArrayList<>(matchingIndexes.entrySet()); - entries.sort(Comparator.comparingInt(entry -> entry.getValue().size())); - ListIterator>> iterator = entries.listIterator(); - - while (iterator.hasNext()) { - Map.Entry> next = iterator.next(); - if (!next.getValue().isEmpty()) { - iterator.remove(); - Integer aMatchingIndex = next.getValue().stream().findFirst().get(); - removeAllMatchingIndexesOf(aMatchingIndex, entries); - } - } - - return entries; - } - - private void removeAllMatchingIndexesOf(Integer aMatchingIndex, List>> matchingIndexes) { - matchingIndexes.forEach(entry -> entry.getValue().remove(aMatchingIndex)); - } - - private Map> findMatchingIndexes(JsonNode actualJsonNode, JsonNode expectedJsonNode) { - Map> matchingElementsIndexes = new HashMap<>(); - for (int e = 0; e < expectedJsonNode.size(); e++) { - for (int a = 0; a < actualJsonNode.size(); a++) { - CompareResult result = checkRecursive("[" + e + "]", actualJsonNode.get(a), expectedJsonNode.get(e)); - if (result.isApplicable() && !result.hasErrors()) { - matchingElementsIndexes.computeIfAbsent(e, key -> new HashSet<>()).add(a); - } - } - } - return matchingElementsIndexes; + return new MatchArrayElements(actualJsonNode, expectedJsonNode).match(); } private CompareResult checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) { @@ -187,4 +130,64 @@ public class JsonAssertContains { } return String.join(".", path).replace(".[", "["); } + + /** + * Match two arrays allowing elements to be in a different order. + */ + private class MatchArrayElements { + private final JsonNode actualJsonNode; + private final JsonNode expectedJsonNode; + private final Map expectedMap = new LinkedHashMap<>(); + private final Map actualMap = new LinkedHashMap<>(); + + MatchArrayElements(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + this.actualJsonNode = actualJsonNode; + this.expectedJsonNode = expectedJsonNode; + for (int e = 0; e < expectedJsonNode.size(); e++) { + expectedMap.put(e, expectedJsonNode.get(e)); + } + for (int a = 0; a < actualJsonNode.size(); a++) { + actualMap.put(a, actualJsonNode.get(a)); + } + } + + CompareResult match() { + Iterator> iterator = expectedMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry expectedEntry = iterator.next(); + int matchPos = findFirstMatch(expectedEntry.getKey(), expectedEntry.getValue()); + if (matchPos > -1) { + iterator.remove(); + } + } + + List errors = new ArrayList<>(); + if (actualJsonNode.size() != expectedJsonNode.size()) { + errors.add(String.format("Unmatched array size for '%s', expected %d but got %d elements", path(), expectedJsonNode.size(), actualJsonNode.size())); + } + // unmatched expected -> actual + for (Map.Entry entry : expectedMap.entrySet()) { + errors.add(String.format("Expected array element '%s[%d]' was not matched to an element in the actual array - element: %s", path(), entry.getKey(), entry.getValue())); + } + // unmatched actual -> expected + for (Map.Entry entry : actualMap.entrySet()) { + errors.add(String.format("Actual array element '%s[%d]' was not matched to an element in the expected array - element: %s", path(), entry.getKey(), entry.getValue())); + } + return CompareResult.errors(errors); + } + + private int findFirstMatch(int e, JsonNode expectedNode) { + Iterator> iterator = actualMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + CompareResult result = checkRecursive("[" + e + "]", entry.getValue(), expectedNode); + if (result.isApplicable() && !result.hasErrors()) { + iterator.remove(); + return entry.getKey(); + } + } + return -1; + } + } + } diff --git a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java index 59d69c00b..7f78c3782 100644 --- a/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java +++ b/ebean-test/src/test/java/io/ebean/test/JsonAssertContainsTest.java @@ -38,10 +38,10 @@ public class JsonAssertContainsTest { System.out.println(exceptionMessage); Stream.of("Expected field 'someString1' to be equal to '\"aaaa\"' but was '\"string1\"", "Expected field 'someValue1' to be equal to '99' but was '1'", - "Unable to match expected element 'someArray1[0]' in the actual array", "Expected field 'someObject1.value1' to be of type 'ARRAY' but was 'NUMBER'", "Expected field 'someObject1.value2' to be of type 'OBJECT' but was 'STRING'", - "Unable to match expected element 'someObject1.array1[0]' in the actual array", + "Expected array element 'someObject1.array1[0]' was not matched to an element in the actual array - element: 101", + "Actual array element 'someObject1.array1[0]' was not matched to an element in the expected array - element: 99", "Expected field 'someObject1.object1.val5' to be present", "Expected field 'someObject1.object1.val6' to be present", "Expected field 'someObject1.object2' to be of type 'NULL' but was 'OBJECT'", @@ -119,8 +119,72 @@ public class JsonAssertContainsTest { JsonAssertContains.assertContains(actual, original); } catch (AssertionError e) { String exceptionMessage = e.getMessage(); - Stream.of("Unable to match expected element '[5]' in the actual array", - "Unable to match expected element '[4]' in the actual array") + Stream.of("Unmatched array size for '', expected 6 but got 4 elements", + "Expected array element '[2]' was not matched to an element in the actual array - element: {\"c\":3}", + "Expected array element '[4]' was not matched to an element in the actual array - element: {\"e\":5}", + "Expected array element '[5]' was not matched to an element in the actual array - element: {\"f\":6}", + "Actual array element '[3]' was not matched to an element in the expected array - element: {\"b\":2}") + .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + return; + } + + Assertions.fail("Expected an exception to be thrown"); + } + + @Test + public void assertNestedArray_ordering() { + JsonNode original = readNodeFromResource("/contains/nested-array.json"); + JsonNode actual = readNodeFromResource("/contains/nested-array-ordering.json"); + JsonAssertContains.assertContains(actual, original); + } + + @Test + public void assertNestedArray_both() { + try { + JsonNode original = readNodeFromResource("/contains/nested-array.json"); + JsonNode actual = readNodeFromResource("/contains/nested-array-both.json"); + JsonAssertContains.assertContains(actual, original); + } catch (AssertionError e) { + String exceptionMessage = e.getMessage(); + Stream.of( + "Expected array element 'someArray1[1]' was not matched to an element in the actual array - element: {\"name\":\"b\"}", + "Actual array element 'someArray1[0]' was not matched to an element in the expected array - element: {\"name\":\"z\"}") + .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + return; + } + + Assertions.fail("Expected an exception to be thrown"); + } + + @Test + public void assertNestedArray_extraExpected() { + try { + JsonNode original = readNodeFromResource("/contains/nested-array.json"); + JsonNode actual = readNodeFromResource("/contains/nested-array-extraExpected.json"); + JsonAssertContains.assertContains(actual, original); + } catch (AssertionError e) { + String exceptionMessage = e.getMessage(); + Stream.of( + "Unmatched array size for 'someArray1', expected 3 but got 4 elements", + "Actual array element 'someArray1[2]' was not matched to an element in the expected array - element: {\"name\":\"d\"}") + .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); + return; + } + + Assertions.fail("Expected an exception to be thrown"); + } + + @Test + public void assertNestedArray_missExpected() { + try { + JsonNode original = readNodeFromResource("/contains/nested-array.json"); + JsonNode actual = readNodeFromResource("/contains/nested-array-missExpected.json"); + JsonAssertContains.assertContains(actual, original); + } catch (AssertionError e) { + String exceptionMessage = e.getMessage(); + Stream.of( + "Unmatched array size for 'someArray1', expected 3 but got 2 elements", + "Expected array element 'someArray1[1]' was not matched to an element in the actual array - element: {\"name\":\"b\"}") .forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError)); return; } diff --git a/ebean-test/src/test/resources/contains/nested-array-both.json b/ebean-test/src/test/resources/contains/nested-array-both.json new file mode 100644 index 000000000..58eca1390 --- /dev/null +++ b/ebean-test/src/test/resources/contains/nested-array-both.json @@ -0,0 +1,8 @@ +{ + "someString1": "string1", + "someArray1": [ + {"name":"z"}, + {"name":"c"}, + {"name":"a"} + ] +} diff --git a/ebean-test/src/test/resources/contains/nested-array-extraExpected.json b/ebean-test/src/test/resources/contains/nested-array-extraExpected.json new file mode 100644 index 000000000..52d94b44d --- /dev/null +++ b/ebean-test/src/test/resources/contains/nested-array-extraExpected.json @@ -0,0 +1,9 @@ +{ + "someString1": "string1", + "someArray1": [ + {"name":"c"}, + {"name":"a"}, + {"name":"d"}, + {"name":"b"} + ] +} diff --git a/ebean-test/src/test/resources/contains/nested-array-missExpected.json b/ebean-test/src/test/resources/contains/nested-array-missExpected.json new file mode 100644 index 000000000..5958b2801 --- /dev/null +++ b/ebean-test/src/test/resources/contains/nested-array-missExpected.json @@ -0,0 +1,7 @@ +{ + "someString1": "string1", + "someArray1": [ + {"name":"c"}, + {"name":"a"} + ] +} diff --git a/ebean-test/src/test/resources/contains/nested-array-ordering.json b/ebean-test/src/test/resources/contains/nested-array-ordering.json new file mode 100644 index 000000000..543d9b6f1 --- /dev/null +++ b/ebean-test/src/test/resources/contains/nested-array-ordering.json @@ -0,0 +1,8 @@ +{ + "someString1": "string1", + "someArray1": [ + {"name":"c"}, + {"name":"a"}, + {"name":"b"} + ] +} diff --git a/ebean-test/src/test/resources/contains/nested-array.json b/ebean-test/src/test/resources/contains/nested-array.json new file mode 100644 index 000000000..13e74ace0 --- /dev/null +++ b/ebean-test/src/test/resources/contains/nested-array.json @@ -0,0 +1,8 @@ +{ + "someString1": "string1", + "someArray1": [ + {"name":"a"}, + {"name":"b"}, + {"name":"c"} + ] +}