1 /*
2  * Copyright (C) 2008 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.collect;
18 
19 import static com.google.common.collect.Iterators.peekingIterator;
20 import static com.google.common.collect.testing.IteratorFeature.MODIFIABLE;
21 import static com.google.common.collect.testing.IteratorFeature.UNMODIFIABLE;
22 import static java.util.Collections.emptyList;
23 
24 import com.google.common.annotations.GwtCompatible;
25 import com.google.common.annotations.GwtIncompatible;
26 import com.google.common.collect.testing.IteratorTester;
27 
28 import junit.framework.TestCase;
29 
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.NoSuchElementException;
35 
36 /**
37   * Unit test for {@link PeekingIterator}.
38   *
39   * @author Mick Killianey
40   */
41 @SuppressWarnings("serial") // No serialization is used in this test
42 @GwtCompatible(emulated = true)
43 public class PeekingIteratorTest extends TestCase {
44 
45   /**
46    * Version of {@link IteratorTester} that compares an iterator over
47    * a given collection of elements (used as the reference iterator)
48    * against a {@code PeekingIterator} that *wraps* such an iterator
49    * (used as the target iterator).
50    *
51    * <p>This IteratorTester makes copies of the master so that it can
52    * later verify that {@link PeekingIterator#remove()} removes the
53    * same elements as the reference's iterator {@code #remove()}.
54    */
55   private static class PeekingIteratorTester<T> extends IteratorTester<T> {
56     private Iterable<T> master;
57     private List<T> targetList;
58 
PeekingIteratorTester(Collection<T> master)59     public PeekingIteratorTester(Collection<T> master) {
60       super(master.size() + 3, MODIFIABLE, master,
61           IteratorTester.KnownOrder.KNOWN_ORDER);
62       this.master = master;
63     }
newTargetIterator()64     @Override protected Iterator<T> newTargetIterator() {
65       // make copy from master to verify later
66       targetList = Lists.newArrayList(master);
67       Iterator<T> iterator = targetList.iterator();
68       return Iterators.peekingIterator(iterator);
69     }
verify(List<T> elements)70     @Override protected void verify(List<T> elements) {
71       // verify same objects were removed from reference and target
72       assertEquals(elements, targetList);
73     }
74   }
75 
actsLikeIteratorHelper(final List<T> list)76   private <T> void actsLikeIteratorHelper(final List<T> list) {
77     // Check with modifiable copies of the list
78     new PeekingIteratorTester<T>(list).test();
79 
80     // Check with unmodifiable lists
81     new IteratorTester<T>(list.size() * 2 + 2, UNMODIFIABLE, list,
82         IteratorTester.KnownOrder.KNOWN_ORDER) {
83       @Override protected Iterator<T> newTargetIterator() {
84         Iterator<T> iterator = Collections.unmodifiableList(list).iterator();
85         return Iterators.peekingIterator(iterator);
86       }
87     }.test();
88   }
89 
testPeekingIteratorBehavesLikeIteratorOnEmptyIterable()90   public void testPeekingIteratorBehavesLikeIteratorOnEmptyIterable() {
91     actsLikeIteratorHelper(Collections.emptyList());
92   }
93 
testPeekingIteratorBehavesLikeIteratorOnSingletonIterable()94   public void testPeekingIteratorBehavesLikeIteratorOnSingletonIterable() {
95     actsLikeIteratorHelper(Collections.singletonList(new Object()));
96   }
97 
98   // TODO(cpovirk): instead of skipping, use a smaller number of steps
99   @GwtIncompatible("works but takes 5 minutes to run")
testPeekingIteratorBehavesLikeIteratorOnThreeElementIterable()100   public void testPeekingIteratorBehavesLikeIteratorOnThreeElementIterable() {
101     actsLikeIteratorHelper(Lists.newArrayList("A", "B", "C"));
102   }
103 
104   @GwtIncompatible("works but takes 5 minutes to run")
testPeekingIteratorAcceptsNullElements()105   public void testPeekingIteratorAcceptsNullElements() {
106     actsLikeIteratorHelper(Lists.newArrayList(null, "A", null));
107   }
108 
testPeekOnEmptyList()109   public void testPeekOnEmptyList() {
110     List<?> list = Collections.emptyList();
111     Iterator<?> iterator = list.iterator();
112     PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator);
113 
114     try {
115       peekingIterator.peek();
116       fail("Should throw NoSuchElementException if nothing to peek()");
117     } catch (NoSuchElementException e) { /* expected */ }
118   }
119 
testPeekDoesntChangeIteration()120   public void testPeekDoesntChangeIteration() {
121     List<?> list = Lists.newArrayList("A", "B", "C");
122     Iterator<?> iterator = list.iterator();
123     PeekingIterator<?> peekingIterator =
124         Iterators.peekingIterator(iterator);
125 
126     assertEquals("Should be able to peek() at first element",
127         "A", peekingIterator.peek());
128     assertEquals("Should be able to peek() first element multiple times",
129         "A", peekingIterator.peek());
130     assertEquals("next() should still return first element after peeking",
131         "A", peekingIterator.next());
132 
133     assertEquals("Should be able to peek() at middle element",
134         "B", peekingIterator.peek());
135     assertEquals("Should be able to peek() middle element multiple times",
136         "B", peekingIterator.peek());
137     assertEquals("next() should still return middle element after peeking",
138         "B", peekingIterator.next());
139 
140     assertEquals("Should be able to peek() at last element",
141         "C", peekingIterator.peek());
142     assertEquals("Should be able to peek() last element multiple times",
143         "C", peekingIterator.peek());
144     assertEquals("next() should still return last element after peeking",
145         "C", peekingIterator.next());
146 
147     try {
148       peekingIterator.peek();
149       fail("Should throw exception if no next to peek()");
150     } catch (NoSuchElementException e) { /* expected */ }
151     try {
152       peekingIterator.peek();
153       fail("Should continue to throw exception if no next to peek()");
154     } catch (NoSuchElementException e) { /* expected */ }
155     try {
156       peekingIterator.next();
157       fail("next() should still throw exception after the end of iteration");
158     } catch (NoSuchElementException e) { /* expected */ }
159   }
160 
testCantRemoveAfterPeek()161   public void testCantRemoveAfterPeek() {
162     List<String> list = Lists.newArrayList("A", "B", "C");
163     Iterator<String> iterator = list.iterator();
164     PeekingIterator<?> peekingIterator = Iterators.peekingIterator(iterator);
165 
166     assertEquals("A", peekingIterator.next());
167     assertEquals("B", peekingIterator.peek());
168 
169     /* Should complain on attempt to remove() after peek(). */
170     try {
171       peekingIterator.remove();
172       fail("remove() should throw IllegalStateException after a peek()");
173     } catch (IllegalStateException e) { /* expected */ }
174 
175     assertEquals("After remove() throws exception, peek should still be ok",
176         "B", peekingIterator.peek());
177 
178     /* Should recover to be able to remove() after next(). */
179     assertEquals("B", peekingIterator.next());
180     peekingIterator.remove();
181     assertEquals("Should have removed an element", 2, list.size());
182     assertFalse("Second element should be gone", list.contains("B"));
183   }
184 
185   static class ThrowsAtEndException extends RuntimeException { /* nothing */ }
186 
187   /**
188     * This Iterator claims to have more elements than the underlying
189     * iterable, but when you try to fetch the extra elements, it throws
190     * an unchecked exception.
191     */
192   static class ThrowsAtEndIterator<E> implements Iterator<E> {
193     Iterator<E> iterator;
ThrowsAtEndIterator(Iterable<E> iterable)194     public ThrowsAtEndIterator(Iterable<E> iterable) {
195       this.iterator = iterable.iterator();
196     }
197     @Override
hasNext()198     public boolean hasNext() {
199       return true;  // pretend that you have more...
200     }
201     @Override
next()202     public E next() {
203       // ...but throw an unchecked exception when you ask for it.
204       if (!iterator.hasNext()) {
205         throw new ThrowsAtEndException();
206       }
207       return iterator.next();
208     }
209     @Override
remove()210     public void remove() {
211       iterator.remove();
212     }
213   }
214 
testPeekingIteratorDoesntAdvancePrematurely()215   public void testPeekingIteratorDoesntAdvancePrematurely() throws Exception {
216     /*
217      * This test will catch problems where the underlying iterator
218      * throws a RuntimeException when retrieving the nth element.
219      *
220      * If the PeekingIterator is caching elements too aggressively,
221      * it may throw the exception on the (n-1)th element (oops!).
222      */
223 
224     /* Checks the case where the first element throws an exception. */
225 
226     List<Integer> list = emptyList();
227     Iterator<Integer> iterator =
228         peekingIterator(new ThrowsAtEndIterator<Integer>(list));
229     assertNextThrows(iterator);
230 
231     /* Checks the case where a later element throws an exception. */
232 
233     list = Lists.newArrayList(1, 2);
234     iterator = peekingIterator(new ThrowsAtEndIterator<Integer>(list));
235     assertTrue(iterator.hasNext());
236     iterator.next();
237     assertTrue(iterator.hasNext());
238     iterator.next();
239     assertNextThrows(iterator);
240   }
241 
assertNextThrows(Iterator<?> iterator)242   private void assertNextThrows(Iterator<?> iterator) {
243     try {
244       iterator.next();
245       fail();
246     } catch (ThrowsAtEndException expected) {
247     }
248   }
249 }
250