Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
List<IndexedValue> indexedValues = analyzer.findContainingIndexedValues();

if (indexedValues.isEmpty()) {
containAnalyzer.reportMismatch(this, actualPath, analyzer.getComparator()
.generateEqualMismatchReport(), expected);
containAnalyzer.reportMismatchedValue(expected);
}

indexedValues.forEach(iv -> cliOutput.registerMatchedLine(iv.idx()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath
CompareToComparator comparator = comparator(AssertionMode.EQUAL);

if (indexedValues.isEmpty()) {
containAnalyzer.reportMismatch(this, actualPath, analyzer.getComparator()
.generateEqualMismatchReport(), expected);
containAnalyzer.reportMismatchedValue(expected);

dataNodes.forEach(n -> comparator.compareUsingEqualOnly(actualPath, n, expected));
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2023 webtau maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.testingisdocumenting.webtau.expectation;

import org.testingisdocumenting.webtau.data.ValuePath;
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;
import org.testingisdocumenting.webtau.reporter.TokenizedMessage;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import static org.testingisdocumenting.webtau.WebTauCore.*;

public class TokenizedReportUtils {
private TokenizedReportUtils() {
}

public static TokenizedMessage generateReportPart(ValuePath topLevelActualPath, TokenizedMessage label, List<List<ValuePathMessage>> messagesGroups) {
if (messagesGroups.stream().allMatch(List::isEmpty)) {
return tokenizedMessage();
}

return tokenizedMessage().add(label).colon().doubleNewLine().add(
generateReportPartWithoutLabel(topLevelActualPath, messagesGroups.stream()));
}

public static TokenizedMessage combineReportParts(TokenizedMessage... parts) {
TokenizedMessage result = tokenizedMessage();

List<TokenizedMessage> nonEmpty = Arrays.stream(parts)
.filter(part -> !part.isEmpty())
.toList();

int idx = 0;
for (TokenizedMessage message : nonEmpty) {
boolean isLast = idx == nonEmpty.size() - 1;

result.add(message);
if (!isLast) {
result.doubleNewLine();
}

idx++;
}

return result;
}

public static TokenizedMessage generateReportPartWithoutLabel(ValuePath topLevelActualPath, Stream<List<ValuePathMessage>> messagesGroupsStream) {
List<List<ValuePathMessage>> messagesGroups = messagesGroupsStream.filter(group -> !group.isEmpty()).toList();
if (messagesGroups.isEmpty()) {
return tokenizedMessage();
}

TokenizedMessage result = tokenizedMessage();
int groupIdx = 0;
for (List<ValuePathMessage> group : messagesGroups) {
TokenizedReportUtils.appendToReport(result, topLevelActualPath, group);

boolean isLastGroup = groupIdx == messagesGroups.size() - 1;
if (!isLastGroup) {
result.newLine();
}

groupIdx++;
}

return result;
}

public static TokenizedMessage appendToReport(TokenizedMessage report, ValuePath topLevelActualPath, List<ValuePathMessage> messages) {
int messageIdx = 0;
for (ValuePathMessage message : messages) {
boolean useFullMessage = !message.getActualPath().equals(topLevelActualPath);
report.add(useFullMessage ? message.getFullMessage() : message.getMessage());

boolean isLast = messageIdx == messages.size() - 1;
if (!isLast) {
report.newLine();
}
messageIdx++;
}

return report;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public TokenizedMessage negativeMatchedTokenizedMessage(ValuePath actualPath, Ob

@Override
public TokenizedMessage negativeMismatchedTokenizedMessage(ValuePath actualPath, Object actual) {
return containAnalyzer.generateMismatchReport();
return containAnalyzer.generateMatchReport();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.testingisdocumenting.webtau.WebTauCore.*;
import static org.testingisdocumenting.webtau.expectation.TokenizedReportUtils.*;

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

private final List<ValuePathMessage> matches;
private final List<ValuePathMessage> mismatches;
private final List<ValuePathMessage> matchMessages;
private final List<ValuePathMessage> mismatchMessages;
private final List<ValuePathMessage> missingMessages;

private final Set<ValuePath> extraMismatchPaths;

private final List<Object> mismatchedExpectedValues;
Expand Down Expand Up @@ -73,49 +77,85 @@ public ValueConverter createValueConverter() {
return convertedActualByPath::getOrDefault;
}

public void reportMismatch(ContainHandler reporter, ValuePathMessage valuePathMessage) {
mismatchMessages.add(valuePathMessage);
}

public void reportMismatches(ContainHandler reporter, List<ValuePathMessage> valuePathMessages) {
mismatchMessages.addAll(valuePathMessages);
}

public void reportMismatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch) {
mismatches.add(new ValuePathMessage(actualPath, mismatch));
reportMismatch(reporter, new ValuePathMessage(actualPath, mismatch));
}

public void reportMissing(ContainHandler reporter, ValuePath actualPath, Object value) {
missingMessages.add(new ValuePathMessage(actualPath, tokenizedMessage().value(value)));
}

public void reportMissing(ContainHandler reporter, ValuePathMessage valuePathMessage) {
missingMessages.add(valuePathMessage);
}

public void reportMissing(ContainHandler reporter, List<ValuePathMessage> valuePathMessages) {
missingMessages.addAll(valuePathMessages);
}

public void reportMismatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch, Object oneOfExpectedValues) {
reportMismatch(reporter, actualPath, mismatch);
public void reportMismatchedValue(Object oneOfExpectedValues) {
mismatchedExpectedValues.add(oneOfExpectedValues);
}

public void reportMatch(ContainHandler reporter, ValuePath actualPath, TokenizedMessage mismatch) {
matches.add(new ValuePathMessage(actualPath, mismatch));
matchMessages.add(new ValuePathMessage(actualPath, mismatch));
}

public Set<ValuePath> generateMatchPaths() {
return extractActualPaths(matches);
return extractActualPaths(matchMessages);
}

public Set<ValuePath> generateMismatchPaths() {
HashSet<ValuePath> result = new HashSet<>(extraMismatchPaths);
result.addAll(extractActualPaths(mismatches));
result.addAll(extractActualPaths(mismatchMessages));
result.addAll(extractActualPaths(missingMessages));

return result;
}

public TokenizedMessage generateMatchReport() {
return TokenizedMessage.join("\n", matches.stream().map(message ->
return TokenizedMessage.join("\n", matchMessages.stream().map(message ->
message.getActualPath().equals(topLevelActualPath) ?
message.getMessage() :
message.getFullMessage()).collect(Collectors.toList()));
}

public TokenizedMessage generateMismatchReport() {
return !mismatches.isEmpty() ?
TokenizedMessage reportDetails = generateMismatchReportDetails(mismatchedExpectedValues.isEmpty());

return reportDetails.isEmpty() && mismatchedExpectedValues.isEmpty() ?
tokenizedMessage().error("no match found") :
tokenizedMessage();
reportDetails;
}

private TokenizedMessage generateMismatchReportDetails(boolean useStrictLabels) {
if (missingMessages.isEmpty()) {
return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(mismatchMessages));
}

return combineReportParts(
generateReportPart(topLevelActualPath, tokenizedMessage().matcher(useStrictLabels ?
"mismatches": "possible mismatches"),
Collections.singletonList(mismatchMessages)),
generateReportPart(topLevelActualPath, tokenizedMessage().matcher(useStrictLabels ?
"missing values": "possible missing values"),
Collections.singletonList(missingMessages)));
}

public boolean noMismatches() {
return mismatches.isEmpty();
return mismatchMessages.isEmpty() && missingMessages.isEmpty() && mismatchedExpectedValues.isEmpty();
}

public boolean noMatches() {
return matches.isEmpty();
return matchMessages.isEmpty();
}

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

public void resetReportData() {
mismatches.clear();
matches.clear();
mismatchMessages.clear();
matchMessages.clear();
mismatchedExpectedValues.clear();
extraMismatchPaths.clear();
missingMessages.clear();
}

private ContainAnalyzer() {
this.matches = new ArrayList<>();
this.mismatches = new ArrayList<>();
this.matchMessages = new ArrayList<>();
this.mismatchMessages = new ArrayList<>();
this.missingMessages = new ArrayList<>();
this.mismatchedExpectedValues = new ArrayList<>();
this.extraMismatchPaths = new HashSet<>();
}
Expand All @@ -150,9 +192,9 @@ private boolean contains(ValuePath actualPath, Object actual, Object expected, b

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

int before = isNegative ? matches.size() :mismatches.size();
int before = isNegative ? matchMessages.size() : (mismatchMessages.size() + missingMessages.size() + mismatchedExpectedValues.size());
containsLogic.execute(handler, convertedActual, convertedExpected);
int after = isNegative ? matches.size() : mismatches.size();
int after = isNegative ? matchMessages.size() : (mismatchMessages.size() + missingMessages.size() + mismatchedExpectedValues.size());

return after == before;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,15 @@ public TokenizedMessage matchedTokenizedMessage(ValuePath actualPath, Object act
@Override
public TokenizedMessage mismatchedTokenizedMessage(ValuePath actualPath, Object actual) {
List<Object> mismatchedExpected = containAnalyzer.getMismatchedExpectedValues();
TokenizedMessage mismatchReport = containAnalyzer.generateMismatchReport();
if (mismatchedExpected.isEmpty() || (mismatchedExpected.size() == 1 && mismatchedExpected.get(0) == expected)) {
return tokenizedMessage().error("no match found");
return mismatchReport.isEmpty() ? tokenizedMessage().error("no match found") : mismatchReport;
}

return tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues());
TokenizedMessage report = tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues());
return mismatchReport.isEmpty() ?
report :
report.newLine().add(mismatchReport);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2023 webtau maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import org.testingisdocumenting.webtau.data.ValuePath;
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;

import java.util.List;
import java.util.stream.Stream;

record CombinedMismatchAndMissing(List<ValuePathMessage> mismatchMessages, List<ValuePathMessage> missingMessage) {
int size() {
return mismatchMessages.size() + missingMessage.size();
}

List<ValuePath> extractPaths() {
return Stream.concat(
mismatchMessages.stream().map(ValuePathMessage::getActualPath),
missingMessage.stream().map(ValuePathMessage::getActualPath)).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@
import org.testingisdocumenting.webtau.data.ValuePath;
import org.testingisdocumenting.webtau.expectation.contain.ContainAnalyzer;
import org.testingisdocumenting.webtau.expectation.contain.ContainHandler;
import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage;

import java.util.List;
import java.util.stream.Collectors;

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

if (indexedValues.isEmpty()) {
containAnalyzer.reportMismatch(this, actualPath, analyzer.getComparator()
.generateEqualMismatchReport(), expected);
containAnalyzer.reportMismatchedValue(expected);
}

// we want to highlight the closest matches in actual output. So among all the iterable values we pick the ones with the least mismatches
// and assume they are the closest match
List<List<ValuePathMessage>> mismatchMessagesPerIdx = analyzer.getMismatchMessagesPerIdx();
int minMismatches = mismatchMessagesPerIdx.stream().map(List::size).min(Integer::compareTo).orElse(0);
List<CombinedMismatchAndMissing> failureMessagesPerIdx = analyzer.getMismatchAndMissing();
int minFailures = failureMessagesPerIdx.stream().map(CombinedMismatchAndMissing::size).min(Integer::compareTo).orElse(0);

long numberOfEntriesWithMinMismatches = mismatchMessagesPerIdx.stream()
.filter(v -> v.size() == minMismatches).count();
long numberOfEntriesWithMinMismatches = failureMessagesPerIdx.stream()
.filter(v -> v.size() == minFailures).count();

if (numberOfEntriesWithMinMismatches != mismatchMessagesPerIdx.size()) {
mismatchMessagesPerIdx.stream()
.filter(v -> v.size() == minMismatches)
.forEach(v -> containAnalyzer.registerExtraMismatchPaths(v.stream().map(ValuePathMessage::getActualPath).collect(Collectors.toList())));
if (numberOfEntriesWithMinMismatches != failureMessagesPerIdx.size()) {
List<CombinedMismatchAndMissing> suspects = failureMessagesPerIdx.stream()
.filter(v -> v.size() == minFailures)
.toList();
suspects.forEach(list -> containAnalyzer.registerExtraMismatchPaths(list.extractPaths()));
suspects.forEach(list -> {
containAnalyzer.reportMismatches(this, list.mismatchMessages());
containAnalyzer.reportMissing(this, list.missingMessage());
});
}

containAnalyzer.registerConvertedActualByPath(analyzer.getComparator().getConvertedActualByPath());
Expand Down
Loading