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