Skip to content

Commit 6e15dc3

Browse files
matchers: contains extra details (#1535)
1 parent 9455ca1 commit 6e15dc3

18 files changed

Lines changed: 371 additions & 145 deletions

File tree

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
4343
List<IndexedValue> indexedValues = analyzer.findContainingIndexedValues();
4444

4545
if (indexedValues.isEmpty()) {
46-
containAnalyzer.reportMismatch(this, actualPath, analyzer.getComparator()
47-
.generateEqualMismatchReport(), expected);
46+
containAnalyzer.reportMismatchedValue(expected);
4847
}
4948

5049
indexedValues.forEach(iv -> cliOutput.registerMatchedLine(iv.idx()));

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
5050
CompareToComparator comparator = comparator(AssertionMode.EQUAL);
5151

5252
if (indexedValues.isEmpty()) {
53-
containAnalyzer.reportMismatch(this, actualPath, analyzer.getComparator()
54-
.generateEqualMismatchReport(), expected);
53+
containAnalyzer.reportMismatchedValue(expected);
5554

5655
dataNodes.forEach(n -> comparator.compareUsingEqualOnly(actualPath, n, expected));
5756
} else {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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;
18+
19+
import org.testingisdocumenting.webtau.data.ValuePath;
20+
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;
21+
import org.testingisdocumenting.webtau.reporter.TokenizedMessage;
22+
23+
import java.util.Arrays;
24+
import java.util.List;
25+
import java.util.stream.Stream;
26+
27+
import static org.testingisdocumenting.webtau.WebTauCore.*;
28+
29+
public class TokenizedReportUtils {
30+
private TokenizedReportUtils() {
31+
}
32+
33+
public static TokenizedMessage generateReportPart(ValuePath topLevelActualPath, TokenizedMessage label, List<List<ValuePathMessage>> messagesGroups) {
34+
if (messagesGroups.stream().allMatch(List::isEmpty)) {
35+
return tokenizedMessage();
36+
}
37+
38+
return tokenizedMessage().add(label).colon().doubleNewLine().add(
39+
generateReportPartWithoutLabel(topLevelActualPath, messagesGroups.stream()));
40+
}
41+
42+
public static TokenizedMessage combineReportParts(TokenizedMessage... parts) {
43+
TokenizedMessage result = tokenizedMessage();
44+
45+
List<TokenizedMessage> nonEmpty = Arrays.stream(parts)
46+
.filter(part -> !part.isEmpty())
47+
.toList();
48+
49+
int idx = 0;
50+
for (TokenizedMessage message : nonEmpty) {
51+
boolean isLast = idx == nonEmpty.size() - 1;
52+
53+
result.add(message);
54+
if (!isLast) {
55+
result.doubleNewLine();
56+
}
57+
58+
idx++;
59+
}
60+
61+
return result;
62+
}
63+
64+
public static TokenizedMessage generateReportPartWithoutLabel(ValuePath topLevelActualPath, Stream<List<ValuePathMessage>> messagesGroupsStream) {
65+
List<List<ValuePathMessage>> messagesGroups = messagesGroupsStream.filter(group -> !group.isEmpty()).toList();
66+
if (messagesGroups.isEmpty()) {
67+
return tokenizedMessage();
68+
}
69+
70+
TokenizedMessage result = tokenizedMessage();
71+
int groupIdx = 0;
72+
for (List<ValuePathMessage> group : messagesGroups) {
73+
TokenizedReportUtils.appendToReport(result, topLevelActualPath, group);
74+
75+
boolean isLastGroup = groupIdx == messagesGroups.size() - 1;
76+
if (!isLastGroup) {
77+
result.newLine();
78+
}
79+
80+
groupIdx++;
81+
}
82+
83+
return result;
84+
}
85+
86+
public static TokenizedMessage appendToReport(TokenizedMessage report, ValuePath topLevelActualPath, List<ValuePathMessage> messages) {
87+
int messageIdx = 0;
88+
for (ValuePathMessage message : messages) {
89+
boolean useFullMessage = !message.getActualPath().equals(topLevelActualPath);
90+
report.add(useFullMessage ? message.getFullMessage() : message.getMessage());
91+
92+
boolean isLast = messageIdx == messages.size() - 1;
93+
if (!isLast) {
94+
report.newLine();
95+
}
96+
messageIdx++;
97+
}
98+
99+
return report;
100+
}
101+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public TokenizedMessage negativeMatchedTokenizedMessage(ValuePath actualPath, Ob
100100

101101
@Override
102102
public TokenizedMessage negativeMismatchedTokenizedMessage(ValuePath actualPath, Object actual) {
103-
return containAnalyzer.generateMismatchReport();
103+
return containAnalyzer.generateMatchReport();
104104
}
105105

106106
@Override

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

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@
3030

3131
import java.util.*;
3232
import java.util.stream.Collectors;
33+
import java.util.stream.Stream;
3334

3435
import static org.testingisdocumenting.webtau.WebTauCore.*;
36+
import static org.testingisdocumenting.webtau.expectation.TokenizedReportUtils.*;
3537

3638
public class ContainAnalyzer {
3739
private static final List<ContainHandler> handlers = discoverHandlers();
3840

39-
private final List<ValuePathMessage> matches;
40-
private final List<ValuePathMessage> mismatches;
41+
private final List<ValuePathMessage> matchMessages;
42+
private final List<ValuePathMessage> mismatchMessages;
43+
private final List<ValuePathMessage> missingMessages;
44+
4145
private final Set<ValuePath> extraMismatchPaths;
4246

4347
private final List<Object> mismatchedExpectedValues;
@@ -73,49 +77,85 @@ public ValueConverter createValueConverter() {
7377
return convertedActualByPath::getOrDefault;
7478
}
7579

80+
public void reportMismatch(ContainHandler reporter, ValuePathMessage valuePathMessage) {
81+
mismatchMessages.add(valuePathMessage);
82+
}
83+
84+
public void reportMismatches(ContainHandler reporter, List<ValuePathMessage> valuePathMessages) {
85+
mismatchMessages.addAll(valuePathMessages);
86+
}
87+
7688
public void reportMismatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch) {
77-
mismatches.add(new ValuePathMessage(actualPath, mismatch));
89+
reportMismatch(reporter, new ValuePathMessage(actualPath, mismatch));
90+
}
91+
92+
public void reportMissing(ContainHandler reporter, ValuePath actualPath, Object value) {
93+
missingMessages.add(new ValuePathMessage(actualPath, tokenizedMessage().value(value)));
94+
}
95+
96+
public void reportMissing(ContainHandler reporter, ValuePathMessage valuePathMessage) {
97+
missingMessages.add(valuePathMessage);
98+
}
99+
100+
public void reportMissing(ContainHandler reporter, List<ValuePathMessage> valuePathMessages) {
101+
missingMessages.addAll(valuePathMessages);
78102
}
79103

80-
public void reportMismatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch, Object oneOfExpectedValues) {
81-
reportMismatch(reporter, actualPath, mismatch);
104+
public void reportMismatchedValue(Object oneOfExpectedValues) {
82105
mismatchedExpectedValues.add(oneOfExpectedValues);
83106
}
84107

85108
public void reportMatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch) {
86-
matches.add(new ValuePathMessage(actualPath, mismatch));
109+
matchMessages.add(new ValuePathMessage(actualPath, mismatch));
87110
}
88111

89112
public Set<ValuePath> generateMatchPaths() {
90-
return extractActualPaths(matches);
113+
return extractActualPaths(matchMessages);
91114
}
92115

93116
public Set<ValuePath> generateMismatchPaths() {
94117
HashSet<ValuePath> result = new HashSet<>(extraMismatchPaths);
95-
result.addAll(extractActualPaths(mismatches));
118+
result.addAll(extractActualPaths(mismatchMessages));
119+
result.addAll(extractActualPaths(missingMessages));
96120

97121
return result;
98122
}
99123

100124
public TokenizedMessage generateMatchReport() {
101-
return TokenizedMessage.join("\n", matches.stream().map(message ->
125+
return TokenizedMessage.join("\n", matchMessages.stream().map(message ->
102126
message.getActualPath().equals(topLevelActualPath) ?
103127
message.getMessage() :
104128
message.getFullMessage()).collect(Collectors.toList()));
105129
}
106130

107131
public TokenizedMessage generateMismatchReport() {
108-
return !mismatches.isEmpty() ?
132+
TokenizedMessage reportDetails = generateMismatchReportDetails(mismatchedExpectedValues.isEmpty());
133+
134+
return reportDetails.isEmpty() && mismatchedExpectedValues.isEmpty() ?
109135
tokenizedMessage().error("no match found") :
110-
tokenizedMessage();
136+
reportDetails;
137+
}
138+
139+
private TokenizedMessage generateMismatchReportDetails(boolean useStrictLabels) {
140+
if (missingMessages.isEmpty()) {
141+
return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(mismatchMessages));
142+
}
143+
144+
return combineReportParts(
145+
generateReportPart(topLevelActualPath, tokenizedMessage().matcher(useStrictLabels ?
146+
"mismatches": "possible mismatches"),
147+
Collections.singletonList(mismatchMessages)),
148+
generateReportPart(topLevelActualPath, tokenizedMessage().matcher(useStrictLabels ?
149+
"missing values": "possible missing values"),
150+
Collections.singletonList(missingMessages)));
111151
}
112152

113153
public boolean noMismatches() {
114-
return mismatches.isEmpty();
154+
return mismatchMessages.isEmpty() && missingMessages.isEmpty() && mismatchedExpectedValues.isEmpty();
115155
}
116156

117157
public boolean noMatches() {
118-
return matches.isEmpty();
158+
return matchMessages.isEmpty();
119159
}
120160

121161
public void registerConvertedActualByPath(Map<ValuePath, Object> convertedActualByPath) {
@@ -127,15 +167,17 @@ public void registerExtraMismatchPaths(List<ValuePath> extraMismatchPaths) {
127167
}
128168

129169
public void resetReportData() {
130-
mismatches.clear();
131-
matches.clear();
170+
mismatchMessages.clear();
171+
matchMessages.clear();
132172
mismatchedExpectedValues.clear();
133173
extraMismatchPaths.clear();
174+
missingMessages.clear();
134175
}
135176

136177
private ContainAnalyzer() {
137-
this.matches = new ArrayList<>();
138-
this.mismatches = new ArrayList<>();
178+
this.matchMessages = new ArrayList<>();
179+
this.mismatchMessages = new ArrayList<>();
180+
this.missingMessages = new ArrayList<>();
139181
this.mismatchedExpectedValues = new ArrayList<>();
140182
this.extraMismatchPaths = new HashSet<>();
141183
}
@@ -150,9 +192,9 @@ private boolean contains(ValuePath actualPath, Object actual, Object expected, b
150192

151193
Object convertedExpected = handler.convertedExpected(actual, expected);
152194

153-
int before = isNegative ? matches.size() :mismatches.size();
195+
int before = isNegative ? matchMessages.size() : (mismatchMessages.size() + missingMessages.size() + mismatchedExpectedValues.size());
154196
containsLogic.execute(handler, convertedActual, convertedExpected);
155-
int after = isNegative ? matches.size() : mismatches.size();
197+
int after = isNegative ? matchMessages.size() : (mismatchMessages.size() + missingMessages.size() + mismatchedExpectedValues.size());
156198

157199
return after == before;
158200
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,15 @@ public TokenizedMessage matchedTokenizedMessage(ValuePath actualPath, Object act
6868
@Override
6969
public TokenizedMessage mismatchedTokenizedMessage(ValuePath actualPath, Object actual) {
7070
List<Object> mismatchedExpected = containAnalyzer.getMismatchedExpectedValues();
71+
TokenizedMessage mismatchReport = containAnalyzer.generateMismatchReport();
7172
if (mismatchedExpected.isEmpty() || (mismatchedExpected.size() == 1 && mismatchedExpected.get(0) == expected)) {
72-
return tokenizedMessage().error("no match found");
73+
return mismatchReport.isEmpty() ? tokenizedMessage().error("no match found") : mismatchReport;
7374
}
7475

75-
return tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues());
76+
TokenizedMessage report = tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues());
77+
return mismatchReport.isEmpty() ?
78+
report :
79+
report.newLine().add(mismatchReport);
7680
}
7781

7882
@Override
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.contain.handlers;
18+
19+
import org.testingisdocumenting.webtau.data.ValuePath;
20+
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;
21+
22+
import java.util.List;
23+
import java.util.stream.Stream;
24+
25+
record CombinedMismatchAndMissing(List<ValuePathMessage> mismatchMessages, List<ValuePathMessage> missingMessage) {
26+
int size() {
27+
return mismatchMessages.size() + missingMessage.size();
28+
}
29+
30+
List<ValuePath> extractPaths() {
31+
return Stream.concat(
32+
mismatchMessages.stream().map(ValuePathMessage::getActualPath),
33+
missingMessage.stream().map(ValuePathMessage::getActualPath)).toList();
34+
}
35+
}

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@
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;
2423

2524
import java.util.List;
26-
import java.util.stream.Collectors;
2725

2826
public class IterableAndSingleValueContainHandler implements ContainHandler {
2927
@Override
@@ -37,22 +35,26 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
3735
List<IndexedValue> indexedValues = analyzer.findContainingIndexedValues();
3836

3937
if (indexedValues.isEmpty()) {
40-
containAnalyzer.reportMismatch(this, actualPath, analyzer.getComparator()
41-
.generateEqualMismatchReport(), expected);
38+
containAnalyzer.reportMismatchedValue(expected);
4239
}
4340

4441
// we want to highlight the closest matches in actual output. So among all the iterable values we pick the ones with the least mismatches
4542
// 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);
43+
List<CombinedMismatchAndMissing> failureMessagesPerIdx = analyzer.getMismatchAndMissing();
44+
int minFailures = failureMessagesPerIdx.stream().map(CombinedMismatchAndMissing::size).min(Integer::compareTo).orElse(0);
4845

49-
long numberOfEntriesWithMinMismatches = mismatchMessagesPerIdx.stream()
50-
.filter(v -> v.size() == minMismatches).count();
46+
long numberOfEntriesWithMinMismatches = failureMessagesPerIdx.stream()
47+
.filter(v -> v.size() == minFailures).count();
5148

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())));
49+
if (numberOfEntriesWithMinMismatches != failureMessagesPerIdx.size()) {
50+
List<CombinedMismatchAndMissing> suspects = failureMessagesPerIdx.stream()
51+
.filter(v -> v.size() == minFailures)
52+
.toList();
53+
suspects.forEach(list -> containAnalyzer.registerExtraMismatchPaths(list.extractPaths()));
54+
suspects.forEach(list -> {
55+
containAnalyzer.reportMismatches(this, list.mismatchMessages());
56+
containAnalyzer.reportMissing(this, list.missingMessage());
57+
});
5658
}
5759

5860
containAnalyzer.registerConvertedActualByPath(analyzer.getComparator().getConvertedActualByPath());

0 commit comments

Comments
 (0)