Bug 1556789 - Refactor extension install in searchservice to use promises r=robwu,daleharvey
☠☠ backed out by e94e4cbfcecb ☠ ☠
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 11 Jul 2019 18:42:00 +0000
changeset 482453 0acf6bafda0f648e510b8ac5d48a80b14dacae87
parent 482452 81f121f3a7cb26ea38e987dbd84dbe1614d05c5b
child 482454 2d10e95cf0cd0a37793ebc0a6f8e64cc79b599eb
push id89783
push userscaraveo@mozilla.com
push dateThu, 11 Jul 2019 20:55:09 +0000
treeherderautoland@2d10e95cf0cd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrobwu, daleharvey
bugs1556789
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1556789 - Refactor extension install in searchservice to use promises r=robwu,daleharvey This provides a set of promises that the searchservice resolves once the search engine has been configured Differential Revision: https://phabricator.services.mozilla.com/D33660
browser/base/content/test/performance/browser_startup_mainthreadio.js
browser/components/extensions/parent/ext-chrome-settings-overrides.js
browser/components/extensions/test/xpcshell/head.js
browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
browser/components/extensions/test/xpcshell/test_ext_urlbar.js
browser/components/preferences/in-content/tests/browser.ini
browser/components/preferences/in-content/tests/browser_engines.js
browser/components/search/test/browser/browser_contextmenu.js
browser/components/search/test/marionette/test_engines_on_restart.py
browser/components/tests/unit/test_distribution.js
browser/components/urlbar/tests/unit/head.js
testing/profiles/common/user.js
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
toolkit/components/places/tests/unit/head_bookmarks.js
toolkit/components/search/SearchEngine.jsm
toolkit/components/search/SearchService.jsm
toolkit/components/search/SearchUtils.jsm
toolkit/components/search/nsISearchService.idl
toolkit/components/search/tests/xpcshell/data/invalid-extension/invalid/manifest.json
toolkit/components/search/tests/xpcshell/data/invalid-extension/list.json
toolkit/components/search/tests/xpcshell/head_search.js
toolkit/components/search/tests/xpcshell/test_async_distribution.js
toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
toolkit/components/search/tests/xpcshell/test_require_engines_for_cache.js
toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
toolkit/components/search/tests/xpcshell/test_validate_engines.js
toolkit/components/search/tests/xpcshell/test_webextensions_install_failure.js
toolkit/components/search/tests/xpcshell/xpcshell.ini
toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
toolkit/components/telemetry/tests/unit/head.js
--- a/browser/base/content/test/performance/browser_startup_mainthreadio.js
+++ b/browser/base/content/test/performance/browser_startup_mainthreadio.js
@@ -582,17 +582,17 @@ const startupPhases = {
       path: "GreD:omni.ja",
       condition: WIN,
       stat: 1,
     },
     {
       // bug 1543090
       path: "XCurProcD:omni.ja",
       condition: WIN,
-      stat: 2,
+      stat: 3,
     },
   ],
 
   // Things that are expected to be completely out of the startup path
   // and loaded lazily when used for the first time by the user should
   // be blacklisted here.
   "before becoming idle": [
     {
--- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js
+++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js
@@ -305,43 +305,44 @@ this.chrome_settings_overrides = class e
       pendingSearchSetupTasks.set(extension.id, searchStartupPromise);
     }
   }
 
   async processSearchProviderManifestEntry() {
     let { extension } = this;
     let { manifest } = extension;
     let searchProvider = manifest.chrome_settings_overrides.search_provider;
-    if (searchProvider.is_default) {
+    let handleIsDefault =
+      searchProvider.is_default && !extension.addonData.builtIn;
+    let engineName = searchProvider.name.trim();
+    // Builtin extensions are never marked with is_default.  We can safely wait on
+    // the search service to fully initialize before handling these extensions.
+    if (handleIsDefault) {
       await searchInitialized;
       if (!this.extension) {
         Cu.reportError(
           `Extension shut down before search provider was registered`
         );
         return;
       }
-    }
-
-    let engineName = searchProvider.name.trim();
-    if (searchProvider.is_default) {
       let engine = Services.search.getEngineByName(engineName);
       let defaultEngines = await Services.search.getDefaultEngines();
       if (
         engine &&
         defaultEngines.some(defaultEngine => defaultEngine.name == engineName)
       ) {
         // Needs to be called every time to handle reenabling, but
         // only sets default for install or enable.
         await this.setDefault(engineName);
         // For built in search engines, we don't do anything further
         return;
       }
     }
     await this.addSearchEngine();
-    if (searchProvider.is_default) {
+    if (handleIsDefault) {
       if (extension.startupReason === "ADDON_INSTALL") {
         // Don't ask if it already the current engine
         let engine = Services.search.getEngineByName(engineName);
         let defaultEngine = await Services.search.getDefault();
         if (defaultEngine.name != engine.name) {
           let subject = {
             wrappedJSObject: {
               // This is a hack because we don't have the browser of
@@ -412,66 +413,30 @@ this.chrome_settings_overrides = class e
         "enable",
         extension.id
       );
     }
   }
 
   async addSearchEngine() {
     let { extension } = this;
-    let isCurrent = false;
-    let index = -1;
-    if (
-      extension.startupReason === "ADDON_UPGRADE" &&
-      !extension.addonData.builtIn
-    ) {
-      let engines = await Services.search.getEnginesByExtensionID(extension.id);
-      if (engines.length > 0) {
-        let firstEngine = engines[0];
-        let firstEngineName = firstEngine.name;
-        // There can be only one engine right now
-        isCurrent =
-          (await Services.search.getDefault()).name == firstEngineName;
-        // Get position of engine and store it
-        index = (await Services.search.getEngines())
-          .map(engine => engine.name)
-          .indexOf(firstEngineName);
-        await Services.search.removeEngine(firstEngine);
-      }
-    }
     try {
+      // This is safe to await prior to SearchService.init completing.
       let engines = await Services.search.addEnginesFromExtension(extension);
-      if (engines.length > 0) {
+      if (engines[0]) {
         await ExtensionSettingsStore.addSetting(
           extension.id,
           DEFAULT_SEARCH_STORE_TYPE,
           ENGINE_ADDED_SETTING_NAME,
           engines[0].name
         );
       }
-      if (
-        extension.startupReason === "ADDON_UPGRADE" &&
-        !extension.addonData.builtIn
-      ) {
-        let engines = await Services.search.getEnginesByExtensionID(
-          extension.id
-        );
-        let engine = Services.search.getEngineByName(engines[0].name);
-        if (isCurrent) {
-          await Services.search.setDefault(engine);
-        }
-        if (index != -1) {
-          await Services.search.moveEngine(engine, index);
-        }
-      }
     } catch (e) {
       Cu.reportError(e);
-      return false;
     }
-    return true;
   }
 };
 
 ExtensionPreferencesManager.addSetting("homepage_override", {
   prefNames: [HOMEPAGE_PREF, HOMEPAGE_EXTENSION_CONTROLLED],
   // ExtensionPreferencesManager will call onPrefsChanged when control changes
   // and it updates the preferences. We are passed the item from
   // ExtensionSettingsStore that details what is in control. If there is an id
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -15,16 +15,22 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   HttpServer: "resource://testing-common/httpd.js",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
   TestUtils: "resource://testing-common/TestUtils.jsm",
 });
 
+// For search related tests, reduce what is happening.  Search tests cover
+// these otherwise.
+Services.prefs.setCharPref("browser.search.region", "US");
+Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
 Services.prefs.setBoolPref("extensions.webextensions.remote", false);
 
 ExtensionTestUtils.init(this);
 
 /**
  * Creates a new HttpServer for testing, and begins listening on the
  * specified port. Automatically shuts down the server when the test
  * unit ends.
--- a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
@@ -13,17 +13,22 @@ AddonTestUtils.overrideCertDB();
 AddonTestUtils.createAppInfo(
   "xpcshell@tests.mozilla.org",
   "XPCShell",
   "1",
   "42"
 );
 
 add_task(async function setup() {
+  Services.prefs.setCharPref("browser.search.region", "US");
+  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
   await AddonTestUtils.promiseStartupManager();
+  await Services.search.init();
 });
 
 add_task(async function test_overrides_update_removal() {
   /* This tests the scenario where the manifest key for homepage and/or
    * search_provider are removed between updates and therefore the
    * settings are expected to revert.  It also tests that an extension
    * can make a builtin extension the default extension without user
    * interaction.  */
--- a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
@@ -25,16 +25,17 @@ AddonTestUtils.createAppInfo(
 const ORIGINAL_NOTIFICATION_TIMEOUT =
   UrlbarProviderExtension.notificationTimeout;
 
 add_task(async function startup() {
   Services.prefs.setCharPref("browser.search.region", "US");
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
   Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
   await AddonTestUtils.promiseStartupManager();
+  await Services.search.init(true);
 
   // Add a test engine and make it default so that when we do searches below,
   // Firefox doesn't try to include search suggestions from the actual default
   // engine from over the network.
   let engine = await Services.search.addEngineWithDetails("Test engine", {
     template: "http://example.com/?s=%S",
   });
   Services.search.defaultEngine = engine;
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -1,12 +1,15 @@
 [DEFAULT]
 prefs =
   extensions.formautofill.available='on'
   extensions.formautofill.creditCards.available=true
+# turn off geo updates for search related tests
+  browser.search.region=US
+  browser.search.geoSpecificDefaults=false
 support-files =
   head.js
   privacypane_tests_perwindow.js
   addons/pl-dictionary.xpi
   addons/set_homepage.xpi
   addons/set_newtab.xpi
 
 [browser_applications_selection.js]
--- a/browser/components/preferences/in-content/tests/browser_engines.js
+++ b/browser/components/preferences/in-content/tests/browser_engines.js
@@ -1,10 +1,13 @@
 // Test Engine list
 add_task(async function() {
+  // running stand-alone, be sure to wait for init
+  await Services.search.init();
+
   let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
     leaveOpen: true,
   });
   is(prefs.selectedPane, "paneSearch", "Search pane is selected by default");
   let doc = gBrowser.contentDocument;
 
   let tree = doc.querySelector("#engineList");
   ok(
--- a/browser/components/search/test/browser/browser_contextmenu.js
+++ b/browser/components/search/test/browser/browser_contextmenu.js
@@ -1,14 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  *  * http://creativecommons.org/publicdomain/zero/1.0/ */
 /*
  * Test searching for the selected text using the context menu
  */
 
+const { SearchExtensionLoader } = ChromeUtils.import(
+  "resource://gre/modules/SearchUtils.jsm"
+);
+
 const ENGINE_NAME = "mozSearch";
 const ENGINE_ID = "mozsearch-engine@search.mozilla.org";
 
 add_task(async function() {
   // We want select events to be fired.
   await SpecialPowers.pushPrefEnv({
     set: [["dom.select_events.enabled", true]],
   });
@@ -23,17 +27,17 @@ add_task(async function() {
     .getProtocolHandler("resource")
     .QueryInterface(Ci.nsIResProtocolHandler);
   let originalSubstitution = resProt.getSubstitution("search-extensions");
   resProt.setSubstitution(
     "search-extensions",
     Services.io.newURI("file://" + searchExtensions.path)
   );
 
-  await Services.search.ensureBuiltinExtension(ENGINE_ID);
+  await SearchExtensionLoader.installAddons([ENGINE_ID]);
 
   let engine = await Services.search.getEngineByName(ENGINE_NAME);
   ok(engine, "Got a search engine");
   let defaultEngine = await Services.search.getDefault();
   await Services.search.setDefault(engine);
 
   let contextMenu = document.getElementById("contentAreaContextMenu");
   ok(contextMenu, "Got context menu XUL");
--- a/browser/components/search/test/marionette/test_engines_on_restart.py
+++ b/browser/components/search/test/marionette/test_engines_on_restart.py
@@ -10,17 +10,18 @@ from marionette_harness.marionette_test 
 
 
 class TestEnginesOnRestart(MarionetteTestCase):
 
     def setUp(self):
         super(TestEnginesOnRestart, self).setUp()
         self.marionette.enforce_gecko_prefs({
             'browser.search.log': True,
-            'browser.search.geoSpecificDefaults': False
+            'browser.search.geoSpecificDefaults': False,
+            'browser.search.addonLoadTimeout': 0
         })
 
     def get_default_search_engine(self):
         """Retrieve the identifier of the default search engine."""
 
         script = """\
         let [resolve] = arguments;
         let searchService = Components.classes[
--- a/browser/components/tests/unit/test_distribution.js
+++ b/browser/components/tests/unit/test_distribution.js
@@ -275,12 +275,17 @@ add_task(async function() {
     "Language Set"
   );
 
   Services.prefs.setCharPref(
     "distribution.searchplugins.defaultLocale",
     "de-DE"
   );
 
+  // Turn off region updates and timeouts for search service
+  Services.prefs.setCharPref("browser.search.region", "DE");
+  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
   await Services.search.init();
   var engine = Services.search.getEngineByName("Google");
   Assert.equal(engine.description, "override-de-DE");
 });
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -30,16 +30,21 @@ XPCOMUtils.defineLazyModuleGetters(this,
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
   UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
   UrlbarResult: "resource:///modules/UrlbarResult.jsm",
   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
 });
 const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
 
+// Turn off region updates and timeouts for search service
+Services.prefs.setCharPref("browser.search.region", "US");
+Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
 /**
  * @param {string} searchString The search string to insert into the context.
  * @param {object} properties Overrides for the default values.
  * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
  *          required options.
  */
 function createContext(searchString = "foo", properties = {}) {
   let context = new UrlbarQueryContext({
@@ -189,16 +194,19 @@ async function addTestEngine(basename, h
     }, "browser-search-engine-modified");
 
     info("Adding engine from URL: " + dataUrl + basename);
     Services.search.addEngine(dataUrl + basename, null, false);
   });
 }
 
 /**
+ * WARNING: use of this function may result in intermittent failures when tests
+ * run in parallel due to reliance on port 9000.  Duplicated in/from unifiedcomplete.
+ *
  * Sets up a search engine that provides some suggestions by appending strings
  * onto the search query.
  *
  * @param {function} suggestionsFn
  *        A function that returns an array of suggestion strings given a
  *        search string.  If not given, a default function is used.
  * @returns {nsISearchEngine} The new engine.
  */
--- a/testing/profiles/common/user.js
+++ b/testing/profiles/common/user.js
@@ -19,16 +19,20 @@ user_pref("browser.newtabpage.activity-s
 // Background thumbnails in particular cause grief, and disabling thumbnails
 // in general can't hurt - we re-enable them when tests need them.
 user_pref("browser.pagethumbnails.capturing_disabled", true);
 // Tell the search service we are running in the US.  This also has the desired
 // side-effect of preventing our geoip lookup.
 user_pref("browser.search.region", "US");
 // This will prevent HTTP requests for region defaults.
 user_pref("browser.search.geoSpecificDefaults", false);
+// Debug builds will timeout on the failsafe timeout for search init,
+// we just turn off the load timeout for tests in general.
+user_pref("browser.search.addonLoadTimeout", 0);
+
 // Disable webapp updates.  Yes, it is supposed to be an integer.
 user_pref("browser.webapps.checkForUpdates", 0);
 // We do not wish to display datareporting policy notifications as it might
 // cause other tests to fail. Tests that wish to test the notification functionality
 // should explicitly disable this pref.
 user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
 user_pref("dom.max_chrome_script_run_time", 0);
 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -69,17 +69,24 @@ AddonTestUtils.init(this, false);
 AddonTestUtils.createAppInfo(
   "xpcshell@tests.mozilla.org",
   "XPCShell",
   "42",
   "42"
 );
 
 add_task(async function setup() {
+  // Tell the search service we are running in the US.  This also has the
+  // desired side-effect of preventing our geoip lookup.
+  Services.prefs.setCharPref("browser.search.region", "US");
+  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
   await AddonTestUtils.promiseStartupManager();
+  await Services.search.init();
 });
 
 async function cleanup() {
   Services.prefs.clearUserPref("browser.urlbar.autoFill");
   Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
   let suggestPrefs = ["history", "bookmark", "openpage", "searches"];
   for (let type of suggestPrefs) {
     Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
@@ -555,16 +562,19 @@ function addTestEngine(basename, httpSer
     }, "browser-search-engine-modified");
 
     info("Adding engine from URL: " + dataUrl + basename);
     Services.search.addEngine(dataUrl + basename, null, false);
   });
 }
 
 /**
+ * WARNING: use of this function may result in intermittent failures when tests
+ * run in parallel due to reliance on port 9000.
+ *
  * Sets up a search engine that provides some suggestions by appending strings
  * onto the search query.
  *
  * @param   {function} suggestionsFn
  *          A function that returns an array of suggestion strings given a
  *          search string.  If not given, a default function is used.
  * @returns {nsISearchEngine} The new engine.
  */
@@ -601,16 +611,17 @@ add_task(async function ensure_search_en
   registerCleanupFunction(() => Services.prefs.clearUserPref(geoPref));
   // Remove any existing engines before adding ours.
   for (let engine of await Services.search.getEngines()) {
     await Services.search.removeEngine(engine);
   }
   await Services.search.addEngineWithDetails("MozSearch", {
     method: "GET",
     template: "http://s.example.com/search",
+    isBuiltin: true,
   });
   let engine = Services.search.getEngineByName("MozSearch");
   await Services.search.setDefault(engine);
 });
 
 /**
  * Add a adaptive result for a given (url, string) tuple.
  * @param {string} aUrl
--- a/toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
@@ -1,22 +1,24 @@
 /* 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/. */
 
 const { PlacesSearchAutocompleteProvider } = ChromeUtils.import(
   "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm"
 );
 
-add_task(async function() {
-  await Services.search.init();
+add_task(async function setup() {
   // Tell the search service we are running in the US.  This also has the
   // desired side-effect of preventing our geoip lookup.
   Services.prefs.setCharPref("browser.search.region", "US");
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
+  await Services.search.init();
 
   Services.search.restoreDefaultEngines();
   Services.search.resetToOriginalDefaultEngine();
 });
 
 add_task(async function search_engine_match() {
   let engine = await Services.search.getDefault();
   let domain = engine.getResultDomain();
@@ -33,19 +35,19 @@ add_task(async function no_match() {
     await PlacesSearchAutocompleteProvider.engineForDomainPrefix("test")
   );
 });
 
 add_task(async function hide_search_engine_nomatch() {
   let engine = await Services.search.getDefault();
   let domain = engine.getResultDomain();
   let token = domain.substr(0, 1);
-  let promiseTopic = promiseSearchTopic("engine-changed");
+  let promiseTopic = promiseSearchTopic("engine-removed");
   await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
-  Assert.ok(engine.hidden);
+  Assert.ok(engine.hidden, "engine was hidden rather than removed");
   let matchedEngine = await PlacesSearchAutocompleteProvider.engineForDomainPrefix(
     token
   );
   Assert.ok(!matchedEngine || matchedEngine.getResultDomain() != domain);
   engine.hidden = false;
   await TestUtils.waitForCondition(() =>
     PlacesSearchAutocompleteProvider.engineForDomainPrefix(token)
   );
@@ -158,41 +160,45 @@ add_task(async function test_parseSubmis
   // Most of the logic of parseSubmissionURL is tested in the search service
   // itself, thus we only do a sanity check of the wrapper here.
   let engine = await Services.search.getDefault();
   let submissionURL = engine.getSubmission("terms").uri.spec;
 
   let result = PlacesSearchAutocompleteProvider.parseSubmissionURL(
     submissionURL
   );
-  Assert.equal(result.engineName, engine.name);
+  Assert.equal(
+    result.engineName,
+    engine.name,
+    "parsed submissionURL has matching engine name"
+  );
   Assert.equal(result.terms, "terms");
 
   result = PlacesSearchAutocompleteProvider.parseSubmissionURL(
     "http://example.org/"
   );
   Assert.equal(result, null);
 });
 
 add_task(async function test_builtin_aliased_search_engine_match() {
   let engine = await PlacesSearchAutocompleteProvider.engineForAlias("@google");
-  Assert.ok(engine);
-  Assert.equal(engine.name, "Google");
+  Assert.ok(engine, "matched an engine with an alias");
+  Assert.equal(engine.name, "Google", "correct engine for alias");
   let promiseTopic = promiseSearchTopic("engine-changed");
   await Promise.all([Services.search.removeEngine(engine), promiseTopic]);
   let matchedEngine = await PlacesSearchAutocompleteProvider.engineForAlias(
     "@google"
   );
   Assert.ok(!matchedEngine);
   engine.hidden = false;
   await TestUtils.waitForCondition(() =>
     PlacesSearchAutocompleteProvider.engineForAlias("@google")
   );
   engine = await PlacesSearchAutocompleteProvider.engineForAlias("@google");
-  Assert.ok(engine);
+  Assert.ok(engine, "matched an engine with an alias");
 });
 
 function promiseSearchTopic(expectedVerb) {
   return new Promise(resolve => {
     Services.obs.addObserver(function observe(subject, topic, verb) {
       info("browser-search-engine-modified: " + verb);
       if (verb == expectedVerb) {
         Services.obs.removeObserver(observe, "browser-search-engine-modified");
--- a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
+++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
@@ -38,19 +38,23 @@ support-files =
 [test_keywords.js]
 [test_multi_word_search.js]
 [test_PlacesSearchAutocompleteProvider.js]
 skip-if = appname == "thunderbird"
 [test_preloaded_sites.js]
 [test_query_url.js]
 [test_remote_tab_matches.js]
 skip-if = !sync
-[test_search_engine_alias.js]
 [test_search_engine_default.js]
 [test_search_engine_host.js]
 [test_search_engine_restyle.js]
 [test_search_suggestions.js]
-[test_special_search.js]
 [test_swap_protocol.js]
 [test_tab_matches.js]
 [test_trimming.js]
 [test_visit_url.js]
 [test_word_boundary_search.js]
+# The following tests use addTestSuggestionsEngine which doesn't
+# play well when run in parallel.
+[test_search_engine_alias.js]
+run-sequentially = Test relies on port 9000, fails intermittently
+[test_special_search.js]
+run-sequentially = Test relies on port 9000, may fail intermittently
--- a/toolkit/components/places/tests/unit/head_bookmarks.js
+++ b/toolkit/components/places/tests/unit/head_bookmarks.js
@@ -13,16 +13,21 @@ var { Services } = ChromeUtils.import("r
   Services.scriptloader.loadSubScript(uri.spec, this);
 }
 
 // Put any other stuff relative to this test folder below.
 const { AddonTestUtils } = ChromeUtils.import(
   "resource://testing-common/AddonTestUtils.jsm"
 );
 
+// Turn off region updates and timeouts for search service
+Services.prefs.setCharPref("browser.search.region", "US");
+Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
 AddonTestUtils.init(this, false);
 AddonTestUtils.overrideCertDB();
 AddonTestUtils.createAppInfo(
   "xpcshell@tests.mozilla.org",
   "XPCShell",
   "1",
   "42"
 );
--- a/toolkit/components/search/SearchEngine.jsm
+++ b/toolkit/components/search/SearchEngine.jsm
@@ -43,16 +43,17 @@ const OPENSEARCH_NAMESPACES = [
 ];
 
 const OPENSEARCH_LOCALNAME = "OpenSearchDescription";
 
 const MOZSEARCH_NS_10 = "http://www.mozilla.org/2006/browser/search/";
 const MOZSEARCH_LOCALNAME = "SearchPlugin";
 
 const USER_DEFINED = "searchTerms";
+const SEARCH_TERM_PARAM = "{searchTerms}";
 
 // Custom search parameters
 const MOZ_PARAM_LOCALE = "moz:locale";
 const MOZ_PARAM_DIST_ID = "moz:distributionID";
 const MOZ_PARAM_OFFICIAL = "moz:official";
 
 // Supported OpenSearch parameters
 // See http://opensearch.a9.com/spec/1.1/querysyntax/#core
@@ -575,18 +576,28 @@ EngineURL.prototype = {
       postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
       postData.setData(stringStream);
     }
 
     return new Submission(Services.io.newURI(url), postData);
   },
 
   _getTermsParameterName() {
-    let queryParam = this.params.find(p => p.value == "{" + USER_DEFINED + "}");
-    return queryParam ? queryParam.name : "";
+    if (this.params.length > 0) {
+      let queryParam = this.params.find(p => p.value == SEARCH_TERM_PARAM);
+      return queryParam ? queryParam.name : "";
+    }
+    // If an engine only used template, then params is empty, fall back to checking the template.
+    let params = new URL(this.template).searchParams;
+    for (let [name, value] of params.entries()) {
+      if (value == SEARCH_TERM_PARAM) {
+        return name;
+      }
+    }
+    return "";
   },
 
   _hasRelation(rel) {
     return this.rels.some(e => e == rel.toLowerCase());
   },
 
   _initWithJSON(json) {
     if (!json.params) {
@@ -809,16 +820,18 @@ SearchEngine.prototype = {
   // The number of days between update checks for new versions
   _updateInterval: null,
   // The url to check at for a new update
   _updateURL: null,
   // The url to check for a new icon
   _iconUpdateURL: null,
   /* The extension ID if added by an extension. */
   _extensionID: null,
+  /* The extension version if added by an extension. */
+  _version: null,
   // Built in search engine extensions.
   _isBuiltin: false,
 
   /**
    * Retrieves the data from the engine's file asynchronously.
    * The document element is placed in the engine's data field.
    *
    * @param {nsIFile} file
@@ -1398,16 +1411,17 @@ SearchEngine.prototype = {
    *   Any special Mozilla Parameters.
    * @param {string} [params.postParams]
    *   Any parameters for a POST method.
    * @param {string} params.template
    *   The url template.
    */
   _initFromMetadata(engineName, params) {
     this._extensionID = params.extensionID;
+    this._version = params.version;
     this._isBuiltin = !!params.isBuiltin;
 
     this._initEngineURLFromMetaData(SearchUtils.URL_TYPE.SEARCH, {
       method: (params.searchPostParams && "POST") || params.method || "GET",
       template: params.template,
       getParams: params.searchGetParams,
       postParams: params.searchPostParams,
       mozParams: params.mozParams,
@@ -1679,16 +1693,19 @@ SearchEngine.prototype = {
     this._metaData = json._metaData || {};
     this._isBuiltin = json._isBuiltin;
     if (json.filePath) {
       this._filePath = json.filePath;
     }
     if (json.extensionID) {
       this._extensionID = json.extensionID;
     }
+    if (json.version) {
+      this._version = json.version;
+    }
     for (let i = 0; i < json._urls.length; ++i) {
       let url = json._urls[i];
       let engineURL = new EngineURL(
         url.type || SearchUtils.URL_TYPE.SEARCH,
         url.method || "GET",
         url.template,
         url.resultDomain || undefined
       );
@@ -1737,16 +1754,19 @@ SearchEngine.prototype = {
     if (this._filePath) {
       // File path is stored so that we can remove legacy xml files
       // from the profile if the user removes the engine.
       json.filePath = this._filePath;
     }
     if (this._extensionID) {
       json.extensionID = this._extensionID;
     }
+    if (this._version) {
+      json.version = this._version;
+    }
 
     return json;
   },
 
   setAttr(name, val) {
     this._metaData[name] = val;
   },
 
--- a/toolkit/components/search/SearchService.jsm
+++ b/toolkit/components/search/SearchService.jsm
@@ -8,25 +8,25 @@ const { XPCOMUtils } = ChromeUtils.impor
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 const { PromiseUtils } = ChromeUtils.import(
   "resource://gre/modules/PromiseUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
-  AddonManager: "resource://gre/modules/AddonManager.jsm",
   clearTimeout: "resource://gre/modules/Timer.jsm",
   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
   ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
   getVerificationHash: "resource://gre/modules/SearchEngine.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   RemoteSettings: "resource://services-settings/remote-settings.js",
   RemoteSettingsClient: "resource://services-settings/RemoteSettingsClient.jsm",
   SearchEngine: "resource://gre/modules/SearchEngine.jsm",
+  SearchExtensionLoader: "resource://gre/modules/SearchUtils.jsm",
   SearchStaticData: "resource://gre/modules/SearchStaticData.jsm",
   SearchUtils: "resource://gre/modules/SearchUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   gEnvironment: ["@mozilla.org/process/environment;1", "nsIEnvironment"],
@@ -48,24 +48,16 @@ XPCOMUtils.defineLazyGetter(this, "distr
 // A text encoder to UTF8, used whenever we commit the cache to disk.
 XPCOMUtils.defineLazyGetter(this, "gEncoder", function() {
   return new TextEncoder();
 });
 
 // Directory service keys
 const NS_APP_DISTRIBUTION_SEARCH_DIR_LIST = "SrchPluginsDistDL";
 
-// We load plugins from EXT_SEARCH_PREFIX, where a list.json
-// file needs to exist to list available engines.
-const EXT_SEARCH_PREFIX = "resource://search-extensions/";
-const APP_SEARCH_PREFIX = "resource://search-plugins/";
-
-// The address we use to sign the built in search extensions with.
-const EXT_SIGNING_ADDRESS = "search.mozilla.org";
-
 const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
 const QUIT_APPLICATION_TOPIC = "quit-application";
 
 // The following constants are left undocumented in nsISearchService.idl
 // For the moment, they are meant for testing/debugging purposes only.
 
 // Delay for batching invalidation of the JSON cache (ms)
 const CACHE_INVALIDATION_DELAY = 1000;
@@ -262,21 +254,22 @@ function fetchRegion(ss) {
     ERROR: 3,
     // Note that we expect to add finer-grained error types here later (eg,
     // dns error, network error, ssl error, etc) with .ERROR remaining as the
     // generic catch-all that doesn't fit into other categories.
   };
   let endpoint = Services.urlFormatter.formatURLPref(
     "browser.search.geoip.url"
   );
-  SearchUtils.log("_fetchRegion starting with endpoint " + endpoint);
   // As an escape hatch, no endpoint means no geoip.
   if (!endpoint) {
     return Promise.resolve();
   }
+  SearchUtils.log("_fetchRegion starting with endpoint " + endpoint);
+
   let startTime = Date.now();
   return new Promise(resolve => {
     // Instead of using a timeout on the xhr object itself, we simulate one
     // using a timer and let the XHR request complete.  This allows us to
     // capture reliable telemetry on what timeout value should actually be
     // used to ensure most users don't see one while not making it so large
     // that many users end up doing a sync init of the search service and thus
     // would see the jank that implies.
@@ -564,16 +557,19 @@ const gEmptyParseSubmissionResult = Obje
 /**
  * The search service handles loading and maintaining of search engines. It will
  * also work out the default lists for each locale/region.
  *
  * @implements {nsISearchService}
  */
 function SearchService() {
   this._initObservers = PromiseUtils.defer();
+  // This deferred promise is resolved once a set of engines have been
+  // parsed out of list.json, which happens in _loadEngines.
+  this._extensionLoadReady = PromiseUtils.defer();
 }
 
 SearchService.prototype = {
   classID: Components.ID("{7319788a-fe93-4db3-9f39-818cf08f4256}"),
 
   // The current status of initialization. Note that it does not determine if
   // initialization is complete, only if an error has been encountered so far.
   _initRV: Cr.NS_OK,
@@ -635,16 +631,17 @@ SearchService.prototype = {
    *   to be absolutely certain of the correct default engine and/ or ordering of
    *   visible engines.
    * @returns {number}
    *   A Components.results success code on success, otherwise a failure code.
    */
   async _init(skipRegionCheck) {
     SearchUtils.log("_init start");
 
+    TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
     try {
       // See if we have a cache file so we don't have to parse a bunch of XML.
       let cache = await this._readCacheFile();
 
       // The init flow is not going to block on a fetch from an external service,
       // but we're kicking it off as soon as possible to prevent UI flickering as
       // much as possible.
       this._ensureKnownRegionPromise = ensureKnownRegion(this)
@@ -660,35 +657,38 @@ SearchService.prototype = {
 
       await this._loadEngines(cache);
 
       // Make sure the current list of engines is persisted, without the need to wait.
       SearchUtils.log("_init: engines loaded, writing cache");
       this._buildCache();
       this._addObservers();
     } catch (ex) {
-      this._initRV = ex.result !== undefined ? ex.result : Cr.NS_ERROR_FAILURE;
+      // If loadEngines has a rejected promise chain, ex is undefined.
+      this._initRV =
+        ex && ex.result !== undefined ? ex.result : Cr.NS_ERROR_FAILURE;
       SearchUtils.log(
-        "_init: failure initializng search: " + ex + "\n" + ex.stack
+        "_init: failure initializing search: " + ex + "\n" + (ex && ex.stack)
       );
     }
     gInitialized = true;
     if (Components.isSuccessCode(this._initRV)) {
+      TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
       this._initObservers.resolve(this._initRV);
     } else {
+      TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
       this._initObservers.reject(this._initRV);
     }
     Services.obs.notifyObservers(
       null,
       SearchUtils.TOPIC_SEARCH_SERVICE,
       "init-complete"
     );
 
     SearchUtils.log("_init: Completed _init");
-    return this._initRV;
   },
 
   /**
    * Obtains the current ignore list from remote settings. This includes
    * verifying the signature of the ignore list within the database.
    *
    * If the signature in the database is invalid, the database will be wiped
    * and the stored dump will be used, until the settings next update.
@@ -842,30 +842,26 @@ SearchService.prototype = {
     let val = this.getGlobalAttr(name);
     if (val && this.getGlobalAttr(name + "Hash") != getVerificationHash(val)) {
       SearchUtils.log("getVerifiedGlobalAttr, invalid hash for " + name);
       return "";
     }
     return val;
   },
 
-  _listJSONURL:
-    (AppConstants.platform == "android"
-      ? APP_SEARCH_PREFIX
-      : EXT_SEARCH_PREFIX) + "list.json",
+  // Some tests need to modify this url, they can do so through SearchUtils.
+  get _listJSONURL() {
+    return SearchUtils.LIST_JSON_URL;
+  },
 
   _engines: {},
   __sortedEngines: null,
   _visibleDefaultEngines: [],
   _searchDefault: null,
   _searchOrder: [],
-  // A Set of installed search extensions reported by AddonManager
-  // startup before SearchSevice has started. Will be installed
-  // during init().
-  _startupExtensions: new Set(),
 
   get _sortedEngines() {
     if (!this.__sortedEngines) {
       return this._buildSortedEngineList();
     }
     return this.__sortedEngines;
   },
 
@@ -1021,22 +1017,25 @@ SearchService.prototype = {
       !cache.engines ||
       cache.version != CACHE_VERSION ||
       cache.locale != Services.locale.requestedLocale ||
       cache.buildID != buildID ||
       cache.visibleDefaultEngines.length !=
         this._visibleDefaultEngines.length ||
       this._visibleDefaultEngines.some(notInCacheVisibleEngines);
 
+    this._engineLocales = this._enginesToLocales(engines);
+    this._extensionLoadReady.resolve();
+
     if (!rebuildCache) {
       SearchUtils.log("_loadEngines: loading from cache directories");
       this._loadEnginesFromCache(cache);
       if (Object.keys(this._engines).length) {
         SearchUtils.log("_loadEngines: done using existing cache");
-        return;
+        return Promise.resolve();
       }
       SearchUtils.log(
         "_loadEngines: No valid engines found in cache. Loading engines from disk."
       );
     }
 
     SearchUtils.log(
       "_loadEngines: Absent or outdated cache. Loading engines from disk."
@@ -1044,87 +1043,43 @@ SearchService.prototype = {
     for (let loadDir of distDirs) {
       let enginesFromDir = await this._loadEnginesFromDir(loadDir);
       enginesFromDir.forEach(this._addEngineToStore, this);
     }
     if (AppConstants.platform == "android") {
       let enginesFromURLs = await this._loadFromChromeURLs(engines, isReload);
       enginesFromURLs.forEach(this._addEngineToStore, this);
     } else {
-      let engineList = this._enginesToLocales(engines);
-      for (let [id, locales] of engineList) {
-        await this.ensureBuiltinExtension(id, locales);
-      }
-
-      SearchUtils.log(
-        "_loadEngines: loading " +
-          this._startupExtensions.size +
-          " engines reported by AddonManager startup"
-      );
-      for (let extension of this._startupExtensions) {
-        await this._installExtensionEngine(extension, [DEFAULT_TAG], true);
-      }
+      return SearchExtensionLoader.installAddons(this._engineLocales.keys());
     }
 
     SearchUtils.log(
       "_loadEngines: loading user-installed engines from the obsolete cache"
     );
     this._loadEnginesFromCache(cache, true);
 
     this._loadEnginesMetadataFromCache(cache);
 
     SearchUtils.log("_loadEngines: done using rebuilt cache");
-  },
-
-  /**
-   * Ensures a built in search WebExtension is installed, installing
-   * it if necessary.
-   *
-   * @param {string} id
-   *   The WebExtension ID.
-   * @param {Array<string>} locales
-   *   An array of locales to use for the WebExtension. If more than
-   *   one is specified, different versions of the same engine may
-   *   be installed.
-   */
-  async ensureBuiltinExtension(id, locales = [DEFAULT_TAG]) {
-    SearchUtils.log("ensureBuiltinExtension: " + id);
-    try {
-      let policy = WebExtensionPolicy.getByID(id);
-      if (!policy) {
-        SearchUtils.log("ensureBuiltinExtension: Installing " + id);
-        let path = EXT_SEARCH_PREFIX + id.split("@")[0] + "/";
-        await AddonManager.installBuiltinAddon(path);
-        policy = WebExtensionPolicy.getByID(id);
-      }
-      // On startup the extension may have not finished parsing the
-      // manifest, wait for that here.
-      await policy.readyPromise;
-      await this._installExtensionEngine(policy.extension, locales);
-      SearchUtils.log("ensureBuiltinExtension: " + id + " installed.");
-    } catch (err) {
-      Cu.reportError(
-        "Failed to install engine: " + err.message + "\n" + err.stack
-      );
-    }
+    return Promise.resolve();
   },
 
   /**
    * Converts array of engines into a Map of extensions + the locales
    * of those extensions to install.
    *
    * @param {array} engines
    *   An array of engines
-   * @returns {Map} A Map of extension names + locales.
+   * @returns {Map} A Map of extension IDs to locales.
    */
   _enginesToLocales(engines) {
     let engineLocales = new Map();
     for (let engine of engines) {
       let [extensionName, locale] = this._parseEngineName(engine);
-      let id = extensionName + "@" + EXT_SIGNING_ADDRESS;
+      let id = SearchUtils.makeExtensionId(extensionName);
       let locales = engineLocales.get(id) || new Set();
       locales.add(locale);
       engineLocales.set(id, locales);
     }
     return engineLocales;
   },
 
   /**
@@ -1197,19 +1152,24 @@ SearchService.prototype = {
       SearchUtils.log("_reInit: already re-initializing, bailing out.");
       return;
     }
     gReinitializing = true;
 
     // Start by clearing the initialized state, so we don't abort early.
     gInitialized = false;
 
+    // Reset any init promises synchronously before the async init below.
+    this._initObservers = PromiseUtils.defer();
+    this._extensionLoadReady = PromiseUtils.defer();
+    // If reset is called prior to reinit, be sure to mark init as started.
+    this._initStarted = true;
+
     (async () => {
       try {
-        this._initObservers = PromiseUtils.defer();
         if (this._batchTask) {
           SearchUtils.log("finalizing batch task");
           let task = this._batchTask;
           this._batchTask = null;
           // Tests manipulate the cache directly, so let's not double-write with
           // stale cache data here.
           if (origin == "test") {
             task.disarm();
@@ -1221,16 +1181,17 @@ SearchService.prototype = {
         // Clear the engines, too, so we don't stick with the stale ones.
         this._engines = {};
         this.__sortedEngines = null;
         this._currentEngine = null;
         this._visibleDefaultEngines = [];
         this._searchDefault = null;
         this._searchOrder = [];
         this._metaData = {};
+        this._engineLocales = null;
 
         // Tests that want to force a synchronous re-initialization need to
         // be notified when we are done uninitializing.
         Services.obs.notifyObservers(
           null,
           SearchUtils.TOPIC_SEARCH_SERVICE,
           "uninit-complete"
         );
@@ -1265,16 +1226,17 @@ SearchService.prototype = {
       } catch (err) {
         SearchUtils.log("Reinit failed: " + err);
         SearchUtils.log(err.stack);
         Services.obs.notifyObservers(
           null,
           SearchUtils.TOPIC_SEARCH_SERVICE,
           "reinit-failed"
         );
+        this._initObservers.reject();
       } finally {
         gReinitializing = false;
         Services.obs.notifyObservers(
           null,
           SearchUtils.TOPIC_SEARCH_SERVICE,
           "reinit-complete"
         );
       }
@@ -1288,16 +1250,18 @@ SearchService.prototype = {
     gInitialized = false;
     this._initObservers = PromiseUtils.defer();
     this._initStarted = this.__sortedEngines = this._currentEngine = this._searchDefault = null;
     this._startupExtensions = new Set();
     this._engines = {};
     this._visibleDefaultEngines = [];
     this._searchOrder = [];
     this._metaData = {};
+    this._extensionLoadReady = PromiseUtils.defer();
+    this._engineLocales = null;
   },
 
   /**
    * Read the cache file asynchronously.
    */
   async _readCacheFile() {
     let json;
     try {
@@ -1342,31 +1306,41 @@ SearchService.prototype = {
   },
 
   _addEngineToStore(engine) {
     if (this._engineMatchesIgnoreLists(engine)) {
       SearchUtils.log("_addEngineToStore: Ignoring engine");
       return;
     }
 
-    SearchUtils.log('_addEngineToStore: Adding engine: "' + engine.name + '"');
-
     // See if there is an existing engine with the same name. However, if this
     // engine is updating another engine, it's allowed to have the same name.
-    var hasSameNameAsUpdate =
-      engine._engineToUpdate && engine.name == engine._engineToUpdate.name;
-    if (engine.name in this._engines && !hasSameNameAsUpdate) {
+    var matchingEngineUpdate =
+      engine._engineToUpdate &&
+      (engine.name == engine._engineToUpdate.name ||
+        (engine._extensionID &&
+          engine._extensionID == engine._engineToUpdate._extensionID));
+    if (engine.name in this._engines && !matchingEngineUpdate) {
       SearchUtils.log("_addEngineToStore: Duplicate engine found, aborting!");
       return;
     }
 
     if (engine._engineToUpdate) {
+      SearchUtils.log(
+        '_addEngineToStore: Updating engine: "' + engine.name + '"'
+      );
       // We need to replace engineToUpdate with the engine that just loaded.
       var oldEngine = engine._engineToUpdate;
 
+      let index = -1;
+      if (this.__sortedEngines) {
+        index = this.__sortedEngines.indexOf(oldEngine);
+      }
+      let isCurrent = this._currentEngine == oldEngine;
+
       // Remove the old engine from the hash, since it's keyed by name, and our
       // name might change (the update might have a new name).
       delete this._engines[oldEngine.name];
 
       // Hack: we want to replace the old engine with the new one, but since
       // people may be holding refs to the nsISearchEngine objects themselves,
       // we'll just copy over all "private" properties (those without a getter
       // or setter) from one object to the other.
@@ -1375,18 +1349,28 @@ SearchService.prototype = {
           oldEngine[p] = engine[p];
         }
       }
       engine = oldEngine;
       engine._engineToUpdate = null;
 
       // Add the engine back
       this._engines[engine.name] = engine;
+      if (index >= 0) {
+        this.__sortedEngines[index] = engine;
+        this._saveSortedEngineList();
+      }
+      if (isCurrent) {
+        this._currentEngine = engine;
+      }
       SearchUtils.notifyAction(engine, SearchUtils.MODIFIED_TYPE.CHANGED);
     } else {
+      SearchUtils.log(
+        '_addEngineToStore: Adding engine: "' + engine.name + '"'
+      );
       // Not an update, just add the new engine.
       this._engines[engine.name] = engine;
       // Only add the engine to the list of sorted engines if the initial list
       // has already been built (i.e. if this.__sortedEngines is non-null). If
       // it hasn't, we're loading engines from disk and the sorted engine list
       // will be built once we need it.
       if (this.__sortedEngines) {
         this.__sortedEngines.push(engine);
@@ -1529,17 +1513,19 @@ SearchService.prototype = {
    */
   async _loadFromChromeURLs(urls, isReload = false) {
     let engines = [];
     for (let url of urls) {
       try {
         SearchUtils.log(
           "_loadFromChromeURLs: loading engine from chrome url: " + url
         );
-        let uri = Services.io.newURI(APP_SEARCH_PREFIX + url + ".xml");
+        let uri = Services.io.newURI(
+          SearchUtils.APP_SEARCH_PREFIX + url + ".xml"
+        );
         let engine = new SearchEngine({
           uri,
           readOnly: true,
         });
         await engine._initFromURI(uri);
         // If there is an existing engine with the same name then update that engine.
         // Only do this during reloads so it doesnt interfere with distribution
         // engines
@@ -1570,28 +1556,28 @@ SearchService.prototype = {
       );
       return [];
     }
 
     // Read list.json to find the engines we need to load.
     let request = new XMLHttpRequest();
     request.overrideMimeType("text/plain");
     let list = await new Promise(resolve => {
-      request.onload = function(event) {
+      request.onload = event => {
         resolve(event.target.responseText);
       };
-      request.onerror = function(event) {
+      request.onerror = event => {
         SearchUtils.log("_findEngines: failed to read " + this._listJSONURL);
         resolve();
       };
       request.open("GET", Services.io.newURI(this._listJSONURL).spec, true);
       request.send();
     });
 
-    return this._parseListJSON(list);
+    return list !== undefined ? this._parseListJSON(list) : [];
   },
 
   _parseListJSON(list) {
     let json;
     try {
       json = JSON.parse(list);
     } catch (e) {
       Cu.reportError("parseListJSON: Failed to parse list.json: " + e);
@@ -1737,17 +1723,17 @@ SearchService.prototype = {
       this._searchDefault = searchSettings[searchRegion].searchDefault;
     } else if ("searchDefault" in searchSettings.default) {
       this._searchDefault = searchSettings.default.searchDefault;
     } else {
       this._searchDefault = json.default.searchDefault;
     }
 
     if (!this._searchDefault) {
-      Cu.reportError("parseListJSON: No searchDefault");
+      SearchUtils.log("parseListJSON: No searchDefault");
     }
 
     if (
       searchRegion &&
       searchRegion in searchSettings &&
       "searchOrder" in searchSettings[searchRegion]
     ) {
       this._searchOrder = searchSettings[searchRegion].searchOrder;
@@ -1920,48 +1906,28 @@ SearchService.prototype = {
 
     return this._sortedEngines.filter(function(engine) {
       return !engine.hidden;
     });
   },
 
   // nsISearchService
   async init(skipRegionCheck = false) {
-    SearchUtils.log("SearchService.init");
     if (this._initStarted) {
       if (!skipRegionCheck) {
         await this._ensureKnownRegionPromise;
       }
       return this._initObservers.promise;
     }
-
-    TelemetryStopwatch.start("SEARCH_SERVICE_INIT_MS");
     this._initStarted = true;
-    try {
-      // Complete initialization by calling asynchronous initializer.
-      await this._init(skipRegionCheck);
-      TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
-    } catch (ex) {
-      if (ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
-        // No need to pursue asynchronous because synchronous fallback was
-        // called and has finished.
-        TelemetryStopwatch.finish("SEARCH_SERVICE_INIT_MS");
-      } else {
-        this._initObservers.reject(ex.result);
-        TelemetryStopwatch.cancel("SEARCH_SERVICE_INIT_MS");
-        throw ex;
-      }
-    }
-    if (!Components.isSuccessCode(this._initRV)) {
-      throw Components.Exception(
-        "SearchService initialization failed",
-        this._initRV
-      );
-    }
-    return this._initRV;
+    SearchUtils.log("SearchService.init");
+
+    // Don't await on _init, _initObservers is resolved or rejected in _init.
+    this._init(skipRegionCheck);
+    return this._initObservers.promise;
   },
 
   get isInitialized() {
     return gInitialized;
   },
 
   // reInit is currently only exposed for testing purposes
   async reInit(skipRegionCheck) {
@@ -2078,120 +2044,122 @@ SearchService.prototype = {
       ) {
         return engine;
       }
     }
     return null;
   },
 
   async addEngineWithDetails(name, details) {
-    SearchUtils.log('addEngineWithDetails: Adding "' + name + '".');
-    let isCurrent = false;
-    var params = details;
-
-    let isBuiltin = !!params.isBuiltin;
-    // We install search extensions during the init phase, both built in
-    // web extensions freshly installed (via addEnginesFromExtension) or
-    // user installed extensions being reenabled calling this directly.
-    if (!gInitialized && !isBuiltin && !params.initEngine) {
+    // We only enforce init when called via the IDL API.  Internally we are adding engines
+    // during init and do not wait on this.
+    if (!gInitialized) {
       await this.init(true);
     }
+    return this._addEngineWithDetails(name, details);
+  },
+
+  async _addEngineWithDetails(name, params) {
+    SearchUtils.log('addEngineWithDetails: Adding "' + name + '".');
+
     if (!name) {
       SearchUtils.fail("Invalid name passed to addEngineWithDetails!");
     }
     if (!params.template) {
       SearchUtils.fail("Invalid template passed to addEngineWithDetails!");
     }
     let existingEngine = this._engines[name];
     if (existingEngine) {
-      if (
+      // Is this a webextension update?  If not we're dealing with legacy opensearch or an override attempt.
+      let webExtUpdate =
         params.extensionID &&
-        existingEngine._loadPath.startsWith(
-          `jar:[profile]/extensions/${params.extensionID}`
-        )
-      ) {
-        // This is a legacy extension engine that needs to be migrated to WebExtensions.
-        isCurrent = this.defaultEngine == existingEngine;
-        await this.removeEngine(existingEngine);
-      } else {
-        SearchUtils.fail(
-          "An engine with that name already exists!",
-          Cr.NS_ERROR_FILE_ALREADY_EXISTS
-        );
+        params.extensionID === existingEngine._extensionID;
+      if (!webExtUpdate) {
+        let webExtBuiltin = params.extensionID && params.isBuiltin;
+        // Is the existing engine a distribution engine?
+        if (
+          webExtBuiltin &&
+          existingEngine._loadPath.startsWith(
+            `[profile]/distribution/searchplugins/`
+          )
+        ) {
+          SearchExtensionLoader.reject(
+            params.extensionID,
+            new Error(
+              `${params.extensionID} cannot override distribution engine.`
+            )
+          );
+          return null;
+        } else if (
+          params.extensionID &&
+          existingEngine._loadPath.startsWith(
+            `jar:[profile]/extensions/${params.extensionID}`
+          )
+        ) {
+          // We uninstall the legacy engine, but we don't need to wait or do anything else here,
+          // _addEngineToStore will handle updating the engine data we're using.
+          this._removeEngineInstall(existingEngine);
+        } else {
+          SearchUtils.fail(
+            `An engine with the name ${name} already exists!`,
+            Cr.NS_ERROR_FILE_ALREADY_EXISTS
+          );
+        }
       }
     }
 
     let newEngine = new SearchEngine({
       name,
-      readOnly: isBuiltin,
+      readOnly: !!params.isBuiltin,
       sanitizeName: true,
     });
     newEngine._initFromMetadata(name, params);
     newEngine._loadPath = "[other]addEngineWithDetails";
     if (params.extensionID) {
       newEngine._loadPath += ":" + params.extensionID;
     }
+    newEngine._engineToUpdate = existingEngine;
 
     this._addEngineToStore(newEngine);
-    if (isCurrent) {
-      this.defaultEngine = newEngine;
-    }
     return newEngine;
   },
 
   async addEnginesFromExtension(extension) {
     SearchUtils.log("addEnginesFromExtension: " + extension.id);
-    if (extension.addonData.builtIn) {
-      SearchUtils.log("addEnginesFromExtension: Ignoring builtIn engine.");
-      return [];
-    }
-    // If we havent started SearchService yet, store this extension
-    // to install in SearchService.init().
-    if (!gInitialized) {
-      this._startupExtensions.add(extension);
-      return [];
-    }
-    return this._installExtensionEngine(extension, [DEFAULT_TAG]);
-  },
-
-  async _installExtensionEngine(extension, locales, initEngine) {
-    SearchUtils.log("installExtensionEngine: " + extension.id);
+    // Wait for the list.json engines to be parsed before
+    // allowing addEnginesFromExtension to continue.  This delays early start
+    // extensions until we are at a stage that they can be handled.
+    await this._extensionLoadReady.promise;
+    let locales = this._engineLocales.get(extension.id) || [DEFAULT_TAG];
 
     let installLocale = async locale => {
       let manifest =
         locale === DEFAULT_TAG
           ? extension.manifest
           : await extension.getLocalizedManifest(locale);
-      return this._addEngineForManifest(
-        extension,
-        manifest,
-        locale,
-        initEngine
-      );
+      return this._addEngineForManifest(extension, manifest, locale);
     };
 
     let engines = [];
     for (let locale of locales) {
       SearchUtils.log(
         "addEnginesFromExtension: installing locale: " +
           extension.id +
           ":" +
           locale
       );
-      engines.push(await installLocale(locale));
+      engines.push(installLocale(locale));
     }
-    return engines;
+    return Promise.all(engines).then(installedEngines => {
+      SearchExtensionLoader.resolve(extension.id);
+      return installedEngines;
+    });
   },
 
-  async _addEngineForManifest(
-    extension,
-    manifest,
-    locale = DEFAULT_TAG,
-    initEngine = false
-  ) {
+  async _addEngineForManifest(extension, manifest, locale = DEFAULT_TAG) {
     let { IconDetails } = ExtensionParent;
 
     // General set of icons for an engine.
     let icons = extension.manifest.icons;
     let iconList = [];
     if (icons) {
       iconList = Object.entries(icons).map(icon => {
         return {
@@ -2236,20 +2204,20 @@ SearchService.prototype = {
       extensionID: extension.id,
       isBuiltin: extension.addonData.builtIn,
       // suggest_url doesn't currently get encoded.
       suggestURL: searchProvider.suggest_url,
       suggestPostParams: searchProvider.suggest_url_post_params,
       suggestGetParams: searchProvider.suggest_url_get_params,
       queryCharset: searchProvider.encoding || "UTF-8",
       mozParams: searchProvider.params,
-      initEngine,
+      version: extension.version,
     };
 
-    return this.addEngineWithDetails(params.name, params);
+    return this._addEngineWithDetails(params.name, params);
   },
 
   async addEngine(engineURL, iconURL, confirm, extensionID) {
     SearchUtils.log('addEngine: Adding "' + engineURL + '".');
     await this.init(true);
     let errCode;
     try {
       var engine = new SearchEngine({
@@ -2287,16 +2255,29 @@ SearchService.prototype = {
 
   async removeWebExtensionEngine(id) {
     SearchUtils.log("removeWebExtensionEngine: " + id);
     for (let engine of await this.getEnginesByExtensionID(id)) {
       await this.removeEngine(engine);
     }
   },
 
+  async _removeEngineInstall(engine) {
+    // Make sure there is a file and this is not a webextension.
+    if (!engine._filePath || engine._extensionID) {
+      return;
+    }
+    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    file.persistentDescriptor = engine._filePath;
+    if (file.exists()) {
+      file.remove(false);
+    }
+    engine._filePath = null;
+  },
+
   async removeEngine(engine) {
     await this.init(true);
     if (!engine) {
       SearchUtils.fail("no engine passed to removeEngine!");
     }
 
     var engineToRemove = null;
     for (var e in this._engines) {
@@ -2318,24 +2299,17 @@ SearchService.prototype = {
 
     if (engineToRemove._readOnly || engineToRemove.isBuiltin) {
       // Just hide it (the "hidden" setter will notify) and remove its alias to
       // avoid future conflicts with other engines.
       engineToRemove.hidden = true;
       engineToRemove.alias = null;
     } else {
       // Remove the engine file from disk if we had a legacy file in the profile.
-      if (engineToRemove._filePath) {
-        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-        file.persistentDescriptor = engineToRemove._filePath;
-        if (file.exists()) {
-          file.remove(false);
-        }
-        engineToRemove._filePath = null;
-      }
+      this._removeEngineInstall(engineToRemove);
 
       // Remove the engine from _sortedEngines
       var index = this._sortedEngines.indexOf(engineToRemove);
       if (index == -1) {
         SearchUtils.fail(
           "Can't find engine to remove in _sortedEngines!",
           Cr.NS_ERROR_FAILURE
         );
--- a/toolkit/components/search/SearchUtils.jsm
+++ b/toolkit/components/search/SearchUtils.jsm
@@ -1,39 +1,62 @@
 /* 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/. */
 
 /* eslint no-shadow: error, mozilla/no-aArgs: error */
 
 "use strict";
 
-var EXPORTED_SYMBOLS = ["SearchUtils"];
+var EXPORTED_SYMBOLS = ["SearchUtils", "SearchExtensionLoader"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonManager: "resource://gre/modules/AddonManager.jsm",
+  AppConstants: "resource://gre/modules/AppConstants.jsm",
+  PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
+  clearTimeout: "resource://gre/modules/Timer.jsm",
+  setTimeout: "resource://gre/modules/Timer.jsm",
 });
 
 const BROWSER_SEARCH_PREF = "browser.search.";
 
+const EXT_SEARCH_PREFIX = "resource://search-extensions/";
+const APP_SEARCH_PREFIX = "resource://search-plugins/";
+
+// By the time we start loading an extension, it should load much
+// faster than 1000ms.  This simply ensures we resolve all the
+// promises and let search init complete if something happens.
+XPCOMUtils.defineLazyPreferenceGetter(
+  this,
+  "ADDON_LOAD_TIMEOUT",
+  BROWSER_SEARCH_PREF + "addonLoadTimeout",
+  1000
+);
+
 XPCOMUtils.defineLazyPreferenceGetter(
   this,
   "loggingEnabled",
   BROWSER_SEARCH_PREF + "log",
   false
 );
 
 var SearchUtils = {
-  APP_SEARCH_PREFIX: "resource://search-plugins/",
+  APP_SEARCH_PREFIX,
 
   BROWSER_SEARCH_PREF,
+  EXT_SEARCH_PREFIX,
+  LIST_JSON_URL:
+    (AppConstants.platform == "android"
+      ? APP_SEARCH_PREFIX
+      : EXT_SEARCH_PREFIX) + "list.json",
 
   /**
    * Topic used for events involving the service itself.
    */
   TOPIC_SEARCH_SERVICE: "browser-search-service",
 
   // See documentation in nsISearchService.idl.
   TOPIC_ENGINE_MODIFIED: "browser-search-engine-modified",
@@ -90,17 +113,16 @@ var SearchUtils = {
   /**
    * Outputs text to the JavaScript console as well as to stdout.
    *
    * @param {string} text
    *   The message to log.
    */
   log(text) {
     if (loggingEnabled) {
-      dump("*** Search: " + text + "\n");
       Services.console.logStringMessage(text);
     }
   },
 
   /**
    * Logs the failure message (if browser.search.log is enabled) and throws.
    * @param {string} message
    *   A message to display
@@ -145,9 +167,127 @@ var SearchUtils = {
         null /* triggeringPrincipal */,
         Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
         Ci.nsIContentPolicy.TYPE_OTHER
       );
     } catch (ex) {}
 
     return null;
   },
+
+  makeExtensionId(name) {
+    return name + "@search.mozilla.org";
+  },
+
+  getExtensionUrl(id) {
+    return EXT_SEARCH_PREFIX + id.split("@")[0] + "/";
+  },
 };
+
+/**
+ * SearchExtensionLoader provides a simple install function that
+ * returns a set of promises.  The caller (SearchService) must resolve
+ * each extension id once it has handled the final part of the install
+ * (creating the SearchEngine).  Once they are resolved, the extensions
+ * are fully functional, in terms of the SearchService, and initialization
+ * can be completed.
+ *
+ * When an extension is installed (that has a search provider), the
+ * extension system will call ss.addEnginesFromExtension. When that is
+ * completed, SearchService calls back to resolve the promise.
+ */
+const SearchExtensionLoader = {
+  _promises: new Map(),
+  // strict is used in tests.
+  _strict: false,
+
+  /**
+   * Creates a deferred promise for an extension install.
+   * @param {string} id the extension id.
+   * @returns {Promise}
+   */
+  _addPromise(id) {
+    let deferred = PromiseUtils.defer();
+    // We never want to have some uncaught problem stop the SearchService
+    // init from completing, so timeout the promise.
+    if (ADDON_LOAD_TIMEOUT > 0) {
+      deferred.timeout = setTimeout(() => {
+        deferred.reject(id, new Error("addon install timed out."));
+        this._promises.delete(id);
+      }, ADDON_LOAD_TIMEOUT);
+    }
+    this._promises.set(id, deferred);
+    return deferred.promise;
+  },
+
+  /**
+   * @param {string} id the extension id to resolve.
+   */
+  resolve(id) {
+    if (this._promises.has(id)) {
+      let deferred = this._promises.get(id);
+      if (deferred.timeout) {
+        clearTimeout(deferred.timeout);
+      }
+      deferred.resolve();
+      this._promises.delete(id);
+    }
+  },
+
+  /**
+   * @param {string} id the extension id to reject.
+   * @param {object} error The error to log when rejecting.
+   */
+  reject(id, error) {
+    if (this._promises.has(id)) {
+      let deferred = this._promises.get(id);
+      if (deferred.timeout) {
+        clearTimeout(deferred.timeout);
+      }
+      // We don't want to reject here because that will reject the promise.all
+      // and stop the searchservice init.  Log the error, and resolve the promise.
+      // strict mode can be used by tests to force an exception to occur.
+      Cu.reportError(`Addon install for search engine ${id} failed: ${error}`);
+      if (this._strict) {
+        deferred.reject();
+      } else {
+        deferred.resolve();
+      }
+      this._promises.delete(id);
+    }
+  },
+
+  _reset() {
+    SearchUtils.log(`SearchExtensionLoader.reset`);
+    for (let id of this._promises.keys()) {
+      this.reject(id, new Error(`installAddons reset during install`));
+    }
+    this._promises = new Map();
+  },
+
+  /**
+   * Tell AOM to install a set of built-in extensions.  If the extension is
+   * already installed, it will be reinstalled.
+   *
+   * @param {Array} engineIDList is an array of extension IDs.
+   * @returns {Promise} resolved when all engines have finished installation.
+   */
+  async installAddons(engineIDList) {
+    SearchUtils.log(`SearchExtensionLoader.installAddons`);
+    // If SearchService calls us again, it is being re-inited.  reset ourselves.
+    this._reset();
+    let promises = [];
+    for (let id of engineIDList) {
+      promises.push(this._addPromise(id));
+      let path = SearchUtils.getExtensionUrl(id);
+      SearchUtils.log(
+        `SearchExtensionLoader.installAddons: installing ${id} at ${path}`
+      );
+      // The AddonManager will install the engine asynchronously
+      AddonManager.installBuiltinAddon(path).catch(error => {
+        // Catch any install errors and propogate.
+        this.reject(id, error);
+      });
+    }
+
+    return Promise.all(promises);
+  },
+};
--- a/toolkit/components/search/nsISearchService.idl
+++ b/toolkit/components/search/nsISearchService.idl
@@ -216,18 +216,16 @@ interface nsISearchService : nsISupports
    */
   Promise init();
 
   /**
    * Exposed for testing.
    */
   void reInit([optional] in boolean skipRegionCheck);
   void reset();
-  Promise ensureBuiltinExtension(in AString id,
-                                [optional] in jsval locales);
 
   /**
    * Determine whether initialization has been completed.
    *
    * Clients of the service can use this attribute to quickly determine whether
    * initialization is complete, and decide to trigger some immediate treatment,
    * to launch asynchronous initialization or to bailout.
    *
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/invalid-extension/invalid/manifest.json
@@ -0,0 +1,19 @@
+{
+  "name": "Invalid",
+  "description": "Invalid Engine",
+  "manifest_version": 2,
+  "version": "1.0",
+  "applications": {
+    "gecko": {
+      "id": "invalid@search.mozilla.org"
+    }
+  },
+  "hidden": true,
+  "chrome_settings_overrides": {
+    "search_provider": {
+      "name": "Invalid",
+      "search_url": "ssh://duckduckgo.com/",
+      "suggest_url": "ssh://ac.duckduckgo.com/ac/q={searchTerms}&type=list"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/invalid-extension/list.json
@@ -0,0 +1,7 @@
+{
+  "default": {
+    "visibleDefaultEngines": [
+      "invalid"
+    ]
+  }
+}
--- a/toolkit/components/search/tests/xpcshell/head_search.js
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -36,16 +36,20 @@ const CACHE_FILENAME = "search.json.mozl
 
 // nsSearchService.js uses Services.appinfo.name to build a salt for a hash.
 // eslint-disable-next-line mozilla/use-services
 var XULRuntime = Cc["@mozilla.org/xre/runtime;1"].getService(Ci.nsIXULRuntime);
 
 // Expand the amount of information available in error logs
 Services.prefs.setBoolPref("browser.search.log", true);
 
+// Some tests load tons of extensions and will timeout, disable the timeout
+// here to allow tests to be slow.
+Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
 // The geo-specific search tests assume certain prefs are already setup, which
 // might not be true when run in comm-central etc.  So create them here.
 Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
 Services.prefs.setIntPref("browser.search.geoip.timeout", 3000);
 // But still disable geoip lookups - tests that need it will re-configure this.
 Services.prefs.setCharPref("browser.search.geoip.url", "");
 // Also disable region defaults - tests using it will also re-configure it.
 Services.prefs
--- a/toolkit/components/search/tests/xpcshell/test_async_distribution.js
+++ b/toolkit/components/search/tests/xpcshell/test_async_distribution.js
@@ -6,24 +6,26 @@ add_task(async function setup() {
 });
 
 add_task(async function test_async_distribution() {
   configureToLoadJarEngines();
   installDistributionEngine();
 
   Assert.ok(!Services.search.isInitialized);
 
-  return Services.search.init().then(function search_initialized(aStatus) {
-    Assert.ok(Components.isSuccessCode(aStatus));
-    Assert.ok(Services.search.isInitialized);
+  let aStatus = await Services.search.init();
+  Assert.ok(Components.isSuccessCode(aStatus));
+  Assert.ok(Services.search.isInitialized);
 
-    // test that the engine from the distribution overrides our jar engine
-    return Services.search.getEngines().then(engines => {
-      Assert.equal(engines.length, 1);
+  // test that the engine from the distribution overrides our jar engine
+  let engines = await Services.search.getEngines();
+  Assert.equal(engines.length, 1);
 
-      let engine = Services.search.getEngineByName("bug645970");
-      Assert.notEqual(engine, null);
+  let engine = Services.search.getEngineByName("bug645970");
+  Assert.ok(!!engine, "engine is installed");
 
-      // check the engine we have is actually the one from the distribution
-      Assert.equal(engine.description, "override");
-    });
-  });
+  // check the engine we have is actually the one from the distribution
+  Assert.equal(
+    engine.description,
+    "override",
+    "distribution engine override installed"
+  );
 });
--- a/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
+++ b/toolkit/components/search/tests/xpcshell/test_list_json_searchorder.js
@@ -1,29 +1,33 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* Check default search engine is picked from list.json searchDefault */
 
 "use strict";
 
 add_task(async function setup() {
+  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+  Services.prefs.setCharPref("browser.search.geoip.url", "");
+  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
   await AddonTestUtils.promiseStartupManager();
 });
 
 // Override list.json with test data from data/list.json
 // and check that searchOrder is working
 add_task(async function test_searchOrderJSON() {
   let url = "resource://test/data/";
   let resProt = Services.io
     .getProtocolHandler("resource")
     .QueryInterface(Ci.nsIResProtocolHandler);
   resProt.setSubstitution("search-extensions", Services.io.newURI(url));
 
-  await asyncReInit();
+  await Services.search.init();
 
   Assert.ok(Services.search.isInitialized, "search initialized");
   Assert.equal(
     Services.search.defaultEngine.name,
     kTestEngineName,
     "expected test list JSON default search engine"
   );
 
--- a/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
+++ b/toolkit/components/search/tests/xpcshell/test_migrateWebExtensionEngine.js
@@ -1,48 +1,160 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+const { ExtensionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/ExtensionXPCShellUtils.jsm"
+);
+ExtensionTestUtils.init(this);
+
+AddonTestUtils.usePrivilegedSignatures = false;
+AddonTestUtils.overrideCertDB();
+
 const kSearchEngineID = "addEngineWithDetails_test_engine";
 const kExtensionID = "test@example.com";
 
 const kSearchEngineDetails = {
   template: "http://example.com/?search={searchTerms}",
   description: "Test Description",
   iconURL:
     "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
   suggestURL: "http://example.com/?suggest={searchTerms}",
   alias: "alias_foo",
-  extensionID: kExtensionID,
 };
 
 add_task(async function setup() {
   await AddonTestUtils.promiseStartupManager();
+  await Services.search.init();
 });
 
 add_task(async function test_migrateLegacyEngine() {
-  Assert.ok(!Services.search.isInitialized);
-
   await Services.search.addEngineWithDetails(
     kSearchEngineID,
     kSearchEngineDetails
   );
 
   // Modify the loadpath so it looks like an legacy plugin loadpath
   let engine = Services.search.getEngineByName(kSearchEngineID);
+  Assert.ok(!!engine, "opensearch engine installed");
   engine.wrappedJSObject._loadPath = `jar:[profile]/extensions/${kExtensionID}.xpi!/engine.xml`;
-  engine.wrappedJSObject._extensionID = null;
+  await Services.search.setDefault(engine);
+  Assert.equal(
+    engine.name,
+    Services.search.defaultEngine.name,
+    "set engine to default"
+  );
 
-  // This should replace the existing engine
-  await Services.search.addEngineWithDetails(
-    kSearchEngineID,
-    kSearchEngineDetails
+  // We assume the default engines are installed, so our position will be after the default engine.
+  // This sets up the test to later test the engine position after updates.
+  let allEngines = await Services.search.getEngines();
+  Assert.ok(
+    allEngines.length > 2,
+    "default engines available " + allEngines.length
+  );
+  let origIndex = allEngines.map(e => e.name).indexOf(kSearchEngineID);
+  Assert.ok(
+    origIndex > 1,
+    "opensearch engine installed at position " + origIndex
+  );
+  await Services.search.moveEngine(engine, origIndex - 1);
+  let index = (await Services.search.getEngines())
+    .map(e => e.name)
+    .indexOf(kSearchEngineID);
+  Assert.equal(
+    origIndex - 1,
+    index,
+    "opensearch engine moved to position " + index
   );
 
+  // Replace the opensearch extension with a webextension
+  let extensionInfo = {
+    useAddonManager: "permanent",
+    manifest: {
+      version: "1.0",
+      applications: {
+        gecko: {
+          id: kExtensionID,
+        },
+      },
+      chrome_settings_overrides: {
+        search_provider: {
+          name: kSearchEngineID,
+          search_url: "https://example.com/?q={searchTerms}",
+        },
+      },
+    },
+  };
+
+  let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+  await extension.startup();
+
   engine = Services.search.getEngineByName(kSearchEngineID);
   Assert.equal(
     engine.wrappedJSObject._loadPath,
     "[other]addEngineWithDetails:" + kExtensionID
   );
   Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID);
+  Assert.equal(engine.wrappedJSObject._version, "1.0");
+  index = (await Services.search.getEngines())
+    .map(e => e.name)
+    .indexOf(kSearchEngineID);
+  Assert.equal(origIndex - 1, index, "webext position " + index);
+  Assert.equal(
+    engine.name,
+    Services.search.defaultEngine.name,
+    "engine stil default"
+  );
+
+  extensionInfo.manifest.version = "2.0";
+  await extension.upgrade(extensionInfo);
+  await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+  engine = Services.search.getEngineByName(kSearchEngineID);
+  Assert.equal(
+    engine.wrappedJSObject._loadPath,
+    "[other]addEngineWithDetails:" + kExtensionID
+  );
+  Assert.equal(engine.wrappedJSObject._extensionID, kExtensionID);
+  Assert.equal(engine.wrappedJSObject._version, "2.0");
+  index = (await Services.search.getEngines())
+    .map(e => e.name)
+    .indexOf(kSearchEngineID);
+  Assert.equal(origIndex - 1, index, "webext position " + index);
+  Assert.equal(
+    engine.name,
+    Services.search.defaultEngine.name,
+    "engine stil default"
+  );
+
+  // A different extension cannot use the same name
+  extensionInfo.manifest.applications.gecko.id = "takeover@search.foo";
+  let otherExt = ExtensionTestUtils.loadExtension(extensionInfo);
+  await otherExt.startup();
+  // Verify correct owner
+  engine = Services.search.getEngineByName(kSearchEngineID);
+  Assert.equal(
+    engine.wrappedJSObject._extensionID,
+    kExtensionID,
+    "prior search engine could not be overwritten"
+  );
+  // Verify no engine installed
+  let engines = await Services.search.getEnginesByExtensionID(
+    "takeover@search.foo"
+  );
+  Assert.equal(engines.length, 0, "no search engines installed");
+  await otherExt.unload();
+
+  // An opensearch engine cannot replace a webextension.
+  try {
+    await Services.search.addEngineWithDetails(
+      kSearchEngineID,
+      kSearchEngineDetails
+    );
+    Assert.ok(false, "unable to install opensearch over webextension");
+  } catch (e) {
+    Assert.ok(true, "unable to install opensearch over webextension");
+  }
+
+  await extension.unload();
 });
--- a/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
+++ b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
@@ -13,17 +13,17 @@ add_task(async function setup() {
 });
 
 add_task(async function test_parseSubmissionURL() {
   // Hide the default engines to prevent them from being used in the search.
   for (let engine of await Services.search.getEngines()) {
     await Services.search.removeEngine(engine);
   }
 
-  let [engine1, engine2, engine3, engine4] = await addTestEngines([
+  let engines = await addTestEngines([
     { name: "Test search engine", xmlFileName: "engine.xml" },
     { name: "Test search engine (fr)", xmlFileName: "engine-fr.xml" },
     {
       name: "bacon_addParam",
       details: {
         alias: "bacon_addParam",
         description: "Search Bacon",
         method: "GET",
@@ -47,128 +47,147 @@ add_task(async function test_parseSubmis
         alias: "bacon",
         description: "Search Bacon",
         method: "GET",
         template: "http://www.bacon.moz/search?q={searchTerms}",
       },
     },
   ]);
 
-  engine3.addParam("q", "{searchTerms}", null);
-  engine4.addParam("q", "{searchTerms}", null);
+  engines[2].addParam("q", "{searchTerms}", null);
+  engines[3].addParam("q", "{searchTerms}", null);
+
+  function testParseSubmissionURL(url, engine, terms = "", offsetTerm) {
+    let result = Services.search.parseSubmissionURL(url);
+    Assert.equal(result.engine.name, engine.name, "engine matches");
+    Assert.equal(result.terms, terms, "term matches");
+    if (offsetTerm) {
+      Assert.ok(
+        url.slice(result.termsOffset).startsWith(offsetTerm),
+        "offset term matches"
+      );
+      Assert.equal(
+        result.termsLength,
+        offsetTerm.length,
+        "offset term length matches"
+      );
+    } else {
+      Assert.equal(result.termsOffset, url.length, "no term offset");
+    }
+  }
 
   // Test the first engine, whose URLs use UTF-8 encoding.
-  let url = "http://www.google.com/search?foo=bar&q=caff%C3%A8";
-  let result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine1);
-  Assert.equal(result.terms, "caff\u00E8");
-  Assert.ok(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
-  Assert.equal(result.termsLength, "caff%C3%A8".length);
+  info("URLs use UTF-8 encoding");
+  testParseSubmissionURL(
+    "http://www.google.com/search?foo=bar&q=caff%C3%A8",
+    engines[0],
+    "caff\u00E8",
+    "caff%C3%A8"
+  );
 
   // The second engine uses a locale-specific domain that is an alternate domain
   // of the first one, but the second engine should get priority when matching.
   // The URL used with this engine uses ISO-8859-1 encoding instead.
-  url = "http://www.google.fr/search?q=caff%E8";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine2);
-  Assert.equal(result.terms, "caff\u00E8");
-  Assert.ok(url.slice(result.termsOffset).startsWith("caff%E8"));
-  Assert.equal(result.termsLength, "caff%E8".length);
+  info("URLs use alternate domain and ISO-8859-1 encoding");
+  testParseSubmissionURL(
+    "http://www.google.fr/search?q=caff%E8",
+    engines[1],
+    "caff\u00E8",
+    "caff%E8"
+  );
 
   // Test a domain that is an alternate domain of those defined.  In this case,
   // the first matching engine from the ordered list should be returned.
-  url = "http://www.google.co.uk/search?q=caff%C3%A8";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine1);
-  Assert.equal(result.terms, "caff\u00E8");
-  Assert.ok(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
-  Assert.equal(result.termsLength, "caff%C3%A8".length);
+  info("URLs use alternate domain");
+  testParseSubmissionURL(
+    "http://www.google.co.uk/search?q=caff%C3%A8",
+    engines[0],
+    "caff\u00E8",
+    "caff%C3%A8"
+  );
 
   // We support parsing URLs from a dynamically added engine.  Those engines use
   // windows-1252 encoding by default.
-  url = "http://www.bacon.test/find?q=caff%E8";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine3);
-  Assert.equal(result.terms, "caff\u00E8");
-  Assert.ok(url.slice(result.termsOffset).startsWith("caff%E8"));
-  Assert.equal(result.termsLength, "caff%E8".length);
-
-  // Test URLs with unescaped unicode characters.
-  url = "http://www.google.com/search?q=foo+b\u00E4r";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine1);
-  Assert.equal(result.terms, "foo b\u00E4r");
-  Assert.ok(url.slice(result.termsOffset).startsWith("foo+b\u00E4r"));
-  Assert.equal(result.termsLength, "foo+b\u00E4r".length);
+  info("URLs use windows-1252");
+  testParseSubmissionURL(
+    "http://www.bacon.test/find?q=caff%E8",
+    engines[2],
+    "caff\u00E8",
+    "caff%E8"
+  );
 
-  // Test search engines with unescaped IDNs.
-  url = "http://www.b\u00FCcher.ch/search?q=foo+bar";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine4);
-  Assert.equal(result.terms, "foo bar");
-  Assert.ok(url.slice(result.termsOffset).startsWith("foo+bar"));
-  Assert.equal(result.termsLength, "foo+bar".length);
+  info("URLs with unescaped unicode characters");
+  testParseSubmissionURL(
+    "http://www.google.com/search?q=foo+b\u00E4r",
+    engines[0],
+    "foo b\u00E4r",
+    "foo+b\u00E4r"
+  );
 
-  // Test search engines with escaped IDNs.
-  url = "http://www.xn--bcher-kva.ch/search?q=foo+bar";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine4);
-  Assert.equal(result.terms, "foo bar");
-  Assert.ok(url.slice(result.termsOffset).startsWith("foo+bar"));
-  Assert.equal(result.termsLength, "foo+bar".length);
+  info("URLs with unescaped IDNs");
+  testParseSubmissionURL(
+    "http://www.b\u00FCcher.ch/search?q=foo+bar",
+    engines[3],
+    "foo bar",
+    "foo+bar"
+  );
 
-  // Parsing of parameters from an engine template URL is not supported.
-  Assert.equal(
-    Services.search.parseSubmissionURL("http://www.bacon.moz/search?q=").engine,
-    null
-  );
-  Assert.equal(
-    Services.search.parseSubmissionURL("https://duckduckgo.com?q=test").engine,
-    null
-  );
-  Assert.equal(
-    Services.search.parseSubmissionURL("https://duckduckgo.com/?q=test").engine,
-    null
+  info("URLs with escaped IDNs");
+  testParseSubmissionURL(
+    "http://www.xn--bcher-kva.ch/search?q=foo+bar",
+    engines[3],
+    "foo bar",
+    "foo+bar"
   );
 
-  // HTTP and HTTPS schemes are interchangeable.
-  url = "https://www.google.com/search?q=caff%C3%A8";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine1);
-  Assert.equal(result.terms, "caff\u00E8");
-  Assert.ok(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+  info("URLs with engines using template params, no value");
+  testParseSubmissionURL("http://www.bacon.moz/search?q=", engines[5]);
 
-  // Decoding search terms with multiple spaces should work.
-  result = Services.search.parseSubmissionURL(
-    "http://www.google.com/search?q=+with++spaces+"
+  info("URLs with engines using template params");
+  testParseSubmissionURL(
+    "https://duckduckgo.com?q=test",
+    engines[4],
+    "test",
+    "test"
   );
-  Assert.equal(result.engine, engine1);
-  Assert.equal(result.terms, " with  spaces ");
+
+  info("HTTP and HTTPS schemes are interchangeable.");
+  testParseSubmissionURL(
+    "https://www.google.com/search?q=caff%C3%A8",
+    engines[0],
+    "caff\u00E8",
+    "caff%C3%A8"
+  );
 
-  // An empty query parameter should work the same.
-  url = "http://www.google.com/search?q=";
-  result = Services.search.parseSubmissionURL(url);
-  Assert.equal(result.engine, engine1);
-  Assert.equal(result.terms, "");
-  Assert.equal(result.termsOffset, url.length);
+  info("Decoding search terms with multiple spaces should work.");
+  testParseSubmissionURL(
+    "http://www.google.com/search?q=+with++spaces+",
+    engines[0],
+    " with  spaces ",
+    "+with++spaces+"
+  );
 
-  // There should be no match when the path is different.
-  result = Services.search.parseSubmissionURL(
+  info("An empty query parameter should work the same.");
+  testParseSubmissionURL("http://www.google.com/search?q=", engines[0]);
+
+  // These test slightly different so we don't use testParseSubmissionURL.
+  info("There should be no match when the path is different.");
+  let result = Services.search.parseSubmissionURL(
     "http://www.google.com/search/?q=test"
   );
   Assert.equal(result.engine, null);
   Assert.equal(result.terms, "");
   Assert.equal(result.termsOffset, -1);
 
-  // There should be no match when the argument is different.
+  info("There should be no match when the argument is different.");
   result = Services.search.parseSubmissionURL(
     "http://www.google.com/search?q2=test"
   );
   Assert.equal(result.engine, null);
   Assert.equal(result.terms, "");
   Assert.equal(result.termsOffset, -1);
 
-  // There should be no match for URIs that are not HTTP or HTTPS.
+  info("There should be no match for URIs that are not HTTP or HTTPS.");
   result = Services.search.parseSubmissionURL("file://localhost/search?q=test");
   Assert.equal(result.engine, null);
   Assert.equal(result.terms, "");
   Assert.equal(result.termsOffset, -1);
 });
--- a/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
+++ b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
@@ -30,17 +30,18 @@ add_task(async function run_test() {
   for (let engine of data.engines) {
     if (engine._name == "bug645970") {
       engine.filePath = file.path;
     }
   }
 
   await promiseSaveCacheData(data);
 
-  await asyncReInit();
+  Services.search.reset();
+  await Services.search.init();
 
   // test the engine is loaded ok.
   let engine = Services.search.getEngineByName("bug645970");
   Assert.notEqual(engine, null);
 
   // remove the engine and verify the file has been removed too.
   await Services.search.removeEngine(engine);
   Assert.ok(!file.exists());
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_require_engines_for_cache.js
@@ -0,0 +1,16 @@
+"strict";
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1255605
+add_task(async function skip_writing_cache_without_engines() {
+  Services.prefs.setCharPref("browser.search.region", "US");
+  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+  await AddonTestUtils.promiseStartupManager();
+
+  useTestEngines("no-extensions");
+  Assert.strictEqual(
+    0,
+    (await Services.search.getEngines()).length,
+    "no engines loaded"
+  );
+  Assert.ok(!removeCacheFile(), "empty cache file was not created.");
+});
--- a/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
+++ b/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
@@ -1,72 +1,50 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(async function setup() {
+  Services.prefs.setCharPref("browser.search.region", "US");
+  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
   configureToLoadJarEngines();
   await AddonTestUtils.promiseStartupManager();
 });
 
 add_task(async function ignore_cache_files_without_engines() {
   let commitPromise = promiseAfterCache();
   let engineCount = (await Services.search.getEngines()).length;
-  Assert.equal(engineCount, 1);
+  Assert.equal(engineCount, 1, "one engine installed on search init");
 
   // Wait for the file to be saved to disk, so that we can mess with it.
   await commitPromise;
 
   // Remove all engines from the cache file.
   let cache = await promiseCacheData();
   cache.engines = [];
   await promiseSaveCacheData(cache);
 
   // Check that after an async re-initialization, we still have the same engine count.
   commitPromise = promiseAfterCache();
   await asyncReInit();
-  Assert.equal(engineCount, (await Services.search.getEngines()).length);
+  Assert.equal(
+    engineCount,
+    (await Services.search.getEngines()).length,
+    "Search got correct number of engines"
+  );
   await commitPromise;
 
   // Check that after a sync re-initialization, we still have the same engine count.
   await promiseSaveCacheData(cache);
   let unInitPromise = SearchTestUtils.promiseSearchNotification(
     "uninit-complete"
   );
   let reInitPromise = asyncReInit();
   await unInitPromise;
-  Assert.ok(!Services.search.isInitialized);
+  Assert.ok(!Services.search.isInitialized, "Search is not initialized");
   // Synchronously check the engine count; will force a sync init.
-  Assert.equal(engineCount, (await Services.search.getEngines()).length);
-  Assert.ok(Services.search.isInitialized);
+  Assert.equal(
+    engineCount,
+    (await Services.search.getEngines()).length,
+    "Search got correct number of engines"
+  );
+  Assert.ok(Services.search.isInitialized, "Search is initialized");
   await reInitPromise;
 });
-
-add_task(async function skip_writing_cache_without_engines() {
-  let unInitPromise = SearchTestUtils.promiseSearchNotification(
-    "uninit-complete"
-  );
-  let reInitPromise = asyncReInit();
-  await unInitPromise;
-
-  // Configure so that no engines will be found.
-  Assert.ok(removeCacheFile());
-  let resProt = Services.io
-    .getProtocolHandler("resource")
-    .QueryInterface(Ci.nsIResProtocolHandler);
-  resProt.setSubstitution(
-    "search-extensions",
-    Services.io.newURI("about:blank")
-  );
-
-  // Let the async-reInit happen.
-  await reInitPromise;
-  Assert.strictEqual(0, (await Services.search.getEngines()).length);
-
-  // Trigger yet another re-init, to flush of any pending cache writing task.
-  unInitPromise = SearchTestUtils.promiseSearchNotification("uninit-complete");
-  reInitPromise = asyncReInit();
-  await unInitPromise;
-
-  // Now check that a cache file doesn't exist.
-  Assert.ok(!removeCacheFile());
-
-  await reInitPromise;
-});
--- a/toolkit/components/search/tests/xpcshell/test_validate_engines.js
+++ b/toolkit/components/search/tests/xpcshell/test_validate_engines.js
@@ -4,44 +4,76 @@
 // Ensure all the engines defined in list.json are valid by
 // creating a new list.json that contains every engine and
 // loading them all.
 
 "use strict";
 
 Cu.importGlobalProperties(["fetch"]);
 
-const { SearchService } = ChromeUtils.import(
-  "resource://gre/modules/SearchService.jsm"
+const { SearchUtils, SearchExtensionLoader } = ChromeUtils.import(
+  "resource://gre/modules/SearchUtils.jsm"
 );
-const LIST_JSON_URL = "resource://search-extensions/list.json";
 
 function traverse(obj, fun) {
   for (var i in obj) {
     fun.apply(this, [i, obj[i]]);
     if (obj[i] !== null && typeof obj[i] == "object") {
       traverse(obj[i], fun);
     }
   }
 }
 
-const ss = new SearchService();
-
-add_task(async function test_validate_engines() {
-  let engines = await fetch(LIST_JSON_URL).then(req => req.json());
+add_task(async function setup() {
+  // Read all the builtin engines and locales, create a giant list.json
+  // that includes everything.
+  let engines = await fetch(SearchUtils.LIST_JSON_URL).then(req => req.json());
 
   let visibleDefaultEngines = new Set();
   traverse(engines, (key, val) => {
     if (key === "visibleDefaultEngines") {
       val.forEach(engine => visibleDefaultEngines.add(engine));
     }
   });
 
   let listjson = {
     default: {
       visibleDefaultEngines: Array.from(visibleDefaultEngines),
     },
   };
-  ss._listJSONURL = "data:application/json," + JSON.stringify(listjson);
+  SearchUtils.LIST_JSON_URL =
+    "data:application/json," + JSON.stringify(listjson);
 
+  // Set strict so the addon install promise is rejected.  This causes
+  // search.init to throw the error, and this test fails.
+  SearchExtensionLoader._strict = true;
   await AddonTestUtils.promiseStartupManager();
-  await ss.init();
+});
+
+add_task(async function test_validate_engines() {
+  // All engines should parse and init should work fine.
+  await Services.search
+    .init()
+    .then(() => {
+      ok(true, "all engines parsed and loaded");
+    })
+    .catch(() => {
+      ok(false, "an engine failed to parse and load");
+    });
 });
+
+add_task(async function test_install_timeout_failure() {
+  // Set an incredibly unachievable timeout here and make sure
+  // that init throws.  We're loading every engine/locale combo under the
+  // sun, it's unlikely we could intermittently succeed in loading
+  // them all.
+  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 1);
+  removeCacheFile();
+  Services.search.reset();
+  await Services.search
+    .init()
+    .then(() => {
+      ok(false, "search init did not time out");
+    })
+    .catch(error => {
+      equal(Cr.NS_ERROR_FAILURE, error, "search init timed out");
+    });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_webextensions_install_failure.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { SearchExtensionLoader } = ChromeUtils.import(
+  "resource://gre/modules/SearchUtils.jsm"
+);
+const { ExtensionTestUtils } = ChromeUtils.import(
+  "resource://testing-common/ExtensionXPCShellUtils.jsm"
+);
+
+ExtensionTestUtils.init(this);
+AddonTestUtils.usePrivilegedSignatures = false;
+AddonTestUtils.overrideCertDB();
+
+add_task(async function test_install_manifest_failure() {
+  // Force addon loading to reject on errors
+  SearchExtensionLoader._strict = true;
+  useTestEngines("invalid-extension");
+  await AddonTestUtils.promiseStartupManager();
+
+  await Services.search
+    .init()
+    .then(() => {
+      ok(false, "search init did not throw");
+    })
+    .catch(e => {
+      equal(Cr.NS_ERROR_FAILURE, e, "search init error");
+    });
+});
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -43,16 +43,18 @@ support-files =
   data/test-extensions/plainengine/favicon.ico
   data/test-extensions/plainengine/manifest.json
   data/test-extensions/special-engine/favicon.ico
   data/test-extensions/special-engine/manifest.json
   data/test-extensions/multilocale/favicon.ico
   data/test-extensions/multilocale/manifest.json
   data/test-extensions/multilocale/_locales/af/messages.json
   data/test-extensions/multilocale/_locales/an/messages.json
+  data/invalid-extension/list.json
+  data/invalid-extension/invalid/manifest.json
 tags=searchmain
 
 [test_nocache.js]
 [test_big_icon.js]
 [test_bug930456.js]
 [test_bug930456_child.js]
 skip-if = true # Is confusing
 [test_engine_set_alias.js]
@@ -94,23 +96,25 @@ tags = addons
 [test_async_disthidden.js]
 [test_rel_searchform.js]
 [test_reloadEngines.js]
 [test_remove_profile_engine.js]
 [test_selectedEngine.js]
 [test_geodefaults.js]
 [test_hidden.js]
 [test_currentEngine_fallback.js]
+[test_require_engines_for_cache.js]
 [test_require_engines_in_cache.js]
 skip-if = (verify && !debug && (os == 'linux'))
 [test_svg_icon.js]
 [test_addEngineWithDetails.js]
 [test_addEngineWithDetailsObject.js]
 [test_addEngineWithExtensionID.js]
 [test_chromeresource_icon2.js]
 [test_engineUpdate.js]
 [test_paramSubstitution.js]
 [test_migrateWebExtensionEngine.js]
 [test_sendSubmissionURL.js]
 [test_validate_engines.js]
 [test_validate_manifests.js]
 [test_webextensions_install.js]
+[test_webextensions_install_failure.js]
 [test_purpose.js]
--- a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
@@ -20,20 +20,24 @@ class TelemetryTestRunner(BaseMarionette
         # Select the appropriate GeckoInstance
         kwargs["app"] = "fxdesktop"
 
         prefs = kwargs.pop("prefs", {})
 
         # Set Firefox Client Telemetry specific preferences
         prefs.update(
             {
-                # Fake the geoip lookup to always return Germany to:
-                #   * avoid net access in tests
-                #   * stabilize browser.search.region to avoid an extra subsession (bug 1545207)
-                "browser.search.geoip.url": "data:application/json,{\"country_code\": \"DE\"}",
+                # Force search region to DE and disable geo lookups.
+                "browser.search.region": "DE",
+                "browser.search.geoSpecificDefaults": False,
+                # Turn off timeouts for loading search extensions
+                "browser.search.addonLoadTimeout": 0,
+                "browser.search.log": True,
+                # geoip is skipped if url is empty (bug 1545207)
+                "browser.search.geoip.url": "",
                 # Disable smart sizing because it changes prefs at startup. (bug 1547750)
                 "browser.cache.disk.smart_size.enabled": False,
                 "toolkit.telemetry.server": "{}/pings".format(SERVER_URL),
                 "toolkit.telemetry.initDelay": 1,
                 "toolkit.telemetry.minSubsessionLength": 0,
                 "datareporting.healthreport.uploadEnabled": True,
                 "datareporting.policy.dataSubmissionEnabled": True,
                 "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -479,16 +479,21 @@ function setEmptyPrefWatchlist() {
     "resource://gre/modules/TelemetryEnvironment.jsm"
   );
   return TelemetryEnvironment.onInitialized().then(() =>
     TelemetryEnvironment.testWatchPreferences(new Map())
   );
 }
 
 if (runningInParent) {
+  // Turn off region updates and timeouts for search service
+  Services.prefs.setCharPref("browser.search.region", "US");
+  Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+  Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+
   // Set logging preferences for all the tests.
   Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
   // Telemetry archiving should be on.
   Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, true);
   // Telemetry xpcshell tests cannot show the infobar.
   Services.prefs.setBoolPref(
     TelemetryUtils.Preferences.BypassNotification,
     true