/*
* Copyright (C) 2008 The Guava Authors
*
* 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.collect.testing;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;
import com.google.common.annotations.GwtCompatible;
import junit.framework.AssertionFailedError;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Stack;
/**
* Most of the logic for {@link IteratorTester} and {@link ListIteratorTester}.
*
* <p>This class is GWT compatible.
*
* @param <E> the type of element returned by the iterator
* @param <I> the type of the iterator ({@link Iterator} or
* {@link ListIterator})
*
* @author Kevin Bourrillion
* @author Chris Povirk
*/
@GwtCompatible
abstract class AbstractIteratorTester<E, I extends Iterator<E>> {
private boolean whenNextThrowsExceptionStopTestingCallsToRemove;
private boolean whenAddThrowsExceptionStopTesting;
/**
* Don't verify iterator behavior on remove() after a call to next()
* throws an exception.
*
* <p>JDK 6 currently has a bug where some iterators get into a undefined
* state when next() throws a NoSuchElementException. The correct
* behavior is for remove() to remove the last element returned by
* next, even if a subsequent next() call threw an exception; however
* JDK 6's HashMap and related classes throw an IllegalStateException
* in this case.
*
* <p>Calling this method causes the iterator tester to skip testing
* any remove() in a stimulus sequence after the reference iterator
* throws an exception in next().
*
* <p>TODO: remove this once we're on 6u5, which has the fix.
*
* @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6529795">
* Sun Java Bug 6529795</a>
*/
public void ignoreSunJavaBug6529795() {
whenNextThrowsExceptionStopTestingCallsToRemove = true;
}
/**
* Don't verify iterator behavior after a call to add() throws an exception.
*
* <p>AbstractList's ListIterator implementation gets into a undefined state
* when add() throws an UnsupportedOperationException. Instead of leaving the
* iterator's position unmodified, it increments it, skipping an element or
* even moving past the end of the list.
*
* <p>Calling this method causes the iterator tester to skip testing in a
* stimulus sequence after the iterator under test throws an exception in
* add().
*
* <p>TODO: remove this once the behavior is fixed.
*
* @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6533203">
* Sun Java Bug 6533203</a>
*/
public void stopTestingWhenAddThrowsException() {
whenAddThrowsExceptionStopTesting = true;
}
private Stimulus<E, ? super I>[] stimuli;
private final Iterator<E> elementsToInsert;
private final Set<IteratorFeature> features;
private final List<E> expectedElements;
private final int startIndex;
private final KnownOrder knownOrder;
/**
* Meta-exception thrown by
* {@link AbstractIteratorTester.MultiExceptionListIterator} instead of
* throwing any particular exception type.
*/
// This class is accessible but not supported in GWT.
private static final class PermittedMetaException extends RuntimeException {
final Set<? extends Class<? extends RuntimeException>> exceptionClasses;
PermittedMetaException(
Set<? extends Class<? extends RuntimeException>> exceptionClasses) {
super("one of " + exceptionClasses);
this.exceptionClasses = exceptionClasses;
}
PermittedMetaException(Class<? extends RuntimeException> exceptionClass) {
this(Collections.singleton(exceptionClass));
}
// It's not supported In GWT, it always returns true.
boolean isPermitted(RuntimeException exception) {
for (Class<? extends RuntimeException> clazz : exceptionClasses) {
if (Platform.checkIsInstance(clazz, exception)) {
return true;
}
}
return false;
}
// It's not supported in GWT, it always passes.
void assertPermitted(RuntimeException exception) {
if (!isPermitted(exception)) {
// TODO: use simple class names
String message = "Exception " + exception.getClass()
+ " was thrown; expected " + this;
Helpers.fail(exception, message);
}
}
@Override public String toString() {
return getMessage();
}
private static final long serialVersionUID = 0;
}
private static final class UnknownElementException extends RuntimeException {
private UnknownElementException(Collection<?> expected, Object actual) {
super("Returned value '"
+ actual + "' not found. Remaining elements: " + expected);
}
private static final long serialVersionUID = 0;
}
/**
* Quasi-implementation of {@link ListIterator} that works from a list of
* elements and a set of features to support (from the enclosing
* {@link AbstractIteratorTester} instance). Instead of throwing exceptions
* like {@link NoSuchElementException} at the appropriate times, it throws
* {@link PermittedMetaException} instances, which wrap a set of all
* exceptions that the iterator could throw during the invocation of that
* method. This is necessary because, e.g., a call to
* {@code iterator().remove()} of an unmodifiable list could throw either
* {@link IllegalStateException} or {@link UnsupportedOperationException}.
* Note that iterator implementations should always throw one of the
* exceptions in a {@code PermittedExceptions} instance, since
* {@code PermittedExceptions} is thrown only when a method call is invalid.
*
* <p>This class is accessible but not supported in GWT as it references
* {@link PermittedMetaException}.
*/
protected final class MultiExceptionListIterator implements ListIterator<E> {
// TODO: track seen elements when order isn't guaranteed
// TODO: verify contents afterward
// TODO: something shiny and new instead of Stack
// TODO: test whether null is supported (create a Feature)
/**
* The elements to be returned by future calls to {@code next()}, with the
* first at the top of the stack.
*/
final Stack<E> nextElements = new Stack<E>();
/**
* The elements to be returned by future calls to {@code previous()}, with
* the first at the top of the stack.
*/
final Stack<E> previousElements = new Stack<E>();
/**
* {@link #nextElements} if {@code next()} was called more recently then
* {@code previous}, {@link #previousElements} if the reverse is true, or --
* overriding both of these -- {@code null} if {@code remove()} or
* {@code add()} has been called more recently than either. We use this to
* determine which stack to pop from on a call to {@code remove()} (or to
* pop from and push to on a call to {@code set()}.
*/
Stack<E> stackWithLastReturnedElementAtTop = null;
MultiExceptionListIterator(List<E> expectedElements) {
Helpers.addAll(nextElements, Helpers.reverse(expectedElements));
for (int i = 0; i < startIndex; i++) {
previousElements.push(nextElements.pop());
}
}
@Override
public void add(E e) {
if (!features.contains(IteratorFeature.SUPPORTS_ADD)) {
throw new PermittedMetaException(UnsupportedOperationException.class);
}
previousElements.push(e);
stackWithLastReturnedElementAtTop = null;
}
@Override
public boolean hasNext() {
return !nextElements.isEmpty();
}
@Override
public boolean hasPrevious() {
return !previousElements.isEmpty();
}
@Override
public E next() {
return transferElement(nextElements, previousElements);
}
@Override
public int nextIndex() {
return previousElements.size();
}
@Override
public E previous() {
return transferElement(previousElements, nextElements);
}
@Override
public int previousIndex() {
return nextIndex() - 1;
}
@Override
public void remove() {
throwIfInvalid(IteratorFeature.SUPPORTS_REMOVE);
stackWithLastReturnedElementAtTop.pop();
stackWithLastReturnedElementAtTop = null;
}
@Override
public void set(E e) {
throwIfInvalid(IteratorFeature.SUPPORTS_SET);
stackWithLastReturnedElementAtTop.pop();
stackWithLastReturnedElementAtTop.push(e);
}
/**
* Moves the given element from its current position in
* {@link #nextElements} to the top of the stack so that it is returned by
* the next call to {@link Iterator#next()}. If the element is not in
* {@link #nextElements}, this method throws an
* {@link UnknownElementException}.
*
* <p>This method is used when testing iterators without a known ordering.
* We poll the target iterator's next element and pass it to the reference
* iterator through this method so it can return the same element. This
* enables the assertion to pass and the reference iterator to properly
* update its state.
*/
void promoteToNext(E e) {
if (nextElements.remove(e)) {
nextElements.push(e);
} else {
throw new UnknownElementException(nextElements, e);
}
}
private E transferElement(Stack<E> source, Stack<E> destination) {
if (source.isEmpty()) {
throw new PermittedMetaException(NoSuchElementException.class);
}
destination.push(source.pop());
stackWithLastReturnedElementAtTop = destination;
return destination.peek();
}
private void throwIfInvalid(IteratorFeature methodFeature) {
Set<Class<? extends RuntimeException>> exceptions
= new HashSet<Class<? extends RuntimeException>>();
if (!features.contains(methodFeature)) {
exceptions.add(UnsupportedOperationException.class);
}
if (stackWithLastReturnedElementAtTop == null) {
exceptions.add(IllegalStateException.class);
}
if (!exceptions.isEmpty()) {
throw new PermittedMetaException(exceptions);
}
}
private List<E> getElements() {
List<E> elements = new ArrayList<E>();
Helpers.addAll(elements, previousElements);
Helpers.addAll(elements, Helpers.reverse(nextElements));
return elements;
}
}
public enum KnownOrder { KNOWN_ORDER, UNKNOWN_ORDER }
@SuppressWarnings("unchecked") // creating array of generic class Stimulus
AbstractIteratorTester(int steps, Iterable<E> elementsToInsertIterable,
Iterable<? extends IteratorFeature> features,
Iterable<E> expectedElements, KnownOrder knownOrder, int startIndex) {
// periodically we should manually try (steps * 3 / 2) here; all tests but
// one should still pass (testVerifyGetsCalled()).
stimuli = new Stimulus[steps];
if (!elementsToInsertIterable.iterator().hasNext()) {
throw new IllegalArgumentException();
}
elementsToInsert = Helpers.cycle(elementsToInsertIterable);
this.features = Helpers.copyToSet(features);
this.expectedElements = Helpers.copyToList(expectedElements);
this.knownOrder = knownOrder;
this.startIndex = startIndex;
}
/**
* I'd like to make this a parameter to the constructor, but I can't because
* the stimulus instances refer to {@code this}.
*/
protected abstract Iterable<? extends Stimulus<E, ? super I>>
getStimulusValues();
/**
* Returns a new target iterator each time it's called. This is the iterator
* you are trying to test. This must return an Iterator that returns the
* expected elements passed to the constructor in the given order. Warning: it
* is not enough to simply pull multiple iterators from the same source
* Iterable, unless that Iterator is unmodifiable.
*/
protected abstract I newTargetIterator();
/**
* Override this to verify anything after running a list of Stimuli.
*
* <p>For example, verify that calls to remove() actually removed
* the correct elements.
*
* @param elements the expected elements passed to the constructor, as mutated
* by {@code remove()}, {@code set()}, and {@code add()} calls
*/
protected void verify(List<E> elements) {}
/**
* Executes the test.
*/
public final void test() {
try {
recurse(0);
} catch (RuntimeException e) {
throw new RuntimeException(Arrays.toString(stimuli), e);
}
}
private void recurse(int level) {
// We're going to reuse the stimuli array 3^steps times by overwriting it
// in a recursive loop. Sneaky.
if (level == stimuli.length) {
// We've filled the array.
compareResultsForThisListOfStimuli();
} else {
// Keep recursing to fill the array.
for (Stimulus<E, ? super I> stimulus : getStimulusValues()) {
stimuli[level] = stimulus;
recurse(level + 1);
}
}
}
private void compareResultsForThisListOfStimuli() {
MultiExceptionListIterator reference =
new MultiExceptionListIterator(expectedElements);
I target = newTargetIterator();
boolean shouldStopTestingCallsToRemove = false;
for (int i = 0; i < stimuli.length; i++) {
Stimulus<E, ? super I> stimulus = stimuli[i];
if (stimulus.equals(remove) && shouldStopTestingCallsToRemove) {
break;
}
try {
boolean threwException = stimulus.executeAndCompare(reference, target);
if (threwException
&& stimulus.equals(next)
&& whenNextThrowsExceptionStopTestingCallsToRemove) {
shouldStopTestingCallsToRemove = true;
}
if (threwException
&& stimulus.equals(add)
&& whenAddThrowsExceptionStopTesting) {
break;
}
List<E> elements = reference.getElements();
verify(elements);
} catch (AssertionFailedError cause) {
Helpers.fail(cause,
"failed with stimuli " + subListCopy(stimuli, i + 1));
}
}
}
private static List<Object> subListCopy(Object[] source, int size) {
final Object[] copy = new Object[size];
System.arraycopy(source, 0, copy, 0, size);
return Arrays.asList(copy);
}
private interface IteratorOperation {
Object execute(Iterator<?> iterator);
}
/**
* Apply this method to both iterators and return normally only if both
* produce the same response.
*
* @return {@code true} if an exception was thrown by the iterators.
*
* @see Stimulus#executeAndCompare(ListIterator, Iterator)
*/
private <T extends Iterator<E>> boolean internalExecuteAndCompare(
T reference, T target, IteratorOperation method)
throws AssertionFailedError {
Object referenceReturnValue = null;
PermittedMetaException referenceException = null;
Object targetReturnValue = null;
RuntimeException targetException = null;
try {
targetReturnValue = method.execute(target);
} catch (RuntimeException e) {
targetException = e;
}
try {
if (method == NEXT_METHOD && targetException == null
&& knownOrder == KnownOrder.UNKNOWN_ORDER) {
/*
* We already know the iterator is an Iterator<E>, and now we know that
* we called next(), so the returned element must be of type E.
*/
@SuppressWarnings("unchecked")
E targetReturnValueFromNext = (E) targetReturnValue;
/*
* We have an Iterator<E> and want to cast it to
* MultiExceptionListIterator. Because we're inside an
* AbstractIteratorTester<E>, that's implicitly a cast to
* AbstractIteratorTester<E>.MultiExceptionListIterator. The runtime
* won't be able to verify the AbstractIteratorTester<E> part, so it's
* an unchecked cast. We know, however, that the only possible value for
* the type parameter is <E>, since otherwise the
* MultiExceptionListIterator wouldn't be an Iterator<E>. The cast is
* safe, even though javac can't tell.
*
* Sun bug 6665356 is an additional complication. Until OpenJDK 7, javac
* doesn't recognize this kind of cast as unchecked cast. Neither does
* Eclipse 3.4. Right now, this suppression is mostly unecessary.
*/
MultiExceptionListIterator multiExceptionListIterator =
(MultiExceptionListIterator) reference;
multiExceptionListIterator.promoteToNext(targetReturnValueFromNext);
}
referenceReturnValue = method.execute(reference);
} catch (PermittedMetaException e) {
referenceException = e;
} catch (UnknownElementException e) {
Helpers.fail(e, e.getMessage());
}
if (referenceException == null) {
if (targetException != null) {
Helpers.fail(targetException,
"Target threw exception when reference did not");
}
/*
* Reference iterator returned a value, so we should expect the
* same value from the target
*/
assertEquals(referenceReturnValue, targetReturnValue);
return false;
}
if (targetException == null) {
fail("Target failed to throw " + referenceException);
}
/*
* Reference iterator threw an exception, so we should expect an acceptable
* exception from the target.
*/
referenceException.assertPermitted(targetException);
return true;
}
private static final IteratorOperation REMOVE_METHOD =
new IteratorOperation() {
@Override
public Object execute(Iterator<?> iterator) {
iterator.remove();
return null;
}
};
private static final IteratorOperation NEXT_METHOD =
new IteratorOperation() {
@Override
public Object execute(Iterator<?> iterator) {
return iterator.next();
}
};
private static final IteratorOperation PREVIOUS_METHOD =
new IteratorOperation() {
@Override
public Object execute(Iterator<?> iterator) {
return ((ListIterator<?>) iterator).previous();
}
};
private final IteratorOperation newAddMethod() {
final Object toInsert = elementsToInsert.next();
return new IteratorOperation() {
@Override
public Object execute(Iterator<?> iterator) {
@SuppressWarnings("unchecked")
ListIterator<Object> rawIterator = (ListIterator<Object>) iterator;
rawIterator.add(toInsert);
return null;
}
};
}
private final IteratorOperation newSetMethod() {
final E toInsert = elementsToInsert.next();
return new IteratorOperation() {
@Override
public Object execute(Iterator<?> iterator) {
@SuppressWarnings("unchecked")
ListIterator<E> li = (ListIterator<E>) iterator;
li.set(toInsert);
return null;
}
};
}
abstract static class Stimulus<E, T extends Iterator<E>> {
private final String toString;
protected Stimulus(String toString) {
this.toString = toString;
}
/**
* Send this stimulus to both iterators and return normally only if both
* produce the same response.
*
* @return {@code true} if an exception was thrown by the iterators.
*/
abstract boolean executeAndCompare(ListIterator<E> reference, T target);
@Override public String toString() {
return toString;
}
}
Stimulus<E, Iterator<E>> hasNext = new Stimulus<E, Iterator<E>>("hasNext") {
@Override boolean
executeAndCompare(ListIterator<E> reference, Iterator<E> target) {
// return only if both are true or both are false
assertEquals(reference.hasNext(), target.hasNext());
return false;
}
};
Stimulus<E, Iterator<E>> next = new Stimulus<E, Iterator<E>>("next") {
@Override boolean
executeAndCompare(ListIterator<E> reference, Iterator<E> target) {
return internalExecuteAndCompare(reference, target, NEXT_METHOD);
}
};
Stimulus<E, Iterator<E>> remove = new Stimulus<E, Iterator<E>>("remove") {
@Override boolean
executeAndCompare(ListIterator<E> reference, Iterator<E> target) {
return internalExecuteAndCompare(reference, target, REMOVE_METHOD);
}
};
@SuppressWarnings("unchecked")
List<Stimulus<E, Iterator<E>>> iteratorStimuli() {
return Arrays.asList(hasNext, next, remove);
}
Stimulus<E, ListIterator<E>> hasPrevious =
new Stimulus<E, ListIterator<E>>("hasPrevious") {
@Override boolean executeAndCompare(
ListIterator<E> reference, ListIterator<E> target) {
// return only if both are true or both are false
assertEquals(reference.hasPrevious(), target.hasPrevious());
return false;
}
};
Stimulus<E, ListIterator<E>> nextIndex =
new Stimulus<E, ListIterator<E>>("nextIndex") {
@Override boolean executeAndCompare(
ListIterator<E> reference, ListIterator<E> target) {
assertEquals(reference.nextIndex(), target.nextIndex());
return false;
}
};
Stimulus<E, ListIterator<E>> previousIndex =
new Stimulus<E, ListIterator<E>>("previousIndex") {
@Override boolean executeAndCompare(
ListIterator<E> reference, ListIterator<E> target) {
assertEquals(reference.previousIndex(), target.previousIndex());
return false;
}
};
Stimulus<E, ListIterator<E>> previous =
new Stimulus<E, ListIterator<E>>("previous") {
@Override boolean executeAndCompare(
ListIterator<E> reference, ListIterator<E> target) {
return internalExecuteAndCompare(reference, target, PREVIOUS_METHOD);
}
};
Stimulus<E, ListIterator<E>> add = new Stimulus<E, ListIterator<E>>("add") {
@Override boolean executeAndCompare(
ListIterator<E> reference, ListIterator<E> target) {
return internalExecuteAndCompare(reference, target, newAddMethod());
}
};
Stimulus<E, ListIterator<E>> set = new Stimulus<E, ListIterator<E>>("set") {
@Override boolean executeAndCompare(
ListIterator<E> reference, ListIterator<E> target) {
return internalExecuteAndCompare(reference, target, newSetMethod());
}
};
@SuppressWarnings("unchecked")
List<Stimulus<E, ListIterator<E>>> listIteratorStimuli() {
return Arrays.asList(
hasPrevious, nextIndex, previousIndex, previous, add, set);
}
}