• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.testing;
18 
19 import static junit.framework.Assert.assertEquals;
20 import static junit.framework.Assert.fail;
21 
22 import com.google.common.annotations.GwtCompatible;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.ListIterator;
30 import java.util.NoSuchElementException;
31 import java.util.Set;
32 import java.util.Stack;
33 import junit.framework.AssertionFailedError;
34 import org.checkerframework.checker.nullness.qual.Nullable;
35 
36 /**
37  * Most of the logic for {@link IteratorTester} and {@link ListIteratorTester}.
38  *
39  * @param <E> the type of element returned by the iterator
40  * @param <I> the type of the iterator ({@link Iterator} or {@link ListIterator})
41  * @author Kevin Bourrillion
42  * @author Chris Povirk
43  */
44 @GwtCompatible
45 abstract class AbstractIteratorTester<E, I extends Iterator<E>> {
46   private Stimulus<E, ? super I>[] stimuli;
47   private final Iterator<E> elementsToInsert;
48   private final Set<IteratorFeature> features;
49   private final List<E> expectedElements;
50   private final int startIndex;
51   private final KnownOrder knownOrder;
52 
53   /**
54    * Meta-exception thrown by {@link AbstractIteratorTester.MultiExceptionListIterator} instead of
55    * throwing any particular exception type.
56    */
57   private abstract static class PermittedMetaException extends RuntimeException {
58     static final PermittedMetaException UOE_OR_ISE =
59         new PermittedMetaException("UnsupportedOperationException or IllegalStateException") {
60           @Override
61           boolean isPermitted(Exception exception) {
62             return exception instanceof UnsupportedOperationException
63                 || exception instanceof IllegalStateException;
64           }
65         };
66     static final PermittedMetaException UOE =
67         new PermittedMetaException("UnsupportedOperationException") {
68           @Override
69           boolean isPermitted(Exception exception) {
70             return exception instanceof UnsupportedOperationException;
71           }
72         };
73     static final PermittedMetaException ISE =
74         new PermittedMetaException("IllegalStateException") {
75           @Override
76           boolean isPermitted(Exception exception) {
77             return exception instanceof IllegalStateException;
78           }
79         };
80     static final PermittedMetaException NSEE =
81         new PermittedMetaException("NoSuchElementException") {
82           @Override
83           boolean isPermitted(Exception exception) {
84             return exception instanceof NoSuchElementException;
85           }
86         };
87 
PermittedMetaException(String message)88     private PermittedMetaException(String message) {
89       super(message);
90     }
91 
isPermitted(Exception exception)92     abstract boolean isPermitted(Exception exception);
93 
assertPermitted(Exception exception)94     void assertPermitted(Exception exception) {
95       if (!isPermitted(exception)) {
96         String message =
97             "Exception "
98                 + exception.getClass().getSimpleName()
99                 + " was thrown; expected "
100                 + getMessage();
101         Helpers.fail(exception, message);
102       }
103     }
104 
105     private static final long serialVersionUID = 0;
106   }
107 
108   private static final class UnknownElementException extends RuntimeException {
UnknownElementException(Collection<?> expected, Object actual)109     private UnknownElementException(Collection<?> expected, Object actual) {
110       super("Returned value '" + actual + "' not found. Remaining elements: " + expected);
111     }
112 
113     private static final long serialVersionUID = 0;
114   }
115 
116   /**
117    * Quasi-implementation of {@link ListIterator} that works from a list of elements and a set of
118    * features to support (from the enclosing {@link AbstractIteratorTester} instance). Instead of
119    * throwing exceptions like {@link NoSuchElementException} at the appropriate times, it throws
120    * {@link PermittedMetaException} instances, which wrap a set of all exceptions that the iterator
121    * could throw during the invocation of that method. This is necessary because, e.g., a call to
122    * {@code iterator().remove()} of an unmodifiable list could throw either {@link
123    * IllegalStateException} or {@link UnsupportedOperationException}. Note that iterator
124    * implementations should always throw one of the exceptions in a {@code PermittedExceptions}
125    * instance, since {@code PermittedExceptions} is thrown only when a method call is invalid.
126    *
127    * <p>This class is accessible but not supported in GWT as it references {@link
128    * PermittedMetaException}.
129    */
130   protected final class MultiExceptionListIterator implements ListIterator<E> {
131     // TODO: track seen elements when order isn't guaranteed
132     // TODO: verify contents afterward
133     // TODO: something shiny and new instead of Stack
134     // TODO: test whether null is supported (create a Feature)
135     /**
136      * The elements to be returned by future calls to {@code next()}, with the first at the top of
137      * the stack.
138      */
139     final Stack<E> nextElements = new Stack<E>();
140     /**
141      * The elements to be returned by future calls to {@code previous()}, with the first at the top
142      * of the stack.
143      */
144     final Stack<E> previousElements = new Stack<E>();
145     /**
146      * {@link #nextElements} if {@code next()} was called more recently then {@code previous},
147      * {@link #previousElements} if the reverse is true, or -- overriding both of these -- {@code
148      * null} if {@code remove()} or {@code add()} has been called more recently than either. We use
149      * this to determine which stack to pop from on a call to {@code remove()} (or to pop from and
150      * push to on a call to {@code set()}).
151      */
152     @Nullable Stack<E> stackWithLastReturnedElementAtTop = null;
153 
MultiExceptionListIterator(List<E> expectedElements)154     MultiExceptionListIterator(List<E> expectedElements) {
155       Helpers.addAll(nextElements, Helpers.reverse(expectedElements));
156       for (int i = 0; i < startIndex; i++) {
157         previousElements.push(nextElements.pop());
158       }
159     }
160 
161     @Override
add(E e)162     public void add(E e) {
163       if (!features.contains(IteratorFeature.SUPPORTS_ADD)) {
164         throw PermittedMetaException.UOE;
165       }
166 
167       previousElements.push(e);
168       stackWithLastReturnedElementAtTop = null;
169     }
170 
171     @Override
hasNext()172     public boolean hasNext() {
173       return !nextElements.isEmpty();
174     }
175 
176     @Override
hasPrevious()177     public boolean hasPrevious() {
178       return !previousElements.isEmpty();
179     }
180 
181     @Override
next()182     public E next() {
183       return transferElement(nextElements, previousElements);
184     }
185 
186     @Override
nextIndex()187     public int nextIndex() {
188       return previousElements.size();
189     }
190 
191     @Override
previous()192     public E previous() {
193       return transferElement(previousElements, nextElements);
194     }
195 
196     @Override
previousIndex()197     public int previousIndex() {
198       return nextIndex() - 1;
199     }
200 
201     @Override
remove()202     public void remove() {
203       throwIfInvalid(IteratorFeature.SUPPORTS_REMOVE);
204 
205       stackWithLastReturnedElementAtTop.pop();
206       stackWithLastReturnedElementAtTop = null;
207     }
208 
209     @Override
set(E e)210     public void set(E e) {
211       throwIfInvalid(IteratorFeature.SUPPORTS_SET);
212 
213       stackWithLastReturnedElementAtTop.pop();
214       stackWithLastReturnedElementAtTop.push(e);
215     }
216 
217     /**
218      * Moves the given element from its current position in {@link #nextElements} to the top of the
219      * stack so that it is returned by the next call to {@link Iterator#next()}. If the element is
220      * not in {@link #nextElements}, this method throws an {@link UnknownElementException}.
221      *
222      * <p>This method is used when testing iterators without a known ordering. We poll the target
223      * iterator's next element and pass it to the reference iterator through this method so it can
224      * return the same element. This enables the assertion to pass and the reference iterator to
225      * properly update its state.
226      */
promoteToNext(E e)227     void promoteToNext(E e) {
228       if (nextElements.remove(e)) {
229         nextElements.push(e);
230       } else {
231         throw new UnknownElementException(nextElements, e);
232       }
233     }
234 
transferElement(Stack<E> source, Stack<E> destination)235     private E transferElement(Stack<E> source, Stack<E> destination) {
236       if (source.isEmpty()) {
237         throw PermittedMetaException.NSEE;
238       }
239 
240       destination.push(source.pop());
241       stackWithLastReturnedElementAtTop = destination;
242       return destination.peek();
243     }
244 
throwIfInvalid(IteratorFeature methodFeature)245     private void throwIfInvalid(IteratorFeature methodFeature) {
246       if (!features.contains(methodFeature)) {
247         if (stackWithLastReturnedElementAtTop == null) {
248           throw PermittedMetaException.UOE_OR_ISE;
249         } else {
250           throw PermittedMetaException.UOE;
251         }
252       } else if (stackWithLastReturnedElementAtTop == null) {
253         throw PermittedMetaException.ISE;
254       }
255     }
256 
getElements()257     private List<E> getElements() {
258       List<E> elements = new ArrayList<E>();
259       Helpers.addAll(elements, previousElements);
260       Helpers.addAll(elements, Helpers.reverse(nextElements));
261       return elements;
262     }
263   }
264 
265   public enum KnownOrder {
266     KNOWN_ORDER,
267     UNKNOWN_ORDER
268   }
269 
270   @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)271   AbstractIteratorTester(
272       int steps,
273       Iterable<E> elementsToInsertIterable,
274       Iterable<? extends IteratorFeature> features,
275       Iterable<E> expectedElements,
276       KnownOrder knownOrder,
277       int startIndex) {
278     // periodically we should manually try (steps * 3 / 2) here; all tests but
279     // one should still pass (testVerifyGetsCalled()).
280     stimuli = new Stimulus[steps];
281     if (!elementsToInsertIterable.iterator().hasNext()) {
282       throw new IllegalArgumentException();
283     }
284     elementsToInsert = Helpers.cycle(elementsToInsertIterable);
285     this.features = Helpers.copyToSet(features);
286     this.expectedElements = Helpers.copyToList(expectedElements);
287     this.knownOrder = knownOrder;
288     this.startIndex = startIndex;
289   }
290 
291   /**
292    * I'd like to make this a parameter to the constructor, but I can't because the stimulus
293    * instances refer to {@code this}.
294    */
getStimulusValues()295   protected abstract Iterable<? extends Stimulus<E, ? super I>> getStimulusValues();
296 
297   /**
298    * Returns a new target iterator each time it's called. This is the iterator you are trying to
299    * test. This must return an Iterator that returns the expected elements passed to the constructor
300    * in the given order. Warning: it is not enough to simply pull multiple iterators from the same
301    * source Iterable, unless that Iterator is unmodifiable.
302    */
newTargetIterator()303   protected abstract I newTargetIterator();
304 
305   /**
306    * Override this to verify anything after running a list of Stimuli.
307    *
308    * <p>For example, verify that calls to remove() actually removed the correct elements.
309    *
310    * @param elements the expected elements passed to the constructor, as mutated by {@code
311    *     remove()}, {@code set()}, and {@code add()} calls
312    */
verify(List<E> elements)313   protected void verify(List<E> elements) {}
314 
315   /** Executes the test. */
316   @SuppressWarnings("CatchingUnchecked") // sneaky checked exception
test()317   public final void test() {
318     try {
319       recurse(0);
320     } catch (Exception e) { // sneaky checked exception
321       throw new RuntimeException(Arrays.toString(stimuli), e);
322     }
323   }
324 
testForEachRemaining()325   public void testForEachRemaining() {
326     for (int i = 0; i < expectedElements.size() - 1; i++) {
327       List<E> targetElements = new ArrayList<E>();
328       Iterator<E> iterator = newTargetIterator();
329       for (int j = 0; j < i; j++) {
330         targetElements.add(iterator.next());
331       }
332       iterator.forEachRemaining(targetElements::add);
333       if (knownOrder == KnownOrder.KNOWN_ORDER) {
334         assertEquals(expectedElements, targetElements);
335       } else {
336         Helpers.assertEqualIgnoringOrder(expectedElements, targetElements);
337       }
338     }
339   }
340 
recurse(int level)341   private void recurse(int level) {
342     // We're going to reuse the stimuli array 3^steps times by overwriting it
343     // in a recursive loop.  Sneaky.
344     if (level == stimuli.length) {
345       // We've filled the array.
346       compareResultsForThisListOfStimuli();
347     } else {
348       // Keep recursing to fill the array.
349       for (Stimulus<E, ? super I> stimulus : getStimulusValues()) {
350         stimuli[level] = stimulus;
351         recurse(level + 1);
352       }
353     }
354   }
355 
compareResultsForThisListOfStimuli()356   private void compareResultsForThisListOfStimuli() {
357     int removes = Collections.frequency(Arrays.asList(stimuli), remove);
358     if ((!features.contains(IteratorFeature.SUPPORTS_REMOVE) && removes > 1)
359         || (stimuli.length >= 5 && removes > 2)) {
360       // removes are the most expensive thing to test, since they often throw exceptions with stack
361       // traces, so we test them a bit less aggressively
362       return;
363     }
364 
365     MultiExceptionListIterator reference = new MultiExceptionListIterator(expectedElements);
366     I target = newTargetIterator();
367     for (int i = 0; i < stimuli.length; i++) {
368       try {
369         stimuli[i].executeAndCompare(reference, target);
370         verify(reference.getElements());
371       } catch (AssertionFailedError cause) {
372         Helpers.fail(cause, "failed with stimuli " + subListCopy(stimuli, i + 1));
373       }
374     }
375   }
376 
subListCopy(Object[] source, int size)377   private static List<Object> subListCopy(Object[] source, int size) {
378     final Object[] copy = new Object[size];
379     System.arraycopy(source, 0, copy, 0, size);
380     return Arrays.asList(copy);
381   }
382 
383   private interface IteratorOperation {
execute(Iterator<?> iterator)384     @Nullable Object execute(Iterator<?> iterator);
385   }
386 
387   /**
388    * Apply this method to both iterators and return normally only if both produce the same response.
389    *
390    * @see Stimulus#executeAndCompare(ListIterator, Iterator)
391    */
392   @SuppressWarnings("CatchingUnchecked") // sneaky checked exception
internalExecuteAndCompare( T reference, T target, IteratorOperation method)393   private <T extends Iterator<E>> void internalExecuteAndCompare(
394       T reference, T target, IteratorOperation method) {
395     Object referenceReturnValue = null;
396     PermittedMetaException referenceException = null;
397     Object targetReturnValue = null;
398     Exception targetException = null;
399 
400     try {
401       targetReturnValue = method.execute(target);
402     } catch (Exception e) { // sneaky checked exception
403       targetException = e;
404     }
405 
406     try {
407       if (method == NEXT_METHOD
408           && targetException == null
409           && knownOrder == KnownOrder.UNKNOWN_ORDER) {
410         /*
411          * We already know the iterator is an Iterator<E>, and now we know that
412          * we called next(), so the returned element must be of type E.
413          */
414         @SuppressWarnings("unchecked")
415         E targetReturnValueFromNext = (E) targetReturnValue;
416         /*
417          * We have an Iterator<E> and want to cast it to
418          * MultiExceptionListIterator. Because we're inside an
419          * AbstractIteratorTester<E>, that's implicitly a cast to
420          * AbstractIteratorTester<E>.MultiExceptionListIterator. The runtime
421          * won't be able to verify the AbstractIteratorTester<E> part, so it's
422          * an unchecked cast. We know, however, that the only possible value for
423          * the type parameter is <E>, since otherwise the
424          * MultiExceptionListIterator wouldn't be an Iterator<E>. The cast is
425          * safe, even though javac can't tell.
426          *
427          * Sun bug 6665356 is an additional complication. Until OpenJDK 7, javac
428          * doesn't recognize this kind of cast as unchecked cast. Neither does
429          * Eclipse 3.4. Right now, this suppression is mostly unnecessary.
430          */
431         MultiExceptionListIterator multiExceptionListIterator =
432             (MultiExceptionListIterator) reference;
433         multiExceptionListIterator.promoteToNext(targetReturnValueFromNext);
434       }
435 
436       referenceReturnValue = method.execute(reference);
437     } catch (PermittedMetaException e) {
438       referenceException = e;
439     } catch (UnknownElementException e) {
440       Helpers.fail(e, e.getMessage());
441     }
442 
443     if (referenceException == null) {
444       if (targetException != null) {
445         Helpers.fail(targetException, "Target threw exception when reference did not");
446       }
447 
448       /*
449        * Reference iterator returned a value, so we should expect the
450        * same value from the target
451        */
452       assertEquals(referenceReturnValue, targetReturnValue);
453 
454       return;
455     }
456 
457     if (targetException == null) {
458       fail("Target failed to throw " + referenceException);
459     }
460 
461     /*
462      * Reference iterator threw an exception, so we should expect an acceptable
463      * exception from the target.
464      */
465     referenceException.assertPermitted(targetException);
466   }
467 
468   private static final IteratorOperation REMOVE_METHOD =
469       new IteratorOperation() {
470         @Override
471         public @Nullable Object execute(Iterator<?> iterator) {
472           iterator.remove();
473           return null;
474         }
475       };
476 
477   private static final IteratorOperation NEXT_METHOD =
478       new IteratorOperation() {
479         @Override
480         public Object execute(Iterator<?> iterator) {
481           return iterator.next();
482         }
483       };
484 
485   private static final IteratorOperation PREVIOUS_METHOD =
486       new IteratorOperation() {
487         @Override
488         public Object execute(Iterator<?> iterator) {
489           return ((ListIterator<?>) iterator).previous();
490         }
491       };
492 
newAddMethod()493   private final IteratorOperation newAddMethod() {
494     final Object toInsert = elementsToInsert.next();
495     return new IteratorOperation() {
496       @Override
497       public @Nullable Object execute(Iterator<?> iterator) {
498         @SuppressWarnings("unchecked")
499         ListIterator<Object> rawIterator = (ListIterator<Object>) iterator;
500         rawIterator.add(toInsert);
501         return null;
502       }
503     };
504   }
505 
506   private final IteratorOperation newSetMethod() {
507     final E toInsert = elementsToInsert.next();
508     return new IteratorOperation() {
509       @Override
510       public @Nullable Object execute(Iterator<?> iterator) {
511         @SuppressWarnings("unchecked")
512         ListIterator<E> li = (ListIterator<E>) iterator;
513         li.set(toInsert);
514         return null;
515       }
516     };
517   }
518 
519   abstract static class Stimulus<E, T extends Iterator<E>> {
520     private final String toString;
521 
522     protected Stimulus(String toString) {
523       this.toString = toString;
524     }
525 
526     /**
527      * Send this stimulus to both iterators and return normally only if both produce the same
528      * response.
529      */
530     abstract void executeAndCompare(ListIterator<E> reference, T target);
531 
532     @Override
533     public String toString() {
534       return toString;
535     }
536   }
537 
538   Stimulus<E, Iterator<E>> hasNext =
539       new Stimulus<E, Iterator<E>>("hasNext") {
540         @Override
541         void executeAndCompare(ListIterator<E> reference, Iterator<E> target) {
542           assertEquals(reference.hasNext(), target.hasNext());
543         }
544       };
545   Stimulus<E, Iterator<E>> next =
546       new Stimulus<E, Iterator<E>>("next") {
547         @Override
548         void executeAndCompare(ListIterator<E> reference, Iterator<E> target) {
549           internalExecuteAndCompare(reference, target, NEXT_METHOD);
550         }
551       };
552   Stimulus<E, Iterator<E>> remove =
553       new Stimulus<E, Iterator<E>>("remove") {
554         @Override
555         void executeAndCompare(ListIterator<E> reference, Iterator<E> target) {
556           internalExecuteAndCompare(reference, target, REMOVE_METHOD);
557         }
558       };
559 
560   @SuppressWarnings("unchecked")
561   List<Stimulus<E, Iterator<E>>> iteratorStimuli() {
562     return Arrays.asList(hasNext, next, remove);
563   }
564 
565   Stimulus<E, ListIterator<E>> hasPrevious =
566       new Stimulus<E, ListIterator<E>>("hasPrevious") {
567         @Override
568         void executeAndCompare(ListIterator<E> reference, ListIterator<E> target) {
569           assertEquals(reference.hasPrevious(), target.hasPrevious());
570         }
571       };
572   Stimulus<E, ListIterator<E>> nextIndex =
573       new Stimulus<E, ListIterator<E>>("nextIndex") {
574         @Override
575         void executeAndCompare(ListIterator<E> reference, ListIterator<E> target) {
576           assertEquals(reference.nextIndex(), target.nextIndex());
577         }
578       };
579   Stimulus<E, ListIterator<E>> previousIndex =
580       new Stimulus<E, ListIterator<E>>("previousIndex") {
581         @Override
582         void executeAndCompare(ListIterator<E> reference, ListIterator<E> target) {
583           assertEquals(reference.previousIndex(), target.previousIndex());
584         }
585       };
586   Stimulus<E, ListIterator<E>> previous =
587       new Stimulus<E, ListIterator<E>>("previous") {
588         @Override
589         void executeAndCompare(ListIterator<E> reference, ListIterator<E> target) {
590           internalExecuteAndCompare(reference, target, PREVIOUS_METHOD);
591         }
592       };
593   Stimulus<E, ListIterator<E>> add =
594       new Stimulus<E, ListIterator<E>>("add") {
595         @Override
596         void executeAndCompare(ListIterator<E> reference, ListIterator<E> target) {
597           internalExecuteAndCompare(reference, target, newAddMethod());
598         }
599       };
600   Stimulus<E, ListIterator<E>> set =
601       new Stimulus<E, ListIterator<E>>("set") {
602         @Override
603         void executeAndCompare(ListIterator<E> reference, ListIterator<E> target) {
604           internalExecuteAndCompare(reference, target, newSetMethod());
605         }
606       };
607 
608   @SuppressWarnings("unchecked")
609   List<Stimulus<E, ListIterator<E>>> listIteratorStimuli() {
610     return Arrays.asList(hasPrevious, nextIndex, previousIndex, previous, add, set);
611   }
612 }
613