/* * 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; import static com.google.common.collect.Iterators.peekingIterator; import static com.google.common.collect.testing.IteratorFeature.MODIFIABLE; import static com.google.common.collect.testing.IteratorFeature.UNMODIFIABLE; import static java.util.Collections.emptyList; import com.google.common.annotations.GwtCompatible; import com.google.common.annotations.GwtIncompatible; import com.google.common.collect.testing.IteratorTester; import junit.framework.TestCase; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; /** * Unit test for {@link PeekingIterator}. * * @author Mick Killianey */ @SuppressWarnings("serial") // No serialization is used in this test @GwtCompatible(emulated = true) public class PeekingIteratorTest extends TestCase { /** * Version of {@link IteratorTester} that compares an iterator over * a given collection of elements (used as the reference iterator) * against a {@code PeekingIterator} that *wraps* such an iterator * (used as the target iterator). * * <p>This IteratorTester makes copies of the master so that it can * later verify that {@link PeekingIterator#remove()} removes the * same elements as the reference's iterator {@code #remove()}. */ private static class PeekingIteratorTester<T> extends IteratorTester<T> { private Iterable<T> master; private List<T> targetList; public PeekingIteratorTester(Collection<T> master) { super(master.size() + 3, MODIFIABLE, master, IteratorTester.KnownOrder.KNOWN_ORDER); this.master = master; } @Override protected Iterator<T> newTargetIterator() { // make copy from master to verify later targetList = Lists.newArrayList(master); Iterator<T> iterator = targetList.iterator(); return Iterators.peekingIterator(iterator); } @Override protected void verify(List<T> elements) { // verify same objects were removed from reference and target assertEquals(elements, targetList); } } private <T> void actsLikeIteratorHelper(final List<T> list) { // Check with modifiable copies of the list new PeekingIteratorTester<T>(list).test(); // Check with unmodifiable lists new IteratorTester<T>(list.size() * 2 + 2, UNMODIFIABLE, list, IteratorTester.KnownOrder.KNOWN_ORDER) { @Override protected Iterator<T> newTargetIterator() { Iterator<T> iterator = Collections.unmodifiableList(list).iterator(); return Iterators.peekingIterator(iterator); } }.test(); } public void testPeekingIteratorBehavesLikeIteratorOnEmptyIterable() { actsLikeIteratorHelper(Collections.emptyList()); } public void testPeekingIteratorBehavesLikeIteratorOnSingletonIterable() { actsLikeIteratorHelper(Collections.singletonList(new Object())); } // TODO(cpovirk): instead of skipping, use a smaller number of steps @GwtIncompatible("works but takes 5 minutes to run") public void testPeekingIteratorBehavesLikeIteratorOnThreeElementIterable() { actsLikeIteratorHelper(Lists.newArrayList("A", "B", "C")); } @GwtIncompatible("works but takes 5 minutes to run") public void testPeekingIteratorAcceptsNullElements() { actsLikeIteratorHelper(Lists.newArrayList(null, "A", null)); } public void testPeekOnEmptyList() { List<?> list = Collections.emptyList(); Iterator<?> iterator = list.iterator(); PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator); try { peekingIterator.peek(); fail("Should throw NoSuchElementException if nothing to peek()"); } catch (NoSuchElementException e) { /* expected */ } } public void testPeekDoesntChangeIteration() { List<?> list = Lists.newArrayList("A", "B", "C"); Iterator<?> iterator = list.iterator(); PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator); assertEquals("Should be able to peek() at first element", "A", peekingIterator.peek()); assertEquals("Should be able to peek() first element multiple times", "A", peekingIterator.peek()); assertEquals("next() should still return first element after peeking", "A", peekingIterator.next()); assertEquals("Should be able to peek() at middle element", "B", peekingIterator.peek()); assertEquals("Should be able to peek() middle element multiple times", "B", peekingIterator.peek()); assertEquals("next() should still return middle element after peeking", "B", peekingIterator.next()); assertEquals("Should be able to peek() at last element", "C", peekingIterator.peek()); assertEquals("Should be able to peek() last element multiple times", "C", peekingIterator.peek()); assertEquals("next() should still return last element after peeking", "C", peekingIterator.next()); try { peekingIterator.peek(); fail("Should throw exception if no next to peek()"); } catch (NoSuchElementException e) { /* expected */ } try { peekingIterator.peek(); fail("Should continue to throw exception if no next to peek()"); } catch (NoSuchElementException e) { /* expected */ } try { peekingIterator.next(); fail("next() should still throw exception after the end of iteration"); } catch (NoSuchElementException e) { /* expected */ } } public void testCantRemoveAfterPeek() { List<String> list = Lists.newArrayList("A", "B", "C"); Iterator<String> iterator = list.iterator(); PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator); assertEquals("A", peekingIterator.next()); assertEquals("B", peekingIterator.peek()); /* Should complain on attempt to remove() after peek(). */ try { peekingIterator.remove(); fail("remove() should throw IllegalStateException after a peek()"); } catch (IllegalStateException e) { /* expected */ } assertEquals("After remove() throws exception, peek should still be ok", "B", peekingIterator.peek()); /* Should recover to be able to remove() after next(). */ assertEquals("B", peekingIterator.next()); peekingIterator.remove(); assertEquals("Should have removed an element", 2, list.size()); assertFalse("Second element should be gone", list.contains("B")); } static class ThrowsAtEndException extends RuntimeException { /* nothing */ } /** * This Iterator claims to have more elements than the underlying * iterable, but when you try to fetch the extra elements, it throws * an unchecked exception. */ static class ThrowsAtEndIterator<E> implements Iterator<E> { Iterator<E> iterator; public ThrowsAtEndIterator(Iterable<E> iterable) { this.iterator = iterable.iterator(); } @Override public boolean hasNext() { return true; // pretend that you have more... } @Override public E next() { // ...but throw an unchecked exception when you ask for it. if (!iterator.hasNext()) { throw new ThrowsAtEndException(); } return iterator.next(); } @Override public void remove() { iterator.remove(); } } public void testPeekingIteratorDoesntAdvancePrematurely() throws Exception { /* * This test will catch problems where the underlying iterator * throws a RuntimeException when retrieving the nth element. * * If the PeekingIterator is caching elements too aggressively, * it may throw the exception on the (n-1)th element (oops!). */ /* Checks the case where the first element throws an exception. */ List<Integer> list = emptyList(); Iterator<Integer> iterator = peekingIterator(new ThrowsAtEndIterator<Integer>(list)); assertNextThrows(iterator); /* Checks the case where a later element throws an exception. */ list = Lists.newArrayList(1, 2); iterator = peekingIterator(new ThrowsAtEndIterator<Integer>(list)); assertTrue(iterator.hasNext()); iterator.next(); assertTrue(iterator.hasNext()); iterator.next(); assertNextThrows(iterator); } private void assertNextThrows(Iterator<?> iterator) { try { iterator.next(); fail(); } catch (ThrowsAtEndException expected) { } } }