From 7268454c9467ecdd9cbf05a7c089b3de0ed9bdad Mon Sep 17 00:00:00 2001 From: mykolagolubyev Date: Tue, 29 Aug 2023 18:10:28 -0400 Subject: [PATCH 1/2] matchers: contains extra details --- .../expectation/CliOutputContainHandler.java | 3 +- .../DataNodeListAndValueContainHandler.java | 3 +- .../expectation/TokenizedReportUtils.java | 101 +++++++++++++++++ .../contain/ContainAllMatcher.java | 2 +- .../expectation/contain/ContainAnalyzer.java | 80 ++++++++++---- .../expectation/contain/ContainMatcher.java | 6 +- .../handlers/CombinedMismatchAndMissing.java | 35 ++++++ .../IterableAndSingleValueContainHandler.java | 26 +++-- .../handlers/IterableContainAnalyzer.java | 11 +- .../contain/handlers/MapContainHandler.java | 7 +- .../equality/CompareToComparator.java | 103 ++++-------------- .../contain/ContainAllMatcherTest.groovy | 6 +- ...bleAndSingleValueContainHandlerTest.groovy | 42 ++++++- .../IterableAndTableContainHandlerTest.groovy | 14 ++- .../handlers/MapContainHandlerTest.groovy | 49 ++++++++- .../webtau/MatchersTest.java | 17 ++- .../handlers/MapMatchersJavaExamplesTest.java | 7 +- .../add-2023-08-23-contains-extra-details.md | 2 + 18 files changed, 369 insertions(+), 145 deletions(-) create mode 100644 webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/TokenizedReportUtils.java create mode 100644 webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/CombinedMismatchAndMissing.java create mode 100644 webtau-docs/znai/release-notes/2.2/add-2023-08-23-contains-extra-details.md diff --git a/webtau-cli/src/main/java/org/testingisdocumenting/webtau/cli/expectation/CliOutputContainHandler.java b/webtau-cli/src/main/java/org/testingisdocumenting/webtau/cli/expectation/CliOutputContainHandler.java index 2fa1f6e0e..5f89bbb45 100644 --- a/webtau-cli/src/main/java/org/testingisdocumenting/webtau/cli/expectation/CliOutputContainHandler.java +++ b/webtau-cli/src/main/java/org/testingisdocumenting/webtau/cli/expectation/CliOutputContainHandler.java @@ -43,8 +43,7 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath List indexedValues = analyzer.findContainingIndexedValues(); if (indexedValues.isEmpty()) { - containAnalyzer.reportMismatch(this, actualPath, analyzer.getComparator() - .generateEqualMismatchReport(), expected); + containAnalyzer.reportMismatchedValue(expected); } indexedValues.forEach(iv -> cliOutput.registerMatchedLine(iv.idx())); diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/data/datanode/DataNodeListAndValueContainHandler.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/data/datanode/DataNodeListAndValueContainHandler.java index 96a57c033..b4b776c71 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/data/datanode/DataNodeListAndValueContainHandler.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/data/datanode/DataNodeListAndValueContainHandler.java @@ -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 { diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/TokenizedReportUtils.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/TokenizedReportUtils.java new file mode 100644 index 000000000..97ac619ce --- /dev/null +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/TokenizedReportUtils.java @@ -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> 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 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> messagesGroupsStream) { + List> messagesGroups = messagesGroupsStream.filter(group -> !group.isEmpty()).toList(); + if (messagesGroups.isEmpty()) { + return tokenizedMessage(); + } + + TokenizedMessage result = tokenizedMessage(); + int groupIdx = 0; + for (List 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 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; + } +} diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcher.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcher.java index 973ad5d8f..77c07e43b 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcher.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcher.java @@ -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 diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java index 10342dca7..88cd8dd55 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java @@ -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 handlers = discoverHandlers(); - private final List matches; - private final List mismatches; + private final List matchMessages; + private final List mismatchMessages; + private final List missingMessages; + private final Set extraMismatchPaths; private final List mismatchedExpectedValues; @@ -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 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 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 generateMatchPaths() { - return extractActualPaths(matches); + return extractActualPaths(matchMessages); } public Set generateMismatchPaths() { HashSet 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() ? 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 convertedActualByPath) { @@ -127,15 +167,17 @@ public void registerExtraMismatchPaths(List 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<>(); } @@ -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; } diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java index 2b77d7723..a008780fa 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java @@ -69,10 +69,12 @@ public TokenizedMessage matchedTokenizedMessage(ValuePath actualPath, Object act public TokenizedMessage mismatchedTokenizedMessage(ValuePath actualPath, Object actual) { List mismatchedExpected = containAnalyzer.getMismatchedExpectedValues(); if (mismatchedExpected.isEmpty() || (mismatchedExpected.size() == 1 && mismatchedExpected.get(0) == expected)) { - return tokenizedMessage().error("no match found"); + return containAnalyzer.generateMismatchReport(); } - return tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues()); + return tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues()) + .newLine() + .add(containAnalyzer.generateMismatchReport()); } @Override diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/CombinedMismatchAndMissing.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/CombinedMismatchAndMissing.java new file mode 100644 index 000000000..3aea39a21 --- /dev/null +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/CombinedMismatchAndMissing.java @@ -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 mismatchMessages, List missingMessage) { + int size() { + return mismatchMessages.size() + missingMessage.size(); + } + + List extractPaths() { + return Stream.concat( + mismatchMessages.stream().map(ValuePathMessage::getActualPath), + missingMessage.stream().map(ValuePathMessage::getActualPath)).toList(); + } +} diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandler.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandler.java index 9bcf64e55..9cd818d17 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandler.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandler.java @@ -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 @@ -37,22 +35,26 @@ public void analyzeContain(ContainAnalyzer containAnalyzer, ValuePath actualPath List 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> mismatchMessagesPerIdx = analyzer.getMismatchMessagesPerIdx(); - int minMismatches = mismatchMessagesPerIdx.stream().map(List::size).min(Integer::compareTo).orElse(0); + List 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 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()); diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableContainAnalyzer.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableContainAnalyzer.java index a47258f13..b41702201 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableContainAnalyzer.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableContainAnalyzer.java @@ -20,7 +20,6 @@ import org.testingisdocumenting.webtau.data.ValuePath; import org.testingisdocumenting.webtau.expectation.equality.CompareToComparator; import org.testingisdocumenting.webtau.expectation.equality.CompareToResult; -import org.testingisdocumenting.webtau.expectation.equality.ValuePathMessage; import java.util.*; @@ -29,13 +28,13 @@ public class IterableContainAnalyzer { private final Object actual; private final Object expected; private final CompareToComparator comparator; - private final List> mismatchMessagesPerIdx; + private final List mismatchAndMissing; public IterableContainAnalyzer(ValuePath actualPath, Object actual, Object expected, boolean isNegative) { this.actualPath = actualPath; this.actual = actual; this.expected = expected; - this.mismatchMessagesPerIdx = new ArrayList<>(); + this.mismatchAndMissing = new ArrayList<>(); this.comparator = CompareToComparator.comparator(isNegative ? CompareToComparator.AssertionMode.NOT_EQUAL : CompareToComparator.AssertionMode.EQUAL); } @@ -55,7 +54,7 @@ public List findContainingIndexedValues() { if (isEqual) { matchedIndexes.add(new IndexedValue(idx, actualValue)); } else { - mismatchMessagesPerIdx.add(compareToResult.getNotEqualMessages()); + mismatchAndMissing.add(new CombinedMismatchAndMissing(compareToResult.getNotEqualMessages(), compareToResult.getMissingMessages())); } idx++; @@ -64,8 +63,8 @@ public List findContainingIndexedValues() { return matchedIndexes; } - public List> getMismatchMessagesPerIdx() { - return mismatchMessagesPerIdx; + public List getMismatchAndMissing() { + return mismatchAndMissing; } public CompareToComparator getComparator() { diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandler.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandler.java index 2c2cbb8eb..5964dfade 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandler.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandler.java @@ -24,8 +24,6 @@ import java.util.Map; -import static org.testingisdocumenting.webtau.WebTauCore.*; - public class MapContainHandler implements ContainHandler { @Override public boolean handle(Object actual, Object expected) { @@ -67,7 +65,7 @@ private void analyzeMapAndMapSingleExpectedEntry(ContainAnalyzer containAnalyzer Map.Entry expectedEntry, boolean isNegative) { if (!actualMap.containsKey(expectedEntry.getKey())) { - containAnalyzer.reportMismatch(this, propertyPath, tokenizedMessage().matcher("is missing")); + containAnalyzer.reportMissing(this, propertyPath, expectedEntry.getValue()); } else { CompareToComparator comparator = CompareToComparator.comparator(); @@ -78,7 +76,8 @@ private void analyzeMapAndMapSingleExpectedEntry(ContainAnalyzer containAnalyzer containAnalyzer.registerConvertedActualByPath(comparator.getConvertedActualByPath()); if (!actualValueEqual) { - containAnalyzer.reportMismatch(this, propertyPath, comparator.generateEqualMismatchReport()); + comparator.getMissingMessages().forEach(m -> containAnalyzer.reportMissing(this, m)); + comparator.getNotEqualMessages().forEach(m -> containAnalyzer.reportMismatch(this, m)); } else { containAnalyzer.reportMatch(this, propertyPath, comparator.generateEqualMatchReport()); } diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/equality/CompareToComparator.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/equality/CompareToComparator.java index 584f00fca..fe010ede4 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/equality/CompareToComparator.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/equality/CompareToComparator.java @@ -31,6 +31,7 @@ import java.util.stream.Stream; import static org.testingisdocumenting.webtau.WebTauCore.*; +import static org.testingisdocumenting.webtau.expectation.TokenizedReportUtils.*; public class CompareToComparator { public enum AssertionMode { @@ -157,34 +158,38 @@ public AssertionMode getAssertionMode() { } public TokenizedMessage generateGreaterThanMismatchReport() { - return generateReportPartWithoutLabel(Stream.of(lessMessages, equalMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(lessMessages, equalMessages)); } public TokenizedMessage generateGreaterThanOrEqualMismatchReport() { - return generateReportPartWithoutLabel(Stream.of(lessMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(lessMessages)); } public TokenizedMessage generateLessThanOrEqualMismatchReport() { - return generateReportPartWithoutLabel(Stream.of(greaterMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(greaterMessages)); } public TokenizedMessage generateLessThanMismatchReport() { - return generateReportPartWithoutLabel(Stream.of(greaterMessages, equalMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(greaterMessages, equalMessages)); } public TokenizedMessage generateNotEqualMismatchReport() { - return generateReportPartWithoutLabel(Stream.of(equalMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(equalMessages)); } public TokenizedMessage generateEqualMismatchReport() { if (missingMessages.isEmpty() && extraMessages.isEmpty()) { - return generateReportPartWithoutLabel(Stream.of(notEqualMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(notEqualMessages)); } return combineReportParts( - generateReportPart(MISMATCHES_LABEL, Collections.singletonList(notEqualMessages)), - generateReportPart(tokenizedMessage().error("missing, but expected values"), Collections.singletonList(missingMessages)), - generateReportPart(tokenizedMessage().error("unexpected values"), Collections.singletonList(extraMessages))); + generateReportPart(topLevelActualPath, MISMATCHES_LABEL, Collections.singletonList(notEqualMessages)), + generateReportPart(topLevelActualPath, tokenizedMessage().error("missing, but expected values"), Collections.singletonList(missingMessages)), + generateReportPart(topLevelActualPath, tokenizedMessage().error("unexpected values"), Collections.singletonList(extraMessages))); + } + + public List getMissingMessages() { + return missingMessages; } public List getNotEqualMessages() { @@ -201,33 +206,33 @@ public Set generateEqualMismatchPaths() { public TokenizedMessage generateNotEqualMatchReport() { if (missingMessages.isEmpty() && extraMessages.isEmpty()) { - return generateReportPartWithoutLabel(Stream.of(notEqualMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(notEqualMessages)); } return combineReportParts( - generateReportPart(MATCHES_LABEL, Collections.singletonList(notEqualMessages)), - generateReportPart(tokenizedMessage().matcher("missing values"), Collections.singletonList(missingMessages)), - generateReportPart(tokenizedMessage().matcher("extra values"), Collections.singletonList(extraMessages))); + generateReportPart(topLevelActualPath, MATCHES_LABEL, Collections.singletonList(notEqualMessages)), + generateReportPart(topLevelActualPath, tokenizedMessage().matcher("missing values"), Collections.singletonList(missingMessages)), + generateReportPart(topLevelActualPath, tokenizedMessage().matcher("extra values"), Collections.singletonList(extraMessages))); } public TokenizedMessage generateGreaterThanMatchReport() { - return generateReportPartWithoutLabel(Stream.of(greaterMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(greaterMessages)); } public TokenizedMessage generateGreaterThanOrEqualMatchReport() { - return generateReportPartWithoutLabel(Stream.of(greaterMessages, equalMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(greaterMessages, equalMessages)); } public TokenizedMessage generateLessThanMatchReport() { - return generateReportPartWithoutLabel(Stream.of(lessMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(lessMessages)); } public TokenizedMessage generateLessThanOrEqualToMatchReport() { - return generateReportPartWithoutLabel(Stream.of(equalMessages, lessMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(equalMessages, lessMessages)); } public TokenizedMessage generateEqualMatchReport() { - return generateReportPartWithoutLabel(Stream.of(equalMessages)); + return generateReportPartWithoutLabel(topLevelActualPath, Stream.of(equalMessages)); } public Set generateEqualMatchPaths() { @@ -390,68 +395,6 @@ private void mergeResults(CompareToComparator comparator) { convertedActualByPath.putAll(comparator.convertedActualByPath); } - private TokenizedMessage generateReportPart(TokenizedMessage label, List> messagesGroups) { - if (messagesGroups.stream().allMatch(List::isEmpty)) { - return tokenizedMessage(); - } - - return tokenizedMessage().add(label).colon().doubleNewLine().add(generateReportPartWithoutLabel(messagesGroups.stream())); - } - - private TokenizedMessage generateReportPartWithoutLabel(Stream> messagesGroupsStream) { - List> messagesGroups = messagesGroupsStream.filter(group -> !group.isEmpty()).toList(); - if (messagesGroups.isEmpty()) { - return tokenizedMessage(); - } - - TokenizedMessage result = tokenizedMessage(); - int groupIdx = 0; - for (List group : messagesGroups) { - int messageIdx = 0; - for (ValuePathMessage message : group) { - boolean useFullMessage = !message.getActualPath().equals(topLevelActualPath); - result.add(useFullMessage ? message.getFullMessage() : message.getMessage()); - - boolean isLast = messageIdx == group.size() - 1; - if (!isLast) { - result.newLine(); - } - messageIdx++; - } - - boolean isLastGroup = groupIdx == messagesGroups.size() - 1; - if (!isLastGroup) { - result.newLine(); - } - - groupIdx++; - } - - return result; - } - - private TokenizedMessage combineReportParts(TokenizedMessage... parts) { - TokenizedMessage result = tokenizedMessage(); - - List 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; - } - private static CompareToHandler findCompareToGreaterLessHandler(Object actual, Object expected) { return handlers.stream(). filter(h -> h.handleGreaterLessEqual(actual, expected)).findFirst(). diff --git a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcherTest.groovy b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcherTest.groovy index 861641548..46eaed9f3 100644 --- a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcherTest.groovy +++ b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/ContainAllMatcherTest.groovy @@ -42,7 +42,11 @@ class ContainAllMatcherTest { @Test void "negative matcher fails only when all the values are present "() { - runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to not contain all ["b", "a"] (Xms)\n' + + runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to not contain all ["b", "a"]:\n' + + ' [value][1]: actual: "b" \n' + + ' expected: not "b" \n' + + ' [value][0]: actual: "a" \n' + + ' expected: not "a" (Xms)\n' + ' \n' + ' [**"a"**, **"b"**, "d"]') { actual(['a', 'b', 'd']).shouldNot(containAll('b', 'a')) diff --git a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandlerTest.groovy b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandlerTest.groovy index ec0e2bfe4..5a7625d85 100644 --- a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandlerTest.groovy +++ b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndSingleValueContainHandlerTest.groovy @@ -1,4 +1,5 @@ /* + * Copyright 2023 webtau maintainers * Copyright 2019 TWO SIGMA OPEN SOURCE, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -63,7 +64,10 @@ class IterableAndSingleValueContainHandlerTest { @Test void "displays complex type when no match found"() { - runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {"firstName": "FN31", "lastName": "LN3"}: no match found (Xms)\n' + + runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {"firstName": "FN31", "lastName": "LN3"}:\n' + + ' [value][2].firstName: actual: "FN3" \n' + + ' expected: "FN31" \n' + + ' ^ (Xms)\n' + ' \n' + ' [\n' + ' {"firstName": "FN1", "lastName": "LN1"},\n' + @@ -78,6 +82,36 @@ class IterableAndSingleValueContainHandlerTest { } } + @Test + void "displays missing and mismatched of the suspect"() { + runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {"firstName": "FN2", "lastName": "LN3", "game": "on"}:\n' + + ' possible mismatches:\n' + + ' \n' + + ' [value][1].lastName: actual: "LN2" \n' + + ' expected: "LN3" \n' + + ' ^\n' + + ' [value][2].firstName: actual: "FN3" \n' + + ' expected: "FN2" \n' + + ' ^\n' + + ' \n' + + ' possible missing values:\n' + + ' \n' + + ' [value][1].game: "on"\n' + + ' [value][2].game: "on" (Xms)\n' + + ' \n' + + ' [\n' + + ' {"firstName": "FN1", "lastName": "LN1", "extra": "T", "game": },\n' + + ' {"firstName": "FN2", "lastName": **"LN2"**, "extra": "B", "game": ****},\n' + + ' {"firstName": **"FN3"**, "lastName": "LN3", "wrong": "C", "game": ****}\n' + + ' ]') { + actual([ + [firstName: 'FN1', lastName: 'LN1', extra: "T"], + [firstName: 'FN2', lastName: 'LN2', extra: "B"], + [firstName: 'FN3', lastName: 'LN3', wrong: "C"], + ]).should(contain([firstName: 'FN2', lastName: 'LN3', game: "on"])) + } + } + @Test void "complex values not contain passes when only a subset of fields matches"() { actual([ @@ -110,8 +144,10 @@ class IterableAndSingleValueContainHandlerTest { @Test void "contain matcher throws when doesn't match"() { - code { + runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain "wod": no match found (Xms)\n' + + ' \n' + + ' ["hello", "world", "of", "testing"]') { actual(['hello', 'world', 'of', 'testing']).should(contain('wod')) - } should throwException("no match found") + } } } diff --git a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndTableContainHandlerTest.groovy b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndTableContainHandlerTest.groovy index e28f0174d..7fdaf0424 100644 --- a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndTableContainHandlerTest.groovy +++ b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/IterableAndTableContainHandlerTest.groovy @@ -57,7 +57,19 @@ class IterableAndTableContainHandlerTest { ' "a1" │ "b1"\n' + ' "a2" │ "b3"\n' + ' "a3" │ "b4":\n' + - ' no matches found for: [{"a": "a2", "b": "b3"}, {"a": "a3", "b": "b4"}] (Xms)\n' + + ' no matches found for: [{"a": "a2", "b": "b3"}, {"a": "a3", "b": "b4"}]\n' + + ' [value][1].b: actual: "b2" \n' + + ' expected: "b3" \n' + + ' ^\n' + + ' [value][2].a: actual: "a3" \n' + + ' expected: "a2" \n' + + ' ^\n' + + ' [value][2].b: actual: "b3" \n' + + ' expected: "b4" \n' + + ' ^\n' + + ' [value][3].a: actual: "a4" \n' + + ' expected: "a3" \n' + + ' ^ (Xms)\n' + ' \n' + ' [{"a": "a1", "b": "b1"}, {"a": "a2", "b": **"b2"**}, {"a": **"a3"**, "b": **"b3"**}, {"a": **"a4"**, "b": "b4"}]') { actual(maps).should(contain(table)) diff --git a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandlerTest.groovy b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandlerTest.groovy index ac27c5f82..42eb58549 100644 --- a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandlerTest.groovy +++ b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/expectation/contain/handlers/MapContainHandlerTest.groovy @@ -43,7 +43,9 @@ class MapContainHandlerTest { def actualMap = [hello: 1, world: 2] def expectedMap = [hello: 1, world: 3] - runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {"hello": 1, "world": 3}: no match found (Xms)\n' + + runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {"hello": 1, "world": 3}:\n' + + ' [value].world: actual: 2 \n' + + ' expected: 3 (Xms)\n' + ' \n' + ' {"hello": 1, "world": **2**}') { actual(actualMap).should(contain(expectedMap)) @@ -52,12 +54,49 @@ class MapContainHandlerTest { @Test void "should detect missing key"() { - def actualMap = [hello: 1, world: 2, nested: [test: "value"]] - def expectedMap = [hello: 1, world: 2, extra: 3, nested: [test: "value", another: "a-value"]] + def actualMap = [hello: 1, world: 2, nested: [test: "value"], anotherExtra: [gold: "fish"]] + def expectedMap = [hello: 1, world: 2, extra: 3, nested: [test: "value", another: "a-value"], anotherExtra: [nestedInExtra: 100]] + + runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {\n' + + ' "hello": 1,\n' + + ' "world": 2,\n' + + ' "extra": 3,\n' + + ' "nested": {"test": "value", "another": "a-value"},\n' + + ' "anotherExtra": {"nestedInExtra": 100}\n' + + ' }:\n' + + ' missing values:\n' + + ' \n' + + ' [value].extra: 3\n' + + ' [value].nested.another: "a-value"\n' + + ' [value].anotherExtra.nestedInExtra: 100 (Xms)\n' + + ' \n' + + ' {\n' + + ' "hello": 1,\n' + + ' "world": 2,\n' + + ' "nested": {"test": "value", "another": ****},\n' + + ' "anotherExtra": {"gold": "fish", "nestedInExtra": ****},\n' + + ' "extra": ****\n' + + ' }') { + actual(actualMap).should(contain(expectedMap)) + } + } - runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {"hello": 1, "world": 2, "extra": 3, "nested": {"test": "value", "another": "a-value"}}: no match found (Xms)\n' + + @Test + void "should display both missing and mismatched"() { + def actualMap = [hello: 1, world: 2] + def expectedMap = [hello: 2, another: 3] + + runExpectExceptionAndValidateOutput(AssertionError, 'X failed expecting [value] to contain {"hello": 2, "another": 3}:\n' + + ' mismatches:\n' + + ' \n' + + ' [value].hello: actual: 1 \n' + + ' expected: 2 \n' + + ' \n' + + ' missing values:\n' + + ' \n' + + ' [value].another: 3 (Xms)\n' + ' \n' + - ' {"hello": 1, "world": 2, "nested": **{"test": "value", "another": }**, "extra": ****}') { + ' {"hello": **1**, "world": 2, "another": ****}') { actual(actualMap).should(contain(expectedMap)) } } diff --git a/webtau-core/src/test/java/org/testingisdocumenting/webtau/MatchersTest.java b/webtau-core/src/test/java/org/testingisdocumenting/webtau/MatchersTest.java index 056aac4a3..8b1158d4d 100644 --- a/webtau-core/src/test/java/org/testingisdocumenting/webtau/MatchersTest.java +++ b/webtau-core/src/test/java/org/testingisdocumenting/webtau/MatchersTest.java @@ -232,10 +232,13 @@ public void listOfBeansAndTableEqualityExample() { @Test public void listOfBeansAndTableContainExample() { runExpectExceptionCaptureAndValidateOutput(AssertionError.class, "beans-table-contain-output", """ - X failed expecting [value] to contain id │ name │ address \s - "ac2" │ "Works" │ {"zipCode": "zip2"} - "ac1" │ "Home" │ {"zipCode": "zip1"}: - no matches found for: [{"id": "ac2", "name": "Works", "address": {"zipCode": "zip2"}}] (Xms) + X failed expecting accounts to contain id │ name │ address \s + "ac2" │ "Works" │ {"zipCode": "zip2"} + "ac1" │ "Home" │ {"zipCode": "zip1"}: + no matches found for: [{"id": "ac2", "name": "Works", "address": {"zipCode": "zip2"}}] + accounts[1].name: actual: "Work" + expected: "Works" + ^ (Xms) \s [ { @@ -258,13 +261,17 @@ public void listOfBeansAndTableContainExample() { } ]""", () -> { // beans-table-contain-example + + List accounts = fetchAccounts(); TableData expected = table("id", "name", "address", _______________________________________, "ac2", "Works", map("zipCode", "zip2"), "ac1", "Home", map("zipCode", "zip1")); - actual(accounts).should(contain(expected)); + actual(accounts, "accounts").should(contain(expected)); + + // beans-table-contain-example }); } diff --git a/webtau-core/src/test/java/org/testingisdocumenting/webtau/expectation/equality/handlers/MapMatchersJavaExamplesTest.java b/webtau-core/src/test/java/org/testingisdocumenting/webtau/expectation/equality/handlers/MapMatchersJavaExamplesTest.java index 79248f746..c86bb6f74 100644 --- a/webtau-core/src/test/java/org/testingisdocumenting/webtau/expectation/equality/handlers/MapMatchersJavaExamplesTest.java +++ b/webtau-core/src/test/java/org/testingisdocumenting/webtau/expectation/equality/handlers/MapMatchersJavaExamplesTest.java @@ -59,14 +59,17 @@ public void equalityMismatch() { @Test public void containMismatch() { runExpectExceptionCaptureAndValidateOutput(AssertionError.class, "maps-contain-console-output", """ - X failed expecting [value] to contain {"firstName": "G-FN", "lastName": "G-LN", "middleName": "G-MD"}: no match found (Xms) + X failed expecting [value] to contain {"firstName": "G-FN", "lastName": "G-LN", "middleName": "G-MD"}: + missing values: + \s + [value].middleName: "G-MD" (Xms) \s { "firstName": "G-FN", "lastName": "G-LN", "address": {"street": "generated-street", "city": "GenSity"}, "middleName": **** - }""", () -> { + }""",() -> { // maps-contain-mismatch Map generated = generate(); actual(generated).should(contain( diff --git a/webtau-docs/znai/release-notes/2.2/add-2023-08-23-contains-extra-details.md b/webtau-docs/znai/release-notes/2.2/add-2023-08-23-contains-extra-details.md new file mode 100644 index 000000000..5c871a1a3 --- /dev/null +++ b/webtau-docs/znai/release-notes/2.2/add-2023-08-23-contains-extra-details.md @@ -0,0 +1,2 @@ +* Add: [Contain Matcher](matchers/java-beans-and-records#java-beans-contain-table-data) now prints potential mismatch details to look for +* Add: `Map` `contains` `Map` explicitly prints missing keys \ No newline at end of file From 20a82b676472c2afba3d3621d8b468fea898bf3c Mon Sep 17 00:00:00 2001 From: mykolagolubyev Date: Tue, 29 Aug 2023 21:00:02 -0400 Subject: [PATCH 2/2] fix --- .../webtau/expectation/contain/ContainAnalyzer.java | 2 +- .../webtau/expectation/contain/ContainMatcher.java | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java index 88cd8dd55..78b4c86e4 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainAnalyzer.java @@ -131,7 +131,7 @@ public TokenizedMessage generateMatchReport() { public TokenizedMessage generateMismatchReport() { TokenizedMessage reportDetails = generateMismatchReportDetails(mismatchedExpectedValues.isEmpty()); - return reportDetails.isEmpty() ? + return reportDetails.isEmpty() && mismatchedExpectedValues.isEmpty() ? tokenizedMessage().error("no match found") : reportDetails; } diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java index a008780fa..6cea19eb2 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/expectation/contain/ContainMatcher.java @@ -68,13 +68,15 @@ public TokenizedMessage matchedTokenizedMessage(ValuePath actualPath, Object act @Override public TokenizedMessage mismatchedTokenizedMessage(ValuePath actualPath, Object actual) { List mismatchedExpected = containAnalyzer.getMismatchedExpectedValues(); + TokenizedMessage mismatchReport = containAnalyzer.generateMismatchReport(); if (mismatchedExpected.isEmpty() || (mismatchedExpected.size() == 1 && mismatchedExpected.get(0) == expected)) { - return containAnalyzer.generateMismatchReport(); + return mismatchReport.isEmpty() ? tokenizedMessage().error("no match found") : mismatchReport; } - return tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues()) - .newLine() - .add(containAnalyzer.generateMismatchReport()); + TokenizedMessage report = tokenizedMessage().error("no matches found for").colon().value(containAnalyzer.getMismatchedExpectedValues()); + return mismatchReport.isEmpty() ? + report : + report.newLine().add(mismatchReport); } @Override