Skip to content

Commit c9bf4ee

Browse files
matchers: add containExactly matcher (#1551)
1 parent 3ca09f3 commit c9bf4ee

File tree

10 files changed

+392
-2
lines changed

10 files changed

+392
-2
lines changed

webtau-core/src/main/java/org/testingisdocumenting/webtau/Matchers.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.testingisdocumenting.webtau.expectation.code.ChangeCodeMatcher;
2424
import org.testingisdocumenting.webtau.expectation.code.ThrowExceptionMatcher;
2525
import org.testingisdocumenting.webtau.expectation.contain.ContainAllMatcher;
26+
import org.testingisdocumenting.webtau.expectation.contain.ContainExactlyMatcher;
2627
import org.testingisdocumenting.webtau.expectation.contain.ContainMatcher;
2728
import org.testingisdocumenting.webtau.expectation.equality.*;
2829
import org.testingisdocumenting.webtau.expectation.state.HiddenValueMatcher;
@@ -208,6 +209,18 @@ public static ContainAllMatcher containingAll(Object... expected) {
208209
return new ContainAllMatcher(Arrays.asList(expected));
209210
}
210211

212+
/**
213+
* Contain exact matcher
214+
* <pre>
215+
* actual(collection).should(containExact(el1, el2, el3));
216+
* </pre>
217+
* @param expected list of values to check
218+
* @return matcher instance
219+
*/
220+
public static ContainExactlyMatcher containExactly(Object... expected) {
221+
return new ContainExactlyMatcher(Arrays.asList(expected));
222+
}
223+
211224
/**
212225
* Greater than matcher
213226
* <pre>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2023 webtau maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.webtau.data;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
public class ValuePathWithValue<E> {
23+
private final ValuePath path;
24+
private final E value;
25+
26+
ValuePathWithValue(ValuePath path, E value) {
27+
this.path = path;
28+
this.value = value;
29+
}
30+
31+
public static List<ValuePathWithValue<Object>> listFromIterable(ValuePath path, Iterable<Object> iterable) {
32+
List<ValuePathWithValue<Object>> result = new ArrayList<>();
33+
int idx = 0;
34+
for (Object v : iterable) {
35+
result.add(new ValuePathWithValue<>(path.index(idx), v));
36+
idx++;
37+
}
38+
39+
return result;
40+
}
41+
42+
public ValuePath getPath() {
43+
return path;
44+
}
45+
46+
public E getValue() {
47+
return value;
48+
}
49+
}

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
@@ -64,7 +64,7 @@ public TokenizedMessage matchingTokenizedMessage(ValuePath actualPath, Object ac
6464

6565
@Override
6666
public TokenizedMessage matchedTokenizedMessage(ValuePath actualPath, Object actual) {
67-
return tokenizedMessage().matcher("contains all").value(expectedList);
67+
return tokenizedMessage().matcher("contains all").valueFirstLinesOnly(expectedList);
6868
}
6969

7070
@Override
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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;
18+
19+
import org.testingisdocumenting.webtau.data.ValuePath;
20+
import org.testingisdocumenting.webtau.data.ValuePathWithValue;
21+
import org.testingisdocumenting.webtau.data.converters.ValueConverter;
22+
import org.testingisdocumenting.webtau.data.render.PrettyPrintable;
23+
import org.testingisdocumenting.webtau.data.render.PrettyPrinter;
24+
import org.testingisdocumenting.webtau.expectation.ExpectedValuesAware;
25+
import org.testingisdocumenting.webtau.expectation.ValueMatcher;
26+
import org.testingisdocumenting.webtau.expectation.equality.CompareToComparator;
27+
import org.testingisdocumenting.webtau.reporter.TokenizedMessage;
28+
29+
import java.util.*;
30+
import java.util.stream.Collectors;
31+
import java.util.stream.Stream;
32+
33+
import static org.testingisdocumenting.webtau.WebTauCore.*;
34+
35+
public class ContainExactlyMatcher implements ValueMatcher, ExpectedValuesAware, PrettyPrintable {
36+
private final Collection<Object> expectedList;
37+
private List<ValuePathWithValue<Object>> actualCopy;
38+
private List<ValuePathWithValue<Object>> expectedCopy;
39+
40+
private CompareToComparator comparator;
41+
42+
public ContainExactlyMatcher(Collection<Object> expected) {
43+
expectedList = expected;
44+
}
45+
46+
@Override
47+
public ValueConverter valueConverter() {
48+
return comparator == null ? ValueConverter.EMPTY : comparator.createValueConverter();
49+
}
50+
51+
@Override
52+
public Stream<Object> expectedValues() {
53+
return expectedList.stream();
54+
}
55+
56+
@Override
57+
public Set<ValuePath> mismatchedPaths() {
58+
return actualCopy.stream().map(ValuePathWithValue::getPath).collect(Collectors.toSet());
59+
}
60+
61+
@Override
62+
public TokenizedMessage matchingTokenizedMessage(ValuePath actualPath, Object actual) {
63+
comparator = CompareToComparator.comparator(CompareToComparator.AssertionMode.EQUAL);
64+
return tokenizedMessage().matcher("to contain exactly").valueFirstLinesOnly(expectedList);
65+
}
66+
67+
@Override
68+
public TokenizedMessage matchedTokenizedMessage(ValuePath actualPath, Object actual) {
69+
return tokenizedMessage().matcher("contains exactly").valueFirstLinesOnly(expectedList);
70+
}
71+
72+
@Override
73+
public TokenizedMessage mismatchedTokenizedMessage(ValuePath actualPath, Object actual) {
74+
TokenizedMessage messageTokens = tokenizedMessage();
75+
if (!expectedCopy.isEmpty()) {
76+
messageTokens = messageTokens.error("no matches found for").colon().value(
77+
expectedCopy.stream().map(ValuePathWithValue::getValue).toList());
78+
}
79+
80+
if (!actualCopy.isEmpty()) {
81+
if (!messageTokens.isEmpty()) {
82+
messageTokens = messageTokens.newLine();
83+
}
84+
messageTokens = messageTokens.error("unexpected elements").colon().value(
85+
actualCopy.stream().map(ValuePathWithValue::getValue).toList());
86+
}
87+
88+
return messageTokens;
89+
}
90+
91+
@Override
92+
public boolean matches(ValuePath actualPath, Object actualIterable) {
93+
return matches(comparator, actualPath, actualIterable);
94+
}
95+
96+
@Override
97+
public TokenizedMessage negativeMatchingTokenizedMessage(ValuePath actualPath, Object actual) {
98+
comparator = CompareToComparator.comparator(CompareToComparator.AssertionMode.NOT_EQUAL);
99+
return tokenizedMessage().matcher("to not contain exactly").valueFirstLinesOnly(expectedList);
100+
}
101+
102+
@Override
103+
public TokenizedMessage negativeMatchedTokenizedMessage(ValuePath actualPath, Object actual) {
104+
return tokenizedMessage().matcher("not contains exactly").valueFirstLinesOnly(expectedList);
105+
}
106+
107+
@Override
108+
public TokenizedMessage negativeMismatchedTokenizedMessage(ValuePath actualPath, Object actual) {
109+
return tokenizedMessage().matcher("contains exactly").value(expectedList);
110+
}
111+
112+
@Override
113+
public boolean negativeMatches(ValuePath actualPath, Object actualIterable) {
114+
return !matches(comparator, actualPath, actualIterable);
115+
}
116+
117+
@Override
118+
public void prettyPrint(PrettyPrinter printer) {
119+
printer.printDelimiter("<");
120+
printer.print(PrettyPrinter.CLASSIFIER_COLOR, "containExactly ");
121+
printer.printObject(expectedList);
122+
printer.printDelimiter(">");
123+
}
124+
125+
@Override
126+
public String toString() {
127+
return PrettyPrinter.renderAsTextWithoutColors(this);
128+
}
129+
130+
@SuppressWarnings("unchecked")
131+
private boolean matches(CompareToComparator comparator, ValuePath actualPath, Object actualIterable) {
132+
if (!(actualIterable instanceof Iterable)) {
133+
return false;
134+
}
135+
136+
actualCopy = ValuePathWithValue.listFromIterable(actualPath, ((Iterable<Object>) actualIterable));
137+
expectedCopy = ValuePathWithValue.listFromIterable(actualPath, expectedList);
138+
139+
Iterator<ValuePathWithValue<Object>> expectedIt = expectedCopy.iterator();
140+
while (expectedIt.hasNext()) {
141+
ValuePathWithValue<Object> expected = expectedIt.next();
142+
Iterator<ValuePathWithValue<Object>> actualIt = actualCopy.iterator();
143+
while (actualIt.hasNext()) {
144+
ValuePathWithValue<Object> actual = actualIt.next();
145+
boolean isEqual = comparator.compareIsEqual(actual.getPath(), actual.getValue(), expected.getValue());
146+
if (isEqual) {
147+
actualIt.remove();
148+
expectedIt.remove();
149+
break;
150+
}
151+
}
152+
}
153+
154+
return actualCopy.isEmpty() && expectedCopy.isEmpty();
155+
}
156+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.util.List;
2323
import java.util.stream.Stream;
2424

25-
record CombinedMismatchAndMissing(List<ValuePathMessage> mismatchMessages, List<ValuePathMessage> missingMessage) {
25+
public record CombinedMismatchAndMissing(List<ValuePathMessage> mismatchMessages, List<ValuePathMessage> missingMessage) {
2626
int size() {
2727
return mismatchMessages.size() + missingMessage.size();
2828
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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
18+
19+
import org.junit.Test
20+
import org.testingisdocumenting.webtau.data.Person
21+
22+
import static org.testingisdocumenting.webtau.WebTauCore.*
23+
24+
class ContainExactlyMatcherGroovyTest {
25+
@Test
26+
void matchRecordsAndMaps() {
27+
// records-and-maps-example
28+
def list = [new Person("id1", 3, 10),
29+
new Person("id2", 4, 20),
30+
new Person("id2", 4, 20)]
31+
32+
actual(list).should(containExactly(
33+
[id: "id2", level: 4, monthsAtCompany: 20],
34+
[id: "id1", level: 3, monthsAtCompany: 10],
35+
[id: "id2", level: 4, monthsAtCompany: 20]))
36+
// records-and-maps-example
37+
}
38+
}

0 commit comments

Comments
 (0)