Merge m-c to b2ginbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 06 Nov 2015 15:14:26 -0800
changeset 271718 f71204181af4a7aadd8862f154d9e32d82ce5cda
parent 271717 99c48eff0e9126189a010a8b0aa6df42b101a891 (current diff)
parent 271618 e2a910c048dc82fc3be53475f18e7f81f03e377b (diff)
child 271719 99f9591bc65d7c51cead6eebdeebb45dd0d911fb
push id67746
push usercbook@mozilla.com
push dateMon, 09 Nov 2015 13:58:59 +0000
treeherdermozilla-inbound@e1ef2be156de [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone45.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
Merge m-c to b2ginbound, a=merge
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/test_peerConnection_trackDisabling.html
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1319,16 +1319,31 @@ BrowserGlue.prototype = {
 
       if (willPrompt) {
         Services.tm.mainThread.dispatch(function() {
           DefaultBrowserCheck.prompt(RecentWindow.getMostRecentBrowserWindow());
         }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
       }
     }
 
+    if (this._mayNeedToWarnAboutTabGroups) {
+      let haveTabGroups = false;
+      let wins = Services.wm.getEnumerator("navigator:browser");
+      while (wins.hasMoreElements()) {
+        let win = wins.getNext();
+        if (win.TabView._tabBrowserHasHiddenTabs() && win.TabView.firstUseExperienced()) {
+          haveTabGroups = true;
+          break;
+        }
+      }
+      if (haveTabGroups) {
+        this._showTabGroupsDeprecationNotification();
+      }
+    }
+
 #ifdef E10S_TESTING_ONLY
     E10SUINotification.checkStatus();
 #endif
   },
 
 #ifdef MOZ_DEV_EDITION
   _createExtraDefaultProfile: function () {
     // If Developer Edition is the only installed Firefox version and no other
@@ -1357,16 +1372,37 @@ BrowserGlue.prototype = {
         }).then(null, e => {
           Cu.reportError("Could not empty profile 'default': " + e);
         });
       }
     }
   },
 #endif
 
+  _showTabGroupsDeprecationNotification() {
+    let brandShortName = gBrandBundle.GetStringFromName("brandShortName");
+    let text = gBrowserBundle.formatStringFromName("tabgroups.deprecationwarning.description",
+                                                   [brandShortName], 1);
+    let learnMore = gBrowserBundle.GetStringFromName("tabgroups.deprecationwarning.learnMore.label");
+    let learnMoreKey = gBrowserBundle.GetStringFromName("tabgroups.deprecationwarning.learnMore.accesskey");
+
+    let win = RecentWindow.getMostRecentBrowserWindow();
+    let notifyBox = win.document.getElementById("high-priority-global-notificationbox");
+    let button = {
+      label: learnMore,
+      accessKey: learnMoreKey,
+      callback: function(aNotificationBar, aButton) {
+        win.openUILinkIn("https://support.mozilla.org/kb/tab-groups-removal", "tab");
+      },
+    };
+
+    notifyBox.appendNotification(text, "tabgroups-removal-notification", null,
+                                 notifyBox.PRIORITY_WARNING_MEDIUM, [button]);
+  },
+
   _onQuitRequest: function BG__onQuitRequest(aCancelQuit, aQuitType) {
     // If user has already dismissed quit request, then do nothing
     if ((aCancelQuit instanceof Ci.nsISupportsPRBool) && aCancelQuit.data)
       return;
 
     // There are several cases where we won't show a dialog here:
     // 1. There is only 1 tab open in 1 window
     // 2. The session will be restored at startup, indicated by
@@ -1864,17 +1900,17 @@ BrowserGlue.prototype = {
     var notifyBox = win.gBrowser.getNotificationBox();
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
     notification.persistence = -1; // Until user closes it
   },
 
   _migrateUI: function BG__migrateUI() {
-    const UI_VERSION = 32;
+    const UI_VERSION = 33;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
     let currentUIVersion = 0;
     try {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } catch(ex) {}
     if (currentUIVersion >= UI_VERSION)
       return;
 
@@ -2209,16 +2245,21 @@ BrowserGlue.prototype = {
       xulStore.removeValue(BROWSER_DOCURL, "bookmarks-menu-button", "class");
       xulStore.removeValue(BROWSER_DOCURL, "home-button", "class");
     }
 
     if (currentUIVersion < 32) {
       this._notifyNotificationsUpgrade().catch(Cu.reportError);
     }
 
+    if (currentUIVersion < 33) {
+      // We'll do something once windows are open:
+      this._mayNeedToWarnAboutTabGroups = true;
+    }
+
     // Update the migration version.
     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
   },
 
   _hasExistingNotificationPermission: function BG__hasExistingNotificationPermission() {
     let enumerator = Services.perms.enumerator;
     while (enumerator.hasMoreElements()) {
       let permission = enumerator.getNext().QueryInterface(Ci.nsIPermission);
@@ -2235,25 +2276,36 @@ BrowserGlue.prototype = {
     }
     yield this._firstWindowReady;
     function clickCallback(subject, topic, data) {
       if (topic != "alertclickcallback")
         return;
       let win = RecentWindow.getMostRecentBrowserWindow();
       win.openUILinkIn(data, "tab");
     }
-    let imageURL = "chrome://browser/skin/web-notifications-icon.svg";
+    // Show the application icon for XUL notifications. We assume system-level
+    // notifications will include their own icon.
+    let imageURL = this._hasSystemAlertsService() ? "" :
+                   "chrome://branding/content/about-logo.png";
     let title = gBrowserBundle.GetStringFromName("webNotifications.upgradeTitle");
     let text = gBrowserBundle.GetStringFromName("webNotifications.upgradeBody");
     let url = Services.urlFormatter.formatURLPref("browser.push.warning.migrationURL");
 
     AlertsService.showAlertNotification(imageURL, title, text,
                                         true, url, clickCallback);
   }),
 
+  _hasSystemAlertsService: function() {
+    try {
+      return !!Cc["@mozilla.org/system-alerts-service;1"].getService(
+        Ci.nsIAlertsService);
+    } catch (e) {}
+    return false;
+  },
+
   // ------------------------------
   // public nsIBrowserGlue members
   // ------------------------------
 
   sanitize: function BG_sanitize(aParentWindow) {
     this._sanitizer.sanitize(aParentWindow);
   },
 
--- a/browser/components/tabview/test/browser.ini
+++ b/browser/components/tabview/test/browser.ini
@@ -99,16 +99,17 @@ skip-if = e10s # Bug 1086190
 skip-if = e10s # Bug 1086190
 [browser_tabview_bug626455.js]
 skip-if = e10s # Bug 1086190
 [browser_tabview_bug626525.js]
 [browser_tabview_bug626791.js]
 skip-if = buildapp == 'mulet'
 [browser_tabview_bug627736.js]
 [browser_tabview_bug628061.js]
+skip-if = true
 [browser_tabview_bug628165.js]
 [browser_tabview_bug628270.js]
 [browser_tabview_bug628887.js]
 [browser_tabview_bug629189.js]
 [browser_tabview_bug629195.js]
 skip-if = os == 'linux'&&debug # bug 981703
 [browser_tabview_bug630102.js]
 [browser_tabview_bug630157.js]
--- a/browser/components/tabview/test/browser_tabview_bug626791.js
+++ b/browser/components/tabview/test/browser_tabview_bug626791.js
@@ -153,16 +153,18 @@ function test() {
     let onLoad = function (newWin) {
       win = newWin;
       removeToolbarButton();
     };
 
     let onShow = function () {
       cw = win.TabView.getContentWindow();
 
+      cw.document.querySelector('.banner').remove();
+
       let groupItem = cw.GroupItems.groupItems[0];
       groupItem.setSize(200, 200, true);
       groupItem.setUserSize();
 
       SimpleTest.waitForFocus(function () {
         assertToolbarButtonNotExists();
         test();
       }, cw);
--- a/browser/components/tabview/test/browser_tabview_bug656778.js
+++ b/browser/components/tabview/test/browser_tabview_bug656778.js
@@ -27,22 +27,22 @@ function test() {
   let assertPreferences = function (startupPage, firstRun, enabledOnce) {
     assertIntPref(TabView.PREF_STARTUP_PAGE, startupPage);
     assertBoolPref(TabView.PREF_FIRST_RUN, firstRun);
     assertBoolPref(TabView.PREF_RESTORE_ENABLED_ONCE, enabledOnce);
   };
 
   let assertNotificationBannerVisible = function (win) {
     let cw = win.TabView.getContentWindow();
-    is(cw.iQ(".banner").length, 1, "notification banner is visible");
+    is(cw.iQ(".banner").length, 2, "notification banner is visible");
   };
 
   let assertNotificationBannerNotVisible = function (win) {
     let cw = win.TabView.getContentWindow();
-    is(cw.iQ(".banner").length, 0, "notification banner is not visible");
+    is(cw.iQ(".banner").length, 1, "notification banner is not visible");
   };
 
   let next = function () {
     if (tests.length == 0) {
       waitForFocus(finish);
       return;
     }
 
--- a/browser/components/tabview/ui.js
+++ b/browser/components/tabview/ui.js
@@ -506,27 +506,31 @@ var UI = {
       dispatchEvent(event);
 
       // Flush pending updates
       GroupItems.flushAppTabUpdates();
 
       TabItems.resumePainting();
     }
 
+    this.notifyDeprecation();
+
     if (gTabView.firstUseExperienced)
       gTabView.enableSessionRestore();
   },
 
   // ----------
   // Function: hideTabView
   // Hides TabView and shows the main browser UI.
   hideTabView: function UI_hideTabView() {
     if (!this.isTabViewVisible() || this._isChangingVisibility)
       return;
 
+    iQ(".banner").remove();
+
     // another tab might be select if user decides to stay on a page when
     // a onclose confirmation prompts.
     GroupItems.removeHiddenGroups();
 
     // We need to set this after removing the hidden groups because doing so
     // might show prompts which will cause us to be called again, and we'd get
     // stuck if we prevent re-entrancy before doing that.
     this._isChangingVisibility = true;
@@ -1536,13 +1540,43 @@ var UI = {
 
     let onFadeIn = function () {
       setTimeout(function () {
         banner.animate({opacity: 0}, {duration: 1500, complete: onFadeOut});
       }, 5000);
     };
 
     banner.animate({opacity: 0.7}, {duration: 1500, complete: onFadeIn});
-  }
+  },
+
+  // Function: notifyDeprecation
+  // Notify the user that tab groups will be deprecated soon.
+  notifyDeprecation() {
+    let brandBundle = gWindow.document.getElementById("bundle_brand");
+    let brandShortName = brandBundle.getString("brandShortName");
+    let browserBundle = gWindow.document.getElementById("bundle_browser");
+    let notificationText = browserBundle.getFormattedString(
+      "tabgroups.deprecationwarning.description", [brandShortName]);
+
+    let learnMoreText = browserBundle.getString("tabgroups.deprecationwarning.learnMore.label");
+
+    let onButtonClick = () => {
+      this.hideTabView();
+      gWindow.openUILinkIn("https://support.mozilla.org/kb/tab-groups-removal", "tab");
+    };
+
+    let button = iQ("<button>")
+      .text(learnMoreText)
+      .css('-moz-margin-start', '10px')
+      .one('click', onButtonClick);
+
+    let banner = iQ("<div>")
+      .text(notificationText)
+      .addClass("banner")
+      .append(button)
+      .appendTo("body");
+
+    banner.animate({opacity: 0.7}, {duration: 1500});
+  },
 };
 
 // ----------
 UI.init();
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -792,8 +792,14 @@ usercontext.personal.label = Personal
 usercontext.work.label = Work
 usercontext.shopping.label = Shopping
 usercontext.banking.label = Banking
 
 muteTab.label = Mute Tab
 muteTab.accesskey = M
 unmuteTab.label = Unmute Tab
 unmuteTab.accesskey = M
+
+# LOCALIZATION NOTE (tabgroups.deprecationwarning.description):
+# %S is brandShortName
+tabgroups.deprecationwarning.description         = Heads up! Tab Groups will be removed from %S soon.
+tabgroups.deprecationwarning.learnMore.label     = Learn More
+tabgroups.deprecationwarning.learnMore.accesskey = L
--- a/browser/themes/linux/tabview/tabview.css
+++ b/browser/themes/linux/tabview/tabview.css
@@ -559,17 +559,17 @@ html[dir=rtl] .iq-resizable-se {
 #searchshade{
   background-color: rgba(0,0,0,.42);
   width: 100%;
   height: 100%;
 }
 
 #search{
   width: 100%;
-  height: 100%;
+  height: calc(100% - 1.7em - 20px); /* leave room for banner */
 }
 
 #searchbox{
   width: 270px;
   max-width: -moz-available;
   -moz-margin-start: 20px;
   height: 30px;
   box-shadow: 0px 1px 0px rgba(255,255,255,.5), 0px -1px 0px rgba(0,0,0,1), 0px 0px 9px rgba(0,0,0,.8);
--- a/browser/themes/osx/tabview/tabview.css
+++ b/browser/themes/osx/tabview/tabview.css
@@ -547,17 +547,17 @@ html[dir=rtl] .iq-resizable-se {
 }
 
 #searchshade:-moz-window-inactive {
   background: linear-gradient(rgba(237,237,237,0.42), rgba(216,216,216,0.42));
 }
 
 #search{
   width: 100%;
-  height: 100%;
+  height: calc(100% - 1.7em - 20px); /* leave room for banner */
 }
 
 #searchbox {
   width: 270px;
   max-width: -moz-available;
   -moz-margin-start: 20px;
   height: 30px;
   box-shadow: 0px 1px 0px rgba(255,255,255,.5), 0px -1px 0px rgba(0,0,0,1), 0px 0px 13px rgba(0,0,0,.8);
--- a/browser/themes/windows/tabview/tabview.css
+++ b/browser/themes/windows/tabview/tabview.css
@@ -564,17 +564,17 @@ html[dir=rtl] .iq-resizable-se {
 #searchshade{
   background-color: rgba(0,0,0,.42);
   width: 100%;
   height: 100%;
 }
 
 #search{
   width: 100%;
-  height: 100%;
+  height: calc(100% - 1.7em - 20px); /* leave room for banner */
 }
 
 #searchbox{
   width: 270px;
   max-width: -moz-available;
   -moz-margin-start: 20px;
   height: 30px;
   box-shadow: 0px 1px 0px rgba(255,255,255,.5), 0px -1px 0px rgba(0,0,0,1), 0px 0px 9px rgba(0,0,0,.8);
