Skip to content

Commit 013c21b

Browse files
viclovskyArtem Eroshenko
authored andcommitted
add JsonUnit integration (via allure-framework#236)
1 parent 48a2f51 commit 013c21b

16 files changed

Lines changed: 1075 additions & 5 deletions

File tree

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@ Usage example:
8989
```
9090
.filter(new AllureRestAssured())
9191
```
92-
You can specify custom templateName:
92+
You can specify custom templates, which should be placed in src/main/resources/tpl folder:
9393
```
94-
.filter(new AllureRestAssured().withTemplate("/templates/custom_template.ftl"))
94+
.filter(new AllureRestAssured()
95+
.withRequestTemplate("custom-http-request.ftl")
96+
.withResponseTemplate("custom-http-response.ftl"))
9597
```
9698

9799
## OkHttp
@@ -136,3 +138,13 @@ Usage example:
136138
.addInterceptorLast(new AllureHttpClientResponse());
137139
```
138140

141+
## JsonUnit
142+
JsonPatchMatcher is extension of JsonUnit matcher, that generates pretty html attachment for differences based on [json diff patch](https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md).
143+
144+
```xml
145+
<dependency>
146+
<groupId>io.qameta.allure</groupId>
147+
<artifactId>allure-jsonunit</artifactId>
148+
<version>$LATEST_VERSION</version>
149+
</dependency>
150+
```

allure-jsonunit/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
description = 'Allure JsonUnit'
2+
3+
apply from: "${gradleScriptDir}/maven-publish.gradle"
4+
apply from: "${gradleScriptDir}/bintray.gradle"
5+
apply plugin: 'maven'
6+
7+
dependencies {
8+
compile project(':allure-attachments')
9+
compile('net.javacrumbs.json-unit:json-unit:2.0.0.RC1')
10+
11+
testCompile('junit:junit')
12+
testCompile('org.hamcrest:hamcrest-library')
13+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.qameta.allure.jsonunit;
2+
3+
import net.javacrumbs.jsonunit.core.Option;
4+
import net.javacrumbs.jsonunit.core.internal.Options;
5+
import org.hamcrest.Matcher;
6+
7+
import java.math.BigDecimal;
8+
9+
/**
10+
* @param <T> the type of matcher
11+
* @see net.javacrumbs.jsonunit.ConfigurableJsonMatcher
12+
*/
13+
public interface AllureConfigurableJsonMatcher<T> extends Matcher<T> {
14+
15+
AllureConfigurableJsonMatcher<T> withTolerance(BigDecimal tolerance);
16+
17+
AllureConfigurableJsonMatcher<T> withTolerance(double tolerance);
18+
19+
AllureConfigurableJsonMatcher<T> when(Option first, Option... next);
20+
21+
AllureConfigurableJsonMatcher<T> withOptions(Options options);
22+
23+
AllureConfigurableJsonMatcher<T> withMatcher(String matcherName, Matcher<?> matcher);
24+
25+
AllureConfigurableJsonMatcher<T> whenIgnoringPaths(String... paths);
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.qameta.allure.jsonunit;
2+
3+
import io.qameta.allure.attachment.AttachmentData;
4+
5+
/**
6+
* @author Victor Orlovsky
7+
*/
8+
public class DiffAttachment implements AttachmentData {
9+
10+
private final String patch;
11+
private final String actual;
12+
private final String expected;
13+
14+
public DiffAttachment(final String actual, final String expected, final String patch) {
15+
this.actual = actual;
16+
this.expected = expected;
17+
this.patch = patch;
18+
}
19+
20+
public String getPatch() {
21+
return patch;
22+
}
23+
24+
public String getActual() {
25+
return actual;
26+
}
27+
28+
public String getExpected() {
29+
return expected;
30+
}
31+
32+
@Override
33+
public String getName() {
34+
return "JSON difference";
35+
}
36+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package io.qameta.allure.jsonunit;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import net.javacrumbs.jsonunit.core.listener.Difference;
6+
import net.javacrumbs.jsonunit.core.listener.DifferenceContext;
7+
import net.javacrumbs.jsonunit.core.listener.DifferenceListener;
8+
import org.apache.commons.lang3.StringUtils;
9+
10+
import java.util.ArrayList;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
/**
16+
* JsonUnit listener, that keeps difference and
17+
* return formatted json to represent deltas
18+
* (i.e. the output of jsondiffpatch.diff).
19+
*/
20+
public class JsonPatchListener implements DifferenceListener {
21+
22+
private static final String UNKNOWN_TYPE_ERROR = "Difference has unknown type";
23+
private final List<Difference> differences = new ArrayList<>();
24+
25+
private DifferenceContext context;
26+
27+
@Override
28+
public void diff(final Difference difference, final DifferenceContext differenceContext) {
29+
this.context = differenceContext;
30+
differences.add(difference);
31+
}
32+
33+
public List<Difference> getDifferences() {
34+
return differences;
35+
}
36+
37+
public DifferenceContext getContext() {
38+
return context;
39+
}
40+
41+
@SuppressWarnings("ReturnCount")
42+
private String getPath(final Difference difference) {
43+
switch (difference.getType()) {
44+
case DIFFERENT:
45+
return difference.getActualPath();
46+
47+
case MISSING:
48+
return difference.getExpectedPath();
49+
50+
case EXTRA:
51+
return difference.getActualPath();
52+
53+
default:
54+
throw new IllegalArgumentException(UNKNOWN_TYPE_ERROR);
55+
}
56+
}
57+
58+
@SuppressWarnings("ReturnCount")
59+
private List<Object> getPatch(final Difference difference) {
60+
final List<Object> result = new ArrayList<>();
61+
62+
switch (difference.getType()) {
63+
case DIFFERENT:
64+
result.add(difference.getExpected());
65+
result.add(difference.getActual());
66+
return result;
67+
68+
case MISSING:
69+
result.add(difference.getExpected());
70+
result.add(0);
71+
result.add(0);
72+
return result;
73+
74+
case EXTRA:
75+
result.add(difference.getActual());
76+
return result;
77+
78+
default:
79+
throw new IllegalArgumentException(UNKNOWN_TYPE_ERROR);
80+
}
81+
}
82+
83+
@SuppressWarnings({"all", "unchecked"})
84+
public String getJsonPatch() {
85+
final Map<String, Object> jsonDiffPatch = new HashMap<>();
86+
// take care of corner case when two jsons absolutelly different
87+
if (getDifferences().size() == 1) {
88+
final Difference difference = getDifferences().get(0);
89+
final String field = getPath(difference);
90+
if (field.isEmpty()) {
91+
final ObjectMapper mapper = new ObjectMapper();
92+
try {
93+
return mapper.writeValueAsString(getPatch(difference));
94+
} catch (JsonProcessingException e) {
95+
throw new IllegalStateException("Could not process patch json", e);
96+
}
97+
}
98+
}
99+
getDifferences().forEach(difference -> {
100+
101+
final String field = getPath(difference);
102+
Map<String, Object> currentMap = jsonDiffPatch;
103+
104+
final String fieldWithDots = StringUtils.replace(field, "[", ".");
105+
final int len = fieldWithDots.length();
106+
int left = 0;
107+
int right = 0;
108+
while (left < len) {
109+
right = fieldWithDots.indexOf('.', left);
110+
if (right == -1) {
111+
right = len;
112+
}
113+
String fieldName = fieldWithDots.substring(left, right);
114+
fieldName = StringUtils.remove(fieldName, "]");
115+
116+
if (right != len) {
117+
if (!fieldName.isEmpty()) {
118+
if (!currentMap.containsKey(fieldName)) {
119+
currentMap.put(fieldName, new HashMap<>());
120+
}
121+
currentMap = (Map<String, Object>) currentMap.get(fieldName);
122+
}
123+
124+
if (field.charAt(right) == '[') {
125+
if (!currentMap.containsKey(fieldName)) {
126+
currentMap.put("_t", "a");
127+
}
128+
}
129+
} else {
130+
final List<?> actualExpectedValue = getPatch(difference);
131+
currentMap.put(fieldName, actualExpectedValue);
132+
}
133+
left = right + 1;
134+
}
135+
});
136+
137+
final ObjectMapper mapper = new ObjectMapper();
138+
try {
139+
return mapper.writeValueAsString(jsonDiffPatch);
140+
} catch (JsonProcessingException e) {
141+
throw new IllegalStateException("Could not process patch json", e);
142+
}
143+
}
144+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.qameta.allure.jsonunit;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.qameta.allure.attachment.DefaultAttachmentProcessor;
6+
import io.qameta.allure.attachment.FreemarkerAttachmentRenderer;
7+
import net.javacrumbs.jsonunit.core.Configuration;
8+
import net.javacrumbs.jsonunit.core.Option;
9+
import net.javacrumbs.jsonunit.core.internal.Diff;
10+
import net.javacrumbs.jsonunit.core.internal.Options;
11+
import org.hamcrest.Description;
12+
import org.hamcrest.Matcher;
13+
14+
import java.math.BigDecimal;
15+
16+
/**
17+
* JsonPatchMatcher is extension of JsonUnit matcher,
18+
* that generates pretty html attachment for differences.
19+
* @param <T> the type
20+
*/
21+
public final class JsonPatchMatcher<T> implements AllureConfigurableJsonMatcher<T> {
22+
private static final String EMPTY_PATH = "";
23+
private static final String FULL_JSON = "fullJson";
24+
private Configuration configuration = Configuration.empty();
25+
private final Object expected;
26+
private String differences;
27+
28+
private JsonPatchMatcher(final Object expected) {
29+
this.expected = expected;
30+
}
31+
32+
public static <T> AllureConfigurableJsonMatcher<T> jsonEquals(final Object expected) {
33+
return new JsonPatchMatcher<T>(expected);
34+
}
35+
36+
public AllureConfigurableJsonMatcher<T> withTolerance(final BigDecimal tolerance) {
37+
configuration = configuration.withTolerance(tolerance);
38+
return this;
39+
}
40+
41+
public AllureConfigurableJsonMatcher<T> withTolerance(final double tolerance) {
42+
configuration = configuration.withTolerance(tolerance);
43+
return this;
44+
}
45+
46+
public AllureConfigurableJsonMatcher<T> when(final Option first, final Option... next) {
47+
configuration = configuration.when(first, next);
48+
return this;
49+
}
50+
51+
public AllureConfigurableJsonMatcher<T> withOptions(final Options options) {
52+
configuration = configuration.withOptions(options);
53+
return this;
54+
}
55+
56+
public AllureConfigurableJsonMatcher<T> withMatcher(final String matcherName, final Matcher matcher) {
57+
configuration = configuration.withMatcher(matcherName, matcher);
58+
return this;
59+
}
60+
61+
public AllureConfigurableJsonMatcher<T> whenIgnoringPaths(final String... paths) {
62+
configuration = configuration.whenIgnoringPaths(paths);
63+
return this;
64+
}
65+
66+
private void render(final JsonPatchListener listener) {
67+
final ObjectMapper mapper = new ObjectMapper();
68+
final String patch = listener.getJsonPatch();
69+
try {
70+
final String actual = mapper.writeValueAsString(listener.getContext().getActualSource());
71+
final String expected = mapper.writeValueAsString(listener.getContext().getExpectedSource());
72+
final DiffAttachment attachment = new DiffAttachment(actual, expected, patch);
73+
new DefaultAttachmentProcessor().addAttachment(attachment,
74+
new FreemarkerAttachmentRenderer("diff.ftl"));
75+
} catch (JsonProcessingException e) {
76+
throw new IllegalStateException("Could not process actual/expected json", e);
77+
}
78+
}
79+
80+
@Override
81+
public boolean matches(final Object actual) {
82+
final JsonPatchListener listener = new JsonPatchListener();
83+
final Diff diff = Diff.create(expected, actual, FULL_JSON, EMPTY_PATH,
84+
configuration.withDifferenceListener(listener));
85+
if (!diff.similar()) {
86+
differences = diff.differences();
87+
render(listener);
88+
}
89+
return diff.similar();
90+
}
91+
92+
public void describeTo(final Description description) {
93+
description.appendText("has no difference");
94+
}
95+
96+
@Override
97+
public void describeMismatch(final Object item, final Description description) {
98+
description.appendText(differences);
99+
}
100+
101+
@SuppressWarnings("deprecation")
102+
@Override
103+
public void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
104+
//do nothing
105+
}
106+
}

allure-jsonunit/src/main/resources/tpl/diff.ftl

Lines changed: 171 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)