This is an automated email from the ASF dual-hosted git repository. kturner pushed a commit to branch 2.1 in repository https://gitbox.apache.org/repos/asf/accumulo.git
The following commit(s) were added to refs/heads/2.1 by this push: new df8c5cb51f fixes bugs in peeking iterator (#5435) df8c5cb51f is described below commit df8c5cb51fe76419cfdc54f4da2f27657f53c23d Author: Keith Turner <ktur...@apache.org> AuthorDate: Fri Mar 28 19:16:04 2025 -0400 fixes bugs in peeking iterator (#5435) PeekingIterator was not handling null properly and was not following the iterator contract and throwing NoSuchElementException when it should. The null bug was if the source iterator returned null for next() this would cause peeking iterator to falsely claim there was no next. When the iterator did not have a next it would return null for next() instead of throwing an exception. Fixed both bugs and added unit test. Looked at the test coverage of the class and added a few other test that cover more then these bugs to get 100% coverage. --- .../apache/accumulo/core/util/PeekingIterator.java | 20 +++--- .../accumulo/core/util/PeekingIteratorTest.java | 78 +++++++++++++++++++++- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/core/util/PeekingIterator.java b/core/src/main/java/org/apache/accumulo/core/util/PeekingIterator.java index c7b85ec159..37d7e55277 100644 --- a/core/src/main/java/org/apache/accumulo/core/util/PeekingIterator.java +++ b/core/src/main/java/org/apache/accumulo/core/util/PeekingIterator.java @@ -19,6 +19,7 @@ package org.apache.accumulo.core.util; import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.function.Predicate; import com.google.common.base.Preconditions; @@ -28,15 +29,10 @@ public class PeekingIterator<E> implements Iterator<E> { boolean isInitialized; Iterator<E> source; E top; + boolean hasNext; public PeekingIterator(Iterator<E> source) { - this.source = source; - if (source.hasNext()) { - top = source.next(); - } else { - top = null; - } - isInitialized = true; + initialize(source); } /** @@ -54,8 +50,10 @@ public class PeekingIterator<E> implements Iterator<E> { this.source = source; if (source.hasNext()) { top = source.next(); + hasNext = true; } else { top = null; + hasNext = false; } isInitialized = true; return this; @@ -73,11 +71,17 @@ public class PeekingIterator<E> implements Iterator<E> { if (!isInitialized) { throw new IllegalStateException("Iterator has not yet been initialized"); } + if (!hasNext) { + throw new NoSuchElementException(); + } + E lastPeeked = top; if (source.hasNext()) { top = source.next(); + hasNext = true; } else { top = null; + hasNext = false; } return lastPeeked; } @@ -92,7 +96,7 @@ public class PeekingIterator<E> implements Iterator<E> { if (!isInitialized) { throw new IllegalStateException("Iterator has not yet been initialized"); } - return top != null; + return hasNext; } /** diff --git a/core/src/test/java/org/apache/accumulo/core/util/PeekingIteratorTest.java b/core/src/test/java/org/apache/accumulo/core/util/PeekingIteratorTest.java index 27bf76a725..ce76c91b0d 100644 --- a/core/src/test/java/org/apache/accumulo/core/util/PeekingIteratorTest.java +++ b/core/src/test/java/org/apache/accumulo/core/util/PeekingIteratorTest.java @@ -24,13 +24,79 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; public class PeekingIteratorTest { + @Test + public void testEmpty() { + var iter = new PeekingIterator<>(Collections.emptyIterator()); + assertFalse(iter.hasNext()); + assertNull(iter.peek()); + assertThrows(NoSuchElementException.class, iter::next); + } + + @Test + public void testNotInitialized() { + var iter = new PeekingIterator<>(); + assertThrows(IllegalStateException.class, iter::hasNext); + assertThrows(IllegalStateException.class, iter::next); + assertThrows(IllegalStateException.class, iter::peek); + } + + @Test + public void testNull() { + List<String> list = new ArrayList<>(); + + list.add("a"); + list.add(null); + list.add("b"); + + var iter = new PeekingIterator<>(list.iterator()); + + assertTrue(iter.hasNext()); + assertEquals("a", iter.peek()); + assertEquals("a", iter.next()); + assertThrows(UnsupportedOperationException.class, iter::remove); + assertTrue(iter.hasNext()); + assertNull(iter.peek()); + assertNull(iter.next()); + assertTrue(iter.hasNext()); + assertEquals("b", iter.peek()); + assertEquals("b", iter.next()); + assertFalse(iter.hasNext()); + assertThrows(NoSuchElementException.class, iter::next); + + iter = new PeekingIterator<>(list.iterator()); + iter.findWithin(Objects::isNull, 3); + assertTrue(iter.hasNext()); + assertNull(iter.peek()); + assertNull(iter.next()); + assertTrue(iter.hasNext()); + assertEquals("b", iter.peek()); + assertEquals("b", iter.next()); + assertFalse(iter.hasNext()); + assertThrows(NoSuchElementException.class, iter::next); + + iter = new PeekingIterator<>(list.iterator()); + iter.findWithin(e -> Objects.equals(e, "b"), 3); + assertTrue(iter.hasNext()); + assertEquals("b", iter.peek()); + assertEquals("b", iter.next()); + assertFalse(iter.hasNext()); + assertThrows(NoSuchElementException.class, iter::next); + + assertEquals(3, list.size()); + } + @Test public void testPeek() { Iterator<Integer> ints = IntStream.range(1, 11).iterator(); @@ -49,7 +115,7 @@ public class PeekingIteratorTest { } assertFalse(peek.hasNext()); - assertNull(peek.next()); + assertThrows(NoSuchElementException.class, peek::next); } @Test @@ -81,7 +147,15 @@ public class PeekingIteratorTest { // Try to advance past the end assertFalse(peek.findWithin((x) -> x != null && x == 7, 3)); assertFalse(peek.hasNext()); - assertNull(peek.next()); + assertNull(peek.peek()); + assertThrows(NoSuchElementException.class, peek::next); + + // test finding that exhaust iterator w/o finding anything + var peek2 = new PeekingIterator<>(ints); + peek2.findWithin(x -> Objects.equals(x, 100), 50); + assertFalse(peek2.hasNext()); + assertNull(peek2.peek()); + assertThrows(NoSuchElementException.class, peek2::next); }