Skip to content

Commit 026bf61

Browse files
add iterator
1 parent 6acd9c9 commit 026bf61

2 files changed

Lines changed: 152 additions & 1 deletion

File tree

src/main/java/com/testingbot/testingbotrest/TestingbotREST.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
import java.math.BigInteger;
2626
import java.security.MessageDigest;
2727
import java.security.NoSuchAlgorithmException;
28+
import java.util.Collections;
2829
import java.util.HashMap;
30+
import java.util.Iterator;
31+
import java.util.NoSuchElementException;
2932
import java.util.concurrent.TimeUnit;
3033

3134
import org.apache.http.HttpEntity;
@@ -420,6 +423,79 @@ public TestingbotTestCollection getTests(int offset, int count, Map<String, Stri
420423
return this.apiGet(url.toString(), new TypeToken<TestingbotTestCollection>(){}.getType());
421424
}
422425

426+
/**
427+
* Lazily iterates over all tests, paging through the API behind the scenes.
428+
* Iteration stops when the API returns a page smaller than {@code pageSize}.
429+
*
430+
* <p>Each call to {@link Iterable#iterator()} starts a fresh walk from offset 0.
431+
*
432+
* @param pageSize the number of tests to fetch per request (must be {@code > 0})
433+
* @return an {@link Iterable} that walks every test in the account
434+
*/
435+
public Iterable<TestingbotTest> iterateTests(int pageSize) {
436+
return iterateTests(pageSize, null);
437+
}
438+
439+
/**
440+
* Lazily iterates over all tests matching the given filters, paging behind the scenes.
441+
*
442+
* @param pageSize the number of tests to fetch per request (must be {@code > 0})
443+
* @param filters optional server-side filters (may be null)
444+
* @return an {@link Iterable} that walks every matching test
445+
*/
446+
public Iterable<TestingbotTest> iterateTests(final int pageSize, final Map<String, String> filters) {
447+
if (pageSize <= 0) {
448+
throw new IllegalArgumentException("pageSize must be > 0");
449+
}
450+
return new Iterable<TestingbotTest>() {
451+
@Override
452+
public Iterator<TestingbotTest> iterator() {
453+
return new Iterator<TestingbotTest>() {
454+
private int offset = 0;
455+
private Iterator<TestingbotTest> page = Collections.<TestingbotTest>emptyList().iterator();
456+
private boolean exhausted = false;
457+
458+
private void advanceIfNeeded() {
459+
if (exhausted || page.hasNext()) {
460+
return;
461+
}
462+
TestingbotTestCollection collection = filters == null
463+
? getTests(offset, pageSize)
464+
: getTests(offset, pageSize, filters);
465+
List<TestingbotTest> items = collection != null && collection.getData() != null
466+
? collection.getData()
467+
: Collections.<TestingbotTest>emptyList();
468+
offset += items.size();
469+
page = items.iterator();
470+
if (items.size() < pageSize) {
471+
exhausted = true;
472+
}
473+
}
474+
475+
@Override
476+
public boolean hasNext() {
477+
advanceIfNeeded();
478+
return page.hasNext();
479+
}
480+
481+
@Override
482+
public TestingbotTest next() {
483+
advanceIfNeeded();
484+
if (!page.hasNext()) {
485+
throw new NoSuchElementException();
486+
}
487+
return page.next();
488+
}
489+
490+
@Override
491+
public void remove() {
492+
throw new UnsupportedOperationException();
493+
}
494+
};
495+
}
496+
};
497+
}
498+
423499
/**
424500
* Creates a new test on TestingBot.
425501
* See https://testingbot.com/support/api

src/test/java/com/testingbot/testingbotrest/TestingbotRestMockTest.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
2222
import java.util.HashMap;
23+
import java.util.Iterator;
2324
import java.util.LinkedHashMap;
2425
import java.util.List;
2526
import java.util.Map;
27+
import java.util.NoSuchElementException;
2628

2729
import static org.junit.Assert.*;
2830

@@ -47,6 +49,8 @@ public class TestingbotRestMockTest {
4749
// configurable response
4850
private volatile int responseStatus = 200;
4951
private volatile String responseBody = "{\"success\":true}";
52+
private final java.util.concurrent.ConcurrentLinkedDeque<String> responseBodies = new java.util.concurrent.ConcurrentLinkedDeque<>();
53+
private final java.util.List<String> requestUrls = java.util.Collections.synchronizedList(new ArrayList<String>());
5054

5155
@Before
5256
public void setUp() throws IOException {
@@ -60,8 +64,13 @@ public void handle(HttpExchange ex) throws IOException {
6064
lastAuth = ex.getRequestHeaders().getFirst("Authorization");
6165
lastUserAgent = ex.getRequestHeaders().getFirst("User-Agent");
6266
lastBody = read(ex.getRequestBody());
67+
requestUrls.add(lastPath + (lastQuery != null ? "?" + lastQuery : ""));
6368

64-
byte[] out = responseBody == null ? new byte[0] : responseBody.getBytes(StandardCharsets.UTF_8);
69+
String body = responseBodies.pollFirst();
70+
if (body == null) {
71+
body = responseBody;
72+
}
73+
byte[] out = body == null ? new byte[0] : body.getBytes(StandardCharsets.UTF_8);
6574
ex.sendResponseHeaders(responseStatus, out.length == 0 ? -1 : out.length);
6675
if (out.length > 0) {
6776
OutputStream os = ex.getResponseBody();
@@ -716,4 +725,70 @@ public void triggerLabSuite() {
716725
api.triggerLabSuite(4);
717726
assertRequest("POST", "/v1/labsuites/4/trigger");
718727
}
728+
729+
// ------------------------------------------------------------------ pagination iterator
730+
731+
private static String pageJson(int from, int count) {
732+
StringBuilder sb = new StringBuilder("{\"data\":[");
733+
for (int i = 0; i < count; i++) {
734+
if (i > 0) {
735+
sb.append(',');
736+
}
737+
sb.append("{\"session_id\":\"s").append(from + i).append("\"}");
738+
}
739+
sb.append("],\"meta\":{}}");
740+
return sb.toString();
741+
}
742+
743+
@Test
744+
public void iterateTestsWalksAllPages() {
745+
// 3 pages of 10, 10, 5 (last short page signals end-of-stream)
746+
responseBodies.add(pageJson(0, 10));
747+
responseBodies.add(pageJson(10, 10));
748+
responseBodies.add(pageJson(20, 5));
749+
750+
int seen = 0;
751+
for (TestingbotTest t : api.iterateTests(10)) {
752+
Assert.assertEquals("s" + seen, t.getSessionId());
753+
seen++;
754+
}
755+
Assert.assertEquals(25, seen);
756+
Assert.assertEquals(3, requestUrls.size());
757+
assertTrue(requestUrls.get(0).contains("offset=0"));
758+
assertTrue(requestUrls.get(1).contains("offset=10"));
759+
assertTrue(requestUrls.get(2).contains("offset=20"));
760+
}
761+
762+
@Test
763+
public void iterateTestsStopsImmediatelyOnEmptyPage() {
764+
responseBodies.add("{\"data\":[],\"meta\":{}}");
765+
Iterator<TestingbotTest> it = api.iterateTests(10).iterator();
766+
assertFalse(it.hasNext());
767+
Assert.assertEquals(1, requestUrls.size());
768+
try {
769+
it.next();
770+
Assert.fail("expected NoSuchElementException");
771+
} catch (NoSuchElementException expected) {
772+
}
773+
}
774+
775+
@Test
776+
public void iterateTestsForwardsFilters() {
777+
responseBodies.add(pageJson(0, 1)); // short page → stops after one request
778+
Map<String, String> filters = new HashMap<>();
779+
filters.put("group", "smoke");
780+
int seen = 0;
781+
for (TestingbotTest ignored : api.iterateTests(10, filters)) {
782+
seen++;
783+
}
784+
Assert.assertEquals(1, seen);
785+
Assert.assertEquals(1, requestUrls.size());
786+
assertTrue(requestUrls.get(0).contains("group=smoke"));
787+
assertTrue(requestUrls.get(0).contains("offset=0"));
788+
}
789+
790+
@Test(expected = IllegalArgumentException.class)
791+
public void iterateTestsRejectsNonPositivePageSize() {
792+
api.iterateTests(0);
793+
}
719794
}

0 commit comments

Comments
 (0)