mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
Merge branch 'feature/json-assert-compare-arrays' of https://github.com/sebastian-mrozek/ebean into sebastian-mrozek-feature/json-assert-compare-arrays
This commit is contained in:
@@ -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<String> 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<String> errors) {
|
||||
return new CompareResult(true, errors);
|
||||
}
|
||||
|
||||
private CompareResult(boolean applicable, List<String> errors) {
|
||||
this.applicable = applicable;
|
||||
this.errors = new ArrayList<>(errors);
|
||||
}
|
||||
|
||||
public boolean isApplicable() {
|
||||
return applicable;
|
||||
}
|
||||
|
||||
public boolean hasErrors() {
|
||||
return !errors.isEmpty();
|
||||
}
|
||||
|
||||
public List<String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -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<String> path = new Stack<>();
|
||||
private final LinkedList<String> 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<String> 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<Integer, Set<Integer>> matchingIndexes = findMatchingIndexes(actualJsonNode, expectedJsonNode);
|
||||
List<Integer> unmatchedIndexes = listUnmatchedIndexes(expectedJsonNode.size(), matchingIndexes);
|
||||
List<Map.Entry<Integer, Set<Integer>>> 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<String> 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<Integer> listUnmatchedIndexes(int size, Map<Integer, Set<Integer>> matchingIndexes) {
|
||||
List<Integer> unmatched = new LinkedList<>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (!matchingIndexes.containsKey(i)) {
|
||||
unmatched.add(i);
|
||||
}
|
||||
}
|
||||
return unmatched;
|
||||
}
|
||||
|
||||
private List<Map.Entry<Integer, Set<Integer>>> removeMultipleMatches(Map<Integer, Set<Integer>> matchingIndexes) {
|
||||
List<Map.Entry<Integer, Set<Integer>>> entries = new ArrayList<>(matchingIndexes.entrySet());
|
||||
entries.sort(Comparator.comparingInt(entry -> entry.getValue().size()));
|
||||
ListIterator<Map.Entry<Integer, Set<Integer>>> iterator = entries.listIterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<Integer, Set<Integer>> 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<Map.Entry<Integer, Set<Integer>>> matchingIndexes) {
|
||||
matchingIndexes.forEach(entry -> entry.getValue().remove(aMatchingIndex));
|
||||
}
|
||||
|
||||
private Map<Integer, Set<Integer>> findMatchingIndexes(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
|
||||
Map<Integer, Set<Integer>> 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<String> errors = new LinkedList<>();
|
||||
Iterator<Map.Entry<String, JsonNode>> expectedFields = expectedJsonNode.fields();
|
||||
while (expectedFields.hasNext()) {
|
||||
Map.Entry<String, JsonNode> 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) {
|
||||
|
||||
@@ -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'")
|
||||
"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));
|
||||
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,39 @@ 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("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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"b": 2,
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"d": 4,
|
||||
"e": 5
|
||||
},
|
||||
{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"b": 2
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"b": 2
|
||||
},
|
||||
{
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"d": 4
|
||||
},
|
||||
{
|
||||
"e": 5
|
||||
},
|
||||
{
|
||||
"f": 6
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": "tyu",
|
||||
"c": 3
|
||||
},
|
||||
{
|
||||
"id": "zxy",
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"id": "123",
|
||||
"b": 2
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"a": 1
|
||||
},
|
||||
{
|
||||
"b": 2
|
||||
},
|
||||
{
|
||||
"c": 3
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user