package alien4cloud.test.utils;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import alien4cloud.utils.YamlParserUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@Slf4j
public class YamlJsonAssert {
public static class YamlJsonNotEqualsException extends RuntimeException {
private static final long serialVersionUID = -4821764413592118483L;
public YamlJsonNotEqualsException(String message, Throwable cause) {
super(message, cause);
}
public YamlJsonNotEqualsException(String message, String expected, String actual) {
super("Expected : [" + expected + "], but actual is [" + actual + "], " + message);
}
public YamlJsonNotEqualsException(String message) {
super(message);
}
}
public static enum DocumentType {
JSON, YAML
}
private static final ObjectMapper YAML_OBJECT_MAPPER = YamlParserUtil.createYamlObjectMapper();
private static final ObjectMapper JSON_OBJECT_MAPPER = newJsonObjectMapper();
private static ObjectMapper newJsonObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
return mapper;
}
public static void assertEquals(String expected, String actual, DocumentType documentType) throws IOException, YamlJsonNotEqualsException {
assertEquals(expected, actual, null, documentType);
}
public static void assertEquals(String expected, String actual, Set<String> ignoredPaths, DocumentType documentType) throws IOException,
YamlJsonNotEqualsException {
ObjectMapper mapper;
switch (documentType) {
case JSON:
mapper = JSON_OBJECT_MAPPER;
break;
case YAML:
mapper = YAML_OBJECT_MAPPER;
break;
default:
throw new IllegalArgumentException("Unsupported document type [" + documentType + "]");
}
JsonNode expectedNode = mapper.readTree(expected);
JsonNode actualNode = mapper.readTree(actual);
LinkedList<String> path = new LinkedList<String>();
path.add("/");
assertEquals(expectedNode, actualNode, path, ignoredPaths);
}
private static boolean isIgnored(String path, Set<String> ignoredPaths) {
if (ignoredPaths != null) {
for (String ignoredPath : ignoredPaths) {
if (path.matches(ignoredPath)) {
return true;
}
}
}
return false;
}
private static void fail(String message, String currentPath, Set<String> ignoredPaths) {
if (!isIgnored(currentPath, ignoredPaths)) {
throw new YamlJsonNotEqualsException(message);
} else {
log.warn(message);
}
}
private static void equals(String message, String currentPath, Set<String> ignoredPaths, String expected, String actual) {
if (!isIgnored(currentPath, ignoredPaths)) {
if (!Objects.equal(expected, actual)) {
throw new YamlJsonNotEqualsException(message, expected, actual);
}
} else {
log.warn("Ignoring value comparison for path [" + currentPath + "], expected = [" + expected + "], actual = [" + actual + "]");
}
}
private static void assertArrayEquals(JsonNode expected, JsonNode actual, LinkedList<String> path, Set<String> ignoredPaths) {
if (expected.isArray()) {
// Expected is also an array try to compare
Iterator<JsonNode> expectedElements = expected.elements();
Iterator<JsonNode> actualElements = actual.elements();
int arrayIndex = 0;
// When comparing array must respect the same order, is this too strict ?
while (actualElements.hasNext()) {
JsonNode actualElement = actualElements.next();
path.push("[" + arrayIndex + "]");
String currentPath = getPrettyPath(path);
if (expectedElements.hasNext()) {
JsonNode expectedElement = expectedElements.next();
// Compare array element
assertEquals(expectedElement, actualElement, path, ignoredPaths);
} else {
String message = buildErrorMessage(currentPath, "Actual array contains more elements than expected");
fail(message, currentPath, ignoredPaths);
}
path.pop();
arrayIndex++;
}
} else {
String currentPath = getPrettyPath(path);
String message = buildErrorMessage(currentPath, "Not expecting array at this level");
fail(message, currentPath, ignoredPaths);
}
}
private static void assertValueEquals(JsonNode expected, JsonNode actual, LinkedList<String> path, Set<String> ignoredPaths) {
if (expected.isValueNode()) {
String expectedValue = expected.asText();
String actualValue = actual.asText();
String currentPath = getPrettyPath(path);
String message = buildErrorMessage(currentPath, "Value not equals ");
if (log.isDebugEnabled()) {
log.debug(currentPath + " : Comparing value expected = [" + expectedValue + "], actual = [" + actualValue + "]");
}
equals(message, currentPath, ignoredPaths, expectedValue, actualValue);
} else {
throw new YamlJsonNotEqualsException(buildErrorMessage(getPrettyPath(path), "Expecting keys : " + Sets.newHashSet(expected.fieldNames())
+ " but not found"));
}
}
private static void assertTreeEquals(JsonNode expected, JsonNode actual, LinkedList<String> path, Set<String> ignoredPaths) {
// If we are here it means it's not an array
// Getting children name
Set<String> expectedFieldSet = Sets.newHashSet(expected.fieldNames());
Set<String> actualFieldSet = Sets.newHashSet(actual.fieldNames());
for (String actualField : actualFieldSet) {
path.push(actualField);
if (!expectedFieldSet.contains(actualField)) {
String currentPath = getPrettyPath(path);
String message = buildErrorMessage(currentPath, "Not expecting keys : [" + actualField + "]");
fail(message, currentPath, ignoredPaths);
} else {
assertEquals(expected.get(actualField), actual.get(actualField), path, ignoredPaths);
}
path.pop();
}
// Remove all actual to check if there's not found children on actual
expectedFieldSet.removeAll(actualFieldSet);
if (expectedFieldSet.size() > 0) {
for (String expectedField : expectedFieldSet) {
path.push(expectedField);
String currentPath = getPrettyPath(path);
String message = buildErrorMessage(getPrettyPath(path), "Expecting keys : " + expectedFieldSet + " but not found");
fail(message, currentPath, ignoredPaths);
path.pop();
}
}
}
private static void failNotValueNode(JsonNode expected, JsonNode actual, LinkedList<String> path, Set<String> ignoredPaths) {
// Expected is a value node, we check if all actual children is ignored
if (expected.isNull() || (expected.isTextual() && expected.asText() == null) || (expected.isTextual() && expected.asText().isEmpty())) {
// If we are here 1. expected is null or 2. expected is text and is null 3. expected is text but empty
Set<String> actualFieldSet = Sets.newHashSet(actual.fieldNames());
for (String actualField : actualFieldSet) {
path.push(actualField);
String currentPath = getPrettyPath(path);
String message = buildErrorMessage(getPrettyPath(path), "Not expecting keys : [" + actualField + " ]");
fail(message, currentPath, ignoredPaths);
path.pop();
}
} else {
// Expected is not null, it might be a real problem except if the parent path is ignored
String currentPath = getPrettyPath(path);
String message = buildErrorMessage(getPrettyPath(path), "Not expecting keys : [" + Sets.newHashSet(actual.fieldNames()) + " ]");
fail(message, currentPath, ignoredPaths);
}
}
private static void assertEquals(JsonNode expected, JsonNode actual, LinkedList<String> path, Set<String> ignoredPaths) {
// Check if it's array node
if (actual.isArray()) {
// Assert that two array are equals
assertArrayEquals(expected, actual, path, ignoredPaths);
return;
}
// Check if it's value node
if (actual.isValueNode()) {
// Assert that two values are equals
assertValueEquals(expected, actual, path, ignoredPaths);
return;
}
// Actual is not a value node, may expected be a value node, we must check that
if (expected.isValueNode()) {
// Actual is not a value node, expected is a value node it's an erroneous case
failNotValueNode(expected, actual, path, ignoredPaths);
return;
}
// If we reach here it means both are tree nodes
assertTreeEquals(expected, actual, path, ignoredPaths);
}
private static String getPrettyPath(LinkedList<String> pathStack) {
StringBuilder buffer = new StringBuilder();
boolean slashAppended = false;
for (String path : Lists.reverse(pathStack)) {
buffer.append(path);
if (!path.equals("/")) {
buffer.append('/');
// Too ugly
slashAppended = true;
}
}
if (slashAppended) {
buffer.setLength(buffer.length() - 1);
}
return buffer.toString();
}
private static String buildErrorMessage(String path, String customMessage) {
return "Current path where assertion failed '" + path + "' : " + customMessage;
}
}