Bug 1291821 - Get tests to work after sync changes r=rnewman
authorGrisha Kruglov <gkruglov@mozilla.com>
Tue, 11 Oct 2016 20:02:02 -0700
changeset 344871 09ae692fe42cb59e7c62276d681666f733c96846
parent 344870 c027b2bb07344e664ba8c9f77e11740a246bebe6
child 344872 c1069ad96647a8d0117ec976d70c6bf3523c48aa
push id37970
push usergkruglov@mozilla.com
push dateSat, 25 Feb 2017 01:09:28 +0000
treeherderautoland@bd232d46a396 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs1291821
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1291821 - Get tests to work after sync changes r=rnewman MozReview-Commit-ID: 3djnmEmzndU
mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
+import android.os.SystemClock;
+
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 import junit.framework.AssertionFailedError;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,31 +47,33 @@ import org.simpleframework.http.Response
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(TestRunner.class)
 public class TestGlobalSession {
   private int          TEST_PORT                = HTTPServerTestHelper.getTestPort();
   private final String TEST_CLUSTER_URL         = "http://localhost:" + TEST_PORT;
   private final String TEST_USERNAME            = "johndoe";
   private final String TEST_PASSWORD            = "password";
   private final String TEST_SYNC_KEY            = "abcdeabcdeabcdeabcdeabcdea";
   private final long   TEST_BACKOFF_IN_SECONDS  = 2401;
+  private final long   SYNC_DEADLINE            = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30);
 
   public static WaitHelper getTestWaiter() {
     return WaitHelper.getTestWaiter();
   }
 
   @Test
   public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, NoSuchStageException {
 
@@ -142,17 +146,17 @@ public class TestGlobalSession {
       final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
       SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
       final GlobalSession session = new MockGlobalSession(config, callback);
 
       getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
         @Override
         public void run() {
           try {
-            session.start();
+            session.start(SYNC_DEADLINE);
           } catch (Exception e) {
             final AssertionFailedError error = new AssertionFailedError();
             error.initCause(e);
             getTestWaiter().performNotify(error);
           }
         }
       }));
 
@@ -190,17 +194,17 @@ public class TestGlobalSession {
       SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
       final GlobalSession session = new MockGlobalSession(config, callback)
                                         .withStage(Stage.fetchInfoCollections, stage);
 
       getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
         @Override
         public void run() {
           try {
-            session.start();
+            session.start(SYNC_DEADLINE);
           } catch (Exception e) {
             final AssertionFailedError error = new AssertionFailedError();
             error.initCause(e);
             getTestWaiter().performNotify(error);
           }
         }
       }));
 
@@ -270,17 +274,17 @@ public class TestGlobalSession {
     final GlobalSession session = new MockGlobalSession(config, callback)
                                       .withStage(Stage.syncBookmarks, stage);
 
     data.startHTTPServer(server);
     WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
       @Override
       public void run() {
         try {
-          session.start();
+          session.start(SYNC_DEADLINE);
         } catch (Exception e) {
           final AssertionFailedError error = new AssertionFailedError();
           error.initCause(e);
           WaitHelper.getTestWaiter().performNotify(error);
         }
       }
     }));
     data.stopHTTPServer();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
@@ -1,37 +1,41 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
+import android.os.SystemClock;
+
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(TestRunner.class)
 public class TestServer11Repository {
 
   private static final String COLLECTION = "bookmarks";
-  private static final String COLLECTION_URL = "http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage";
+  private static final String COLLECTION_URL = "http://foo.com/1.5/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage";
+  private static final long SYNC_DEADLINE = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30);
 
   protected final InfoCollections infoCollections = new InfoCollections();
   protected final InfoConfiguration infoConfiguration = new InfoConfiguration();
 
   public static void assertQueryEquals(String expected, URI u) {
     Assert.assertEquals(expected, u.getRawQuery());
   }
 
   @Test
   public void testCollectionURI() throws URISyntaxException {
-    Server11Repository noTrailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections, infoConfiguration);
-    Server11Repository trailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL + "/", null, infoCollections, infoConfiguration);
-    Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", noTrailingSlash.collectionURI().toASCIIString());
-    Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", trailingSlash.collectionURI().toASCIIString());
+    Server11Repository noTrailingSlash = new Server11Repository(COLLECTION, SYNC_DEADLINE, COLLECTION_URL, null, infoCollections, infoConfiguration);
+    Server11Repository trailingSlash = new Server11Repository(COLLECTION, SYNC_DEADLINE, COLLECTION_URL + "/", null, infoCollections, infoConfiguration);
+    Assert.assertEquals("http://foo.com/1.5/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", noTrailingSlash.collectionURI().toASCIIString());
+    Assert.assertEquals("http://foo.com/1.5/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", trailingSlash.collectionURI().toASCIIString());
   }
 }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
