Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 16 Sep 2016 14:35:12 -0700
changeset 357828 1e1c3173c058956b225823c67f7376fb2757ce89
parent 357827 4e8e39b7c8ed539688c6b7bf4b3e8ebff013e936 (current diff)
parent 357759 b401cb17167b34c362eb819259effbb3c0979f59 (diff)
child 357829 70565fcf27279b6abfc058f88ee27ad3ad99855b
push id1324
push usermtabara@mozilla.com
push dateMon, 16 Jan 2017 13:07:44 +0000
treeherdermozilla-release@a01c49833940 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound, a=merge
gfx/layers/Layers.cpp
toolkit/modules/FinderHighlighter.jsm
--- a/browser/base/content/browser-media.js
+++ b/browser/base/content/browser-media.js
@@ -214,21 +214,27 @@ let gDecoderDoctorHandler = {
       }
       return gNavigatorBundle.getString("decoder.noCodecs.message");
     }
     if (type == "adobe-cdm-not-activated" &&
         AppConstants.platform == "win") {
       if (AppConstants.isPlatformAndVersionAtMost("win", "5.9")) {
         return gNavigatorBundle.getString("decoder.noCodecsXP.message");
       }
+      if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.1")) {
+        return gNavigatorBundle.getString("decoder.noCodecsVista.message");
+      }
       return gNavigatorBundle.getString("decoder.noCodecs.message");
     }
     if (type == "platform-decoder-not-found") {
+      if (AppConstants.isPlatformAndVersionAtLeast("win", "6.1")) {
+        return gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+      }
       if (AppConstants.isPlatformAndVersionAtLeast("win", "6")) {
-        return gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+        return gNavigatorBundle.getString("decoder.noHWAccelerationVista.message");
       }
       if (AppConstants.platform == "linux") {
         return gNavigatorBundle.getString("decoder.noCodecsLinux.message");
       }
     }
     return "";
   },
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -123,18 +123,18 @@ tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tab");
 }
 
 .tabbrowser-tab:not([pinned]) {
   -moz-box-flex: 100;
   max-width: 210px;
   min-width: 100px;
   width: 0;
-  transition: min-width 200ms ease-out,
-              max-width 230ms ease-out;
+  transition: min-width 100ms ease-out,
+              max-width 100ms ease-out;
 }
 
 .tabbrowser-tab:not([pinned]):not([fadein]) {
   max-width: 0.1px;
   min-width: 0.1px;
   visibility: hidden;
 }
 
--- a/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
+++ b/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
@@ -1,23 +1,90 @@
-add_task(function*() {
-  registerCleanupFunction(function* () {
-    yield PlacesUtils.bookmarks.remove(bm);
-  });
+// The order of these tests matters!
 
+add_task(function* setup () {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
   let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: "http://example.com/?q=%s",
                                                 title: "test" });
+  registerCleanupFunction(function* () {
+    yield PlacesUtils.bookmarks.remove(bm);
+    yield BrowserTestUtils.removeTab(tab);
+  });
   yield PlacesUtils.keywords.insert({ keyword: "keyword",
                                       url: "http://example.com/?q=%s" });
+  // Needs at least one success.
+  ok(true, "Setup complete");
+});
 
-  yield new Promise(resolve => waitForFocus(resolve, window));
-
+add_task(function* test_keyword() {
   yield promiseAutocompleteResultPopup("keyword bear");
   gURLBar.focus();
   EventUtils.synthesizeKey("d", {});
   EventUtils.synthesizeKey("VK_RETURN", {});
+  info("wait for the page to load");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+                                      false, "http://example.com/?q=beard");
+});
 
-  yield promiseTabLoadEvent(gBrowser.selectedTab);
-  is(gBrowser.selectedBrowser.currentURI.spec,
-     "http://example.com/?q=beard",
-     "Latest typed characters should have been used");
+add_task(function* test_sametext() {
+  yield promiseAutocompleteResultPopup("example.com", window, true);
+
+  // Simulate re-entering the same text searched the last time. This may happen
+  // through a copy paste, but clipboard handling is not much reliable, so just
+  // fire an input event.
+  info("synthesize input event");
+  let event = document.createEvent("Events");
+  event.initEvent("input", true, true);
+  gURLBar.dispatchEvent(event);
+  EventUtils.synthesizeKey("VK_RETURN", {});
+
+  info("wait for the page to load");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+                                       false, "http://example.com/");
+});
+
+add_task(function* test_after_empty_search() {
+  yield promiseAutocompleteResultPopup("");
+  gURLBar.focus();
+  gURLBar.value = "e";
+  EventUtils.synthesizeKey("x", {});
+  EventUtils.synthesizeKey("VK_RETURN", {});
+
+  info("wait for the page to load");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+                                       false, "http://example.com/");
 });
