blob: 11d880ef9e83ed2acf6419cad28e7c5cad23aef1 [file] [log] [blame]
/*
* Copyright (c) 2011 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.common.truth;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.lenientFormat;
import static com.google.common.collect.Maps.immutableEntry;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.simpleFact;
import static com.google.common.truth.Facts.facts;
import static com.google.common.truth.SubjectUtils.countDuplicatesAndAddTypeInfo;
import static com.google.common.truth.SubjectUtils.hasMatchingToStringPair;
import static com.google.common.truth.SubjectUtils.objectToTypeName;
import static com.google.common.truth.SubjectUtils.retainMatchingToString;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.LinkedHashMultiset;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
/**
* Propositions for {@link Map} subjects.
*
* @author Christian Gruber
* @author Kurt Alfred Kluever
*/
// Can't be final since SortedMapSubject extends it
public class MapSubject extends Subject<MapSubject, Map<?, ?>> {
MapSubject(FailureMetadata metadata, @NullableDecl Map<?, ?> map) {
super(metadata, map);
}
/** Fails if the subject is not equal to the given object. */
@Override
public void isEqualTo(@NullableDecl Object other) {
if (Objects.equal(actual(), other)) {
return;
}
// Fail but with a more descriptive message:
if (!(other instanceof Map)) {
super.isEqualTo(other);
return;
}
boolean mapEquals = containsEntriesInAnyOrder((Map<?, ?>) other, "is equal to", false);
if (mapEquals) {
failWithoutActual(
simpleFact(
lenientFormat(
"Not true that %s is equal to <%s>. It is equal according to the contract of "
+ "Map.equals(Object), but this implementation returned false",
actualAsString(), other)));
}
}
/** Fails if the map is not empty. */
public void isEmpty() {
if (!actual().isEmpty()) {
failWithActual(simpleFact("expected to be empty"));
}
}
/** Fails if the map is empty. */
public void isNotEmpty() {
if (actual().isEmpty()) {
failWithoutActual(simpleFact("expected not to be empty"));
}
}
/** Fails if the map does not have the given size. */
public void hasSize(int expectedSize) {
checkArgument(expectedSize >= 0, "expectedSize (%s) must be >= 0", expectedSize);
check("size()").that(actual().size()).isEqualTo(expectedSize);
}
/** Fails if the map does not contain the given key. */
public void containsKey(@NullableDecl Object key) {
check("keySet()").that(actual().keySet()).contains(key);
}
/** Fails if the map contains the given key. */
public void doesNotContainKey(@NullableDecl Object key) {
check("keySet()").that(actual().keySet()).doesNotContain(key);
}
/** Fails if the map does not contain the given entry. */
public void containsEntry(@NullableDecl Object key, @NullableDecl Object value) {
Entry<Object, Object> entry = Maps.immutableEntry(key, value);
if (!actual().entrySet().contains(entry)) {
List<Object> keyList = Lists.newArrayList(key);
List<Object> valueList = Lists.newArrayList(value);
if (hasMatchingToStringPair(actual().keySet(), keyList)) {
failWithoutActual(
simpleFact(
lenientFormat(
"Not true that %s contains entry <%s (%s)>. However, it does contain keys "
+ "<%s>.",
actualAsString(),
entry,
objectToTypeName(entry),
countDuplicatesAndAddTypeInfo(
retainMatchingToString(actual().keySet(), keyList /* itemsToCheck */)))));
} else if (hasMatchingToStringPair(actual().values(), valueList)) {
failWithoutActual(
simpleFact(
lenientFormat(
"Not true that %s contains entry <%s (%s)>. However, it does contain values "
+ "<%s>.",
actualAsString(),
entry,
objectToTypeName(entry),
countDuplicatesAndAddTypeInfo(
retainMatchingToString(actual().values(), valueList /* itemsToCheck */)))));
} else if (actual().containsKey(key)) {
Object actualValue = actual().get(key);
/*
* In the case of a null expected or actual value, clarify that the key *is* present and
* *is* expected to be present. That is, get() isn't returning null to indicate that the key
* is missing, and the user isn't making an assertion that the key is missing.
*/
StandardSubjectBuilder check = check("get(%s)", key);
if (value == null || actualValue == null) {
check = check.withMessage("key is present but with a different value");
}
// See the comment on IterableSubject's use of failEqualityCheckForEqualsWithoutDescription.
check.that(actualValue).failEqualityCheckForEqualsWithoutDescription(value);
} else if (actual().containsValue(value)) {
Set<Object> keys = new LinkedHashSet<>();
for (Entry<?, ?> actualEntry : actual().entrySet()) {
if (Objects.equal(actualEntry.getValue(), value)) {
keys.add(actualEntry.getKey());
}
}
failWithoutActual(
simpleFact(
lenientFormat(
"Not true that %s contains entry <%s>. "
+ "However, the following keys are mapped to <%s>: %s",
actualAsString(), entry, value, keys)));
} else {
fail("contains entry", entry);
}
}
}
/** Fails if the map contains the given entry. */
public void doesNotContainEntry(@NullableDecl Object key, @NullableDecl Object value) {
checkNoNeedToDisplayBothValues("entrySet()")
.that(actual().entrySet())
.doesNotContain(immutableEntry(key, value));
}
/** Fails if the map is not empty. */
@CanIgnoreReturnValue
public Ordered containsExactly() {
return containsExactlyEntriesIn(ImmutableMap.of());
}
/**
* Fails if the map does not contain exactly the given set of key/value pairs.
*
* <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
* key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
*
* <p>The arguments must not contain duplicate keys.
*/
@CanIgnoreReturnValue
public Ordered containsExactly(@NullableDecl Object k0, @NullableDecl Object v0, Object... rest) {
return containsExactlyEntriesIn(accumulateMap("containsExactly", k0, v0, rest));
}
@CanIgnoreReturnValue
public Ordered containsAtLeast(@NullableDecl Object k0, @NullableDecl Object v0, Object... rest) {
return containsAtLeastEntriesIn(accumulateMap("containsAtLeast", k0, v0, rest));
}
private static Map<Object, Object> accumulateMap(
String functionName, @NullableDecl Object k0, @NullableDecl Object v0, Object... rest) {
checkArgument(
rest.length % 2 == 0,
"There must be an equal number of key/value pairs "
+ "(i.e., the number of key/value parameters (%s) must be even).",
rest.length + 2);
Map<Object, Object> expectedMap = Maps.newLinkedHashMap();
expectedMap.put(k0, v0);
Multiset<Object> keys = LinkedHashMultiset.create();
keys.add(k0);
for (int i = 0; i < rest.length; i += 2) {
Object key = rest[i];
expectedMap.put(key, rest[i + 1]);
keys.add(key);
}
checkArgument(
keys.size() == expectedMap.size(),
"Duplicate keys (%s) cannot be passed to %s().",
keys,
functionName);
return expectedMap;
}
/** Fails if the map does not contain exactly the given set of entries in the given map. */
@CanIgnoreReturnValue
public Ordered containsExactlyEntriesIn(Map<?, ?> expectedMap) {
if (expectedMap.isEmpty()) {
if (actual().isEmpty()) {
return IN_ORDER;
} else {
isEmpty(); // fails
return ALREADY_FAILED;
}
}
boolean containsAnyOrder =
containsEntriesInAnyOrder(expectedMap, "contains exactly", /* allowUnexpected= */ false);
if (containsAnyOrder) {
return new MapInOrder(expectedMap, "contains exactly these entries in order");
} else {
return ALREADY_FAILED;
}
}
/** Fails if the map does not contain at least the given set of entries in the given map. */
@CanIgnoreReturnValue
public Ordered containsAtLeastEntriesIn(Map<?, ?> expectedMap) {
if (expectedMap.isEmpty()) {
return IN_ORDER;
}
boolean containsAnyOrder =
containsEntriesInAnyOrder(expectedMap, "contains at least", /* allowUnexpected= */ true);
if (containsAnyOrder) {
return new MapInOrder(expectedMap, "contains at least these entries in order");
} else {
return ALREADY_FAILED;
}
}
@CanIgnoreReturnValue
private boolean containsEntriesInAnyOrder(
Map<?, ?> expectedMap, String failVerb, boolean allowUnexpected) {
MapDifference<Object, Object, Object> diff =
MapDifference.create(actual(), expectedMap, allowUnexpected, EQUALITY);
if (diff.isEmpty()) {
return true;
}
failWithoutActual(
simpleFact(
lenientFormat(
"Not true that %s %s <%s>. It %s",
actualAsString(), failVerb, expectedMap, diff.describe(VALUE_DIFFERENCE_FORMAT))));
return false;
}
private interface ValueTester<A, E> {
boolean test(@NullableDecl A actualValue, @NullableDecl E expectedValue);
}
private static final ValueTester<Object, Object> EQUALITY =
new ValueTester<Object, Object>() {
@Override
public boolean test(@NullableDecl Object actualValue, @NullableDecl Object expectedValue) {
return Objects.equal(actualValue, expectedValue);
}
};
// This is mostly like the MapDifference code in com.google.common.collect, generalized to remove
// the requirement that the values of the two maps are of the same type and are compared with a
// symmetric Equivalence.
private static class MapDifference<K, A, E> {
private final Map<K, E> missing;
private final Map<K, A> unexpected;
private final Map<K, ValueDifference<A, E>> wrongValues;
private final Set<K> allKeys;
static <K, A, E> MapDifference<K, A, E> create(
Map<? extends K, ? extends A> actual,
Map<? extends K, ? extends E> expected,
boolean allowUnexpected,
ValueTester<? super A, ? super E> valueTester) {
Map<K, A> unexpected = new LinkedHashMap<>(actual);
Map<K, E> missing = new LinkedHashMap<>();
Map<K, ValueDifference<A, E>> wrongValues = new LinkedHashMap<>();
for (Entry<? extends K, ? extends E> expectedEntry : expected.entrySet()) {
K expectedKey = expectedEntry.getKey();
E expectedValue = expectedEntry.getValue();
if (actual.containsKey(expectedKey)) {
A actualValue = unexpected.remove(expectedKey);
if (!valueTester.test(actualValue, expectedValue)) {
wrongValues.put(expectedKey, new ValueDifference<>(actualValue, expectedValue));
}
} else {
missing.put(expectedKey, expectedValue);
}
}
if (allowUnexpected) {
unexpected.clear();
}
return new MapDifference<>(
missing, unexpected, wrongValues, Sets.union(actual.keySet(), expected.keySet()));
}
private MapDifference(
Map<K, E> missing,
Map<K, A> unexpected,
Map<K, ValueDifference<A, E>> wrongValues,
Set<K> allKeys) {
this.missing = missing;
this.unexpected = unexpected;
this.wrongValues = wrongValues;
this.allKeys = allKeys;
}
boolean isEmpty() {
return missing.isEmpty() && unexpected.isEmpty() && wrongValues.isEmpty();
}
String describe(Function<ValueDifference<A, E>, String> valueDiffFormat) {
boolean includeKeyTypes = includeKeyTypes();
StringBuilder description = new StringBuilder();
if (!missing.isEmpty()) {
description
.append("is missing keys for the following entries: ")
.append(includeKeyTypes ? addKeyTypes(missing) : missing);
}
if (!unexpected.isEmpty()) {
if (description.length() > 0) {
description.append(" and ");
}
description
.append("has the following entries with unexpected keys: ")
.append(includeKeyTypes ? addKeyTypes(unexpected) : unexpected);
}
if (!wrongValues.isEmpty()) {
if (description.length() > 0) {
description.append(" and ");
}
Map<K, String> wrongValuesFormatted = Maps.transformValues(wrongValues, valueDiffFormat);
description
.append("has the following entries with matching keys but different values: ")
.append(includeKeyTypes ? addKeyTypes(wrongValuesFormatted) : wrongValuesFormatted);
}
return description.toString();
}
private boolean includeKeyTypes() {
// We will annotate all the keys in the diff with their types if any of the keys involved have
// the same toString() without being equal.
Set<K> keys = Sets.newHashSet();
keys.addAll(missing.keySet());
keys.addAll(unexpected.keySet());
keys.addAll(wrongValues.keySet());
return hasMatchingToStringPair(keys, allKeys);
}
}
private static class ValueDifference<A, E> {
private final A actual;
private final E expected;
ValueDifference(@NullableDecl A actual, @NullableDecl E expected) {
this.actual = actual;
this.expected = expected;
}
}
/** A formatting function for value differences when compared for equality. */
private static final Function<ValueDifference<Object, Object>, String> VALUE_DIFFERENCE_FORMAT =
new Function<ValueDifference<Object, Object>, String>() {
@Override
public String apply(ValueDifference<Object, Object> values) {
boolean includeTypes =
String.valueOf(values.actual).equals(String.valueOf(values.expected));
return lenientFormat(
"(expected %s but got %s)",
includeTypes ? new TypedToStringWrapper(values.expected) : values.expected,
includeTypes ? new TypedToStringWrapper(values.actual) : values.actual);
}
};
private static final Map<Object, Object> addKeyTypes(Map<?, ?> in) {
Map<Object, Object> out = Maps.newLinkedHashMap();
for (Map.Entry<?, ?> entry : in.entrySet()) {
out.put(new TypedToStringWrapper(entry.getKey()), entry.getValue());
}
return out;
}
private static class TypedToStringWrapper {
private final Object delegate;
TypedToStringWrapper(Object delegate) {
this.delegate = delegate;
}
@Override
public boolean equals(Object other) {
return Objects.equal(delegate, other);
}
@Override
public int hashCode() {
return Objects.hashCode(delegate);
}
@Override
public String toString() {
return lenientFormat("%s (%s)", delegate, objectToTypeName(delegate));
}
}
private class MapInOrder implements Ordered {
private final Map<?, ?> expectedMap;
private final String failVerb;
MapInOrder(Map<?, ?> expectedMap, String failVerb) {
this.expectedMap = expectedMap;
this.failVerb = failVerb;
}
/**
* Checks whether the common elements between actual and expected are in the same order.
*
* <p>This doesn't check whether the keys have the same values or whether all the required keys
* are actually present. That was supposed to be done before the "in order" part.
*/
@Override
public void inOrder() {
// We're using the fact that Sets.intersection keeps the order of the first set.
List<?> expectedKeyOrder =
Lists.newArrayList(Sets.intersection(expectedMap.keySet(), actual().keySet()));
List<?> actualKeyOrder =
Lists.newArrayList(Sets.intersection(actual().keySet(), expectedMap.keySet()));
if (!actualKeyOrder.equals(expectedKeyOrder)) {
failWithoutActual(
simpleFact(
lenientFormat(
"Not true that %s %s <%s>", actualAsString(), failVerb, expectedMap)));
}
}
}
/** Ordered implementation that does nothing because it's already known to be true. */
private static final Ordered IN_ORDER =
new Ordered() {
@Override
public void inOrder() {}
};
/** Ordered implementation that does nothing because an earlier check already caused a failure. */
private static final Ordered ALREADY_FAILED =
new Ordered() {
@Override
public void inOrder() {}
};
/**
* Starts a method chain for a check in which the actual values (i.e. the values of the {@link
* Map} under test) are compared to expected values using the given {@link Correspondence}. The
* actual values must be of type {@code A}, the expected values must be of type {@code E}. The
* check is actually executed by continuing the method chain. For example:
*
* <pre>{@code
* assertThat(actualMap)
* .comparingValuesUsing(correspondence)
* .containsEntry(expectedKey, expectedValue);
* }</pre>
*
* where {@code actualMap} is a {@code Map<?, A>} (or, more generally, a {@code Map<?, ? extends
* A>}), {@code correspondence} is a {@code Correspondence<A, E>}, and {@code expectedValue} is an
* {@code E}.
*
* <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
*
* <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
* encounter an actual value that is not of type {@code A} or an expected value that is not of
* type {@code E}.
*/
public <A, E> UsingCorrespondence<A, E> comparingValuesUsing(
Correspondence<? super A, ? super E> correspondence) {
return new UsingCorrespondence<>(correspondence);
}
/**
* A partially specified check in which the actual values (i.e. the values of the {@link Map}
* under test) are compared to expected values using a {@link Correspondence}. The expected values
* are of type {@code E}. Call methods on this object to actually execute the check.
*
* <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
*/
public final class UsingCorrespondence<A, E> {
private final Correspondence<? super A, ? super E> correspondence;
private UsingCorrespondence(Correspondence<? super A, ? super E> correspondence) {
this.correspondence = checkNotNull(correspondence);
}
/**
* Fails if the map does not contain an entry with the given key and a value that corresponds to
* the given value.
*/
public void containsEntry(@NullableDecl Object expectedKey, @NullableDecl E expectedValue) {
if (actual().containsKey(expectedKey)) {
// Found matching key.
A actualValue = getCastSubject().get(expectedKey);
Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
if (correspondence.safeCompare(actualValue, expectedValue, exceptions)) {
// The expected key had the expected value. There's no need to check exceptions here,
// because if Correspondence.compare() threw then safeCompare() would return false.
return;
}
// Found matching key with non-matching value.
@NullableDecl
String diff = correspondence.safeFormatDiff(actualValue, expectedValue, exceptions);
if (diff != null) {
failWithoutActual(
facts(
simpleFact(
lenientFormat(
"Not true that %s contains an entry with key <%s> and a value that "
+ "%s <%s>. However, it has a mapping from that key to <%s> "
+ "(diff: %s)",
actualAsString(),
expectedKey,
correspondence,
expectedValue,
actualValue,
diff)))
.and(exceptions.describeAsAdditionalInfo()));
} else {
failWithoutActual(
facts(
simpleFact(
lenientFormat(
"Not true that %s contains an entry with key <%s> and a value that "
+ "%s <%s>. However, it has a mapping from that key to <%s>",
actualAsString(),
expectedKey,
correspondence,
expectedValue,
actualValue)))
.and(exceptions.describeAsAdditionalInfo()));
}
} else {
// Did not find matching key. Look for the matching value with a different key.
Set<Object> keys = new LinkedHashSet<>();
Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
for (Entry<?, A> actualEntry : getCastSubject().entrySet()) {
if (correspondence.safeCompare(actualEntry.getValue(), expectedValue, exceptions)) {
keys.add(actualEntry.getKey());
}
}
if (!keys.isEmpty()) {
// Found matching values with non-matching keys.
failWithoutActual(
facts(
simpleFact(
lenientFormat(
"Not true that %s contains an entry with key <%s> and a value that "
+ "%s <%s>. However, the following keys are mapped to such "
+ "values: <%s>",
actualAsString(), expectedKey, correspondence, expectedValue, keys)))
.and(exceptions.describeAsAdditionalInfo()));
} else {
// Did not find matching key or value.
failWithoutActual(
facts(
simpleFact(
lenientFormat(
"Not true that %s contains an entry with key <%s> and a value that "
+ "%s <%s>",
actualAsString(), expectedKey, correspondence, expectedValue)))
.and(exceptions.describeAsAdditionalInfo()));
}
}
}
/**
* Fails if the map contains an entry with the given key and a value that corresponds to the
* given value.
*/
public void doesNotContainEntry(
@NullableDecl Object excludedKey, @NullableDecl E excludedValue) {
if (actual().containsKey(excludedKey)) {
// Found matching key. Fail if the value matches, too.
A actualValue = getCastSubject().get(excludedKey);
Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
if (correspondence.safeCompare(actualValue, excludedValue, exceptions)) {
// The matching key had a matching value. There's no need to check exceptions here,
// because if Correspondence.compare() threw then safeCompare() would return false.
failWithoutActual(
simpleFact(
lenientFormat(
"Not true that %s does not contain an entry with key <%s> and a value that "
+ "%s <%s>. It maps that key to <%s>",
actualAsString(), excludedKey, correspondence, excludedValue, actualValue)));
}
// The value didn't match, but we still need to fail if we hit an exception along the way.
if (exceptions.hasCompareException()) {
failWithActual(
exceptions
.describeAsMainCause()
.and(
simpleFact(
"comparing contents by testing that no entry had the forbidden key and "
+ "a value that "
+ correspondence
+ " the forbidden value"),
fact("forbidden key", excludedKey),
fact("forbidden value", excludedValue)));
}
}
}
/**
* Fails if the map does not contain exactly the given set of keys mapping to values that
* correspond to the given values.
*
* <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
* if any other type is encountered.
*
* <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
* key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
*/
// TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
// For bonus points, checking that the even-numbered values are of type E would be sweet.
@CanIgnoreReturnValue
public Ordered containsExactly(@NullableDecl Object k0, @NullableDecl E v0, Object... rest) {
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsExactly", k0, v0, rest);
return containsExactlyEntriesIn(expectedMap);
}
/**
* Fails if the map does not contain at least the given set of keys mapping to values that
* correspond to the given values.
*
* <p>The values must all be of type {@code E}, and a {@link ClassCastException} will be thrown
* if any other type is encountered.
*
* <p><b>Warning:</b> the use of varargs means that we cannot guarantee an equal number of
* key/value pairs at compile time. Please make sure you provide varargs in key/value pairs!
*/
// TODO(b/25744307): Can we add an error-prone check that rest.length % 2 == 0?
// For bonus points, checking that the even-numbered values are of type E would be sweet.
@CanIgnoreReturnValue
public Ordered containsAtLeast(@NullableDecl Object k0, @NullableDecl E v0, Object... rest) {
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap("containsAtLeast", k0, v0, rest);
return containsAtLeastEntriesIn(expectedMap);
}
/**
* Fails if the map does not contain exactly the keys in the given map, mapping to values that
* correspond to the values of the given map.
*/
@CanIgnoreReturnValue
public <K, V extends E> Ordered containsExactlyEntriesIn(Map<K, V> expectedMap) {
if (expectedMap.isEmpty()) {
if (actual().isEmpty()) {
return IN_ORDER;
} else {
isEmpty(); // fails
return ALREADY_FAILED;
}
}
return internalContainsEntriesIn("exactly", expectedMap, false);
}
/**
* Fails if the map does not contain at least the keys in the given map, mapping to values that
* correspond to the values of the given map.
*/
@CanIgnoreReturnValue
public <K, V extends E> Ordered containsAtLeastEntriesIn(Map<K, V> expectedMap) {
if (expectedMap.isEmpty()) {
return IN_ORDER;
}
return internalContainsEntriesIn("at least", expectedMap, true);
}
private <K, V extends E> Ordered internalContainsEntriesIn(
String modifier, Map<K, V> expectedMap, boolean allowUnexpected) {
final Correspondence.ExceptionStore exceptions = Correspondence.ExceptionStore.forMapValues();
MapDifference<Object, A, V> diff =
MapDifference.create(
getCastSubject(),
expectedMap,
allowUnexpected,
new ValueTester<A, E>() {
@Override
public boolean test(A actualValue, E expectedValue) {
return correspondence.safeCompare(actualValue, expectedValue, exceptions);
}
});
if (diff.isEmpty()) {
// The maps correspond exactly. There's no need to check exceptions here, because if
// Correspondence.compare() threw then safeCompare() would return false and the diff would
// record that we had the wrong value for that key.
return new MapInOrder(
expectedMap,
lenientFormat(
"contains, in order, %s one entry that has a key that is equal to and a "
+ "value that %s the key and value of each entry of",
modifier, correspondence));
}
failWithoutActual(
facts(
simpleFact(
lenientFormat(
"Not true that %s contains %s one entry that has a key that is "
+ "equal to and a value that %s the key and value of each entry of "
+ "<%s>. It %s",
actualAsString(),
modifier,
correspondence,
expectedMap,
diff.describe(this.<V>valueDiffFormat(exceptions)))))
.and(exceptions.describeAsAdditionalInfo()));
return ALREADY_FAILED;
}
/**
* Returns a formatting function for value differences when compared using the current
* correspondence.
*/
private final <V extends E> Function<ValueDifference<A, V>, String> valueDiffFormat(
final Correspondence.ExceptionStore exceptions) {
return new Function<ValueDifference<A, V>, String>() {
@Override
public String apply(ValueDifference<A, V> values) {
@NullableDecl
String diffString =
correspondence.safeFormatDiff(values.actual, values.expected, exceptions);
if (diffString != null) {
return lenientFormat(
"(expected %s but got %s, diff: %s)", values.expected, values.actual, diffString);
} else {
return lenientFormat("(expected %s but got %s)", values.expected, values.actual);
}
}
};
}
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
private Map<?, A> getCastSubject() {
return (Map<?, A>) actual();
}
}
}