@@ -1,47 +1,44 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
+import android.os.SystemClock;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
 import org.mozilla.android.sync.test.helpers.BaseTestStorageRequestDelegate;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
-import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.InfoConfiguration;
-import org.mozilla.gecko.sync.JSONRecordFetcher;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.repositories.FetchFailedException;
-import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 import org.mozilla.gecko.sync.repositories.StoreFailedException;
-import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory;
-import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository;
 import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.simpleframework.http.ContentType;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.io.IOException;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestServer11RepositorySession {
 
@@ -58,36 +55,31 @@ public class TestServer11RepositorySessi
       System.out.println("Content-Type:" + contentType);
       super.handle(request, response, 200, "{success:[]}");
     }
   }
 
   private static final int    TEST_PORT   = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER = "http://localhost:" + TEST_PORT + "/";
   static final String LOCAL_BASE_URL      = TEST_SERVER + "1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/";
-  static final String LOCAL_INFO_BASE_URL = LOCAL_BASE_URL + "info/";
-  static final String LOCAL_COUNTS_URL    = LOCAL_INFO_BASE_URL + "collection_counts";
 
   // Corresponds to rnewman+atest1@mozilla.com, local.
   static final String TEST_USERNAME          = "n6ec3u5bee3tixzp2asys7bs6fve4jfw";
   static final String TEST_PASSWORD          = "passowrd";
   static final String SYNC_KEY          = "eh7ppnb82iwr5kt3z3uyi5vr44";
 
   public final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
   protected final InfoCollections infoCollections = new InfoCollections() {
     @Override
     public Long getTimestamp(String collection) {
       return 0L;
     }
   };
   protected final InfoConfiguration infoConfiguration = new InfoConfiguration();
 
-  // Few-second timeout so that our longer operations don't time out and cause spurious error-handling results.
-  private static final int SHORT_TIMEOUT = 10000;
-
   public AuthHeaderProvider getAuthHeaderProvider() {
     return new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
   }
 
   private HTTPServerTestHelper data     = new HTTPServerTestHelper();
 
   public class TestSyncStorageRequestDelegate extends
   BaseTestStorageRequestDelegate {
@@ -113,17 +105,19 @@ public class TestServer11RepositorySessi
     }
     return local;
   }
 
   protected Exception doSynchronize(MockServer server) throws Exception {
     final String COLLECTION = "test";
 
     final TrackingWBORepository local = getLocal(100);
-    final Server11Repository remote = new Server11Repository(COLLECTION, getCollectionURL(COLLECTION), authHeaderProvider, infoCollections, infoConfiguration);
+    final Server11Repository remote = new Server11Repository(
+            COLLECTION, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30),
+            getCollectionURL(COLLECTION), authHeaderProvider, infoCollections, infoConfiguration);
     KeyBundle collectionKey = new KeyBundle(TEST_USERNAME, SYNC_KEY);
     Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(remote, collectionKey);
     cryptoRepo.recordFactory = new BookmarkRecordFactory();
 
     final Synchronizer synchronizer = new ServerLocalSynchronizer();
     synchronizer.repositoryA = cryptoRepo;
     synchronizer.repositoryB = local;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
@@ -1,37 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.repositories.downloaders;
 
+import android.net.Uri;
+import android.os.SystemClock;
 import android.support.annotation.NonNull;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncResponse;
 import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
-import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
 
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 
 import static org.junit.Assert.*;
 import static org.junit.Assert.assertEquals;
 
