Skip to content

Commit d44be00

Browse files
guyben13ronshapiro
authored andcommitted
Add containsAtLeast to MapSubject and MultimapSubject
RELNOTES=added containsAtLeast and containsAtLeastEntriesIn to MapSubject and MultimapSubject. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=232282456
1 parent 81ac47d commit d44be00

File tree

4 files changed

+1332
-33
lines changed

4 files changed

+1332
-33
lines changed

core/src/main/java/com/google/common/truth/MapSubject.java

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void isEqualTo(@NullableDecl Object other) {
7070
return;
7171
}
7272

73-
boolean mapEquals = containsExactlyEntriesInAnyOrder((Map<?, ?>) other, "is equal to");
73+
boolean mapEquals = containsEntriesInAnyOrder((Map<?, ?>) other, "is equal to", false);
7474
if (mapEquals) {
7575
failWithoutActual(
7676
simpleFact(
@@ -194,11 +194,16 @@ public Ordered containsExactly() {
194194
*/
195195
@CanIgnoreReturnValue
196196
public Ordered containsExactly(@NullableDecl Object k0, @NullableDecl Object v0, Object... rest) {
197-
return containsExactlyEntriesIn(accumulateMap(k0, v0, rest));
197+
return containsExactlyEntriesIn(accumulateMap("containsExactly", k0, v0, rest));
198+
}
199+
200+
@CanIgnoreReturnValue
201+
public Ordered containsAtLeast(@NullableDecl Object k0, @NullableDecl Object v0, Object... rest) {
202+
return containsAtLeastEntriesIn(accumulateMap("containsAtLeast", k0, v0, rest));
198203
}
199204

200205
private static Map<Object, Object> accumulateMap(
201-
@NullableDecl Object k0, @NullableDecl Object v0, Object... rest) {
206+
String functionName, @NullableDecl Object k0, @NullableDecl Object v0, Object... rest) {
202207
checkArgument(
203208
rest.length % 2 == 0,
204209
"There must be an equal number of key/value pairs "
@@ -216,8 +221,9 @@ private static Map<Object, Object> accumulateMap(
216221
}
217222
checkArgument(
218223
keys.size() == expectedMap.size(),
219-
"Duplicate keys (%s) cannot be passed to containsExactly().",
220-
keys);
224+
"Duplicate keys (%s) cannot be passed to %s().",
225+
keys,
226+
functionName);
221227
return expectedMap;
222228
}
223229

@@ -232,18 +238,35 @@ public Ordered containsExactlyEntriesIn(Map<?, ?> expectedMap) {
232238
return ALREADY_FAILED;
233239
}
234240
}
235-
boolean containsAnyOrder = containsExactlyEntriesInAnyOrder(expectedMap, "contains exactly");
241+
boolean containsAnyOrder =
242+
containsEntriesInAnyOrder(expectedMap, "contains exactly", /* allowUnexpected= */ false);
236243
if (containsAnyOrder) {
237244
return new MapInOrder(expectedMap, "contains exactly these entries in order");
238245
} else {
239246
return ALREADY_FAILED;
240247
}
241248
}
242249

250+
/** Fails if the map does not contain at least the given set of entries in the given map. */
243251
@CanIgnoreReturnValue
244-
private boolean containsExactlyEntriesInAnyOrder(Map<?, ?> expectedMap, String failVerb) {
252+
public Ordered containsAtLeastEntriesIn(Map<?, ?> expectedMap) {
253+
if (expectedMap.isEmpty()) {
254+
return IN_ORDER;
255+
}
256+
boolean containsAnyOrder =
257+
containsEntriesInAnyOrder(expectedMap, "contains at least", /* allowUnexpected= */ true);
258+
if (containsAnyOrder) {
259+
return new MapInOrder(expectedMap, "contains at least these entries in order");
260+
} else {
261+
return ALREADY_FAILED;
262+
}
263+
}
264+
265+
@CanIgnoreReturnValue
266+
private boolean containsEntriesInAnyOrder(
267+
Map<?, ?> expectedMap, String failVerb, boolean allowUnexpected) {
245268
MapDifference<Object, Object, Object> diff =
246-
MapDifference.create(actual(), expectedMap, EQUALITY);
269+
MapDifference.create(actual(), expectedMap, allowUnexpected, EQUALITY);
247270
if (diff.isEmpty()) {
248271
return true;
249272
}
@@ -274,10 +297,12 @@ private static class MapDifference<K, A, E> {
274297
private final Map<K, E> missing;
275298
private final Map<K, A> unexpected;
276299
private final Map<K, ValueDifference<A, E>> wrongValues;
300+
private final Set<K> allKeys;
277301

278302
static <K, A, E> MapDifference<K, A, E> create(
279303
Map<? extends K, ? extends A> actual,
280304
Map<? extends K, ? extends E> expected,
305+
boolean allowUnexpected,
281306
ValueTester<? super A, ? super E> valueTester) {
282307
Map<K, A> unexpected = new LinkedHashMap<>(actual);
283308
Map<K, E> missing = new LinkedHashMap<>();
@@ -294,14 +319,22 @@ static <K, A, E> MapDifference<K, A, E> create(
294319
missing.put(expectedKey, expectedValue);
295320
}
296321
}
297-
return new MapDifference<>(missing, unexpected, wrongValues);
322+
if (allowUnexpected) {
323+
unexpected.clear();
324+
}
325+
return new MapDifference<>(
326+
missing, unexpected, wrongValues, Sets.union(actual.keySet(), expected.keySet()));
298327
}
299328

300329
private MapDifference(
301-
Map<K, E> missing, Map<K, A> unexpected, Map<K, ValueDifference<A, E>> wrongValues) {
330+
Map<K, E> missing,
331+
Map<K, A> unexpected,
332+
Map<K, ValueDifference<A, E>> wrongValues,
333+
Set<K> allKeys) {
302334
this.missing = missing;
303335
this.unexpected = unexpected;
304336
this.wrongValues = wrongValues;
337+
this.allKeys = allKeys;
305338
}
306339

307340
boolean isEmpty() {
@@ -343,7 +376,7 @@ private boolean includeKeyTypes() {
343376
keys.addAll(missing.keySet());
344377
keys.addAll(unexpected.keySet());
345378
keys.addAll(wrongValues.keySet());
346-
return hasMatchingToStringPair(keys, keys);
379+
return hasMatchingToStringPair(keys, allKeys);
347380
}
348381
}
349382

@@ -413,10 +446,19 @@ private class MapInOrder implements Ordered {
413446
this.failVerb = failVerb;
414447
}
415448

449+
/**
450+
* Checks whether the common elements between actual and expected are in the same order.
451+
*
452+
* <p>This doesn't check whether the keys have the same values or whether all the required keys
453+
* are actually present. That was supposed to be done before the "in order" part.
454+
*/
416455
@Override
417456
public void inOrder() {
418-
List<?> expectedKeyOrder = Lists.newArrayList(expectedMap.keySet());
419-
List<?> actualKeyOrder = Lists.newArrayList(actual().keySet());
457+
// We're using the fact that Sets.intersection keeps the order of the first set.
458+
List<?> expectedKeyOrder =
459+
Lists.newArrayList(Sets.intersection(expectedMap.keySet(), actual().keySet()));
460+
List<?> actualKeyOrder =
461+
Lists.newArrayList(Sets.intersection(actual().keySet(), expectedMap.keySet()));
420462
if (!actualKeyOrder.equals(expectedKeyOrder)) {
421463
failWithoutActual(
422464
simpleFact(
@@ -614,10 +656,29 @@ public void doesNotContainEntry(
614656
@CanIgnoreReturnValue
615657
public Ordered containsExactly(@NullableDecl Object k0, @NullableDecl E v0, Object... rest) {
616658
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
617-
Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap(k0, v0, rest);
659+
Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsExactly", k0, v0, rest);
618660
return containsExactlyEntriesIn(expectedMap);
619661
}
620662

663+
/**
664+
* Fails if the map does not contain at least the given set of keys mapping to values that
665+
* correspond to the given values.
666+
*
667+
* <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
668+
* if any other type is encountered.
669+
*
670+
* <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
671+
* key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
672+
*/
673+
// TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
674+
// For bonus points, checking that the even-numbered values are of type E would be sweet.
675+
@CanIgnoreReturnValue
676+
public Ordered containsAtLeast(@NullableDecl Object k0, @NullableDecl E v0, Object... rest) {
677+
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
678+
Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsAtLeast", k0, v0, rest);
679+
return containsAtLeastEntriesIn(expectedMap);
680+
}
681+
621682
/**
622683
* Fails if the map does not contain exactly the keys in the given map, mapping to values that
623684
* correspond to the values of the given map.
@@ -632,11 +693,29 @@ public <K, V extends E> Ordered containsExactlyEntriesIn(Map<K, V> expectedMap)
632693
return ALREADY_FAILED;
633694
}
634695
}
696+
return internalContainsEntriesIn("exactly", expectedMap, false);
697+
}
698+
699+
/**
700+
* Fails if the map does not contain at least the keys in the given map, mapping to values that
701+
* correspond to the values of the given map.
702+
*/
703+
@CanIgnoreReturnValue
704+
public <K, V extends E> Ordered containsAtLeastEntriesIn(Map<K, V> expectedMap) {
705+
if (expectedMap.isEmpty()) {
706+
return IN_ORDER;
707+
}
708+
return internalContainsEntriesIn("at least", expectedMap, true);
709+
}
710+
711+
private <K, V extends E> Ordered internalContainsEntriesIn(
712+
String modifier, Map<K, V> expectedMap, boolean allowUnexpected) {
635713
final Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
636714
MapDifference<Object, A, V> diff =
637715
MapDifference.create(
638716
getCastSubject(),
639717
expectedMap,
718+
allowUnexpected,
640719
new ValueTester<A, E>() {
641720
@Override
642721
public boolean test(A actualValue, E expectedValue) {
@@ -650,18 +729,19 @@ public boolean test(A actualValue, E expectedValue) {
650729
return new MapInOrder(
651730
expectedMap,
652731
lenientFormat(
653-
"contains, in order, exactly one entry that has a key that is equal to and a value "
654-
+ "that %s the key and value of each entry of",
655-
correspondence));
732+
"contains, in order, %s one entry that has a key that is equal to and a "
733+
+ "value that %s the key and value of each entry of",
734+
modifier, correspondence));
656735
}
657736
failWithoutActual(
658737
facts(
659738
simpleFact(
660739
lenientFormat(
661-
"Not true that %s contains exactly one entry that has a key that is "
740+
"Not true that %s contains %s one entry that has a key that is "
662741
+ "equal to and a value that %s the key and value of each entry of "
663742
+ "<%s>. It %s",
664743
actualAsString(),
744+
modifier,
665745
correspondence,
666746
expectedMap,
667747
diff.describe(this.<V>valueDiffFormat(exceptions)))))

0 commit comments

Comments
 (0)