• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2011 Google, Inc.
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 package com.google.common.truth;
17 
18 import static com.google.common.base.Preconditions.checkArgument;
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.base.Strings.lenientFormat;
21 import static com.google.common.collect.Maps.immutableEntry;
22 import static com.google.common.truth.Fact.fact;
23 import static com.google.common.truth.Fact.simpleFact;
24 import static com.google.common.truth.SubjectUtils.countDuplicatesAndAddTypeInfo;
25 import static com.google.common.truth.SubjectUtils.hasMatchingToStringPair;
26 import static com.google.common.truth.SubjectUtils.objectToTypeName;
27 import static com.google.common.truth.SubjectUtils.retainMatchingToString;
28 import static java.util.Collections.singletonList;
29 
30 import com.google.common.base.Objects;
31 import com.google.common.collect.ImmutableList;
32 import com.google.common.collect.ImmutableMap;
33 import com.google.common.collect.LinkedHashMultiset;
34 import com.google.common.collect.Lists;
35 import com.google.common.collect.Maps;
36 import com.google.common.collect.Multiset;
37 import com.google.common.collect.Sets;
38 import com.google.common.truth.Correspondence.DiffFormatter;
39 import com.google.errorprone.annotations.CanIgnoreReturnValue;
40 import java.util.LinkedHashMap;
41 import java.util.LinkedHashSet;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 import org.checkerframework.checker.nullness.qual.Nullable;
46 
47 /**
48  * Propositions for {@link Map} subjects.
49  *
50  * @author Christian Gruber
51  * @author Kurt Alfred Kluever
52  */
53 public class MapSubject extends Subject {
54   private final @Nullable Map<?, ?> actual;
55 
56   /**
57    * Constructor for use by subclasses. If you want to create an instance of this class itself, call
58    * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}.
59    */
MapSubject(FailureMetadata metadata, @Nullable Map<?, ?> map)60   protected MapSubject(FailureMetadata metadata, @Nullable Map<?, ?> map) {
61     super(metadata, map);
62     this.actual = map;
63   }
64 
65   @Override
isEqualTo(@ullable Object other)66   public final void isEqualTo(@Nullable Object other) {
67     if (Objects.equal(actual, other)) {
68       return;
69     }
70 
71     // Fail but with a more descriptive message:
72 
73     if (actual == null || !(other instanceof Map)) {
74       super.isEqualTo(other);
75       return;
76     }
77 
78     containsEntriesInAnyOrder((Map<?, ?>) other, /* allowUnexpected= */ false);
79   }
80 
81   /** Fails if the map is not empty. */
isEmpty()82   public final void isEmpty() {
83     if (!checkNotNull(actual).isEmpty()) {
84       failWithActual(simpleFact("expected to be empty"));
85     }
86   }
87 
88   /** Fails if the map is empty. */
isNotEmpty()89   public final void isNotEmpty() {
90     if (checkNotNull(actual).isEmpty()) {
91       failWithoutActual(simpleFact("expected not to be empty"));
92     }
93   }
94 
95   /** Fails if the map does not have the given size. */
hasSize(int expectedSize)96   public final void hasSize(int expectedSize) {
97     checkArgument(expectedSize >= 0, "expectedSize (%s) must be >= 0", expectedSize);
98     check("size()").that(checkNotNull(actual).size()).isEqualTo(expectedSize);
99   }
100 
101   /** Fails if the map does not contain the given key. */
containsKey(@ullable Object key)102   public final void containsKey(@Nullable Object key) {
103     check("keySet()").that(checkNotNull(actual).keySet()).contains(key);
104   }
105 
106   /** Fails if the map contains the given key. */
doesNotContainKey(@ullable Object key)107   public final void doesNotContainKey(@Nullable Object key) {
108     check("keySet()").that(checkNotNull(actual).keySet()).doesNotContain(key);
109   }
110 
111   /** Fails if the map does not contain the given entry. */
containsEntry(@ullable Object key, @Nullable Object value)112   public final void containsEntry(@Nullable Object key, @Nullable Object value) {
113     Map.Entry<@Nullable Object, @Nullable Object> entry = immutableEntry(key, value);
114     checkNotNull(actual);
115     if (!actual.entrySet().contains(entry)) {
116       List<@Nullable Object> keyList = singletonList(key);
117       List<@Nullable Object> valueList = singletonList(value);
118       if (actual.containsKey(key)) {
119         Object actualValue = actual.get(key);
120         /*
121          * In the case of a null expected or actual value, clarify that the key *is* present and
122          * *is* expected to be present. That is, get() isn't returning null to indicate that the key
123          * is missing, and the user isn't making an assertion that the key is missing.
124          */
125         StandardSubjectBuilder check = check("get(%s)", key);
126         if (value == null || actualValue == null) {
127           check = check.withMessage("key is present but with a different value");
128         }
129         // See the comment on IterableSubject's use of failEqualityCheckForEqualsWithoutDescription.
130         check.that(actualValue).failEqualityCheckForEqualsWithoutDescription(value);
131       } else if (hasMatchingToStringPair(actual.keySet(), keyList)) {
132         failWithoutActual(
133             fact("expected to contain entry", entry),
134             fact("an instance of", objectToTypeName(entry)),
135             simpleFact("but did not"),
136             fact(
137                 "though it did contain keys",
138                 countDuplicatesAndAddTypeInfo(
139                     retainMatchingToString(actual.keySet(), /* itemsToCheck= */ keyList))),
140             fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
141       } else if (actual.containsValue(value)) {
142         Set<@Nullable Object> keys = new LinkedHashSet<>();
143         for (Map.Entry<?, ?> actualEntry : actual.entrySet()) {
144           if (Objects.equal(actualEntry.getValue(), value)) {
145             keys.add(actualEntry.getKey());
146           }
147         }
148         failWithoutActual(
149             fact("expected to contain entry", entry),
150             simpleFact("but did not"),
151             fact("though it did contain keys with that value", keys),
152             fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
153       } else if (hasMatchingToStringPair(actual.values(), valueList)) {
154         failWithoutActual(
155             fact("expected to contain entry", entry),
156             fact("an instance of", objectToTypeName(entry)),
157             simpleFact("but did not"),
158             fact(
159                 "though it did contain values",
160                 countDuplicatesAndAddTypeInfo(
161                     retainMatchingToString(actual.values(), /* itemsToCheck= */ valueList))),
162             fact("full contents", actualCustomStringRepresentationForPackageMembersToCall()));
163       } else {
164         failWithActual("expected to contain entry", entry);
165       }
166     }
167   }
168 
169   /** Fails if the map contains the given entry. */
doesNotContainEntry(@ullable Object key, @Nullable Object value)170   public final void doesNotContainEntry(@Nullable Object key, @Nullable Object value) {
171     checkNoNeedToDisplayBothValues("entrySet()")
172         .that(checkNotNull(actual).entrySet())
173         .doesNotContain(immutableEntry(key, value));
174   }
175 
176   /** Fails if the map is not empty. */
177   @CanIgnoreReturnValue
containsExactly()178   public final Ordered containsExactly() {
179     return containsExactlyEntriesIn(ImmutableMap.of());
180   }
181 
182   /**
183    * Fails if the map does not contain exactly the given set of key/value pairs.
184    *
185    * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
186    * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
187    *
188    * <p>The arguments must not contain duplicate keys.
189    */
190   @CanIgnoreReturnValue
containsExactly( @ullable Object k0, @Nullable Object v0, @Nullable Object... rest)191   public final Ordered containsExactly(
192       @Nullable Object k0, @Nullable Object v0, @Nullable Object... rest) {
193     return containsExactlyEntriesIn(accumulateMap("containsExactly", k0, v0, rest));
194   }
195 
196   @CanIgnoreReturnValue
containsAtLeast( @ullable Object k0, @Nullable Object v0, @Nullable Object... rest)197   public final Ordered containsAtLeast(
198       @Nullable Object k0, @Nullable Object v0, @Nullable Object... rest) {
199     return containsAtLeastEntriesIn(accumulateMap("containsAtLeast", k0, v0, rest));
200   }
201 
accumulateMap( String functionName, @Nullable Object k0, @Nullable Object v0, @Nullable Object... rest)202   private static Map<@Nullable Object, @Nullable Object> accumulateMap(
203       String functionName, @Nullable Object k0, @Nullable Object v0, @Nullable Object... rest) {
204     checkArgument(
205         rest.length % 2 == 0,
206         "There must be an equal number of key/value pairs "
207             + "(i.e., the number of key/value parameters (%s) must be even).",
208         rest.length + 2);
209 
210     Map<@Nullable Object, @Nullable Object> expectedMap = Maps.newLinkedHashMap();
211     expectedMap.put(k0, v0);
212     Multiset<@Nullable Object> keys = LinkedHashMultiset.create();
213     keys.add(k0);
214     for (int i = 0; i < rest.length; i += 2) {
215       Object key = rest[i];
216       expectedMap.put(key, rest[i + 1]);
217       keys.add(key);
218     }
219     checkArgument(
220         keys.size() == expectedMap.size(),
221         "Duplicate keys (%s) cannot be passed to %s().",
222         keys,
223         functionName);
224     return expectedMap;
225   }
226 
227   /** Fails if the map does not contain exactly the given set of entries in the given map. */
228   @CanIgnoreReturnValue
containsExactlyEntriesIn(Map<?, ?> expectedMap)229   public final Ordered containsExactlyEntriesIn(Map<?, ?> expectedMap) {
230     if (expectedMap.isEmpty()) {
231       if (checkNotNull(actual).isEmpty()) {
232         return IN_ORDER;
233       } else {
234         isEmpty(); // fails
235         return ALREADY_FAILED;
236       }
237     }
238     boolean containsAnyOrder = containsEntriesInAnyOrder(expectedMap, /* allowUnexpected= */ false);
239     if (containsAnyOrder) {
240       return new MapInOrder(expectedMap, /* allowUnexpected= */ false, /* correspondence= */ null);
241     } else {
242       return ALREADY_FAILED;
243     }
244   }
245 
246   /** Fails if the map does not contain at least the given set of entries in the given map. */
247   @CanIgnoreReturnValue
containsAtLeastEntriesIn(Map<?, ?> expectedMap)248   public final Ordered containsAtLeastEntriesIn(Map<?, ?> expectedMap) {
249     if (expectedMap.isEmpty()) {
250       return IN_ORDER;
251     }
252     boolean containsAnyOrder = containsEntriesInAnyOrder(expectedMap, /* allowUnexpected= */ true);
253     if (containsAnyOrder) {
254       return new MapInOrder(expectedMap, /* allowUnexpected= */ true, /* correspondence= */ null);
255     } else {
256       return ALREADY_FAILED;
257     }
258   }
259 
260   @CanIgnoreReturnValue
containsEntriesInAnyOrder(Map<?, ?> expectedMap, boolean allowUnexpected)261   private boolean containsEntriesInAnyOrder(Map<?, ?> expectedMap, boolean allowUnexpected) {
262     MapDifference<@Nullable Object, @Nullable Object, @Nullable Object> diff =
263         MapDifference.create(checkNotNull(actual), expectedMap, allowUnexpected, Objects::equal);
264     if (diff.isEmpty()) {
265       return true;
266     }
267     // TODO(cpovirk): Consider adding a special-case where the diff contains exactly one key which
268     // is present with the wrong value, doing an isEqualTo assertion on the values. Pro: This gives
269     // us all the extra power of isEqualTo, including maybe throwing a ComparisonFailure. Con: It
270     // might be misleading to report a single mismatched value when the assertion was on the whole
271     // map - this could be mitigated by adding extra info explaining that. (Would need to ensure
272     // that it still fails in cases where e.g. the value is 1 and it should be 1L, where isEqualTo
273     // succeeds: perhaps failEqualityCheckForEqualsWithoutDescription will do the right thing.)
274     // First, we need to decide whether this kind of cleverness is a line we want to cross.
275     // (See also containsEntry, which does do an isEqualTo-like assertion when the expected key is
276     // present with the wrong value, which may be the closest we currently get to this.)
277     failWithoutActual(
278         ImmutableList.<Fact>builder()
279             .addAll(diff.describe(/* differ= */ null))
280             .add(simpleFact("---"))
281             .add(fact(allowUnexpected ? "expected to contain at least" : "expected", expectedMap))
282             .add(butWas())
283             .build());
284     return false;
285   }
286 
287   private interface ValueTester<A extends @Nullable Object, E extends @Nullable Object> {
test(A actualValue, E expectedValue)288     boolean test(A actualValue, E expectedValue);
289   }
290 
291   private interface Differ<A extends @Nullable Object, E extends @Nullable Object> {
diff(A actual, E expected)292     @Nullable String diff(A actual, E expected);
293   }
294 
295   // This is mostly like the MapDifference code in com.google.common.collect, generalized to remove
296   // the requirement that the values of the two maps are of the same type and are compared with a
297   // symmetric Equivalence.
298   private static class MapDifference<
299       K extends @Nullable Object, A extends @Nullable Object, E extends @Nullable Object> {
300     private final Map<K, E> missing;
301     private final Map<K, A> unexpected;
302     private final Map<K, ValueDifference<A, E>> wrongValues;
303     private final Set<K> allKeys;
304 
305     static <K extends @Nullable Object, A extends @Nullable Object, E extends @Nullable Object>
create( Map<? extends K, ? extends A> actual, Map<? extends K, ? extends E> expected, boolean allowUnexpected, ValueTester<? super A, ? super E> valueTester)306         MapDifference<K, A, E> create(
307             Map<? extends K, ? extends A> actual,
308             Map<? extends K, ? extends E> expected,
309             boolean allowUnexpected,
310             ValueTester<? super A, ? super E> valueTester) {
311       Map<K, A> unexpected = new LinkedHashMap<>(actual);
312       Map<K, E> missing = new LinkedHashMap<>();
313       Map<K, ValueDifference<A, E>> wrongValues = new LinkedHashMap<>();
314       for (Map.Entry<? extends K, ? extends E> expectedEntry : expected.entrySet()) {
315         K expectedKey = expectedEntry.getKey();
316         E expectedValue = expectedEntry.getValue();
317         if (actual.containsKey(expectedKey)) {
318           @SuppressWarnings("UnnecessaryCast") // needed by nullness checker
319           A actualValue = (A) unexpected.remove(expectedKey);
320           if (!valueTester.test(actualValue, expectedValue)) {
321             wrongValues.put(expectedKey, new ValueDifference<>(actualValue, expectedValue));
322           }
323         } else {
324           missing.put(expectedKey, expectedValue);
325         }
326       }
327       if (allowUnexpected) {
328         unexpected.clear();
329       }
330       return new MapDifference<>(
331           missing, unexpected, wrongValues, Sets.union(actual.keySet(), expected.keySet()));
332     }
333 
MapDifference( Map<K, E> missing, Map<K, A> unexpected, Map<K, ValueDifference<A, E>> wrongValues, Set<K> allKeys)334     private MapDifference(
335         Map<K, E> missing,
336         Map<K, A> unexpected,
337         Map<K, ValueDifference<A, E>> wrongValues,
338         Set<K> allKeys) {
339       this.missing = missing;
340       this.unexpected = unexpected;
341       this.wrongValues = wrongValues;
342       this.allKeys = allKeys;
343     }
344 
isEmpty()345     boolean isEmpty() {
346       return missing.isEmpty() && unexpected.isEmpty() && wrongValues.isEmpty();
347     }
348 
describe(@ullable Differ<? super A, ? super E> differ)349     ImmutableList<Fact> describe(@Nullable Differ<? super A, ? super E> differ) {
350       boolean includeKeyTypes = includeKeyTypes();
351       ImmutableList.Builder<Fact> facts = ImmutableList.builder();
352       if (!wrongValues.isEmpty()) {
353         facts.add(simpleFact("keys with wrong values"));
354       }
355       for (Map.Entry<K, ValueDifference<A, E>> entry : wrongValues.entrySet()) {
356         facts.add(fact("for key", maybeAddType(entry.getKey(), includeKeyTypes)));
357         facts.addAll(entry.getValue().describe(differ));
358       }
359       if (!missing.isEmpty()) {
360         facts.add(simpleFact("missing keys"));
361       }
362       for (Map.Entry<K, E> entry : missing.entrySet()) {
363         facts.add(fact("for key", maybeAddType(entry.getKey(), includeKeyTypes)));
364         facts.add(fact("expected value", entry.getValue()));
365       }
366       if (!unexpected.isEmpty()) {
367         facts.add(simpleFact("unexpected keys"));
368       }
369       for (Map.Entry<K, A> entry : unexpected.entrySet()) {
370         facts.add(fact("for key", maybeAddType(entry.getKey(), includeKeyTypes)));
371         facts.add(fact("unexpected value", entry.getValue()));
372       }
373       return facts.build();
374     }
375 
includeKeyTypes()376     private boolean includeKeyTypes() {
377       // We will annotate all the keys in the diff with their types if any of the keys involved have
378       // the same toString() without being equal.
379       Set<K> keys = Sets.newHashSet();
380       keys.addAll(missing.keySet());
381       keys.addAll(unexpected.keySet());
382       keys.addAll(wrongValues.keySet());
383       return hasMatchingToStringPair(keys, allKeys);
384     }
385   }
386 
387   private static class ValueDifference<A extends @Nullable Object, E extends @Nullable Object> {
388     private final A actual;
389     private final E expected;
390 
ValueDifference(A actual, E expected)391     ValueDifference(A actual, E expected) {
392       this.actual = actual;
393       this.expected = expected;
394     }
395 
describe(@ullable Differ<? super A, ? super E> differ)396     ImmutableList<Fact> describe(@Nullable Differ<? super A, ? super E> differ) {
397       boolean includeTypes =
398           differ == null && String.valueOf(actual).equals(String.valueOf(expected));
399       ImmutableList.Builder<Fact> facts =
400           ImmutableList.<Fact>builder()
401               .add(fact("expected value", maybeAddType(expected, includeTypes)))
402               .add(fact("but got value", maybeAddType(actual, includeTypes)));
403 
404       if (differ != null) {
405         String diffString = differ.diff(actual, expected);
406         if (diffString != null) {
407           facts.add(fact("diff", diffString));
408         }
409       }
410       return facts.build();
411     }
412   }
413 
maybeAddType(@ullable Object object, boolean includeTypes)414   private static String maybeAddType(@Nullable Object object, boolean includeTypes) {
415     return includeTypes
416         ? lenientFormat("%s (%s)", object, objectToTypeName(object))
417         : String.valueOf(object);
418   }
419 
420   private class MapInOrder implements Ordered {
421 
422     private final Map<?, ?> expectedMap;
423     private final boolean allowUnexpected;
424     private final @Nullable Correspondence<?, ?> correspondence;
425 
MapInOrder( Map<?, ?> expectedMap, boolean allowUnexpected, @Nullable Correspondence<?, ?> correspondence)426     MapInOrder(
427         Map<?, ?> expectedMap,
428         boolean allowUnexpected,
429         @Nullable Correspondence<?, ?> correspondence) {
430       this.expectedMap = expectedMap;
431       this.allowUnexpected = allowUnexpected;
432       this.correspondence = correspondence;
433     }
434 
435     /**
436      * Checks whether the common elements between actual and expected are in the same order.
437      *
438      * <p>This doesn't check whether the keys have the same values or whether all the required keys
439      * are actually present. That was supposed to be done before the "in order" part.
440      */
441     @Override
inOrder()442     public void inOrder() {
443       // We're using the fact that Sets.intersection keeps the order of the first set.
444       checkNotNull(actual);
445       List<?> expectedKeyOrder =
446           Lists.newArrayList(Sets.intersection(expectedMap.keySet(), actual.keySet()));
447       List<?> actualKeyOrder =
448           Lists.newArrayList(Sets.intersection(actual.keySet(), expectedMap.keySet()));
449       if (!actualKeyOrder.equals(expectedKeyOrder)) {
450         ImmutableList.Builder<Fact> facts =
451             ImmutableList.<Fact>builder()
452                 .add(
453                     simpleFact(
454                         allowUnexpected
455                             ? "required entries were all found, but order was wrong"
456                             : "entries match, but order was wrong"))
457                 .add(
458                     fact(
459                         allowUnexpected ? "expected to contain at least" : "expected",
460                         expectedMap));
461         if (correspondence != null) {
462           facts.addAll(correspondence.describeForMapValues());
463         }
464         failWithActual(facts.build());
465       }
466     }
467   }
468 
469   /** Ordered implementation that does nothing because it's already known to be true. */
470   private static final Ordered IN_ORDER = () -> {};
471 
472   /** Ordered implementation that does nothing because an earlier check already caused a failure. */
473   private static final Ordered ALREADY_FAILED = () -> {};
474 
475   /**
476    * Starts a method chain for a check in which the actual values (i.e. the values of the {@link
477    * Map} under test) are compared to expected values using the given {@link Correspondence}. The
478    * actual values must be of type {@code A}, the expected values must be of type {@code E}. The
479    * check is actually executed by continuing the method chain. For example:
480    *
481    * <pre>{@code
482    * assertThat(actualMap)
483    *   .comparingValuesUsing(correspondence)
484    *   .containsEntry(expectedKey, expectedValue);
485    * }</pre>
486    *
487    * where {@code actualMap} is a {@code Map<?, A>} (or, more generally, a {@code Map<?, ? extends
488    * A>}), {@code correspondence} is a {@code Correspondence<A, E>}, and {@code expectedValue} is an
489    * {@code E}.
490    *
491    * <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
492    *
493    * <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
494    * encounter an actual value that is not of type {@code A} or an expected value that is not of
495    * type {@code E}.
496    */
497   public final <A extends @Nullable Object, E extends @Nullable Object>
comparingValuesUsing( Correspondence<? super A, ? super E> correspondence)498       UsingCorrespondence<A, E> comparingValuesUsing(
499           Correspondence<? super A, ? super E> correspondence) {
500     return new UsingCorrespondence<>(correspondence);
501   }
502 
503   /**
504    * Starts a method chain for a check in which failure messages may use the given {@link
505    * DiffFormatter} to describe the difference between an actual value (i.e. a value in the {@link
506    * Map} under test) and the value it is expected to be equal to, but isn't. The actual and
507    * expected values must be of type {@code V}. The check is actually executed by continuing the
508    * method chain. For example:
509    *
510    * <pre>{@code
511    * assertThat(actualMap)
512    *   .formattingDiffsUsing(FooTestHelper::formatDiff)
513    *   .containsExactly(key1, foo1, key2, foo2, key3, foo3);
514    * }</pre>
515    *
516    * where {@code actualMap} is a {@code Map<?, Foo>} (or, more generally, a {@code Map<?, ? extends
517    * Foo>}), {@code FooTestHelper.formatDiff} is a static method taking two {@code Foo} arguments
518    * and returning a {@link String}, and {@code foo1}, {@code foo2}, and {@code foo3} are {@code
519    * Foo} instances.
520    *
521    * <p>Unlike when using {@link #comparingValuesUsing}, the values are still compared using object
522    * equality, so this method does not affect whether a test passes or fails.
523    *
524    * <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
525    * encounter a value that is not of type {@code V}.
526    *
527    * @since 1.1
528    */
formattingDiffsUsing( DiffFormatter<? super V, ? super V> formatter)529   public final <V> UsingCorrespondence<V, V> formattingDiffsUsing(
530       DiffFormatter<? super V, ? super V> formatter) {
531     return comparingValuesUsing(Correspondence.<V>equality().formattingDiffsUsing(formatter));
532   }
533 
534   /**
535    * A partially specified check in which the actual values (i.e. the values of the {@link Map}
536    * under test) are compared to expected values using a {@link Correspondence}. The expected values
537    * are of type {@code E}. Call methods on this object to actually execute the check.
538    *
539    * <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
540    */
541   public final class UsingCorrespondence<A extends @Nullable Object, E extends @Nullable Object> {
542 
543     private final Correspondence<? super A, ? super E> correspondence;
544 
UsingCorrespondence(Correspondence<? super A, ? super E> correspondence)545     private UsingCorrespondence(Correspondence<? super A, ? super E> correspondence) {
546       this.correspondence = checkNotNull(correspondence);
547     }
548 
549     /**
550      * Fails if the map does not contain an entry with the given key and a value that corresponds to
551      * the given value.
552      */
553     @SuppressWarnings("UnnecessaryCast") // needed by nullness checker
containsEntry(@ullable Object expectedKey, E expectedValue)554     public void containsEntry(@Nullable Object expectedKey, E expectedValue) {
555       if (checkNotNull(actual).containsKey(expectedKey)) {
556         // Found matching key.
557         A actualValue = getCastSubject().get(expectedKey);
558         Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
559         if (correspondence.safeCompare((A) actualValue, expectedValue, exceptions)) {
560           // The expected key had the expected value. There's no need to check exceptions here,
561           // because if Correspondence.compare() threw then safeCompare() would return false.
562           return;
563         }
564         // Found matching key with non-matching value.
565         String diff = correspondence.safeFormatDiff((A) actualValue, expectedValue, exceptions);
566         if (diff != null) {
567           failWithoutActual(
568               ImmutableList.<Fact>builder()
569                   .add(fact("for key", expectedKey))
570                   .add(fact("expected value", expectedValue))
571                   .addAll(correspondence.describeForMapValues())
572                   .add(fact("but got value", actualValue))
573                   .add(fact("diff", diff))
574                   .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
575                   .addAll(exceptions.describeAsAdditionalInfo())
576                   .build());
577         } else {
578           failWithoutActual(
579               ImmutableList.<Fact>builder()
580                   .add(fact("for key", expectedKey))
581                   .add(fact("expected value", expectedValue))
582                   .addAll(correspondence.describeForMapValues())
583                   .add(fact("but got value", actualValue))
584                   .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
585                   .addAll(exceptions.describeAsAdditionalInfo())
586                   .build());
587         }
588       } else {
589         // Did not find matching key. Look for the matching value with a different key.
590         Set<@Nullable Object> keys = new LinkedHashSet<>();
591         Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
592         for (Map.Entry<?, A> actualEntry : getCastSubject().entrySet()) {
593           if (correspondence.safeCompare(actualEntry.getValue(), expectedValue, exceptions)) {
594             keys.add(actualEntry.getKey());
595           }
596         }
597         if (!keys.isEmpty()) {
598           // Found matching values with non-matching keys.
599           failWithoutActual(
600               ImmutableList.<Fact>builder()
601                   .add(fact("for key", expectedKey))
602                   .add(fact("expected value", expectedValue))
603                   .addAll(correspondence.describeForMapValues())
604                   .add(simpleFact("but was missing"))
605                   .add(fact("other keys with matching values", keys))
606                   .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
607                   .addAll(exceptions.describeAsAdditionalInfo())
608                   .build());
609         } else {
610           // Did not find matching key or value.
611           failWithoutActual(
612               ImmutableList.<Fact>builder()
613                   .add(fact("for key", expectedKey))
614                   .add(fact("expected value", expectedValue))
615                   .addAll(correspondence.describeForMapValues())
616                   .add(simpleFact("but was missing"))
617                   .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
618                   .addAll(exceptions.describeAsAdditionalInfo())
619                   .build());
620         }
621       }
622     }
623 
624     /**
625      * Fails if the map contains an entry with the given key and a value that corresponds to the
626      * given value.
627      */
628     @SuppressWarnings("UnnecessaryCast") // needed by nullness checker
doesNotContainEntry(@ullable Object excludedKey, E excludedValue)629     public void doesNotContainEntry(@Nullable Object excludedKey, E excludedValue) {
630       if (checkNotNull(actual).containsKey(excludedKey)) {
631         // Found matching key. Fail if the value matches, too.
632         A actualValue = getCastSubject().get(excludedKey);
633         Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
634         if (correspondence.safeCompare((A) actualValue, excludedValue, exceptions)) {
635           // The matching key had a matching value. There's no need to check exceptions here,
636           // because if Correspondence.compare() threw then safeCompare() would return false.
637           failWithoutActual(
638               ImmutableList.<Fact>builder()
639                   .add(fact("expected not to contain", immutableEntry(excludedKey, excludedValue)))
640                   .addAll(correspondence.describeForMapValues())
641                   .add(fact("but contained", immutableEntry(excludedKey, actualValue)))
642                   .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
643                   .addAll(exceptions.describeAsAdditionalInfo())
644                   .build());
645         }
646         // The value didn't match, but we still need to fail if we hit an exception along the way.
647         if (exceptions.hasCompareException()) {
648           failWithoutActual(
649               ImmutableList.<Fact>builder()
650                   .addAll(exceptions.describeAsMainCause())
651                   .add(fact("expected not to contain", immutableEntry(excludedKey, excludedValue)))
652                   .addAll(correspondence.describeForMapValues())
653                   .add(simpleFact("found no match (but failing because of exception)"))
654                   .add(fact("full map", actualCustomStringRepresentationForPackageMembersToCall()))
655                   .build());
656         }
657       }
658     }
659 
660     /**
661      * Fails if the map does not contain exactly the given set of keys mapping to values that
662      * correspond to the given values.
663      *
664      * <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
665      * if any other type is encountered.
666      *
667      * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
668      * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
669      */
670     // TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
671     // For bonus points, checking that the even-numbered values are of type E would be sweet.
672     @CanIgnoreReturnValue
containsExactly(@ullable Object k0, @Nullable E v0, @Nullable Object... rest)673     public Ordered containsExactly(@Nullable Object k0, @Nullable E v0, @Nullable Object... rest) {
674       @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
675       Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsExactly", k0, v0, rest);
676       return containsExactlyEntriesIn(expectedMap);
677     }
678 
679     /**
680      * Fails if the map does not contain at least the given set of keys mapping to values that
681      * correspond to the given values.
682      *
683      * <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
684      * if any other type is encountered.
685      *
686      * <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
687      * key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
688      */
689     // TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
690     // For bonus points, checking that the even-numbered values are of type E would be sweet.
691     @CanIgnoreReturnValue
containsAtLeast(@ullable Object k0, @Nullable E v0, @Nullable Object... rest)692     public Ordered containsAtLeast(@Nullable Object k0, @Nullable E v0, @Nullable Object... rest) {
693       @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
694       Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsAtLeast", k0, v0, rest);
695       return containsAtLeastEntriesIn(expectedMap);
696     }
697 
698     /**
699      * Fails if the map does not contain exactly the keys in the given map, mapping to values that
700      * correspond to the values of the given map.
701      */
702     @CanIgnoreReturnValue
containsExactlyEntriesIn(Map<?, ? extends E> expectedMap)703     public Ordered containsExactlyEntriesIn(Map<?, ? extends E> expectedMap) {
704       if (expectedMap.isEmpty()) {
705         if (checkNotNull(actual).isEmpty()) {
706           return IN_ORDER;
707         } else {
708           isEmpty(); // fails
709           return ALREADY_FAILED;
710         }
711       }
712       return internalContainsEntriesIn(expectedMap, /* allowUnexpected= */ false);
713     }
714 
715     /**
716      * Fails if the map does not contain at least the keys in the given map, mapping to values that
717      * correspond to the values of the given map.
718      */
719     @CanIgnoreReturnValue
containsAtLeastEntriesIn(Map<?, ? extends E> expectedMap)720     public Ordered containsAtLeastEntriesIn(Map<?, ? extends E> expectedMap) {
721       if (expectedMap.isEmpty()) {
722         return IN_ORDER;
723       }
724       return internalContainsEntriesIn(expectedMap, /* allowUnexpected= */ true);
725     }
726 
internalContainsEntriesIn( Map<K, V> expectedMap, boolean allowUnexpected)727     private <K extends @Nullable Object, V extends E> Ordered internalContainsEntriesIn(
728         Map<K, V> expectedMap, boolean allowUnexpected) {
729       Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
730       MapDifference<@Nullable Object, A, V> diff =
731           MapDifference.create(
732               getCastSubject(),
733               expectedMap,
734               allowUnexpected,
735               new ValueTester<A, E>() {
736                 @Override
737                 public boolean test(A actualValue, E expectedValue) {
738                   return correspondence.safeCompare(actualValue, expectedValue, exceptions);
739                 }
740               });
741       if (diff.isEmpty()) {
742         // The maps correspond exactly. There's no need to check exceptions here, because if
743         // Correspondence.compare() threw then safeCompare() would return false and the diff would
744         // record that we had the wrong value for that key.
745         return new MapInOrder(expectedMap, allowUnexpected, correspondence);
746       }
747       failWithoutActual(
748           ImmutableList.<Fact>builder()
749               .addAll(diff.describe(differ(exceptions)))
750               .add(simpleFact("---"))
751               .add(fact(allowUnexpected ? "expected to contain at least" : "expected", expectedMap))
752               .addAll(correspondence.describeForMapValues())
753               .add(butWas())
754               .addAll(exceptions.describeAsAdditionalInfo())
755               .build());
756       return ALREADY_FAILED;
757     }
758 
differ(Correspondence.ExceptionStore exceptions)759     private <V extends E> Differ<A, V> differ(Correspondence.ExceptionStore exceptions) {
760       return (actual, expected) -> correspondence.safeFormatDiff(actual, expected, exceptions);
761     }
762 
763     @SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
getCastSubject()764     private Map<?, A> getCastSubject() {
765       return (Map<?, A>) checkNotNull(actual);
766     }
767   }
768 }
769