Skip to content

Commit 54ff493

Browse files
matchers: display map missing keys inline (#1523)
1 parent 6462a23 commit 54ff493

16 files changed

Lines changed: 328 additions & 30 deletions

File tree

webtau-core-groovy/src/test/groovy/org/testingisdocumenting/webtau/data/table/TableDataGroovyTest.groovy

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package org.testingisdocumenting.webtau.data.table
1919

2020
import org.junit.Test
21-
import org.testingisdocumenting.webtau.TestListeners
2221

2322
import java.time.LocalDate
2423

@@ -99,6 +98,16 @@ class TableDataGroovyTest {
9998
48 | null }
10099
}
101100

101+
// @Test
102+
// void "matching a single value inside"() {
103+
// def table = ["col A" | "col B" | "col C"] {
104+
// _____________________________
105+
// 10 | "hello" | "world"
106+
// 20 | "next" | "event" }
107+
//
108+
// table.should contain("hello")
109+
// }
110+
102111
@Test
103112
void "should generate multiple rows from multi-values"() {
104113
def tableData = createTableDataWithPermute()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2023 webtau maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.webtau.expectation.equality.handlers
18+
19+
import org.junit.Test
20+
21+
import static org.testingisdocumenting.webtau.WebTauCore.*
22+
import static org.testingisdocumenting.webtau.testutils.TestConsoleOutput.*
23+
24+
class MapMatchersGroovyExamplesTest {
25+
@Test
26+
void equalityMismatch() {
27+
code {
28+
// maps-equal-mismatch
29+
Map<String, ?> generated = generate()
30+
generated.should == [firstName: "G-FN", lastName: "G-LN",
31+
address: [street: "generated-street", city: "GenCity", zipCode: "12345"]]
32+
// maps-equal-mismatch
33+
} should throwException(AssertionError)
34+
}
35+
36+
private static Map<String, ?> generate() {
37+
return map("firstName", "G-FN", "lastName", "G-LN",
38+
"address", map("street", "generated-street", "city", "GenSity"))
39+
}
40+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2023 webtau maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.webtau;
18+
19+
import org.testingisdocumenting.webtau.data.render.PrettyPrintable;
20+
import org.testingisdocumenting.webtau.data.render.PrettyPrinter;
21+
22+
public class MissingEntryPlaceholder implements PrettyPrintable {
23+
public static final MissingEntryPlaceholder ENTRY = new MissingEntryPlaceholder();
24+
25+
@Override
26+
public void prettyPrint(PrettyPrinter printer) {
27+
printer.print(PrettyPrinter.DANGER_COLOR, "<missing>");
28+
}
29+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2023 webtau maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.webtau.data;
18+
19+
import org.testingisdocumenting.webtau.MissingEntryPlaceholder;
20+
21+
import java.util.*;
22+
23+
public class MapWithTrackedMissingKeys extends LinkedHashMap<Object, Object> {
24+
private final Set<Object> missingKeys = new LinkedHashSet<>();
25+
26+
public MapWithTrackedMissingKeys(Map<?, ?> original) {
27+
super(convertNestedMaps(original));
28+
}
29+
30+
@Override
31+
public boolean containsKey(Object key) {
32+
boolean contains = super.containsKey(key);
33+
if (!contains) {
34+
missingKeys.add(key);
35+
}
36+
37+
return contains;
38+
}
39+
40+
@Override
41+
public Set<Map.Entry<Object, Object>> entrySet() {
42+
LinkedHashSet<Map.Entry<Object, Object>> result = new LinkedHashSet<>();
43+
44+
super.forEach((key, value) -> result.add(new SimpleEntry<>(key, wrapValueIfRequired(value))));
45+
46+
missingKeys.forEach(key -> result.add(new SimpleEntry<>(key, MissingEntryPlaceholder.ENTRY)));
47+
48+
return result;
49+
}
50+
51+
@Override
52+
public int size() {
53+
return super.size() + missingKeys.size();
54+
}
55+
56+
private static Map<?, ?> convertNestedMaps(Map<?, ?> original) {
57+
Map<Object, Object> result = new LinkedHashMap<>();
58+
for (Map.Entry<?, ?> entry : original.entrySet()) {
59+
Object value = entry.getValue();
60+
result.put(entry.getKey(), wrapValueIfRequired(value));
61+
}
62+
63+
return result;
64+
}
65+
66+
private static Object wrapValueIfRequired(Object v) {
67+
return v instanceof Map ?
68+
new MapWithTrackedMissingKeys((Map<?, ?>) v) : v;
69+
}
70+
}

webtau-core/src/main/java/org/testingisdocumenting/webtau/data/render/PrettyPrinter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class PrettyPrinter implements Iterable<PrettyPrinterLine> {
4040
public static final Color NUMBER_COLOR = Color.BLUE;
4141
public static final Color KEY_COLOR = Color.PURPLE;
4242
public static final Color CLASSIFIER_COLOR = Color.CYAN;
43+
public static final Color DANGER_COLOR = Color.RED;
4344
public static final Color UNKNOWN_COLOR = Color.CYAN;
4445

4546
private static final int INDENTATION_STEP = 2;

webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.testingisdocumenting.webtau.utils.TraceUtils;
3030

3131
import java.util.*;
32-
import java.util.function.Consumer;
3332
import java.util.stream.Collectors;
3433

3534
import static org.testingisdocumenting.webtau.WebTauCore.*;
@@ -57,15 +56,16 @@ public List<Object> getMismatchedExpectedValues() {
5756
public boolean contains(ValuePath actualPath, Object actual, Object expected) {
5857
updateTopLevelActualPath(actualPath);
5958

60-
return contains(actual, expected, false,
61-
(handler) -> handler.analyzeContain(this, actualPath, actual, expected));
59+
return contains(actualPath, actual, expected, false,
60+
(handler, convertedActual, convertedExpected) -> handler.analyzeContain(this, actualPath, convertedActual, convertedExpected));
6261
}
6362

6463
public boolean notContains(ValuePath actualPath, Object actual, Object expected) {
6564
updateTopLevelActualPath(actualPath);
6665

67-
return contains(actual, expected, true,
68-
(handler) -> handler.analyzeNotContain(this, actualPath, actual, expected));
66+
return contains(actualPath, actual, expected, true,
67+
(handler, convertedActual, convertedExpected) ->
68+
handler.analyzeNotContain(this, actualPath, convertedActual, convertedExpected));
6969
}
7070

7171
public ValueConverter createValueConverter() {
@@ -130,13 +130,18 @@ private ContainAnalyzer() {
130130
this.mismatchedExpectedValues = new ArrayList<>();
131131
}
132132

133-
private boolean contains(Object actual, Object expected, boolean isNegative, Consumer<ContainHandler> handle) {
133+
private boolean contains(ValuePath actualPath, Object actual, Object expected, boolean isNegative, ContainsLogic containsLogic) {
134134
ContainHandler handler = handlers.stream().
135135
filter(h -> h.handle(actual, expected)).findFirst().
136136
orElseThrow(() -> noHandlerFound(actual, expected));
137137

138+
Object convertedActual = handler.convertedActual(actual, expected);
139+
recordConvertedActual(actualPath, actual, convertedActual);
140+
141+
Object convertedExpected = handler.convertedExpected(actual, expected);
142+
138143
int before = isNegative ? matches.size() :mismatches.size();
139-
handle.accept(handler);
144+
containsLogic.execute(handler, convertedActual, convertedExpected);
140145
int after = isNegative ? matches.size() : mismatches.size();
141146

142147
return after == before;
@@ -165,9 +170,21 @@ private static List<ContainHandler> discoverHandlers() {
165170
return result;
166171
}
167172

173+
private void recordConvertedActual(ValuePath actualPath, Object actual, Object convertedActual) {
174+
if (actual == convertedActual) {
175+
return;
176+
}
177+
178+
convertedActualByPath.put(actualPath, convertedActual);
179+
}
180+
168181
private RuntimeException noHandlerFound(Object actual, Object expected) {
169182
return new RuntimeException(
170183
"no contains handler found for\nactual: " + PrettyPrinter.renderAsTextWithoutColors(actual) + " " + TraceUtils.renderType(actual) +
171184
"\nexpected: " + PrettyPrinter.renderAsTextWithoutColors(expected) + " " + TraceUtils.renderType(expected));
172185
}
186+
187+
interface ContainsLogic {
188+
void execute(ContainHandler handler, Object convertedActual, Object convertedExpected);
189+
}
173190
}

webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainHandler.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,28 @@ public interface ContainHandler {
2323

2424
void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath, Object actual, Object expected);
2525
void analyzeNotContain(ContainAnalyzer containAnalyzer, ValuePath actualPath, Object actual, Object expected);
26+
27+
/**
28+
* value optionally can be converted to another value to be passed down comparison chain.
29+
* exposed as outside method for more precise reporting of actual values in case of a failure.
30+
*
31+
* @param actual original actual
32+
* @param expected expected value
33+
* @return optionally converted actual
34+
*/
35+
default Object convertedActual(Object actual, Object expected) {
36+
return actual;
37+
}
38+
39+
/**
40+
* value optionally can be converted to another value to be passed down comparison chain.
41+
* exposed as outside method for more precise reporting of expected values for reporting
42+
*
43+
* @param actual original actual
44+
* @param expected original expected
45+
* @return optionally converted expected
46+
*/
47+
default Object convertedExpected(Object actual, Object expected) {
48+
return expected;
49+
}
2650
}

webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandler.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.testingisdocumenting.webtau.expectation.contain.handlers;
1818

19+
import org.testingisdocumenting.webtau.data.MapWithTrackedMissingKeys;
1920
import org.testingisdocumenting.webtau.data.ValuePath;
2021
import org.testingisdocumenting.webtau.expectation.contain.ContainAnalyzer;
2122
import org.testingisdocumenting.webtau.expectation.contain.ContainHandler;
@@ -31,35 +32,40 @@ public boolean handle(Object actual, Object expected) {
3132
return actual instanceof Map && expected instanceof Map;
3233
}
3334

35+
@Override
36+
public Object convertedActual(Object actual, Object expected) {
37+
return new MapWithTrackedMissingKeys((Map<?, ?>) actual);
38+
}
39+
3440
@Override
3541
public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath, Object actual, Object expected) {
36-
analyze(containAnalyzer, actualPath, actual, expected, false);
42+
analyzeMapAndMap(containAnalyzer, actualPath, actual, expected, false);
3743
}
3844

3945
@Override
4046
public void analyzeNotContain(ContainAnalyzer containAnalyzer, ValuePath actualPath, Object actual, Object expected) {
41-
analyze(containAnalyzer, actualPath, actual, expected, true);
47+
analyzeMapAndMap(containAnalyzer, actualPath, actual, expected, true);
4248
}
4349

44-
private void analyze(ContainAnalyzer containAnalyzer, ValuePath actualPath,
45-
Object actual, Object expected,
46-
boolean isNegative) {
50+
private void analyzeMapAndMap(ContainAnalyzer containAnalyzer, ValuePath actualPath,
51+
Object actual, Object expected,
52+
boolean isNegative) {
4753
Map<?, ?> actualMap = (Map<?, ?>) actual;
4854
Map<?, ?> expectedMap = (Map<?, ?>) expected;
4955

5056
for (Map.Entry<?, ?> expectedEntry : expectedMap.entrySet()) {
5157
Object expectedKey = expectedEntry.getKey();
5258

5359
ValuePath propertyPath = actualPath.property(expectedKey.toString());
54-
analyzePositiveNegative(containAnalyzer, actualMap, propertyPath, expectedEntry, isNegative);
60+
analyzeMapAndMapSingleExpectedEntry(containAnalyzer, actualMap, propertyPath, expectedEntry, isNegative);
5561
}
5662
}
5763

58-
private void analyzePositiveNegative(ContainAnalyzer containAnalyzer,
59-
Map<?, ?> actualMap,
60-
ValuePath propertyPath,
61-
Map.Entry<?, ?> expectedEntry,
62-
boolean isNegative) {
64+
private void analyzeMapAndMapSingleExpectedEntry(ContainAnalyzer containAnalyzer,
65+
Map<?, ?> actualMap,
66+
ValuePath propertyPath,
67+
Map.Entry<?, ?> expectedEntry,
68+
boolean isNegative) {
6369
if (!actualMap.containsKey(expectedEntry.getKey())) {
6470
containAnalyzer.reportMismatch(this, propertyPath, tokenizedMessage().matcher("is missing"));
6571
} else {
@@ -70,6 +76,7 @@ private void analyzePositiveNegative(ContainAnalyzer containAnalyzer,
7076
!comparator.compareIsNotEqual(propertyPath, actualValue, expectedEntry.getValue()):
7177
comparator.compareIsEqual(propertyPath, actualValue, expectedEntry.getValue());
7278

79+
containAnalyzer.registerConvertedActualByPath(comparator.getConvertedActualByPath());
7380
if (!actualValueEqual) {
7481
containAnalyzer.reportMismatch(this, propertyPath, comparator.generateEqualMismatchReport());
7582
} else {

webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/equality/handlers/MapsCompareToHandler.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.testingisdocumenting.webtau.expectation.equality.handlers;
1919

20+
import org.testingisdocumenting.webtau.data.MapWithTrackedMissingKeys;
2021
import org.testingisdocumenting.webtau.data.ValuePath;
2122
import org.testingisdocumenting.webtau.expectation.equality.CompareToComparator;
2223
import org.testingisdocumenting.webtau.expectation.equality.CompareToHandler;
@@ -31,6 +32,11 @@ public boolean handleEquality(Object actual, Object expected) {
3132
return actual instanceof Map && expected instanceof Map;
3233
}
3334

35+
@Override
36+
public Object convertedActual(Object actual, Object expected) {
37+
return new MapWithTrackedMissingKeys((Map<?, ?>) actual);
38+
}
39+
3440
@Override
3541
public void compareEqualOnly(CompareToComparator compareToComparator, ValuePath actualPath, Object actual, Object expected) {
3642
Map<?, ?> actualMap = (Map<?, ?>) actual;
@@ -64,9 +70,9 @@ void compare() {
6470
private void handleKey(Object key) {
6571
ValuePath propertyPath = actualPath.property(key.toString());
6672

67-
if (! actualMap.containsKey(key)) {
73+
if (!actualMap.containsKey(key)) {
6874
compareToComparator.reportMissing(MapsCompareToHandler.this, propertyPath, expectedMap.get(key));
69-
} else if (! expectedMap.containsKey(key)) {
75+
} else if (!expectedMap.containsKey(key)) {
7076
compareToComparator.reportExtra(MapsCompareToHandler.this, propertyPath, actualMap.get(key));
7177
} else {
7278
compareToComparator.compareUsingEqualOnly(propertyPath, actualMap.get(key), expectedMap.get(key));

0 commit comments

Comments
 (0)