Merge mozilla-central to mozilla-inbound
authorIris Hsiao <ihsiao@mozilla.com>
Tue, 02 May 2017 11:10:52 +0800
changeset 355964 e94824b92c00d84910271435ff8d19e256e3f6ee
parent 355963 2cc2f8862fdb77fbd55580ba09d628a28bdaae86 (current diff)
parent 355936 2e7c10a9b86e30691f67855f6c8f98d984508d7c (diff)
child 355965 9316ff6512163e7d08e981de7ba4dc390ca6a926
push id31752
push usercbook@mozilla.com
push dateTue, 02 May 2017 09:05:11 +0000
treeherdermozilla-central@48c0fd9c9ec5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone55.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 mozilla-central to mozilla-inbound
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -17,16 +17,19 @@ with Files("tests/unit/test_browserGlue_
     BUG_COMPONENT = ("Firefox", "General")
 
 with Files("tests/unit/test_distribution.js"):
     BUG_COMPONENT = ("Firefox", "Distributions")
 
 with Files("safebrowsing/**"):
     BUG_COMPONENT = ("Toolkit", "Safe Browsing")
 
+with Files('controlcenter/**'):
+    BUG_COMPONENT = ('Firefox', 'General')
+
 
 DIRS += [
     'about',
     'contextualidentity',
     'customizableui',
     'dirprovider',
     'downloads',
     'extensions',
@@ -72,14 +75,8 @@ EXTRA_JS_MODULES += [
 BROWSER_CHROME_MANIFESTS += [
     'safebrowsing/content/test/browser.ini',
     'tests/browser/browser.ini'
 ]
 
 XPCSHELL_TESTS_MANIFESTS += [
     'tests/unit/xpcshell.ini'
 ]
-
-with Files('safebrowsing/*'):
-    BUG_COMPONENT = ('Toolkit', 'Phishing Protection')
-
-with Files('controlcenter/**'):
-    BUG_COMPONENT = ('Firefox', 'General')
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -1105,18 +1105,23 @@ this.PlacesUIUtils = {
   },
 
   get leftPaneQueries() {
     // build the map
     this.leftPaneFolderId;
     return this.leftPaneQueries;
   },
 
+  get leftPaneFolderId() {
+    delete this.leftPaneFolderId;
+    return this.leftPaneFolderId = this.maybeRebuildLeftPane();
+  },
+
   // Get the folder id for the organizer left-pane folder.
-  get leftPaneFolderId() {
+  maybeRebuildLeftPane() {
     let leftPaneRoot = -1;
     let allBookmarksId;
 
     // Shortcuts to services.
     let bs = PlacesUtils.bookmarks;
     let as = PlacesUtils.annotations;
 
     // This is the list of the left pane queries.
@@ -1158,18 +1163,18 @@ this.PlacesUIUtils = {
         // This will throw if the annotation is an orphan.
         bs.removeItem(aItemId);
       } catch (e) { /* orphan anno */ }
     }
 
     // Returns true if item really exists, false otherwise.
     function itemExists(aItemId) {
       try {
-        bs.getItemIndex(aItemId);
-        return true;
+        let index = bs.getItemIndex(aItemId);
+        return index > -1;
       } catch (e) {
         return false;
       }
     }
 
     // Get all items marked as being the left pane folder.
     let items = as.getItemsWithAnnotation(this.ORGANIZER_FOLDER_ANNO);
     if (items.length > 1) {
@@ -1246,18 +1251,17 @@ this.PlacesUIUtils = {
       if (corrupt || queriesCount != EXPECTED_QUERY_COUNT) {
         // Queries number is wrong, so the left pane must be corrupt.
         // Note: we can't just remove the leftPaneRoot, because some query could
         // have a bad parent, so we have to remove all items one by one.
         queryItems.forEach(safeRemoveItem);
         safeRemoveItem(leftPaneRoot);
       } else {
         // Everything is fine, return the current left pane folder.
-        delete this.leftPaneFolderId;
-        return this.leftPaneFolderId = leftPaneRoot;
+        return leftPaneRoot;
       }
     }
 
     // Create a new left pane folder.
     var callback = {
       // Helper to create an organizer special query.
       create_query: function CB_create_query(aQueryName, aParentId, aQueryUrl) {
         let itemId = bs.insertBookmark(aParentId,
@@ -1340,18 +1344,17 @@ this.PlacesUIUtils = {
 
         // All Bookmarks->Unfiled Bookmarks Query.
         this.create_query("UnfiledBookmarks", allBookmarksId,
                           "place:folder=UNFILED_BOOKMARKS");
       }
     };
     bs.runInBatchMode(callback, null);
 
-    delete this.leftPaneFolderId;
-    return this.leftPaneFolderId = leftPaneRoot;
+    return leftPaneRoot;
   },
 
   /**
    * Get the folder id for the organizer left-pane folder.
    */
   get allBookmarksFolderId() {
     // ensure the left-pane root is initialized;
     this.leftPaneFolderId;
--- a/build/autoconf/compiler-opts.m4
+++ b/build/autoconf/compiler-opts.m4
@@ -246,34 +246,30 @@ if test "$GNU_CC" -a "$GCC_USE_GNU_LD" -
              DSO_LDOPTS="$DSO_LDOPTS -Wl,--gc-sections"
          fi
     else
         DSO_LDOPTS="$DSO_LDOPTS -Wl,--gc-sections"
     fi
 fi
 
 # bionic in Android < 4.1 doesn't support PIE
-# On OSX, the linker defaults to building PIE programs when targetting OSX 10.7+,
-# but not when targetting OSX < 10.7. OSX < 10.7 doesn't support running PIE
-# programs, so as long as support for OSX 10.6 is kept, we can't build PIE.
-# Even after dropping 10.6 support, MOZ_PIE would not be useful since it's the
-# default (and clang says the -pie option is not used).
+# On OSX, the linker defaults to building PIE programs when targeting OSX 10.7.
 # On other Unix systems, some file managers (Nautilus) can't start PIE programs
 if test -n "$gonkdir" && test "$ANDROID_VERSION" -ge 16; then
     MOZ_PIE=1
 else
     MOZ_PIE=
 fi
 
 MOZ_ARG_ENABLE_BOOL(pie,
 [  --enable-pie           Enable Position Independent Executables],
     MOZ_PIE=1,
     MOZ_PIE= )
 
-if test "$GNU_CC" -a -n "$MOZ_PIE"; then
+if test "$GNU_CC$CLANG_CC" -a -n "$MOZ_PIE"; then
     AC_MSG_CHECKING([for PIE support])
     _SAVE_LDFLAGS=$LDFLAGS
     LDFLAGS="$LDFLAGS -pie"
     AC_TRY_LINK(,,AC_MSG_RESULT([yes])
                   [MOZ_PROGRAM_LDFLAGS="$MOZ_PROGRAM_LDFLAGS -pie"],
                   AC_MSG_RESULT([no])
                   AC_MSG_ERROR([--enable-pie requires PIE support from the linker.]))
     LDFLAGS=$_SAVE_LDFLAGS
copy from dom/smil/test/test_smilWithTransition.html
copy to dom/smil/test/file_smilWithTransition.html
--- a/dom/smil/test/test_smilWithTransition.html
+++ b/dom/smil/test/file_smilWithTransition.html
@@ -1,32 +1,39 @@
 <!doctype html>
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=1315874
 -->
 <head>
   <meta charset="utf-8">
   <title>Test SMIL does not trigger CSS Transitions (bug 1315874)</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <a target="_blank"
   href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315874">Mozilla Bug
   1315874</a>
 <svg>
   <rect width="100%" height="100%"
         style="fill: red; transition: fill 10s" id="rect">
     <animate attributeName="fill" to="lime" dur="1s" fill="freeze">
   </rect>
 </svg>
-<pre id="test">
 <script  type="text/javascript">
-  SimpleTest.waitForExplicitFinish();
+  // Bring SimpleTest's function from opener.
+  if (opener) {
+    var is = opener.is.bind(opener);
+    var ok = opener.ok.bind(opener);
+    function finish() {
+      var o = opener;
+      self.close();
+      o.SimpleTest.finish();
+    }
+  }
+
   window.addEventListener('load', runTests, false);
 
   var rect = document.getElementById('rect');
   var svg = document.getElementsByTagName('svg')[0];
   is(getComputedStyle(rect).fill, 'rgb(255, 0, 0)',
      'The initial color should be red.');
 
   function runTests() {
@@ -34,17 +41,17 @@ https://bugzilla.mozilla.org/show_bug.cg
       svg.setCurrentTime(1);
       ok(getComputedStyle(rect).fill, 'rgb(0, 255, 0)',
          'The end color should be lime.');
 
       return waitForAnimationFrames(2);
     }).then(function() {
       var anim = document.getAnimations()[0];
       ok(!anim, 'Transition should not be created by restyling for SMIL');
-      SimpleTest.finish();
+      finish();
     });
   }
 
   // Utility methods from testcommon.js
   // For detail, see dom/animation/test/testcommon.js.
 
   function waitForFrame() {
     return new Promise(function(resolve, reject) {
--- a/dom/smil/test/mochitest.ini
+++ b/dom/smil/test/mochitest.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 support-files =
   db_smilAnimateMotion.js
   db_smilCSSFromBy.js
   db_smilCSSFromTo.js
   db_smilCSSPaced.js
   db_smilCSSPropertyList.js
   db_smilMappedAttrList.js
+  file_smilWithTransition.html
   smilAnimateMotionValueLists.js
   smilExtDoc_helper.svg
   smilTestUtils.js
   smilXHR_helper.svg
 
 [test_smilAccessKey.xhtml]
 [test_smilAnimateMotion.xhtml]
 [test_smilAnimateMotionInvalidValues.xhtml]
--- a/dom/smil/test/test_smilWithTransition.html
+++ b/dom/smil/test/test_smilWithTransition.html
@@ -1,72 +1,18 @@
 <!doctype html>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=1315874
--->
+<meta charset=utf-8>
 <head>
-  <meta charset="utf-8">
-  <title>Test SMIL does not trigger CSS Transitions (bug 1315874)</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
-<body>
-<a target="_blank"
-  href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315874">Mozilla Bug
-  1315874</a>
-<svg>
-  <rect width="100%" height="100%"
-        style="fill: red; transition: fill 10s" id="rect">
-    <animate attributeName="fill" to="lime" dur="1s" fill="freeze">
-  </rect>
-</svg>
 <pre id="test">
-<script  type="text/javascript">
-  SimpleTest.waitForExplicitFinish();
-  window.addEventListener('load', runTests, false);
-
-  var rect = document.getElementById('rect');
-  var svg = document.getElementsByTagName('svg')[0];
-  is(getComputedStyle(rect).fill, 'rgb(255, 0, 0)',
-     'The initial color should be red.');
-
-  function runTests() {
-    waitForFrame().then(function() {
-      svg.setCurrentTime(1);
-      ok(getComputedStyle(rect).fill, 'rgb(0, 255, 0)',
-         'The end color should be lime.');
+<script>
+'use strict';
 
-      return waitForAnimationFrames(2);
-    }).then(function() {
-      var anim = document.getAnimations()[0];
-      ok(!anim, 'Transition should not be created by restyling for SMIL');
-      SimpleTest.finish();
-    });
-  }
-
-  // Utility methods from testcommon.js
-  // For detail, see dom/animation/test/testcommon.js.
-
-  function waitForFrame() {
-    return new Promise(function(resolve, reject) {
-      requestAnimationFrame(function(time) {
-        resolve();
-      });
-    });
-  }
-
-  function waitForAnimationFrames(frameCount) {
-    return new Promise(function(resolve, reject) {
-      function handleFrame() {
-        if (--frameCount <= 0) {
-          resolve();
-        } else {
-          window.requestAnimationFrame(handleFrame);
-        }
-      }
-      window.requestAnimationFrame(handleFrame);
-    });
-  }
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+  { "set": [["dom.animations-api.core.enabled", true]]},
+  function() {
+    window.open("file_smilWithTransition.html");
+  });
 </script>
-</pre>
-</body>
 </html>
--- a/layout/style/StyleAnimationValue.cpp
+++ b/layout/style/StyleAnimationValue.cpp
@@ -2878,19 +2878,20 @@ StyleAnimationValue::AddWeighted(nsCSSPr
     case eUnit_UnparsedString:
     case eUnit_URL:
     case eUnit_DiscreteCSSValue:
       return false;
 
     case eUnit_Enumerated:
       switch (aProperty) {
         case eCSSProperty_font_stretch: {
-          // Animate just like eUnit_Integer.
-          int32_t result = floor(aCoeff1 * double(aValue1.GetIntValue()) +
-                                 aCoeff2 * double(aValue2.GetIntValue()));
+          // https://drafts.csswg.org/css-fonts-3/#font-stretch-animation
+          double interpolatedValue = aCoeff1 * double(aValue1.GetIntValue()) +
+                                     aCoeff2 * double(aValue2.GetIntValue());
+          int32_t result = floor(interpolatedValue + 0.5);
           if (result < NS_STYLE_FONT_STRETCH_ULTRA_CONDENSED) {
             result = NS_STYLE_FONT_STRETCH_ULTRA_CONDENSED;
           } else if (result > NS_STYLE_FONT_STRETCH_ULTRA_EXPANDED) {
             result = NS_STYLE_FONT_STRETCH_ULTRA_EXPANDED;
           }
           aResultValue.SetIntValue(result, eUnit_Enumerated);
           return true;
         }
@@ -2912,20 +2913,20 @@ StyleAnimationValue::AddWeighted(nsCSSPr
       int32_t val2 = enum2 == NS_STYLE_VISIBILITY_VISIBLE;
       double interp = aCoeff1 * val1 + aCoeff2 * val2;
       int32_t result = interp > 0.0 ? NS_STYLE_VISIBILITY_VISIBLE
                                     : (val1 ? enum2 : enum1);
       aResultValue.SetIntValue(result, eUnit_Visibility);
       return true;
     }
     case eUnit_Integer: {
-      // http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types-
-      // says we should use floor
-      int32_t result = floor(aCoeff1 * double(aValue1.GetIntValue()) +
-                             aCoeff2 * double(aValue2.GetIntValue()));
+      // https://drafts.csswg.org/css-transitions/#animtype-integer
+      double interpolatedValue = aCoeff1 * double(aValue1.GetIntValue()) +
+                                 aCoeff2 * double(aValue2.GetIntValue());
+      int32_t result = floor(interpolatedValue + 0.5);
       if (aProperty == eCSSProperty_font_weight) {
         if (result < 100) {
           result = 100;
         } else if (result > 900) {
           result = 900;
         }
         result -= result % 100;
       } else {
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -1831,17 +1831,17 @@ function test_radius_transition(prop) {
 
 function test_integer_transition(prop) {
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "4", "");
   is(cs.getPropertyValue(prop), "4",
      "integer-valued property " + prop + ": computed value before transition");
   div.style.setProperty("transition-property", prop, "");
   div.style.setProperty(prop, "-14", "");
-  is(cs.getPropertyValue(prop), "-1",
+  is(cs.getPropertyValue(prop), "0",
      "integer-valued property " + prop + ": interpolation of integers");
   check_distance(prop, "6", "1", "-14");
 
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "-4", "");
   is(cs.getPropertyValue(prop), "-4",
      "integer-valued property " + prop + ": computed value before transition");
   div.style.setProperty("transition-property", prop, "");
@@ -1875,17 +1875,17 @@ function test_font_stretch(prop) {
      "font-stretch property " + prop + ": interpolation of font-stretches");
   check_distance(prop, "normal", "semi-expanded", "ultra-expanded");
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "expanded", "");
   is(cs.getPropertyValue(prop), "expanded",
      "font-stretch property " + prop + ": computed value before transition");
   div.style.setProperty("transition-property", prop, "");
   div.style.setProperty(prop, "extra-condensed", "");
-  is(cs.getPropertyValue(prop), "normal",
+  is(cs.getPropertyValue(prop), "semi-expanded",
      "font-stretch property " + prop + ": interpolation of font-stretches");
   check_distance(prop, "expanded", "semi-expanded", "condensed");
 
   div.style.setProperty("transition-timing-function", FUNC_NEGATIVE, "");
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "ultra-condensed", "");
   is(cs.getPropertyValue(prop), "ultra-condensed",
      "font-stretch property " + prop + ": flush before clamping test");
@@ -1959,17 +1959,17 @@ function test_grid_gap(prop) {
 
 function test_pos_integer_or_auto_transition(prop) {
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "4", "");
   is(cs.getPropertyValue(prop), "4",
      "integer-valued property " + prop + ": computed value before transition");
   div.style.setProperty("transition-property", prop, "");
   div.style.setProperty(prop, "11", "");
-  is(cs.getPropertyValue(prop), "5",
+  is(cs.getPropertyValue(prop), "6",
      "integer-valued property " + prop + ": interpolation of integers");
   check_distance(prop, "4", "6", "12");
   div.style.setProperty(prop, "auto", "");
   is(cs.getPropertyValue(prop), "auto",
      "integer-valued property " + prop + ": auto not interpolable");
   div.style.setProperty(prop, "8", "");
   is(cs.getPropertyValue(prop), "8",
      "integer-valued property " + prop + ": computed value before transition");
--- a/security/sandbox/mac/SandboxPolicies.h
+++ b/security/sandbox/mac/SandboxPolicies.h
@@ -259,27 +259,30 @@ static const char contentSandboxRules[] 
   ;          no read/write access to ~/Library,
   ;          no read/write access to $PROFILE,
   ;          read access permitted to $PROFILE/{extensions,chrome}
     (if (string=? sandbox-level-2 "TRUE")
       (if (string=? hasFilePrivileges "TRUE")
         ; This process has blanket file read privileges
         (allow file-read*)
         ; This process does not have blanket file read privileges
-        (if (string=? hasProfileDir "TRUE")
-          ; we have a profile dir
-          (begin
-            (allow file-read* (require-all
-                (require-not (home-subpath "/Library"))
-                (require-not (subpath profileDir))))
-            (allow file-read*
-                (profile-subpath "/extensions")
-                (profile-subpath "/chrome")))
-          ; we don't have a profile dir
-          (allow file-read* (require-not (home-subpath "/Library"))))))
+        (begin
+          ; bug 1201935
+          (allow file-read* (home-subpath "/Library/Caches/TemporaryItems"))
+          (if (string=? hasProfileDir "TRUE")
+            ; we have a profile dir
+            (begin
+              (allow file-read* (require-all
+                  (require-not (home-subpath "/Library"))
+                  (require-not (subpath profileDir))))
+              (allow file-read*
+                  (profile-subpath "/extensions")
+                  (profile-subpath "/chrome")))
+            ; we don't have a profile dir
+            (allow file-read* (require-not (home-subpath "/Library")))))))
 
   ; level 3: global read access permitted, no global write access,
   ;          no read access to the home directory,
   ;          read access permitted to $PROFILE/{extensions,chrome}
     (if (string=? sandbox-level-3 "TRUE")
       (if (string=? hasFilePrivileges "TRUE")
         ; This process has blanket file read privileges
         (allow file-read*)
@@ -312,20 +315,16 @@ static const char contentSandboxRules[] 
         (iokit-user-client-class "AppleGraphicsControlClient")
         (iokit-user-client-class "AppleGraphicsPolicyClient"))
 
   ; bug 1153809
     (allow iokit-open
         (iokit-user-client-class "NVDVDContextTesla")
         (iokit-user-client-class "Gen6DVDContext"))
 
-  ; bug 1201935
-    (allow file-read*
-        (home-subpath "/Library/Caches/TemporaryItems"))
-
   ; bug 1237847
     (allow file-read*
         (subpath appTempDir))
     (allow file-write*
         (subpath appTempDir))
 
   ; bug 1324610
     (allow network-outbound (literal "/private/var/run/cupsd"))
--- a/security/sandbox/test/browser_content_sandbox_fs.js
+++ b/security/sandbox/test/browser_content_sandbox_fs.js
@@ -243,16 +243,19 @@ function* testFileAccess() {
   if (fileContentProcessEnabled) {
     // open a tab in a file content process
     gBrowser.selectedTab =
       gBrowser.addTab("about:blank", {preferredRemoteType: "file"});
     // get the browser for the file content process tab
     fileBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
   }
 
+  // Current level
+  let level = prefs.getIntPref("security.sandbox.content.level");
+
   // Directories/files to test accessing from content processes.
   // For directories, we test whether a directory listing is allowed
   // or blocked. For files, we test if we can read from the file.
   // Each entry in the array represents a test file or directory
   // that will be read from either a web or file process.
   let tests = [];
 
   let profileDir = GetProfileDir();
@@ -286,16 +289,40 @@ function* testFileAccess() {
       desc:     "home dir",
       ok:       true,
       browser:  fileBrowser,
       file:     homeDir,
       minLevel: 0,
     });
   }
 
+  if (isMac()) {
+    // If ~/Library/Caches/TemporaryItems exists, when level <= 2 we
+    // make sure it's readable. For level 3, we make sure it isn't.
+    let homeTempDir = GetHomeDir();
+    homeTempDir.appendRelativePath('Library/Caches/TemporaryItems');
+    if (homeTempDir.exists()) {
+      let shouldBeReadable, minLevel;
+      if (level >= minHomeReadSandboxLevel()) {
+        shouldBeReadable = false;
+        minLevel = minHomeReadSandboxLevel();
+      } else {
+        shouldBeReadable = true;
+        minLevel = 0;
+      }
+      tests.push({
+        desc:     "home library cache temp dir",
+        ok:       shouldBeReadable,
+        browser:  webBrowser,
+        file:     homeTempDir,
+        minLevel: minLevel,
+      });
+    }
+  }
+
   let extensionsDir = GetProfileEntry("extensions");
   if (extensionsDir.exists() && extensionsDir.isDirectory()) {
     tests.push({
       desc:     "extensions dir",
       ok:       true,
       browser:  webBrowser,
       file:     extensionsDir,
       minLevel: 0,
@@ -326,17 +353,16 @@ function* testFileAccess() {
       file:     cookiesFile,
       minLevel: minProfileReadSandboxLevel(),
     });
   } else {
     ok(false, `${cookiesFile.path} is a valid file`);
   }
 
   // remove tests not enabled by the current sandbox level
-  let level = prefs.getIntPref("security.sandbox.content.level");
   tests = tests.filter((test) => { return (test.minLevel <= level); });
 
   for (let test of tests) {
     let testFunc = test.file.isDirectory? readDir : readFile;
     let okString = test.ok? "allowed" : "blocked";
     let processType = test.browser === webBrowser ? "web" : "file";
 
     let result = yield ContentTask.spawn(test.browser, test.file.path,
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -496,8 +496,51 @@ function registerRotaryEngine() {
 
 // Set the validation prefs to attempt validation every time to avoid non-determinism.
 function enableValidationPrefs() {
   Svc.Prefs.set("engine.bookmarks.validation.interval", 0);
   Svc.Prefs.set("engine.bookmarks.validation.percentageChance", 100);
   Svc.Prefs.set("engine.bookmarks.validation.maxRecords", -1);
   Svc.Prefs.set("engine.bookmarks.validation.enabled", true);
 }
+
+function serverForEnginesWithKeys(users, engines, callback) {
+  // Generate and store a fake default key bundle to avoid resetting the client
+  // before the first sync.
+  let wbo = Service.collectionKeys.generateNewKeysWBO();
+  let modified = new_timestamp();
+  Service.collectionKeys.setContents(wbo.cleartext, modified);
+
+  let allEngines = [Service.clientsEngine].concat(engines);
+
+  let globalEngines = allEngines.reduce((entries, engine) => {
+    let { name, version, syncID } = engine;
+    entries[name] = { version, syncID };
+    return entries;
+  }, {});
+
+  let contents = allEngines.reduce((collections, engine) => {
+    collections[engine.name] = {};
+    return collections;
+  }, {
+    meta: {
+      global: {
+        syncID: Service.syncID,
+        storageVersion: STORAGE_VERSION,
+        engines: globalEngines,
+      },
+    },
+    crypto: {
+      keys: encryptPayload(wbo.cleartext),
+    },
+  });
+
+  return serverForUsers(users, contents, callback);
+}
+
+function serverForFoo(engine, callback) {
+  // The bookmarks engine *always* tracks changes, meaning we might try
+  // and sync due to the bookmarks we ourselves create! Worse, because we
+  // do an engine sync only, there's no locking - so we end up with multiple
+  // syncs running. Neuter that by making the threshold very large.
+  Service.scheduler.syncThreshold = 10000000;
+  return serverForEnginesWithKeys({"foo": "password"}, engine, callback);
+}
--- a/services/sync/tests/unit/test_bookmark_decline_undecline.js
+++ b/services/sync/tests/unit/test_bookmark_decline_undecline.js
@@ -11,55 +11,16 @@ Cu.import("resource://services-sync/engi
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 
 initTestLogging("Trace");
 
 Service.engineManager.register(BookmarksEngine);
 
-function serverForFoo(engine) {
-  // The bookmarks engine *always* tracks changes, meaning we might try
-  // and sync due to the bookmarks we ourselves create! Worse, because we
-  // do an engine sync only, there's no locking - so we end up with multiple
-  // syncs running. Neuter that by making the threshold very large.
-  Service.scheduler.syncThreshold = 10000000;
-  let clientsEngine = Service.clientsEngine;
-  return serverForUsers({"foo": "password"}, {
-    meta: {
-      global: {
-        syncID: Service.syncID,
-        storageVersion: STORAGE_VERSION,
-        engines: {
-          clients: {
-            version: clientsEngine.version,
-            syncID: clientsEngine.syncID,
-          },
-          bookmarks: {
-            version: engine.version,
-            syncID: engine.syncID,
-          },
-        },
-      },
-    },
-    crypto: {
-      keys: encryptPayload({
-        id: "keys",
-        // Generate a fake default key bundle to avoid resetting the client
-        // before the first sync.
-        default: [
-          Svc.Crypto.generateRandomKey(),
-          Svc.Crypto.generateRandomKey(),
-        ],
-      }),
-    },
-    bookmarks: {}
-  });
-}
-
 // A stored reference to the collection won't be valid after disabling.
 function getBookmarkWBO(server, guid) {
   let coll = server.user("foo").collection("bookmarks");
   if (!coll) {
     return null;
   }
   return coll.wbo(guid);
 }
--- a/services/sync/tests/unit/test_bookmark_duping.js
+++ b/services/sync/tests/unit/test_bookmark_duping.js
@@ -20,34 +20,21 @@ const bms = PlacesUtils.bookmarks;
 Service.engineManager.register(BookmarksEngine);
 
 const engine = new BookmarksEngine(Service);
 const store = engine._store;
 store._log.level = Log.Level.Trace;
 engine._log.level = Log.Level.Trace;
 
 async function setup() {
- let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bookmarks: {version: engine.version,
-                                          syncID: engine.syncID}}}},
-    bookmarks: {},
-  });
-
-  generateNewKeys(Service.collectionKeys);
-
+ let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.user("foo").collection("bookmarks");
 
-  // The bookmarks engine *always* tracks changes, meaning we might try
-  // and sync due to the bookmarks we ourselves create! Worse, because we
-  // do an engine sync only, there's no locking - so we end up with multiple
-  // syncs running. Neuter that by making the threshold very large.
-  Service.scheduler.syncThreshold = 10000000;
-
   Svc.Obs.notify("weave:engine:start-tracking");   // We skip usual startup...
 
   return { server, collection };
 }
 
 async function cleanup(server) {
   Svc.Obs.notify("weave:engine:stop-tracking");
   let promiseStartOver = promiseOneObserver("weave:service:start-over:finish");
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -277,30 +277,16 @@ add_task(async function bad_record_allID
   do_check_true(all.has("toolbar"));
 
   _("Clean up.");
   PlacesUtils.bookmarks.removeItem(badRecordID);
   await PlacesSyncUtils.bookmarks.reset();
   await promiseStopServer(server);
 });
 
-function serverForFoo(engine) {
-  // The bookmarks engine *always* tracks changes, meaning we might try
-  // and sync due to the bookmarks we ourselves create! Worse, because we
-  // do an engine sync only, there's no locking - so we end up with multiple
-  // syncs running. Neuter that by making the threshold very large.
-  Service.scheduler.syncThreshold = 10000000;
-
-  return serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bookmarks: {version: engine.version,
-                                          syncID: engine.syncID}}}},
-    bookmarks: {}
-  });
-}
-
 add_task(async function test_processIncoming_error_orderChildren() {
   _("Ensure that _orderChildren() is called even when _processIncoming() throws an error.");
 
   let engine = new BookmarksEngine(Service);
   let store  = engine._store;
   let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
--- a/services/sync/tests/unit/test_bookmark_repair.js
+++ b/services/sync/tests/unit/test_bookmark_repair.js
@@ -84,36 +84,17 @@ async function cleanup(server) {
   await promiseStopServer(server);
 }
 
 add_task(async function test_bookmark_repair_integration() {
   enableValidationPrefs();
 
   _("Ensure that a validation error triggers a repair request.");
 
-  let contents = {
-    meta: {
-      global: {
-        engines: {
-          clients: {
-            version: clientsEngine.version,
-            syncID: clientsEngine.syncID,
-          },
-          bookmarks: {
-            version: bookmarksEngine.version,
-            syncID: bookmarksEngine.syncID,
-          },
-        }
-      }
-    },
-    clients: {},
-    bookmarks: {},
-    crypto: {},
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(bookmarksEngine);
   await SyncTestingInfrastructure(server);
 
   let user = server.user("foo");
 
   let initialID = Service.clientsEngine.localID;
   let remoteID = Utils.makeGUID();
   try {
 
@@ -324,36 +305,17 @@ add_task(async function test_bookmark_re
   }
 });
 
 add_task(async function test_repair_client_missing() {
   enableValidationPrefs();
 
   _("Ensure that a record missing from the client only will get re-downloaded from the server");
 
-  let contents = {
-    meta: {
-      global: {
-        engines: {
-          clients: {
-            version: clientsEngine.version,
-            syncID: clientsEngine.syncID,
-          },
-          bookmarks: {
-            version: bookmarksEngine.version,
-            syncID: bookmarksEngine.syncID,
-          },
-        }
-      }
-    },
-    clients: {},
-    bookmarks: {},
-    crypto: {},
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(bookmarksEngine);
   await SyncTestingInfrastructure(server);
 
   let user = server.user("foo");
 
   let initialID = Service.clientsEngine.localID;
   let remoteID = Utils.makeGUID();
   try {
 
@@ -415,36 +377,17 @@ add_task(async function test_repair_clie
   }
 });
 
 add_task(async function test_repair_server_missing() {
   enableValidationPrefs();
 
   _("Ensure that a record missing from the server only will get re-upload from the client");
 
-  let contents = {
-    meta: {
-      global: {
-        engines: {
-          clients: {
-            version: clientsEngine.version,
-            syncID: clientsEngine.syncID,
-          },
-          bookmarks: {
-            version: bookmarksEngine.version,
-            syncID: bookmarksEngine.syncID,
-          },
-        }
-      }
-    },
-    clients: {},
-    bookmarks: {},
-    crypto: {},
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(bookmarksEngine);
   await SyncTestingInfrastructure(server);
 
   let user = server.user("foo");
 
   let initialID = Service.clientsEngine.localID;
   let remoteID = Utils.makeGUID();
   try {
 
@@ -499,36 +442,17 @@ add_task(async function test_repair_serv
   }
 });
 
 add_task(async function test_repair_server_deleted() {
   enableValidationPrefs();
 
   _("Ensure that a record marked as deleted on the server but present on the client will get deleted on the client");
 
-  let contents = {
-    meta: {
-      global: {
-        engines: {
-          clients: {
-            version: clientsEngine.version,
-            syncID: clientsEngine.syncID,
-          },
-          bookmarks: {
-            version: bookmarksEngine.version,
-            syncID: bookmarksEngine.syncID,
-          },
-        }
-      }
-    },
-    clients: {},
-    bookmarks: {},
-    crypto: {},
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(bookmarksEngine);
   await SyncTestingInfrastructure(server);
 
   let user = server.user("foo");
 
   let initialID = Service.clientsEngine.localID;
   let remoteID = Utils.makeGUID();
   try {
 
--- a/services/sync/tests/unit/test_bookmark_repair_responder.js
+++ b/services/sync/tests/unit/test_bookmark_repair_responder.js
@@ -29,49 +29,19 @@ function checkRecordedEvents(expected) {
   recordedEvents = [];
 }
 
 function getServerBookmarks(server) {
   return server.user("foo").collection("bookmarks");
 }
 
 async function setup() {
-  let clientsEngine = Service.clientsEngine;
   let bookmarksEngine = Service.engineManager.get("bookmarks");
 
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {
-      global: {
-        syncID: Service.syncID,
-        storageVersion: STORAGE_VERSION,
-        engines: {
-          clients: {
-            version: clientsEngine.version,
-            syncID: clientsEngine.syncID,
-          },
-          bookmarks: {
-            version: bookmarksEngine.version,
-            syncID: bookmarksEngine.syncID,
-          },
-        },
-      },
-    },
-    crypto: {
-      keys: encryptPayload({
-        id: "keys",
-        // Generate a fake default key bundle to avoid resetting the client
-        // before the first sync.
-        default: [
-          Svc.Crypto.generateRandomKey(),
-          Svc.Crypto.generateRandomKey(),
-        ],
-      }),
-    },
-  });
-
+  let server = serverForFoo(bookmarksEngine);
   await SyncTestingInfrastructure(server);
 
   // Disable validation so that we don't try to automatically repair the server
   // when we sync.
   Svc.Prefs.set("engine.bookmarks.validation.enabled", false);
 
   return server;
 }
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -41,24 +41,16 @@ function smartBookmarkCount() {
 function clearBookmarks() {
   _("Cleaning up existing items.");
   PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.bookmarksMenuFolder);
   PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.tagsFolder);
   PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.toolbarFolder);
   PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.unfiledBookmarksFolder);
 }
 
-function serverForFoo(engineData) {
-  return serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bookmarks: {version: engineData.version,
-                                          syncID: engineData.syncID}}}},
-    bookmarks: {}
-  });
-}
-
 // Verify that Places smart bookmarks have their annotation uploaded and
 // handled locally.
 add_task(async function test_annotation_uploaded() {
   let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let startCount = smartBookmarkCount();
 
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -133,16 +133,175 @@ async function insertBookmarksToMigrate(
   }, {
     guid: exampleBmk.guid,
     syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
   });
 
   await PlacesUtils.bookmarks.remove(exampleBmk.guid);
 }
 
+// `PlacesUtils.annotations.setItemAnnotation` prevents us from setting
+// annotations on nonexistent items, so this test helper writes to the DB
+// directly.
+function setAnnoUnchecked(itemId, name, value, type) {
+  return PlacesUtils.withConnectionWrapper(
+    "test_bookmark_tracker: setItemAnnoUnchecked", async function(db) {
+      await db.executeCached(`
+        INSERT OR IGNORE INTO moz_anno_attributes (name)
+        VALUES (:name)`,
+        { name });
+
+      let annoIds = await db.executeCached(`
+        SELECT a.id, a.dateAdded
+        FROM moz_items_annos a WHERE a.item_id = :itemId`,
+        { itemId });
+
+      let annoId;
+      let dateAdded;
+      let lastModified = PlacesUtils.toPRTime(Date.now());
+
+      if (annoIds.length) {
+        annoId = annoIds[0].getResultByName("id");
+        dateAdded = annoIds[0].getResultByName("dateAdded");
+      } else {
+        annoId = null;
+        dateAdded = lastModified;
+      }
+
+      await db.executeCached(`
+        INSERT OR REPLACE INTO moz_items_annos (id, item_id, anno_attribute_id,
+          content, flags, expiration, type, dateAdded, lastModified)
+        VALUES (:annoId, :itemId, (SELECT id FROM moz_anno_attributes
+                                   WHERE name = :name),
+                :value, 0, :expiration, :type, :dateAdded, :lastModified)`,
+        { annoId, itemId, name, value, type,
+          expiration: PlacesUtils.annotations.EXPIRE_NEVER,
+          dateAdded, lastModified });
+    }
+  );
+}
+
+add_task(async function test_leftPaneFolder() {
+  _("Ensure we never track left pane roots");
+
+  try {
+    await startTracking();
+
+    // Creates the organizer queries as a side effect.
+    let leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
+    _(`Left pane root ID: ${leftPaneId}`);
+
+    {
+      let changes = await tracker.promiseChangedIDs();
+      deepEqual(changes, {}, "New left pane queries should not be tracked");
+      do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
+    }
+
+    _("Reset synced bookmarks to simulate a disconnect");
+    await PlacesSyncUtils.bookmarks.reset();
+
+    {
+      let changes = await tracker.promiseChangedIDs();
+      deepEqual(Object.keys(changes).sort(), ["menu", "mobile", "toolbar", "unfiled"],
+        "Left pane queries should not be tracked after reset");
+      do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
+      await PlacesTestUtils.markBookmarksAsSynced();
+    }
+
+    // The following tests corrupt the left pane queries in different ways.
+    // `PlacesUIUtils.maybeRebuildLeftPane` will rebuild the entire root, but
+    // none of those changes should be tracked by Sync.
+
+    _("Annotate unrelated folder as left pane root");
+    {
+      let folder = await PlacesUtils.bookmarks.insert({
+        parentGuid: PlacesUtils.bookmarks.rootGuid,
+        type: PlacesUtils.bookmarks.TYPE_FOLDER,
+        title: "Fake left pane root",
+      });
+      let folderId = await PlacesUtils.promiseItemId(folder.guid);
+      await setAnnoUnchecked(folderId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO, 0,
+                             PlacesUtils.annotations.TYPE_INT32);
+
+      leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
+      _(`Left pane root ID after deleting unrelated folder: ${leftPaneId}`);
+
+      let changes = await tracker.promiseChangedIDs();
+      deepEqual(changes, {},
+        "Should not track left pane items after deleting unrelated folder");
+    }
+
+    _("Corrupt organizer left pane version");
+    {
+      await setAnnoUnchecked(leftPaneId, PlacesUIUtils.ORGANIZER_FOLDER_ANNO,
+                             -1, PlacesUtils.annotations.TYPE_INT32);
+
+      leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
+      _(`Left pane root ID after restoring version: ${leftPaneId}`);
+
+      let changes = await tracker.promiseChangedIDs();
+      deepEqual(changes, {},
+        "Should not track left pane items after restoring version");
+    }
+
+    _("Set left pane anno on nonexistent item");
+    {
+      await setAnnoUnchecked(999, PlacesUIUtils.ORGANIZER_QUERY_ANNO,
+                             "Tags", PlacesUtils.annotations.TYPE_STRING);
+
+      leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
+      _(`Left pane root ID after detecting nonexistent item: ${leftPaneId}`);
+
+      let changes = await tracker.promiseChangedIDs();
+      deepEqual(changes, {},
+        "Should not track left pane items after detecting nonexistent item");
+    }
+
+    _("Move query out of left pane root");
+    {
+      let queryId = await PlacesUIUtils.leftPaneQueries.Downloads;
+      let queryGuid = await PlacesUtils.promiseItemGuid(queryId);
+      await PlacesUtils.bookmarks.update({
+        guid: queryGuid,
+        parentGuid: PlacesUtils.bookmarks.rootGuid,
+        index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+      });
+
+      leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
+      _(`Left pane root ID after restoring moved query: ${leftPaneId}`);
+
+      let changes = await tracker.promiseChangedIDs();
+      deepEqual(changes, {},
+        "Should not track left pane items after restoring moved query");
+    }
+
+    _("Add duplicate query");
+    {
+      let leftPaneGuid = await PlacesUtils.promiseItemGuid(leftPaneId);
+      let query = await PlacesUtils.bookmarks.insert({
+        parentGuid: leftPaneGuid,
+        url: `place:folder=TAGS`,
+      });
+      let queryId = await PlacesUtils.promiseItemId(query.guid);
+      await setAnnoUnchecked(queryId, PlacesUIUtils.ORGANIZER_QUERY_ANNO,
+                             "Tags", PlacesUtils.annotations.TYPE_STRING);
+
+      leftPaneId = PlacesUIUtils.maybeRebuildLeftPane();
+      _(`Left pane root ID after removing dupe query: ${leftPaneId}`);
+
+      let changes = await tracker.promiseChangedIDs();
+      deepEqual(changes, {},
+        "Should not track left pane items after removing dupe query");
+    }
+  } finally {
+    _("Clean up.");
+    await cleanup();
+  }
+});
+
 add_task(async function test_tracking() {
   _("Test starting and stopping the tracker");
 
   // Remove existing tracking information for roots.
   await startTracking();
 
   let folder = PlacesUtils.bookmarks.createFolder(
     PlacesUtils.bookmarks.bookmarksMenuFolder,
@@ -1521,49 +1680,43 @@ add_task(async function test_onItemDelet
     await verifyTrackedItems([fx_guid, tb_guid, folder1_guid, folder2_guid]);
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
   } finally {
     _("Clean up.");
     await cleanup();
   }
 });
 
-async function ensureMobileQuery() {
-  tracker._ensureMobileQuery();
-  await PlacesTestUtils.promiseAsyncUpdates();
-}
-
 add_task(async function test_mobile_query() {
   _("Ensure we correctly create the mobile query");
 
   try {
     await startTracking();
 
     // Creates the organizer queries as a side effect.
     let leftPaneId = PlacesUIUtils.leftPaneFolderId;
     _(`Left pane root ID: ${leftPaneId}`);
-    await PlacesTestUtils.promiseAsyncUpdates();
 
     let allBookmarksGuids = await fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
                                                      "AllBookmarks");
     equal(allBookmarksGuids.length, 1, "Should create folder with all bookmarks queries");
     let allBookmarkGuid = allBookmarksGuids[0];
 
     _("Try creating query after organizer is ready");
-    await ensureMobileQuery();
+    tracker._ensureMobileQuery();
     let queryGuids = await fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
                                               "MobileBookmarks");
     equal(queryGuids.length, 0, "Should not create query without any mobile bookmarks");
 
     _("Insert mobile bookmark, then create query");
     let mozBmk = await PlacesUtils.bookmarks.insert({
       parentGuid: PlacesUtils.bookmarks.mobileGuid,
       url: "https://mozilla.org",
     });
-    await ensureMobileQuery();
+    tracker._ensureMobileQuery();
     queryGuids = await fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
                                           "MobileBookmarks");
     equal(queryGuids.length, 1, "Should create query once mobile bookmarks exist");
 
     let queryGuid = queryGuids[0];
 
     let queryInfo = await PlacesUtils.bookmarks.fetch(queryGuid);
     equal(queryInfo.url, `place:folder=${PlacesUtils.mobileFolderId}`, "Query should point to mobile root");
@@ -1574,35 +1727,34 @@ add_task(async function test_mobile_quer
     await PlacesUtils.bookmarks.update({
       guid: PlacesUtils.bookmarks.mobileGuid,
       title: "renamed root",
     });
     await PlacesUtils.bookmarks.update({
       guid: queryGuid,
       title: "renamed query",
     });
-    await ensureMobileQuery();
+    tracker._ensureMobileQuery();
     let rootInfo = await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.mobileGuid);
     equal(rootInfo.title, "Mobile Bookmarks", "Should fix root title");
     queryInfo = await PlacesUtils.bookmarks.fetch(queryGuid);
     equal(queryInfo.title, "Mobile Bookmarks", "Should fix query title");
 
     _("Point query to different folder");
     await PlacesUtils.bookmarks.update({
       guid: queryGuid,
       url: "place:folder=BOOKMARKS_MENU",
     });
-    await ensureMobileQuery();
+    tracker._ensureMobileQuery();
     queryInfo = await PlacesUtils.bookmarks.fetch(queryGuid);
     equal(queryInfo.url.href, `place:folder=${PlacesUtils.mobileFolderId}`,
       "Should fix query URL to point to mobile root");
 
     _("We shouldn't track the query or the left pane root");
     await verifyTrackedItems([mozBmk.guid, "mobile"]);
-    do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 5);
   } finally {
     _("Clean up.");
     await cleanup();
   }
 });
 
 add_task(async function test_skip_migration() {
   await insertBookmarksToMigrate();
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -53,34 +53,28 @@ function cleanup() {
   engine._tracker.clearChangedIDs();
   engine._resetClient();
   // We don't finalize storage at cleanup, since we use the same clients engine
   // instance across all tests.
 }
 
 add_task(async function test_bad_hmac() {
   _("Ensure that Clients engine deletes corrupt records.");
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
   let deletedCollections = [];
   let deletedItems       = [];
   let callback = {
     __proto__: SyncServerCallback,
     onItemDeleted(username, coll, wboID) {
       deletedItems.push(coll + "/" + wboID);
     },
     onCollectionDeleted(username, coll) {
       deletedCollections.push(coll);
     }
   }
-  let server = serverForUsers({"foo": "password"}, contents, callback);
+  let server = serverForFoo(engine, callback);
   let user   = server.user("foo");
 
   function check_clients_count(expectedCount) {
     let stack = Components.stack.caller;
     let coll  = user.collection("clients");
 
     // Treat a non-existent collection as empty.
     equal(expectedCount, coll ? coll.count() : 0, stack);
@@ -207,23 +201,17 @@ add_task(async function test_properties(
     cleanup();
   }
 });
 
 add_task(async function test_full_sync() {
   _("Ensure that Clients engine fetches all records for each sync.");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
   let user   = server.user("foo");
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   let activeID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
     id: activeID,
@@ -278,23 +266,17 @@ add_task(async function test_full_sync()
       await promiseStopServer(server);
     }
   }
 });
 
 add_task(async function test_sync() {
   _("Ensure that Clients engine uploads a new client record once a week.");
 
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
   let user   = server.user("foo");
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   function clientWBO() {
     return user.collection("clients").wbo(engine.localID);
   }
@@ -364,23 +346,17 @@ add_task(async function test_client_name
 
   cleanup();
 });
 
 add_task(async function test_last_modified() {
   _("Ensure that remote records have a sane serverLastModified attribute.");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
   let user   = server.user("foo");
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   let activeID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
     id: activeID,
@@ -585,23 +561,17 @@ add_task(async function test_process_inc
 
   cleanup();
 });
 
 add_task(async function test_filter_duplicate_names() {
   _("Ensure that we exclude clients with identical names that haven't synced in a week.");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
   let user   = server.user("foo");
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   // Synced recently.
   let recentID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(recentID, encryptPayload({
@@ -742,23 +712,17 @@ add_task(async function test_filter_dupl
 });
 
 add_task(async function test_command_sync() {
   _("Ensure that commands are synced across clients.");
 
   engine._store.wipe();
   generateNewKeys(Service.collectionKeys);
 
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server   = serverForUsers({"foo": "password"}, contents);
+  let server   = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let user     = server.user("foo");
   let remoteId = Utils.makeGUID();
 
   function clientWBO(id) {
     return user.collection("clients").wbo(id);
   }
@@ -819,23 +783,17 @@ add_task(async function test_command_syn
 });
 
 add_task(async function test_clients_not_in_fxa_list() {
   _("Ensure that clients not in the FxA devices list are marked as stale.");
 
   engine._store.wipe();
   generateNewKeys(Service.collectionKeys);
 
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server   = serverForUsers({"foo": "password"}, contents);
+  let server   = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let user     = server.user("foo");
   let remoteId = Utils.makeGUID();
   let remoteId2 = Utils.makeGUID();
 
   _("Create remote client records");
   server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
@@ -1001,23 +959,17 @@ add_task(async function test_optional_cl
 
   cleanup();
 });
 
 add_task(async function test_merge_commands() {
   _("Verifies local commands for remote clients are merged with the server's");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   let desktopID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
     id: desktopID,
     name: "Desktop client",
@@ -1077,23 +1029,17 @@ add_task(async function test_merge_comma
     }
   }
 });
 
 add_task(async function test_duplicate_remote_commands() {
   _("Verifies local commands for remote clients are sent only once (bug 1289287)");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   let desktopID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(desktopID, encryptPayload({
     id: desktopID,
     name: "Desktop client",
@@ -1142,23 +1088,17 @@ add_task(async function test_duplicate_r
     }
   }
 });
 
 add_task(async function test_upload_after_reboot() {
   _("Multiple downloads, reboot, then upload (bug 1289287)");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   let deviceBID = Utils.makeGUID();
   let deviceCID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(deviceBID, encryptPayload({
     id: deviceBID,
@@ -1230,23 +1170,17 @@ add_task(async function test_upload_afte
     }
   }
 });
 
 add_task(async function test_keep_cleared_commands_after_reboot() {
   _("Download commands, fail upload, reboot, then apply new commands (bug 1289287)");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   let deviceBID = Utils.makeGUID();
   let deviceCID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(engine.localID, encryptPayload({
     id: engine.localID,
@@ -1354,23 +1288,17 @@ add_task(async function test_keep_cleare
     }
   }
 });
 
 add_task(async function test_deleted_commands() {
   _("Verifies commands for a deleted client are discarded");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   let activeID = Utils.makeGUID();
   server.insertWBO("foo", "clients", new ServerWBO(activeID, encryptPayload({
     id: activeID,
     name: "Active client",
@@ -1418,23 +1346,17 @@ add_task(async function test_deleted_com
     }
   }
 });
 
 add_task(async function test_send_uri_ack() {
   _("Ensure a sent URI is deleted when the client syncs");
 
   let now = Date.now() / 1000;
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server = serverForUsers({"foo": "password"}, contents);
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   generateNewKeys(Service.collectionKeys);
 
   try {
     let fakeSenderID = Utils.makeGUID();
 
     _("Initial sync for empty clients collection");
@@ -1481,23 +1403,17 @@ add_task(async function test_send_uri_ac
 });
 
 add_task(async function test_command_sync() {
   _("Notify other clients when writing their record.");
 
   engine._store.wipe();
   generateNewKeys(Service.collectionKeys);
 
-  let contents = {
-    meta: {global: {engines: {clients: {version: engine.version,
-                                        syncID: engine.syncID}}}},
-    clients: {},
-    crypto: {}
-  };
-  let server    = serverForUsers({"foo": "password"}, contents);
+  let server    = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let collection = server.getCollection("foo", "clients");
   let remoteId   = Utils.makeGUID();
   let remoteId2  = Utils.makeGUID();
 
   _("Create remote client record 1");
   server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
@@ -1552,23 +1468,17 @@ add_task(async function ensureSameFlowID
   let origRecordTelemetryEvent = Service.recordTelemetryEvent;
   Service.recordTelemetryEvent = (object, method, value, extra) => {
     events.push({ object, method, value, extra });
   }
 
   try {
     // Setup 2 clients, send them a command, and ensure we get to events
     // written, both with the same flowID.
-    let contents = {
-      meta: {global: {engines: {clients: {version: engine.version,
-                                          syncID: engine.syncID}}}},
-      clients: {},
-      crypto: {}
-    };
-    let server    = serverForUsers({"foo": "password"}, contents);
+    let server    = serverForFoo(engine);
     await SyncTestingInfrastructure(server);
 
     let remoteId   = Utils.makeGUID();
     let remoteId2  = Utils.makeGUID();
 
     _("Create remote client record 1");
     server.insertWBO("foo", "clients", new ServerWBO(remoteId, encryptPayload({
       id: remoteId,
--- a/services/sync/tests/unit/test_password_engine.js
+++ b/services/sync/tests/unit/test_password_engine.js
@@ -3,61 +3,26 @@ Cu.import("resource://services-sync/serv
 Cu.import("resource://testing-common/services/sync/utils.js");
 
 const LoginInfo = Components.Constructor(
   "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
 
 const PropertyBag = Components.Constructor(
   "@mozilla.org/hash-property-bag;1", Ci.nsIWritablePropertyBag);
 
-function serverForEngine(engine) {
-  let clientsEngine = Service.clientsEngine;
-  return serverForUsers({"foo": "password"}, {
-    meta: {
-      global: {
-        syncID: Service.syncID,
-        storageVersion: STORAGE_VERSION,
-        engines: {
-          clients: {
-            version: clientsEngine.version,
-            syncID: clientsEngine.syncID,
-          },
-          [engine.name]: {
-            version: engine.version,
-            syncID: engine.syncID,
-          },
-        },
-      },
-    },
-    crypto: {
-      keys: encryptPayload({
-        id: "keys",
-        // Generate a fake default key bundle to avoid resetting the client
-        // before the first sync.
-        default: [
-          Svc.Crypto.generateRandomKey(),
-          Svc.Crypto.generateRandomKey(),
-        ],
-      }),
-    },
-    [engine.name]: {},
-  });
-}
-
 add_task(async function test_password_engine() {
   _("Basic password sync test");
 
   let engine = Service.engineManager.get("passwords");
   let store = engine._store;
 
-  let server = serverForEngine(engine);
+  let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   let collection = server.user("foo").collection("passwords");
 
-  generateNewKeys(Service.collectionKeys);
   enableValidationPrefs();
 
   _("Add new login to upload during first sync");
   let newLogin;
   {
     let login = new LoginInfo("https://example.com", "", null, "username",
       "password", "", "");
     Services.logins.addLogin(login);
--- a/services/sync/tests/unit/test_telemetry.js
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -95,21 +95,17 @@ add_task(async function test_basic() {
 
   Svc.Prefs.resetBranch("");
   await promiseStopServer(server);
 });
 
 add_task(async function test_processIncoming_error() {
   let engine = new BookmarksEngine(Service);
   let store  = engine._store;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bookmarks: {version: engine.version,
-                                           syncID: engine.syncID}}}},
-    bookmarks: {}
-  });
+  let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   let collection = server.user("foo").collection("bookmarks");
   try {
     // Create a bogus record that when synced down will provoke a
     // network error which in turn provokes an exception in _processIncoming.
     const BOGUS_GUID = "zzzzzzzzzzzz";
     let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
     bogus_record.get = function get() {
@@ -149,21 +145,17 @@ add_task(async function test_processInco
     store.wipe();
     await cleanAndGo(engine, server);
   }
 });
 
 add_task(async function test_uploading() {
   let engine = new BookmarksEngine(Service);
   let store  = engine._store;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bookmarks: {version: engine.version,
-                                           syncID: engine.syncID}}}},
-    bookmarks: {}
-  });
+  let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
 
   let parent = PlacesUtils.toolbarFolderId;
   let uri = Utils.makeURI("http://getfirefox.com/");
 
   let bmk_id = PlacesUtils.bookmarks.insertBookmark(parent, uri,
     PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
 
@@ -333,21 +325,17 @@ add_task(async function test_sync_partia
 });
 
 add_task(async function test_generic_engine_fail() {
   enableValidationPrefs();
 
   Service.engineManager.register(SteamEngine);
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {steam: {version: engine.version,
-                                      syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   let e = new Error("generic failure message")
   engine._errToThrow = e;
 
   try {
     _(`test_generic_engine_fail: Steam tracker contents: ${
       JSON.stringify(engine._tracker.changedIDs)}`);
     let ping = await sync_and_validate_telem(true);
@@ -363,21 +351,17 @@ add_task(async function test_generic_eng
 });
 
 add_task(async function test_engine_fail_ioerror() {
   enableValidationPrefs();
 
   Service.engineManager.register(SteamEngine);
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {steam: {version: engine.version,
-                                      syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   // create an IOError to re-throw as part of Sync.
   try {
     // (Note that fakeservices.js has replaced Utils.jsonMove etc, but for
     // this test we need the real one so we get real exceptions from the
     // filesystem.)
     await Utils._real_jsonMove("file-does-not-exist", "anything", {});
   } catch (ex) {
@@ -405,22 +389,19 @@ add_task(async function test_initial_syn
   enableValidationPrefs();
 
   Service.engineManager.register(SteamEngine);
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
   let engines = {};
   // These are the only ones who actually have things to sync at startup.
   let engineNames = ["clients", "bookmarks", "prefs", "tabs"];
-  let conf = { meta: { global: { engines } } };
-  for (let e of engineNames) {
-    engines[e] = { version: engine.version, syncID: engine.syncID };
-    conf[e] = {};
-  }
-  let server = serverForUsers({"foo": "password"}, conf);
+  let server = serverForEnginesWithKeys({"foo": "password"}, ["bookmarks", "prefs", "tabs"].map(name =>
+    Service.engineManager.get(name)
+  ));
   await SyncTestingInfrastructure(server);
   try {
     _(`test_initial_sync_engines: Steam tracker contents: ${
       JSON.stringify(engine._tracker.changedIDs)}`);
     let ping = await wait_for_ping(() => Service.sync(), true);
 
     equal(ping.engines.find(e => e.name === "clients").outgoing[0].sent, 1);
     equal(ping.engines.find(e => e.name === "tabs").outgoing[0].sent, 1);
@@ -443,21 +424,17 @@ add_task(async function test_initial_syn
 });
 
 add_task(async function test_nserror() {
   enableValidationPrefs();
 
   Service.engineManager.register(SteamEngine);
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {steam: {version: engine.version,
-                                      syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   engine._errToThrow = Components.Exception("NS_ERROR_UNKNOWN_HOST", Cr.NS_ERROR_UNKNOWN_HOST);
   try {
     _(`test_nserror: Steam tracker contents: ${
       JSON.stringify(engine._tracker.changedIDs)}`);
     let ping = await sync_and_validate_telem(true);
     deepEqual(ping.status, {
       service: SYNC_FAILED_PARTIAL,
@@ -522,20 +499,17 @@ add_task(async function test_discarding(
 })
 
 add_task(async function test_no_foreign_engines_in_error_ping() {
   enableValidationPrefs();
 
   Service.engineManager.register(BogusEngine);
   let engine = Service.engineManager.get("bogus");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
   engine._errToThrow = new Error("Oh no!");
   await SyncTestingInfrastructure(server);
   try {
     let ping = await sync_and_validate_telem(true);
     equal(ping.status.service, SYNC_FAILED_PARTIAL);
     ok(ping.engines.every(e => e.name !== "bogus"));
   } finally {
     await cleanAndGo(engine, server);
@@ -544,21 +518,17 @@ add_task(async function test_no_foreign_
 });
 
 add_task(async function test_sql_error() {
   enableValidationPrefs();
 
   Service.engineManager.register(SteamEngine);
   let engine = Service.engineManager.get("steam");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {steam: {version: engine.version,
-                                      syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
   await SyncTestingInfrastructure(server);
   engine._sync = function() {
     // Just grab a DB connection and issue a bogus SQL statement synchronously.
     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
     Async.querySpinningly(db.createAsyncStatement("select bar from foo"));
   };
   try {
     _(`test_sql_error: Steam tracker contents: ${
@@ -573,20 +543,17 @@ add_task(async function test_sql_error()
 });
 
 add_task(async function test_no_foreign_engines_in_success_ping() {
   enableValidationPrefs();
 
   Service.engineManager.register(BogusEngine);
   let engine = Service.engineManager.get("bogus");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   try {
     let ping = await sync_and_validate_telem();
     ok(ping.engines.every(e => e.name !== "bogus"));
   } finally {
     await cleanAndGo(engine, server);
     Service.engineManager.unregister(engine);
@@ -594,20 +561,17 @@ add_task(async function test_no_foreign_
 });
 
 add_task(async function test_events() {
   enableValidationPrefs();
 
   Service.engineManager.register(BogusEngine);
   let engine = Service.engineManager.get("bogus");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   try {
     let serverTime = AsyncResource.serverTime;
     Service.recordTelemetryEvent("object", "method", "value", { foo: "bar" });
     let ping = await wait_for_ping(() => Service.sync(), true, true);
     equal(ping.events.length, 1);
     let [timestamp, category, method, object, value, extra] = ping.events[0];
@@ -641,20 +605,17 @@ add_task(async function test_events() {
 });
 
 add_task(async function test_invalid_events() {
   enableValidationPrefs();
 
   Service.engineManager.register(BogusEngine);
   let engine = Service.engineManager.get("bogus");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
 
   async function checkNotRecorded(...args) {
     Service.recordTelemetryEvent.call(args);
     let ping = await wait_for_ping(() => Service.sync(), false, true);
     equal(ping.events, undefined);
   }
 
   await SyncTestingInfrastructure(server);
@@ -690,20 +651,17 @@ add_task(async function test_no_ping_for
   enableValidationPrefs();
 
   let telem = get_sync_test_telemetry();
   let oldSubmit = telem.submit;
 
   Service.engineManager.register(BogusEngine);
   let engine = Service.engineManager.get("bogus");
   engine.enabled = true;
-  let server = serverForUsers({"foo": "password"}, {
-    meta: {global: {engines: {bogus: {version: engine.version, syncID: engine.syncID}}}},
-    steam: {}
-  });
+  let server = serverForFoo(engine);
 
   await SyncTestingInfrastructure(server);
   try {
     let submitPromise = new Promise(resolve => {
       telem.submit = function() {
         let result = oldSubmit.apply(this, arguments);
         resolve(result);
       };
--- a/servo/components/selectors/parser.rs
+++ b/servo/components/selectors/parser.rs
@@ -164,37 +164,16 @@ impl<Impl: SelectorImpl> SelectorInner<I
         }
 
         SelectorInner {
             complex: c,
             ancestor_hashes: hashes,
         }
     }
 
-    /// Creates a clone of this selector with everything to the left of
-    /// (and including) the rightmost ancestor combinator removed. So
-    /// the selector |span foo > bar + baz| will become |bar + baz|.
-    /// This is used for revalidation selectors in servo.
-    ///
-    /// The bloom filter hashes are copied, even though they correspond to
-    /// parts of the selector that have been stripped out, because they are
-    /// still useful for fast-rejecting the reduced selectors.
-    pub fn slice_to_first_ancestor_combinator(&self) -> Self {
-        let maybe_pos = self.complex.iter_raw()
-                            .position(|s| s.as_combinator()
-                                           .map_or(false, |c| c.is_ancestor()));
-        match maybe_pos {
-            None => self.clone(),
-            Some(index) => SelectorInner {
-                complex: self.complex.slice_to(index),
-                ancestor_hashes: self.ancestor_hashes.clone(),
-            },
-        }
-    }
-
     /// Creates a SelectorInner from a Vec of Components. Used in tests.
     pub fn from_vec(vec: Vec<Component<Impl>>) -> Self {
         let complex = ComplexSelector::from_vec(vec);
         Self::new(complex)
     }
 }
 
 #[derive(PartialEq, Eq, Hash, Clone)]
--- a/servo/components/style/stylist.rs
+++ b/servo/components/style/stylist.rs
@@ -22,17 +22,18 @@ use properties::INHERIT_ALL;
 use properties::PropertyDeclarationBlock;
 use restyle_hints::{RestyleHint, DependencySet};
 use rule_tree::{CascadeLevel, RuleTree, StrongRuleNode, StyleSource};
 use selector_parser::{SelectorImpl, PseudoElement, Snapshot};
 use selectors::Element;
 use selectors::bloom::BloomFilter;
 use selectors::matching::{AFFECTED_BY_STYLE_ATTRIBUTE, AFFECTED_BY_PRESENTATIONAL_HINTS};
 use selectors::matching::{ElementSelectorFlags, StyleRelations, matches_selector};
-use selectors::parser::{Component, Selector, SelectorInner, SelectorMethods, LocalName as LocalNameSelector};
+use selectors::parser::{Combinator, Component, Selector, SelectorInner, SelectorIter};
+use selectors::parser::{SelectorMethods, LocalName as LocalNameSelector};
 use selectors::visitor::SelectorVisitor;
 use shared_lock::{Locked, SharedRwLockReadGuard, StylesheetGuards};
 use sink::Push;
 use smallvec::VecLike;
 use std::borrow::Borrow;
 use std::collections::HashMap;
 use std::fmt;
 use std::hash::Hash;
@@ -370,20 +371,17 @@ impl Stylist {
                              rule.clone(),
                              self.rules_source_order,
                              selector.specificity));
     }
 
     #[inline]
     fn note_for_revalidation(&mut self, selector: &Selector<SelectorImpl>) {
         if needs_revalidation(selector) {
-            // For revalidation, we can skip everything left of the first ancestor
-            // combinator.
-            let revalidation_sel = selector.inner.slice_to_first_ancestor_combinator();
-            self.selectors_for_cache_revalidation.push(revalidation_sel);
+            self.selectors_for_cache_revalidation.push(selector.inner.clone());
         }
     }
 
     /// Computes the style for a given "precomputed" pseudo-element, taking the
     /// universal rules and applying them.
     ///
     /// If `inherit_all` is true, then all properties are inherited from the
     /// parent; otherwise, non-inherited properties are reset to their initial
@@ -918,28 +916,57 @@ impl Drop for Stylist {
         // list are indeed free.
         unsafe { self.rule_tree.gc(); }
     }
 }
 
 /// Visitor determine whether a selector requires cache revalidation.
 ///
 /// Note that we just check simple selectors and eagerly return when the first
-/// need for revalidation is found, so we don't need to store state on the visitor.
+/// need for revalidation is found, so we don't need to store state on the
+/// visitor.
+///
+/// Also, note that it's important to check the whole selector, due to cousins
+/// sharing arbitrarily deep in the DOM, not just the rightmost part of it
+/// (unfortunately, though).
+///
+/// With cousin sharing, we not only need to care about selectors in stuff like
+/// foo:first-child, but also about selectors like p:first-child foo, since the
+/// two parents may have shared style, and in that case we can test cousins
+/// whose matching depends on the selector up in the chain.
+///
+/// TODO(emilio): We can optimize when matching only siblings to only match the
+/// rightmost selector until a descendant combinator is found, I guess, and in
+/// general when we're sharing at depth `n`, to the `n + 1` sequences of
+/// descendant combinators.
+///
+/// I don't think that in presence of the bloom filter it's worth it, though.
 struct RevalidationVisitor;
 
 impl SelectorVisitor for RevalidationVisitor {
     type Impl = SelectorImpl;
 
-    /// Check whether a rightmost sequence of simple selectors containing this
-    /// simple selector to be explicitly matched against both the style sharing
-    /// cache entry and the candidate.
+
+    fn visit_complex_selector(&mut self,
+                              _: SelectorIter<SelectorImpl>,
+                              combinator: Option<Combinator>) -> bool {
+        let is_sibling_combinator =
+            combinator.map_or(false, |c| c.is_sibling());
+
+        !is_sibling_combinator
+    }
+
+
+    /// Check whether sequence of simple selectors containing this simple
+    /// selector to be explicitly matched against both the style sharing cache
+    /// entry and the candidate.
     ///
-    /// We use this for selectors that can have different matching behavior between
-    /// siblings that are otherwise identical as far as the cache is concerned.
+    /// We use this for selectors that can have different matching behavior
+    /// between siblings that are otherwise identical as far as the cache is
+    /// concerned.
     fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
         match *s {
             Component::AttrExists(_) |
             Component::AttrEqual(_, _, _) |
             Component::AttrIncludes(_, _) |
             Component::AttrDashMatch(_, _) |
             Component::AttrPrefixMatch(_, _) |
             Component::AttrSubstringMatch(_, _) |
@@ -965,33 +992,17 @@ impl SelectorVisitor for RevalidationVis
             }
         }
     }
 }
 
 /// Returns true if the given selector needs cache revalidation.
 pub fn needs_revalidation(selector: &Selector<SelectorImpl>) -> bool {
     let mut visitor = RevalidationVisitor;
-
-    // We only need to consider the rightmost sequence of simple selectors, so
-    // we can stop at the first combinator. This is because:
-    // * If it's an ancestor combinator, we can ignore everything to the left
-    //   because matching won't differ between siblings.
-    // * If it's a sibling combinator, then we know we need revalidation.
-    let mut iter = selector.inner.complex.iter();
-    for ss in &mut iter {
-        if !ss.visit(&mut visitor) {
-            return true;
-        }
-    }
-
-    // If none of the simple selectors in the rightmost sequence required
-    // revalidation, we need revalidation if and only if the combinator is a
-    // sibling combinator.
-    iter.next_sequence().map_or(false, |c| c.is_sibling())
+    !selector.visit(&mut visitor)
 }
 
 /// Map that contains the CSS rules for a specific PseudoElement
 /// (or lack of PseudoElement).
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 struct PerPseudoElementSelectorMap {
     /// Rules from user agent stylesheets
     user_agent: SelectorMap,
--- a/servo/ports/cef/lib.rs
+++ b/servo/ports/cef/lib.rs
@@ -1,15 +1,14 @@
 /* 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/. */
 
 #![allow(non_camel_case_types)]
 #![feature(box_syntax)]
-#![feature(core_intrinsics)]
 #![feature(link_args)]
 
 #[macro_use]
 extern crate log;
 
 extern crate servo;
 extern crate compositing;
 
--- a/servo/ports/cef/stubs.rs
+++ b/servo/ports/cef/stubs.rs
@@ -7,19 +7,17 @@
 //! crash.
 
 macro_rules! stub(
     ($name:ident) => (
         #[no_mangle]
         #[allow(non_snake_case)]
         pub extern "C" fn $name() {
             println!("CEF stub function called: {}", stringify!($name));
-            unsafe {
-                ::std::intrinsics::abort()
-            }
+            ::std::process::abort()
         }
     )
 );
 
 stub!(cef_add_cross_origin_whitelist_entry);
 stub!(cef_add_web_plugin_directory);
 stub!(cef_add_web_plugin_path);
 stub!(cef_begin_tracing);
--- a/servo/ports/servo/main.rs
+++ b/servo/ports/servo/main.rs
@@ -10,17 +10,17 @@
 //! This browser's implementation of `WindowMethods` is built on top
 //! of [glutin], the cross-platform OpenGL utility and windowing
 //! library.
 //!
 //! For the engine itself look next door in `components/servo/lib.rs`.
 //!
 //! [glutin]: https://github.com/tomaka/glutin
 
-#![feature(start, core_intrinsics)]
+#![feature(start)]
 
 #[cfg(target_os = "android")]
 extern crate android_injected_glue;
 extern crate backtrace;
 // The window backed by glutin
 extern crate glutin_app as app;
 #[macro_use]
 extern crate log;
@@ -53,28 +53,26 @@ pub mod platform {
     #[cfg(not(target_os = "macos"))]
     pub fn deinit() {}
 }
 
 #[cfg(not(target_os = "android"))]
 fn install_crash_handler() {
     use backtrace::Backtrace;
     use sig::ffi::Sig;
-    use std::intrinsics::abort;
+    use std::process::abort;
     use std::thread;
 
     fn handler(_sig: i32) {
         let name = thread::current()
             .name()
             .map(|n| format!(" for thread \"{}\"", n))
             .unwrap_or("".to_owned());
         println!("Stack trace{}\n{:?}", name, Backtrace::new());
-        unsafe {
-            abort();
-        }
+        abort();
     }
 
     signal!(Sig::SEGV, handler); // handle segfaults
     signal!(Sig::ILL, handler); // handle stack overflow and unsupported CPUs
     signal!(Sig::IOT, handler); // handle double panics
     signal!(Sig::BUS, handler); // handle invalid memory access
 }
 
--- a/servo/tests/unit/style/stylist.rs
+++ b/servo/tests/unit/style/stylist.rs
@@ -104,23 +104,21 @@ fn test_revalidation_selectors() {
         // Note: it would be nice to test :moz-any and the various other non-TS
         // pseudo classes supported by gecko, but we don't have access to those
         // in these unit tests. :-(
 
         // Sibling combinators.
         "span + div",
         "span ~ div",
 
-        // Revalidation selectors that will get sliced.
-        "td > h1[dir]",
-        "td > span + h1[dir]",
-        "table td > span + div ~ h1[dir]",
+        // Selectors in the ancestor chain (needed for cousin sharing).
+        "p:first-child span",
     ]).into_iter()
       .filter(|s| needs_revalidation(&s))
-      .map(|s| s.inner.slice_to_first_ancestor_combinator().complex)
+      .map(|s| s.inner.complex)
       .collect::<Vec<_>>();
 
     let reference = parse_selectors(&[
         // Attribute selectors.
         "div[foo]",
         "div:not([foo])",
         "div[foo = \"bar\"]",
         "div[foo ~= \"bar\"]",
@@ -142,20 +140,18 @@ fn test_revalidation_selectors() {
         "div:first-of-type",
         "div:last-of-type",
         "div:only-of-type",
 
         // Sibling combinators.
         "span + div",
         "span ~ div",
 
-        // Revalidation selectors that got sliced.
-        "h1[dir]",
-        "span + h1[dir]",
-        "span + div ~ h1[dir]",
+        // Selectors in the ancestor chain (needed for cousin sharing).
+        "p:first-child span",
     ]).into_iter()
       .map(|s| s.inner.complex)
       .collect::<Vec<_>>();
 
     assert_eq!(test.len(), reference.len());
     for (t, r) in test.into_iter().zip(reference.into_iter()) {
         assert_eq!(t, r)
     }
--- a/startupcache/test/TestStartupCache.cpp
+++ b/startupcache/test/TestStartupCache.cpp
@@ -22,16 +22,17 @@
 #include "nsIPrefBranch.h"
 #include "nsIPrefService.h"
 #include "nsIXPConnect.h"
 #include "nsThreadUtils.h"
 #include "prenv.h"
 #include "prio.h"
 #include "prprf.h"
 #include "mozilla/Maybe.h"
+#include "mozilla/Printf.h"
 #include "mozilla/UniquePtr.h"
 
 using namespace JS;
 
 using namespace mozilla::scache;
 using mozilla::UniquePtr;
 
 void
@@ -59,17 +60,17 @@ protected:
 };
 
 TestStartupCache::TestStartupCache()
 {
   NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mSCFile));
   mSCFile->AppendNative(NS_LITERAL_CSTRING("test-startupcache.tmp"));
   nsAutoCString path;
   mSCFile->GetNativePath(path);
-  char* env = PR_smprintf("MOZ_STARTUP_CACHE=%s", path.get());
+  char* env = mozilla::Smprintf("MOZ_STARTUP_CACHE=%s", path.get()).release();
   PR_SetEnv(env);
   // We intentionally leak `env` here because it is required by PR_SetEnv
   MOZ_LSAN_INTENTIONALLY_LEAK_OBJECT(env);
   StartupCache::GetSingleton()->InvalidateCache();
 }
 TestStartupCache::~TestStartupCache()
 {
   PR_SetEnv("MOZ_STARTUP_CACHE=");
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -309,17 +309,17 @@ EnvHasValue(const char *name)
   const char *val = PR_GetEnv(name);
   return (val && *val);
 }
 
 // Save the given word to the specified environment variable.
 static void
 SaveWordToEnv(const char *name, const nsACString & word)
 {
-  char *expr = PR_smprintf("%s=%s", name, PromiseFlatCString(word).get());
+  char *expr = Smprintf("%s=%s", name, PromiseFlatCString(word).get()).release();
   if (expr)
     PR_SetEnv(expr);
   // We intentionally leak |expr| here since it is required by PR_SetEnv.
 }
 
 // Save the path of the given file to the specified environment variable.
 static void
 SaveFileToEnv(const char *name, nsIFile *file)
@@ -2805,17 +2805,17 @@ static struct SavedVar {
   {"XUL_APP_FILE", nullptr}
 };
 
 static void SaveStateForAppInitiatedRestart()
 {
   for (auto & savedVar : gSavedVars) {
     const char *s = PR_GetEnv(savedVar.name);
     if (s)
-      savedVar.value = PR_smprintf("%s=%s", savedVar.name, s);
+      savedVar.value = Smprintf("%s=%s", savedVar.name, s).release();
   }
 }
 
 static void RestoreStateForAppInitiatedRestart()
 {
   for (auto & savedVar : gSavedVars) {
     if (savedVar.value)
       PR_SetEnv(savedVar.value);
@@ -4410,17 +4410,18 @@ XREMain::XRE_mainRun()
 
 #ifdef XP_WIN
   // Hack to sync up the various environment storages. XUL_APP_FILE is special
   // in that it comes from a different CRT (firefox.exe's static-linked copy).
   // Ugly details in http://bugzil.la/1175039#c27
   char appFile[MAX_PATH];
   if (GetEnvironmentVariableA("XUL_APP_FILE", appFile, sizeof(appFile))) {
     SmprintfPointer saved = mozilla::Smprintf("XUL_APP_FILE=%s", appFile);
-    PR_SetEnv(saved.get());
+    // We intentionally leak the string here since it is required by PR_SetEnv.
+    PR_SetEnv(saved.release());
   }
 #endif
 
   SaveStateForAppInitiatedRestart();
 
   // clear out any environment variables which may have been set
   // during the relaunch process now that we know we won't be relaunching.
   SaveToEnv("XRE_PROFILE_PATH=");
--- a/toolkit/xre/nsUpdateDriver.cpp
+++ b/toolkit/xre/nsUpdateDriver.cpp
@@ -19,16 +19,17 @@
 #include "prenv.h"
 #include "nsVersionComparator.h"
 #include "nsDirectoryServiceDefs.h"
 #include "nsThreadUtils.h"
 #include "nsIXULAppInfo.h"
 #include "mozilla/Preferences.h"
 #include "nsPrintfCString.h"
 #include "mozilla/DebugOnly.h"
+#include "mozilla/Printf.h"
 
 #ifdef XP_MACOSX
 #include "nsILocalFileMac.h"
 #include "nsCommandLineServiceMac.h"
 #include "MacLaunchHelper.h"
 #include "updaterfileutils_osx.h"
 #include "mozilla/Monitor.h"
 #endif
@@ -401,28 +402,25 @@ CopyUpdaterIntoUpdateDir(nsIFile *greDir
 #include "prprf.h"
 #define PATH_SEPARATOR ":"
 #define LD_LIBRARY_PATH_ENVVAR_NAME "LD_LIBRARY_PATH"
 static void
 AppendToLibPath(const char *pathToAppend)
 {
   char *pathValue = getenv(LD_LIBRARY_PATH_ENVVAR_NAME);
   if (nullptr == pathValue || '\0' == *pathValue) {
-    char *s = PR_smprintf("%s=%s", LD_LIBRARY_PATH_ENVVAR_NAME, pathToAppend);
+    // Leak the string because that is required by PR_SetEnv.
+    char *s = Smprintf("%s=%s", LD_LIBRARY_PATH_ENVVAR_NAME, pathToAppend).release();
     PR_SetEnv(s);
   } else if (!strstr(pathValue, pathToAppend)) {
-    char *s = PR_smprintf("%s=%s" PATH_SEPARATOR "%s",
-                    LD_LIBRARY_PATH_ENVVAR_NAME, pathToAppend, pathValue);
+    // Leak the string because that is required by PR_SetEnv.
+    char *s = Smprintf("%s=%s" PATH_SEPARATOR "%s",
+                       LD_LIBRARY_PATH_ENVVAR_NAME, pathToAppend, pathValue).release();
     PR_SetEnv(s);
   }
-
-  // The memory used by PR_SetEnv is not copied to the environment on all
-  // platform, it can be used by reference directly. So we purposely do not
-  // call PR_smprintf_free on s.  Subsequent calls to PR_SetEnv will free
-  // the old memory first.
 }
 #endif
 
 /**
  * Applies, switches, or stages an update.
  *
  * @param greDir       the GRE directory
  * @param updateDir    the update root directory
--- a/xpcom/ds/nsVariant.cpp
+++ b/xpcom/ds/nsVariant.cpp
@@ -9,16 +9,18 @@
 #include "prdtoa.h"
 #include <math.h>
 #include "nsCycleCollectionParticipant.h"
 #include "xpt_struct.h"
 #include "nsReadableUtils.h"
 #include "nsMemory.h"
 #include "nsString.h"
 #include "nsCRTGlue.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Printf.h"
 
 /***************************************************************************/
 // Helpers for static convert functions...
 
 static nsresult
 String2Double(const char* aString, double* aResult)
 {
   char* next;
@@ -741,17 +743,17 @@ nsDiscriminatedUnion::ConvertToID(nsID* 
   }
 }
 
 /***************************************************************************/
 
 nsresult
 nsDiscriminatedUnion::ToString(nsACString& aOutString) const
 {
-  char* ptr;
+  mozilla::SmprintfPointer pptr;
 
   switch (mType) {
     // all the stuff we don't handle...
     case nsIDataType::VTYPE_ASTRING:
     case nsIDataType::VTYPE_DOMSTRING:
     case nsIDataType::VTYPE_UTF8STRING:
     case nsIDataType::VTYPE_CSTRING:
     case nsIDataType::VTYPE_CHAR_STR:
@@ -773,70 +775,72 @@ nsDiscriminatedUnion::ToString(nsACStrin
     case nsIDataType::VTYPE_ARRAY:
     case nsIDataType::VTYPE_INTERFACE:
     case nsIDataType::VTYPE_INTERFACE_IS:
     default:
       return NS_ERROR_CANNOT_CONVERT_DATA;
 
     // nsID has its own text formatter.
 
-    case nsIDataType::VTYPE_ID:
-      ptr = u.mIDValue.ToString();
+    case nsIDataType::VTYPE_ID: {
+      char* ptr = u.mIDValue.ToString();
       if (!ptr) {
         return NS_ERROR_OUT_OF_MEMORY;
       }
       aOutString.Assign(ptr);
       free(ptr);
       return NS_OK;
+    }
 
-    // Can't use PR_smprintf for floats, since it's locale-dependent
+    // Can't use Smprintf for floats, since it's locale-dependent
 #define CASE__APPENDFLOAT_NUMBER(type_, member_)                        \
     case nsIDataType::type_ :                                           \
     {                                                                   \
         nsAutoCString str;                                              \
         str.AppendFloat(u.member_);                                     \
         aOutString.Assign(str);                                         \
         return NS_OK;                                                   \
     }
 
     CASE__APPENDFLOAT_NUMBER(VTYPE_FLOAT,  mFloatValue)
     CASE__APPENDFLOAT_NUMBER(VTYPE_DOUBLE, mDoubleValue)
 
 #undef CASE__APPENDFLOAT_NUMBER
 
-    // the rest can be PR_smprintf'd and use common code.
+    // the rest can be Smprintf'd and use common code.
 
 #define CASE__SMPRINTF_NUMBER(type_, format_, cast_, member_)          \
     case nsIDataType::type_:                                           \
-        ptr = PR_smprintf( format_ , (cast_) u.member_);               \
+        static_assert(sizeof(cast_) >= sizeof(u.member_),              \
+                      "size of type should be at least as big as member"); \
+        pptr = mozilla::Smprintf( format_ , (cast_) u.member_);        \
         break;
 
     CASE__SMPRINTF_NUMBER(VTYPE_INT8,   "%d",   int,      mInt8Value)
     CASE__SMPRINTF_NUMBER(VTYPE_INT16,  "%d",   int,      mInt16Value)
     CASE__SMPRINTF_NUMBER(VTYPE_INT32,  "%d",   int,      mInt32Value)
-    CASE__SMPRINTF_NUMBER(VTYPE_INT64,  "%lld", int64_t,  mInt64Value)
+    CASE__SMPRINTF_NUMBER(VTYPE_INT64,  "%" PRId64, int64_t,  mInt64Value)
 
     CASE__SMPRINTF_NUMBER(VTYPE_UINT8,  "%u",   unsigned, mUint8Value)
     CASE__SMPRINTF_NUMBER(VTYPE_UINT16, "%u",   unsigned, mUint16Value)
     CASE__SMPRINTF_NUMBER(VTYPE_UINT32, "%u",   unsigned, mUint32Value)
-    CASE__SMPRINTF_NUMBER(VTYPE_UINT64, "%llu", int64_t,  mUint64Value)
+    CASE__SMPRINTF_NUMBER(VTYPE_UINT64, "%" PRIu64, int64_t,  mUint64Value)
 
     // XXX Would we rather print "true" / "false" ?
     CASE__SMPRINTF_NUMBER(VTYPE_BOOL,   "%d",   int,      mBoolValue)
 
     CASE__SMPRINTF_NUMBER(VTYPE_CHAR,   "%c",   char,     mCharValue)
 
 #undef CASE__SMPRINTF_NUMBER
   }
 
-  if (!ptr) {
+  if (!pptr) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
-  aOutString.Assign(ptr);
-  PR_smprintf_free(ptr);
+  aOutString.Assign(pptr.get());
   return NS_OK;
 }
 
 nsresult
 nsDiscriminatedUnion::ConvertToAString(nsAString& aResult) const
 {
   switch (mType) {
     case nsIDataType::VTYPE_ASTRING: