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