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"} + ] +}