@@ -39,17 +43,17 @@ import static org.junit.Assert.assertEqu
 public class BatchingDownloaderTest {
     private MockSever11Repository serverRepository;
     private Server11RepositorySession repositorySession;
     private MockSessionFetchRecordsDelegate sessionFetchRecordsDelegate;
     private MockDownloader mockDownloader;
     private String DEFAULT_COLLECTION_NAME = "dummyCollection";
     private String DEFAULT_COLLECTION_URL = "http://dummy.url/";
     private long DEFAULT_NEWER = 1;
-    private String DEFAULT_SORT = "index";
+    private String DEFAULT_SORT = "oldest";
     private String DEFAULT_IDS = "1";
     private String DEFAULT_LMHEADER = "12345678";
 
     class MockSessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate {
         public boolean isFailure;
         public boolean isFetched;
         public boolean isSuccess;
         public int batchesCompleted;
@@ -100,18 +104,18 @@ public class BatchingDownloaderTest {
         public long newer;
         public long limit;
         public boolean full;
         public String sort;
         public String ids;
         public String offset;
         public boolean abort;
 
-        public MockDownloader(Server11Repository repository, Server11RepositorySession repositorySession) {
-            super(repository, repositorySession);
+        public MockDownloader(RepositorySession repositorySession, boolean allowMultipleBatches) {
+            super(null, Uri.EMPTY, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30), allowMultipleBatches, repositorySession);
         }
 
         @Override
         public void fetchWithParameters(long newer,
                                  long batchLimit,
                                  boolean full,
                                  String sort,
                                  String ids,
@@ -144,22 +148,17 @@ public class BatchingDownloaderTest {
             return super.makeSyncStorageCollectionRequest(newer, batchLimit, full, sort, ids, offset);
         }
     }
 
     class MockSever11Repository extends Server11Repository {
         public MockSever11Repository(@NonNull String collection, @NonNull String storageURL,
                                      AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections,
                                      @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException {
-            super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration);
-        }
-
-        @Override
-        public long getDefaultTotalLimit() {
-            return 200;
+            super(collection, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30), storageURL, authHeaderProvider, infoCollections, infoConfiguration);
         }
     }
 
     class MockRepositorySession extends Server11RepositorySession {
         public boolean abort;
 
         public MockRepositorySession(Repository repository) {
             super(repository);
@@ -173,17 +172,17 @@ public class BatchingDownloaderTest {
 
     @Before
     public void setUp() throws Exception {
         sessionFetchRecordsDelegate = new MockSessionFetchRecordsDelegate();
 
         serverRepository = new MockSever11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, null,
                 new InfoCollections(), new InfoConfiguration());
         repositorySession = new Server11RepositorySession(serverRepository);
-        mockDownloader = new MockDownloader(serverRepository, repositorySession);
+        mockDownloader = new MockDownloader(repositorySession, true);
     }
 
     @Test
     public void testFlattenId() {
         String[] emptyGuid = new String[]{};
         String flatten =  BatchingDownloader.flattenIDs(emptyGuid);
         assertEquals("", flatten);
 
@@ -199,241 +198,123 @@ public class BatchingDownloaderTest {
         multiGuid[0] = guid0;
         multiGuid[1] = guid1;
         multiGuid[2] = guid2;
         flatten = BatchingDownloader.flattenIDs(multiGuid);
         assertEquals("123456789abc,456789abc,789abc", flatten);
     }
 
     @Test
-    public void testEncodeParam() throws Exception {
-        String param = "123&123";
-        String encodedParam = mockDownloader.encodeParam(param);
-        assertEquals("123%26123", encodedParam);
-    }
-
-    @Test(expected=IllegalArgumentException.class)
-    public void testOverTotalLimit() throws Exception {
-        // Per-batch limits exceed total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 200;
-            }
-        };
-        MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
+    public void testBatchingTrivial() throws Exception {
+        MockDownloader mockDownloader = new MockDownloader(repositorySession, true);
 
         assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-    }
+        // Number of records == batch limit.
+        final long BATCH_LIMIT = 100;
+        mockDownloader.fetchSince(sessionFetchRecordsDelegate, DEFAULT_NEWER, BATCH_LIMIT, DEFAULT_SORT);
 
-    @Test
-    public void testTotalLimit() throws Exception {
-        // Total and per-batch limits are the same.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 100;
-            }
-        };
-        MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
-
-        assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-
-        SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, "100", "100");
+        SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, null, "100");
         SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
         mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request,
-                DEFAULT_NEWER, limit, true, DEFAULT_SORT, DEFAULT_IDS);
+                DEFAULT_NEWER, BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         assertTrue(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(0, sessionFetchRecordsDelegate.batchesCompleted);
     }
 
     @Test
