/*
* 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 com.google.common.base.Objects;
import com.google.common.collect.LinkedHashMultiset;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
/**
* 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(FailureStrategy failureStrategy, @Nullable Map<?, ?> map) {
super(failureStrategy, map);
}
/** Fails if the subject is not equal to the given object. */
@Override
public void isEqualTo(@Nullable Object other) {
if (!Objects.equal(actual(), other)) {
if (other instanceof Map) {
MapDifference<?, ?> diff = Maps.difference((Map<?, ?>) other, (Map<?, ?>) actual());
String errorMsg = "The subject";
if (!diff.entriesOnlyOnLeft().isEmpty()) {
errorMsg += " is missing the following entries: " + diff.entriesOnlyOnLeft();
if (!diff.entriesOnlyOnRight().isEmpty() || !diff.entriesDiffering().isEmpty()) {
errorMsg += " and";
}
}
if (!diff.entriesOnlyOnRight().isEmpty()) {
errorMsg += " has the following extra entries: " + diff.entriesOnlyOnRight();
if (!diff.entriesDiffering().isEmpty()) {
errorMsg += " and";
}
}
if (!diff.entriesDiffering().isEmpty()) {
errorMsg += " has the following different entries: " + diff.entriesDiffering();
}
failWithRawMessage(
"Not true that %s is equal to <%s>. " + errorMsg, actualAsString(), other);
} else {
fail("is equal to", other);
}
}
}
/** Fails if the map is not empty. */
public void isEmpty() {
if (!actual().isEmpty()) {
fail("is empty");
}
}
/** Fails if the map is empty. */
public void isNotEmpty() {
if (actual().isEmpty()) {
fail("is not 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);
int actualSize = actual().size();
if (actualSize != expectedSize) {
failWithBadResults("has a size of", expectedSize, "is", actualSize);
}
}
/** Fails if the map does not contain the given key. */
public void containsKey(@Nullable Object key) {
if (!actual().containsKey(key)) {
fail("contains key", key);
}
}
/** Fails if the map contains the given key. */
public void doesNotContainKey(@Nullable Object key) {
if (actual().containsKey(key)) {
fail("does not contain key", key);
}
}
/** Fails if the map does not contain the given entry. */
public void containsEntry(@Nullable Object key, @Nullable Object value) {
Entry<Object, Object> entry = Maps.immutableEntry(key, value);
if (!actual().entrySet().contains(entry)) {
if (actual().containsKey(key)) {
failWithRawMessage(
"Not true that %s contains entry <%s>. However, it has a mapping from <%s> to <%s>",
actualAsString(), entry, key, actual().get(key));
}
if (actual().containsValue(value)) {
Set<Object> keys = new LinkedHashSet<Object>();
for (Entry<?, ?> actualEntry : actual().entrySet()) {
if (Objects.equal(actualEntry.getValue(), value)) {
keys.add(actualEntry.getKey());
}
}
failWithRawMessage(
"Not true that %s contains entry <%s>. "
+ "However, the following keys are mapped to <%s>: %s",
actualAsString(), entry, value, keys);
}
fail("contains entry", entry);
}
}
/** Fails if the map contains the given entry. */
public void doesNotContainEntry(@Nullable Object key, @Nullable Object value) {
Entry<Object, Object> entry = Maps.immutableEntry(key, value);
if (actual().entrySet().contains(entry)) {
fail("does not contain entry", entry);
}
}
/** Fails if the map is not empty. */
@CanIgnoreReturnValue
public Ordered containsExactly() {
return check().that(actual().entrySet()).containsExactly();
}
/**
* 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!
*/
@CanIgnoreReturnValue
public Ordered containsExactly(@Nullable Object k0, @Nullable Object v0, Object... rest) {
return containsExactlyEntriesIn(accumulateMap(k0, v0, rest));
}
private static Map<Object, Object> accumulateMap(
@Nullable Object k0, @Nullable 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 containsExactly().",
keys);
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) {
return check().that(actual().entrySet()).containsExactlyElementsIn(expectedMap.entrySet());
}
/**
* Starts a method chain for a test proposition 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
* proposition 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<A, E> correspondence) {
return new UsingCorrespondence<A, E>(correspondence);
}
/**
* A partially specified proposition 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 proposition.
*
* <p>Note that keys will always be compared with regular object equality ({@link Object#equals}).
*/
public final class UsingCorrespondence<A, E> {
private final Correspondence<A, E> correspondence;
private UsingCorrespondence(Correspondence<A, 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(@Nullable Object expectedKey, @Nullable E expectedValue) {
if (actual().containsKey(expectedKey)) {
// Found matching key.
A actualValue = getCastSubject().get(expectedKey);
if (correspondence.compare(actualValue, expectedValue)) {
// Found matching key and value. Test passes!
return;
}
// Found matching key with non-matching value.
failWithRawMessage(
"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);
} else {
// Did not find matching key.
Set<Object> keys = new LinkedHashSet<Object>();
for (Entry<?, A> actualEntry : getCastSubject().entrySet()) {
if (correspondence.compare(actualEntry.getValue(), expectedValue)) {
keys.add(actualEntry.getKey());
}
}
if (!keys.isEmpty()) {
// Found matching values with non-matching keys.
failWithRawMessage(
"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);
} else {
// Did not find matching key or value.
failWithRawMessage(
"Not true that %s contains an entry with key <%s> and a value that %s <%s>",
actualAsString(), expectedKey, correspondence, expectedValue);
}
}
}
/**
* Fails if the map contains an entry with the given key and a value that corresponds to the
* given value.
*/
public void doesNotContainEntry(@Nullable Object excludedKey, @Nullable E excludedValue) {
if (actual().containsKey(excludedKey)) {
A actualValue = getCastSubject().get(excludedKey);
if (correspondence.compare(actualValue, excludedValue)) {
failWithRawMessage(
"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);
}
}
}
/**
* 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(@Nullable Object k0, @Nullable E v0, Object... rest) {
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
Map<Object, E> expectedMap = (Map<Object, E>) accumulateMap(k0, v0, rest);
return containsExactlyEntriesIn(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) {
return check()
.that(actual().entrySet())
.comparingElementsUsing(new EntryCorrespondence<K, A, V>(correspondence))
.containsExactlyElementsIn(expectedMap.entrySet());
}
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
private Map<?, A> getCastSubject() {
return (Map<?, A>) actual();
}
}
static final class EntryCorrespondence<K, A, E>
extends Correspondence<Map.Entry<K, A>, Map.Entry<K, E>> {
private final Correspondence<A, ? super E> valueCorrespondence;
EntryCorrespondence(Correspondence<A, ? super E> valueCorrespondence) {
this.valueCorrespondence = valueCorrespondence;
}
@Override
public boolean compare(Entry<K, A> actual, Entry<K, E> expected) {
return actual.getKey().equals(expected.getKey())
&& valueCorrespondence.compare(actual.getValue(), expected.getValue());
}
@Override
public String toString() {
return StringUtil.format(
"has a key that is equal to and a value that %s the key and value of",
valueCorrespondence);
}
}
}