+
+add_task(function* test_disabled_ac() {
+  // Disable autocomplete.
+  let suggestHistory = Preferences.get("browser.urlbar.suggest.history");
+  Preferences.set("browser.urlbar.suggest.history", false);
+  let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark");
+  Preferences.set("browser.urlbar.suggest.bookmark", false);
+  let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage");
+  Preferences.set("browser.urlbar.suggest.openpages", false);
+
+  Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+  let engine = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engine;
+
+  registerCleanupFunction(function* () {
+    Preferences.set("browser.urlbar.suggest.history", suggestHistory);
+    Preferences.set("browser.urlbar.suggest.bookmark", suggestBookmarks);
+    Preferences.set("browser.urlbar.suggest.openpage", suggestOpenPages);
+
+    Services.search.currentEngine = originalEngine;
+    let engine = Services.search.getEngineByName("MozSearch");
+    Services.search.removeEngine(engine);
+  });
+
+  gURLBar.focus();
+  gURLBar.value = "e";
+  EventUtils.synthesizeKey("x", {});
+  EventUtils.synthesizeKey("VK_RETURN", {});
+
+  info("wait for the page to load");
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedTab.linkedBrowser,
+                                       false, "http://example.com/?q=ex");
+});
--- a/browser/base/content/test/urlbar/browser_canonizeURL.js
+++ b/browser/base/content/test/urlbar/browser_canonizeURL.js
@@ -11,16 +11,23 @@ var pairs = [
   ["example.foo/bar ", "http://example.foo/bar"],
   ["1.1.1.1", "http://1.1.1.1/"],
   ["ftp://example", "ftp://example/"],
   ["ftp.example.bar", "http://ftp.example.bar/"],
   ["ex ample", Services.search.defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec],
 ];
 
 add_task(function*() {
+  // Disable autoFill for this test, since it could mess up the results.
+  let autoFill = Preferences.get("browser.urlbar.autoFill");
+  Preferences.set("browser.urlbar.autoFill", false);
+  registerCleanupFunction(() => {
+    Preferences.set("browser.urlbar.autoFill", autoFill);
+  });
+
   for (let [inputValue, expectedURL] of pairs) {
     let focusEventPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
     let messagePromise = BrowserTestUtils.waitForMessage(gBrowser.selectedBrowser.messageManager,
                                                          "browser_canonizeURL:start");
 
     let stoppedLoadPromise = ContentTask.spawn(gBrowser.selectedBrowser, [inputValue, expectedURL],
       function([inputValue, expectedURL]) {
         return new Promise(resolve => {
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -5,16 +5,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
   "resource://testing-common/PlacesTestUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
   "resource:///modules/ContentCrashHandlers.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+  "resource://gre/modules/Preferences.jsm");
 
 function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
   retryTimes = typeof retryTimes !== 'undefined' ?  retryTimes : 30;
   var tries = 0;
   var interval = setInterval(function() {
     if (tries >= retryTimes) {
       ok(false, errorMsg);
       moveOn();
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -121,17 +121,23 @@ file, You can obtain one at http://mozil
         this.inputField.removeEventListener("mousemove", this, false);
         this.inputField.removeEventListener("mouseout", this, false);
         this.inputField.removeEventListener("overflow", this, false);
         this.inputField.removeEventListener("underflow", this, false);
       ]]></destructor>
 
       <field name="_value">""</field>
       <field name="gotResultForCurrentQuery">false</field>
-      <field name="handleEnterWhenGotResult">false</field>
+
+      <!--
+        This is set around HandleHenter so it can be used in handleCommand.
+        It is also used to track whether we must handle a delayed handleEnter,
+        by checking if it has been cleared.
+      -->
+      <field name="handleEnterInstance">null</field>
 
       <!--
         For performance reasons we want to limit the size of the text runs we
         build and show to the user.
       -->
       <field name="textRunsMaxLen">255</field>
 
       <!--
@@ -382,16 +388,20 @@ file, You can obtain one at http://mozil
                              event &&
                              event.altKey &&
                              !isTabEmpty(gBrowser.selectedTab);
               where = altEnter ? "tab" : "current";
             }
           }
 
           let url = this.value;
+          if (!url) {
+            return;
+          }
+
           let mayInheritPrincipal = false;
           let postData = null;
           let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
           let matchLastLocationChange = true;
 
           let action = this._parseActionUrl(url);
           if (action) {
             switch (action.type) {
@@ -447,35 +457,34 @@ file, You can obtain one at http://mozil
                 this._recordSearchEngineLoad(selectedOneOff.engine, value,
                                              event, where, openUILinkParams);
               this._loadURL(url, postData, where, openUILinkParams,
                             matchLastLocationChange, mayInheritPrincipal);
               return;
             }
           }
 
-          this._canonizeURL(event, response => {
-            [url, postData, mayInheritPrincipal] = response;
+          url = this._canonizeURL(event, url);
+          getShortcutOrURIAndPostData(url).then(({url, postData, mayInheritPrincipal}) => {
             if (url) {
               matchLastLocationChange =
-                lastLocationChange ==
-                gBrowser.selectedBrowser.lastLocationChange;
+                lastLocationChange == gBrowser.selectedBrowser.lastLocationChange;
               this._loadURL(url, postData, where, openUILinkParams,
                             matchLastLocationChange, mayInheritPrincipal);
             }
           });
         ]]></body>
       </method>
 
       <property name="oneOffSearchQuery">
         <getter><![CDATA[
           // this.textValue may be an autofilled string.  Search only with the
           // portion that the user typed, if any, by preferring the autocomplete
-          // controller's searchString (including _searchStringOnHandleEnter).
-          return this._searchStringOnHandleEnter ||
+          // controller's searchString (including handleEnterInstance.searchString).
+          return (this.handleEnterInstance && this.handleEnterInstance.searchString) ||
                  this.mController.searchString ||
                  this.textValue;
         ]]></getter>
       </property>
 
       <method name="_loadURL">
         <parameter name="url"/>
         <parameter name="postData"/>
@@ -562,23 +571,19 @@ file, You can obtain one at http://mozil
               .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
           let submission = engine.getSubmission(query, null, "keyword");
           return [submission.uri.spec, submission.postData];
         ]]></body>
       </method>
 
       <method name="_canonizeURL">
         <parameter name="aTriggeringEvent"/>
-        <parameter name="aCallback"/>
+        <parameter name="aUrl"/>
         <body><![CDATA[
-          var url = this.value;
-          if (!url) {
-            aCallback(["", null, false]);
-            return;
-          }
+          let url = aUrl;
 
           // Only add the suffix when the URL bar value isn't already "URL-like",
           // and only if we get a keyboard event, to match user expectations.
           if (/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(url) &&
               (aTriggeringEvent instanceof KeyEvent)) {
             let accel = this.AppConstants.platform == "macosx" ?
                         aTriggeringEvent.metaKey :
                         aTriggeringEvent.ctrlKey;
@@ -619,19 +624,17 @@ file, You can obtain one at http://mozil
               } else {
                 url = url + suffix;
               }
 
               url = "http://www." + url;
             }
           }
 
-          getShortcutOrURIAndPostData(url).then(data => {
-            aCallback([data.url, data.postData, data.mayInheritPrincipal]);
-          });
+          return url;
         ]]></body>
       </method>
 
       <field name="_contentIsCropped">false</field>
 
       <method name="_initURLTooltip">
         <body><![CDATA[
           if (this.focused || !this._contentIsCropped)
@@ -1000,18 +1003,22 @@ file, You can obtain one at http://mozil
 
       <method name="onInput">
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (!this.mIgnoreInput && this.mController.input == this) {
             this._value = this.inputField.value;
             gBrowser.userTypedValue = this.value;
             this.valueIsTyped = true;
-            this.gotResultForCurrentQuery = false;
-            this.mController.handleText();
+            // Only wait for a result when we are sure to get one.  In some
+            // cases, like when pasting the same exact text, we may not fire
+            // a new search and we won't get a result.
+            if (this.mController.handleText()) {
+              this.gotResultForCurrentQuery = false;
+            }
           }
           this.resetActionType();
         ]]></body>
       </method>
 
       <method name="handleEnter">
         <parameter name="event"/>
         <body><![CDATA[
@@ -1024,41 +1031,46 @@ file, You can obtain one at http://mozil
           // result selected.
           // If anything other than the default (first) result is selected, then
           // it must have been manually selected by the human. We let this
           // explicit choice be used, even if it may be related to a previous
           // input.
           // However, if the default result is automatically selected, we
           // ensure that it corresponds to the current input.
 
+          // Store the current search string so it can be used in
+          // handleCommand, which will be called as a result of
+          // mController.handleEnter().
+          // Note this is also used to detect if we should perform a delayed
+          // handleEnter, in such a case it won't have been cleared.
+          this.handleEnterInstance = {
+            searchString: this.mController.searchString,
+            event: event
+          };
+
           if (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery) {
-            // Store the current search string so it can be used in
-            // handleCommand, which will be called as a result of
-            // mController.handleEnter().  handleEnter will reset it.
-            this._searchStringOnHandleEnter = this.mController.searchString;
             let rv = this.mController.handleEnter(false, event);
-            delete this._searchStringOnHandleEnter;
+            this.handleEnterInstance = null;
             return rv;
           }
 
-          this.handleEnterWhenGotResult = true;
-
           return true;
         ]]></body>
       </method>
 
       <method name="handleDelete">
         <body><![CDATA[
           // If the heuristic result is selected, then the autocomplete
           // controller's handleDelete implementation will remove it, which is
           // not what we want.  So in that case, call handleText so it acts as
           // a backspace on the text value instead of removing the result.
           if (this.popup.selectedIndex == 0 &&
               this.popup._isFirstResultHeuristic) {
-            return this.mController.handleText();
+            this.mController.handleText();
+            return false;
           }
           return this.mController.handleDelete();
         ]]></body>
       </method>
 
       <field name="_userMadeSearchSuggestionsChoice"><![CDATA[
         false
       ]]></field>
@@ -1746,37 +1758,58 @@ file, You can obtain one at http://mozil
       </method>
 
       <method name="onResultsAdded">
         <body>
           <![CDATA[
             // If nothing is selected yet, select the first result if it is a
             // pre-selected "heuristic" result.  (See UnifiedComplete.js.)
             if (this.selectedIndex == -1 && this._isFirstResultHeuristic) {
-              // Don't handle this as a user-initiated action.
-              this._ignoreNextSelect = true;
-
               // Don't fire DOMMenuItemActive so that screen readers still see
               // the input as being focused.
               this.richlistbox.suppressMenuItemEvent = true;
-
               this.selectedIndex = 0;
               this.richlistbox.suppressMenuItemEvent = false;
-              this._ignoreNextSelect = false;
             }
 
             this.input.gotResultForCurrentQuery = true;
-            if (this.input.handleEnterWhenGotResult) {
-              this.input.handleEnterWhenGotResult = false;
-              this.input.mController.handleEnter(false);
+
+            // Check if we should perform a delayed handleEnter.
+            if (this.input.handleEnterInstance) {
+              try {
+                // Safety check: handle only if the search string didn't change.
+                let instance = this.input.handleEnterInstance;
+                if (this.input.mController.searchString == instance.searchString) {
+                  this.input.mController.handleEnter(false, instance.event);
+                }
+              } finally {
+                this.input.handleEnterInstance = null;
+              }
             }
           ]]>
         </body>
       </method>
 
+      <method name="_onSearchBegin">
+        <body><![CDATA[
+          // Set the selected index to 0 (heuristic) until a result comes back
+          // and we can evaluate it better.
+          //
+          // This is required to properly manage delayed handleEnter:
+          // 1. if a search starts we set selectedIndex to 0 here, and it will
+          //    be updated by onResultsAdded. Since selectedIndex is 0,
+          //    handleEnter will delay the action if a result didn't arrive yet.
+          // 2. if a search doesn't start (for example if autocomplete is
+          //    disabled), this won't be called, and the selectedIndex will be
+          //    the default -1 value. Then handleEnter will know it should not
+          //    delay the action, cause a result wont't ever arrive.
+          this.selectedIndex = 0;
+        ]]></body>
+      </method>
+
     </implementation>
     <handlers>
 
       <handler event="SelectedOneOffButtonChanged"><![CDATA[
         this._selectedOneOffChanged();
       ]]></handler>
 
       <handler event="mousedown"><![CDATA[
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -306,29 +306,69 @@ function GetBookmarksResource(aProfileFo
 }
 
 function GetHistoryResource(aProfileFolder) {
   let historyFile = aProfileFolder.clone();
   historyFile.append("History");
   if (!historyFile.exists())
     return null;
 
+  function getRows(dbOptions) {
+    const RETRYLIMIT = 10;
+    const RETRYINTERVAL = 100;
+    return Task.spawn(function* innerGetRows() {
+      let rows = null;
+      for (let retryCount = RETRYLIMIT; retryCount && !rows; retryCount--) {
+        // Attempt to get the rows. If this succeeds, we will bail out of the loop,
+        // close the database in a failsafe way, and pass the rows back.
+        // If fetching the rows throws, we will wait RETRYINTERVAL ms
+        // and try again. This will repeat a maximum of RETRYLIMIT times.
+        let db;
+        let didOpen = false;
+        let exceptionSeen;
+        try {
+          db = yield Sqlite.openConnection(dbOptions);
+          didOpen = true;
+          rows = yield db.execute(`SELECT url, title, last_visit_time, typed_count
+                                   FROM urls WHERE hidden = 0`);
+        } catch (ex) {
+          if (!exceptionSeen) {
+            Cu.reportError(ex);
+          }
+          exceptionSeen = ex;
+        } finally {
+          try {
+            if (didOpen) {
+              yield db.close();
+            }
+          } catch (ex) {}
+        }
+        if (exceptionSeen) {
+          yield new Promise(resolve => setTimeout(resolve, RETRYINTERVAL));
+        }
+      }
+      if (!rows) {
+        throw new Error("Couldn't get rows from the Chrome history database.");
+      }
+      return rows;
+    });
+  }
+
   return {
     type: MigrationUtils.resourceTypes.HISTORY,
 
     migrate(aCallback) {
       Task.spawn(function* () {
-        let db = yield Sqlite.openConnection({
+        let dbOptions = {
+          readOnly: true,
+          ignoreLockingMode: true,
           path: historyFile.path
-        });
+        };
 
-        let rows = yield db.execute(`SELECT url, title, last_visit_time, typed_count
-                                     FROM urls WHERE hidden = 0`);
-        yield db.close();
-
+        let rows = yield getRows(dbOptions);
         let places = [];
         for (let row of rows) {
           try {
             // if having typed_count, we changes transition type to typed.
             let transType = PlacesUtils.history.TRANSITION_LINK;
             if (row.getResultByName("typed_count") > 0)
               transType = PlacesUtils.history.TRANSITION_TYPED;
 
--- a/devtools/client/framework/toolbox-window.xul
+++ b/devtools/client/framework/toolbox-window.xul
@@ -37,10 +37,11 @@
          modifiers="accel,alt"
          disabled="true"/>
     <key id="toolbox-key-toggle-F12"
          keycode="&toggleToolboxF12.keycode;"
          keytext="&toggleToolboxF12.keytext;"
          command="toolbox-cmd-close"/>
   </keyset>
 
-  <iframe id="toolbox-iframe" flex="1" forceOwnRefreshDriver=""></iframe>
+  <tooltip id="aHTMLTooltip" page="true"/>
+  <iframe id="toolbox-iframe" flex="1" forceOwnRefreshDriver="" tooltip="aHTMLTooltip"></iframe>
 </window>
--- a/devtools/client/shared/view-source.js
+++ b/devtools/client/shared/view-source.js
@@ -57,17 +57,17 @@ exports.viewSourceInDebugger = Task.asyn
   let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
   let { panelWin: dbg } = yield toolbox.loadTool("jsdebugger");
 
   // New debugger frontend
   if (Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) {
     yield toolbox.selectTool("jsdebugger");
     // TODO: Properly handle case where source will never exist in the
     // debugger
-    dbg.actions.selectSourceURL(sourceURL);
+    dbg.actions.selectSourceURL(sourceURL, { line: sourceLine });
     return true;
   }
 
   // Old debugger frontend
   if (!debuggerAlreadyOpen) {
     yield dbg.DebuggerController.waitForSourcesLoaded();
   }
 
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -1035,22 +1035,19 @@ void HTMLMediaElement::AbortExistingLoad
   if (mTextTrackManager) {
     mTextTrackManager->NotifyReset();
   }
 
   mEventDeliveryPaused = false;
   mPendingEvents.Clear();
 }
 
-void HTMLMediaElement::NoSupportedMediaSourceError()
-{
-  NS_ASSERTION(mNetworkState == NETWORK_LOADING,
-               "Not loading during source selection?");
-
-  mError = new MediaError(this, MEDIA_ERR_SRC_NOT_SUPPORTED);
+void HTMLMediaElement::NoSupportedMediaSourceError(const nsACString& aErrorDetails)
+{
+  mError = new MediaError(this, MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails);
   ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_NO_SOURCE);
   DispatchAsyncEvent(NS_LITERAL_STRING("error"));
   ChangeDelayLoadStatus(false);
   UpdateAudioChannelPlayingState();
   OpenUnsupportedMediaWithExtenalAppIfNeeded();
 }
 
 typedef void (HTMLMediaElement::*SyncSectionFn)();
@@ -4411,17 +4408,21 @@ void HTMLMediaElement::FirstFrameLoaded(
   }
 }
 
 void HTMLMediaElement::NetworkError()
 {
   if (mDecoder) {
     ShutdownDecoder();
   }
-  Error(MEDIA_ERR_NETWORK);
+  if (mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING) {
+    NoSupportedMediaSourceError();
+  } else {
+    Error(MEDIA_ERR_NETWORK);
+  }
 }
 
 void HTMLMediaElement::DecodeError(const MediaResult& aError)
 {
   nsAutoString src;
   GetCurrentSrc(src);
   const char16_t* params[] = { src.get() };
   ReportLoadError("MediaLoadDecodeError", params, ArrayLength(params));
@@ -4437,58 +4438,53 @@ void HTMLMediaElement::DecodeError(const
   if (mIsLoadingFromSourceChildren) {
     mError = nullptr;
     if (mSourceLoadCandidate) {
       DispatchAsyncSourceError(mSourceLoadCandidate);
       QueueLoadFromSourceTask();
     } else {
       NS_WARNING("Should know the source we were loading from!");
     }
+  } else if (mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING) {
+    NoSupportedMediaSourceError(aError.Description());
   } else {
-    Error(MEDIA_ERR_DECODE, aError);
+    Error(MEDIA_ERR_DECODE, aError.Description());
   }
 }
 
 bool HTMLMediaElement::HasError() const
 {
   return GetError();
 }
 
 void HTMLMediaElement::LoadAborted()
 {
   Error(MEDIA_ERR_ABORTED);
 }
 
 void HTMLMediaElement::Error(uint16_t aErrorCode,
-                             const MediaResult& aErrorDetails)
+                             const nsACString& aErrorDetails)
 {
   NS_ASSERTION(aErrorCode == MEDIA_ERR_DECODE ||
                aErrorCode == MEDIA_ERR_NETWORK ||
                aErrorCode == MEDIA_ERR_ABORTED,
                "Only use MediaError codes!");
+  NS_ASSERTION(mReadyState > HAVE_NOTHING,
+               "Shouldn't be called when readyState is HAVE_NOTHING");
 
   // Since we have multiple paths calling into DecodeError, e.g.
   // MediaKeys::Terminated and EMEH264Decoder::Error. We should take the 1st
   // one only in order not to fire multiple 'error' events.
   if (mError) {
     return;
   }
-  nsCString message;
-  if (NS_FAILED(aErrorDetails)) {
-    message = aErrorDetails.Description();
-  }
-  mError = new MediaError(this, aErrorCode, message);
+  mError = new MediaError(this, aErrorCode, aErrorDetails);
 
   DispatchAsyncEvent(NS_LITERAL_STRING("error"));
-  if (mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING) {
-    ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY);
-    DispatchAsyncEvent(NS_LITERAL_STRING("emptied"));
-  } else {
-    ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_IDLE);
-  }
+  ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_IDLE);
   ChangeDelayLoadStatus(false);
   UpdateAudioChannelPlayingState();
 }
 
 void HTMLMediaElement::PlaybackEnded()
 {
   // We changed state which can affect AddRemoveSelfReference
   AddRemoveSelfReference();
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -956,17 +956,17 @@ protected:
    */
   void AbortExistingLoads();
 
   /**
    * Called when all potential resources are exhausted. Changes network
    * state to NETWORK_NO_SOURCE, and sends error event with code
    * MEDIA_ERR_SRC_NOT_SUPPORTED.
    */
-  void NoSupportedMediaSourceError();
+  void NoSupportedMediaSourceError(const nsACString& aErrorDetails = nsCString());
 
   /**
    * Attempts to load resources from the <source> children. This is a
    * substep of the resource selection algorithm. Do not call this directly,
    * call QueueLoadFromSourceTask() instead.
    */
   void LoadFromSourceChildren();
 
@@ -1122,17 +1122,17 @@ protected:
    * Dispatches an error event to a child source element.
    */
   void DispatchAsyncSourceError(nsIContent* aSourceElement);
 
   /**
    * Resets the media element for an error condition as per aErrorCode.
    * aErrorCode must be one of nsIDOMHTMLMediaError codes.
    */
-  void Error(uint16_t aErrorCode, const MediaResult& aErrorDetails = NS_OK);
+  void Error(uint16_t aErrorCode, const nsACString& aErrorDetails = nsCString());
 
   /**
    * Returns the URL spec of the currentSrc.
    **/
   void GetCurrentSpec(nsCString& aString);
 
   /**
    * Process any media fragment entries in the URI
--- a/dom/media/MediaResult.h
+++ b/dom/media/MediaResult.h
@@ -43,16 +43,19 @@ public:
 
   // Interoperations with nsresult.
   bool operator==(nsresult aResult) const { return aResult == mCode; }
   bool operator!=(nsresult aResult) const { return aResult != mCode; }
   operator nsresult () const { return mCode; }
 
   nsCString Description() const
   {
+    if (NS_SUCCEEDED(mCode)) {
+      return nsCString();
+    }
     return nsPrintfCString("0x%08x: %s", mCode, mMessage.get());
   }
 
 private:
   nsresult mCode;
   nsCString mMessage;
 };
 
--- a/dom/media/test/test_decode_error.html
+++ b/dom/media/test/test_decode_error.html
@@ -21,17 +21,21 @@ function startTest(test, token) {
 
   var v = document.createElement("video");
   manager.started(token);
   v.addEventListener("error", function (event) {
     var el = event.currentTarget;
     is(event.type, "error", "Expected event of type 'error'");
     ok(el.error, "Element 'error' attr expected to have a value");
     ok(el.error instanceof MediaError, "Element 'error' attr expected to be MediaError");
-    is(el.error.code, MediaError.MEDIA_ERR_DECODE, "Expected a decode error");
+    if (v.readyState == v.HAVE_NOTHING) {
+      is(el.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, "Expected media not supported error");
+    } else {
+      is(el.error.code, MediaError.MEDIA_ERR_DECODE, "Expected a decode error");
+    }
     ok(typeof el.error.message === 'string' || el.error.essage instanceof String, "Element 'message' attr expected to be a string");
     ok(el.error.message.length > 0, "Element 'message' attr has content");
     el._sawError = true;
     manager.finished(token);
   }, false);
 
   v.addEventListener("loadeddata", function () {
     ok(false, "Unexpected loadeddata event");
--- a/dom/media/test/test_error_in_video_document.html
+++ b/dom/media/test/test_error_in_video_document.html
@@ -26,23 +26,21 @@ function documentVideo() {
                  .contentDocument.body.getElementsByTagName("video")[0];
 }
 
 function check() {
   var v = documentVideo();
 
   // Debug info for Bug 608634
   ok(true, "iframe src=" + document.body.getElementsByTagName("iframe")[0].src);
-  is(v.readyState, 0, "Ready state");
+  is(v.readyState, v.HAVE_NOTHING, "Ready state");
 
-  is(v.networkState, 0, "Network state");
   isnot(v.error, null, "Error object");
-  if (v.error)
-    is(v.error.code, MediaError.MEDIA_ERR_DECODE, "Error code");
-
+  is(v.networkState, v.NETWORK_NO_SOURCE, "Network state");
+  is(v.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, "Expected media not supported error");
   SimpleTest.finish();
 }
 
 // Find an error test that we'd think we should be able to play (if it
 // wasn't already known to fail).
 var t = getPlayableVideo(gErrorTests);
 if (!t) {
   todo(false, "No types supported");
--- a/dom/media/test/test_invalid_reject.html
+++ b/dom/media/test/test_invalid_reject.html
@@ -27,18 +27,23 @@ function startTest(test, token) {
       'playing'
   ];
   events.forEach( function(e) {
     v.addEventListener(e, badEvent(e));
   });
 
   // Seeing a decoder error is a success.
   v.addEventListener("error", function onerror(e) {
-    is(v.error.code, v.error.MEDIA_ERR_DECODE,
-      "decoder should reject " + test.name);
+    if (v.readyState == v.HAVE_NOTHING) {
+      is(v.error.code, v.error.MEDIA_ERR_SRC_NOT_SUPPORTED,
+         "decoder should reject " + test.name);
+    } else {
+      is(v.error.code, v.error.MEDIA_ERR_DECODE,
+         "decoder should reject " + test.name);
+    }
     v.removeEventListener('error', onerror, false);
     manager.finished(token);
   });
 
   // Now try to load and play the file, which should result in the
   // error event handler above being called, terminating the test.
   document.body.appendChild(v);
   v.src = test.name;
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -149,17 +149,17 @@ public class CombinedHistoryAdapter exte
                             // top of the list.
                             if (linearLayoutManager != null && scrollPos == 0) {
                                 linearLayoutManager.scrollToPosition(0);
                             }
                         } else {
                             notifyItemRemoved(RECENT_TABS_SMARTFOLDER_INDEX);
                         }
 
-                        if (countReliable) {
+                        if (countReliable && panelStateChangeListener != null) {
                             panelStateChangeListener.setCachedRecentTabsCount(recentTabsCount);
                         }
                     }
                 });
             }
         };
         return recentTabsUpdateHandler;
     }
--- a/netwerk/base/Predictor.cpp
+++ b/netwerk/base/Predictor.cpp
@@ -2402,16 +2402,32 @@ Predictor::UpdateCacheability(nsIURI *so
 }
 
 void
 Predictor::UpdateCacheabilityInternal(nsIURI *sourceURI, nsIURI *targetURI,
                                       uint32_t httpStatus,
                                       const nsCString &method)
 {
   PREDICTOR_LOG(("Predictor::UpdateCacheability httpStatus=%u", httpStatus));
+
+  if (!mInitialized) {
+    PREDICTOR_LOG(("    not initialized"));
+    return;
+  }
+
+  if (!mEnabled) {
+    PREDICTOR_LOG(("    not enabled"));
+    return;
+  }
+
+  if (!mEnablePrefetch) {
+    PREDICTOR_LOG(("    prefetch not enabled"));
+    return;
+  }
+
   uint32_t openFlags = nsICacheStorage::OPEN_READONLY |
                        nsICacheStorage::OPEN_SECRETLY |
                        nsICacheStorage::CHECK_MULTITHREADED;
   RefPtr<Predictor::CacheabilityAction> action =
     new Predictor::CacheabilityAction(targetURI, httpStatus, method, this);
   nsAutoCString uri;
   targetURI->GetAsciiSpec(uri);
   PREDICTOR_LOG(("    uri=%s action=%p", uri.get(), action.get()));
--- a/python/mozlint/mozlint/types.py
+++ b/python/mozlint/mozlint/types.py
@@ -1,17 +1,21 @@
 # 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/.
 
 from __future__ import unicode_literals
 
 import re
+import sys
 from abc import ABCMeta, abstractmethod
 
+from mozlog import get_default_logger, commandline, structuredlog
+from mozlog.reader import LogHandler
+
 from . import result
 from .pathutils import filterpaths
 
 
 class BaseType(object):
     """Abstract base class for all types of linters."""
     __metaclass__ = ABCMeta
     batch = False
@@ -96,14 +100,43 @@ class ExternalType(BaseType):
     """
     batch = True
 
     def _lint(self, files, linter, **lintargs):
         payload = linter['payload']
         return payload(files, **lintargs)
 
 
+class LintHandler(LogHandler):
+    def __init__(self, linter):
+        self.linter = linter
+        self.results = []
+
+    def lint(self, data):
+        self.results.append(result.from_linter(self.linter, **data))
+
+
+class StructuredLogType(BaseType):
+    batch = True
+
+    def _lint(self, files, linter, **lintargs):
+        payload = linter["payload"]
+        handler = LintHandler(linter)
+        logger = linter.get("logger")
+        if logger is None:
+            logger = get_default_logger()
+        if logger is None:
+            logger = structuredlog.StructuredLogger(linter["name"])
+            commandline.setup_logging(logger, {}, {"mach": sys.stdout})
+        logger.add_handler(handler)
+        try:
+            payload(files, logger, **lintargs)
+        except KeyboardInterrupt:
+            pass
+        return handler.results
+
 supported_types = {
     'string': StringType(),
     'regex': RegexType(),
     'external': ExternalType(),
+    'structured_log': StructuredLogType()
 }
 """Mapping of type string to an associated instance."""
--- a/python/mozlint/setup.py
+++ b/python/mozlint/setup.py
@@ -1,16 +1,16 @@
 # 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/.
 
 from setuptools import setup
 
 VERSION = 0.1
-DEPS = []
+DEPS = ["mozlog>=3.4"]
 
 setup(
     name='mozlint',
     description='Framework for registering and running micro lints',
     license='MPL 2.0',
     author='Andrew Halberstadt',
     author_email='ahalberstadt@mozilla.com',
     url='',
new file mode 100644
--- /dev/null
+++ b/python/mozlint/test/linters/structured.lint
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+
+def lint(files, logger, **kwargs):
+    for path in files:
+        with open(path, 'r') as fh:
+            for i, line in enumerate(fh.readlines()):
+                if 'foobar' in line:
+                    logger.lint_error(path=path,
+                                      lineno=i+1,
+                                      column=1,
+                                      rule="no-foobar")
+
+
+LINTER = {
+    'name': "StructuredLinter",
+    'description': "It's bad to have the string foobar in js files.",
+    'include': [
+        'files',
+    ],
+    'type': 'structured_log',
+    'extensions': ['.js', '.jsm'],
+    'payload': lint,
+}
--- a/python/mozlint/test/test_types.py
+++ b/python/mozlint/test/test_types.py
@@ -12,17 +12,17 @@ from mozlint.result import ResultContain
 
 @pytest.fixture
 def path(filedir):
     def _path(name):
         return os.path.join(filedir, name)
     return _path
 
 
-@pytest.fixture(params=['string.lint', 'regex.lint', 'external.lint'])
+@pytest.fixture(params=['string.lint', 'regex.lint', 'external.lint', 'structured.lint'])
 def linter(lintdir, request):
     return os.path.join(lintdir, request.param)
 
 
 def test_linter_types(lint, linter, files, path):
     lint.read(linter)
     result = lint.roll(files)
     assert isinstance(result, dict)
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -145,17 +145,17 @@ Tracker.prototype = {
     }
 
     if (this.ignoreAll || this._ignored.includes(id)) {
       return false;
     }
 
     // Default to the current time in seconds if no time is provided.
     if (when == null) {
-      when = Date.now() / 1000;
+      when = this._now();
     }
 
     // Add/update the entry if we have a newer time.
     if ((this.changedIDs[id] || -Infinity) < when) {
       this._saveChangedID(id, when);
     }
 
     return true;
@@ -178,16 +178,20 @@ Tracker.prototype = {
   },
 
   clearChangedIDs: function () {
     this._log.trace("Clearing changed ID list");
     this.changedIDs = {};
     this.saveChangedIDs();
   },
 
+  _now() {
+    return Date.now() / 1000;
+  },
+
   _isTracking: false,
 
   // Override these in your subclasses.
   startTracking: function () {
   },
 
   stopTracking: function () {
   },
@@ -1270,16 +1274,28 @@ SyncEngine.prototype = {
 
     // Remember this id to delete at the end of sync
     if (this._delete.ids == null)
       this._delete.ids = [id];
     else
       this._delete.ids.push(id);
   },
 
+  _switchItemToDupe(localDupeGUID, incomingItem) {
+    // The local, duplicate ID is always deleted on the server.
+    this._deleteId(localDupeGUID);
+
+    // We unconditionally change the item's ID in case the engine knows of
+    // an item but doesn't expose it through itemExists. If the API
+    // contract were stronger, this could be changed.
+    this._log.debug("Switching local ID to incoming: " + localDupeGUID + " -> " +
+                    incomingItem.id);
+    this._store.changeItemID(localDupeGUID, incomingItem.id);
+  },
+
   /**
    * Reconcile incoming record with local state.
    *
    * This function essentially determines whether to apply an incoming record.
    *
    * @param  item
    *         Record from server to be tested for application.
    * @return boolean
@@ -1343,50 +1359,42 @@ SyncEngine.prototype = {
     // data. If the incoming record does not exist locally, we check for a local
     // duplicate existing under a different ID. The default implementation of
     // _findDupe() is empty, so engines have to opt in to this functionality.
     //
     // If we find a duplicate, we change the local ID to the incoming ID and we
     // refresh the metadata collected above. See bug 710448 for the history
     // of this logic.
     if (!existsLocally) {
-      let dupeID = this._findDupe(item);
-      if (dupeID) {
-        this._log.trace("Local item " + dupeID + " is a duplicate for " +
+      let localDupeGUID = this._findDupe(item);
+      if (localDupeGUID) {
+        this._log.trace("Local item " + localDupeGUID + " is a duplicate for " +
                         "incoming item " + item.id);
 
-        // The local, duplicate ID is always deleted on the server.
-        this._deleteId(dupeID);
-
         // The current API contract does not mandate that the ID returned by
         // _findDupe() actually exists. Therefore, we have to perform this
         // check.
-        existsLocally = this._store.itemExists(dupeID);
-
-        // We unconditionally change the item's ID in case the engine knows of
-        // an item but doesn't expose it through itemExists. If the API
-        // contract were stronger, this could be changed.
-        this._log.debug("Switching local ID to incoming: " + dupeID + " -> " +
-                        item.id);
-        this._store.changeItemID(dupeID, item.id);
+        existsLocally = this._store.itemExists(localDupeGUID);
 
         // If the local item was modified, we carry its metadata forward so
         // appropriate reconciling can be performed.
-        if (this._modified.has(dupeID)) {
+        if (this._modified.has(localDupeGUID)) {
           locallyModified = true;
-          localAge = Date.now() / 1000 -
-            this._modified.getModifiedTimestamp(dupeID);
+          localAge = this._tracker._now() - this._modified.getModifiedTimestamp(localDupeGUID);
           remoteIsNewer = remoteAge < localAge;
 
-          this._modified.swap(dupeID, item.id);
+          this._modified.swap(localDupeGUID, item.id);
         } else {
           locallyModified = false;
           localAge = null;
         }
 
+        // Tell the engine to do whatever it needs to switch the items.
+        this._switchItemToDupe(localDupeGUID, item);
+
         this._log.debug("Local item after duplication: age=" + localAge +
                         "; modified=" + locallyModified + "; exists=" +
                         existsLocally);
       } else {
         this._log.trace("No duplicate found for incoming item: " + item.id);
       }
     }
 
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -504,16 +504,67 @@ BookmarksEngine.prototype = {
         // which won't have a `deleted` property.
         continue;
       }
       let guid = BookmarkSpecialIds.syncIDToPlacesGUID(syncID);
       guids.push(guid);
     }
     return guids;
   },
+
+  // Called when _findDupe returns a dupe item and the engine has decided to
+  // switch the existing item to the new incoming item.
+  _switchItemToDupe(localDupeGUID, incomingItem) {
+    // We unconditionally change the item's ID in case the engine knows of
+    // an item but doesn't expose it through itemExists. If the API
+    // contract were stronger, this could be changed.
+    this._log.debug("Switching local ID to incoming: " + localDupeGUID + " -> " +
+                    incomingItem.id);
+    this._store.changeItemID(localDupeGUID, incomingItem.id);
+
+    // And mark the parent as being modified. Given we de-dupe based on the
+    // parent *name* it's possible the item having its GUID changed has a
+    // different parent from the incoming record.
+    // So we need to find the GUID of the local parent.
+    let now = this._tracker._now();
+    let localID = this._store.idForGUID(incomingItem.id);
+    let localParentID = PlacesUtils.bookmarks.getFolderIdForItem(localID);
+    let localParentGUID = this._store.GUIDForId(localParentID);
+    this._modified.set(localParentGUID, { modified: now, deleted: false });
+
+    // And we also add the parent as reflected in the incoming record as the
+    // de-dupe process might have used an existing item in a different folder.
+    // But only if the parent exists, otherwise we will upload a deleted item
+    // when it might actually be valid, just unknown to us. Note that this
+    // scenario will still leave us with inconsistent client and server states;
+    // the incoming record on the server references a parent that isn't the
+    // actual parent locally - see bug 1297955.
+    if (localParentGUID != incomingItem.parentid) {
+      let remoteParentID = this._store.idForGUID(incomingItem.parentid);
+      if (remoteParentID > 0) {
+        // The parent specified in the record does exist, so we are going to
+        // attempt a move when we come to applying the record. Mark the parent
+        // as being modified so we will later upload it with the new child
+        // reference.
+        this._modified.set(incomingItem.parentid, { modified: now, deleted: false });
+      } else {
+        // We aren't going to do a move as we don't have the parent (yet?).
+        // When applying the record we will add our special PARENT_ANNO
+        // annotation, so if it arrives in the future (either this Sync or a
+        // later one) it will be reparented.
+        this._log.debug(`Incoming duplicate item ${incomingItem.id} specifies ` +
+                        `non-existing parent ${incomingItem.parentid}`);
+      }
+    }
+
+    // The local, duplicate ID is always deleted on the server - but for
+    // bookmarks it is a logical delete.
+    // Simply adding this (now non-existing) ID to the tracker is enough.
+    this._modified.set(localDupeGUID, { modified: now, deleted: true });
+  },
 };
 
 function BookmarksStore(name, engine) {
   Store.call(this, name, engine);
 
   // Explicitly nullify our references to our cached services so we don't leak
   Svc.Obs.add("places-shutdown", function() {
     for (let query in this._stmts) {
@@ -560,17 +611,17 @@ BookmarksStore.prototype = {
       return;
     }
 
     // Figure out the local id of the parent GUID if available
     let parentGUID = record.parentid;
     if (!parentGUID) {
       throw "Record " + record.id + " has invalid parentid: " + parentGUID;
     }
-    this._log.debug("Local parent is " + parentGUID);
+    this._log.debug("Remote parent is " + parentGUID);
 
     // Do the normal processing of incoming records
     Store.prototype.applyIncoming.call(this, record);
 
     if (record.type == "folder" && record.children) {
       this._childrenToOrder[record.id] = record.children;
     }
   },
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_bookmark_duping.js
@@ -0,0 +1,644 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://services-sync/bookmark_validator.js");
+
+
+initTestLogging("Trace");
+
+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;
+
+function promiseOneObserver(topic) {
+  return new Promise((resolve, reject) => {
+    let observer = function(subject, topic, data) {
+      Services.obs.removeObserver(observer, topic);
+      resolve({ subject: subject, data: data });
+    }
+    Services.obs.addObserver(observer, topic, false);
+  });
+}
+
+function setup() {
+ let server = serverForUsers({"foo": "password"}, {
+    meta: {global: {engines: {bookmarks: {version: engine.version,
+                                          syncID: engine.syncID}}}},
+    bookmarks: {},
+  });
+
+  generateNewKeys(Service.collectionKeys);
+
+  new SyncTestingInfrastructure(server.server);
+
+  let collection = server.user("foo").collection("bookmarks");
+
+  Svc.Obs.notify("weave:engine:start-tracking");   // We skip usual startup...
+
+  return { server, collection };
+}
+
+function* cleanup(server) {
+  Svc.Obs.notify("weave:engine:stop-tracking");
+  Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", true);
+  let promiseStartOver = promiseOneObserver("weave:service:start-over:finish");
+  Service.startOver();
+  yield promiseStartOver;
+  yield new Promise(resolve => server.stop(resolve));
+  yield bms.eraseEverything();
+}
+
+function getFolderChildrenIDs(folderId) {
+  let index = 0;
+  let result = [];
+  while (true) {
+    let childId = bms.getIdForItemAt(folderId, index);
+    if (childId == -1) {
+      break;
+    }
+    result.push(childId);
+    index++;
+  }
+  return result;
+}
+
+function createFolder(parentId, title) {
+  let id = bms.createFolder(parentId, title, 0);
+  let guid = store.GUIDForId(id);
+  return { id, guid };
+}
+
+function createBookmark(parentId, url, title, index = bms.DEFAULT_INDEX) {
+  let uri = Utils.makeURI(url);
+  let id = bms.insertBookmark(parentId, uri, index, title)
+  let guid = store.GUIDForId(id);
+  return { id, guid };
+}
+
+function getServerRecord(collection, id) {
+  let wbo = collection.get({ full: true, ids: [id] });
+  // Whew - lots of json strings inside strings.
+  return JSON.parse(JSON.parse(JSON.parse(wbo).payload).ciphertext);
+}
+
+function* promiseNoLocalItem(guid) {
+  // Check there's no item with the specified guid.
+  let got = yield bms.fetch({ guid });
+  ok(!got, `No record remains with GUID ${guid}`);
+  // and while we are here ensure the places cache doesn't still have it.
+  yield Assert.rejects(PlacesUtils.promiseItemId(guid));
+}
+
+function* validate(collection, expectedFailures = []) {
+  let validator = new BookmarkValidator();
+  let records = collection.payloads();
+
+  let problems = validator.inspectServerRecords(records).problemData;
+  // all non-zero problems.
+  let summary = problems.getSummary().filter(prob => prob.count != 0);
+
+  // split into 2 arrays - expected and unexpected.
+  let isInExpectedFailures = elt =>  {
+    for (let i = 0; i < expectedFailures.length; i++) {
+      if (elt.name == expectedFailures[i].name && elt.count == expectedFailures[i].count) {
+        return true;
+      }
+    }
+    return false;
+  }
+  let expected = [];
+  let unexpected = [];
+  for (let elt of summary) {
+    (isInExpectedFailures(elt) ? expected : unexpected).push(elt);
+  }
+  if (unexpected.length || expected.length != expectedFailures.length) {
+    do_print("Validation failed:");
+    do_print(JSON.stringify(summary));
+    // print the entire validator output as it has IDs etc.
+    do_print(JSON.stringify(problems, undefined, 2));
+    // All server records and the entire bookmark tree.
+    do_print("Server records:\n" + JSON.stringify(collection.payloads(), undefined, 2));
+    let tree = yield PlacesUtils.promiseBookmarksTree("", { includeItemIds: true });
+    do_print("Local bookmark tree:\n" + JSON.stringify(tree, undefined, 2));
+    ok(false);
+  }
+}
+
+add_task(function* test_dupe_bookmark() {
+  _("Ensure that a bookmark we consider a dupe is handled correctly.");
+
+  let { server, collection } = this.setup();
+
+  try {
+    // The parent folder and one bookmark in it.
+    let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+    let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
+
+    engine.sync();
+
+    // We've added the bookmark, its parent (folder1) plus "menu", "toolbar" and "unfiled"
+    equal(collection.count(), 5);
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+
+    // Now create a new incoming record that looks alot like a dupe.
+    let newGUID = Utils.makeGUID();
+    let to_apply = {
+      id: newGUID,
+      bmkUri: "http://getfirefox.com/",
+      type: "bookmark",
+      title: "Get Firefox!",
+      parentName: "Folder 1",
+      parentid: folder1_guid,
+    };
+
+    collection.insert(newGUID, encryptPayload(to_apply), Date.now() / 1000 + 10);
+    _("Syncing so new dupe record is processed");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    // We should have logically deleted the dupe record.
+    equal(collection.count(), 6);
+    ok(getServerRecord(collection, bmk1_guid).deleted);
+    // and physically removed from the local store.
+    yield promiseNoLocalItem(bmk1_guid);
+    // Parent should still only have 1 item.
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+    // The parent record on the server should now reference the new GUID and not the old.
+    let serverRecord = getServerRecord(collection, folder1_guid);
+    ok(!serverRecord.children.includes(bmk1_guid));
+    ok(serverRecord.children.includes(newGUID));
+
+    // and a final sanity check - use the validator
+    yield validate(collection);
+  } finally {
+    yield cleanup(server);
+  }
+});
+
+add_task(function* test_dupe_reparented_bookmark() {
+  _("Ensure that a bookmark we consider a dupe from a different parent is handled correctly");
+
+  let { server, collection } = this.setup();
+
+  try {
+    // The parent folder and one bookmark in it.
+    let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+    let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
+    // Another parent folder *with the same name*
+    let {id: folder2_id, guid: folder2_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+
+    do_print(`folder1_guid=${folder1_guid}, folder2_guid=${folder2_guid}, bmk1_guid=${bmk1_guid}`);
+
+    engine.sync();
+
+    // We've added the bookmark, 2 folders plus "menu", "toolbar" and "unfiled"
+    equal(collection.count(), 6);
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+    equal(getFolderChildrenIDs(folder2_id).length, 0);
+
+    // Now create a new incoming record that looks alot like a dupe of the
+    // item in folder1_guid, but with a record that points to folder2_guid.
+    let newGUID = Utils.makeGUID();
+    let to_apply = {
+      id: newGUID,
+      bmkUri: "http://getfirefox.com/",
+      type: "bookmark",
+      title: "Get Firefox!",
+      parentName: "Folder 1",
+      parentid: folder2_guid,
+    };
+
+    collection.insert(newGUID, encryptPayload(to_apply), Date.now() / 1000 + 10);
+
+    _("Syncing so new dupe record is processed");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    // We should have logically deleted the dupe record.
+    equal(collection.count(), 7);
+    ok(getServerRecord(collection, bmk1_guid).deleted);
+    // and physically removed from the local store.
+    yield promiseNoLocalItem(bmk1_guid);
+    // The original folder no longer has the item
+    equal(getFolderChildrenIDs(folder1_id).length, 0);
+    // But the second dupe folder does.
+    equal(getFolderChildrenIDs(folder2_id).length, 1);
+
+    // The record for folder1 on the server should reference neither old or new GUIDs.
+    let serverRecord1 = getServerRecord(collection, folder1_guid);
+    ok(!serverRecord1.children.includes(bmk1_guid));
+    ok(!serverRecord1.children.includes(newGUID));
+
+    // The record for folder2 on the server should only reference the new new GUID.
+    let serverRecord2 = getServerRecord(collection, folder2_guid);
+    ok(!serverRecord2.children.includes(bmk1_guid));
+    ok(serverRecord2.children.includes(newGUID));
+
+    // and a final sanity check - use the validator
+    yield validate(collection);
+  } finally {
+    yield cleanup(server);
+  }
+});
+
+add_task(function* test_dupe_reparented_locally_changed_bookmark() {
+  _("Ensure that a bookmark with local changes we consider a dupe from a different parent is handled correctly");
+
+  let { server, collection } = this.setup();
+
+  try {
+    // The parent folder and one bookmark in it.
+    let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+    let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
+    // Another parent folder *with the same name*
+    let {id: folder2_id, guid: folder2_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+
+    do_print(`folder1_guid=${folder1_guid}, folder2_guid=${folder2_guid}, bmk1_guid=${bmk1_guid}`);
+
+    engine.sync();
+
+    // We've added the bookmark, 2 folders plus "menu", "toolbar" and "unfiled"
+    equal(collection.count(), 6);
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+    equal(getFolderChildrenIDs(folder2_id).length, 0);
+
+    // Now create a new incoming record that looks alot like a dupe of the
+    // item in folder1_guid, but with a record that points to folder2_guid.
+    let newGUID = Utils.makeGUID();
+    let to_apply = {
+      id: newGUID,
+      bmkUri: "http://getfirefox.com/",
+      type: "bookmark",
+      title: "Get Firefox!",
+      parentName: "Folder 1",
+      parentid: folder2_guid,
+    };
+
+    collection.insert(newGUID, encryptPayload(to_apply), Date.now() / 1000 + 10);
+
+    // Make a change to the bookmark that's a dupe, and set the modification
+    // time further in the future than the incoming record. This will cause
+    // us to issue the infamous "DATA LOSS" warning in the logs but cause us
+    // to *not* apply the incoming record.
+    engine._tracker.addChangedID(bmk1_guid, Date.now() / 1000 + 60);
+
+    _("Syncing so new dupe record is processed");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    // We should have logically deleted the dupe record.
+    equal(collection.count(), 7);
+    ok(getServerRecord(collection, bmk1_guid).deleted);
+    // and physically removed from the local store.
+    yield promiseNoLocalItem(bmk1_guid);
+    // The original folder still longer has the item
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+    // The second folder does not.
+    equal(getFolderChildrenIDs(folder2_id).length, 0);
+
+    // The record for folder1 on the server should reference only the GUID.
+    let serverRecord1 = getServerRecord(collection, folder1_guid);
+    ok(!serverRecord1.children.includes(bmk1_guid));
+    ok(serverRecord1.children.includes(newGUID));
+
+    // The record for folder2 on the server should reference nothing.
+    let serverRecord2 = getServerRecord(collection, folder2_guid);
+    ok(!serverRecord2.children.includes(bmk1_guid));
+    ok(!serverRecord2.children.includes(newGUID));
+
+    // and a final sanity check - use the validator
+    yield validate(collection);
+  } finally {
+    yield cleanup(server);
+  }
+});
+
+add_task(function* test_dupe_reparented_to_earlier_appearing_parent_bookmark() {
+  _("Ensure that a bookmark we consider a dupe from a different parent that " +
+    "appears in the same sync before the dupe item");
+
+  let { server, collection } = this.setup();
+
+  try {
+    // The parent folder and one bookmark in it.
+    let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+    let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
+    // One more folder we'll use later.
+    let {id: folder2_id, guid: folder2_guid} = createFolder(bms.toolbarFolder, "A second folder");
+
+    do_print(`folder1=${folder1_guid}, bmk1=${bmk1_guid} folder2=${folder2_guid}`);
+
+    engine.sync();
+
+    // We've added the bookmark, 2 folders plus "menu", "toolbar" and "unfiled"
+    equal(collection.count(), 6);
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+
+    let newGUID = Utils.makeGUID();
+    let newParentGUID = Utils.makeGUID();
+
+    // Have the new parent appear before the dupe item.
+    collection.insert(newParentGUID, encryptPayload({
+      id: newParentGUID,
+      type: "folder",
+      title: "Folder 1",
+      parentName: "A second folder",
+      parentid: folder2_guid,
+      children: [newGUID],
+      tags: [],
+    }), Date.now() / 1000 + 10);
+
+    // And also the update to "folder 2" that references the new parent.
+    collection.insert(folder2_guid, encryptPayload({
+      id: folder2_guid,
+      type: "folder",
+      title: "A second folder",
+      parentName: "Bookmarks Toolbar",
+      parentid: "toolbar",
+      children: [newParentGUID],
+      tags: [],
+    }), Date.now() / 1000 + 10);
+
+    // Now create a new incoming record that looks alot like a dupe of the
+    // item in folder1_guid, with a record that points to a parent with the
+    // same name which appeared earlier in this sync.
+    collection.insert(newGUID, encryptPayload({
+      id: newGUID,
+      bmkUri: "http://getfirefox.com/",
+      type: "bookmark",
+      title: "Get Firefox!",
+      parentName: "Folder 1",
+      parentid: newParentGUID,
+      tags: [],
+    }), Date.now() / 1000 + 10);
+
+
+    _("Syncing so new records are processed.");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    // Everything should be parented correctly.
+    equal(getFolderChildrenIDs(folder1_id).length, 0);
+    let newParentID = store.idForGUID(newParentGUID);
+    let newID = store.idForGUID(newGUID);
+    deepEqual(getFolderChildrenIDs(newParentID), [newID]);
+
+    // Make sure the validator thinks everything is hunky-dory.
+    yield validate(collection);
+  } finally {
+    yield cleanup(server);
+  }
+});
+
+add_task(function* test_dupe_reparented_to_later_appearing_parent_bookmark() {
+  _("Ensure that a bookmark we consider a dupe from a different parent that " +
+    "doesn't exist locally as we process the child, but does appear in the same sync");
+
+  let { server, collection } = this.setup();
+
+  try {
+    // The parent folder and one bookmark in it.
+    let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+    let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
+    // One more folder we'll use later.
+    let {id: folder2_id, guid: folder2_guid} = createFolder(bms.toolbarFolder, "A second folder");
+
+    do_print(`folder1=${folder1_guid}, bmk1=${bmk1_guid} folder2=${folder2_guid}`);
+
+    engine.sync();
+
+    // We've added the bookmark, 2 folders plus "menu", "toolbar" and "unfiled"
+    equal(collection.count(), 6);
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+
+    // Now create a new incoming record that looks alot like a dupe of the
+    // item in folder1_guid, but with a record that points to a parent with the
+    // same name, but a non-existing local ID.
+    let newGUID = Utils.makeGUID();
+    let newParentGUID = Utils.makeGUID();
+
+    collection.insert(newGUID, encryptPayload({
+      id: newGUID,
+      bmkUri: "http://getfirefox.com/",
+      type: "bookmark",
+      title: "Get Firefox!",
+      parentName: "Folder 1",
+      parentid: newParentGUID,
+      tags: [],
+    }), Date.now() / 1000 + 10);
+
+    // Now have the parent appear after (so when the record above is processed
+    // this is still unknown.)
+    collection.insert(newParentGUID, encryptPayload({
+      id: newParentGUID,
+      type: "folder",
+      title: "Folder 1",
+      parentName: "A second folder",
+      parentid: folder2_guid,
+      children: [newGUID],
+      tags: [],
+    }), Date.now() / 1000 + 10);
+    // And also the update to "folder 2" that references the new parent.
+    collection.insert(folder2_guid, encryptPayload({
+      id: folder2_guid,
+      type: "folder",
+      title: "A second folder",
+      parentName: "Bookmarks Toolbar",
+      parentid: "toolbar",
+      children: [newParentGUID],
+      tags: [],
+    }), Date.now() / 1000 + 10);
+
+    _("Syncing so out-of-order records are processed.");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    // The intended parent did end up existing, so it should be parented
+    // correctly after de-duplication.
+    equal(getFolderChildrenIDs(folder1_id).length, 0);
+    let newParentID = store.idForGUID(newParentGUID);
+    let newID = store.idForGUID(newGUID);
+    deepEqual(getFolderChildrenIDs(newParentID), [newID]);
+
+    // Make sure the validator thinks everything is hunky-dory.
+    yield validate(collection);
+  } finally {
+    yield cleanup(server);
+  }
+});
+
+add_task(function* test_dupe_reparented_to_future_arriving_parent_bookmark() {
+  _("Ensure that a bookmark we consider a dupe from a different parent that " +
+    "doesn't exist locally and doesn't appear in this Sync is handled correctly");
+
+  let { server, collection } = this.setup();
+
+  try {
+    // The parent folder and one bookmark in it.
+    let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+    let {id: bmk1_id, guid: bmk1_guid} = createBookmark(folder1_id, "http://getfirefox.com/", "Get Firefox!");
+    // One more folder we'll use later.
+    let {id: folder2_id, guid: folder2_guid} = createFolder(bms.toolbarFolder, "A second folder");
+
+    do_print(`folder1=${folder1_guid}, bmk1=${bmk1_guid} folder2=${folder2_guid}`);
+
+    engine.sync();
+
+    // We've added the bookmark, 2 folders plus "menu", "toolbar" and "unfiled"
+    equal(collection.count(), 6);
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+
+    // Now create a new incoming record that looks alot like a dupe of the
+    // item in folder1_guid, but with a record that points to a parent with the
+    // same name, but a non-existing local ID.
+    let newGUID = Utils.makeGUID();
+    let newParentGUID = Utils.makeGUID();
+
+    collection.insert(newGUID, encryptPayload({
+      id: newGUID,
+      bmkUri: "http://getfirefox.com/",
+      type: "bookmark",
+      title: "Get Firefox!",
+      parentName: "Folder 1",
+      parentid: newParentGUID,
+      tags: [],
+    }), Date.now() / 1000 + 10);
+
+    _("Syncing so new dupe record is processed");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    // We should have logically deleted the dupe record.
+    equal(collection.count(), 7);
+    ok(getServerRecord(collection, bmk1_guid).deleted);
+    // and physically removed from the local store.
+    yield promiseNoLocalItem(bmk1_guid);
+    // The intended parent doesn't exist, so it remains in the original folder
+    equal(getFolderChildrenIDs(folder1_id).length, 1);
+
+    // The record for folder1 on the server should reference the new GUID.
+    let serverRecord1 = getServerRecord(collection, folder1_guid);
+    ok(!serverRecord1.children.includes(bmk1_guid));
+    ok(serverRecord1.children.includes(newGUID));
+
+    // As the incoming parent is missing the item should have been annotated
+    // with that missing parent.
+    equal(PlacesUtils.annotations.getItemAnnotation(store.idForGUID(newGUID), "sync/parent"),
+          newParentGUID);
+
+    // Check the validator. Sadly, this is known to cause a mismatch between
+    // the server and client views of the tree.
+    let expected = [
+      // We haven't fixed the incoming record that referenced the missing parent.
+      { name: "orphans", count: 1 },
+    ];
+    yield validate(collection, expected);
+
+    // Now have the parent magically appear in a later sync - but
+    // it appears as being in a different parent from our existing "Folder 1",
+    // so the folder itself isn't duped.
+    collection.insert(newParentGUID, encryptPayload({
+      id: newParentGUID,
+      type: "folder",
+      title: "Folder 1",
+      parentName: "A second folder",
+      parentid: folder2_guid,
+      children: [newGUID],
+      tags: [],
+    }), Date.now() / 1000 + 10);
+    // We also queue an update to "folder 2" that references the new parent.
+    collection.insert(folder2_guid, encryptPayload({
+      id: folder2_guid,
+      type: "folder",
+      title: "A second folder",
+      parentName: "Bookmarks Toolbar",
+      parentid: "toolbar",
+      children: [newParentGUID],
+      tags: [],
+    }), Date.now() / 1000 + 10);
+
+    _("Syncing so missing parent appears");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    // The intended parent now does exist, so it should have been reparented.
+    equal(getFolderChildrenIDs(folder1_id).length, 0);
+    let newParentID = store.idForGUID(newParentGUID);
+    let newID = store.idForGUID(newGUID);
+    deepEqual(getFolderChildrenIDs(newParentID), [newID]);
+
+    // validation now has different errors :(
+    expected = [
+      // The validator reports multipleParents because:
+      // * The incoming record newParentGUID still (and correctly) references
+      //   newGUID as a child.
+      // * Our original Folder1 was updated to include newGUID when it
+      //   originally de-deuped and couldn't find the parent.
+      // * When the parent *did* eventually arrive we used the parent annotation
+      //   to correctly reparent - but that reparenting process does not change
+      //   the server record.
+      // Hence, newGUID is a child of both those server records :(
+      { name: "multipleParents", count: 1 },
+    ];
+    yield validate(collection, expected);
+
+  } finally {
+    yield cleanup(server);
+  }
+});
+
+add_task(function* test_dupe_empty_folder() {
+  _("Ensure that an empty folder we consider a dupe is handled correctly.");
+  // Empty folders aren't particularly interesting in practice (as that seems
+  // an edge-case) but duping folders with items is broken - bug 1293163.
+  let { server, collection } = this.setup();
+
+  try {
+    // The folder we will end up duping away.
+    let {id: folder1_id, guid: folder1_guid } = createFolder(bms.toolbarFolder, "Folder 1");
+
+    engine.sync();
+
+    // We've added 1 folder, "menu", "toolbar" and "unfiled"
+    equal(collection.count(), 4);
+
+    // Now create new incoming records that looks alot like a dupe of "Folder 1".
+    let newFolderGUID = Utils.makeGUID();
+    collection.insert(newFolderGUID, encryptPayload({
+      id: newFolderGUID,
+      type: "folder",
+      title: "Folder 1",
+      parentName: "Bookmarks Toolbar",
+      parentid: "toolbar",
+      children: [],
+    }), Date.now() / 1000 + 10);
+
+    _("Syncing so new dupe records are processed");
+    engine.lastSync = engine.lastSync - 0.01;
+    engine.sync();
+
+    yield validate(collection);
+
+    // Collection now has one additional record - the logically deleted dupe.
+    equal(collection.count(), 5);
+    // original folder should be logically deleted.
+    ok(getServerRecord(collection, folder1_guid).deleted);
+    yield promiseNoLocalItem(folder1_guid);
+  } finally {
+    yield cleanup(server);
+  }
+});
+// XXX - TODO - folders with children. Bug 1293163
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -139,16 +139,17 @@ tags = addons
 [test_addons_reconciler.js]
 tags = addons
 [test_addons_store.js]
 run-sequentially = Hardcoded port in static files.
 tags = addons
 [test_addons_tracker.js]
 tags = addons
 [test_bookmark_batch_fail.js]
+[test_bookmark_duping.js]
 [test_bookmark_engine.js]
 [test_bookmark_invalid.js]
 [test_bookmark_legacy_microsummaries_support.js]
 [test_bookmark_livemarks.js]
 [test_bookmark_order.js]
 [test_bookmark_places_query_rewriting.js]
 [test_bookmark_record.js]
 [test_bookmark_smart_bookmarks.js]
--- a/storage/mozStorageConnection.cpp
+++ b/storage/mozStorageConnection.cpp
@@ -467,32 +467,36 @@ private:
 
 } // namespace
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Connection
 
 Connection::Connection(Service *aService,
                        int aFlags,
-                       bool aAsyncOnly)
+                       bool aAsyncOnly,
+                       bool aIgnoreLockingMode)
 : sharedAsyncExecutionMutex("Connection::sharedAsyncExecutionMutex")
 , sharedDBMutex("Connection::sharedDBMutex")
 , threadOpenedOn(do_GetCurrentThread())
 , mDBConn(nullptr)
 , mAsyncExecutionThreadShuttingDown(false)
 #ifdef DEBUG
 , mAsyncExecutionThreadIsAlive(false)
 #endif
 , mConnectionClosed(false)
 , mTransactionInProgress(false)
 , mProgressHandler(nullptr)
 , mFlags(aFlags)
+, mIgnoreLockingMode(aIgnoreLockingMode)
 , mStorageService(aService)
 , mAsyncOnly(aAsyncOnly)
 {
+  MOZ_ASSERT(!mIgnoreLockingMode || mFlags & SQLITE_OPEN_READONLY,
+             "Can't ignore locking for a non-readonly connection!");
   mStorageService->registerConnection(this);
 }
 
 Connection::~Connection()
 {
   (void)Close();
 
   MOZ_ASSERT(!mAsyncExecutionThread,
@@ -572,16 +576,17 @@ Connection::getAsyncExecutionTarget()
 
   return mAsyncExecutionThread;
 }
 
 nsresult
 Connection::initialize()
 {
   NS_ASSERTION (!mDBConn, "Initialize called on already opened database!");
+  MOZ_ASSERT(!mIgnoreLockingMode, "Can't ignore locking on an in-memory db.");
   PROFILER_LABEL("mozStorageConnection", "initialize",
     js::ProfileEntry::Category::STORAGE);
 
   // in memory database requested, sqlite uses a magic file name
   int srv = ::sqlite3_open_v2(":memory:", &mDBConn, mFlags, nullptr);
   if (srv != SQLITE_OK) {
     mDBConn = nullptr;
     return convertResultCode(srv);
@@ -605,18 +610,25 @@ Connection::initialize(nsIFile *aDatabas
     js::ProfileEntry::Category::STORAGE);
 
   mDatabaseFile = aDatabaseFile;
 
   nsAutoString path;
   nsresult rv = aDatabaseFile->GetPath(path);
   NS_ENSURE_SUCCESS(rv, rv);
 
+#ifdef XP_WIN
+  static const char* sIgnoreLockingVFS = "win32-none";
+#else
+  static const char* sIgnoreLockingVFS = "unix-none";
+#endif
+  const char* vfs = mIgnoreLockingMode ? sIgnoreLockingVFS : nullptr;
+
   int srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn,
-                              mFlags, nullptr);
+                              mFlags, vfs);
   if (srv != SQLITE_OK) {
     mDBConn = nullptr;
     return convertResultCode(srv);
   }
 
   // Do not set mFileURL here since this is database does not have an associated
   // URL.
   mDatabaseFile = aDatabaseFile;
--- a/storage/mozStorageConnection.h
+++ b/storage/mozStorageConnection.h
@@ -64,18 +64,26 @@ public:
    *        connection.
    * @param aFlags
    *        The flags to pass to sqlite3_open_v2.
    * @param aAsyncOnly
    *        If |true|, the Connection only implements asynchronous interface:
    *        - |mozIStorageAsyncConnection|;
    *        If |false|, the result also implements synchronous interface:
    *        - |mozIStorageConnection|.
+   * @param aIgnoreLockingMode
+   *        If |true|, ignore locks in force on the file. Only usable with
+   *        read-only connections. Defaults to false.
+   *        Use with extreme caution. If sqlite ignores locks, reads may fail
+   *        indicating database corruption (the database won't actually be
+   *        corrupt) or produce wrong results without any indication that has
+   *        happened.
    */
-  Connection(Service *aService, int aFlags, bool aAsyncOnly);
+  Connection(Service *aService, int aFlags, bool aAsyncOnly,
+             bool aIgnoreLockingMode = false);
 
   /**
    * Creates the connection to an in-memory database.
    */
   nsresult initialize();
 
   /**
    * Creates the connection to the database.
@@ -351,16 +359,21 @@ private:
    */
   nsCOMPtr<mozIStorageProgressHandler> mProgressHandler;
 
   /**
    * Stores the flags we passed to sqlite3_open_v2.
    */
   const int mFlags;
 
+  /**
+   * Stores whether we should ask sqlite3_open_v2 to ignore locking.
+   */
+  const bool mIgnoreLockingMode;
+
   // This is here for two reasons: 1) It's used to make sure that the
   // connections do not outlive the service.  2) Our custom collating functions
   // call its localeCompareStrings() method.
   RefPtr<Service> mStorageService;
 
   /**
    * If |false|, this instance supports synchronous operations
    * and it can be cast to |mozIStorageConnection|.
--- a/storage/mozStorageService.cpp
+++ b/storage/mozStorageService.cpp
@@ -743,66 +743,89 @@ Service::OpenAsyncDatabase(nsIVariant *a
                            mozIStorageCompletionCallback *aCallback)
 {
   if (!NS_IsMainThread()) {
     return NS_ERROR_NOT_SAME_THREAD;
   }
   NS_ENSURE_ARG(aDatabaseStore);
   NS_ENSURE_ARG(aCallback);
 
-  nsCOMPtr<nsIFile> storageFile;
-  int flags = SQLITE_OPEN_READWRITE;
+  nsresult rv;
+  bool shared = false;
+  bool readOnly = false;
+  bool ignoreLockingMode = false;
+  int32_t growthIncrement = -1;
+
+#define FAIL_IF_SET_BUT_INVALID(rv)\
+  if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { \
+    return NS_ERROR_INVALID_ARG; \
+  }
+
+  // Deal with options first:
+  if (aOptions) {
+    rv = aOptions->GetPropertyAsBool(NS_LITERAL_STRING("readOnly"), &readOnly);
+    FAIL_IF_SET_BUT_INVALID(rv);
 
+    rv = aOptions->GetPropertyAsBool(NS_LITERAL_STRING("ignoreLockingMode"),
+                                     &ignoreLockingMode);
+    FAIL_IF_SET_BUT_INVALID(rv);
+    // Specifying ignoreLockingMode will force use of the readOnly flag:
+    if (ignoreLockingMode) {
+      readOnly = true;
+    }
+
+    rv = aOptions->GetPropertyAsBool(NS_LITERAL_STRING("shared"), &shared);
+    FAIL_IF_SET_BUT_INVALID(rv);
+
+    // NB: we re-set to -1 if we don't have a storage file later on.
+    rv = aOptions->GetPropertyAsInt32(NS_LITERAL_STRING("growthIncrement"),
+                                      &growthIncrement);
+    FAIL_IF_SET_BUT_INVALID(rv);
+  }
+  int flags = readOnly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE;
+
+  nsCOMPtr<nsIFile> storageFile;
   nsCOMPtr<nsISupports> dbStore;
-  nsresult rv = aDatabaseStore->GetAsISupports(getter_AddRefs(dbStore));
+  rv = aDatabaseStore->GetAsISupports(getter_AddRefs(dbStore));
   if (NS_SUCCEEDED(rv)) {
     // Generally, aDatabaseStore holds the database nsIFile.
     storageFile = do_QueryInterface(dbStore, &rv);
     if (NS_FAILED(rv)) {
       return NS_ERROR_INVALID_ARG;
     }
 
     rv = storageFile->Clone(getter_AddRefs(storageFile));
     MOZ_ASSERT(NS_SUCCEEDED(rv));
 
-    // Ensure that SQLITE_OPEN_CREATE is passed in for compatibility reasons.
-    flags |= SQLITE_OPEN_CREATE;
+    if (!readOnly) {
+      // Ensure that SQLITE_OPEN_CREATE is passed in for compatibility reasons.
+      flags |= SQLITE_OPEN_CREATE;
+    }
 
-    // Extract and apply the shared-cache option.
-    bool shared = false;
-    if (aOptions) {
-      rv = aOptions->GetPropertyAsBool(NS_LITERAL_STRING("shared"), &shared);
-      if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) {
-        return NS_ERROR_INVALID_ARG;
-      }
-    }
+    // Apply the shared-cache option.
     flags |= shared ? SQLITE_OPEN_SHAREDCACHE : SQLITE_OPEN_PRIVATECACHE;
   } else {
     // Sometimes, however, it's a special database name.
     nsAutoCString keyString;
     rv = aDatabaseStore->GetAsACString(keyString);
     if (NS_FAILED(rv) || !keyString.EqualsLiteral("memory")) {
       return NS_ERROR_INVALID_ARG;
     }
 
     // Just fall through with nullptr storageFile, this will cause the storage
     // connection to use a memory DB.
   }
 
-  int32_t growthIncrement = -1;
-  if (aOptions && storageFile) {
-    rv = aOptions->GetPropertyAsInt32(NS_LITERAL_STRING("growthIncrement"),
-                                      &growthIncrement);
-    if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) {
-      return NS_ERROR_INVALID_ARG;
-    }
+  if (!storageFile && growthIncrement >= 0) {
+    return NS_ERROR_INVALID_ARG;
   }
 
   // Create connection on this thread, but initialize it on its helper thread.
-  RefPtr<Connection> msc = new Connection(this, flags, true);
+  RefPtr<Connection> msc = new Connection(this, flags, true,
+                                          ignoreLockingMode);
   nsCOMPtr<nsIEventTarget> target = msc->getAsyncExecutionTarget();
   MOZ_ASSERT(target, "Cannot initialize a connection that has been closed already");
 
   RefPtr<AsyncInitDatabase> asyncInit =
     new AsyncInitDatabase(msc,
                           storageFile,
                           growthIncrement,
                           aCallback);
--- a/storage/test/unit/test_storage_connection.js
+++ b/storage/test/unit/test_storage_connection.js
@@ -350,22 +350,37 @@ function* standardAsyncTest(promisedDB, 
 add_task(function* test_open_async() {
   yield standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default");
   yield standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg");
   yield standardAsyncTest(openAsyncDatabase(getTestDB(),
     {shared: false, growthIncrement: 54}), "non-default options");
   yield standardAsyncTest(openAsyncDatabase("memory"),
     "in-memory database", true);
   yield standardAsyncTest(openAsyncDatabase("memory",
-    {shared: false, growthIncrement: 54}),
+    {shared: false}),
     "in-memory database and options", true);
 
-  do_print("Testing async opening with bogus options 1");
+  do_print("Testing async opening with bogus options 0");
   let raised = false;
   let adb = null;
+
+  try {
+    adb = yield openAsyncDatabase("memory", {shared: false, growthIncrement: 54});
+  } catch (ex) {
+    raised = true;
+  } finally {
+    if (adb) {
+      yield asyncClose(adb);
+    }
+  }
+  do_check_true(raised);
+
+  do_print("Testing async opening with bogus options 1");
+  raised = false;
+  adb = null;
   try {
     adb = yield openAsyncDatabase(getTestDB(), {shared: "forty-two"});
   } catch (ex) {
     raised = true;
   } finally {
     if (adb) {
       yield asyncClose(adb);
     }
--- a/taskcluster/ci/source-check/mozlint.yml
+++ b/taskcluster/ci/source-check/mozlint.yml
@@ -77,19 +77,21 @@ wptlint-gecko/opt:
         platform: lint/opt
     worker-type: aws-provisioner-v1/b2gtest
     worker:
         implementation: docker-worker
         docker-image: {in-tree: "lint"}
         max-run-time: 1800
     run:
         using: mach
-        mach: lint -l wpt -f treeherder
+        mach: lint -l wpt -l wpt_manifest -f treeherder
     run-on-projects:
         - integration
         - release
     when:
         files-changed:
             - 'testing/web-platform/tests/**'
+            - 'testing/web-platform/mozilla/tests/**'
+            - 'testing/web-platform/meta/MANIFEST.json'
+            - 'testing/web-platform/mozilla/meta/MANIFEST.json'
             - 'python/mozlint/**'
             - 'tools/lint/**'
             - 'testing/docker/lint/**'
-
--- a/testing/firefox-ui/tests/functional/security/test_safe_browsing_initial_download.py
+++ b/testing/firefox-ui/tests/functional/security/test_safe_browsing_initial_download.py
@@ -2,44 +2,45 @@
 # 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/.
 
 import os
 import re
 
 from firefox_ui_harness.testcases import FirefoxTestCase
 from marionette_driver import Wait
+from marionette_driver.errors import TimeoutException
 
 
 class TestSafeBrowsingInitialDownload(FirefoxTestCase):
 
     test_data = [{
         'platforms': ['linux', 'windows_nt', 'darwin'],
         'files': [
             # Phishing
-            r'goog-badbinurl-shavar.pset',
-            r'goog-badbinurl-shavar.sbstore',
-            r'goog-malware-shavar.pset',
-            r'goog-malware-shavar.sbstore',
-            r'goog(pub)?-phish-shavar.pset',
-            r'goog(pub)?-phish-shavar.sbstore',
-            r'goog-unwanted-shavar.pset',
-            r'goog-unwanted-shavar.sbstore',
+            r'^goog-badbinurl-shavar.pset$',
+            r'^goog-badbinurl-shavar.sbstore$',
+            r'^goog-malware-shavar.pset$',
+            r'^goog-malware-shavar.sbstore$',
+            r'^goog(pub)?-phish-shavar.pset$',
+            r'^goog(pub)?-phish-shavar.sbstore$',
+            r'^goog-unwanted-shavar.pset$',
+            r'^goog-unwanted-shavar.sbstore$',
 
             # Tracking Protections
-            r'base-track-digest256.pset',
-            r'base-track-digest256.sbstore',
-            r'mozstd-trackwhite-digest256.pset',
-            r'mozstd-trackwhite-digest256.sbstore'
+            r'^base-track-digest256.pset$',
+            r'^base-track-digest256.sbstore$',
+            r'^mozstd-trackwhite-digest256.pset$',
+            r'^mozstd-trackwhite-digest256.sbstore$',
         ],
     }, {
         'platforms': ['windows_nt'],
         'files': [
-            r'goog-downloadwhite-digest256.pset',
-            r'goog-downloadwhite-digest256.sbstore'
+            r'^goog-downloadwhite-digest256.pset$',
+            r'^goog-downloadwhite-digest256.sbstore$',
         ]
     },
     ]
 
     browser_prefs = {
         'browser.safebrowsing.downloads.enabled': 'true',
         'browser.safebrowsing.phishing.enabled': 'true',
         'browser.safebrowsing.malware.enabled': 'true',
@@ -66,12 +67,19 @@ class TestSafeBrowsingInitialDownload(Fi
 
     def test_safe_browsing_initial_download(self):
         wait = Wait(self.marionette, timeout=self.browser.timeout_page_load,
                     ignored_exceptions=[OSError])
 
         for data in self.test_data:
             if self.platform not in data['platforms']:
                 continue
+
             for item in data['files']:
-                wait.until(
-                    lambda _: [f for f in os.listdir(self.sb_files_path) if re.search(item, f)],
-                    message='Safe Browsing File: {} not found!'.format(item))
+                try:
+                    wait.until(
+                        lambda _: [f for f in os.listdir(self.sb_files_path)
+                                   if re.search(item, f)],
+                        message='Safe Browsing File: {} not found!'.format(item))
+                except TimeoutException:
+                    self.logger.info('Downloaded safebrowsing files: {}'.format(
+                        os.listdir(self.sb_files_path)))
+                    raise
--- a/testing/mozbase/mozlog/mozlog/formatters/errorsummary.py
+++ b/testing/mozbase/mozlog/mozlog/formatters/errorsummary.py
@@ -48,8 +48,20 @@ class ErrorSummaryFormatter(BaseFormatte
         return self._output("log", data)
 
     def crash(self, item):
         data = {"test": item.get("test"),
                 "signature": item["signature"],
                 "stackwalk_stdout": item.get("stackwalk_stdout"),
                 "stackwalk_stderr": item.get("stackwalk_stderr")}
         return self._output("crash", data)
+
+    def lint(self, item):
+        data = {
+            "level": item["level"],
+            "path": item["path"],
+            "message": item["message"],
+            "lineno": item["lineno"],
+            "column": item.get("column"),
+            "rule": item.get("rule"),
+            "linter": item.get("linter")
+        }
+        self._output("lint", data)
--- a/testing/mozbase/mozlog/mozlog/formatters/machformatter.py
+++ b/testing/mozbase/mozlog/mozlog/formatters/machformatter.py
@@ -351,16 +351,35 @@ class MachFormatter(base.BaseFormatter):
         else:
             rv = "%s %s" % (level, data["message"])
 
         if "stack" in data:
             rv += "\n%s" % data["stack"]
 
         return rv
 
+    def lint(self, data):
+        term = self.terminal if self.terminal is not None else NullTerminal()
+        fmt = "{path}  {c1}{lineno}{column}  {c2}{level}{normal}  {message}  {c1}{rule}({linter}){normal}"
+        message = fmt.format(
+            path=data["path"],
+            normal=term.normal,
+            c1=term.grey,
+            c2=term.red if data["level"] == 'error' else term.yellow,
+            lineno=str(data["lineno"]),
+            column=(":" + str(data["column"])) if data.get("column") else "",
+            level=data["level"],
+            message=data["message"],
+            rule='{} '.format(data["rule"]) if data.get("rule") else "",
+            linter=data["linter"].lower() if data.get("linter") else "",
+        )
+
+        return message
+
+
     def _get_subtest_data(self, data):
         test = self._get_test_id(data)
         return self.status_buffer.get(test, {"count": 0, "unexpected": [], "pass": 0})
 
     def _time(self, data):
         entry_time = data["time"]
         if self.write_interval and self.last_time is not None:
             t = entry_time - self.last_time
--- a/testing/mozbase/mozlog/mozlog/formatters/tbplformatter.py
+++ b/testing/mozbase/mozlog/mozlog/formatters/tbplformatter.py
@@ -228,8 +228,14 @@ class TbplFormatter(BaseFormatter):
 
     @output_subtests
     def valgrind_error(self, data):
         rv = "TEST-UNEXPECTED-VALGRIND-ERROR | " + data['primary'] + "\n"
         for line in data['secondary']:
             rv = rv + line + "\n"
 
         return rv
+
+    def lint(self, data):
+        fmt = "TEST-UNEXPECTED-{level} | {path}:{lineno}{column} | {message} ({rule})"
+        data["column"] = ":%s" % data["column"] if data["column"] else ""
+        data['rule'] = data['rule'] or data['linter'] or ""
+        message.append(fmt.format(**data))
--- a/testing/mozbase/mozlog/mozlog/logtypes.py
+++ b/testing/mozbase/mozlog/mozlog/logtypes.py
@@ -152,23 +152,37 @@ class Status(DataType):
 
 class SubStatus(Status):
     allowed = ["PASS", "FAIL", "ERROR", "TIMEOUT", "ASSERT", "NOTRUN", "SKIP"]
 
 class Dict(DataType):
     def convert(self, data):
         return dict(data)
 
+
 class List(DataType):
     def __init__(self, name, item_type, default=no_default, optional=False):
         DataType.__init__(self, name, default, optional)
         self.item_type = item_type(None)
 
     def convert(self, data):
         return [self.item_type.convert(item) for item in data]
 
 class Int(DataType):
     def convert(self, data):
         return int(data)
 
+
 class Any(DataType):
     def convert(self, data):
         return data
+
+
+class Tuple(DataType):
+    def __init__(self, name, item_types, default=no_default, optional=False):
+        DataType.__init__(self, name, default, optional)
+        self.item_types = item_types
+
+    def convert(self, data):
+        if len(data) != len(self.item_types):
+            raise ValueError("Expected %i items got %i" % (len(self.item_types), len(data)))
+        return tuple(item_type.convert(value)
+                     for item_type, value in zip(self.item_types, data))
--- a/testing/mozbase/mozlog/mozlog/structuredlog.py
+++ b/testing/mozbase/mozlog/mozlog/structuredlog.py
@@ -6,17 +6,17 @@ from __future__ import unicode_literals
 
 from multiprocessing import current_process
 from threading import current_thread, Lock
 import json
 import sys
 import time
 import traceback
 
-from logtypes import Unicode, TestId, Status, SubStatus, Dict, List, Int, Any
+from logtypes import Unicode, TestId, Status, SubStatus, Dict, List, Int, Any, Tuple
 from logtypes import log_action, convertor_registry
 
 """Structured Logging for recording test results.
 
 Allowed actions, and subfields:
   suite_start
       tests  - List of test names
 
@@ -88,16 +88,18 @@ def set_default_logger(default_logger):
     """
     global _default_logger_name
 
     _default_logger_name = default_logger.name
 
 log_levels = dict((k.upper(), v) for v, k in
                   enumerate(["critical", "error", "warning", "info", "debug"]))
 
+lint_levels = ["ERROR", "WARNING"]
+
 def log_actions():
     """Returns the set of actions implemented by mozlog."""
     return set(convertor_registry.keys())
 
 class LoggerState(object):
     def __init__(self):
         self.handlers = []
         self.running_tests = set()
@@ -433,21 +435,54 @@ def _log_func(level_name):
 :param exc_info: Either a boolean indicating whether to include a traceback
                  derived from sys.exc_info() or a three-item tuple in the
                  same format as sys.exc_info() containing exception information
                  to log.
 """ % level_name
     log.__name__ = str(level_name).lower()
     return log
 
+def _lint_func(level_name):
+    @log_action(Unicode("path"),
+                Unicode("message", default=""),
+                Int("lineno", default=0),
+                Int("column", default=None, optional=True),
+                Unicode("hint", default=None, optional=True),
+                Unicode("source", default=None, optional=True),
+                Unicode("rule", default=None, optional=True),
+                Tuple("lineoffset", (Int, Int), default=None, optional=True),
+                Unicode("linter", default=None, optional=True))
+    def lint(self, data):
+        data["level"] = level_name
+        self._log_data("lint", data)
+    lint.__doc__ = """Log an error resulting from a failed lint check
 
-# Create all the methods on StructuredLog for debug levels
+        :param linter: name of the linter that flagged this error
+        :param path: path to the file containing the error
+        :param message: text describing the error
+        :param lineno: line number that contains the error
+        :param column: column containing the error
+        :param hint: suggestion for fixing the error (optional)
+        :param source: source code context of the error (optional)
+        :param rule: name of the rule that was violated (optional)
+        :param lineoffset: denotes an error spans multiple lines, of the form
+                           (<lineno offset>, <num lines>) (optional)
+        """
+    lint.__name__ = str("lint_%s" % level_name)
+    return lint
+
+
+# Create all the methods on StructuredLog for log/lint levels
 for level_name in log_levels:
     setattr(StructuredLogger, level_name.lower(), _log_func(level_name))
 
+for level_name in lint_levels:
+    level_name = level_name.lower()
+    name = "lint_%s" % level_name
+    setattr(StructuredLogger, name, _lint_func(level_name))
 
 class StructuredLogFileLike(object):
     """Wrapper for file-like objects to redirect writes to logger
     instead. Each call to `write` becomes a single log entry of type `log`.
 
     When using this it is important that the callees i.e. the logging
     handlers do not themselves try to write to the wrapped file as this
     will cause infinite recursion.
--- a/testing/mozbase/mozlog/setup.py
+++ b/testing/mozbase/mozlog/setup.py
@@ -1,16 +1,16 @@
 # 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/.
 
 from setuptools import setup, find_packages
 
 PACKAGE_NAME = 'mozlog'
-PACKAGE_VERSION = '3.3'
+PACKAGE_VERSION = '3.4'
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
       description="Robust log handling specialized for logging in the Mozilla universe",
       long_description="see http://mozbase.readthedocs.org/",
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
       url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
--- a/testing/web-platform/mach_commands.py
+++ b/testing/web-platform/mach_commands.py
@@ -232,34 +232,23 @@ testing/web-platform/tests for tests tha
             wpt_kwargs = vars(p.parse_args(["--manifest-update", path]))
             context.commands.dispatch("web-platform-tests", context, **wpt_kwargs)
 
         if proc:
             proc.wait()
 
 
 class WPTManifestUpdater(MozbuildObject):
-    def run_update(self):
-        import imp
+    def run_update(self, check_clean=False, **kwargs):
+        import manifestupdate
         from wptrunner import wptlogging
-        from wptrunner.wptcommandline import get_test_paths, set_from_config
-        from wptrunner.testloader import ManifestLoader
-
-        wpt_dir = os.path.abspath(os.path.join(self.topsrcdir, 'testing', 'web-platform'))
 
-        localpaths = imp.load_source("localpaths",
-                                     os.path.join(wpt_dir, "tests", "tools", "localpaths.py"))
-        kwargs = {"config": os.path.join(wpt_dir, "wptrunner.ini"),
-                  "tests_root": None,
-                  "metadata_root": None}
-
-        wptlogging.setup({}, {"mach": sys.stdout})
-        set_from_config(kwargs)
-        test_paths = get_test_paths(kwargs["config"])
-        ManifestLoader(test_paths, force_manifest_update=True).load()
+        logger = wptlogging.setup(kwargs, {"mach": sys.stdout})
+        wpt_dir = os.path.abspath(os.path.join(self.topsrcdir, 'testing', 'web-platform'))
+        manifestupdate.update(logger, wpt_dir, check_clean)
 
 
 def create_parser_wpt():
     from wptrunner import wptcommandline
     return wptcommandline.create_parser(["firefox"])
 
 def create_parser_update():
     from update import updatecommandline
@@ -287,18 +276,26 @@ def create_parser_create():
     p.add_argument("--mismatch", action="store_true",
                    help="Create a mismatch reftest")
     p.add_argument("--wait", action="store_true",
                    help="Create a reftest that waits until takeScreenshot() is called")
     p.add_argument("path", action="store", help="Path to the test file")
     return p
 
 
+def create_parser_manifest_update():
+    import manifestupdate
+    return manifestupdate.create_parser()
+
+
 @CommandProvider
 class MachCommands(MachCommandBase):
+    def setup(self):
+        self._activate_virtualenv()
+
     @Command("web-platform-tests",
              category="testing",
              conditions=[conditions.is_firefox],
              parser=create_parser_wpt)
     def run_web_platform_tests(self, **params):
         self.setup()
 
         if "test_objects" in params:
@@ -331,19 +328,16 @@ class MachCommands(MachCommandBase):
         return wpt_updater.run_update(**params)
 
     @Command("wpt-update",
              category="testing",
              parser=create_parser_update)
     def update_wpt(self, **params):
         return self.update_web_platform_tests(**params)
 
-    def setup(self):
-        self._activate_virtualenv()
-
     @Command("web-platform-tests-reduce",
              category="testing",
              conditions=[conditions.is_firefox],
              parser=create_parser_reduce)
     def unstable_web_platform_tests(self, **params):
         self.setup()
         wpt_reduce = self._spawn(WebPlatformTestsReduce)
         return wpt_reduce.run_reduce(**params)
@@ -367,13 +361,14 @@ class MachCommands(MachCommandBase):
     @Command("wpt-create",
              category="testing",
              conditions=[conditions.is_firefox],
              parser=create_parser_create)
     def create_wpt(self, **params):
         return self.create_web_platform_test(**params)
 
     @Command("wpt-manifest-update",
-             category="testing")
-    def wpt_manifest_update(self, **parms):
+             category="testing",
+             parser=create_parser_manifest_update)
+    def wpt_manifest_update(self, **params):
         self.setup()
         wpt_manifest_updater = self._spawn(WPTManifestUpdater)
-        wpt_manifest_updater.run_update()
+        return wpt_manifest_updater.run_update(**params)
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/manifestupdate.py
@@ -0,0 +1,89 @@
+import argparse
+import imp
+import os
+import sys
+
+from mozlog.structured import commandline
+from wptrunner.wptcommandline import get_test_paths, set_from_config
+from wptrunner.testloader import ManifestLoader
+
+def create_parser():
+    p = argparse.ArgumentParser()
+    p.add_argument("--check-clean", action="store_true",
+                   help="Check that updating the manifest doesn't lead to any changes")
+    commandline.add_logging_group(p)
+
+    return p
+
+
+def update(logger, wpt_dir, check_clean=True):
+    localpaths = imp.load_source("localpaths",
+                                 os.path.join(wpt_dir, "tests", "tools", "localpaths.py"))
+    kwargs = {"config": os.path.join(wpt_dir, "wptrunner.ini"),
+              "tests_root": None,
+              "metadata_root": None}
+
+    set_from_config(kwargs)
+    config = kwargs["config"]
+    test_paths = get_test_paths(config)
+
+    if check_clean:
+        old_manifests = {}
+        for data in test_paths.itervalues():
+            path = os.path.join(data["metadata_path"], "MANIFEST.json")
+            with open(path) as f:
+                old_manifests[path] = f.readlines()
+
+    try:
+        ManifestLoader(test_paths, force_manifest_update=True).load()
+
+        rv = 0
+
+        if check_clean:
+            clean = diff_manifests(logger, old_manifests)
+            if not clean:
+                rv = 1
+    finally:
+        if check_clean:
+            for path, data in old_manifests.iteritems():
+                logger.info("Restoring manifest %s" % path)
+                with open(path, "w") as f:
+                    f.writelines(data)
+
+    return rv
+
+def diff_manifests(logger, old_manifests):
+    logger.info("Diffing old and new manifests")
+    import difflib
+
+    clean = True
+    for path, old in old_manifests.iteritems():
+        with open(path) as f:
+            new = f.readlines()
+
+        if old != new:
+            clean = False
+            sm = difflib.SequenceMatcher(a=old, b=new)
+            for group in sm.get_grouped_opcodes():
+                logged = False
+                message = []
+                for op, old_0, old_1, new_0, new_1 in group:
+                    if op != "equal" and not logged:
+                        logged = True
+                        logger.lint_error(path=path,
+                                          message="Manifest changed",
+                                          lineno=(old_0 + 1),
+                                          source="\n".join(old[old_0:old_1]),
+                                          linter="wpt-manifest")
+                    if op == "equal":
+                        message.extend(' ' + line for line in old[old_0:old_1])
+                    if op in ('replace', 'delete'):
+                        message.extend('-' + line for line in old[old_0:old_1])
+                    if op in ('replace', 'insert'):
+                        message.extend('+' + line for line in new[new_0:new_1])
+                logger.info("".join(message))
+    if clean:
+        logger.info("No differences found")
+
+    return clean
+
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -37375,22 +37375,16 @@
         ],
         "IndexedDB/idbcursor-continuePrimaryKey-exception-order.htm": [
           {
             "path": "IndexedDB/idbcursor-continuePrimaryKey-exception-order.htm",
             "timeout": "long",
             "url": "/IndexedDB/idbcursor-continuePrimaryKey-exception-order.htm"
           }
         ],
-        "editing/other/delete.html": [
-          {
-            "path": "editing/other/delete.html",
-            "url": "/editing/other/delete.html"
-          }
-        ],
         "editing/other/restoration.html": [
           {
             "path": "editing/other/restoration.html",
             "url": "/editing/other/restoration.html"
           }
         ],
         "html/browsers/history/the-location-interface/location-prototype-setting.html": [
           {
@@ -37447,34 +37441,16 @@
           }
         ],
         "html/semantics/scripting-1/the-script-element/script-onload-insertion-point.html": [
           {
             "path": "html/semantics/scripting-1/the-script-element/script-onload-insertion-point.html",
             "url": "/html/semantics/scripting-1/the-script-element/script-onload-insertion-point.html"
           }
         ],
-        "html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-1.html": [
-          {
-            "path": "html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-1.html",
-            "url": "/html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-1.html"
-          }
-        ],
-        "html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-2.html": [
-          {
-            "path": "html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-2.html",
-            "url": "/html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-2.html"
-          }
-        ],
-        "html/semantics/scripting-1/the-script-element/script-onload-insertion-point.html": [
-          {
-            "path": "html/semantics/scripting-1/the-script-element/script-onload-insertion-point.html",
-            "url": "/html/semantics/scripting-1/the-script-element/script-onload-insertion-point.html"
-          }
-        ],
         "selectors/child-indexed-pseudo-class.html": [
           {
             "path": "selectors/child-indexed-pseudo-class.html",
             "url": "/selectors/child-indexed-pseudo-class.html"
           }
         ],
         "svg/linking/scripted/href-animate-element.html": [
           {
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -175,18 +175,19 @@ NS_IMETHODIMP
 nsAutoCompleteController::StartSearch(const nsAString &aSearchString)
 {
   mSearchString = aSearchString;
   StartSearches();
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsAutoCompleteController::HandleText()
+nsAutoCompleteController::HandleText(bool *_retval)
 {
+  *_retval = false;
   // Note: the events occur in the following order when IME is used.
   // 1. a compositionstart event(HandleStartComposition)
   // 2. some input events (HandleText), eCompositionState_Composing
   // 3. a compositionend event(HandleEndComposition)
   // 4. an input event(HandleText), eCompositionState_Committing
   // We should do nothing during composition.
   if (mCompositionState == eCompositionState_Composing) {
     return NS_OK;
@@ -279,16 +280,17 @@ nsAutoCompleteController::HandleText()
       bool cancel;
       HandleKeyNavigation(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel);
       return NS_OK;
     }
     ClosePopup();
     return NS_OK;
   }
 
+  *_retval = true;
   StartSearches();
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAutoCompleteController::HandleEnter(bool aIsPopupSelection,
                                       nsIDOMEvent *aEvent,
@@ -618,28 +620,30 @@ nsAutoCompleteController::HandleDelete(b
   if (!mInput)
     return NS_OK;
 
   nsCOMPtr<nsIAutoCompleteInput> input(mInput);
   bool isOpen = false;
   input->GetPopupOpen(&isOpen);
   if (!isOpen || mRowCount <= 0) {
     // Nothing left to delete, proceed as normal
-    HandleText();
+    bool unused = false;
+    HandleText(&unused);
     return NS_OK;
   }
 
   nsCOMPtr<nsIAutoCompletePopup> popup;
   input->GetPopup(getter_AddRefs(popup));
 
   int32_t index, searchIndex, rowIndex;
   popup->GetSelectedIndex(&index);
   if (index == -1) {
     // No row is selected in the list
-    HandleText();
+    bool unused = false;
+    HandleText(&unused);
     return NS_OK;
   }
 
   RowIndexToSearch(index, &searchIndex, &rowIndex);
   NS_ENSURE_TRUE(searchIndex >= 0 && rowIndex >= 0, NS_ERROR_FAILURE);
 
   nsIAutoCompleteResult *result = mResults.SafeObjectAt(searchIndex);
   NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
@@ -1188,17 +1192,17 @@ nsAutoCompleteController::StartSearch(ui
           searchResult != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
           searchResult != nsIAutoCompleteResult::RESULT_NOMATCH)
         result = nullptr;
     }
 
     nsAutoString searchParam;
     nsresult rv = input->GetSearchParam(searchParam);
     if (NS_FAILED(rv))
-        return rv;
+      return rv;
 
     // FormFill expects the searchParam to only contain the input element id,
     // other consumers may have other expectations, so this modifies it only
     // for new consumers handling autoFill by themselves.
     if (mProhibitAutoFill && mClearingAutoFillSearchesAgain) {
       searchParam.AppendLiteral(" prohibit-autofill");
     }
 
@@ -1622,16 +1626,20 @@ nsAutoCompleteController::ProcessResult(
       uint32_t delta = totalMatchCount - oldRowCount;
 
       mRowCount += delta;
       if (mTree) {
         mTree->RowCountChanged(oldRowCount, delta);
       }
     }
 
+    // Try to autocomplete the default index for this search.
+    // Do this before invalidating so the binding knows about it.
+    CompleteDefaultIndex(aSearchIndex);
+
     // Refresh the popup view to display the new search results
     nsCOMPtr<nsIAutoCompletePopup> popup;
     input->GetPopup(getter_AddRefs(popup));
     NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
     popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT);
 
     uint32_t minResults;
     input->GetMinResultsForPopup(&minResults);
@@ -1639,20 +1647,18 @@ nsAutoCompleteController::ProcessResult(
     // Make sure the popup is open, if necessary, since we now have at least one
     // search result ready to display. Don't force the popup closed if we might
     // get results in the future to avoid unnecessarily canceling searches.
     if (mRowCount || !minResults) {
       OpenPopup();
     } else if (mSearchesOngoing == 0) {
       ClosePopup();
     }
-  }
-
-  if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
-      searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
+  } else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
+             searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
     // Try to autocomplete the default index for this search.
     CompleteDefaultIndex(aSearchIndex);
   }
 
   return NS_OK;
 }
 
 nsresult
@@ -1731,20 +1737,21 @@ nsAutoCompleteController::CompleteDefaul
     return NS_OK;
 
   bool shouldComplete;
   input->GetCompleteDefaultIndex(&shouldComplete);
   if (!shouldComplete)
     return NS_OK;
 
   nsAutoString resultValue;
-  if (NS_SUCCEEDED(GetDefaultCompleteValue(aResultIndex, true, resultValue)))
+  if (NS_SUCCEEDED(GetDefaultCompleteValue(aResultIndex, true, resultValue))) {
     CompleteValue(resultValue);
 
-  mDefaultIndexCompleted = true;
+    mDefaultIndexCompleted = true;
+  }
 
   return NS_OK;
 }
 
 nsresult
 nsAutoCompleteController::GetDefaultCompleteResult(int32_t aResultIndex,
                                                    nsIAutoCompleteResult** _result,
                                                    int32_t* _defaultIndex)
--- a/toolkit/components/autocomplete/nsIAutoCompleteController.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteController.idl
@@ -33,57 +33,61 @@ interface nsIAutoCompleteController : ns
    */
   readonly attribute unsigned long matchCount;
 
   /*
    * Start a search on a string, assuming the input property is already set.
    */
   void startSearch(in AString searchString);
 
-  /* 
+  /*
    * Stop all asynchronous searches
    */
   void stopSearch();
 
   /*
    * Notify the controller that the user has changed text in the textbox.
    * This includes all means of changing the text value, including typing a
    * character, backspacing, deleting, pasting, committing composition or
    * canceling composition.
    *
    * NOTE: handleText() must be called after composition actually ends, even if
    *       the composition is canceled and the textbox value isn't changed.
    *       Then, implementation of handleText() can access the editor when
    *       it's not in composing mode. DOM compositionend event is not good
    *       timing for calling handleText(). DOM input event immediately after
    *       DOM compositionend event is the best timing to call this.
+   *
+   * @return whether this handler started a new search.
    */
-  void handleText();
+  boolean handleText();
 
   /*
    * Notify the controller that the user wishes to enter the current text. If
    * aIsPopupSelection is true, then a selection was made from the popup, so
    * fill this value into the input field before continuing. If false, just
    * use the current value of the input field.
    *
    * @param aIsPopupSelection
    *        Pass true if the selection was made from the popup.
    * @param aEvent
    *        The event that triggered the enter, like a key event if the user
    *        pressed the Return key or a click event if the user clicked a popup
    *        item.
-   * @return True if the controller wishes to prevent event propagation and default event
+   * @return Whether the controller wishes to prevent event propagation and
+   *         default event.
    */
   boolean handleEnter(in boolean aIsPopupSelection,
                       [optional] in nsIDOMEvent aEvent);
 
   /*
    * Notify the controller that the user wishes to revert autocomplete
    *
-   * @return True if the controller wishes to prevent event propagation and default event
+   * @return Whether the controller wishes to prevent event propagation and
+   *         default event.
    */
   boolean handleEscape();
 
   /*
    * Notify the controller that the user wishes to start composition
    *
    * NOTE: nsIAutoCompleteController implementation expects that this is called
    *       by DOM compositionstart handler.
@@ -93,35 +97,38 @@ interface nsIAutoCompleteController : ns
   /*
    * Notify the controller that the user wishes to end composition
    *
    * NOTE: nsIAutoCompleteController implementation expects that this is called
    *       by DOM compositionend handler.
    */
   void handleEndComposition();
 
-  /* 
+  /*
    * Handle tab. Just closes up.
    */
   void handleTab();
 
   /*
    * Notify the controller of the following key navigation events:
    *   up, down, left, right, page up, page down
    *
-   * @return True if the controller wishes to prevent event propagation and default event
+   * @return Whether the controller wishes to prevent event propagation and
+   *         default event
    */
   boolean handleKeyNavigation(in unsigned long key);
 
   /*
    * Notify the controller that the user chose to delete the current
    * auto-complete result.
+   *
+   * @return Whether the controller removed a result item.
    */
   boolean handleDelete();
-  
+
   /*
    * Get the value of the result at a given index in the last completed search
    */
   AString getValueAt(in long index);
 
   /*
    * Get the label of the result at a given index in the last completed search
    */
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -820,18 +820,19 @@ nsFormFillController::HandleEvent(nsIDOM
   }
   if (type.EqualsLiteral("mousedown")) {
     return MouseDown(aEvent);
   }
   if (type.EqualsLiteral("keypress")) {
     return KeyPress(aEvent);
   }
   if (type.EqualsLiteral("input")) {
+    bool unused = false;
     return (!mSuppressOnInput && mController && mFocusedInput) ?
-           mController->HandleText() : NS_OK;
+           mController->HandleText(&unused) : NS_OK;
   }
   if (type.EqualsLiteral("blur")) {
     if (mFocusedInput)
       StopControllingInput();
     return NS_OK;
   }
   if (type.EqualsLiteral("compositionstart")) {
     NS_ASSERTION(mController, "should have a controller!");
@@ -931,37 +932,39 @@ nsFormFillController::KeyPress(nsIDOMEve
   if (!mFocusedInput || !mController)
     return NS_OK;
 
   nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent);
   if (!keyEvent)
     return NS_ERROR_FAILURE;
 
   bool cancel = false;
+  bool unused = false;
 
   uint32_t k;
   keyEvent->GetKeyCode(&k);
   switch (k) {
   case nsIDOMKeyEvent::DOM_VK_DELETE:
 #ifndef XP_MACOSX
     mController->HandleDelete(&cancel);
     break;
   case nsIDOMKeyEvent::DOM_VK_BACK_SPACE:
-    mController->HandleText();
+    mController->HandleText(&unused);
     break;
 #else
   case nsIDOMKeyEvent::DOM_VK_BACK_SPACE:
     {
       bool isShift = false;
       keyEvent->GetShiftKey(&isShift);
 
-      if (isShift)
+      if (isShift) {
         mController->HandleDelete(&cancel);
-      else
-        mController->HandleText();
+      } else {
+        mController->HandleText(&unused);
+      }
 
       break;
     }
 #endif
   case nsIDOMKeyEvent::DOM_VK_PAGE_UP:
   case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN:
     {
       bool isCtrl, isAlt, isMeta;
@@ -1061,17 +1064,18 @@ nsFormFillController::MouseDown(nsIDOMEv
   if (!input)
     return NS_OK;
 
   nsAutoString value;
   input->GetTextValue(value);
   if (value.Length() > 0) {
     // Show the popup with a filtered result set
     mController->SetSearchString(EmptyString());
-    mController->HandleText();
+    bool unused = false;
+    mController->HandleText(&unused);
   } else {
     // Show the popup with the complete result set.  Can't use HandleText()
     // because it doesn't display the popup if the input is blank.
     bool cancel = false;
     mController->HandleKeyNavigation(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel);
   }
 
   return NS_OK;
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1045,16 +1045,20 @@ extends="chrome://global/content/binding
           return val;
         ]]>
         </setter>
       </property>
 
       <method name="onSearchBegin">
         <body><![CDATA[
           this.richlistbox.mouseSelectedIndex = -1;
+
+          if (typeof this._onSearchBegin == "function") {
+            this._onSearchBegin();
+          }
         ]]></body>
       </method>
 
       <method name="openAutocompletePopup">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body>
           <![CDATA[
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -41,31 +41,28 @@ const kModalStyles = {
     ["background", "#ffc535"],
     ["border-radius", "3px"],
     ["box-shadow", "0 2px 0 0 rgba(0,0,0,.1)"],
     ["color", "#000"],
     ["display", "-moz-box"],
     ["margin", "-2px 0 0 -2px !important"],
     ["padding", "2px !important"],
     ["pointer-events", "none"],
-    ["transition-property", "opacity, transform, top, left"],
-    ["transition-duration", "50ms"],
-    ["transition-timing-function", "linear"],
     ["white-space", "nowrap"],
+    ["will-change", "transform"],
     ["z-index", 2]
   ],
   outlineNodeDebug: [ ["z-index", 2147483647] ],
   outlineText: [
     ["margin", "0 !important"],
     ["padding", "0 !important"],
     ["vertical-align", "top !important"]
   ],
   maskNode: [
     ["background", "#000"],
-    ["mix-blend-mode", "multiply"],
     ["opacity", ".35"],
     ["pointer-events", "none"],
     ["position", "absolute"],
     ["z-index", 1]
   ],
   maskNodeDebug: [
     ["z-index", 2147483646],
     ["top", 0],
@@ -111,16 +108,19 @@ function mockAnonymousContentNode(domNod
       if (!node.hasAttribute(attrName))
         return;
       node.removeAttribute(attrName);
     },
     remove() {
       try {
         domNode.parentNode.removeChild(domNode);
       } catch (ex) {}
+    },
+    setAnimationForElement(id, keyframes, duration) {
+      return (domNode.querySelector("#" + id) || domNode).animate(keyframes, duration);
     }
   };
 }
 
 let gWindows = new Map();
 
 /**
  * FinderHighlighter class that is used by Finder.jsm to take care of the
@@ -864,17 +864,18 @@ FinderHighlighter.prototype = {
     // 1. No outline nodes were built before, or
     // 2. When the amount of rectangles to draw is different from before, or
     // 3. When there's more than one rectangle to draw, because it's impossible
     //    to animate that consistently with AnonymousContent nodes.
     let rebuildOutline = (!outlineAnonNode || rectCount !== dict.previousRangeRectsCount ||
       rectCount != 1);
     dict.previousRangeRectsCount = rectCount;
 
-    let document = range.startContainer.ownerDocument;
+    let window = range.startContainer.ownerDocument.defaultView.top;
+    let document = window.document;
     // First see if we need to and can remove the previous outline nodes.
     if (rebuildOutline && outlineAnonNode) {
       if (kDebug) {
         outlineAnonNode.remove();
       } else {
         try {
           document.removeAnonymousContent(outlineAnonNode);
         } catch (ex) {}
@@ -1148,18 +1149,17 @@ FinderHighlighter.prototype = {
       this._scheduleRepaintOfMask.bind(this, window, { contentChanged: true }),
       this._scheduleRepaintOfMask.bind(this, window, { updateAllRanges: true }),
       this._scheduleRepaintOfMask.bind(this, window, { scrollOnly: true }),
       this.hide.bind(this, window, null)
     ];
     let target = this.iterator._getDocShell(window).chromeEventHandler;
     target.addEventListener("MozAfterPaint", dict.highlightListeners[0]);
     target.addEventListener("resize", dict.highlightListeners[1]);
-    target.addEventListener("DOMMouseScroll", dict.highlightListeners[2]);
-    target.addEventListener("mousewheel", dict.highlightListeners[2]);
+    target.addEventListener("scroll", dict.highlightListeners[2]);
     target.addEventListener("click", dict.highlightListeners[3]);
   },
 
   /**
    * Remove event listeners from content.
    *
    * @param {nsIDOMWindow} window
    */
@@ -1167,18 +1167,17 @@ FinderHighlighter.prototype = {
     window = window.top;
     let dict = this.getForWindow(window);
     if (!dict.highlightListeners)
       return;
 
     let target = this.iterator._getDocShell(window).chromeEventHandler;
     target.removeEventListener("MozAfterPaint", dict.highlightListeners[0]);
     target.removeEventListener("resize", dict.highlightListeners[1]);
-    target.removeEventListener("DOMMouseScroll", dict.highlightListeners[2]);
-    target.removeEventListener("mousewheel", dict.highlightListeners[2]);
+    target.removeEventListener("scroll", dict.highlightListeners[2]);
     target.removeEventListener("click", dict.highlightListeners[3]);
 
     dict.highlightListeners = null;
   },
 
   /**
    * For a given node returns its editable parent or null if there is none.
    * It's enough to check if node is a text node and its parent's parent is
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -863,16 +863,25 @@ ConnectionData.prototype = Object.freeze
  *   shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
  *       will attempt to minimize its memory usage after this many
  *       milliseconds of connection idle. The connection is idle when no
  *       statements are executing. There is no default value which means no
  *       automatic memory minimization will occur. Please note that this is
  *       *not* a timer on the idle service and this could fire while the
  *       application is active.
  *
+ *   readOnly -- (bool) Whether to open the database with SQLITE_OPEN_READONLY
+ *       set. If used, writing to the database will fail. Defaults to false.
+ *
+ *   ignoreLockingMode -- (bool) Whether to ignore locks on the database held
+ *       by other connections. If used, implies readOnly. Defaults to false.
+ *       USE WITH EXTREME CAUTION. This mode WILL produce incorrect results or
+ *       return "false positive" corruption errors if other connections write
+ *       to the DB at the same time.
+ *
  * FUTURE options to control:
  *
  *   special named databases
  *   pragma TEMP STORE = MEMORY
  *   TRUNCATE JOURNAL
  *   SYNCHRONOUS = full
  *
  * @param options
@@ -910,22 +919,31 @@ function openConnection(options) {
   }
 
   let file = FileUtils.File(path);
   let identifier = getIdentifierByPath(path);
 
   log.info("Opening database: " + path + " (" + identifier + ")");
 
   return new Promise((resolve, reject) => {
-    let dbOptions = null;
+    let dbOptions = Cc["@mozilla.org/hash-property-bag;1"].
+                    createInstance(Ci.nsIWritablePropertyBag);
     if (!sharedMemoryCache) {
-      dbOptions = Cc["@mozilla.org/hash-property-bag;1"].
-        createInstance(Ci.nsIWritablePropertyBag);
       dbOptions.setProperty("shared", false);
     }
+    if (options.readOnly) {
+      dbOptions.setProperty("readOnly", true);
+    }
+    if (options.ignoreLockingMode) {
+      dbOptions.setProperty("ignoreLockingMode", true);
+      dbOptions.setProperty("readOnly", true);
+    }
+
+    dbOptions = dbOptions.enumerator.hasMoreElements() ? dbOptions : null;
+
     Services.storage.openAsyncDatabase(file, dbOptions, (status, connection) => {
       if (!connection) {
         log.warn(`Could not open connection to ${path}: ${status}`);
         reject(new Error(`Could not open connection to ${path}: ${status}`));
         return;
       }
       log.info("Connection opened");
       try {
--- a/tools/lint/docs/create.rst
+++ b/tools/lint/docs/create.rst
@@ -24,27 +24,38 @@ Now ``no-eval.lint`` gets passed into :f
 Linter Types
 ------------
 
 There are three types of linters, though more may be added in the future.
 
 1. string - fails if substring is found
 2. regex - fails if regex matches
 3. external - fails if a python function returns a non-empty result list
+4. structured_log - fails if a mozlog logger emits any lint_error or lint_warning log messages
 
 As seen from the example above, string and regex linters are very easy to create, but they
 should be avoided if possible. It is much better to use a context aware linter for the language you
 are trying to lint. For example, use eslint to lint JavaScript files, use flake8 to lint python
 files, etc.
 
-Which brings us to the third and most interesting type of linter, external.  External linters call
-an arbitrary python function which is responsible for not only running the linter, but ensuring the
-results are structured properly. For example, an external type could shell out to a 3rd party
-linter, collect the output and format it into a list of :class:`ResultContainer` objects.
+Which brings us to the third and most interesting type of linter,
+external.  External linters call an arbitrary python function which is
+responsible for not only running the linter, but ensuring the results
+are structured properly. For example, an external type could shell out
+to a 3rd party linter, collect the output and format it into a list of
+:class:`ResultContainer` objects. The signature for this python
+function is ``lint(files, **kwargs)``, where ``files`` is a list of
+files to lint.
 
+Structured log linters are much like external linters, but suitable
+for cases where the linter code is using mozlog and emits
+``lint_error`` or ``lint_warning`` logging messages when the lint
+fails. This is recommended for writing novel gecko-specific lints. In
+this case the signature for lint functions is ``lint(files, logger,
+**kwargs)``.
 
 LINTER Definition
 -----------------
 
 Each ``.lint`` file must have a variable called LINTER which is a dict containing metadata about the
 linter. Here are the supported keys:
 
 * name - The name of the linter (required)
@@ -59,16 +70,20 @@ linter. Here are the supported keys:
 In addition to the above, some ``.lint`` files correspond to a single lint rule. For these, the
 following additional keys may be specified:
 
 * message - A string to print on infraction (optional)
 * hint - A string with a clue on how to fix the infraction (optional)
 * rule - An id string for the lint rule (optional)
 * level - The severity of the infraction, either 'error' or 'warning' (optional)
 
+For structured_log lints the following additional keys apply:
+
+* logger - A StructuredLog object to use for logging. If not supplied
+  one will be created (optional)
 
 Example
 -------
 
 Here is an example of an external linter that shells out to the python flake8 linter:
 
 .. code-block:: python
 
new file mode 100644
--- /dev/null
+++ b/tools/lint/wpt_manifest.lint
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+import imp
+import json
+import os
+import sys
+
+from mozprocess import ProcessHandler
+
+from mozlint import result
+
+
+def lint(files, logger, **kwargs):
+    wpt_dir = os.path.join(kwargs["root"], "testing", "web-platform")
+    manifestupdate = imp.load_source("manifestupdate",
+                                     os.path.join(wpt_dir, "manifestupdate.py"))
+    manifestupdate.update(logger, wpt_dir, True)
+
+
+LINTER = {
+    'name': "wpt_manifest",
+    'description': "web-platform-tests manifest lint",
+    'include': [
+        'testing/web-platform/tests',
+        'testing/web-platform/mozilla/tests',
+    ],
+    'exclude': [],
+    'type': 'structured_log',
+    'payload': lint,
+}
--- a/widget/gtk/IMContextWrapper.cpp
+++ b/widget/gtk/IMContextWrapper.cpp
@@ -174,16 +174,17 @@ IMContextWrapper::IMContextWrapper(nsWin
     , mCompositionStart(UINT32_MAX)
     , mProcessingKeyEvent(nullptr)
     , mCompositionState(eCompositionState_NotComposing)
     , mIsIMFocused(false)
     , mIsDeletingSurrounding(false)
     , mLayoutChanged(false)
     , mSetCursorPositionOnKeyEvent(true)
     , mPendingResettingIMContext(false)
+    , mRetrieveSurroundingSignalReceived(false)
 {
     static bool sFirstInstance = true;
     if (sFirstInstance) {
         sFirstInstance = false;
         sUseSimpleContext =
             Preferences::GetBool(
                 "intl.ime.use_simple_context_on_password_field",
                 kUseSimpleContextDefault);
@@ -925,38 +926,43 @@ IMContextWrapper::Blur()
     mIsIMFocused = false;
 }
 
 void
 IMContextWrapper::OnSelectionChange(nsWindow* aCaller,
                                     const IMENotification& aIMENotification)
 {
     mSelection.Assign(aIMENotification);
+    bool retrievedSurroundingSignalReceived =
+      mRetrieveSurroundingSignalReceived;
+    mRetrieveSurroundingSignalReceived = false;
 
     if (MOZ_UNLIKELY(IsDestroyed())) {
         return;
     }
 
     const IMENotification::SelectionChangeDataBase& selectionChangeData =
         aIMENotification.mSelectionChangeData;
 
     MOZ_LOG(gGtkIMLog, LogLevel::Info,
         ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
          "mSelectionChangeData={ mOffset=%u, Length()=%u, mReversed=%s, "
          "mWritingMode=%s, mCausedByComposition=%s, "
          "mCausedBySelectionEvent=%s, mOccurredDuringComposition=%s "
-         "} }), mCompositionState=%s, mIsDeletingSurrounding=%s",
+         "} }), mCompositionState=%s, mIsDeletingSurrounding=%s, "
+         "mRetrieveSurroundingSignalReceived=%s",
          this, aCaller, selectionChangeData.mOffset,
          selectionChangeData.Length(),
          ToChar(selectionChangeData.mReversed),
          GetWritingModeName(selectionChangeData.GetWritingMode()).get(),
          ToChar(selectionChangeData.mCausedByComposition),
          ToChar(selectionChangeData.mCausedBySelectionEvent),
          ToChar(selectionChangeData.mOccurredDuringComposition),
-         GetCompositionStateName(), ToChar(mIsDeletingSurrounding)));
+         GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
+         ToChar(retrievedSurroundingSignalReceived)));
 
     if (aCaller != mLastFocusedWindow) {
         MOZ_LOG(gGtkIMLog, LogLevel::Error,
             ("0x%p   OnSelectionChange(), FAILED, "
              "the caller isn't focused window, mLastFocusedWindow=0x%p",
              this, mLastFocusedWindow));
         return;
     }
@@ -1006,17 +1012,28 @@ IMContextWrapper::OnSelectionChange(nsWi
     }
 
     // When the selection change is caused by dispatching composition event,
     // selection set event and/or occurred before starting current composition,
     // we shouldn't notify IME of that and commit existing composition.
     if (!selectionChangeData.mCausedByComposition &&
         !selectionChangeData.mCausedBySelectionEvent &&
         !occurredBeforeComposition) {
-        ResetIME();
+        // Hack for ibus-pinyin.  ibus-pinyin will synthesize a set of
+        // composition which commits with empty string after calling
+        // gtk_im_context_reset().  Therefore, selecting text causes
+        // unexpectedly removing it.  For preventing it but not breaking the
+        // other IMEs which use surrounding text, we should call it only when
+        // surrounding text has been retrieved after last selection range was
+        // set.  If it's not retrieved, that means that current IME doesn't
+        // have any content cache, so, it must not need the notification of
+        // selection change.
+        if (IsComposing() || retrievedSurroundingSignalReceived) {
+            ResetIME();
+        }
     }
 }
 
 /* static */
 void
 IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
                                              IMContextWrapper* aModule)
 {
@@ -1159,16 +1176,17 @@ IMContextWrapper::OnRetrieveSurroundingN
         return FALSE;
     }
 
     NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
     uint32_t cursorPosInUTF8 = utf8Str.Length();
     AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
     gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
                                    cursorPosInUTF8);
+    mRetrieveSurroundingSignalReceived = true;
     return TRUE;
 }
 
 /* static */
 gboolean
 IMContextWrapper::OnDeleteSurroundingCallback(GtkIMContext* aContext,
                                               gint aOffset,
                                               gint aNChars,
--- a/widget/gtk/IMContextWrapper.h
+++ b/widget/gtk/IMContextWrapper.h
@@ -279,16 +279,19 @@ protected:
     bool mSetCursorPositionOnKeyEvent;
     // mPendingResettingIMContext becomes true if selection change notification
     // is received during composition but the selection change occurred before
     // starting the composition.  In such case, we cannot notify IME of
     // selection change during composition because we don't want to commit
     // the composition in such case.  However, we should notify IME of the
     // selection change after the composition is committed.
     bool mPendingResettingIMContext;
+    // mRetrieveSurroundingSignalReceived is true after "retrieve_surrounding"
+    // signal is received until selection is changed in Gecko.
+    bool mRetrieveSurroundingSignalReceived;
 
     // sLastFocusedContext is a pointer to the last focused instance of this
     // class.  When a instance is destroyed and sLastFocusedContext refers it,
     // this is cleared.  So, this refers valid pointer always.
     static IMContextWrapper* sLastFocusedContext;
 
     // sUseSimpleContext indeicates if password editors and editors with
     // |ime-mode: disabled;| should use GtkIMContextSimple.