-    public void testOverHalfOfTotalLimit() throws Exception {
-        // Per-batch limit is just a bit lower than total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 75;
-            }
-        };
-        MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
+    public void testBatchingSingleBatchMode() throws Exception {
+        MockDownloader mockDownloader = new MockDownloader(repositorySession, false);
 
         assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-
-        String offsetHeader = "75";
-        String recordsHeader = "75";
-        SyncStorageResponse response = makeSyncStorageResponse(200,  DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
-
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+        // Number of records > batch limit. But, we're only allowed to make one batch request.
+        final long BATCH_LIMIT = 100;
+        mockDownloader.fetchSince(sessionFetchRecordsDelegate, DEFAULT_NEWER, BATCH_LIMIT, DEFAULT_SORT);
 
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
-        offsetHeader = "150";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+        String offsetHeader = "25";
+        String recordsHeader = "500";
+        SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+        SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
+        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request,
+                DEFAULT_NEWER, BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         assertTrue(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(0, sessionFetchRecordsDelegate.batchesCompleted);
     }
 
     @Test
-    public void testHalfOfTotalLimit() throws Exception {
-        // Per-batch limit is half of total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 50;
-            }
-        };
-        mockDownloader = new MockDownloader(repository, repositorySession);
+    public void testBatching() throws Exception {
+        final long BATCH_LIMIT = 25;
+        mockDownloader = new MockDownloader(repositorySession, true);
 
         assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
+        mockDownloader.fetchSince(sessionFetchRecordsDelegate, DEFAULT_NEWER, BATCH_LIMIT, DEFAULT_SORT);
 
-        String offsetHeader = "50";
-        String recordsHeader = "50";
+        String offsetHeader = "25";
+        String recordsHeader = "25";
         SyncStorageResponse response = makeSyncStorageResponse(200,  DEFAULT_LMHEADER, offsetHeader, recordsHeader);
         SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
         mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
+        assertSameParameters(mockDownloader, BATCH_LIMIT);
         assertEquals(offsetHeader, mockDownloader.offset);
         assertFalse(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(1, sessionFetchRecordsDelegate.batchesCompleted);
 
-        // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
-        offsetHeader = "100";
+        // The next batch, we still have an offset token and has not exceed the total limit.
+        offsetHeader = "50";
         response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
         mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
+
+        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+        // Verify the same parameters are used in the next fetch.
+        assertSameParameters(mockDownloader, BATCH_LIMIT);
+        assertEquals(offsetHeader, mockDownloader.offset);
+        assertFalse(sessionFetchRecordsDelegate.isSuccess);
+        assertFalse(sessionFetchRecordsDelegate.isFetched);
+        assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(2, sessionFetchRecordsDelegate.batchesCompleted);
+
+        // The next batch, we still have an offset token and has not exceed the total limit.
+        offsetHeader = "75";
+        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
+
+        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+        // Verify the same parameters are used in the next fetch.
+        assertSameParameters(mockDownloader, BATCH_LIMIT);
+        assertEquals(offsetHeader, mockDownloader.offset);
+        assertFalse(sessionFetchRecordsDelegate.isSuccess);
+        assertFalse(sessionFetchRecordsDelegate.isFetched);
+        assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(3, sessionFetchRecordsDelegate.batchesCompleted);
+
+        // No more offset token, so we complete batching.
+        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, null, recordsHeader);
+        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         assertTrue(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
-    }
-
-    @Test
-    public void testFractionOfTotalLimit() throws Exception {
-        // Per-batch limit is a small fraction of the total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 25;
-            }
-        };
-        mockDownloader = new MockDownloader(repository, repositorySession);
-
-        assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-
-        String offsetHeader = "25";
-        String recordsHeader = "25";
-        SyncStorageResponse response = makeSyncStorageResponse(200,  DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token and has not exceed the total limit.
-        offsetHeader = "50";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token and has not exceed the total limit.
-        offsetHeader = "75";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
-        offsetHeader = "100";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        assertTrue(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(3, sessionFetchRecordsDelegate.batchesCompleted);
     }
 
     @Test
     public void testFailureLMChangedMultiBatch() throws Exception {
         assertNull(mockDownloader.getLastModified());
 
         String lmHeader = "12345678";
         String offsetHeader = "100";
@@ -490,22 +371,38 @@ public class BatchingDownloaderTest {
         assertFalse(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
         assertEquals(record, sessionFetchRecordsDelegate.record);
     }
 
     @Test
     public void testAbortRequests() {
         MockRepositorySession mockRepositorySession = new MockRepositorySession(serverRepository);
-        BatchingDownloader downloader = new BatchingDownloader(serverRepository, mockRepositorySession);
+        BatchingDownloader downloader = new BatchingDownloader(null, Uri.EMPTY, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30), true, mockRepositorySession);
         assertFalse(mockRepositorySession.abort);
         downloader.abortRequests();
         assertTrue(mockRepositorySession.abort);
     }
 
+    @Test
+    public void testBuildCollectionURI() {
+        try {
+            assertEquals("?full=1&newer=5000.000", BatchingDownloader.buildCollectionURI(Uri.EMPTY, true, 5000000L, -1, null, null, null).toString());
+            assertEquals("?newer=1230.000", BatchingDownloader.buildCollectionURI(Uri.EMPTY, false, 1230000L, -1, null, null, null).toString());
+            assertEquals("?newer=5000.000&limit=10", BatchingDownloader.buildCollectionURI(Uri.EMPTY, false, 5000000L, 10, null, null, null).toString());
+            assertEquals("?full=1&newer=5000.000&sort=index", BatchingDownloader.buildCollectionURI(Uri.EMPTY, true, 5000000L, 0, "index", null, null).toString());
+            assertEquals("?full=1&ids=123%2Cabc", BatchingDownloader.buildCollectionURI(Uri.EMPTY, true, -1L, -1, null, "123,abc", null).toString());
+
+            final Uri baseUri = Uri.parse("https://moztest.org/collection/");
+            assertEquals(baseUri + "?full=1&ids=123%2Cabc&offset=1234", BatchingDownloader.buildCollectionURI(baseUri, true, -1L, -1, null, "123,abc", "1234").toString());
+        } catch (URISyntaxException e) {
+            fail();
+        }
+    }
+
     private void assertSameParameters(MockDownloader mockDownloader, long limit) {
         assertEquals(DEFAULT_NEWER, mockDownloader.newer);
         assertEquals(limit, mockDownloader.limit);
         assertTrue(mockDownloader.full);
         assertEquals(DEFAULT_SORT, mockDownloader.sort);
         assertEquals(DEFAULT_IDS, mockDownloader.ids);
     }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.repositories.uploaders;
 
 import android.net.Uri;
+import android.os.SystemClock;
 import android.support.annotation.NonNull;
 
 import static org.junit.Assert.*;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.MockRecord;
@@ -20,16 +21,17 @@ import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
 
 import java.net.URISyntaxException;
 import java.util.Random;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(TestRunner.class)
 public class BatchingUploaderTest {
     class MockExecutorService implements Executor {
         int totalPayloads = 0;
         int commitPayloads = 0;
 
         @Override
@@ -475,16 +477,17 @@ public class BatchingUploaderTest {
                 }
             };
         }
 
 
         try {
             return new Server11Repository(
                     "dummyCollection",
+                    SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30),
                     "http://dummy.url/",
                     null,
                     infoCollections,
                     infoConfiguration
             );
         } catch (URISyntaxException e) {
             // Won't throw, and this won't happen.
             return null;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
+import android.os.SystemClock;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
@@ -22,16 +24,17 @@ import org.mozilla.gecko.sync.crypto.Key
 import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestEnsureCrypto5KeysStage {
@@ -95,17 +98,17 @@ public class TestEnsureCrypto5KeysStage 
 
   public void doSession(MockServer server) {
     data.startHTTPServer(server);
     try {
       WaitHelper.getTestWaiter().performWait(new Runnable() {
         @Override
         public void run() {
           try {
-            session.start();
+            session.start(SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30));
           } catch (AlreadySyncingException e) {
             WaitHelper.getTestWaiter().performNotify(e);
           }
         }
       });
     } finally {
     data.stopHTTPServer();
     }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
+import android.os.SystemClock;
+
 import org.json.simple.JSONArray;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.net.test.TestMetaGlobal;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.android.sync.test.helpers.MockServer;
@@ -33,16 +35,17 @@ import org.mozilla.gecko.sync.stage.Fetc
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.io.IOException;
 import java.net.URI;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestFetchMetaGlobalStage {
@@ -158,17 +161,17 @@ public class TestFetchMetaGlobalStage {
   }
 
   protected void doSession(MockServer server) {
     data.startHTTPServer(server);
     WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
       @Override
       public void run() {
         try {
-          session.start();
+          session.start(SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30));
         } catch (AlreadySyncingException e) {
           WaitHelper.getTestWaiter().performNotify(e);
         }
       }
     }));
     data.stopHTTPServer();
   }