From 147a6e81628e7457acaeec09132fa65079d998f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20K=C5=99e=C4=8Dan?= Date: Thu, 4 Jul 2024 19:09:38 +0200 Subject: [PATCH] #44 Normalizer (#787) --- .../net/javacrumbs/jsonunit/core/Option.java | 4 ++ .../jsonunit/core/internal/Diff.java | 19 +++-- .../jsonunit/core/internal/Normalizer.java | 69 +++++++++++++++++++ .../test/base/AbstractAssertJTest.java | 69 +++++++++++++++++++ 4 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Normalizer.java diff --git a/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/Option.java b/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/Option.java index 003bae96..1c37c2b9 100644 --- a/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/Option.java +++ b/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/Option.java @@ -50,6 +50,10 @@ public enum Option { */ IGNORING_VALUES, + /** + * Changes the exception thrown, so it's more friendly to IDE diff visualization. + */ + REPORTING_DIFFERENCE_AS_NORMALIZED_STRING, /** * Stops comparison at the first difference. Can bring performance boots to use-cases that do not need the full list of all differences. */ diff --git a/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Diff.java b/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Diff.java index 1e6da2d7..55f9ef57 100644 --- a/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Diff.java +++ b/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Diff.java @@ -21,6 +21,7 @@ import static net.javacrumbs.jsonunit.core.Option.IGNORING_EXTRA_ARRAY_ITEMS; import static net.javacrumbs.jsonunit.core.Option.IGNORING_EXTRA_FIELDS; import static net.javacrumbs.jsonunit.core.Option.IGNORING_VALUES; +import static net.javacrumbs.jsonunit.core.Option.REPORTING_DIFFERENCE_AS_NORMALIZED_STRING; import static net.javacrumbs.jsonunit.core.Option.TREATING_NULL_AS_ABSENT; import static net.javacrumbs.jsonunit.core.internal.ClassUtils.isClassPresent; import static net.javacrumbs.jsonunit.core.internal.DifferenceContextImpl.differenceContext; @@ -34,6 +35,7 @@ import static net.javacrumbs.jsonunit.core.internal.JsonUtils.quoteIfNeeded; import static net.javacrumbs.jsonunit.core.internal.Node.KeyValue; import static net.javacrumbs.jsonunit.core.internal.Node.NodeType; +import static net.javacrumbs.jsonunit.core.internal.Normalizer.toNormalizedString; import java.math.BigDecimal; import java.util.ArrayList; @@ -55,6 +57,7 @@ import net.javacrumbs.jsonunit.core.internal.ArrayComparison.ComparisonResult; import net.javacrumbs.jsonunit.core.internal.ArrayComparison.NodeWithIndex; import net.javacrumbs.jsonunit.core.listener.Difference; +import org.opentest4j.AssertionFailedError; /** * Compares JSON structures. Mainly for internal use, the API might be more volatile than the rest. @@ -684,9 +687,6 @@ private void logDifferences() { /** * Returns children of an ObjectNode. - * - * @param node - * @return */ private static Map getFields(Node node) { Map result = new HashMap<>(); @@ -720,7 +720,18 @@ public void failIfDifferent() { public void failIfDifferent(String message) { if (!similar()) { - throw createException(message, differences); + if (!configuration.getOptions().contains(REPORTING_DIFFERENCE_AS_NORMALIZED_STRING) + || actualRoot.isMissingNode()) { + throw createException(message, differences); + } else { + String normalizedExpected = toNormalizedString(expectedRoot); + String normalizedActual = toNormalizedString(actualRoot); + throw new AssertionFailedError( + "JSON documents are different: expected <" + normalizedExpected + ">" + "but was <" + + normalizedActual + ">", + normalizedExpected, + normalizedActual); + } } } diff --git a/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Normalizer.java b/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Normalizer.java new file mode 100644 index 00000000..728ebbde --- /dev/null +++ b/json-unit-core/src/main/java/net/javacrumbs/jsonunit/core/internal/Normalizer.java @@ -0,0 +1,69 @@ +package net.javacrumbs.jsonunit.core.internal; + +import static java.util.Comparator.comparing; + +import java.util.Iterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import net.javacrumbs.jsonunit.core.internal.Node.KeyValue; + +class Normalizer { + + private static int depth = 2; + + static String toNormalizedString(Node node) { + StringBuilder sb = new StringBuilder(); + normalize(node, sb, 0); + return sb.toString(); + } + + private static void normalize(Node node, StringBuilder sb, int indent) { + switch (node.getNodeType()) { + case OBJECT -> normalizeObject(node, sb, indent); + case ARRAY -> normalizeArray(node, sb, indent); + case STRING -> sb.append('\"').append(node).append('"'); + default -> sb.append(node); + } + } + + private static void normalizeArray(Node node, StringBuilder sb, int indent) { + sb.append("[\n"); + Iterator elements = node.arrayElements(); + while (elements.hasNext()) { + var element = elements.next(); + addIndent(sb, indent + depth); + normalize(element, sb, indent + depth); + if (elements.hasNext()) sb.append(","); + sb.append('\n'); + } + addIndent(sb, indent); + sb.append("]"); + } + + private static void normalizeObject(Node node, StringBuilder sb, int indent) { + sb.append("{\n"); + Iterator sortedValues = + stream(node.fields()).sorted(comparing(KeyValue::getKey)).iterator(); + while (sortedValues.hasNext()) { + var keyValue = sortedValues.next(); + addIndent(sb, indent + depth); + sb.append('"').append(keyValue.getKey()).append("\": "); + normalize(keyValue.getValue(), sb, indent + depth); + if (sortedValues.hasNext()) sb.append(","); + sb.append('\n'); + } + addIndent(sb, indent); + sb.append("}"); + } + + private static void addIndent(StringBuilder sb, int indent) { + for (int i = 0; i < indent; i++) { + sb.append(' '); + } + } + + private static Stream stream(Iterator iterator) { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); + } +} diff --git a/tests/test-base/src/main/java/net/javacrumbs/jsonunit/test/base/AbstractAssertJTest.java b/tests/test-base/src/main/java/net/javacrumbs/jsonunit/test/base/AbstractAssertJTest.java index 6bcae888..e11cbb35 100644 --- a/tests/test-base/src/main/java/net/javacrumbs/jsonunit/test/base/AbstractAssertJTest.java +++ b/tests/test-base/src/main/java/net/javacrumbs/jsonunit/test/base/AbstractAssertJTest.java @@ -32,6 +32,7 @@ import static net.javacrumbs.jsonunit.core.Option.IGNORING_EXTRA_ARRAY_ITEMS; import static net.javacrumbs.jsonunit.core.Option.IGNORING_EXTRA_FIELDS; import static net.javacrumbs.jsonunit.core.Option.IGNORING_VALUES; +import static net.javacrumbs.jsonunit.core.Option.REPORTING_DIFFERENCE_AS_NORMALIZED_STRING; import static net.javacrumbs.jsonunit.core.Option.TREATING_NULL_AS_ABSENT; import static net.javacrumbs.jsonunit.core.internal.JsonUtils.jsonSource; import static net.javacrumbs.jsonunit.test.base.RegexBuilder.regex; @@ -54,6 +55,7 @@ import net.javacrumbs.jsonunit.test.base.AbstractJsonAssertTest.DivisionMatcher; import org.hamcrest.Matcher; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; import org.opentest4j.MultipleFailuresError; @@ -2311,6 +2313,73 @@ void shouldFailFast() { """); } + @Nested + class ReportAsString { + @Test + void shouldSortMapKeys() { + assertThatThrownBy(() -> assertThatJson("{\"a\": {\"c\": [{\"e\": 2}, 3]}, \"b\": false}") + .when(REPORTING_DIFFERENCE_AS_NORMALIZED_STRING) + .isEqualTo("{\"b\": true, \"a\": {\"c\": [{\"e\": 3}, 5]}}")) + .hasMessage( + """ + JSON documents are different: expected <{ + "a": { + "c": [ + { + "e": 3 + }, + 5 + ] + }, + "b": true + }>but was <{ + "a": { + "c": [ + { + "e": 2 + }, + 3 + ] + }, + "b": false + }>"""); + } + + @Test + void shouldWorkWithMissingPath() { + assertThatThrownBy(() -> assertThatJson("{\"a\": 1}") + .when(REPORTING_DIFFERENCE_AS_NORMALIZED_STRING) + .inPath("c") + .isEqualTo("{\"b\": true}")) + .hasMessage( + """ + JSON documents are different: + Missing node in path "c". + """); + } + + @Test + void shouldWorkWithPaths() { + assertThatThrownBy(() -> assertThatJson("{\"a\": {\"c\": [{\"e\": 2}, 3]}, \"b\": false}") + .when(REPORTING_DIFFERENCE_AS_NORMALIZED_STRING) + .inPath("a.c") + .isEqualTo("[{\"e\": 3}, 5]")) + .hasMessage( + """ + JSON documents are different: expected <[ + { + "e": 3 + }, + 5 + ]>but was <[ + { + "e": 2 + }, + 3 + ]>"""); + } + } + private static final String json = """ {