Skip to content

Commit 9455ca1

Browse files
matchers: contains highlight closest match (#1534)
1 parent 70f1aeb commit 9455ca1

15 files changed

Lines changed: 117 additions & 108 deletions

File tree

webtau-cli/src/main/java/org/testingisdocumenting/webtau/cli/expectation/CliOutputContainHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
4747
.generateEqualMismatchReport(), expected);
4848
}
4949

50-
indexedValues.forEach(iv -> cliOutput.registerMatchedLine(iv.getIdx()));
50+
indexedValues.forEach(iv -> cliOutput.registerMatchedLine(iv.idx()));
5151
}
5252

5353
@Override
@@ -59,8 +59,8 @@ public void analyzeNotContain(ContainAnalyzer containAnalyzer, ValuePath actualP
5959
List<IndexedValue> indexedValues = analyzer.findContainingIndexedValues();
6060

6161
indexedValues.forEach(indexedValue ->
62-
containAnalyzer.reportMatch(this, actualPath.index(indexedValue.getIdx()),
63-
tokenizedMessage().matcher("equals").value(indexedValue.getValue())
62+
containAnalyzer.reportMatch(this, actualPath.index(indexedValue.idx()),
63+
tokenizedMessage().matcher("equals").value(indexedValue.value())
6464
));
6565
}
6666

webtau-core/src/main/java/org/testingisdocumenting/webtau/data/datanode/DataNodeListAndValueContainHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
5555

5656
dataNodes.forEach(n -> comparator.compareUsingEqualOnly(actualPath, n, expected));
5757
} else {
58-
indexedValues.forEach(iv -> comparator.compareUsingEqualOnly(actualPath, dataNodes.get(iv.getIdx()), expected));
58+
indexedValues.forEach(iv -> comparator.compareUsingEqualOnly(actualPath, dataNodes.get(iv.idx()), expected));
5959
}
6060
}
6161

@@ -75,11 +75,11 @@ public void analyzeNotContain(ContainAnalyzer containAnalyzer, ValuePath actualP
7575
CompareToComparator comparator = comparator(AssertionMode.NOT_EQUAL);
7676

7777
indexedValues.forEach(indexedValue -> {
78-
ValuePath indexedPath = actualPath.index(indexedValue.getIdx());
78+
ValuePath indexedPath = actualPath.index(indexedValue.idx());
7979

8080
containAnalyzer.reportMatch(this, indexedPath,
81-
tokenizedMessage().error("equals").valueFirstLinesOnly(indexedValue.getValue()));
82-
comparator.compareUsingEqualOnly(indexedPath, dataNodes.get(indexedValue.getIdx()), expected);
81+
tokenizedMessage().error("equals").valueFirstLinesOnly(indexedValue.value()));
82+
comparator.compareUsingEqualOnly(indexedPath, dataNodes.get(indexedValue.idx()), expected);
8383
});
8484
}
8585
}

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import org.testingisdocumenting.webtau.expectation.contain.handlers.IterableAndTableContainHandler;
2424
import org.testingisdocumenting.webtau.expectation.contain.handlers.IterableAndSingleValueContainHandler;
2525
import org.testingisdocumenting.webtau.expectation.contain.handlers.NullContainHandler;
26-
import org.testingisdocumenting.webtau.expectation.equality.ActualPathMessage;
26+
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;
2727
import org.testingisdocumenting.webtau.reporter.TokenizedMessage;
2828
import org.testingisdocumenting.webtau.utils.ServiceLoaderUtils;
2929
import org.testingisdocumenting.webtau.utils.TraceUtils;
@@ -36,8 +36,9 @@
3636
public class ContainAnalyzer {
3737
private static final List<ContainHandler> handlers = discoverHandlers();
3838

39-
private final List<ActualPathMessage> matches;
40-
private final List<ActualPathMessage> mismatches;
39+
private final List<ValuePathMessage> matches;
40+
private final List<ValuePathMessage> mismatches;
41+
private final Set<ValuePath> extraMismatchPaths;
4142

4243
private final List<Object> mismatchedExpectedValues;
4344

@@ -73,7 +74,7 @@ public ValueConverter createValueConverter() {
7374
}
7475

7576
public void reportMismatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch) {
76-
mismatches.add(new ActualPathMessage(actualPath, mismatch));
77+
mismatches.add(new ValuePathMessage(actualPath, mismatch));
7778
}
7879

