/*
* Copyright 2014 Google Inc. All rights reserved.
*
* 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 org.inferred.freebuilder.processor;
import static com.google.common.collect.Maps.immutableEntry;
import static com.google.common.truth.Truth.THROW_ASSERTION_ERROR;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.Ordered;
import com.google.common.truth.Subject;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
/** Basic Truth {@link Subject} for {@link Multimap} instances. */
public class MultimapSubject<K, V> extends Subject<MultimapSubject<K, V>, Multimap<K, V>> {
public static <K, V> MultimapSubject<K, V> assertThat(Multimap<K, V> subject) {
return new MultimapSubject<K, V>(THROW_ASSERTION_ERROR, subject);
}
/**
* Fails if the subject is not empty.
*/
public void isEmpty() {
if (!getSubject().isEmpty()) {
fail("is empty");
}
}
/**
* Fails if the subject is empty.
*/
public void isNotEmpty() {
if (getSubject().isEmpty()) {
fail("is empty");
}
}
/**
* Fails if the subject does not contain the given key-value pair(s).
*
* <p>First part of a fluent API for containment assertions. Example:<pre><code>
* assertThat(multimap)
* .contains("fore", 3.22)
* .and("aft", 4.4, 5.2)
* .andNothingElse()
* .inOrder();</pre></code>
*/
@SafeVarargs
public final ContainmentAssertion<K, V> contains(K key, V value, V... values) {
return new InOrderContainmentAssertion().and(key, value, values);
}
public abstract static class ContainmentAssertion<K, V> {
/**
* Fails if the subject does not contain the given key-value pair(s), <em>in addition</em> to
* any entries found by previous {@link #contains} and {@link #and} calls.
*
* @see #contains
*/
@SafeVarargs
public final ContainmentAssertion<K, V> and(K key, V value, V... values) {
ContainmentAssertion<K, V> result = this.and(immutableEntry(key, value));
for (V otherValue : values) {
result = result.and(immutableEntry(key, otherValue));
}
return result;
}
protected abstract ContainmentAssertion<K, V> and(Entry<K, V> entry);
/**
* Fails if the subject contains any entries that have not already been matched by previous
* {@link #contains} and {@link #and} calls. Call {@link Ordered#inOrder()} on the result to
* further assert that all entries were matched in the same order specified by the test.
*
* @see #contains
*/
public abstract Ordered andNothingElse();
}
private MultimapSubject(FailureStrategy failureStrategy, Multimap<K, V> subject) {
super(failureStrategy, subject);
}
/** Implementation of {@code ContainmentAssertion} for a subject that may still be in order. */
private class InOrderContainmentAssertion extends ContainmentAssertion<K, V> {
final Iterator<Entry<K, V>> entryIterator;
final List<Entry<K, V>> expected = new ArrayList<Entry<K, V>>();
InOrderContainmentAssertion() {
entryIterator = getSubject().entries().iterator();
}
@Override protected ContainmentAssertion<K, V> and(Entry<K, V> entry) {
expected.add(entry);
if (!entryIterator.hasNext()) {
fail("contains", expected, "is missing", entry);
return new FailedContainmentAssertion();
}
Entry<K, V> actualEntry = entryIterator.next();
if (entry.equals(actualEntry)) {
return this;
}
// The subject does not match the order specified by the user, but the entry may still be
// present. Delegate to an OutOfOrderContainmentAssertion.
Multimap<K, V> remaining = ArrayListMultimap.create();
remaining.put(actualEntry.getKey(), actualEntry.getValue());
while (entryIterator.hasNext()) {
actualEntry = entryIterator.next();
remaining.put(actualEntry.getKey(), actualEntry.getValue());
}
expected.remove(expected.size() - 1);
return new OutOfOrderContainmentAssertion(expected, remaining).and(entry);
}
@Override public Ordered andNothingElse() {
if (entryIterator.hasNext()) {
failWithBadResults(
"contains", expected, "has unexpected items", ImmutableList.copyOf(entryIterator));
}
return IN_ORDER;
}
}
/** Implementation of {@code ContainmentAssertion} for a subject that is mis-ordered. */
private class OutOfOrderContainmentAssertion extends ContainmentAssertion<K, V> {
final List<Entry<K, V>> expected;
final Multimap<K, V> remaining;
OutOfOrderContainmentAssertion(List<Entry<K, V>> expected, Multimap<K, V> remaining) {
this.expected = expected;
this.remaining = remaining;
}
@Override protected ContainmentAssertion<K, V> and(Entry<K, V> entry) {
expected.add(entry);
if (!remaining.remove(entry.getKey(), entry.getValue())) {
fail("contains", expected, "is missing", entry);
return new FailedContainmentAssertion();
}
return this;
}
@Override public Ordered andNothingElse() {
if (!remaining.isEmpty()) {
failWithBadResults(
"contains", expected, "has unexpected items", remaining.entries());
}
return new NotInOrder("contains only these elements in order", expected);
}
}
/** Implementation of {@code ContainmentAssertion} for a subject that has already failed. */
private class FailedContainmentAssertion extends ContainmentAssertion<K, V> {
@Override
protected ContainmentAssertion<K, V> and(Entry<K, V> entry) {
return this;
}
@Override public Ordered andNothingElse() {
return IN_ORDER;
}
}
/** 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 always fails. */
private class NotInOrder implements Ordered {
final String check;
final Iterable<?> required;
NotInOrder(String check, Iterable<?> required) {
this.check = check;
this.required = required;
}
@Override public void inOrder() {
fail(check, required);
}
}
}