|
1 | 1 | package io.cucumber.compatibility; |
2 | 2 |
|
| 3 | +import com.fasterxml.jackson.core.JsonPointer; |
3 | 4 | import com.fasterxml.jackson.databind.JsonNode; |
4 | 5 | import com.fasterxml.jackson.databind.node.ArrayNode; |
5 | | -import com.fasterxml.jackson.databind.node.BooleanNode; |
6 | | -import com.fasterxml.jackson.databind.node.NumericNode; |
7 | 6 | import com.fasterxml.jackson.databind.node.ObjectNode; |
8 | | -import com.fasterxml.jackson.databind.node.TextNode; |
9 | 7 | import org.hamcrest.CoreMatchers; |
10 | 8 | import org.hamcrest.Description; |
11 | 9 | import org.hamcrest.Matcher; |
12 | 10 | import org.hamcrest.TypeSafeDiagnosingMatcher; |
13 | 11 |
|
14 | | -import java.util.ArrayList; |
| 12 | +import java.util.Collections; |
15 | 13 | import java.util.LinkedHashMap; |
16 | | -import java.util.List; |
17 | 14 | import java.util.Map; |
18 | | -import java.util.Spliterator; |
19 | | -import java.util.stream.Collectors; |
20 | | - |
21 | | -import static java.util.Spliterators.spliteratorUnknownSize; |
22 | | -import static java.util.stream.StreamSupport.stream; |
23 | | -import static org.hamcrest.CoreMatchers.anyOf; |
24 | | -import static org.hamcrest.CoreMatchers.is; |
25 | | -import static org.hamcrest.CoreMatchers.isA; |
26 | | -import static org.hamcrest.CoreMatchers.not; |
27 | | -import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; |
28 | | -import static org.hamcrest.collection.IsIterableContainingInOrder.contains; |
29 | | -import static org.hamcrest.collection.IsIterableContainingInRelativeOrder.containsInRelativeOrder; |
30 | | -import static org.hamcrest.collection.IsMapContaining.hasEntry; |
31 | | -import static org.hamcrest.collection.IsMapContaining.hasKey; |
32 | | - |
33 | | -public class AComparableMessage extends |
34 | | - TypeSafeDiagnosingMatcher<JsonNode> { |
35 | | - |
36 | | - private final List<Matcher<?>> expectedFields; |
37 | | - private final int depth; |
38 | | - |
39 | | - public AComparableMessage(String messageType, JsonNode expectedMessage) { |
40 | | - this(messageType, expectedMessage, 0); |
41 | | - } |
| 15 | +import java.util.regex.Pattern; |
| 16 | + |
| 17 | +import static java.util.Objects.requireNonNull; |
| 18 | + |
| 19 | +public class AComparableMessage extends TypeSafeDiagnosingMatcher<JsonNode> { |
42 | 20 |
|
43 | | - AComparableMessage(String messageType, JsonNode expectedMessage, int depth) { |
44 | | - this.depth = depth + 1; |
45 | | - this.expectedFields = extractExpectedFields(messageType, expectedMessage, this.depth); |
| 21 | + private final JsonNode expectedMessage; |
| 22 | + private final String messageType; |
| 23 | + private final Map<Pattern, Matcher<?>> replacements; |
| 24 | + private final Map<JsonPointer, JsonNode> expectedFields; |
| 25 | + private final Map<JsonPointer, Matcher<?>> expectedMatchers; |
| 26 | + |
| 27 | + public AComparableMessage(String messageType, JsonNode expectedMessage, Map<Pattern, Matcher<?>> replacements) { |
| 28 | + this.expectedMessage = expectedMessage; |
| 29 | + this.messageType = requireNonNull(messageType); |
| 30 | + this.replacements = requireNonNull(replacements); |
| 31 | + this.expectedFields = extractFieldsAndPointers(requireNonNull(expectedMessage)); |
| 32 | + this.expectedMatchers = createMatchers(expectedFields); |
46 | 33 | } |
47 | 34 |
|
48 | | - private static List<Matcher<?>> extractExpectedFields(String messageType, JsonNode expectedMessage, int depth) { |
49 | | - List<Matcher<?>> expected = new ArrayList<>(); |
50 | | - asMapOfJsonNameToField(expectedMessage).forEach((fieldName, expectedValue) -> { |
51 | | - switch (fieldName) { |
52 | | - // exception: error messages are platform specific |
53 | | - case "exception": |
54 | | - case "message": |
55 | | - expected.add(hasEntry(is(fieldName), isA(expectedValue.getClass()))); |
56 | | - expected.add(hasEntry(is(fieldName), isA(expectedValue.getClass()))); |
57 | | - break; |
58 | | - |
59 | | - // exception: the CCK uses relative paths as uris |
60 | | - case "uri": |
61 | | - expected.add(hasEntry(is(fieldName), isA(expectedValue.getClass()))); |
62 | | - break; |
63 | | - |
64 | | - // exception: the CCK expects source references with URIs but |
65 | | - // Java can only provide method and stack trace references. |
66 | | - case "sourceReference": |
67 | | - expected.add(hasKey(is(fieldName))); |
68 | | - break; |
69 | | - |
70 | | - // exception: ids are not predictable |
71 | | - case "id": |
72 | | - // exception: not yet implemented |
73 | | - if ("testRunStarted".equals(messageType)) { |
74 | | - expected.add(not(hasKey(fieldName))); |
75 | | - break; |
76 | | - } |
77 | | - case "pickleId": |
78 | | - case "astNodeId": |
79 | | - case "hookId": |
80 | | - case "pickleStepId": |
81 | | - case "testCaseId": |
82 | | - case "testStepId": |
83 | | - case "testCaseStartedId": |
84 | | - expected.add(hasEntry(is(fieldName), isA(TextNode.class))); |
85 | | - break; |
86 | | - // exception: not yet implemented |
87 | | - case "testRunStartedId": |
88 | | - expected.add(not(hasKey(fieldName))); |
89 | | - break; |
90 | | - // exception: protocolVersion can vary |
91 | | - case "protocolVersion": |
92 | | - expected.add(hasEntry(is(fieldName), isA(TextNode.class))); |
93 | | - break; |
94 | | - case "astNodeIds": |
95 | | - case "stepDefinitionIds": |
96 | | - if (expectedValue instanceof ArrayNode) { |
97 | | - ArrayNode expectedValues = (ArrayNode) expectedValue; |
98 | | - if (expectedValues.isEmpty()) { |
99 | | - expected.add(hasEntry(is(fieldName), emptyIterable())); |
100 | | - } else { |
101 | | - expected.add(hasEntry(is(fieldName), containsInRelativeOrder(isA(TextNode.class)))); |
102 | | - } |
103 | | - break; |
104 | | - } |
105 | | - // exception: timestamps and durations are not predictable |
106 | | - case "timestamp": |
107 | | - case "duration": |
108 | | - expected.add(hasEntry(is(fieldName), isA(expectedValue.getClass()))); |
109 | | - break; |
110 | | - |
111 | | - // exception: Mata fields depend on the platform |
112 | | - case "implementation": |
113 | | - case "runtime": |
114 | | - case "os": |
115 | | - case "cpu": |
116 | | - expected.add(hasEntry(is(fieldName), isA(expectedValue.getClass()))); |
117 | | - break; |
118 | | - case "ci": |
119 | | - // exception: Absent when running locally, present in ci |
120 | | - expected.add( |
121 | | - anyOf(not(hasKey(is(fieldName))), hasEntry(is(fieldName), |
122 | | - isA(expectedValue.getClass())))); |
123 | | - break; |
124 | | - default: |
125 | | - expected.add(hasEntry(is(fieldName), aComparableValue(messageType, |
126 | | - expectedValue, |
127 | | - depth))); |
128 | | - } |
| 35 | + private Map<JsonPointer, Matcher<?>> createMatchers(Map<JsonPointer, JsonNode> expectedFields) { |
| 36 | + Map<JsonPointer, Matcher<?>> expectedMatchers = new LinkedHashMap<>(); |
| 37 | + expectedFields.forEach((jsonPointer, node) -> { |
| 38 | + Matcher<JsonNode> defaultValue = CoreMatchers.equalTo(node); |
| 39 | + expectedMatchers.put(jsonPointer, findReplacement(jsonPointer, defaultValue)); |
129 | 40 | }); |
130 | | - return expected; |
| 41 | + return expectedMatchers; |
131 | 42 | } |
132 | 43 |
|
133 | | - @SuppressWarnings("unchecked") |
134 | | - private static Matcher<?> aComparableValue(String messageType, Object value, int depth) { |
135 | | - if (value instanceof ObjectNode) { |
136 | | - JsonNode message = (JsonNode) value; |
137 | | - return new AComparableMessage(messageType, message, depth); |
138 | | - } |
139 | | - |
140 | | - if (value instanceof ArrayNode) { |
141 | | - ArrayNode values = (ArrayNode) value; |
142 | | - Spliterator<JsonNode> spliterator = spliteratorUnknownSize(values.iterator(), 0); |
143 | | - List<Matcher<? super Object>> allComparableValues = stream(spliterator, false) |
144 | | - .map(o -> aComparableValue(messageType, o, depth)) |
145 | | - .map(o -> (Matcher<? super Object>) o) |
146 | | - .collect(Collectors.toList()); |
147 | | - if (allComparableValues.isEmpty()) { |
148 | | - return emptyIterable(); |
| 44 | + private Matcher<?> findReplacement(JsonPointer jsonPointer, Matcher<JsonNode> defaultValue) { |
| 45 | + for (Map.Entry<Pattern, Matcher<?>> entry : replacements.entrySet()) { |
| 46 | + if (entry.getKey().matcher(jsonPointer.toString()).matches()) { |
| 47 | + return entry.getValue(); |
149 | 48 | } |
150 | | - return contains(allComparableValues); |
151 | 49 | } |
| 50 | + return defaultValue; |
| 51 | + } |
| 52 | + |
| 53 | + private Map<JsonPointer, JsonNode> extractFieldsAndPointers(JsonNode node) { |
| 54 | + JsonPointer path = JsonPointer.empty(); |
| 55 | + return extractFieldsAndPointers(path, node); |
| 56 | + } |
152 | 57 |
|
153 | | - if (value instanceof TextNode |
154 | | - || value instanceof NumericNode |
155 | | - || value instanceof BooleanNode) { |
156 | | - return CoreMatchers.is(value); |
| 58 | + private Map<JsonPointer, JsonNode> extractFieldsAndPointers(JsonPointer path, JsonNode node) { |
| 59 | + if (node instanceof ObjectNode) { |
| 60 | + return extractFieldsAndPointers(path, (ObjectNode) node); |
157 | 61 | } |
158 | | - throw new IllegalArgumentException("Unsupported type " + value.getClass() + |
159 | | - ": " + value); |
| 62 | + if (node instanceof ArrayNode) { |
| 63 | + return extractFieldsAndPointers(path, (ArrayNode) node); |
| 64 | + } |
| 65 | + return Collections.singletonMap(path, node); |
160 | 66 | } |
161 | 67 |
|
162 | | - @Override |
163 | | - public void describeTo(Description description) { |
164 | | - StringBuilder padding = new StringBuilder(); |
165 | | - for (int i = 0; i < depth + 1; i++) { |
166 | | - padding.append("\t"); |
| 68 | + private Map<JsonPointer, JsonNode> extractFieldsAndPointers(JsonPointer path, ObjectNode node) { |
| 69 | + Map<JsonPointer, JsonNode> expectedFields = new LinkedHashMap<>(); |
| 70 | + node.fieldNames().forEachRemaining(fieldName -> { |
| 71 | + JsonNode field = node.get(fieldName); |
| 72 | + JsonPointer fieldPath = path.appendProperty(fieldName); |
| 73 | + expectedFields.putAll(extractFieldsAndPointers(fieldPath, field)); |
| 74 | + }); |
| 75 | + return expectedFields; |
| 76 | + } |
| 77 | + |
| 78 | + private Map<JsonPointer, JsonNode> extractFieldsAndPointers(JsonPointer path, ArrayNode node) { |
| 79 | + Map<JsonPointer, JsonNode> expectedFields = new LinkedHashMap<>(); |
| 80 | + for (int i = 0, size = node.size(); i < size; i++) { |
| 81 | + JsonNode element = node.get(i); |
| 82 | + JsonPointer elementPath = path.appendIndex(i); |
| 83 | + expectedFields.putAll(extractFieldsAndPointers(elementPath, element)); |
167 | 84 | } |
168 | | - description.appendList("\n" + padding, ",\n" + padding, |
169 | | - "\n", expectedFields); |
| 85 | + return expectedFields; |
170 | 86 | } |
171 | 87 |
|
172 | 88 | @Override |
173 | | - protected boolean matchesSafely(JsonNode actual, Description mismatchDescription) { |
174 | | - Map<String, Object> actualFields = asMapOfJsonNameToField(actual); |
175 | | - for (Matcher<?> expectedField : expectedFields) { |
176 | | - if (!expectedField.matches(actualFields)) { |
177 | | - expectedField.describeMismatch(actualFields, mismatchDescription); |
| 89 | + protected boolean matchesSafely(JsonNode item, Description mismatchDescription) { |
| 90 | + for (Map.Entry<JsonPointer, Matcher<?>> entry : expectedMatchers.entrySet()) { |
| 91 | + JsonPointer pointer = entry.getKey(); |
| 92 | + Matcher<?> expected = entry.getValue(); |
| 93 | + JsonNode actual = item.at(pointer); |
| 94 | + |
| 95 | + if (!expected.matches(actual)) { |
| 96 | + mismatchDescription |
| 97 | + .appendText(pointer.toString()).appendText(" ") |
| 98 | + .appendText(actual.toString()).appendText(" "); |
| 99 | + // Copy and paste needed to suppress this finding. |
| 100 | + // System.out.printf("%s.put(Pattern.compile(\"%s\"), |
| 101 | + // isA(%s.class));%n", messageType, key, |
| 102 | + // actual.getClass().getSimpleName()); |
178 | 103 | return false; |
179 | 104 | } |
180 | 105 | } |
181 | 106 | return true; |
182 | 107 | } |
183 | 108 |
|
184 | | - private static Map<String, Object> asMapOfJsonNameToField(JsonNode envelope) { |
185 | | - Map<String, Object> map = new LinkedHashMap<>(); |
186 | | - envelope.fieldNames() |
187 | | - .forEachRemaining(jsonField -> { |
188 | | - JsonNode value = envelope.get(jsonField); |
189 | | - map.put(jsonField, value); |
190 | | - }); |
191 | | - return map; |
| 109 | + @Override |
| 110 | + public void describeTo(Description description) { |
| 111 | + description.appendValue(expectedMessage); |
192 | 112 | } |
193 | 113 |
|
| 114 | + @Override |
| 115 | + public String toString() { |
| 116 | + return "AComparableMessage{" + |
| 117 | + "expectedMessage=" + expectedMessage + |
| 118 | + ", messageType='" + messageType + '\'' + |
| 119 | + ", replacements=" + replacements + |
| 120 | + ", expectedFields=" + expectedFields + |
| 121 | + ", expectedMatchers=" + expectedMatchers + |
| 122 | + '}'; |
| 123 | + } |
194 | 124 | } |
0 commit comments