7980
public void reportMismatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch, Object oneOfExpectedValues) {
@@ -82,15 +83,18 @@ public void reportMismatch(ContainHandler reporter, ValuePath actualPath, Tokeni
8283
}
8384

8485
public void reportMatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch) {
85-
matches.add(new ActualPathMessage(actualPath, mismatch));
86+
matches.add(new ValuePathMessage(actualPath, mismatch));
8687
}
8788

8889
public Set<ValuePath> generateMatchPaths() {
8990
return extractActualPaths(matches);
9091
}
9192

9293
public Set<ValuePath> generateMismatchPaths() {
93-
return extractActualPaths(mismatches);
94+
HashSet<ValuePath> result = new HashSet<>(extraMismatchPaths);
95+
result.addAll(extractActualPaths(mismatches));
96+
97+
return result;
9498
}
9599

96100
public TokenizedMessage generateMatchReport() {
@@ -118,16 +122,22 @@ public void registerConvertedActualByPath(Map<ValuePath, Object> convertedActual
118122
this.convertedActualByPath.putAll(convertedActualByPath);
119123
}
120124

125+
public void registerExtraMismatchPaths(List<ValuePath> extraMismatchPaths) {
126+
this.extraMismatchPaths.addAll(extraMismatchPaths);
127+
}
128+
121129
public void resetReportData() {
122130
mismatches.clear();
123131
matches.clear();
124132
mismatchedExpectedValues.clear();
133+
extraMismatchPaths.clear();
125134
}
126135

127136
private ContainAnalyzer() {
128137
this.matches = new ArrayList<>();
129138
this.mismatches = new ArrayList<>();
130139
this.mismatchedExpectedValues = new ArrayList<>();
140+
this.extraMismatchPaths = new HashSet<>();
131141
}
132142

133143
private boolean contains(ValuePath actualPath, Object actual, Object expected, boolean isNegative, ContainsLogic containsLogic) {
@@ -147,10 +157,10 @@ private boolean contains(ValuePath actualPath, Object actual, Object expected, b
147157
return after == before;
148158
}
149159

150-
private Set<ValuePath> extractActualPaths(List<ActualPathMessage> notEqualMessages) {
160+
private Set<ValuePath> extractActualPaths(List<ValuePathMessage> notEqualMessages) {
151161
return notEqualMessages
152162
.stream()
153-
.map(ActualPathMessage::getActualPath)
163+
.map(ValuePathMessage::getActualPath)
154164
.collect(Collectors.toSet());
155165
}
156166

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
* Copyright 2023 webtau maintainers
23
* Copyright 2019 TWO SIGMA OPEN SOURCE, LLC
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,20 +17,6 @@
1617

1718
package org.testingisdocumenting.webtau.expectation.contain.handlers;
1819

19-
public class IndexedValue {
20-
private int idx;
21-
private Object value;
22-
23-
public IndexedValue(int idx, Object value) {
24-
this.idx = idx;
25-
this.value = value;
26-
}
27-
28-
public int getIdx() {
29-
return idx;
30-
}
31-
32-
public Object getValue() {
33-
return value;
34-
}
20+
public record IndexedValue(int idx, Object value) {
3521
}
22+

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import org.testingisdocumenting.webtau.data.ValuePath;
2121
import org.testingisdocumenting.webtau.expectation.contain.ContainAnalyzer;
2222
import org.testingisdocumenting.webtau.expectation.contain.ContainHandler;
23+
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;
2324

2425
import java.util.List;
26+
import java.util.stream.Collectors;
2527

2628
public class IterableAndSingleValueContainHandler implements ContainHandler {
2729
@Override
@@ -39,6 +41,20 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
3941
.generateEqualMismatchReport(), expected);
4042
}
4143

44+
// we want to highlight the closest matches in actual output. So among all the iterable values we pick the ones with the least mismatches
45+
// and assume they are the closest match
46+
List<List<ValuePathMessage>> mismatchMessagesPerIdx = analyzer.getMismatchMessagesPerIdx();
47+
int minMismatches = mismatchMessagesPerIdx.stream().map(List::size).min(Integer::compareTo).orElse(0);
48+
49+
long numberOfEntriesWithMinMismatches = mismatchMessagesPerIdx.stream()
50+
.filter(v -> v.size() == minMismatches).count();
51+
52+
if (numberOfEntriesWithMinMismatches != mismatchMessagesPerIdx.size()) {
53+
mismatchMessagesPerIdx.stream()
54+
.filter(v -> v.size() == minMismatches)
55+
.forEach(v -> containAnalyzer.registerExtraMismatchPaths(v.stream().map(ValuePathMessage::getActualPath).collect(Collectors.toList())));
56+
}
57+
4258
containAnalyzer.registerConvertedActualByPath(analyzer.getComparator().getConvertedActualByPath());
4359
}
4460

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,22 @@
2020
import org.testingisdocumenting.webtau.data.ValuePath;
2121
import org.testingisdocumenting.webtau.expectation.equality.CompareToComparator;
2222
import org.testingisdocumenting.webtau.expectation.equality.CompareToResult;
23+
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;
2324

24-
import java.util.ArrayList;
25-
import java.util.Iterator;
26-
import java.util.List;
25+
import java.util.*;
2726

2827
public class IterableContainAnalyzer {
2928
private final ValuePath actualPath;
3029
private final Object actual;
3130
private final Object expected;
3231
private final CompareToComparator comparator;
32+
private final List<List<ValuePathMessage>> mismatchMessagesPerIdx;
3333

3434
public IterableContainAnalyzer(ValuePath actualPath, Object actual, Object expected, boolean isNegative) {
3535
this.actualPath = actualPath;
3636
this.actual = actual;
3737
this.expected = expected;
38+
this.mismatchMessagesPerIdx = new ArrayList<>();
3839
this.comparator = CompareToComparator.comparator(isNegative ? CompareToComparator.AssertionMode.NOT_EQUAL : CompareToComparator.AssertionMode.EQUAL);
3940
}
4041

@@ -53,6 +54,8 @@ public List<IndexedValue> findContainingIndexedValues() {
5354
boolean isEqual = compareToResult.isEqual();
5455
if (isEqual) {
5556
matchedIndexes.add(new IndexedValue(idx, actualValue));
57+
} else {
58+
mismatchMessagesPerIdx.add(compareToResult.getNotEqualMessages());
5659
}
5760

5861
idx++;
@@ -61,6 +64,10 @@ public List<IndexedValue> findContainingIndexedValues() {
6164
return matchedIndexes;
6265
}
6366

67+
public List<List<ValuePathMessage>> getMismatchMessagesPerIdx() {
68+
return mismatchMessagesPerIdx;
69+
}
70+
6471
public CompareToComparator getComparator() {
6572
return comparator;
6673
}

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ public String getMessage() {
5757

5858
private static final List<CompareToHandler> handlers = discoverHandlers();
5959

60-
private final List<ActualPathMessage> equalMessages = new ArrayList<>();
61-
private final List<ActualPathMessage> notEqualMessages = new ArrayList<>();
62-
private final List<ActualPathMessage> greaterMessages = new ArrayList<>();
63-
private final List<ActualPathMessage> lessMessages = new ArrayList<>();
64-
private final List<ActualPathMessage> missingMessages = new ArrayList<>();
65-
private final List<ActualPathMessage> extraMessages = new ArrayList<>();
60+
private final List<ValuePathMessage> equalMessages = new ArrayList<>();
61+
private final List<ValuePathMessage> notEqualMessages = new ArrayList<>();
62+
private final List<ValuePathMessage> greaterMessages = new ArrayList<>();
63+
private final List<ValuePathMessage> lessMessages = new ArrayList<>();
64+
private final List<ValuePathMessage> missingMessages = new ArrayList<>();
65+
private final List<ValuePathMessage> extraMessages = new ArrayList<>();
6666

6767
// when actual value was converted for comparison, e.g. bean to Map, it will go into this map
6868
private final Map<ValuePath, Object> convertedActualByPath = new HashMap<>();
@@ -187,7 +187,7 @@ public TokenizedMessage generateEqualMismatchReport() {
187187
generateReportPart(tokenizedMessage().error("unexpected values"), Collections.singletonList(extraMessages)));
188188
}
189189

190-
public List<ActualPathMessage> getNotEqualMessages() {
190+
public List<ValuePathMessage> getNotEqualMessages() {
191191
return notEqualMessages;
192192
}
193193

@@ -234,7 +234,7 @@ public Set<ValuePath> generateEqualMatchPaths() {
234234
return extractActualPaths(equalMessages);
235235
}
236236

237-
public List<ActualPathMessage> getEqualMessages() {
237+
public List<ValuePathMessage> getEqualMessages() {
238238
return equalMessages;
239239
}
240240

@@ -248,27 +248,27 @@ public void resetReportData() {
248248
}
249249

250250
public void reportMissing(CompareToHandler reporter, ValuePath actualPath, Object value) {
251-
missingMessages.add(new ActualPathMessage(actualPath, tokenizedMessage().value(value)));
251+
missingMessages.add(new ValuePathMessage(actualPath, tokenizedMessage().value(value)));
252252
}
253253

254254
public void reportExtra(CompareToHandler reporter, ValuePath actualPath, Object value) {
255-
extraMessages.add(new ActualPathMessage(actualPath, tokenizedMessage().value(value)));
255+
extraMessages.add(new ValuePathMessage(actualPath, tokenizedMessage().value(value)));
256256
}
257257

258258
public void reportEqual(CompareToHandler reporter, ValuePath actualPath, TokenizedMessage message) {
259-
equalMessages.add(new ActualPathMessage(actualPath, message));
259+
equalMessages.add(new ValuePathMessage(actualPath, message));
260260
}
261261

262262
public void reportNotEqual(CompareToHandler reporter, ValuePath actualPath, TokenizedMessage message) {
263-
notEqualMessages.add(new ActualPathMessage(actualPath, message));
263+
notEqualMessages.add(new ValuePathMessage(actualPath, message));
264264
}
265265

266266
public void reportGreater(CompareToHandler reporter, ValuePath actualPath, TokenizedMessage message) {
267-
greaterMessages.add(new ActualPathMessage(actualPath, message));
267+
greaterMessages.add(new ValuePathMessage(actualPath, message));
268268
}
269269

270270
public void reportLess(CompareToHandler reporter, ValuePath actualPath, TokenizedMessage message) {
271-
lessMessages.add(new ActualPathMessage(actualPath, message));
271+
lessMessages.add(new ValuePathMessage(actualPath, message));
272272
}
273273

274274
public void reportEqualOrNotEqual(CompareToHandler reporter, boolean isEqual, ValuePath actualPath, TokenizedMessage message) {
@@ -390,25 +390,25 @@ private void mergeResults(CompareToComparator comparator) {
390390
convertedActualByPath.putAll(comparator.convertedActualByPath);
391391
}
392392

393-
private TokenizedMessage generateReportPart(TokenizedMessage label, List<List<ActualPathMessage>> messagesGroups) {
393+
private TokenizedMessage generateReportPart(TokenizedMessage label, List<List<ValuePathMessage>> messagesGroups) {
394394
if (messagesGroups.stream().allMatch(List::isEmpty)) {
395395
return tokenizedMessage();
396396
}
397397

398398
return tokenizedMessage().add(label).colon().doubleNewLine().add(generateReportPartWithoutLabel(messagesGroups.stream()));
399399
}
400400

401-
private TokenizedMessage generateReportPartWithoutLabel(Stream<List<ActualPathMessage>> messagesGroupsStream) {
402-
List<List<ActualPathMessage>> messagesGroups = messagesGroupsStream.filter(group -> !group.isEmpty()).collect(Collectors.toList());
401+
private TokenizedMessage generateReportPartWithoutLabel(Stream<List<ValuePathMessage>> messagesGroupsStream) {
402+
List<List<ValuePathMessage>> messagesGroups = messagesGroupsStream.filter(group -> !group.isEmpty()).toList();
403403
if (messagesGroups.isEmpty()) {
404404
return tokenizedMessage();
405405
}
406406

407407
TokenizedMessage result = tokenizedMessage();
408408
int groupIdx = 0;
409-
for (List<ActualPathMessage> group : messagesGroups) {
409+
for (List<ValuePathMessage> group : messagesGroups) {
410410
int messageIdx = 0;
411-
for (ActualPathMessage message : group) {
411+
for (ValuePathMessage message : group) {
412412
boolean useFullMessage = !message.getActualPath().equals(topLevelActualPath);
413413
result.add(useFullMessage ? message.getFullMessage() : message.getMessage());
414414

@@ -435,7 +435,7 @@ private TokenizedMessage combineReportParts(TokenizedMessage... parts) {
435435

436436
List<TokenizedMessage> nonEmpty = Arrays.stream(parts)
437437
.filter(part -> !part.isEmpty())
438-
.collect(Collectors.toList());
438+
.toList();
439439

440440
int idx = 0;
441441
for (TokenizedMessage message : nonEmpty) {
@@ -479,10 +479,10 @@ private static RuntimeException noHandlerFound(Object actual, Object expected) {
479479
"\nexpected: " + PrettyPrinter.renderAsTextWithoutColors(expected) + " " + TraceUtils.renderType(expected));
480480
}
481481

482-
private Set<ValuePath> extractActualPaths(List<ActualPathMessage> messages) {
482+
private Set<ValuePath> extractActualPaths(List<ValuePathMessage> messages) {
483483
return messages
484484
.stream()
485-
.map(ActualPathMessage::getActualPath)
485+
.map(ValuePathMessage::getActualPath)
486486
.collect(Collectors.toSet());
487487
}
488488
}

0 commit comments

Comments
 (0)