Bug 1541210 - Write unit tests to ensure that the correct default engines are present for all locales/regions. r=daleharvey
authorMark Banner <standard8@mozilla.com>
Tue, 23 Apr 2019 20:22:01 +0000
changeset 470556 5a728b586c417ce346840297353cb86a7d38629c
parent 470555 cffeafe28a4565fdd07582546190591c2279dcf0
child 470557 fd2bf318a8b29e7c1ab67b985c19c2c218a76d6e
push id35908
push useraciure@mozilla.com
push dateWed, 24 Apr 2019 04:28:40 +0000
treeherdermozilla-central@c9f0730a57a6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdaleharvey
bugs1541210
milestone68.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 1541210 - Write unit tests to ensure that the correct default engines are present for all locales/regions. r=daleharvey Differential Revision: https://phabricator.services.mozilla.com/D27879
toolkit/components/search/moz.build
toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js
toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js
toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js
toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js
toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js
toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.ini
--- a/toolkit/components/search/moz.build
+++ b/toolkit/components/search/moz.build
@@ -1,15 +1,18 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+XPCSHELL_TESTS_MANIFESTS += [
+    'tests/xpcshell/searchconfigs/xpcshell.ini',
+    'tests/xpcshell/xpcshell.ini',
+]
 
 if CONFIG['MOZ_TOOLKIT_SEARCH']:
     XPIDL_SOURCES += [
         'nsISearchService.idl',
     ]
 
 XPIDL_MODULE = 'toolkit_search'
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/head_searchconfig.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
+  OS: "resource://gre/modules/osfile.jsm",
+  SearchTestUtils: "resource://testing-common/SearchTestUtils.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
+
+const GLOBAL_SCOPE = this;
+
+/**
+ * This class implements the test harness for search configuration tests.
+ * These tests are designed to ensure that the correct search engines are
+ * loaded for the various region/locale configurations.
+ *
+ * The configuration for each test is represented by an object having the
+ * following properties:
+ *
+ * - identifier (string)
+ *   The identifier for the search engine under test.
+ * - default (object)
+ *   An inclusion/exclusion configuration (see below) to detail when this engine
+ *   should be listed as default.
+ *
+ * The inclusion/exclusion configuration is represented as an object having the
+ * following properties:
+ *
+ * - included (array)
+ *   An optional array of region/locale pairs.
+ * - excluded (array)
+ *   An optional array of region/locale pairs.
+ *
+ * If the object is empty, the engine is assumed not to be part of any locale/region
+ * pair.
+ * If the object has `excluded` but not `included`, then the engine is assumed to
+ * be part of every locale/region pair except for where it matches the exclusions.
+ *
+ * The region/locale pairs are represented as an object having the following
+ * properties:
+ *
+ * - region (array)
+ *   An array of two-letter region codes.
+ * - locale (object)
+ *   A locale object which may consist of:
+ *   - matches (array)
+ *     An array of locale strings which should exactly match the locale.
+ *   - startsWith (array)
+ *     An array of locale strings which the locale should start with.
+ */
+class SearchConfigTest {
+  /**
+   * @param {object} config
+   *   The initial configuration for this test, see above.
+   */
+  constructor(config = {}) {
+    this._config = config;
+
+    // This is intended for development-only. Setting it to true restricts the
+    // set of locales and regions that are covered, to provide tests that are
+    // quicker to run.
+    // Turning it on will generate one error at the end of the test, as a reminder
+    // that it needs to be changed back before shipping.
+    this._testDebug = false;
+  }
+
+  /**
+   * Sets up the test.
+   */
+  async setup() {
+    AddonTestUtils.init(GLOBAL_SCOPE);
+    AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42");
+
+    // Disable region checks.
+    Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+    Services.prefs.setCharPref("browser.search.geoip.url", "");
+
+    await AddonTestUtils.promiseStartupManager();
+    await Services.search.init();
+
+    Assert.ok(Services.search.isInitialized,
+      "Should have correctly initialized the search service");
+  }
+
+  /**
+   * Runs the test.
+   */
+  async run() {
+    const locales = await this._getLocales();
+    const regions = this._regions;
+
+    // We loop on region and then locale, so that we always cause a re-init
+    // when updating the requested/available locales.
+    for (let region of regions) {
+      for (let locale of locales) {
+        const infoString = `region: "${region}" locale: "${locale}"`;
+        info(`Checking ${infoString}`);
+        await this._reinit(region, locale);
+
+        this._assertDefaultEngines(region, locale, infoString);
+      }
+    }
+
+    Assert.ok(!this._testDebug, "Should not have test debug turned on in production");
+  }
+
+  /**
+   * Causes re-initialization of the SearchService with the new region and locale.
+   *
+   * @param {string} region
+   *   The two-letter region code.
+   * @param {string} locale
+   *   The two-letter locale code.
+   */
+  async _reinit(region, locale) {
+    Services.prefs.setStringPref("browser.search.region", region.toUpperCase());
+    const reinitCompletePromise =
+      SearchTestUtils.promiseSearchNotification("reinit-complete");
+    Services.locale.availableLocales = [locale];
+    Services.locale.requestedLocales = [locale];
+    Services.search.reInit();
+    await reinitCompletePromise;
+
+    Assert.ok(Services.search.isInitialized,
+      "Should have completely re-initialization, if it fails check logs for if reinit was successful");
+  }
+
+  /**
+   * @returns {Set} the list of regions for the tests to run with.
+   */
+  get _regions() {
+    if (this._testDebug) {
+      return new Set(["by", "cn", "kz", "us", "ru", "tr"]);
+    }
+    return Services.intl.getAvailableLocaleDisplayNames("region");
+  }
+
+  /**
+   * @returns {array} the list of locales for the tests to run with.
+   */
+  async _getLocales() {
+    if (this._testDebug) {
+      return ["be", "en-US", "kk", "tr", "ru", "zh-CN"];
+    }
+    const data = await OS.File.read(do_get_file("all-locales").path, {encoding: "utf-8"});
+    return data.split("\n").filter(e => e != "");
+  }
+
+  /**
+   * Determines if a locale matches with a locales section in the configuration.
+   *
+   * @param {object} locales
+   * @param {array} [locales.matches]
+   *   Array of locale names to match exactly.
+   * @param {array} [locales.startsWith]
+   *   Array of locale names to match the start.
+   * @param {string} locale
+   *   The two-letter locale code.
+   * @returns {boolean}
+   *   True if the locale matches.
+   */
+  _localeIncludes(locales, locale) {
+    if ("matches" in locales &&
+        locales.matches.includes(locale)) {
+      return true;
+    }
+    if ("startsWith" in locales) {
+      return locales.startsWith.find(element => element.startsWith(locale));
+    }
+
+    return false;
+  }
+
+  /**
+   * Determines if a locale/region pair match a section of the configuration.
+   *
+   * @param {object} section
+   *   The configuration section to match against.
+   * @param {string} region
+   *   The two-letter region code.
+   * @param {string} locale
+   *   The two-letter locale code.
+   * @returns {boolean}
+   *   True if the locale/region pair matches the section.
+   */
+  _localeRegionInSection(section, region, locale) {
+    for (const {regions, locales} of section) {
+      if (regions.includes(region) &&
+          this._localeIncludes(locales, locale)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Asserts whether the engine is correctly set as default or not.
+   *
+   * @param {string} region
+   *   The two-letter region code.
+   * @param {string} locale
+   *   The two-letter locale code.
+   * @param {string} infoString
+   *   An additional message to output with assertions.
+   */
+  _assertDefaultEngines(region, locale, infoString) {
+    // We use the originalDefaultEngine here as due to the way we run the tests
+    // the default engine may not have changed.
+    const identifier = Services.search.originalDefaultEngine.identifier;
+
+    // If there's not included/excluded, then this shouldn't be the default anywhere.
+    if (!("included" in this._config.default) &&
+        !("excluded" in this._config.default)) {
+      Assert.notEqual(identifier,
+        this._config.identifier,
+        `Should not be set as the default engine for any locale/region,
+         currently set for ${infoString}`);
+      return;
+    }
+
+    // If there's no included section, we assume the engine is default everywhere
+    // and we should apply the exclusions instead.
+    if (("included" in this._config.default &&
+        this._localeRegionInSection(this._config.default.included, region, locale)) ||
+        ("excluded" in this._config.default &&
+         !this._localeRegionInSection(this._config.default.excluded, region, locale))) {
+      Assert.equal(identifier,
+        this._config.identifier,
+        `Should be set as the default engine for ${infoString}`);
+      return;
+    }
+    Assert.notEqual(identifier,
+      this._config.identifier,
+      `Should not be set as the default engine for ${infoString}`);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_baidu.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+  identifier: "baidu",
+  default: {
+    included: [{
+      regions: [
+        "cn",
+      ],
+      locales: {
+        matches: ["zh-CN"],
+      },
+    }],
+  },
+});
+
+add_task(async function setup() {
+  await test.setup();
+});
+
+add_task(async function test_searchConfig_baidu() {
+  await test.run();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_bing.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+  identifier: "bing",
+  default: {
+    // Not included anywhere.
+  },
+});
+
+add_task(async function setup() {
+  await test.setup();
+});
+
+add_task(async function test_searchConfig_bing() {
+  await test.run();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_google.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+  identifier: "google",
+  default: {
+    // Included everywhere apart from the exclusions below. These are basically
+    // just excluding what Yandex and Baidu include.
+    excluded: [{
+      regions: [
+        "ru", "tr", "by", "kz",
+      ],
+      locales: {
+        matches: ["ru", "tr", "be", "kk"],
+        startsWith: ["en"],
+      },
+    }, {
+      regions: ["cn"],
+      locales: {
+        matches: ["zh-CN"],
+      },
+    }],
+  },
+});
+
+add_task(async function setup() {
+  await test.setup();
+});
+
+add_task(async function test_searchConfig_google() {
+  await test.run();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/test_yandex.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const test = new SearchConfigTest({
+  identifier: "yandex",
+  default: {
+    included: [{
+      regions: [
+        "ru", "tr", "by", "kz",
+      ],
+      locales: {
+        matches: ["ru", "tr", "be", "kk"],
+        startsWith: ["en"],
+      },
+    }],
+  },
+});
+
+add_task(async function setup() {
+  await test.setup();
+});
+
+add_task(async function test_searchConfig_yandex() {
+  await test.run();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/searchconfigs/xpcshell.ini
@@ -0,0 +1,27 @@
+[DEFAULT]
+firefox-appdir = browser
+head = head_searchconfig.js
+skip-if = toolkit == 'android'
+support-files =
+  ../../../../../../browser/locales/all-locales
+
+[test_baidu.js]
+# This is an extensive test and currently takes a long time. Therefore skip on
+# debug/asan and extend the timeout length otherwise.
+skip-if = debug || asan
+requesttimeoutfactor = 2
+[test_bing.js]
+# This is an extensive test and currently takes a long time. Therefore skip on
+# debug/asan and extend the timeout length otherwise.
+skip-if = debug || asan
+requesttimeoutfactor = 2
+[test_google.js]
+# This is an extensive test and currently takes a long time. Therefore skip on
+# debug/asan and extend the timeout length otherwise.
+skip-if = debug || asan
+requesttimeoutfactor = 2
+[test_yandex.js]
+# This is an extensive test and currently takes a long time. Therefore skip on
+# debug/asan and extend the timeout length otherwise.
+skip-if = debug || asan
+requesttimeoutfactor = 2