blob: 8725de0f9ddb873c44abe05e3542afeb612a8431 [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.truth.SubjectUtils.accumulate;
import static com.google.common.truth.SubjectUtils.countDuplicates;
import static java.util.Arrays.asList;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.LinkedHashMultiset;
import com.google.common.collect.Lists;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Propositions for {@link Iterable} subjects.
*
* <p><b>Note:</b>
*
* <ul>
* <li>Assertions may iterate through the given {@link Iterable} more than once. If you have an
* unusual implementation of {@link Iterable} which does not support multiple iterations
* (sometimes known as a "one-shot iterable"), you must copy your iterable into a collection
* which does (e.g. {@code ImmutableList.copyOf(iterable)} or, if your iterable may contain
* null, {@code newArrayList(iterable)}). If you don't, you may see surprising failures.
* <li>Assertions may also require that the elements in the given {@link Iterable} implement
* {@link Object#hashCode} correctly.
* </ul>
*
* @author Kurt Alfred Kluever
* @author Pete Gillin
*/
// Can't be final since MultisetSubject extends it
public class IterableSubject extends Subject<IterableSubject, Iterable<?>> {
// TODO(kak): Make this package-protected?
protected IterableSubject(FailureStrategy failureStrategy, @Nullable Iterable<?> list) {
super(failureStrategy, list);
}
/** Fails if the subject is not empty. */
public final void isEmpty() {
if (!Iterables.isEmpty(actual())) {
fail("is empty");
}
}
/** Fails if the subject is empty. */
public final void isNotEmpty() {
if (Iterables.isEmpty(actual())) {
// TODO(kak): "Not true that <[]> is not empty" doesn't really need the <[]>,
// since it's empty. But would the bulkier "the subject" really be better?
// At best, we could *replace* <[]> with a given label (rather than supplementing it).
// Perhaps the right failure message is just "<[]> should not have been empty"
fail("is not empty");
}
}
/** Fails if the subject does not have the given size. */
public final void hasSize(int expectedSize) {
checkArgument(expectedSize >= 0, "expectedSize(%s) must be >= 0", expectedSize);
int actualSize = Iterables.size(actual());
if (actualSize != expectedSize) {
failWithBadResults("has a size of", expectedSize, "is", actualSize);
}
}
/** Attests (with a side-effect failure) that the subject contains the supplied item. */
public final void contains(@Nullable Object element) {
if (!Iterables.contains(actual(), element)) {
failWithRawMessage("%s should have contained <%s>", actualAsString(), element);
}
}
/** Attests (with a side-effect failure) that the subject does not contain the supplied item. */
public final void doesNotContain(@Nullable Object element) {
if (Iterables.contains(actual(), element)) {
failWithRawMessage("%s should not have contained <%s>", actualAsString(), element);
}
}
/** Attests that the subject does not contain duplicate elements. */
public final void containsNoDuplicates() {
List<Entry<?>> duplicates = Lists.newArrayList();
for (Multiset.Entry<?> entry : LinkedHashMultiset.create(actual()).entrySet()) {
if (entry.getCount() > 1) {
duplicates.add(entry);
}
}
if (!duplicates.isEmpty()) {
failWithRawMessage("%s has the following duplicates: <%s>", actualAsString(), duplicates);
}
}
/** Attests that the subject contains at least one of the provided objects or fails. */
public final void containsAnyOf(
@Nullable Object first, @Nullable Object second, @Nullable Object... rest) {
containsAny("contains any of", accumulate(first, second, rest));
}
/**
* Attests that the subject contains at least one of the objects contained in the provided
* collection or fails.
*/
public final void containsAnyIn(Iterable<?> expected) {
containsAny("contains any element in", expected);
}
private void containsAny(String failVerb, Iterable<?> expected) {
Collection<?> actual = iterableToCollection(actual());
for (Object item : expected) {
if (actual.contains(item)) {
return;
}
}
fail(failVerb, expected);
}
private static <T> Collection<T> iterableToCollection(Iterable<T> iterable) {
if (iterable instanceof Collection) {
// Should be safe to assume that any Iterable implementing Collection isn't a one-shot
// iterable, right? I sure hope so.
return (Collection<T>) iterable;
} else {
return Lists.newArrayList(iterable);
}
}
private static <T> List<T> iterableToList(Iterable<T> iterable) {
if (iterable instanceof List) {
return (List<T>) iterable;
} else {
return Lists.newArrayList(iterable);
}
}
/**
* Attests that the actual iterable contains at least all of the expected elements or fails. If an
* element appears more than once in the expected elements to this call then it must appear at
* least that number of times in the actual elements.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method. The expected elements must appear in the given order
* within the actual elements, but they are not required to be consecutive.
*/
@CanIgnoreReturnValue
public final Ordered containsAllOf(
@Nullable Object firstExpected,
@Nullable Object secondExpected,
@Nullable Object... restOfExpected) {
return containsAll(
"contains all of", accumulate(firstExpected, secondExpected, restOfExpected));
}
/**
* Attests that the actual iterable contains at least all of the expected elements or fails. If an
* element appears more than once in the expected elements then it must appear at least that
* number of times in the actual elements.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method. The expected elements must appear in the given order
* within the actual elements, but they are not required to be consecutive.
*/
@CanIgnoreReturnValue
public final Ordered containsAllIn(Iterable<?> expected) {
return containsAll("contains all elements in", expected);
}
private Ordered containsAll(String failVerb, Iterable<?> expectedIterable) {
List<?> actual = Lists.newLinkedList(actual());
Collection<?> expected = iterableToCollection(expectedIterable);
List<Object> missing = Lists.newArrayList();
List<Object> actualNotInOrder = Lists.newArrayList();
boolean ordered = true;
// step through the expected elements...
for (Object e : expected) {
int index = actual.indexOf(e);
if (index != -1) { // if we find the element in the actual list...
// drain all the elements that come before that element into actualNotInOrder
moveElements(actual, actualNotInOrder, index);
// and remove the element from the actual list
actual.remove(0);
} else { // otherwise try removing it from actualNotInOrder...
if (actualNotInOrder.remove(e)) { // if it was in actualNotInOrder, we're not in order
ordered = false;
} else { // if it's not in actualNotInOrder, we're missing an expected element
missing.add(e);
}
}
}
// if we have any missing expected elements, fail
if (!missing.isEmpty()) {
failWithBadResults(failVerb, expected, "is missing", countDuplicates(missing));
}
return ordered ? IN_ORDER : new NotInOrder("contains all elements in order", expected);
}
/**
* Removes at most the given number of available elements from the input list and adds them to the
* given output collection.
*/
private static void moveElements(List<?> input, Collection<Object> output, int maxElements) {
for (int i = 0; i < maxElements; i++) {
output.add(input.remove(0));
}
}
/**
* Attests that a subject contains exactly the provided objects or fails.
*
* <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the
* parameters asserts that the object must likewise be duplicated exactly 3 times in the subject.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method.
*/
@CanIgnoreReturnValue
public final Ordered containsExactly(@Nullable Object... varargs) {
List<Object> expected = (varargs == null) ? Lists.newArrayList((Object) null) : asList(varargs);
return containsExactlyElementsIn(
expected, varargs != null && varargs.length == 1 && varargs[0] instanceof Iterable);
}
/**
* Attests that a subject contains exactly the provided objects or fails.
*
* <p>Multiplicity is respected. For example, an object duplicated exactly 3 times in the {@code
* Iterable} parameter asserts that the object must likewise be duplicated exactly 3 times in the
* subject.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method.
*/
@CanIgnoreReturnValue
public final Ordered containsExactlyElementsIn(Iterable<?> expected) {
return containsExactlyElementsIn(expected, false);
}
private Ordered containsExactlyElementsIn(Iterable<?> required, boolean addElementsInWarning) {
String failSuffix =
addElementsInWarning
? ". Passing an iterable to the varargs method containsExactly(Object...) is "
+ "often not the correct thing to do. Did you mean to call "
+ "containsExactlyElementsIn(Iterable) instead?"
: "";
Iterator<?> actualIter = actual().iterator();
Iterator<?> requiredIter = required.iterator();
// Step through both iterators comparing elements pairwise.
while (actualIter.hasNext() && requiredIter.hasNext()) {
Object actualElement = actualIter.next();
Object requiredElement = requiredIter.next();
// As soon as we encounter a pair of elements that differ, we know that inOrder()
// cannot succeed, so we can check the rest of the elements more normally.
// Since any previous pairs of elements we iterated over were equal, they have no
// effect on the result now.
if (!Objects.equal(actualElement, requiredElement)) {
// Missing elements; elements that are not missing will be removed as we iterate.
Collection<Object> missing = Lists.newArrayList();
missing.add(requiredElement);
Iterators.addAll(missing, requiredIter);
// Extra elements that the subject had but shouldn't have.
Collection<Object> extra = Lists.newArrayList();
// Remove all actual elements from missing, and add any that weren't in missing
// to extra.
if (!missing.remove(actualElement)) {
extra.add(actualElement);
}
while (actualIter.hasNext()) {
Object item = actualIter.next();
if (!missing.remove(item)) {
extra.add(item);
}
}
// Fail if there are either missing or extra elements.
// TODO(kak): Possible enhancement: Include "[1 copy]" if the element does appear in
// the subject but not enough times. Similarly for unexpected extra items.
if (!missing.isEmpty()) {
if (!extra.isEmpty()) {
// Subject is both missing required elements and contains extra elements
failWithRawMessage(
"Not true that %s contains exactly <%s>. "
+ "It is missing <%s> and has unexpected items <%s>%s",
actualAsString(),
required,
countDuplicates(missing),
countDuplicates(extra),
failSuffix);
} else {
failWithBadResultsAndSuffix(
"contains exactly", required, "is missing", countDuplicates(missing), failSuffix);
}
}
if (!extra.isEmpty()) {
failWithBadResultsAndSuffix(
"contains exactly",
required,
"has unexpected items",
countDuplicates(extra),
failSuffix);
}
// Since we know the iterables were not in the same order, inOrder() can just fail.
return new NotInOrder("contains exactly these elements in order", required);
}
}
// Here, we must have reached the end of one of the iterators without finding any
// pairs of elements that differ. If the actual iterator still has elements, they're
// extras. If the required iterator has elements, they're missing elements.
if (actualIter.hasNext()) {
failWithBadResultsAndSuffix(
"contains exactly",
required,
"has unexpected items",
countDuplicates(Lists.newArrayList(actualIter)),
failSuffix);
} else if (requiredIter.hasNext()) {
failWithBadResultsAndSuffix(
"contains exactly",
required,
"is missing",
countDuplicates(Lists.newArrayList(requiredIter)),
failSuffix);
}
// If neither iterator has elements, we reached the end and the elements were in
// order, so inOrder() can just succeed.
return IN_ORDER;
}
/**
* Fails with the bad results and a suffix.
*
* @param verb the proposition being asserted
* @param expected the expectations against which the subject is compared
* @param failVerb the failure of the proposition being asserted
* @param actual the actual value the subject was compared against
* @param suffix a suffix to append to the failure message
*/
protected final void failWithBadResultsAndSuffix(
String verb, Object expected, String failVerb, Object actual, String suffix) {
failWithRawMessage(
"Not true that %s %s <%s>. It %s <%s>%s",
actualAsString(),
verb,
expected,
failVerb,
(actual == null) ? "null reference" : actual,
suffix);
}
/**
* Attests that a actual iterable contains none of the excluded objects or fails. (Duplicates are
* irrelevant to this test, which fails if any of the actual elements equal any of the excluded.)
*/
public final void containsNoneOf(
@Nullable Object firstExcluded,
@Nullable Object secondExcluded,
@Nullable Object... restOfExcluded) {
containsNone("contains none of", accumulate(firstExcluded, secondExcluded, restOfExcluded));
}
/**
* Attests that a actual iterable contains none of the elements contained in the excluded iterable
* or fails. (Duplicates are irrelevant to this test, which fails if any of the actual elements
* equal any of the excluded.)
*/
public final void containsNoneIn(Iterable<?> excluded) {
containsNone("contains no elements in", excluded);
}
private void containsNone(String failVerb, Iterable<?> excluded) {
Collection<?> actual = iterableToCollection(actual());
Collection<Object> present = new ArrayList<Object>();
for (Object item : Sets.newLinkedHashSet(excluded)) {
if (actual.contains(item)) {
present.add(item);
}
}
if (!present.isEmpty()) {
failWithBadResults(failVerb, excluded, "contains", present);
}
}
/** Ordered implementation that always fails. */
private class NotInOrder implements Ordered {
private final String check;
private final Iterable<?> required;
NotInOrder(String check, Iterable<?> required) {
this.check = check;
this.required = required;
}
@Override
public void inOrder() {
fail(check, required);
}
}
/** 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() {}
};
/**
* Fails if the iterable is not strictly ordered, according to the natural ordering of its
* elements. Strictly ordered means that each element in the iterable is <i>strictly</i> greater
* than the element that preceded it.
*
* @throws ClassCastException if any pair of elements is not mutually Comparable
* @throws NullPointerException if any element is null
*/
public final void isStrictlyOrdered() {
isStrictlyOrdered(Ordering.natural());
}
/**
* Fails if the iterable is not strictly ordered, according to the given comparator. Strictly
* ordered means that each element in the iterable is <i>strictly</i> greater than the element
* that preceded it.
*
* @throws ClassCastException if any pair of elements is not mutually Comparable
*/
@SuppressWarnings({"unchecked"})
public final void isStrictlyOrdered(final Comparator<?> comparator) {
checkNotNull(comparator);
pairwiseCheck(
new PairwiseChecker() {
@Override
public void check(Object prev, Object next) {
if (((Comparator<Object>) comparator).compare(prev, next) >= 0) {
fail("is strictly ordered", prev, next);
}
}
});
}
/**
* Fails if the iterable is not ordered, according to the natural ordering of its elements.
* Ordered means that each element in the iterable is greater than or equal to the element that
* preceded it.
*
* @throws ClassCastException if any pair of elements is not mutually Comparable
* @throws NullPointerException if any element is null
*/
public final void isOrdered() {
isOrdered(Ordering.natural());
}
/** @deprecated Use {@link #isOrdered} instead. */
@Deprecated
public final void isPartiallyOrdered() {
isOrdered();
}
/**
* Fails if the iterable is not ordered, according to the given comparator. Ordered means that
* each element in the iterable is greater than or equal to the element that preceded it.
*
* @throws ClassCastException if any pair of elements is not mutually Comparable
*/
@SuppressWarnings({"unchecked"})
public final void isOrdered(final Comparator<?> comparator) {
checkNotNull(comparator);
pairwiseCheck(
new PairwiseChecker() {
@Override
public void check(Object prev, Object next) {
if (((Comparator<Object>) comparator).compare(prev, next) > 0) {
fail("is ordered", prev, next);
}
}
});
}
/** @deprecated Use {@link #isOrdered(Comparator)} instead. */
@Deprecated
public final void isPartiallyOrdered(final Comparator<?> comparator) {
isOrdered(comparator);
}
private interface PairwiseChecker {
void check(Object prev, Object next);
}
private void pairwiseCheck(PairwiseChecker checker) {
Iterator<?> iterator = actual().iterator();
if (iterator.hasNext()) {
Object prev = iterator.next();
while (iterator.hasNext()) {
Object next = iterator.next();
checker.check(prev, next);
prev = next;
}
}
}
/**
* Starts a method chain for a test proposition in which the actual elements (i.e. the elements of
* the {@link Iterable} under test) are compared to expected elements using the given {@link
* Correspondence}. The actual elements must be of type {@code A}, the expected elements must be
* of type {@code E}. The proposition is actually executed by continuing the method chain. For
* example: <pre> {@code
* assertThat(actualIterable).comparingElementsUsing(correspondence).contains(expected);}</pre>
* where {@code actualIterable} is an {@code Iterable<A>} (or, more generally, an {@code
* Iterable<? extends A>}), {@code correspondence} is a {@code Correspondence<A, E>}, and {@code
* expected} is an {@code E}.
*
* <p>Any of the methods on the returned object may throw {@link ClassCastException} if they
* encounter an actual element that is not of type {@code A}.
*/
public <A, E> UsingCorrespondence<A, E> comparingElementsUsing(
Correspondence<A, E> correspondence) {
return new UsingCorrespondence<A, E>(correspondence);
}
/**
* A partially specified proposition in which the actual elements (normally the elements of the
* {@link Iterable} under test) are compared to expected elements using a {@link Correspondence}.
* The expected elements are of type {@code E}. Call methods on this object to actually execute
* the proposition.
*
* <p>The actual elements may alternatively be from an array of doubles or floats (see {@link
* PrimitiveDoubleArraySubject#usingTolerance} and {@link
* PrimitiveFloatArraySubject#usingTolerance}).
*
* <p>NOTE: This class is under constructions and more methods will be added in future versions.
*/
public final class UsingCorrespondence<A, E> {
private final Correspondence<A, E> correspondence;
private UsingCorrespondence(Correspondence<A, E> correspondence) {
this.correspondence = checkNotNull(correspondence);
}
/**
* Attests that the subject contains at least one element that corresponds to the given expected
* element.
*/
public void contains(@Nullable E expected) {
for (A actual : getCastActual()) {
if (correspondence.compare(actual, expected)) {
return;
}
}
fail("contains at least one element that " + correspondence, expected);
}
/** Attests that none of the actual elements correspond to the given element. */
public void doesNotContain(@Nullable E excluded) {
List<A> matchingElements = new ArrayList<A>();
for (A actual : getCastActual()) {
if (correspondence.compare(actual, excluded)) {
matchingElements.add(actual);
}
}
if (!matchingElements.isEmpty()) {
failWithRawMessage(
"%s should not have contained an element that %s <%s>. "
+ "It contained the following such elements: <%s>",
actualAsString(), correspondence, excluded, matchingElements);
}
}
/**
* Attests that subject contains exactly elements that correspond to the expected elements, i.e.
* that there is a 1:1 mapping between the actual elements and the expected elements where each
* pair of elements correspond.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method.
*/
@SafeVarargs
@CanIgnoreReturnValue
public final Ordered containsExactly(@Nullable E... expected) {
return containsExactlyElementsIn(
(expected == null) ? Lists.newArrayList((E) null) : asList(expected));
}
/**
* Attests that subject contains exactly elements that correspond to the expected elements, i.e.
* that there is a 1:1 mapping between the actual elements and the expected elements where each
* pair of elements correspond.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method.
*/
@CanIgnoreReturnValue
public Ordered containsExactlyElementsIn(Iterable<? extends E> expected) {
List<A> actualList = iterableToList(getCastActual());
List<? extends E> expectedList = iterableToList(expected);
// Check if the elements correspond in order. This allows the common case of a passing test
// using inOrder() to complete in linear time.
if (correspondInOrderExactly(actualList.iterator(), expectedList.iterator())) {
return IN_ORDER;
}
// We know they don't correspond in order, so we're going to have to do an any-order test.
// Find a many:many mapping between the indexes of the elements which correspond, and check
// it for completeness.
ImmutableSetMultimap<Integer, Integer> candidateMapping =
findCandidateMapping(actualList, expectedList);
failIfCandidateMappingHasMissingOrExtra(actualList, expectedList, candidateMapping);
// We know that every expected element maps to at least one actual element, and vice versa.
// Find a maximal 1:1 mapping, and check it for completeness.
ImmutableBiMap<Integer, Integer> maximalOneToOneMapping =
findMaximalOneToOneMapping(candidateMapping);
failIfOneToOneMappingHasMissingOrExtra(actualList, expectedList, maximalOneToOneMapping);
// The 1:1 mapping is complete, so the test succeeds (but we know from above that the mapping
// is not in order).
return new NotInOrder(
"contains, in order, exactly one element that " + correspondence + " each element of",
expected);
}
/**
* Returns whether the actual and expected iterators have the same number of elements and, when
* iterated pairwise, every pair of actual and expected values satisfies the correspondence.
*/
private boolean correspondInOrderExactly(
Iterator<? extends A> actual, Iterator<? extends E> expected) {
while (actual.hasNext() && expected.hasNext()) {
A actualElement = actual.next();
E expectedElement = expected.next();
if (!correspondence.compare(actualElement, expectedElement)) {
return false;
}
}
return !(actual.hasNext() || expected.hasNext());
}
/**
* Given a list of actual elements and a list of expected elements, finds a many:many mapping
* between actual and expected elements where a pair of elements maps if it satisfies the
* correspondence. Returns this mapping as a multimap where the keys are indexes into the actual
* list and the values are indexes into the expected list.
*/
private ImmutableSetMultimap<Integer, Integer> findCandidateMapping(
List<? extends A> actual, List<? extends E> expected) {
ImmutableSetMultimap.Builder<Integer, Integer> mapping = ImmutableSetMultimap.builder();
for (int actualIndex = 0; actualIndex < actual.size(); actualIndex++) {
for (int expectedIndex = 0; expectedIndex < expected.size(); expectedIndex++) {
if (correspondence.compare(actual.get(actualIndex), expected.get(expectedIndex))) {
mapping.put(actualIndex, expectedIndex);
}
}
}
return mapping.build();
}
/**
* Given a list of actual elements, a list of expected elements, and a many:many mapping between
* actual and expected elements specified as a multimap of indexes into the actual list to
* indexes into the expected list, checks that every actual element maps to at least one
* expected element and vice versa, and fails if this is not the case.
*/
void failIfCandidateMappingHasMissingOrExtra(
List<? extends A> actual,
List<? extends E> expected,
ImmutableMultimap<Integer, Integer> mapping) {
List<? extends A> extra = findNotIndexed(actual, mapping.keySet());
List<? extends E> missing = findNotIndexed(expected, mapping.inverse().keySet());
Optional<String> missingOrExtraMessage = describeMissingOrExtra(extra, missing);
if (missingOrExtraMessage.isPresent()) {
failWithRawMessage(
"Not true that %s contains exactly one element that %s each element of <%s>. It %s",
actualAsString(),
correspondence,
expected,
missingOrExtraMessage.get());
}
}
/**
* Given a list of extra elements and a list of missing elements, returns an absent value if
* both are empty, and otherwise returns a verb phrase (suitable for appearing after the subject
* of the verb) describing them.
*/
private Optional<String> describeMissingOrExtra(
List<? extends A> extra, List<? extends E> missing) {
if (!missing.isEmpty() && !extra.isEmpty()) {
return Optional.of(
StringUtil.format(
"is missing an element that %s %s and has unexpected elements <%s>",
correspondence, formatMissing(missing), extra));
} else if (!missing.isEmpty()) {
return Optional.of(
StringUtil.format(
"is missing an element that %s %s", correspondence, formatMissing(missing)));
} else if (!extra.isEmpty()) {
return Optional.of(StringUtil.format("has unexpected elements <%s>", extra));
} else {
return Optional.absent();
}
}
/**
* Returns all the elements of the given list other than those with the given indexes. Assumes
* that all the given indexes really are valid indexes into the list.
*/
private <T> List<T> findNotIndexed(List<T> list, Set<Integer> indexes) {
if (indexes.size() == list.size()) {
// If there are as many distinct valid indexes are there are elements in the list then every
// index must be in there once.
return ImmutableList.of();
}
List<T> notIndexed = Lists.newArrayList();
for (int index = 0; index < list.size(); index++) {
if (!indexes.contains(index)) {
notIndexed.add(list.get(index));
}
}
return notIndexed;
}
/**
* Returns a description of the missing items suitable for inclusion in failure messages. If
* there is a single item, returns {@code "<item>"}. Otherwise, returns
* {@code "each of <[item, item, item]>"}.
*/
private String formatMissing(List<?> missing) {
if (missing.size() == 1) {
return "<" + missing.get(0) + ">";
} else {
return "each of <" + missing + ">";
}
}
/**
* Given a many:many mapping between actual elements and expected elements, finds a 1:1
* mapping which is the subset of that many:many mapping which includes the largest possible
* number of elements. The input and output mappings are each described as a map or multimap
* where the keys are indexes into the actual list and the values are indexes into the expected
* list. If there are multiple possible output mappings tying for the largest possible, this
* returns an arbitrary one.
*/
private ImmutableBiMap<Integer, Integer> findMaximalOneToOneMapping(
ImmutableMultimap<Integer, Integer> edges) {
/*
* Finding this 1:1 mapping is analogous to finding a maximum cardinality bipartite matching
* (https://en.wikipedia.org/wiki/Matching_(graph_theory)#In_unweighted_bipartite_graphs).
* - The two sets of elements together correspond to the vertices of a graph.
* - The many:many mapping corresponds to the edges of that graph.
* - The graph is therefore bipartite, with the two sets of elements corresponding to the two
* parts.
* - A 1:1 mapping corresponds to a matching on that bipartite graph (aka an independent edge
* set, i.e. a subset of the edges with no common vertices).
* - And the 1:1 mapping which includes the largest possible number of elements corresponds
* to the maximum cardinality matching.
*
* So we'll apply a standard algorithm for doing maximum cardinality bipartite matching.
*/
return GraphMatching.maximumCardinalityBipartiteMatching(edges);
}
/**
* Given a list of actual elements, a list of expected elements, and a 1:1 mapping between
* actual and expected elements specified as a bimap of indexes into the actual list to indexes
* into the expected list, checks that every actual element maps to an expected element and vice
* versa, and fails if this is not the case.
*/
void failIfOneToOneMappingHasMissingOrExtra(
List<? extends A> actual,
List<? extends E> expected,
BiMap<Integer, Integer> mapping) {
List<? extends A> extra = findNotIndexed(actual, mapping.keySet());
List<? extends E> missing = findNotIndexed(expected, mapping.values());
Optional<String> missingOrExtraMessage = describeMissingOrExtra(extra, missing);
if (missingOrExtraMessage.isPresent()) {
failWithRawMessage(
"Not true that %s contains exactly one element that %s each element of <%s>. "
+ "It contains at least one element that matches each expected element, "
+ "and every element it contains matches at least one expected element, "
+ "but there was no 1:1 mapping between all the actual and expected elements. "
+ "Using the most complete 1:1 mapping (or one such mapping, if there is a tie), "
+ "it %s",
actualAsString(),
correspondence,
expected,
missingOrExtraMessage.get());
}
}
/**
* Attests that the subject contains elements that corresponds to all of the expected elements,
* i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
* elements where each pair of elements correspond.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method. The elements must appear in the given order within the
* subject, but they are not required to be consecutive.
*/
@SafeVarargs
@CanIgnoreReturnValue
public final Ordered containsAllOf(@Nullable E first, @Nullable E second, @Nullable E... rest) {
return containsAllIn(accumulate(first, second, rest));
}
/**
* Attests that the subject contains elements that corresponds to all of the expected elements,
* i.e. that there is a 1:1 mapping between any subset of the actual elements and the expected
* elements where each pair of elements correspond.
*
* <p>To also test that the contents appear in the given order, make a call to {@code inOrder()}
* on the object returned by this method. The elements must appear in the given order within the
* subject, but they are not required to be consecutive.
*/
@CanIgnoreReturnValue
public Ordered containsAllIn(Iterable<? extends E> expected) {
List<A> actualList = iterableToList(getCastActual());
List<? extends E> expectedList = iterableToList(expected);
// Check if the expected elements correspond in order to any subset of the actual elements.
// This allows the common case of a passing test using inOrder() to complete in linear time.
if (correspondInOrderAllIn(actualList.iterator(), expectedList.iterator())) {
return IN_ORDER;
}
// We know they don't correspond in order, so we're going to have to do an any-order test.
// Find a many:many mapping between the indexes of the elements which correspond, and check
// it for completeness.
ImmutableSetMultimap<Integer, Integer> candidateMapping =
findCandidateMapping(actualList, expectedList);
failIfCandidateMappingHasMissing(expectedList, candidateMapping);
// We know that every expected element maps to at least one actual element, and vice versa.
// Find a maximal 1:1 mapping, and check it for completeness.
ImmutableBiMap<Integer, Integer> maximalOneToOneMapping =
findMaximalOneToOneMapping(candidateMapping);
failIfOneToOneMappingHasMissing(expectedList, maximalOneToOneMapping);
// The 1:1 mapping maps all the expected elements, so the test succeeds (but we know from
// above that the mapping is not in order).
return new NotInOrder(
"contains, in order, at least one element that " + correspondence + " each element of",
expected);
}
/**
* Returns whether all the elements of the expected iterator and any subset of the elements of
* the actual iterator can be paired up in order, such that every pair of actual and expected
* elements satisfies the correspondence.
*/
private boolean correspondInOrderAllIn(
Iterator<? extends A> actual, Iterator<? extends E> expected) {
// We take a greedy approach here, iterating through the expected elements and pairing each
// with the first applicable actual element. This is fine for the in-order test, since there's
// no way that paring an expected element with a later actual element permits a solution which
// couldn't be achieved by pairing it with the first. (For the any-order test, we may want to
// pair an expected element with a later actual element so that we can pair the earlier actual
// element with a later expected element, but that doesn't apply here.)
while (expected.hasNext()) {
E expectedElement = expected.next();
if (!findCorresponding(actual, expectedElement)) {
return false;
}
}
return true;
}
/**
* Advances the actual iterator looking for an element which corresponds to the expected
* element. Returns whether or not it finds one.
*/
private boolean findCorresponding(Iterator<? extends A> actual, E expectedElement) {
while (actual.hasNext()) {
A actualElement = actual.next();
if (correspondence.compare(actualElement, expectedElement)) {
return true;
}
}
return false;
}
/**
* Given a list of expected elements and a many:many mapping between actual and expected
* elements specified as a multimap of indexes into an actual list to indexes into the expected
* list, checks that every expected element maps to at least one actual element, and fails if
* this is not the case. Actual elements which do not map to any expected elements are ignored.
*/
void failIfCandidateMappingHasMissing(
List<? extends E> expected, ImmutableMultimap<Integer, Integer> mapping) {
List<? extends E> missing = findNotIndexed(expected, mapping.inverse().keySet());
if (!missing.isEmpty()) {
failWithRawMessage(
"Not true that %s contains at least one element that %s each element of <%s>. "
+ "It is missing an element that %s %s",
actualAsString(), correspondence, expected, correspondence, formatMissing(missing));
}
}
/**
* Given a list of expected elements, and a 1:1 mapping between actual and expected elements
* specified as a bimap of indexes into an actual list to indexes into the expected list, checks
* that every expected element maps to an actual element. Actual elements which do not map to
* any expected elements are ignored.
*/
void failIfOneToOneMappingHasMissing(
List<? extends E> expected, BiMap<Integer, Integer> mapping) {
List<? extends E> missing = findNotIndexed(expected, mapping.values());
if (!missing.isEmpty()) {
failWithRawMessage(
"Not true that %s contains at least one element that %s each element of <%s>. "
+ "It contains at least one element that matches each expected element, "
+ "but there was no 1:1 mapping between all the expected elements and any subset "
+ "of the actual elements. Using the most complete 1:1 mapping (or one such "
+ "mapping, if there is a tie), it is missing an element that %s %s",
actualAsString(), correspondence, expected, correspondence, formatMissing(missing));
}
}
/**
* Attests that the subject contains at least one element that corresponds to at least one of
* the expected elements.
*/
@SafeVarargs
public final void containsAnyOf(@Nullable E first, @Nullable E second, @Nullable E... rest) {
containsAny(
StringUtil.format("contains at least one element that %s any of", correspondence),
accumulate(first, second, rest));
}
/**
* Attests that the subject contains at least one element that corresponds to at least one of
* the expected elements.
*/
public void containsAnyIn(Iterable<? extends E> expected) {
containsAny(
StringUtil.format("contains at least one element that %s any element in", correspondence),
expected);
}
private void containsAny(String failVerb, Iterable<? extends E> expected) {
Collection<A> actual = iterableToCollection(getCastActual());
for (E expectedItem : expected) {
for (A actualItem : actual) {
if (correspondence.compare(actualItem, expectedItem)) {
return;
}
}
}
fail(failVerb, expected);
}
/**
* Attests that the subject contains no elements that correspond to any of the given elements.
* (Duplicates are irrelevant to this test, which fails if any of the subject elements
* correspond to any of the given elements.)
*/
@SafeVarargs
public final void containsNoneOf(
@Nullable E firstExcluded, @Nullable E secondExcluded, @Nullable E... restOfExcluded) {
containsNone("any of", accumulate(firstExcluded, secondExcluded, restOfExcluded));
}
/**
* Attests that the subject contains no elements that correspond to any of the given elements.
* (Duplicates are irrelevant to this test, which fails if any of the subject elements
* correspond to any of the given elements.)
*/
public void containsNoneIn(Iterable<? extends E> excluded) {
containsNone("any element in", excluded);
}
private void containsNone(String excludedPrefix, Iterable<? extends E> excluded) {
Collection<A> actual = iterableToCollection(getCastActual());
Collection<E> present = new ArrayList<E>();
for (E excludedItem : Sets.newLinkedHashSet(excluded)) {
for (A actualItem : actual) {
if (correspondence.compare(actualItem, excludedItem)) {
present.add(excludedItem);
}
}
}
if (!present.isEmpty()) {
failWithRawMessage(
"Not true that %s contains no element that %s %s <%s>. "
+ "It contains at least one element that %s each of <%s>",
actualAsString(), correspondence, excludedPrefix, excluded, correspondence, present);
}
}
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
private Iterable<A> getCastActual() {
return (Iterable<A>) actual();
}
}
}