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..b76f872de --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/CompareResult.java @@ -0,0 +1,39 @@ +package io.ebean.test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class CompareResult { + + private final boolean applicable; + private final List errors; + + static final CompareResult NO_ERRORS = new CompareResult(true, Collections.emptyList()); + static final CompareResult NOT_APPLICABLE = new CompareResult(false, Collections.emptyList()); + + static CompareResult error(String error) { + return new CompareResult(true, Collections.singletonList(error)); + } + + static CompareResult errors(List errors) { + return new CompareResult(true, errors); + } + + private CompareResult(boolean applicable, List errors) { + this.applicable = applicable; + this.errors = new ArrayList<>(errors); + } + + boolean isApplicable() { + return applicable; + } + + 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 25e1e065a..df89c0b16 100644 --- a/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java +++ b/ebean-test/src/main/java/io/ebean/test/JsonAssertContains.java @@ -3,10 +3,7 @@ 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.*; /** * Perform traversal of JsonNodes comparing against an expected JsonNode that @@ -16,15 +13,15 @@ import java.util.Stack; 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 +29,72 @@ 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)); - } - // do not continue (object or scalar type check) - return false; + return new MatchArrayElements(actualJsonNode, expectedJsonNode).match(); } - private boolean checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) { + private CompareResult checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) { if (!expectedJsonNode.isObject()) { - return true; + return CompareResult.NOT_APPLICABLE; } + List errors = new LinkedList<>(); Iterator> expectedFields = expectedJsonNode.fields(); while (expectedFields.hasNext()) { Map.Entry expectedField = expectedFields.next(); @@ -89,17 +103,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) { @@ -115,4 +130,64 @@ 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 2c25776f9..7f78c3782 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; @@ -33,18 +35,21 @@ public class JsonAssertContainsTest { JsonAssertContains.assertContains(original, expected); } 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'") + "Expected field 'someObject1.value1' to be of type 'ARRAY' but was 'NUMBER'", + "Expected field 'someObject1.value2' to be of type 'OBJECT' but was 'STRING'", + "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'", + "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"); } @@ -59,7 +64,9 @@ public class JsonAssertContainsTest { 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 @@ -72,7 +79,9 @@ public class JsonAssertContainsTest { 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 @@ -83,4 +92,103 @@ public class JsonAssertContainsTest { assertThat(contains.path("a")).isEqualTo("a"); assertThat(contains.path("b")).isEqualTo("b"); } + + + @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); + } catch (AssertionError e) { + String exceptionMessage = e.getMessage(); + 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; + } + + Assertions.fail("Expected an exception to be thrown"); + } } 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/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"} + ] +} 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,