--- a/dom/base/nsDocumentEncoder.cpp
+++ b/dom/base/nsDocumentEncoder.cpp
@@ -1436,17 +1436,17 @@ nsHTMLCopyEncoder::SetSelection(nsISelec
     // checking for selection inside a plaintext form widget
     if (selContent->IsAnyOfHTMLElements(nsGkAtoms::input, nsGkAtoms::textarea))
     {
       mIsTextWidget = true;
       break;
     }
 #ifdef MOZ_THUNDERBIRD
     else if (selContent->IsElement()) {
-      nsRefPtr<nsStyleContext> styleContext =
+      RefPtr<nsStyleContext> styleContext =
         nsComputedDOMStyle::GetStyleContextForElementNoFlush(
           selContent->AsElement(), nullptr, nullptr);
       if (styleContext) {
         const nsStyleText* textStyle = styleContext->StyleText();
         if (textStyle->mWhiteSpace == NS_STYLE_WHITESPACE_PRE_WRAP) {
           mIsTextWidget = true;
         }
       }
--- a/dom/canvas/test/captureStream_common.js
+++ b/dom/canvas/test/captureStream_common.js
@@ -20,17 +20,16 @@ function CaptureStreamTestHelper(width, 
   this.cout.width = this.elemWidth;
   this.cout.height = this.elemHeight;
   document.body.appendChild(this.cout);
 }
 
 CaptureStreamTestHelper.prototype = {
   /* Predefined colors for use in the methods below. */
   black: { data: [0, 0, 0, 255], name: "black" },
-  blackTransparent: { data: [0, 0, 0, 0], name: "blackTransparent" },
   green: { data: [0, 255, 0, 255], name: "green" },
   red: { data: [255, 0, 0, 255], name: "red" },
 
   /* Default element size for createAndAppendElement() */
   elemWidth: 100,
   elemHeight: 100,
 
   /*
@@ -48,100 +47,65 @@ CaptureStreamTestHelper.prototype = {
   },
 
   /* Request a frame from the stream played by |video|. */
   requestFrame: function (video) {
     info("Requesting frame from " + video.id);
     video.srcObject.requestFrame();
   },
 
-  /*
-   * Returns the pixel at (|offsetX|, |offsetY|) (from top left corner) of
-   * |video| as an array of the pixel's color channels: [R,G,B,A].
-   */
-  getPixel: function (video, offsetX, offsetY) {
-    offsetX = offsetX || 0; // Set to 0 if not passed in.
-    offsetY = offsetY || 0; // Set to 0 if not passed in.
+  /* Tests the top left pixel of |video| against |refData|. Format [R,G,B,A]. */
+  testPixel: function (video, refData, threshold) {
     var ctxout = this.cout.getContext('2d');
     ctxout.drawImage(video, 0, 0);
-    return ctxout.getImageData(offsetX, offsetY, 1, 1).data;
-  },
-
-  /*
-   * Returns true if px lies within the per-channel |threshold| of the
-   * referenced color for all channels. px is on the form of an array of color
-   * channels, [R,G,B,A]. Each channel is in the range [0, 255].
-   */
-  isPixel: function (px, refColor, threshold) {
-    threshold = threshold || 0; // Default to 0 (exact match) if not passed in.
-    return px.every((ch, i) => Math.abs(ch - refColor.data[i]) <= threshold);
+    var pixel = ctxout.getImageData(0, 0, 1, 1).data;
+    return pixel.every((val, i) => Math.abs(val - refData[i]) <= threshold);
   },
 
   /*
-   * Returns true if px lies further away than |threshold| of the
-   * referenced color for any channel. px is on the form of an array of color
-   * channels, [R,G,B,A]. Each channel is in the range [0, 255].
-   */
-  isPixelNot: function (px, refColor, threshold) {
-    if (threshold === undefined) {
-      // Default to 127 (should be sufficiently far away) if not passed in.
-      threshold = 127;
-    }
-    return px.some((ch, i) => Math.abs(ch - refColor.data[i]) > threshold);
-  },
-
-  /*
-   * Returns a promise that resolves when the provided function |test|
-   * returns true.
+   * Returns a promise that resolves when the pixel matches. Use |threshold|
+   * for fuzzy matching the color on each channel, in the range [0,255].
    */
-  waitForPixel: function (video, offsetX, offsetY, test, timeout) {
+  waitForPixel: function (video, refColor, threshold, infoString) {
     return new Promise(resolve => {
-      const startTime = video.currentTime;
+      info("Testing " + video.id + " against [" + refColor.data.join(',') + "]");
       CaptureStreamTestHelper2D.prototype.clear.call(this, this.cout);
-      var ontimeupdate = () => {
-        const pixelMatch = test(this.getPixel(video, offsetX, offsetY));
-        if (!pixelMatch &&
-            (!timeout || video.currentTime < startTime + (timeout / 1000.0))) {
-          // No match yet and,
-          // No timeout (waiting indefinitely) or |timeout| has not passed yet.
-          return;
+      video.ontimeupdate = () => {
+        if (this.testPixel(video, refColor.data, threshold)) {
+          ok(true, video.id + " " + infoString);
+          video.ontimeupdate = null;
+          resolve();
         }
-        video.removeEventListener("timeupdate", ontimeupdate);
-        resolve(pixelMatch);
       };
-      video.addEventListener("timeupdate", ontimeupdate);
     });
   },
 
   /*
-   * Returns a promise that resolves when the top left pixel of |video| matches
-   * on all channels. Use |threshold| for fuzzy matching the color on each
-   * channel, in the range [0,255].
-   */
-  waitForPixelColor: function (video, refColor, threshold, infoString) {
-    info("Waiting for video " + video.id + " to match [" +
-         refColor.data.join(',') + "] - " + refColor.name +
-         " (" + infoString + ")");
-    return this.waitForPixel(video, 0, 0,
-                             px => this.isPixel(px, refColor, threshold))
-      .then(() => ok(true, video.id + " " + infoString));
-  },
-
-  /*
-   * Returns a promise that resolves after |timeout| ms of playback or when the
-   * top left pixel of |video| becomes |refColor|. The test is failed if the
+   * Returns a promise that resolves after |timeout| ms of playback or when a
+   * pixel of |video| becomes the color |refData|. The test is failed if the
    * timeout is not reached.
    */
-  waitForPixelColorTimeout: function (video, refColor, threshold, timeout, infoString) {
-    info("Waiting for " + video.id + " to time out after " + timeout +
-         "ms against [" + refColor.data.join(',') + "] - " + refColor.name);
-    return this.waitForPixel(video, 0, 0,
-                             px => this.isPixel(px, refColor, threshold),
-                             timeout)
-      .then(result => ok(!result, video.id + " " + infoString));
+  waitForPixelToTimeout: function (video, refColor, threshold, timeout, infoString) {
+    return new Promise(resolve => {
+      info("Waiting for " + video.id + " to time out after " + timeout +
+           "ms against [" + refColor.data.join(',') + "] - " + refColor.name);
+      CaptureStreamTestHelper2D.prototype.clear.call(this, this.cout);
+      var startTime = video.currentTime;
+      video.ontimeupdate = () => {
+        if (this.testPixel(video, refColor.data, threshold)) {
+          ok(false, video.id + " " + infoString);
+          video.ontimeupdate = null;
+          resolve();
+        } else if (video.currentTime > startTime + (timeout / 1000.0)) {
+          ok(true, video.id + " " + infoString);
+          video.ontimeupdate = null;
+          resolve();
+        }
+      };
+    });
   },
 
   /* Create an element of type |type| with id |id| and append it to the body. */
   createAndAppendElement: function (type, id) {
     var e = document.createElement(type);
     e.id = id;
     e.width = this.elemWidth;
     e.height = this.elemHeight;
--- a/dom/canvas/test/test_capture.html
+++ b/dom/canvas/test/test_capture.html
@@ -18,87 +18,72 @@ function checkDrawColorInitialRed() {
   info("Checking that all video elements become red when initiated just after the first drawColor(red).");
 
   h.drawColor(c, h.red);
 
   vauto.srcObject = c.captureStream();
   vmanual.srcObject = c.captureStream(0);
   vrate.srcObject = c.captureStream(10);
 
-  ok(h.isPixel(h.getPixel(vauto), h.blackTransparent, 0),
-     "vauto should not be drawn to before stable state");
-  ok(h.isPixel(h.getPixel(vrate), h.blackTransparent, 0),
-     "vrate should not be drawn to before stable state");
-  ok(h.isPixel(h.getPixel(vmanual), h.blackTransparent, 0),
-     "vmanual should not be drawn to before stable state");
+  ok(h.testPixel(vauto, [0, 0, 0, 0], 0), "vauto hould not be drawn to before stable state");
+  ok(h.testPixel(vrate, [0, 0, 0, 0], 0), "vrate Should not be drawn to before stable state");
+  ok(h.testPixel(vmanual, [0, 0, 0, 0], 0), "vmanual Should not be drawn to before stable state");
 
   return Promise.resolve()
-    .then(() => h.waitForPixelColor(vauto, h.red, 0,
-                                    "should become red automatically"))
-    .then(() => h.waitForPixelColor(vrate, h.red, 0,
-                                    "should become red automatically"))
-    .then(() => h.waitForPixelColor(vmanual, h.red, 0,
-                                    "should become red when we get" +
-                                    " to stable state (first frame)"));
+    .then(() => h.waitForPixel(vauto, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vrate, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red when we get" +
+                                               " to stable state (first frame)"));
 }
 
 function checkDrawColorGreen() {
   info("Checking that drawing green propagates properly to video elements.");
 
   var drawing = h.startDrawing(() => h.drawColor(c, h.green));
 
   return Promise.resolve()
-    .then(() => h.waitForPixelColor(vauto, h.green, 0,
-                                    "should become green automatically"))
-    .then(() => h.waitForPixelColor(vrate, h.green, 0,
-                                    "should become green automatically"))
-    .then(() => h.waitForPixelColor(vmanual, h.red, 0,
-                                    "should still be red"))
+    .then(() => h.waitForPixel(vauto, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vrate, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should still be red"))
     .then(() => h.requestFrame(vmanual))
-    .then(() => h.waitForPixelColor(vmanual, h.green, 0,
-                                    "should become green after requstFrame()"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should become green after requstFrame()"))
     .catch(err => ok(false, "checkDrawColorGreen failed: ", err))
     .then(() => drawing.stop());
 }
 
 function checkRequestFrameOrderGuarantee() {
   info("Checking that requestFrame() immediately after a drawColor() " +
        "call results in the expected frame seen in the stream.");
 
   return Promise.resolve()
-    .then(() => h.waitForPixelColor(vmanual, h.green, 0,
-                                    "should still be green"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should still be green"))
     .then(() => h.drawColor(c, h.red))   // 1. Draw canvas red
     .then(() => h.requestFrame(vmanual)) // 2. Immediately request a frame
-    .then(() => h.waitForPixelColor(vmanual, h.red, 0,
-                                    "should become red after call order test"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red after call order test"))
 }
 
 function checkDrawImageNotCleanRed() {
   info("Checking that drawImage with not origin-clean image renders streams useless.");
   var ctx = c.getContext('2d');
   var notCleanRed = new Image();
   var drawing;
 
   return new Promise((resolve, reject) => {
     notCleanRed.onload = resolve;
     notCleanRed.onerror = () => reject(new Error("Failed to load tainted image."));
     notCleanRed.src = "http://example.com/tests/dom/canvas/test/image_red_crossorigin_credentials.png";
     document.body.appendChild(notCleanRed);
   })
     .then(() => drawing = h.startDrawing(() => ctx.drawImage(notCleanRed, 0, 0, c.width, c.height)))
     .then(() => h.testNotClean(c))
-    .then(() => h.waitForPixelColorTimeout(vauto, h.red, 0, 1000,
-                                           "should not become red"))
-    .then(() => h.isPixelNot(h.getPixel(vrate), h.red, 250,
-                             "should not have become red"))
-    .then(() => h.waitForPixelColor(vmanual, h.green, 0, "should still be green"))
+    .then(() => h.waitForPixelToTimeout(vauto, h.red, 0, 1000, "should not become red"))
+    .then(() => h.waitForPixelToTimeout(vrate, h.red, 0, 0, "should not become red"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should still be green"))
     .then(() => h.requestFrame(vmanual))
-    .then(() => h.waitForPixelColorTimeout(vmanual, h.red, 0, 1000,
-                                           "should not become red"))
+    .then(() => h.waitForPixelToTimeout(vmanual, h.red, 0, 1000, "should not become red"))
     .catch(err => ok(false, "checkDrawImageNotCleanRed failed: ", err))
     .then(() => drawing.stop());
 }
 
 function finish() {
   ok(true, 'Test complete.');
   SimpleTest.finish();
 }
--- a/dom/canvas/test/webgl-mochitest/test_capture.html
+++ b/dom/canvas/test/webgl-mochitest/test_capture.html
@@ -49,75 +49,59 @@ function checkClearColorInitialRed() {
   info("Checking that clearing to red works for first frame.");
 
   h.clearColor(c, h.red);
 
   vauto.srcObject = c.captureStream();
   vmanual.srcObject = c.captureStream(0);
   vrate.srcObject = c.captureStream(10);
 
-  ok(h.isPixel(h.getPixel(vauto), h.blackTransparent, 0,
-     "vauto should not be drawn to before stable state"));
-  ok(h.isPixel(h.getPixel(vrate), h.blackTransparent, 0,
-     "vrate should not be drawn to before stable state"));
-  ok(h.isPixel(h.getPixel(vmanual), h.blackTransparent, 0,
-     "vmanual should not be drawn to before stable state"));
+  ok(h.testPixel(vauto, [0, 0, 0, 0], 0), "Should not be drawn to before stable state");
+  ok(h.testPixel(vrate, [0, 0, 0, 0], 0), "Should not be drawn to before stable state");
+  ok(h.testPixel(vmanual, [0, 0, 0, 0], 0), "Should not be drawn to before stable state");
 
   return Promise.resolve()
-    .then(() => h.waitForPixelColor(vauto, h.red, 0,
-                                    "should become red automatically"))
-    .then(() => h.waitForPixelColor(vrate, h.red, 0,
-                                    "should become red automatically"))
-    .then(() => h.waitForPixelColor(vmanual, h.red, 0,
-                                    "should become red when we get to stable " +
-                                    "state (first frame)"))
+    .then(() => h.waitForPixel(vauto, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vrate, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red when we get to stable state (first frame)"))
 }
 
 function checkDrawColorGreen() {
   info("Checking that drawing green results in green video frames.");
   var drawing = h.startDrawing(h.drawColor.bind(h, c, h.green));
   checkGLError('after DrawColor');
   return Promise.resolve()
-    .then(() => h.waitForPixelColor(vauto, h.green, 0,
-                                    "should become green automatically"))
-    .then(() => h.waitForPixelColor(vrate, h.green, 0,
-                                    "should become green automatically"))
-    .then(() => h.waitForPixelColor(vmanual, h.red, 0,
-                                    "should still be red"))
+    .then(() => h.waitForPixel(vauto, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vrate, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should still be red"))
     .then(() => h.requestFrame(vmanual))
-    .then(() => h.waitForPixelColor(vmanual, h.green, 0,
-                                    "should become green after requstFrame()"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should become green after requstFrame()"))
     .then(() => drawing.stop());
 }
 
 function checkClearColorRed() {
   info("Checking that clearing to red works.");
   var drawing = h.startDrawing(h.clearColor.bind(h, c, h.red));
   return Promise.resolve()
-    .then(() => h.waitForPixelColor(vauto, h.red, 0,
-                                    "should become red automatically"))
-    .then(() => h.waitForPixelColor(vrate, h.red, 0,
-                                    "should become red automatically"))
-    .then(() => h.waitForPixelColor(vmanual, h.green, 0,
-                                    "should still be green"))
+    .then(() => h.waitForPixel(vauto, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vrate, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should still be green"))
     .then(() => h.requestFrame(vmanual))
-    .then(() => h.waitForPixelColor(vmanual, h.red, 0,
-                                    "should become red after requestFrame()"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red after requestFrame()"))
     .then(() => drawing.stop());
 }
 
 function checkRequestFrameOrderGuarantee() {
   info("Checking that requestFrame() immediately after a draw " +
        "call results in the expected frame seen in the stream.");
   return Promise.resolve()
-    .then(() => h.waitForPixelColor(vmanual, h.red, 0, "should still be red"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should still be red"))
     .then(() => h.drawColor(c, h.green)) // 1. Draw canvas green
     .then(() => h.requestFrame(vmanual)) // 2. Immediately request a frame
-    .then(() => h.waitForPixelColor(vmanual, h.green, 0,
-                                    "should become green after call order test"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should become green after call order test"))
 }
 
 function finish() {
   ok(true, 'Test complete.');
   SimpleTest.finish();
 }
 
 function beginTest() {
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -1410,32 +1410,35 @@ MediaManager::EnumerateRawDevices(uint64
                                   MediaSourceEnum aVideoType,
                                   MediaSourceEnum aAudioType,
                                   bool aFake, bool aFakeTracks)
 {
   MOZ_ASSERT(NS_IsMainThread());
   RefPtr<PledgeSourceSet> p = new PledgeSourceSet();
   uint32_t id = mOutstandingPledges.Append(*p);
 
+  // Check if the preference for using audio/video loopback devices is
+  // enabled. This is currently used for automated media tests only.
+  //
+  // If present (and we're doing non-exotic cameras and microphones) use them
+  // instead of our built-in fake devices, except if fake tracks are requested
+  // (a feature of the built-in ones only).
+
   nsAdoptingCString audioLoopDev, videoLoopDev;
-  if (!aFake) {
-    // Fake stream not requested. The entire device stack is available.
-    // Loop in loopback devices if they are set, and their respective type is
-    // requested. This is currently used for automated media tests only.
-    if (aVideoType == MediaSourceEnum::Camera) {
+  if (!aFakeTracks) {
+    if (aVideoType == dom::MediaSourceEnum::Camera) {
+      audioLoopDev = Preferences::GetCString("media.audio_loopback_dev");
       videoLoopDev = Preferences::GetCString("media.video_loopback_dev");
+
+      if (aFake && !audioLoopDev.IsEmpty() && !videoLoopDev.IsEmpty()) {
+        aFake = false;
+      }
+    } else {
+      aFake = false;
     }
-    if (aAudioType == MediaSourceEnum::Microphone) {
-      audioLoopDev = Preferences::GetCString("media.audio_loopback_dev");
-    }
-  }
-
-  if (!aFake) {
-    // Fake tracks only make sense when we have a fake stream.
-    aFakeTracks = false;
   }
 
   MediaManager::PostTask(FROM_HERE, NewTaskFrom([id, aWindowId, audioLoopDev,
                                                  videoLoopDev, aVideoType,
                                                  aAudioType, aFake,
                                                  aFakeTracks]() mutable {
     RefPtr<MediaEngine> backend;
     if (aFake) {
--- a/dom/media/test/test_mediarecorder_record_canvas_captureStream.html
+++ b/dom/media/test/test_mediarecorder_record_canvas_captureStream.html
@@ -51,17 +51,17 @@ function startTest() {
     video.id = "recorded-video";
     video.src = URL.createObjectURL(blob);
     video.play();
     video.onerror = err => {
       ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
       SimpleTest.finish();
     };
     document.getElementById("content").appendChild(video);
-    helper.waitForPixelColor(video, helper.red, 128, "Should become red")
+    helper.waitForPixel(video, helper.red, 128, "Should become red")
       .then(SimpleTest.finish);
   };
 
   mediaRecorder.start();
   is(mediaRecorder.state, "recording", "Media recorder should be recording");
 }
 
 SimpleTest.waitForExplicitFinish();
--- a/dom/media/tests/mochitest/identity/identityPcTest.js
+++ b/dom/media/tests/mochitest/identity/identityPcTest.js
@@ -15,16 +15,17 @@ function identityPcTest(remoteOptions) {
   });
   test.setMediaConstraints([{
     audio: true,
     video: true,
     peerIdentity: id2
   }], [remoteOptions || {
     audio: true,
     video: true,
+    fake: true,
     peerIdentity: id1
   }]);
   test.pcLocal.setIdentityProvider('test1.example.com', 'idp.js');
   test.pcRemote.setIdentityProvider('test2.example.com', 'idp.js');
   test.chain.append([
     function PEER_IDENTITY_IS_SET_CORRECTLY(test) {
       // no need to wait to check identity in this case,
       // setRemoteDescription should wait for the IdP to complete
--- a/dom/media/tests/mochitest/identity/test_peerConnection_asymmetricIsolation.html
+++ b/dom/media/tests/mochitest/identity/test_peerConnection_asymmetricIsolation.html
@@ -15,17 +15,18 @@ createHTML({
 });
 
 function theTest() {
   // Override the remote media capture options to remove isolation for the
   // remote party; the test verifies that the media it receives on the local
   // side is isolated anyway.
   identityPcTest({
     audio: true,
-    video: true
+    video: true,
+    fake: true
   });
 }
 runNetworkTest(theTest);
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -145,18 +145,16 @@ skip-if = toolkit == 'gonk' || buildapp 
 [test_peerConnection_setLocalAnswerInStable.html]
 [test_peerConnection_setLocalOfferInHaveRemoteOffer.html]
 [test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html]
 [test_peerConnection_setRemoteAnswerInStable.html]
 [test_peerConnection_setRemoteOfferInHaveLocalOffer.html]
 [test_peerConnection_throwInCallbacks.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_toJSON.html]
-[test_peerConnection_trackDisabling.html]
-
 [test_peerConnection_twoAudioStreams.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' || (android_version == '18' && debug) # b2g (Bug 1059867), android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_twoAudioTracksInOneStream.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' || (android_version == '18' && debug) # b2g (Bug 1059867), android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_twoAudioVideoStreams.html]
 # b2g(Bug 960442, video support for WebRTC is disabled on b2g), Bug 1180000 for Linux debug e10s, android(Bug 1189784, timeouts on 4.3 emulator)
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' || (os == 'linux' && debug && e10s) || android_version == '18'
 [test_peerConnection_twoAudioVideoStreamsCombined.html]
--- a/dom/media/tests/mochitest/test_enumerateDevices.html
+++ b/dom/media/tests/mochitest/test_enumerateDevices.html
@@ -41,34 +41,37 @@ runTest(() =>
     var jsoned = JSON.parse(JSON.stringify(devices));
     is(jsoned[0].kind, devices[0].kind, "kind survived serializer");
     is(jsoned[0].deviceId, devices[0].deviceId, "deviceId survived serializer");
   })
   // Check deviceId failure paths for video.
   .then(() => mustSucceed("unknown plain deviceId on video",
                           () => navigator.mediaDevices.getUserMedia({
     video: { deviceId: "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=" },
+    fake: true,
   })))
   .then(() => mustSucceed("unknown plain deviceId on audio",
                           () => navigator.mediaDevices.getUserMedia({
     audio: { deviceId: "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=" },
+    fake: true,
   })))
   .then(() => mustFailWith("unknown exact deviceId on video",
                            "OverconstrainedError", "deviceId",
                            () => navigator.mediaDevices.getUserMedia({
     video: { deviceId: { exact: "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=" } },
+    fake: true,
   })))
   .then(() => mustFailWith("unknown exact deviceId on audio",
                            "OverconstrainedError", "deviceId",
                            () => navigator.mediaDevices.getUserMedia({
     audio: { deviceId: { exact: "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=" } },
+    fake: true,
   })))
-  // Check the special case of no devices found.
-  .then(() => pushPrefs(["media.navigator.streams.fake", false],
-                        ["media.audio_loopback_dev", "none"],
+  // Check the special case of no devices found (these prefs override fake).
+  .then(() => pushPrefs(["media.audio_loopback_dev", "none"],
                         ["media.video_loopback_dev", "none"]))
   .then(() => navigator.mediaDevices.enumerateDevices())
   .then(devices => ok(devices.length === 0, "No devices found")));
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html
@@ -20,34 +20,30 @@
     const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
     if (IsMacOSX10_6orOlder() || isWinXP) {
         ok(true, "Screensharing disabled for OSX10.6 and WinXP");
         return;
     }
     var testVideo = createMediaElement('video', 'testVideo');
 
     return Promise.resolve()
-      .then(() => getUserMedia({
-        video: { mediaSource: "browser",
-                 scrollWithPage: true },
-        fake: false
-      }))
+      .then(() => getUserMedia({ video: { mediaSource: "browser",
+                                          scrollWithPage: true } }))
       .then(stream => {
         var playback = new LocalMediaStreamPlayback(testVideo, stream);
         return playback.playMediaWithDeprecatedStreamStop(false);
       })
       .then(() => getUserMedia({
         video: {
           mediaSource: "browser",
           viewportOffsetX: 0,
           viewportOffsetY: 0,
           viewportWidth: 100,
           viewportHeight: 100
-        },
-        fake: false
+        }
       }))
       .then(stream => {
         var playback = new LocalMediaStreamPlayback(testVideo, stream);
         playback.startMedia(false);
         return playback.verifyPlaying()
           .then(() => Promise.all([
             () => testVideo.srcObject.getVideoTracks()[0].applyConstraints({
               mediaSource: "browser",
--- a/dom/media/tests/mochitest/test_getUserMedia_constraints.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_constraints.html
@@ -11,23 +11,26 @@ createHTML({ title: "Test getUserMedia c
 /**
   Tests covering gUM constraints API for audio, video and fake video. Exercise
   successful parsing code and ensure that unknown required constraints and
   overconstraining cases produce appropriate errors.
 */
 var tests = [
   // Each test here tests a different constraint or codepath.
   { message: "unknown required constraint on video ignored",
-    constraints: { video: { somethingUnknown: { exact: 0 } } },
+    constraints: { video: { somethingUnknown: { exact: 0 } },
+                   fake: true },
     error: null },
   { message: "unknown required constraint on audio ignored",
-    constraints: { audio: { somethingUnknown: { exact: 0 } } },
+    constraints: { audio: { somethingUnknown: { exact: 0 } },
+                   fake: true },
     error: null },
   { message: "audio overconstrained by facingMode ignored",
-    constraints: { audio: { facingMode: { exact: 'left' } } },
+    constraints: { audio: { facingMode: { exact: 'left' } },
+                   fake: true },
     error: null },
   { message: "full screensharing requires permission",
     constraints: { video: { mediaSource: 'screen' } },
     error: "SecurityError" },
   { message: "application screensharing requires permission",
     constraints: { video: { mediaSource: 'application' } },
     error: "SecurityError" },
   { message: "window screensharing requires permission",
@@ -43,31 +46,32 @@ var tests = [
   { message: "unknown mediaSource in audio fails",
     constraints: { audio: { mediaSource: 'uncle' } },
     error: "OverconstrainedError",
     constraint: "mediaSource" },
   { message: "emtpy constraint fails",
     constraints: { },
     error: "NotSupportedError" },
   { message: "Success-path: optional video facingMode + audio ignoring facingMode",
-    constraints: { audio: { mediaSource: 'microphone',
+    constraints: { fake: true,
+                   audio: { mediaSource: 'microphone',
                             facingMode: 'left',
                             foo: 0,
                             advanced: [{ facingMode: 'environment' },
                                        { facingMode: 'user' },
                                        { bar: 0 }] },
                    video: { mediaSource: 'camera',
                             foo: 0,
                             advanced: [{ facingMode: 'environment' },
                                        { facingMode: ['user'] },
                                        { facingMode: ['left', 'right', 'user'] },
                                        { bar: 0 }] } },
     error: null },
   { message: "legacy facingMode ignored",
-    constraints: { video: { mandatory: { facingMode: 'left' } } },
+    constraints: { video: { mandatory: { facingMode: 'left' } }, fake: true },
     error: null },
 ];
 
 var mustSupport = [
   'width', 'height', 'frameRate', 'facingMode', 'deviceId',
   // Yet to add:
   //  'aspectRatio', 'frameRate', 'volume', 'sampleRate', 'sampleSize',
   //  'echoCancellation', 'latency', 'groupId'
--- a/dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html
@@ -8,17 +8,17 @@
 <pre id="test">
 <script type="application/javascript">
 createHTML({
   title: "Test getUserMedia peerIdentity Constraint",
   bug: "942367"
 });
 function theTest() {
   function testPeerIdentityConstraint(withConstraint) {
-    var config = { audio: true, video: true };
+    var config = { audio: true, video: true, fake: true };
     if (withConstraint) {
       config.peerIdentity = 'user@example.com';
     }
     info('getting media with constraints: ' + JSON.stringify(config));
     return getUserMedia(config)
       .then(stream => Promise.all([
         audioIsSilence(withConstraint, stream),
         videoIsBlack(withConstraint, stream)
--- a/dom/media/tests/mochitest/test_peerConnection_callbacks.html
+++ b/dom/media/tests/mochitest/test_peerConnection_callbacks.html
@@ -60,17 +60,17 @@ var delivered = new Promise(resolve => {
 
 runNetworkTest(function() {
   v1 = createMediaElement('video', 'v1');
   v2 = createMediaElement('video', 'v2');
   var canPlayThrough = new Promise(resolve => v2.canplaythrough = resolve);
   is(v2.currentTime, 0, "v2.currentTime is zero at outset");
 
   // not testing legacy gUM here
-  navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+  navigator.mediaDevices.getUserMedia({ fake: true, video: true, audio: true })
     .then(stream => pc1.addStream(v1.mozSrcObject = stream))
     .then(() => pcall(pc1, pc1.createOffer))
     .then(offer => pcall(pc1, pc1.setLocalDescription, offer))
     .then(() => pcall(pc2, pc2.setRemoteDescription, pc1.localDescription))
     .then(() => pcall(pc2, pc2.createAnswer))
     .then(answer => pcall(pc2, pc2.setLocalDescription, answer))
     .then(() => pcall(pc1, pc1.setRemoteDescription, pc2.localDescription))
     .then(() => delivered)
--- a/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html
+++ b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html
@@ -34,29 +34,27 @@ runNetworkTest(() => {
     }
   ]);
   test.chain.append([
     function FIND_REMOTE_VIDEO() {
       vremote = document.getElementById('pcRemote_remote1_video');
       ok(!!vremote, "Should have remote video element for pcRemote");
     },
     function WAIT_FOR_REMOTE_GREEN() {
-      return h.waitForPixelColor(vremote, h.green, 128,
-                                 "pcRemote's remote should become green");
+      return h.waitForPixel(vremote, h.green, 128, "pcRemote's remote should become green");
     },
     function DRAW_LOCAL_RED() {
       // After requesting a frame it will be captured at the time of next render.
       // Next render will happen at next stable state, at the earliest,
       // i.e., this order of `requestFrame(); draw();` should work.
       stream.requestFrame();
       h.drawColor(canvas, h.red);
     },
     function WAIT_FOR_REMOTE_RED() {
-      return h.waitForPixelColor(vremote, h.red, 128,
-                                 "pcRemote's remote should become red");
+      return h.waitForPixel(vremote, h.red, 128, "pcRemote's remote should become red");
     }
   ]);
   test.run();
 });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html
+++ b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html
@@ -84,31 +84,29 @@ runNetworkTest(() => {
     }
   ]);
   test.chain.append([
     function FIND_REMOTE_VIDEO() {
       vremote = document.getElementById('pcRemote_remote1_video');
       ok(!!vremote, "Should have remote video element for pcRemote");
     },
     function WAIT_FOR_REMOTE_GREEN() {
-      return h.waitForPixelColor(vremote, h.green, 128,
-                                 "pcRemote's remote should become green");
+      return h.waitForPixel(vremote, h.green, 128, "pcRemote's remote should become green");
     },
     function REQUEST_FRAME(test) {
       // After requesting a frame it will be captured at the time of next render.
       // Next render will happen at next stable state, at the earliest,
       // i.e., this order of `requestFrame(); draw();` should work.
       test.pcLocal.canvasStream.requestFrame();
     },
     function DRAW_LOCAL_RED() {
       h.drawColor(canvas, h.red);
     },
     function WAIT_FOR_REMOTE_RED() {
-      return h.waitForPixelColor(vremote, h.red, 128,
-                                 "pcRemote's remote should become red");
+      return h.waitForPixel(vremote, h.red, 128, "pcRemote's remote should become red");
     }
   ]);
   test.run();
 });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
+++ b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
@@ -31,17 +31,17 @@
 
   runNetworkTest(function() {
     v1 = createMediaElement('video', 'v1');
     v2 = createMediaElement('video', 'v2');
     var canPlayThrough = new Promise(resolve => v2.canplaythrough = e => resolve());
 
     is(v2.currentTime, 0, "v2.currentTime is zero at outset");
 
-    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+    navigator.mediaDevices.getUserMedia({ fake: true, video: true, audio: true })
     .then(stream => pc1.addStream(v1.srcObject = stream))
     .then(() => pc1.createOffer({})) // check that createOffer accepts arg.
     .then(offer => pc1.setLocalDescription(offer))
     .then(() => pc2.setRemoteDescription(pc1.localDescription))
     .then(() => pc2.createAnswer({}))  // check that createAnswer accepts arg.
     .then(answer => pc2.setLocalDescription(answer))
     .then(() => pc1.setRemoteDescription(pc2.localDescription))
     .then(() => delivered)
--- a/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
@@ -25,17 +25,17 @@
     var sender = pc.getSenders().find(sn => sn.track.kind == "video");
     var oldTrack = sender.track;
     ok(sender, "We have a sender for video");
     ok(allLocalStreamsHaveSender(pc),
        "Shouldn't have any streams without a corresponding sender");
 
     var newTrack;
     var audiotrack;
-    return navigator.mediaDevices.getUserMedia({video:true, audio:true})
+    return navigator.mediaDevices.getUserMedia({video:true, audio:true, fake:true})
       .then(newStream => {
         window.grip = newStream;
         newTrack = newStream.getVideoTracks()[0];
         audiotrack = newStream.getAudioTracks()[0];
         isnot(newTrack, sender.track, "replacing with a different track");
         ok(!pc.getLocalStreams().some(s => s == newStream),
            "from a different stream");
         return sender.replaceTrack(newTrack);
@@ -107,18 +107,17 @@
         var pc = test.pcLocal._pc;
         var sender = pc.getSenders().find(sn => sn.track.kind == "audio");
         ok(sender, "track has a sender");
         var oldSenderCount = pc.getSenders().length;
         var oldTrack = sender.track;
 
         var sourceNode = test.audioCtx.createOscillator();
         sourceNode.type = 'sine';
-        // We need a frequency not too close to the fake audio track
-        // (440Hz for loopback devices, 1kHz for fake tracks).
+        // We need a frequency not too close to the fake audio track (1kHz).
         sourceNode.frequency.value = 2000;
         sourceNode.start();
 
         var destNode = test.audioCtx.createMediaStreamDestination();
         sourceNode.connect(destNode);
         var newTrack = destNode.stream.getAudioTracks()[0];
 
         return sender.replaceTrack(newTrack)
@@ -153,17 +152,17 @@
         }
         try {
           test.pcLocal._pc.addTrack(track, stream);
           ok(false, "addTrack existing track should fail");
         } catch (e) {
           is(e.name, "InvalidParameterError",
              "addTrack existing track should fail");
         }
-        return navigator.mediaDevices.getUserMedia({video:true})
+        return navigator.mediaDevices.getUserMedia({video:true, fake: true})
           .then(differentStream => {
             var track = differentStream.getVideoTracks()[0];
             try {
               test.pcLocal._pc.addTrack(track, stream);
               ok(false, "addTrack w/wrong stream should fail");
             } catch (e) {
               is(e.name, "InvalidParameterError",
                  "addTrack w/wrong stream should fail");
--- a/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
@@ -16,17 +16,17 @@
     test = new PeerConnectionTest(options);
     test.setMediaConstraints([{video:true}], [{video:true}]);
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_REPLACE_VIDEO_TRACK_THEN_ADD_SECOND_STREAM(test) {
           var oldstream = test.pcLocal._pc.getLocalStreams()[0];
           var oldtrack = oldstream.getVideoTracks()[0];
           var sender = test.pcLocal._pc.getSenders()[0];
-          return navigator.mediaDevices.getUserMedia({video:true})
+          return navigator.mediaDevices.getUserMedia({video:true, fake:true})
             .then(newstream => {
               var newtrack = newstream.getVideoTracks()[0];
               return test.pcLocal.senderReplaceTrack(0, newtrack, newstream.id);
             })
             .then(() => {
               test.setMediaConstraints([{video: true}, {video: true}],
                                        [{video: true}]);
               return test.pcLocal.getAllUserMedia([{video: true}]);
--- a/dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html
+++ b/dom/media/tests/mochitest/test_peerConnection_throwInCallbacks.html
@@ -43,17 +43,17 @@ runNetworkTest(function () {
   let pc0, pc1, pc2;
   // Test failure callbacks (limited to 1 for now)
   pc0 = new RTCPeerConnection();
   pc0.createOffer(getFail(), function(err) {
     pc1 = new RTCPeerConnection();
     pc2 = new RTCPeerConnection();
 
     // Test success callbacks (happy path)
-    navigator.mozGetUserMedia({video:true}, function(video1) {
+    navigator.mozGetUserMedia({video:true, fake: true}, function(video1) {
       pc1.addStream(video1);
       pc1.createOffer(function(offer) {
         pc1.setLocalDescription(offer, function() {
           pc2.setRemoteDescription(offer, function() {
             pc2.createAnswer(function(answer) {
               pc2.setLocalDescription(answer, function() {
                 pc1.setRemoteDescription(answer, function() {
                   throw new Error("Expected");
deleted file mode 100644
--- a/dom/media/tests/mochitest/test_peerConnection_trackDisabling.html
+++ /dev/null
@@ -1,95 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
-</head>
-<body>
-<pre id="test">
-<script type="application/javascript;version=1.8">
-createHTML({
-  bug: "1219711",
-  title: "Disabling locally should be reflected remotely",
-  visible: true
-});
-
-runNetworkTest(() => {
-  var test = new PeerConnectionTest();
-
-  // Always use fake tracks since we depend on video to be somewhat green and
-  // audio to have a large 1000Hz component.
-  test.setMediaConstraints([{audio: true, video: true, fake: true}], []);
-  test.chain.append([
-    function CHECK_ASSUMPTIONS() {
-      is(test.pcLocal.mediaElements.length, 1,
-         "pcLocal should only have one media element");
-      is(test.pcRemote.mediaElements.length, 1,
-         "pcRemote should only have one media element");
-      is(test.pcLocal.streams.length, 1,
-         "pcLocal should only have one stream (the local one)");
-      is(test.pcRemote.streams.length, 1,
-         "pcRemote should only have one stream (the remote one)");
-    },
-    function CHECK_VIDEO() {
-      var h = new CaptureStreamTestHelper2D();
-      var localVideo = test.pcLocal.mediaElements[0];
-      var remoteVideo = test.pcRemote.mediaElements[0];
-      // We check a pixel somewhere away from the top left corner since
-      // MediaEngineDefault puts semi-transparent time indicators there.
-      const offsetX = 50;
-      const offsetY = 50;
-      const threshold = 128;
-      return Promise.resolve()
-        .then(() => info("Checking local video enabled"))
-        .then(() => h.waitForPixel(localVideo, offsetX, offsetY,
-                                   px => h.isPixelNot(px, h.black, 128)))
-        .then(() => info("Checking remote video enabled"))
-        .then(() => h.waitForPixel(remoteVideo, offsetX, offsetY,
-                                   px => h.isPixelNot(px, h.black, 128)))
-
-        .then(() => test.pcLocal.streams[0].getVideoTracks()[0].enabled = false)
-
-        .then(() => info("Checking local video disabled"))
-        .then(() => h.waitForPixel(localVideo, offsetX, offsetY,
-                                   px => h.isPixel(px, h.blackTransparent, 128)))
-        .then(() => info("Checking remote video disabled"))
-        .then(() => h.waitForPixel(remoteVideo, offsetX, offsetY,
-                                   px => h.isPixel(px, h.black, 128)))
-    },
-    function CHECK_AUDIO() {
-      var ac = new AudioContext();
-      var localAnalyser = new AudioStreamAnalyser(ac, test.pcLocal.streams[0]);
-      var remoteAnalyser = new AudioStreamAnalyser(ac, test.pcRemote.streams[0]);
-
-      var checkAudio = (analyser, fun) => {
-        analyser.enableDebugCanvas();
-        return analyser.waitForAnalysisSuccess(fun)
-          .then(() => analyser.disableDebugCanvas());
-      };
-
-      var freq1k = localAnalyser.binIndexForFrequency(1000);
-      var checkAudioEnabled = analyser =>
-        checkAudio(analyser, array => array[freq1k] > 200);
-      var checkAudioDisabled = analyser =>
-        checkAudio(analyser, array => array[freq1k] < 50);
-
-      return Promise.resolve()
-        .then(() => info("Checking local audio enabled"))
-        .then(() => checkAudioEnabled(localAnalyser))
-        .then(() => info("Checking remote audio enabled"))
-        .then(() => checkAudioEnabled(remoteAnalyser))
-
-        .then(() => test.pcLocal.streams[0].getAudioTracks()[0].enabled = false)
-
-        .then(() => info("Checking local audio disabled"))
-        .then(() => checkAudioDisabled(localAnalyser))
-        .then(() => info("Checking remote audio disabled"))
-        .then(() => checkAudioDisabled(remoteAnalyser))
-    }
-  ]);
-  test.run();
-});
-</script>
-</pre>
-</body>
-</html>
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sun Oct 18 17:00:46 PDT 2015
+#Mon Nov 02 13:44:39 GMT 2015
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-all.zip
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -548,16 +548,22 @@ public class BrowserApp extends GeckoApp
         if (AndroidGamepadManager.handleKeyEvent(event)) {
             return true;
         }
         return super.onKeyUp(keyCode, event);
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        if (!isSupportedSDK()) {
+            // This build does not support the Android version of the device; Exit early.
+            super.onCreate(savedInstanceState);
+            return;
+        }
+
         final Intent intent = getIntent();
 
         // Note that we're calling GeckoProfile.get *before GeckoApp.onCreate*.
         // This means we're reliant on the logic in GeckoProfile to correctly
         // look up our launch intent (via BrowserApp's Activity-ness) and pull
         // out the arguments. Be careful if you change that!
         final GeckoProfile p = GeckoProfile.get(this);
 
@@ -1212,16 +1218,22 @@ public class BrowserApp extends GeckoApp
 
     @Override
     public void setAccessibilityEnabled(boolean enabled) {
         mDynamicToolbar.setAccessibilityEnabled(enabled);
     }
 
     @Override
     public void onDestroy() {
+        if (!isSupportedSDK()) {
+            // This build does not support the Android version of the device; Exit early.
+            super.onDestroy();
+            return;
+        }
+
         mDynamicToolbar.destroy();
 
         if (mBrowserToolbar != null)
             mBrowserToolbar.onDestroy();
 
         if (mFindInPageBar != null) {
             mFindInPageBar.onDestroy();
             mFindInPageBar = null;
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -59,16 +59,17 @@ import android.graphics.BitmapFactory;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.location.Location;
 import android.location.LocationListener;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.StrictMode;
 import android.provider.ContactsContract;
 import android.provider.MediaStore.Images.Media;
@@ -1215,16 +1216,24 @@ public abstract class GeckoApp
     public void onCreate(Bundle savedInstanceState) {
         GeckoAppShell.ensureCrashHandling();
 
         // Enable Android Strict Mode for developers' local builds (the "default" channel).
         if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) {
             enableStrictMode();
         }
 
+        if (!isSupportedSDK()) {
+            // This build does not support the Android version of the device: Show an error and finish the app.
+            super.onCreate(savedInstanceState);
+            showSDKVersionError();
+            finish();
+            return;
+        }
+
         // The clock starts...now. Better hurry!
         mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI");
         mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY");
 
         final SafeIntent intent = new SafeIntent(getIntent());
         final String action = intent.getAction();
         final String args = intent.getStringExtra("args");
 
@@ -2128,16 +2137,23 @@ public abstract class GeckoApp
             StrictMode.setThreadPolicy(savedPolicy);
         }
 
         super.onRestart();
     }
 
     @Override
     public void onDestroy() {
+        if (!isSupportedSDK()) {
+            // This build does not support the Android version of the device:
+            // We did not initialize anything, so skip cleaning up.
+            super.onDestroy();
+            return;
+        }
+
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
             "Gecko:Ready",
             "Gecko:DelayedStartup",
             "Gecko:Exited",
             "Accessibility:Event",
             "NativeApp:IsDebuggable");
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener)this,
@@ -2230,16 +2246,26 @@ public abstract class GeckoApp
                   .putExtra(Intent.EXTRA_INTENT, mRestartIntent);
             startService(intent);
         } else {
             // Exiting, so kill our own process.
             Process.killProcess(Process.myPid());
         }
     }
 
+    protected boolean isSupportedSDK() {
+        return Build.VERSION.SDK_INT >= Versions.MIN_SDK_VERSION &&
+               Build.VERSION.SDK_INT <= Versions.MAX_SDK_VERSION;
+    }
+
+    public void showSDKVersionError() {
+        final String message = getString(R.string.unsupported_sdk_version, Build.CPU_ABI, Build.VERSION.SDK_INT);
+        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+    }
+
     // Get a temporary directory, may return null
     public static File getTempDirectory() {
         File dir = GeckoApplication.get().getExternalFilesDir("temp");
         return dir;
     }
 
     // Delete any files in our temporary directory
     public static void deleteTempFiles() {
--- a/mobile/android/base/build.gradle
+++ b/mobile/android/base/build.gradle
@@ -4,16 +4,18 @@ apply plugin: 'com.android.library'
 
 android {
     compileSdkVersion 23
     buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
+        // Used by Robolectric based tests; see TestRunner.
+        buildConfigField 'String', 'BUILD_DIR', "\"${project.buildDir}\""
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
 
     lintOptions {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -743,8 +743,13 @@ just addresses the organization to follo
 <!-- LOCALIZATION NOTE (bookmarks_addons): link title for https://addons.mozilla.org/en-US/mobile -->
 <!ENTITY bookmarks_addons "Firefox: Customize with add-ons">
 <!-- LOCALIZATION NOTE (bookmarks_support): link title for https://support.mozilla.org/ -->
 <!ENTITY bookmarks_support "Firefox: Support">
 <!-- LOCALIZATION NOTE (bookmarks_restricted_support): link title for https://support.mozilla.org/kb/controlledaccess -->
 <!ENTITY bookmarks_restricted_support2 "Firefox Help and Support for restricted profiles on Android tablets">
 <!-- LOCALIZATION NOTE (bookmarks_restricted_webmaker):link title for https://webmaker.org -->
 <!ENTITY bookmarks_restricted_webmaker "Learn the Web: Mozilla Webmaker">
+
+<!-- LOCALIZATION NOTE (unsupported_sdk_version): The user installed a build of this app that does not support
+     the Android version of this device. the formatS1 is replaced by the CPU ABI (e.g., ARMv7); the formatS2 is
+     replaced by the Android OS version (e.g., 14)-->
+<!ENTITY unsupported_sdk_version "Sorry! This &brandShortName; won\'t work on this device (&formatS1;, &formatS2;). Please download the correct version.">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -590,9 +590,11 @@
 
   <string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
 
   <string name="intent_uri_cannot_open">&intent_uri_cannot_open;</string>
   <string name="intent_uri_private_browsing_prompt">&intent_uri_private_browsing_prompt;</string>
   <string name="intent_uri_private_browsing_multiple_match_title">&intent_uri_private_browsing_multiple_match_title;</string>
 
   <string name="devtools_auth_scan_header">&devtools_auth_scan_header;</string>
+
+  <string name="unsupported_sdk_version">&unsupported_sdk_version;</string>
 </resources>
--- a/mobile/android/gradle/base/build.gradle
+++ b/mobile/android/gradle/base/build.gradle
@@ -2,16 +2,18 @@ apply plugin: 'com.android.library'
 
 android {
     compileSdkVersion 23
     buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
+        // Used by Robolectric based tests; see TestRunner.
+        buildConfigField 'String', 'BUILD_DIR', "\"${project.buildDir}\""
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
 
     lintOptions {
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -170,16 +170,18 @@ class ArtifactSubCommand(SubCommand):
 
     def __call__(self, func):
         after = SubCommand.__call__(self, func)
         args = [
             CommandArgument('--tree', metavar='TREE', type=str,
                 help='Firefox tree.'),
             CommandArgument('--job', metavar='JOB', choices=['android-api-11', 'android-x86'],
                 help='Build job.'),
+            CommandArgument('--verbose', '-v', action='store_true',
+                help='Print verbose output.'),
         ]
         for arg in args:
             after = arg(after)
         return after
 
 
 @CommandProvider
 class PackageFrontend(MachCommandBase):
@@ -202,19 +204,20 @@ class PackageFrontend(MachCommandBase):
         to download, cache, and install binary artifacts from Mozilla automation,
         replacing whatever may be in your object directory.  Use |mach artifact last|
         to see what binary artifacts were last used.
 
         Never build libxul again!
         '''
         pass
 
+    def _set_log_level(self, verbose):
+        self.log_manager.terminal_handler.setLevel(logging.INFO if not verbose else logging.DEBUG)
+
     def _make_artifacts(self, tree=None, job=None):
-        self.log_manager.terminal_handler.setLevel(logging.INFO)
-
         self._activate_virtualenv()
         self.virtualenv_manager.install_pip_package('pylru==1.0.9')
         self.virtualenv_manager.install_pip_package('taskcluster==0.0.16')
         self.virtualenv_manager.install_pip_package('mozregression==1.0.2')
 
         state_dir = self._mach_context.state_dir
         cache_dir = os.path.join(state_dir, 'package-frontend')
 
@@ -237,40 +240,44 @@ class PackageFrontend(MachCommandBase):
 
     @ArtifactSubCommand('artifact', 'install',
         'Install a good pre-built artifact.')
     @CommandArgument('source', metavar='SRC', nargs='?', type=str,
         help='Where to fetch and install artifacts from.  Can be omitted, in '
             'which case the current hg repository is inspected; an hg revision; '
             'a remote URL; or a local file.',
         default=None)
-    def artifact_install(self, source=None, tree=None, job=None):
+    def artifact_install(self, source=None, tree=None, job=None, verbose=False):
+        self._set_log_level(verbose)
         tree, job = self._compute_defaults(tree, job)
         artifacts = self._make_artifacts(tree=tree, job=job)
         return artifacts.install_from(source, self.distdir)
 
     @ArtifactSubCommand('artifact', 'last',
         'Print the last pre-built artifact installed.')
-    def artifact_print_last(self, tree=None, job=None):
+    def artifact_print_last(self, tree=None, job=None, verbose=False):
+        self._set_log_level(verbose)
         tree, job = self._compute_defaults(tree, job)
         artifacts = self._make_artifacts(tree=tree, job=job)
         artifacts.print_last()
         return 0
 
     @ArtifactSubCommand('artifact', 'print-cache',
         'Print local artifact cache for debugging.')
-    def artifact_print_cache(self, tree=None, job=None):
+    def artifact_print_cache(self, tree=None, job=None, verbose=False):
+        self._set_log_level(verbose)
         tree, job = self._compute_defaults(tree, job)
         artifacts = self._make_artifacts(tree=tree, job=job)
         artifacts.print_cache()
         return 0
 
     @ArtifactSubCommand('artifact', 'clear-cache',
         'Delete local artifacts and reset local artifact cache.')
-    def artifact_clear_cache(self, tree=None, job=None):
+    def artifact_clear_cache(self, tree=None, job=None, verbose=False):
+        self._set_log_level(verbose)
         tree, job = self._compute_defaults(tree, job)
         artifacts = self._make_artifacts(tree=tree, job=job)
         artifacts.clear_cache()
         return 0
 
 @CommandProvider
 class AndroidEmulatorCommands(MachCommandBase):
     """
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestAccountAuthenticatorStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestAccountAuthenticatorStage.java
@@ -1,43 +1,42 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.io.PrintStream;
-import java.net.URISyntaxException;
-
+import ch.boye.httpclientandroidlib.HttpResponse;
 import org.junit.After;
 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.MockServer;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.setup.auth.AuthenticateAccountStage;
 import org.mozilla.gecko.sync.setup.auth.AuthenticateAccountStage.AuthenticateAccountStageDelegate;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-import ch.boye.httpclientandroidlib.HttpResponse;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URISyntaxException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Tests the authentication request stage of manual Account setup.
  * @author liuche
  *
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestAccountAuthenticatorStage {
   private static final int TEST_PORT      = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
 
   private static final String USERNAME  = "john-hashed";
   private static final String PASSWORD  = "password";
 
   private MockServer authServer;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java
@@ -1,34 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.ProtocolVersion;
-import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
-import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
-import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestBackoff {
   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  = 1201;
 
   /**
    * Test that interpretHTTPFailure calls requestBackoff if
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java
@@ -1,23 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-
+import ch.boye.httpclientandroidlib.Header;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
 
-import ch.boye.httpclientandroidlib.Header;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertEquals;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestBrowserIDAuthHeaderProvider {
   @Test
   public void testHeader() {
     Header header = new BrowserIDAuthHeaderProvider("assertion").getAuthHeader(null, null, null);
 
     assertEquals("authorization", header.getName().toLowerCase());
     assertEquals("BrowserID assertion", header.getValue());
   }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
@@ -1,42 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.io.PrintStream;
-import java.math.BigDecimal;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.List;
-
+import ch.boye.httpclientandroidlib.HttpStatus;
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.ParseException;
 import org.junit.After;
 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.android.sync.test.helpers.MockSyncClientsEngineStage;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.testhelpers.CommandHelpers;
 import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate;
 import org.mozilla.gecko.background.testhelpers.MockClientsDatabaseAccessor;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.CollectionKeys;
 import org.mozilla.gecko.sync.CommandProcessor.Command;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
@@ -47,23 +36,33 @@ import org.mozilla.gecko.sync.crypto.Key
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 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.NullCursorException;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-import ch.boye.httpclientandroidlib.HttpStatus;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
 
-@RunWith(RobolectricGradleTestRunner.class)
+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 TestClientsEngineStage extends MockSyncClientsEngineStage {
   public final static String LOG_TAG = "TestClientsEngSta";
 
   public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException {
     super();
     session = initializeSession();
   }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
@@ -1,34 +1,33 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-
+import ch.boye.httpclientandroidlib.Header;
 import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 
-import ch.boye.httpclientandroidlib.Header;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import static org.junit.Assert.assertEquals;
 
 /**
  * Test the transfer of a UTF-8 string from desktop, and ensure that it results in the
  * correct hashed Basic Auth header.
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestCredentialsEndToEnd {
 
   public static final String REAL_PASSWORD         = "pïgéons1";
   public static final String USERNAME              = "utvm3mk6hnngiir2sp4jsxf2uvoycrv6";
   public static final String DESKTOP_PASSWORD_JSON = "{\"password\":\"pïgéons1\"}";
   public static final String BTOA_PASSWORD         = "cMOvZ8Opb25zMQ==";
   public static final int    DESKTOP_ASSERTED_SIZE = 10;
   public static final String DESKTOP_BASIC_AUTH    = "Basic dXR2bTNtazZobm5naWlyMnNwNGpzeGYydXZveWNydjY6cMOvZ8Opb25zMQ==";
--- 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,42 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-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;
-
-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 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.json.simple.parser.ParseException;
 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.MockResourceDelegate;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.MockAbstractNonRepositorySyncStage;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
 import org.mozilla.gecko.background.testhelpers.MockPrefsGlobalSession;
 import org.mozilla.gecko.background.testhelpers.MockServerSyncStage;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.MetaGlobal;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
@@ -46,26 +36,34 @@ import org.mozilla.gecko.sync.crypto.Key
 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.domain.VersionConstants;
 import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.ProtocolVersion;
-import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
-import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+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;
 
-@RunWith(RobolectricGradleTestRunner.class)
+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;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java
@@ -1,21 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+
+@RunWith(TestRunner.class)
 public class TestHeaderParsing {
 
   @SuppressWarnings("static-method")
   @Test
   public void testDecimalSecondsToMilliseconds() {
     assertEquals(Utils.decimalSecondsToMilliseconds(""),         -1);
     assertEquals(Utils.decimalSecondsToMilliseconds("1234.1.1"), -1);
     assertEquals(Utils.decimalSecondsToMilliseconds("1234"),     1234000);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java
@@ -1,37 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
 
 import java.io.IOException;
 import java.io.PrintStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
-import org.mozilla.android.sync.test.helpers.MockServer;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.sync.net.AuthHeaderProvider;
-import org.mozilla.gecko.sync.net.BaseResource;
-import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
-import org.mozilla.gecko.sync.net.SyncStorageCollectionRequestDelegate;
-import org.mozilla.gecko.sync.net.SyncStorageResponse;
-import org.robolectric.RobolectricGradleTestRunner;
-import org.simpleframework.http.Request;
-import org.simpleframework.http.Response;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestLineByLineHandling {
   private static final int     TEST_PORT   = HTTPServerTestHelper.getTestPort();
   private static final String  TEST_SERVER = "http://localhost:" + TEST_PORT;
   private static final String  LOG_TAG     = "TestLineByLineHandling";
   static String                STORAGE_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/lines";
   private HTTPServerTestHelper data        = new HTTPServerTestHelper();
 
   public ArrayList<String>     lines       = new ArrayList<String>();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java
@@ -1,42 +1,42 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 import org.json.simple.parser.ParseException;
 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.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.MetaGlobal;
 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestMetaGlobal {
   public static Object monitor = new Object();
 
   private static final int    TEST_PORT    = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER  = "http://localhost:" + TEST_PORT;
   private static final String TEST_SYNC_ID = "foobar";
 
   public static final String USER_PASS = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd:password";
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java
@@ -1,35 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+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.MockResourceDelegate;
+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.net.BaseResource;
+import org.mozilla.gecko.sync.net.HttpResponseObserver;
+
+import java.net.URISyntaxException;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import java.net.URISyntaxException;
-
-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.MockResourceDelegate;
-import org.mozilla.android.sync.test.helpers.MockServer;
-import org.mozilla.gecko.background.testhelpers.WaitHelper;
-import org.mozilla.gecko.sync.net.BaseResource;
-import org.mozilla.gecko.sync.net.HttpResponseObserver;
-
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestResource {
   private static final int    TEST_PORT   = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
 
   private HTTPServerTestHelper data     = new HTTPServerTestHelper();
 
   @SuppressWarnings("static-method")
   @Before
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java
@@ -1,27 +1,26 @@
 package org.mozilla.android.sync.net.test;
 
-import java.util.Date;
-
-import org.junit.Test;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.net.SyncResponse;
-
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import ch.boye.httpclientandroidlib.message.BasicStatusLine;
-import org.robolectric.RobolectricGradleTestRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.SyncResponse;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestRetryAfter {
   private int TEST_SECONDS = 120;
 
   @Test
   public void testRetryAfterParsesSeconds() {
     final HttpResponse response = new BasicHttpResponse(
         new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
     response.addHeader("Retry-After", Long.toString(TEST_SECONDS)); // Retry-After given in seconds.
--- 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,24 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import java.net.URI;
-import java.net.URISyntaxException;
-
 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.repositories.Server11Repository;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.net.URI;
+import java.net.URISyntaxException;
+
+@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";
 
   protected final InfoCollections infoCollections = new InfoCollections();
 
   public static void assertQueryEquals(String expected, URI u) {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java
@@ -1,37 +1,37 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-
 import org.json.simple.JSONObject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 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.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
 public class TestSyncStorageRequest {
   private static final int    TEST_PORT   = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
 
   private static final String LOCAL_META_URL  = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/meta/global";
   private static final String LOCAL_BAD_REQUEST_URL  = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/bad";
 
   private static final String EXPECTED_ERROR_CODE = "12";
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java
@@ -1,30 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import java.util.ArrayList;
-import java.util.concurrent.ExecutorService;
-
+import android.content.Context;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.sync.repositories.FetchFailedException;
 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
 import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
 import org.mozilla.gecko.sync.repositories.StoreFailedException;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
-import android.content.Context;
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
 
 public class SynchronizerHelpers {
   public static final String FAIL_SENTINEL = "Fail";
 
   /**
    * Store one at a time, failing if the guid contains FAIL_SENTINEL.
    */
   public static class FailFetchWBORepository extends WBORepository {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java
@@ -1,38 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
+import org.json.simple.JSONArray;
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Set;
-
-import org.json.simple.JSONArray;
-import org.json.simple.parser.ParseException;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.apache.commons.codec.binary.Base64;
-import org.mozilla.gecko.sync.CollectionKeys;
-import org.mozilla.gecko.sync.CryptoRecord;
-import org.mozilla.gecko.sync.NoCollectionKeysSetException;
-import org.mozilla.gecko.sync.NonObjectJSONException;
-import org.mozilla.gecko.sync.crypto.CryptoException;
-import org.mozilla.gecko.sync.crypto.KeyBundle;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestCollectionKeys {
 
   @Test
   public void testDefaultKeys() throws CryptoException, NoCollectionKeysSetException {
     CollectionKeys ck = new CollectionKeys();
     try {
       ck.defaultKeyBundle();
       fail("defaultKeys should throw.");
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java
@@ -1,34 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.CommandRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.json.simple.parser.ParseException;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.CommandProcessor;
-import org.mozilla.gecko.sync.CommandRunner;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.GlobalSession;
-import org.mozilla.gecko.sync.NonObjectJSONException;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestCommandProcessor extends CommandProcessor {
 
   public static final String commandType = "displayURI";
   public static final String commandWithNoArgs = "{\"command\":\"displayURI\"}";
   public static final String commandWithNoType = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",\"PKsljsuqYbGg\"]}";
   public static final String wellFormedCommand = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",\"PKsljsuqYbGg\"],\"command\":\"displayURI\"}";
   public static final String wellFormedCommandWithNullArgs = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",null,\"PKsljsuqYbGg\",null],\"command\":\"displayURI\"}";
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java
@@ -1,40 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.Arrays;
-
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
 import org.mozilla.gecko.sync.repositories.domain.Record;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestCryptoRecord {
   String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYHqeg3KW9+m6Q=";
   String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70=";
 
   @Test
   public void testBaseCryptoRecordEncrypt() throws IOException, ParseException, NonObjectJSONException, CryptoException {
     ExtendedJSONObject clearPayload = ExtendedJSONObject.parseJSONObject("{\"id\":\"5qRsgXWRJZXr\",\"title\":\"Index of file:///Users/jason/Library/Application Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1,\"date\":1319149012372425}]}");
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestJPakeSetup.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestJPakeSetup.java
@@ -1,42 +1,42 @@
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.math.BigInteger;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
 import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.jpake.Gx3OrGx4IsZeroOrOneException;
 import org.mozilla.gecko.sync.jpake.IncorrectZkpException;
 import org.mozilla.gecko.sync.jpake.JPakeClient;
 import org.mozilla.gecko.sync.jpake.JPakeCrypto;
 import org.mozilla.gecko.sync.jpake.JPakeNumGenerator;
 import org.mozilla.gecko.sync.jpake.JPakeNumGeneratorRandom;
 import org.mozilla.gecko.sync.jpake.JPakeParty;
 import org.mozilla.gecko.sync.jpake.stage.ComputeKeyVerificationStage;
 import org.mozilla.gecko.sync.jpake.stage.VerifyPairingStage;
 import org.mozilla.gecko.sync.setup.Constants;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
 public class TestJPakeSetup {
   // Note: will throw NullPointerException if aborts. Only use stateless public
   // methods.
 
   @Test
   public void testGx3OrGx4ZeroOrOneThrowsException()
       throws UnsupportedEncodingException
   {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java
@@ -1,39 +1,39 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.db.Tab;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 import org.mozilla.gecko.sync.repositories.domain.RecordParseException;
 import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.io.IOException;
+import java.util.ArrayList;
 
-@RunWith(RobolectricGradleTestRunner.class)
+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 TestRecord {
 
   @SuppressWarnings("static-method")
   @Test
   public void testQueryRecord() throws NonObjectJSONException, IOException, ParseException {
     final String expectedGUID = "Bl3n3gpKag3s";
     final String testRecord =
         "{\"id\":\"" + expectedGUID + "\"," +
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java
@@ -1,38 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.SynchronizerHelpers.FailFetchWBORepository;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionCreationDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFinishDelegate;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.synchronizer.RecordsChannel;
 import org.mozilla.gecko.sync.synchronizer.RecordsChannelDelegate;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestRecordsChannel {
 
   protected WBORepository remote;
   protected WBORepository local;
 
   protected RepositorySession source;
   protected RepositorySession sink;
   protected RecordsChannelDelegate rcDelegate;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java
@@ -1,50 +1,49 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.util.HashMap;
-
+import android.content.SharedPreferences;
 import org.json.simple.parser.ParseException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
 import org.mozilla.gecko.background.testhelpers.MockPrefsGlobalSession;
 import org.mozilla.gecko.background.testhelpers.MockServerSyncStage;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.CommandProcessor;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.MetaGlobalException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
-import android.content.SharedPreferences;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.io.IOException;
+import java.util.HashMap;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 /**
  * Test that reset commands properly invoke the reset methods on the correct stage.
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestResetCommands {
   private static final String TEST_USERNAME    = "johndoe";
   private static final String TEST_PASSWORD    = "password";
   private static final String TEST_SYNC_KEY    = "abcdeabcdeabcdeabcdeabcdea";
 
   public static void performNotify() {
     WaitHelper.getTestWaiter().performNotify();
   }
--- 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,29 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import ch.boye.httpclientandroidlib.HttpEntity;
 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.MockRecord;
+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.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;
@@ -38,24 +31,30 @@ import org.mozilla.gecko.sync.repositori
 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.repositories.domain.Record;
 import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository;
 import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.ContentType;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-import ch.boye.httpclientandroidlib.HttpEntity;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.atomic.AtomicBoolean;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestServer11RepositorySession {
 
   public class POSTMockServer extends MockServer {
     @Override
     public void handle(Request request, Response response) {
       try {
         String content = request.getContent();
         System.out.println("Content:" + content);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java
@@ -1,41 +1,41 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import java.util.ArrayList;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.SynchronizerHelpers.BatchFailStoreWBORepository;
 import org.mozilla.android.sync.test.SynchronizerHelpers.BeginErrorWBORepository;
 import org.mozilla.android.sync.test.SynchronizerHelpers.BeginFailedException;
 import org.mozilla.android.sync.test.SynchronizerHelpers.FailFetchWBORepository;
 import org.mozilla.android.sync.test.SynchronizerHelpers.FinishErrorWBORepository;
 import org.mozilla.android.sync.test.SynchronizerHelpers.FinishFailedException;
 import org.mozilla.android.sync.test.SynchronizerHelpers.SerialFailStoreWBORepository;
 import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.FetchFailedException;
 import org.mozilla.gecko.sync.repositories.StoreFailedException;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.util.ArrayList;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+@RunWith(TestRunner.class)
 public class TestServerLocalSynchronizer {
   public static final String LOG_TAG = "TestServLocSync";
 
   protected Synchronizer getSynchronizer(WBORepository remote, WBORepository local) {
     BookmarkRecord[] inbounds = new BookmarkRecord[] {
         new BookmarkRecord("inboundSucc1", "bookmarks", 1, false),
         new BookmarkRecord("inboundSucc2", "bookmarks", 1, false),
         new BookmarkRecord("inboundFail1", "bookmarks", 1, false),
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java
@@ -1,24 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import java.net.URI;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.Sync11Configuration;
 import org.mozilla.gecko.sync.SyncConfiguration;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.net.URI;
+
+@RunWith(TestRunner.class)
 public class TestSyncConfiguration {
   @Test
   public void testURLs() throws Exception {
     final MockSharedPreferences prefs = new MockSharedPreferences();
 
     // N.B., the username isn't used in the cluster path.
     SyncConfiguration fxaConfig = new SyncConfiguration("username", null, prefs);
     fxaConfig.clusterURL = new URI("http://db1.oldsync.dev.lcip.org/1.1/174");
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncKeyVerification.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncKeyVerification.java
@@ -1,23 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.fail;
-
 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.setup.InvalidSyncKeyException;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
 public class TestSyncKeyVerification {
 
   private int[] mutateIndices;
   private final String validBasicKey = "abcdefghijkmnpqrstuvwxyz23"; // 26 char, valid characters.
   char[] invalidChars = new char[] { '1', 'l', 'o', '0' };
 
   @Before
   public void setUp() {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java
@@ -1,41 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.util.Date;
-
+import android.content.Context;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSessionDelegate;
 
-import android.content.Context;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.util.Date;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
 public class TestSynchronizer {
   public static final String LOG_TAG = "TestSynchronizer";
 
   public static void assertInRangeInclusive(long earliest, long value, long latest) {
     assertTrue(earliest <= value);
     assertTrue(latest   >= value);
   }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java
@@ -1,44 +1,43 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-
+import android.content.Context;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.SynchronizerHelpers.DataAvailableWBORepository;
 import org.mozilla.android.sync.test.SynchronizerHelpers.ShouldSkipWBORepository;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSessionDelegate;
 
-import android.content.Context;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
 public class TestSynchronizerSession {
   public static final String LOG_TAG = TestSynchronizerSession.class.getSimpleName();
 
   protected static void assertFirstContainsSecond(Map<String, Record> first, Map<String, Record> second) {
     for (Entry<String, Record> entry : second.entrySet()) {
       assertTrue("Expected key " + entry.getKey(), first.containsKey(entry.getKey()));
       Record record = first.get(entry.getKey());
       assertEquals(entry.getValue(), record);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java
@@ -1,29 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+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.SyncConstants;
+import org.mozilla.gecko.sync.Utils;
 
 import java.io.UnsupportedEncodingException;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.SyncConstants;
-import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestUtils extends Utils {
 
   @Test
   public void testGenerateGUID() {
     for (int i = 0; i < 1000; ++i) {
       assertEquals(12, Utils.generateGuid().length());
     }
   }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java
@@ -1,23 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-
 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.SyncStorageRequestDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 
+import java.io.IOException;
+
+import static org.junit.Assert.fail;
+
 public class BaseTestStorageRequestDelegate implements
     SyncStorageRequestDelegate {
 
   protected final AuthHeaderProvider authHeaderProvider;
 
   public BaseTestStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
     this.authHeaderProvider = authHeaderProvider;
   }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java
@@ -1,21 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import java.util.concurrent.ExecutorService;
-
 import junit.framework.AssertionFailedError;
-
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
 
+import java.util.concurrent.ExecutorService;
+
 public class ExpectSuccessRepositorySessionBeginDelegate
 extends ExpectSuccessDelegate
 implements RepositorySessionBeginDelegate {
 
   public ExpectSuccessRepositorySessionBeginDelegate(WaitHelper waitHelper) {
     super(waitHelper);
   }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
 import junit.framework.AssertionFailedError;
-
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 
 public class ExpectSuccessRepositorySessionCreationDelegate extends
     ExpectSuccessDelegate implements RepositorySessionCreationDelegate {
 
   public ExpectSuccessRepositorySessionCreationDelegate(WaitHelper waitHelper) {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java
@@ -1,22 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import java.util.ArrayList;
-import java.util.concurrent.ExecutorService;
-
 import junit.framework.AssertionFailedError;
-
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+
 public class ExpectSuccessRepositorySessionFetchRecordsDelegate extends
     ExpectSuccessDelegate implements RepositorySessionFetchRecordsDelegate {
   public ArrayList<Record> fetchedRecords = new ArrayList<Record>();
 
   public ExpectSuccessRepositorySessionFetchRecordsDelegate(WaitHelper waitHelper) {
     super(waitHelper);
   }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java
@@ -1,22 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import java.util.concurrent.ExecutorService;
-
 import junit.framework.AssertionFailedError;
-
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
 
+import java.util.concurrent.ExecutorService;
+
 public class ExpectSuccessRepositorySessionFinishDelegate extends
     ExpectSuccessDelegate implements RepositorySessionFinishDelegate {
 
   public ExpectSuccessRepositorySessionFinishDelegate(WaitHelper waitHelper) {
     super(waitHelper);
   }
 
   @Override
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
@@ -1,20 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import java.util.concurrent.ExecutorService;
-
 import junit.framework.AssertionFailedError;
-
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
 
+import java.util.concurrent.ExecutorService;
+
 public class ExpectSuccessRepositorySessionStoreDelegate extends
     ExpectSuccessDelegate implements RepositorySessionStoreDelegate {
 
   public ExpectSuccessRepositorySessionStoreDelegate(WaitHelper waitHelper) {
     super(waitHelper);
   }
 
   @Override
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java
@@ -1,20 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import java.util.concurrent.ExecutorService;
-
 import junit.framework.AssertionFailedError;
-
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 
+import java.util.concurrent.ExecutorService;
+
 public class ExpectSuccessRepositoryWipeDelegate extends ExpectSuccessDelegate
     implements RepositorySessionWipeDelegate {
 
   public ExpectSuccessRepositoryWipeDelegate(WaitHelper waitHelper) {
     super(waitHelper);
   }
 
   @Override
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java
@@ -1,26 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import static org.junit.Assert.fail;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.simpleframework.transport.connect.Connection;
+import org.simpleframework.transport.connect.SocketConnection;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.util.IdentityHashMap;
 import java.util.Map;
 
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.sync.net.BaseResource;
-import org.mozilla.gecko.sync.net.BaseResourceDelegate;
-import org.simpleframework.transport.connect.Connection;
-import org.simpleframework.transport.connect.SocketConnection;
+import static org.junit.Assert.fail;
 
 /**
  * Test helper code to bind <code>MockServer</code> instances to ports.
  * <p>
  * Maintains a collection of running servers and (by default) throws helpful
  * errors if two servers are started "on top" of each other. The
  * <b>unchecked</b> exception thrown contains a stack trace pointing to where
  * the new server is being created and where the pre-existing server was
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java
@@ -1,22 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import static org.junit.Assert.assertEquals;
-
-import java.net.URI;
-
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+
 /**
  * A callback for use with a GlobalSession that records what happens for later
  * inspection.
  *
  * This callback is expected to be used from within the friendly confines of a
  * WaitHelper performWait.
  */
 public class MockGlobalSessionCallback implements GlobalSessionCallback {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java
@@ -1,28 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 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.ResourceDelegate;
 
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.client.ClientProtocolException;
-import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
-import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import static org.junit.Assert.assertEquals;
 
 public class MockResourceDelegate implements ResourceDelegate {
   public WaitHelper waitHelper = null;
   public static String USER_PASS    = "john:password";
   public static String EXPECT_BASIC = "Basic am9objpwYXNzd29yZA==";
 
   public boolean handledHttpResponse = false;
   public HttpResponse httpResponse = null;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java
@@ -1,24 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.io.PrintStream;
-
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.Utils;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 import org.simpleframework.http.core.Container;
 
+import java.io.IOException;
+import java.io.PrintStream;
+
+import static org.junit.Assert.assertEquals;
+
 public class MockServer implements Container {
   public static final String LOG_TAG = "MockServer";
 
   public int statusCode = 200;
   public String body = "Hello World";
 
   public MockServer() {
   }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java
@@ -1,19 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers;
 
-import static org.junit.Assert.assertTrue;
-
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
 
+import static org.junit.Assert.assertTrue;
+
 public class MockSyncClientsEngineStage extends SyncClientsEngineStage {
   public class MockClientUploadDelegate extends ClientUploadDelegate {
     HTTPServerTestHelper data;
 
     public MockClientUploadDelegate(HTTPServerTestHelper data) {
       this.data = data;
     }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java
@@ -1,16 +1,16 @@
 package org.mozilla.android.sync.test.helpers;
 
-import java.util.HashMap;
-
 import org.simpleframework.http.Path;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
+import java.util.HashMap;
+
 /**
  * A trivial server that collects and returns WBOs.
  *
  * @author rnewman
  *
  */
 public class MockWBOServer extends MockServer {
   public HashMap<String, HashMap<String, String> > collections;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java
@@ -1,30 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test.helpers.test;
 
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper.HTTPServerAlreadyRunningError;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
-import org.mozilla.android.sync.test.helpers.MockServer;
-import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper.HTTPServerAlreadyRunningError;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestHTTPServerTestHelper {
   public static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
 
   protected MockServer mockServer = new MockServer();
 
   @Test
   public void testStartStop() {
     // Need to be able to start and stop multiple times.
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java
@@ -1,37 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.common.log.writers.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-
+import android.util.Log;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.common.log.writers.LevelFilteringLogWriter;
 import org.mozilla.gecko.background.common.log.writers.LogWriter;
 import org.mozilla.gecko.background.common.log.writers.PrintLogWriter;
 import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter;
 import org.mozilla.gecko.background.common.log.writers.StringLogWriter;
 import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 
-import android.util.Log;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestLogWriters {
 
   public static final String TEST_LOG_TAG_1 = "TestLogTag1";
   public static final String TEST_LOG_TAG_2 = "TestLogTag2";
 
   public static final String TEST_MESSAGE_1  = "LOG TEST MESSAGE one";
   public static final String TEST_MESSAGE_2  = "LOG TEST MESSAGE two";
   public static final String TEST_MESSAGE_3  = "LOG TEST MESSAGE three";
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountAgeLockoutHelper.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountAgeLockoutHelper.java
@@ -1,23 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.fxa.test;
 
-import java.util.Calendar;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.util.Calendar;
+
+@RunWith(TestRunner.class)
 public class TestFxAccountAgeLockoutHelper {
   @Test
   public void testPassesAgeCheck() {
     Calendar today = Calendar.getInstance();
     int birthMonthIndex = today.get(Calendar.MONTH);
     int birthDate = today.get(Calendar.DATE);
     int birthYear = today.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
     Assert.assertTrue("Minimum age as of today",
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java
@@ -1,27 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.fxa.test;
 
+import junit.framework.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.BaseResource;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
-import junit.framework.Assert;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.background.fxa.FxAccountClient20;
-import org.mozilla.gecko.sync.net.BaseResource;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestFxAccountClient20 {
   protected static class MockFxAccountClient20 extends FxAccountClient20 {
     public MockFxAccountClient20(String serverURI, Executor executor) {
       super(serverURI, executor);
     }
 
     // Public for testing.
     @Override
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java
@@ -1,32 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.fxa.test;
 
-import java.math.BigInteger;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.net.SRPConstants;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.math.BigInteger;
 
 /**
  * Test vectors from
  * <a href="https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#stretch-KDF">https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#stretch-KDF</a>
  * and
  * <a href="https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol/5a9bc81e499306d769ca19b40b50fa60123df15d">https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol/5a9bc81e499306d769ca19b40b50fa60123df15d</a>.
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestFxAccountUtils {
   protected static void assertEncoding(String base16String, String utf8String) throws Exception {
     Assert.assertEquals(base16String, FxAccountUtils.bytes(utf8String));
   }
 
   @Test
   public void testUTF8Encoding() throws Exception {
     assertEncoding("616e6472c3a9406578616d706c652e6f7267", "andré@example.org");
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/prune/test/TestPrunePolicy.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/prune/test/TestPrunePolicy.java
@@ -1,29 +1,28 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.healthreport.prune.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
+import android.content.SharedPreferences;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.healthreport.HealthReportConstants;
 import org.mozilla.gecko.background.healthreport.prune.PrunePolicy;
 import org.mozilla.gecko.background.healthreport.prune.PrunePolicyStorage;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 
-import android.content.SharedPreferences;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestPrunePolicy {
   public static class MockPrunePolicy extends PrunePolicy {
     public MockPrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPrefs) {
       super(storage, sharedPrefs);
     }
 
     @Override
     public boolean attemptPruneBySize(final long time) {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/test/HealthReportStorageStub.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/test/HealthReportStorageStub.java
@@ -1,18 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.healthreport.test;
 
-import org.json.JSONObject;
-
 import android.database.Cursor;
 import android.util.SparseArray;
-
+import org.json.JSONObject;
 import org.mozilla.gecko.background.healthreport.Environment;
 import org.mozilla.gecko.background.healthreport.HealthReportStorage;
 
 public class HealthReportStorageStub implements HealthReportStorage {
   public void close() { throw new UnsupportedOperationException(); }
 
   public int getDay(long time) { throw new UnsupportedOperationException(); }
   public int getDay() { throw new UnsupportedOperationException(); }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/MockAndroidSubmissionClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/MockAndroidSubmissionClient.java
@@ -1,16 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.healthreport.upload.test;
 
 import android.content.Context;
 import android.content.SharedPreferences;
-
 import org.mozilla.gecko.background.healthreport.Environment;
 import org.mozilla.gecko.background.healthreport.Environment.UIType;
 import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
 import org.mozilla.gecko.background.healthreport.HealthReportStorage;
 import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
 import org.mozilla.gecko.background.healthreport.test.HealthReportStorageStub;
 import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestObsoleteDocumentTracker.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestObsoleteDocumentTracker.java
@@ -1,37 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.healthreport.upload.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import android.content.SharedPreferences;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.healthreport.HealthReportConstants;
+import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
 
 import java.util.AbstractMap.SimpleImmutableEntry;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Set;
 
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.background.healthreport.HealthReportConstants;
-import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker;
-import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
-import android.content.SharedPreferences;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestObsoleteDocumentTracker {
   public static class MockObsoleteDocumentTracker extends ObsoleteDocumentTracker {
     public MockObsoleteDocumentTracker(SharedPreferences sharedPrefs) {
       super(sharedPrefs);
     }
 
     @Override
     public ExtendedJSONObject getObsoleteIds() {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestSubmissionPolicy.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestSubmissionPolicy.java
@@ -1,37 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.healthreport.upload.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.net.UnknownHostException;
-import java.util.Collection;
-import java.util.HashSet;
-
+import android.content.SharedPreferences;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.healthreport.HealthReportConstants;
 import org.mozilla.gecko.background.healthreport.upload.SubmissionClient;
 import org.mozilla.gecko.background.healthreport.upload.SubmissionPolicy;
 import org.mozilla.gecko.background.healthreport.upload.test.TestObsoleteDocumentTracker.MockObsoleteDocumentTracker;
 import org.mozilla.gecko.background.healthreport.upload.test.TestSubmissionPolicy.MockSubmissionClient.Response;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 
-import android.content.SharedPreferences;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.HashSet;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestSubmissionPolicy {
   public static class MockSubmissionClient implements SubmissionClient {
     public String lastId = null;
     public Collection<String> lastOldIds = null;
 
     public enum Response { SUCCESS, SOFT_FAILURE, HARD_FAILURE };
     public Response upload = Response.SUCCESS;
     public Response delete = Response.SUCCESS;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestSubmissionsTracker.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestSubmissionsTracker.java
@@ -1,30 +1,28 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.healthreport.upload.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient.SubmissionsFieldName;
+import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockHealthReportStorage;
+import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockSubmissionsTracker;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 
 import java.util.Arrays;
 import java.util.HashSet;
 
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient.SubmissionsFieldName;
-import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient;
-import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockHealthReportStorage;
-import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockSubmissionsTracker;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestSubmissionsTracker {
   protected static class MockHealthReportStorage2 extends MockHealthReportStorage {
     public final int FIRST_ATTEMPT_ID = SubmissionsFieldName.FIRST_ATTEMPT.getID(this);
     public final int CONTINUATION_ATTEMPT_ID = SubmissionsFieldName.CONTINUATION_ATTEMPT.getID(this);
 
     public final int SUCCESS_ID = SubmissionsFieldName.SUCCESS.getID(this);
     public final int CLIENT_FAILURE_ID = SubmissionsFieldName.CLIENT_FAILURE.getID(this);
     public final int TRANSPORT_FAILURE_ID = SubmissionsFieldName.TRANSPORT_FAILURE.getID(this);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestTrackingRequestDelegate.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestTrackingRequestDelegate.java
@@ -1,32 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.healthreport.upload.test;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.healthreport.HealthReportStorage;
+import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient.SubmissionsTracker.TrackingRequestDelegate;
+import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockHealthReportStorage;
+import org.mozilla.gecko.background.testhelpers.StubDelegate;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
 import java.util.HashSet;
 
-import org.junit.Before;
-import org.junit.Test;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.background.healthreport.HealthReportStorage;
-import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient.SubmissionsTracker.TrackingRequestDelegate;
-import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient;
-import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockHealthReportStorage;
-import org.mozilla.gecko.background.testhelpers.StubDelegate;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestTrackingRequestDelegate {
   public static class MockAndroidSubmissionClient2 extends MockAndroidSubmissionClient {
     public MockAndroidSubmissionClient2() {
       super(null, null, null);
     }
 
     @Override
     public void setLastUploadLocalTimeAndDocumentId(long localTime, String id) { /* Do nothing. */ }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java
@@ -1,20 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.test;
 
+import ch.boye.httpclientandroidlib.HttpEntity;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 
-import ch.boye.httpclientandroidlib.HttpEntity;
-
 public class EntityTestHelper {
   private static final int DEFAULT_SIZE = 1024;
 
   public static byte[] bytesFromEntity(final HttpEntity entity) throws IOException {
     final InputStream is = entity.getContent();
 
     if (is instanceof ByteArrayInputStream) {
       final int size = is.available();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/TestBoundedByteArrayEntity.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/TestBoundedByteArrayEntity.java
@@ -1,25 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.test;
 
-import java.io.IOException;
-import java.util.Arrays;
-
+import ch.boye.httpclientandroidlib.HttpEntity;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.bagheera.BoundedByteArrayEntity;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 
-import ch.boye.httpclientandroidlib.HttpEntity;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.io.IOException;
+import java.util.Arrays;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestBoundedByteArrayEntity {
   private static void expectFail(byte[] input, int start, int end) {
     try {
       new BoundedByteArrayEntity(input, start, end);
       Assert.fail("Should have thrown.");
     } catch (Exception ex) {
       return;
     }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/TestDeflation.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/TestDeflation.java
@@ -1,28 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.test;
 
+import ch.boye.httpclientandroidlib.HttpEntity;
+import junit.framework.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.bagheera.DeflateHelper;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
 import java.util.Arrays;
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
 
-import junit.framework.Assert;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.background.bagheera.DeflateHelper;
-import org.mozilla.gecko.background.common.log.Logger;
-
-import ch.boye.httpclientandroidlib.HttpEntity;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestDeflation {
   public static final String TEST_BODY_A = "";
   public static final String TEST_BODY_B = "éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{";
   public static final String TEST_BODY_C = "{}\n";
   public static final String TEST_BODY_D =
       "{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{éíôü}A" +
       "BCDEFGHaaQRSTUVWXYZá{Zá{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{éíôü}ABCDEFGH" +
       "aQRSTUVWXYZá{Zá{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{}\n";
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
@@ -1,24 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
-
 import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
 import org.mozilla.gecko.sync.repositories.RecordFactory;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.stage.ServerSyncStage;
 
+import java.io.IOException;
+import java.net.URISyntaxException;
+
 /**
  * A stage that joins two Repositories with no wrapping.
  */
 public abstract class BaseMockServerSyncStage extends ServerSyncStage {
 
   public Repository local;
   public Repository remote;
   public String name;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java
@@ -1,19 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.net.URI;
-
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
+import java.net.URI;
+
 public class DefaultGlobalSessionCallback implements GlobalSessionCallback {
 
   @Override
   public void requestBackoff(long backoff) {
   }
 
   @Override
   public boolean wantNodeAssignment() {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/JPakeNumGeneratorFixed.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/JPakeNumGeneratorFixed.java
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.math.BigInteger;
+import org.mozilla.gecko.sync.jpake.JPakeNumGenerator;
 
-import org.mozilla.gecko.sync.jpake.JPakeNumGenerator;
+import java.math.BigInteger;
 
 public class JPakeNumGeneratorFixed implements JPakeNumGenerator {
   private String[] values;
   private int index = 0;
 
   public JPakeNumGeneratorFixed(String[] values) {
     this.values = values;
   }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java
@@ -1,22 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
 import org.mozilla.gecko.sync.CommandProcessor.Command;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
 public class MockClientsDatabaseAccessor extends ClientsDatabaseAccessor {
   public boolean storedRecord = false;
   public boolean dbWiped = false;
   public boolean clientsTableWiped = false;
   public boolean closed = false;
   public boolean storedArrayList = false;
   public boolean storedCommand;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
@@ -1,28 +1,28 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.io.IOException;
-import java.util.HashMap;
-
 import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.CompletedStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
+import java.io.IOException;
+import java.util.HashMap;
+
 
 public class MockGlobalSession extends MockPrefsGlobalSession {
 
   public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException {
     this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback);
   }
 
   public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback)
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
@@ -1,28 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.io.IOException;
-
+import android.content.Context;
+import android.content.SharedPreferences;
 import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 
-import android.content.Context;
-import android.content.SharedPreferences;
+import java.io.IOException;
 
 /**
  * GlobalSession touches the Android prefs system. Stub that out.
  */
 public class MockPrefsGlobalSession extends GlobalSession {
 
   public MockSharedPreferences prefs;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java
@@ -1,12 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-
 public class MockServerSyncStage extends BaseMockServerSyncStage {
   @Override
   public void execute() {
     session.advance();
   }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java
@@ -0,0 +1,138 @@
+/**
+ * The MIT License
+ * 
+ * Copyright (c) 2010 Xtreme Labs and Pivotal Labs
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package org.mozilla.gecko.background.testhelpers;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.manifest.AndroidManifest;
+import org.robolectric.res.FileFsFile;
+import org.robolectric.res.FsFile;
+import org.robolectric.util.Logger;
+import org.robolectric.util.ReflectionHelpers;
+
+/**
+ * Test runner customized for running unit tests either through the Gradle CLI or
+ * Android Studio. The runner uses the build type and build flavor to compute the
+ * resource, asset, and AndroidManifest paths.
+ *
+ * This test runner requires that you set the 'constants' field on the @Config
+ * annotation (or the org.robolectric.Config.properties file) for your tests.
+ *
+ * This is a modified version of
+ * https://github.com/robolectric/robolectric/blob/8676da2daa4c140679fb5903696b8191415cec8f/robolectric/src/main/java/org/robolectric/RobolectricGradleTestRunner.java
+ * that uses a Gradle `buildConfigField` to find build outputs.
+ * See https://github.com/robolectric/robolectric/issues/1648#issuecomment-113731011.
+ */
+public class TestRunner extends RobolectricGradleTestRunner {
+    private FsFile buildFolder;
+
+    public TestRunner(Class<?> klass) throws InitializationError {
+        super(klass);
+    }
+
+    @Override
+    protected AndroidManifest getAppManifest(Config config) {
+        if (config.constants() == Void.class) {
+            Logger.error("Field 'constants' not specified in @Config annotation");
+            Logger.error("This is required when using RobolectricGradleTestRunner!");
+            throw new RuntimeException("No 'constants' field in @Config annotation!");
+        }
+
+        buildFolder = FileFsFile.from(getBuildDir(config)).join("intermediates");
+
+        final String type = getType(config);
+        final String flavor = getFlavor(config);
+        final String packageName = getPackageName(config);
+
+        final FsFile res;
+        final FsFile assets;
+        final FsFile manifest;
+
+        if (areResourcesFromLibrary()) {
+            FsFile bundlesFolder = buildFolder.join("bundles", flavor, type);
+            res = bundlesFolder.join("res");
+            assets = bundlesFolder.join("assets");
+            manifest = bundlesFolder.join("AndroidManifest.xml");
+        } else {
+            if (buildFolder.join("res", "merged").exists()) {
+                res = buildFolder.join("res", "merged", flavor, type);
+            } else if(buildFolder.join("res").exists()) {
+                res = buildFolder.join("res", flavor, type);
+            } else {
+                throw new IllegalStateException("No resource folder found");
+            }
+            assets = buildFolder.join("assets", flavor, type);
+            manifest = buildFolder.join("manifests", "full", flavor, type, "AndroidManifest.xml");
+        }
+
+        Logger.debug("Robolectric assets directory: " + assets.getPath());
+        Logger.debug("   Robolectric res directory: " + res.getPath());
+        Logger.debug("   Robolectric manifest path: " + manifest.getPath());
+        Logger.debug("    Robolectric package name: " + packageName);
+        return new AndroidManifest(manifest, res, assets, packageName);
+    }
+
+    private boolean areResourcesFromLibrary() {
+        return buildFolder.join("bundles").exists();
+    }
+
+    private static String getType(Config config) {
+        try {
+            return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE");
+        } catch (Throwable e) {
+            return null;
+        }
+    }
+
+    private static String getFlavor(Config config) {
+        try {
+            return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR");
+        } catch (Throwable e) {
+            return null;
+        }
+    }
+
+    private static String getPackageName(Config config) {
+        try {
+            final String packageName = config.packageName();
+            if (packageName != null && !packageName.isEmpty()) {
+                return packageName;
+            } else {
+                return ReflectionHelpers.getStaticField(config.constants(), "APPLICATION_ID");
+            }
+        } catch (Throwable e) {
+            return null;
+        }
+    }
+
+    private String getBuildDir(Config config) {
+        try {
+            return ReflectionHelpers.getStaticField(config.constants(), "BUILD_DIR");
+        } catch (Throwable e) {
+            return null;
+        }
+    }
+}
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java
@@ -1,34 +1,33 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.util.Map.Entry;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
+import android.content.Context;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
 import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
 import org.mozilla.gecko.sync.repositories.RecordFilter;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
-import android.content.Context;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 public class WBORepository extends Repository {
 
   public class WBORepositoryStats {
     public long created         = -1;
     public long begun           = -1;
     public long fetchBegan      = -1;
     public long fetchCompleted  = -1;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
@@ -1,19 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
+import org.mozilla.gecko.background.common.log.Logger;
+
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
 
-import org.mozilla.gecko.background.common.log.Logger;
-
 /**
  * Implements waiting for asynchronous test events.
  *
  * Call WaitHelper.getTestWaiter() to get the unique instance.
  *
  * Call performWait(runnable) to execute runnable synchronously.
  * runnable *must* call performNotify() on all exit paths to signal to
  * the TestWaiter that the runnable has completed.
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java
@@ -1,23 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.browserid.test;
 
-import java.math.BigInteger;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.browserid.ASNUtils;
 import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
 public class TestASNUtils {
   public void doTestEncodeDecodeArrays(int length1, int length2) {
     if (4 + length1 + length2 > 127) {
       throw new IllegalArgumentException("Total length must be < 128 - 4.");
     }
     byte[] first = Utils.generateRandomBytes(length1);
     byte[] second = Utils.generateRandomBytes(length2);
     byte[] encoded = ASNUtils.encodeTwoArraysToASN1(first, second);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java
@@ -1,25 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.browserid.test;
 
-import java.math.BigInteger;
-
 import junit.framework.Assert;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.DSACryptoImplementation;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
 public class TestDSACryptoImplementation {
   @Test
   public void testToJSONObject() throws Exception {
     BigInteger p = new BigInteger("fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17", 16);
     BigInteger q = new BigInteger("962eddcc369cba8ebb260ee6b6a126d9346e38c5", 16);
     BigInteger g = new BigInteger("678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4", 16);
     BigInteger x = new BigInteger("9516d860392003db5a4f168444903265467614db", 16);
     BigInteger y = new BigInteger("455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0", 16);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java
@@ -1,29 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.browserid.test;
 
-import java.math.BigInteger;
-import java.security.GeneralSecurityException;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.DSACryptoImplementation;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.browserid.SigningPrivateKey;
 import org.mozilla.gecko.browserid.VerifyingPublicKey;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+
+@RunWith(TestRunner.class)
 public class TestJSONWebTokenUtils {
   public void doTestEncodeDecode(BrowserIDKeyPair keyPair) throws Exception {
     SigningPrivateKey privateKey = keyPair.getPrivate();
     VerifyingPublicKey publicKey = keyPair.getPublic();
 
     ExtendedJSONObject o = new ExtendedJSONObject();
     o.put("key", "value");
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java
@@ -1,25 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.browserid.test;
 
-import java.math.BigInteger;
-
 import junit.framework.Assert;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
 public class TestRSACryptoImplementation {
   @Test
   public void testToJSONObject() throws Exception {
     BigInteger n = new BigInteger("7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577");
     BigInteger e = new BigInteger("65537");
     BigInteger d = new BigInteger("2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205");
 
     BrowserIDKeyPair keyPair = new BrowserIDKeyPair(
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java
@@ -1,24 +1,25 @@
 /* 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.fxa;
 
-import org.mozilla.gecko.background.fxa.SkewHandler;
-import org.mozilla.gecko.sync.net.BaseResource;
 import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricGradleTestRunner;
+import org.mozilla.gecko.background.fxa.SkewHandler;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.BaseResource;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestSkewHandler {
   public TestSkewHandler() {
   }
 
   @Test
   public void testSkewUpdating() throws Throwable {
     SkewHandler h = new SkewHandler("foo.com");
     assertEquals(0L, h.getSkewInSeconds());
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
@@ -1,36 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.fxa.login;
 
-import java.io.UnsupportedEncodingException;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
 import org.mozilla.gecko.browserid.MockMyIDTokenFactory;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
-import ch.boye.httpclientandroidlib.HttpStatus;
-import ch.boye.httpclientandroidlib.ProtocolVersion;
-import ch.boye.httpclientandroidlib.entity.StringEntity;
-import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 
 public class MockFxAccountClient implements FxAccountClient {
   protected static MockMyIDTokenFactory mockMyIdTokenFactory = new MockMyIDTokenFactory();
 
   public final String serverURI = "http://testServer.com";
 
   public final Map<String, User> users = new HashMap<String, User>();
   public final Map<String, String> sessionTokens = new HashMap<String, String>();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java
@@ -1,34 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.fxa.login;
 
-import java.security.NoSuchAlgorithmException;
-import java.util.LinkedList;
-
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mozilla.gecko.BuildConfig;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
-import org.robolectric.annotation.Config;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.security.NoSuchAlgorithmException;
+import java.util.LinkedList;
+
+@RunWith(TestRunner.class)
 public class TestFxAccountLoginStateMachine {
   // private static final String TEST_AUDIENCE = "http://testAudience.com";
   private static final String TEST_EMAIL = "test@test.com";
   private static byte[] TEST_EMAIL_UTF8;
   private static final String TEST_PASSWORD = "testtest";
   private static byte[] TEST_PASSWORD_UTF8;
   private static byte[] TEST_QUICK_STRETCHED_PW;
   private static byte[] TEST_UNWRAPKB;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java
@@ -1,27 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.fxa.login;
 
 import junit.framework.Assert;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mozilla.gecko.BuildConfig;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.DSACryptoImplementation;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
-import org.robolectric.annotation.Config;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestStateFactory {
   @Test
   public void testGetStateV3() throws Exception {
     MigratedFromSync11 migrated = new MigratedFromSync11("email", "uid", true, "password");
 
     // For the current version, we expect to read back what we wrote.
     ExtendedJSONObject o;
     State state;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java
@@ -1,26 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.crypto.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base32;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
 
 import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.apache.commons.codec.binary.Base32;
-import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestBase32 {
 
   public static void assertSame(byte[] arrayOne, byte[] arrayTwo) {
     assertTrue(Arrays.equals(arrayOne, arrayTwo));
   }
 
   @Test
   public void testBase32() throws UnsupportedEncodingException {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java
@@ -1,31 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.crypto.test;
 
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.CryptoInfo;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
-import java.io.UnsupportedEncodingException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.apache.commons.codec.binary.Base64;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.sync.crypto.CryptoException;
-import org.mozilla.gecko.sync.crypto.CryptoInfo;
-import org.mozilla.gecko.sync.crypto.KeyBundle;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestCryptoInfo {
 
   @Test
   public void testEncryptedHMACIsSet() throws CryptoException, UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
     KeyBundle kb = KeyBundle.withRandomKeys();
     CryptoInfo encrypted = CryptoInfo.encrypt("plaintext".getBytes("UTF-8"), kb);
     assertSame(kb, encrypted.getKeys());
     assertTrue(encrypted.generatedHMACIsHMAC());
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java
@@ -1,32 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.crypto.test;
 
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.HKDF;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
 
 import java.util.Arrays;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.apache.commons.codec.binary.Base64;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.sync.crypto.HKDF;
-import org.mozilla.gecko.sync.crypto.KeyBundle;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /*
  * This class tests the HKDF.java class.
  * The tests are the 3 HMAC-based test cases
  * from the RFC 5869 specification.
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestHKDF {
   @Test
   public void testCase1() {
     String IKM  = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b";
     String salt = "000102030405060708090a0b0c";
     String info = "f0f1f2f3f4f5f6f7f8f9";
     int L       = 42;
     String PRK  = "077709362c2e32df0ddc3f0dc47bba63" +
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java
@@ -1,29 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.crypto.test;
 
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
 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 java.io.UnsupportedEncodingException;
-import java.util.Arrays;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.apache.commons.codec.binary.Base64;
-import org.mozilla.gecko.sync.crypto.CryptoException;
-import org.mozilla.gecko.sync.crypto.KeyBundle;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestKeyBundle {
   @Test
   public void testCreateKeyBundle() throws UnsupportedEncodingException, CryptoException {
     String username              = "smqvooxj664hmrkrv6bw4r4vkegjhkns";
     String friendlyBase32SyncKey = "gbh7teqqcgyzd65svjgibd7tqy";
     String base64EncryptionKey   = "069EnS3EtDK4y1tZ1AyKX+U7WEsWRp9b" +
                                    "RIKLdW/7aoE=";
     String base64HmacKey         = "LF2YCS1QCgSNCf0BCQvQ06SGH8jqJDi9" +
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java
@@ -1,34 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.crypto.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.PBKDF2;
 
 import java.io.UnsupportedEncodingException;
 import java.security.GeneralSecurityException;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.sync.crypto.PBKDF2;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 /**
  * Test PBKDF2 implementations against vectors from
  * <dl>
  * <dt>SHA-256</dt>
  * <dd><a href="https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors">https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors</a></dd>
  * <dd><a href="https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c">https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c</a></dd>
  * </dl>
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestPBKDF2 {
 
   @Test
   public final void testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException {
     String  p = "password";
     String  s = "salt";
     int dkLen = 32;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java
@@ -1,30 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.crypto.test;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
-import org.mozilla.gecko.sync.CollectionKeys;
-import org.mozilla.gecko.sync.NoCollectionKeysSetException;
-import org.mozilla.gecko.sync.crypto.CryptoException;
-import org.mozilla.gecko.sync.crypto.KeyBundle;
-import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestPersistedCrypto5Keys {
   MockSharedPreferences prefs = null;
 
   @Before
   public void setUp() {
     prefs = new MockSharedPreferences();
   }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java
@@ -1,22 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.crypto.test;
 
-import java.math.BigInteger;
-
 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.net.SRPConstants;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.math.BigInteger;
+
+@RunWith(TestRunner.class)
 public class TestSRPConstants extends SRPConstants {
   public void assertSRPConstants(SRPConstants.Parameters params, int bitLength) {
     Assert.assertNotNull(params.g);
     Assert.assertNotNull(params.N);
     Assert.assertEquals(bitLength, bitLength);
     Assert.assertEquals(bitLength / 8, params.byteLength);
     Assert.assertEquals(bitLength / 4, params.hexLength);
     BigInteger N = params.N;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java
@@ -1,51 +1,50 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.middleware.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-
 import junit.framework.AssertionFailedError;
-
 import org.json.simple.parser.ParseException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionBeginDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionCreationDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFetchRecordsDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFinishDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionStoreDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositoryWipeDelegate;
 import org.mozilla.gecko.background.testhelpers.MockRecord;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository;
 import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepositorySession;
 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
 import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.repositories.domain.Record;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestCrypto5MiddlewareRepositorySession {
   public static WaitHelper getTestWaiter() {
     return WaitHelper.getTestWaiter();
   }
 
   public static void performWait(Runnable runnable) {
     getTestWaiter().performWait(runnable);
   }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java
@@ -1,31 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpPost;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.HMACAuthHeaderProvider;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.net.HMACAuthHeaderProvider;
+import static org.junit.Assert.assertEquals;
 
-import ch.boye.httpclientandroidlib.client.methods.HttpGet;
-import ch.boye.httpclientandroidlib.client.methods.HttpPost;
-import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
-import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestHMACAuthHeaderProvider {
   // Expose a few protected static member functions as public for testing.
   protected static class LeakyHMACAuthHeaderProvider extends HMACAuthHeaderProvider {
     public LeakyHMACAuthHeaderProvider(String identifier, String key) {
       super(identifier, key);
     }
 
     public static String getRequestString(HttpUriRequest request, long timestampInSeconds, String nonce, String extra) {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java
@@ -1,41 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
-
 import ch.boye.httpclientandroidlib.Header;
 import ch.boye.httpclientandroidlib.client.methods.HttpGet;
 import ch.boye.httpclientandroidlib.client.methods.HttpPost;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
 import ch.boye.httpclientandroidlib.entity.StringEntity;
 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
 import ch.boye.httpclientandroidlib.message.BasicHeader;
 import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
-import org.robolectric.RobolectricGradleTestRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import static org.junit.Assert.assertEquals;
 
 /**
  * These test vectors were taken from
  * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/README.md">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/README.md</a>.
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestHawkAuthHeaderProvider {
   // Expose a few protected static member functions as public for testing.
   protected static class LeakyHawkAuthHeaderProvider extends HawkAuthHeaderProvider {
     public LeakyHawkAuthHeaderProvider(String tokenId, byte[] reqHMACKey) {
       // getAuthHeader takes includePayloadHash as a parameter.
       super(tokenId, reqHMACKey, false, 0L);
     }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java
@@ -1,38 +1,37 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.net.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.security.GeneralSecurityException;
-
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
 import org.junit.Assert;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.Resource;
 import org.mozilla.gecko.sync.net.SyncResponse;
 
-import ch.boye.httpclientandroidlib.Header;
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.client.ClientProtocolException;
-import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
-import ch.boye.httpclientandroidlib.entity.StringEntity;
-import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
-import ch.boye.httpclientandroidlib.message.BasicHeader;
-import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 
 public class TestLiveHawkAuth {
   /**
    * Hawk comes with an example/usage.js server. Modify it to serve indefinitely,
    * un-comment the following line, and verify that the port and credentials
    * have not changed; then the following test should pass.
    */
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java
@@ -1,43 +1,41 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.net.test;
 
-import java.util.concurrent.Executors;
-
+import ch.boye.httpclientandroidlib.protocol.HTTP;
 import junit.framework.Assert;
-
 import org.junit.After;
 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.MockServer;
 import org.mozilla.gecko.background.fxa.FxAccountClient10;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
 import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-import ch.boye.httpclientandroidlib.protocol.HTTP;
+import java.util.concurrent.Executors;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestUserAgentHeaders {
   private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
 
   protected final HTTPServerTestHelper data = new HTTPServerTestHelper();
 
   protected class UserAgentServer extends MockServer {
     public String lastUserAgent = null;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java
@@ -1,32 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.repositories.android.test;
 
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.repositories.android.BookmarksInsertionManager;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.sync.repositories.android.BookmarksInsertionManager;
-import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestBookmarksInsertionManager {
   public BookmarksInsertionManager manager;
   public ArrayList<String[]> insertions;
 
   @Before
   public void setUp() {
     insertions = new ArrayList<String[]>();
     Set<String> writtenFolders = new HashSet<String>();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java
@@ -1,24 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.repositories.domain;
 
+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.Utils;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.CryptoRecord;
-import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestClientRecord {
 
   @Test
   public void testEnsureDefaults() {
     // Ensure defaults.
     ClientRecord record = new ClientRecord();
     assertEquals(ClientRecord.COLLECTION_NAME, record.collection);
     assertEquals(0, record.lastModified);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java
@@ -1,25 +1,25 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.repositories.domain.test;
 
+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.Utils;
+import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.CryptoRecord;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestFormHistoryRecord {
   public static FormHistoryRecord withIdFieldNameAndValue(long id, String fieldName, String value) {
     FormHistoryRecord fr = new FormHistoryRecord();
     fr.androidID = id;
     fr.fieldName = fieldName;
     fr.fieldValue = value;
 
     return fr;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java
@@ -1,21 +1,22 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.repositories.test;
 
-import static org.junit.Assert.*;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
-import org.robolectric.RobolectricGradleTestRunner;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(TestRunner.class)
 public class TestRepositorySessionBundle {
   @Test
   public void testSetGetTimestamp() {
     RepositorySessionBundle bundle = new RepositorySessionBundle(-1);
     assertEquals(-1, bundle.getTimestamp());
 
     bundle.setTimestamp(10);
     assertEquals(10, bundle.getTimestamp());
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java
@@ -1,34 +1,34 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.repositories.test;
 
-import java.net.URISyntaxException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 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.JSONRecordFetcher;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.net.URISyntaxException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(TestRunner.class)
 public class TestSafeConstrainedServer11Repository {
   private static final int     TEST_PORT      = HTTPServerTestHelper.getTestPort();
   private static final String  TEST_SERVER    = "http://localhost:" + TEST_PORT + "/";
   private static final String  TEST_USERNAME  = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd";
   private static final String  TEST_BASE_PATH = "/1.1/" + TEST_USERNAME + "/";
 
   public AuthHeaderProvider getAuthHeaderProvider() {
     return null;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureClusterURLStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureClusterURLStage.java
@@ -1,48 +1,47 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 import org.json.simple.parser.ParseException;
 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;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.AlreadySyncingException;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.NodeAuthenticationException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.stage.EnsureClusterURLStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.ProtocolVersion;
-import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
-import ch.boye.httpclientandroidlib.message.BasicStatusLine;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(TestRunner.class)
 public class TestEnsureClusterURLStage {
   private static final int     TEST_PORT        = HTTPServerTestHelper.getTestPort();
   private static final String  TEST_SERVER      = "http://localhost:" + TEST_PORT + "/";
   private static final String  TEST_OLD_CLUSTER_URL = TEST_SERVER + "cluster/old/";
   private static final String  TEST_NEW_CLUSTER_URL = TEST_SERVER + "cluster/new/";
   static String                TEST_NW_URL      = TEST_SERVER + "/1.0/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/node/weave"; // GET https://server/pathname/version/username/node/weave
   private HTTPServerTestHelper data             = new HTTPServerTestHelper();
 
--- 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,44 +1,44 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.Collection;
-
 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;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.AlreadySyncingException;
 import org.mozilla.gecko.sync.CollectionKeys;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+
+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 {
   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 String TEST_JSON_NO_CRYPTO =
--- 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,33 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.net.URI;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 import org.json.simple.JSONArray;
 import org.json.simple.parser.ParseException;
 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;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.AlreadySyncingException;
 import org.mozilla.gecko.sync.CollectionKeys;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.MetaGlobal;
@@ -36,21 +27,30 @@ import org.mozilla.gecko.sync.SyncConfig
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.FreshStartDelegate;
 import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
 import org.mozilla.gecko.sync.delegates.WipeServerDelegate;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
-import org.robolectric.RobolectricGradleTestRunner;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+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 {
   @SuppressWarnings("unused")
   private static final String  LOG_TAG          = "TestMetaGlobalStage";
 
   private static final int     TEST_PORT        = HTTPServerTestHelper.getTestPort();
   private static final String  TEST_SERVER      = "http://localhost:" + TEST_PORT + "/";
   private static final String  TEST_CLUSTER_URL = TEST_SERVER + "cluster/";
   static String                TEST_NW_URL      = TEST_SERVER + "/1.0/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/node/weave"; // GET https://server/pathname/version/username/node/weave
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java
@@ -1,24 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
-import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
 import java.util.HashSet;
 import java.util.Set;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
-import org.robolectric.RobolectricGradleTestRunner;
+import static org.junit.Assert.assertEquals;
 
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestStageLookup {
 
   @Test
   public void testStageLookupByName() {
     Set<Stage> namedStages = new HashSet<Stage>(Stage.getNamedStages());
     Set<Stage> expected = new HashSet<Stage>();
     expected.add(Stage.syncClientsEngine);
     expected.add(Stage.syncBookmarks);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java
@@ -1,38 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.test;
 
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
+
+import java.io.IOException;
+
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import java.io.IOException;
-
-import org.json.simple.JSONArray;
-import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.NonArrayJSONException;
-import org.mozilla.gecko.sync.NonObjectJSONException;
-import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
-import org.robolectric.RobolectricGradleTestRunner;
-
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestExtendedJSONObject {
   public static String exampleJSON = "{\"modified\":1233702554.25,\"success\":[\"{GXS58IDC}12\",\"{GXS58IDC}13\",\"{GXS58IDC}15\",\"{GXS58IDC}16\",\"{GXS58IDC}18\",\"{GXS58IDC}19\"],\"failed\":{\"{GXS58IDC}11\":[\"invalid parentid\"],\"{GXS58IDC}14\":[\"invalid parentid\"],\"{GXS58IDC}17\":[\"invalid parentid\"],\"{GXS58IDC}20\":[\"invalid parentid\"]}}";
   public static String exampleIntegral = "{\"modified\":1233702554,}";
 
   @Test
   public void testDeepCopy() throws NonObjectJSONException, IOException, ParseException, NonArrayJSONException {
     ExtendedJSONObject a = new ExtendedJSONObject(exampleJSON);
     ExtendedJSONObject c = a.deepCopy();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java
@@ -1,30 +1,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.test;
 
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.InfoCounts;
+import org.mozilla.gecko.sync.Utils;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.InfoCollections;
-import org.mozilla.gecko.sync.InfoCounts;
-import org.mozilla.gecko.sync.Utils;
-import org.robolectric.RobolectricGradleTestRunner;
-
 /**
  * Test both info/collections and info/collection_counts.
  */
-@RunWith(RobolectricGradleTestRunner.class)
+@RunWith(TestRunner.class)
 public class TestInfoCollections {
   public static final String TEST_COLLECTIONS_JSON =
       "{\"history\":1.3319567131E9, "    +
       " \"bookmarks\":1.33195669592E9, " +
       " \"prefs\":1.33115408641E9, "     +
       " \"crypto\":1.32046063664E9, "    +
       " \"meta\":1.321E9, "              +
       " \"forms\":1.33136685374E9, "     +
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java
@@ -1,35 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import java.util.Set;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.MetaGlobal;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
 import org.mozilla.gecko.sync.PersistedMetaGlobal;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
-import org.robolectric.RobolectricGradleTestRunner;
+
+import java.util.Set;
 
-@RunWith(RobolectricGradleTestRunner.class)
+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 TestPersistedMetaGlobal {
   MockSharedPreferences prefs = null;
   private final String TEST_META_URL = "metaURL";
   private final String TEST_CREDENTIALS = "credentials";
 
   @Before
   public void setUp() {
     prefs = new MockSharedPreferences();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java
@@ -1,58 +1,56 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.tokenserver.test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.client.methods.HttpGet;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 import junit.framework.Assert;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.common.log.writers.StringLogWriter;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.SyncResponse;
 import org.mozilla.gecko.tokenserver.TokenServerClient;
 import org.mozilla.gecko.tokenserver.TokenServerClient.TokenFetchResourceDelegate;
 import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
 import org.mozilla.gecko.tokenserver.TokenServerException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
 import org.mozilla.gecko.tokenserver.TokenServerToken;
 
-import ch.boye.httpclientandroidlib.Header;
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.ProtocolVersion;
-import ch.boye.httpclientandroidlib.client.methods.HttpGet;
-import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
-import ch.boye.httpclientandroidlib.entity.StringEntity;
-import ch.boye.httpclientandroidlib.message.BasicHeader;
-import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
-import ch.boye.httpclientandroidlib.message.BasicStatusLine;
-import org.robolectric.RobolectricGradleTestRunner;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
-@RunWith(RobolectricGradleTestRunner.class)
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(TestRunner.class)
 public class TestTokenServerClient {
   public static final String JSON = "application/json";
   public static final String TEXT = "text/plain";
 
   public static final String TEST_TOKEN_RESPONSE = "{\"api_endpoint\": \"https://stage-aitc1.services.mozilla.com/1.0/1659259\"," +
       "\"duration\": 300," +
       "\"id\": \"eySHORTENED\"," +
       "\"key\": \"-plSHORTENED\"," +
--- a/toolkit/components/search/tests/xpcshell/test_geodefaults.js
+++ b/toolkit/components/search/tests/xpcshell/test_geodefaults.js
@@ -62,27 +62,21 @@ function checkRequest(cohort = "") {
   do_check_eq(req._queryString, cohort ? "/" + cohort : "");
 }
 
 add_task(function* no_request_if_prefed_off() {
   // Disable geoSpecificDefaults and check no HTTP request is made.
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
   yield asyncInit();
   checkNoRequest();
+  yield promiseAfterCache();
 
   // The default engine should be set based on the prefs.
   do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
 
-  // Change the default engine and then revert to default to ensure
-  // the metadata file exists.
-  let commitPromise = promiseAfterCache();
-  Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
-  Services.search.resetToOriginalDefaultEngine();
-  yield commitPromise;
-
   // Ensure nothing related to geoSpecificDefaults has been written in the metadata.
   let metadata = yield promiseGlobalMetadata();
   do_check_eq(typeof metadata.searchDefaultExpir, "undefined");
   do_check_eq(typeof metadata.searchDefault, "undefined");
   do_check_eq(typeof metadata.searchDefaultHash, "undefined");
 
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
 });