Bug 1451159 - [mochitest] Load profile data from testing/profiles/common r=gbrown
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 19 Apr 2018 16:12:49 -0400
changeset 473066 0ba46c050bb2377f409554806cb07e70c28803d5
parent 473065 5f1efa279b095f6ab27ed903e8384615538aac90
child 473067 ef1db4e8bf066e5fcf80a0cd85835fada3a1dbbd
child 473077 8f88c817b3340c1857632da600179295ab4b01c4
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgbrown
bugs1451159
milestone61.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 1451159 - [mochitest] Load profile data from testing/profiles/common r=gbrown This makes mochitest use mozprofile's new 'merge' feature instead of explicitly loading the user.js preference file. This means that any extensions that get dropped into testing/profiles/common/extensions, will automatically run as part of the mochitest run. This can be useful for testing how extensions impact tests locally or on try. In the near future, all our other test harnesses will also start using this profile directory. MozReview-Commit-ID: 34aSqdnkHqx
testing/mochitest/runjunit.py
testing/mochitest/runtests.py
testing/mochitest/tests/python/python.ini
testing/mochitest/tests/python/test_build_profile.py
--- a/testing/mochitest/runjunit.py
+++ b/testing/mochitest/runjunit.py
@@ -11,20 +11,19 @@ import sys
 import tempfile
 import traceback
 
 import mozcrash
 import mozinfo
 import mozlog
 import moznetwork
 from mozdevice import ADBAndroid
-from mozprofile import Profile, Preferences, DEFAULT_PORTS
+from mozprofile import Profile, DEFAULT_PORTS
 from mozprofile.permissions import ServerLocations
 from runtests import MochitestDesktop, update_mozinfo
-from six import string_types
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 try:
     from mozbuild.base import (
         MozbuildObject,
         MachCommandConditions as conditions,
     )
@@ -94,39 +93,22 @@ class JUnitTestRunner(MochitestDesktop):
             self.options.certPath = os.path.join(build_obj.topsrcdir,
                                                  'build', 'pgo', 'certs')
 
     def build_profile(self):
         """
            Create a local profile with test prefs and proxy definitions and
            push it to the remote device.
         """
-        preferences = [os.path.join(here, 'profile_data', 'common', 'user.js')]
-        prefs = {}
-        for path in preferences:
-            prefs.update(Preferences.read_prefs(path))
-
-        interpolation = {
-            "server": "%s:%s" %
-            (self.options.webServer, self.options.httpPort)}
 
-        for k, v in prefs.items():
-            if isinstance(v, string_types):
-                v = v.format(**interpolation)
-            prefs[k] = Preferences.cast(v)
+        self.profile = Profile(locations=self.locations, proxy=self.proxy(self.options))
+        self.options.profilePath = self.profile.profile
 
-        proxy = {'remote': self.options.webServer,
-                 'http': self.options.httpPort,
-                 'https': self.options.sslPort,
-                 'ws': self.options.sslPort
-                 }
-
-        self.profile = Profile(locations=self.locations, preferences=prefs,
-                               proxy=proxy)
-        self.options.profilePath = self.profile.profile
+        # Set preferences
+        self.merge_base_profiles(self.options)
 
         if self.fillCertificateDB(self.options):
             self.log.error("Certificate integration failed")
 
         self.device.mkdir(self.remote_profile, parents=True)
         self.device.push(self.profile.profile, self.remote_profile)
         self.log.debug("profile %s -> %s" %
                        (str(self.profile.profile), str(self.remote_profile)))
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -47,17 +47,16 @@ from manifestparser import TestManifest
 from manifestparser.filters import (
     chunk_by_dir,
     chunk_by_runtime,
     chunk_by_slice,
     pathprefix,
     subsuite,
     tags,
 )
-from six import string_types
 
 try:
     from marionette_driver.addons import Addons
     from marionette_harness import Marionette
 except ImportError as e:
     # Defer ImportError until attempt to use Marionette
     def reraise(*args, **kwargs):
         raise(e)
@@ -847,16 +846,17 @@ class MochitestDesktop(object):
     CHROME_PATH = "redirect.html"
 
     certdbNew = False
     sslTunnel = None
     DEFAULT_TIMEOUT = 60.0
     mediaDevices = None
 
     patternFiles = {}
+    base_profiles = ('common',)
 
     # XXX use automation.py for test name to avoid breaking legacy
     # TODO: replace this with 'runtests.py' or 'mochitest' or the like
     test_name = 'automation.py'
 
     def __init__(self, flavor, logger_options, quiet=False):
         update_mozinfo()
         self.flavor = flavor
@@ -905,25 +905,30 @@ class MochitestDesktop(object):
         self.result = {}
 
         self.start_script = os.path.join(here, 'start_desktop.js')
 
     def environment(self, **kwargs):
         kwargs['log'] = self.log
         return test_environment(**kwargs)
 
-    def extraPrefs(self, extraPrefs):
-        """interpolate extra preferences from option strings"""
+    def extraPrefs(self, prefs):
+        """Interpolate extra preferences from option strings"""
 
         try:
-            return dict(parseKeyValue(extraPrefs, context='--setpref='))
+            prefs = dict(parseKeyValue(prefs, context='--setpref='))
         except KeyValueParseError as e:
             print(str(e))
             sys.exit(1)
 
+        for pref, value in prefs.items():
+            value = Preferences.cast(value)
+            prefs[pref] = value
+        return prefs
+
     def getFullPath(self, path):
         " Get an absolute path relative to self.oldcwd."
         return os.path.normpath(
             os.path.join(
                 self.oldcwd,
                 os.path.expanduser(path)))
 
     def getLogFilePath(self, logFile):
@@ -1815,128 +1820,128 @@ toolbar#nav-bar {
             elif ext == ".client":
                 call([pk12util, "-i", os.path.join(options.certPath, item),
                       "-w", pwfilePath, "-d", certdbPath],
                      env=toolsEnv)
 
         os.unlink(pwfilePath)
         return 0
 
+    def proxy(self, options):
+        # proxy
+        # use SSL port for legacy compatibility; see
+        # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
+        # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
+        # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
+        #             'ws': str(self.webSocketPort)
+        return {
+            'remote': options.webServer,
+            'http': options.httpPort,
+            'https': options.sslPort,
+            'ws': options.sslPort,
+        }
+
+    def merge_base_profiles(self, options):
+        """Merge extra profile data from testing/profiles."""
+        profile_data_dir = os.path.join(SCRIPT_DIR, 'profile_data')
+
+        # If possible, read profile data from topsrcdir. This prevents us from
+        # requiring a re-build to pick up newly added extensions in the
+        # <profile>/extensions directory.
+        if build_obj:
+            path = os.path.join(build_obj.topsrcdir, 'testing', 'profiles')
+            if os.path.isdir(path):
+                profile_data_dir = path
+
+        # values to use when interpolating preferences
+        interpolation = {
+            "server": "%s:%s" % (options.webServer, options.httpPort),
+        }
+
+        for profile in self.base_profiles:
+            path = os.path.join(profile_data_dir, profile)
+            self.profile.merge(path, interpolation=interpolation)
+
     def buildProfile(self, options):
         """ create the profile and add optional chrome bits and files if requested """
-        if options.flavor == 'browser' and options.timeout:
-            options.extraPrefs.append(
-                "testing.browserTestHarness.timeout=%d" %
-                options.timeout)
-        # browser-chrome tests use a fairly short default timeout of 45 seconds;
-        # this is sometimes too short on asan and debug, where we expect reduced
-        # performance.
-        if (mozinfo.info["asan"] or mozinfo.info["debug"]) and \
-                options.flavor == 'browser' and options.timeout is None:
-            self.log.info("Increasing default timeout to 90 seconds")
-            options.extraPrefs.append("testing.browserTestHarness.timeout=90")
-
-        options.extraPrefs.append(
-            "browser.tabs.remote.autostart=%s" %
-            ('true' if options.e10s else 'false'))
-
-        options.extraPrefs.append(
-            "dom.ipc.tabs.nested.enabled=%s" %
-            ('true' if options.nested_oop else 'false'))
-
-        options.extraPrefs.append(
-            "idle.lastDailyNotification=%d" %
-            int(time.time()))
-
-        # Enable tracing output for detailed failures in case of
-        # failing connection attempts, and hangs (bug 1397201)
-        options.extraPrefs.append("marionette.log.level=%s" % "TRACE")
-
-        if getattr(self, 'testRootAbs', None):
-            options.extraPrefs.append(
-                "mochitest.testRoot=%s" %
-                self.testRootAbs)
-
         # get extensions to install
         extensions = self.getExtensionsToInstall(options)
 
-        # preferences
-        preferences = [os.path.join(SCRIPT_DIR, 'profile_data', 'common', 'user.js')]
-        prefs = {}
-        for path in preferences:
-            prefs.update(Preferences.read_prefs(path))
-
-        prefs.update(self.extraPrefs(options.extraPrefs))
-
-        # Bug 1262954: For windows XP + e10s disable acceleration
-        if platform.system() in ("Windows", "Microsoft") and \
-           '5.1' in platform.version() and options.e10s:
-            prefs['layers.acceleration.disabled'] = True
-
         # Whitelist the _tests directory (../..) so that TESTING_JS_MODULES work
         tests_dir = os.path.dirname(os.path.dirname(SCRIPT_DIR))
         sandbox_whitelist_paths = [tests_dir] + options.sandboxReadWhitelist
         if (platform.system() == "Linux" or
             platform.system() in ("Windows", "Microsoft")):
             # Trailing slashes are needed to indicate directories on Linux and Windows
             sandbox_whitelist_paths = map(lambda p: os.path.join(p, ""),
                                           sandbox_whitelist_paths)
 
-        # interpolate preferences
-        interpolation = {
-            "server": "%s:%s" %
-            (options.webServer, options.httpPort)}
-
-        for pref in prefs:
-            if isinstance(prefs[pref], string_types):
-                prefs[pref] = prefs[pref].format(**interpolation)
-            prefs[pref] = Preferences.cast(prefs[pref])
-        # TODO: make this less hacky
-        # https://bugzilla.mozilla.org/show_bug.cgi?id=913152
-
-        # proxy
-        # use SSL port for legacy compatibility; see
-        # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
-        # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
-        # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
-        #             'ws': str(self.webSocketPort)
-        proxy = {'remote': options.webServer,
-                 'http': options.httpPort,
-                 'https': options.sslPort,
-                 'ws': options.sslPort
-                 }
-
-        # See if we should use fake media devices.
-        if options.useTestMediaDevices:
-            prefs['media.audio_loopback_dev'] = self.mediaDevices['audio']
-            prefs['media.video_loopback_dev'] = self.mediaDevices['video']
-
-        # create a profile
+        # Create the profile
         self.profile = Profile(profile=options.profilePath,
                                addons=extensions,
                                locations=self.locations,
-                               preferences=prefs,
-                               proxy=proxy,
-                               whitelistpaths=sandbox_whitelist_paths
+                               proxy=self.proxy(options),
+                               whitelistpaths=sandbox_whitelist_paths,
                                )
 
         # Fix options.profilePath for legacy consumers.
         options.profilePath = self.profile.profile
 
         manifest = self.addChromeToProfile(options)
         self.copyExtraFilesToProfile(options)
 
         # create certificate database for the profile
         # TODO: this should really be upstreamed somewhere, maybe mozprofile
         certificateStatus = self.fillCertificateDB(options)
         if certificateStatus:
             self.log.error(
                 "TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed")
             return None
 
+        # Set preferences in the following order (latter overrides former):
+        # 1) Preferences from base profile (e.g from testing/profiles)
+        # 2) Prefs hardcoded in this function
+        # 3) Prefs from --setpref
+
+        # Prefs from base profiles
+        self.merge_base_profiles(options)
+
+        # Hardcoded prefs (TODO move these into a base profile)
+        prefs = {
+            "browser.tabs.remote.autostart": options.e10s,
+            "dom.ipc.tabs.nested.enabled": options.nested_oop,
+            "idle.lastDailyNotification": int(time.time()),
+            # Enable tracing output for detailed failures in case of
+            # failing connection attempts, and hangs (bug 1397201)
+            "marionette.log.level": "TRACE",
+        }
+
+        if options.flavor == 'browser' and options.timeout:
+            prefs["testing.browserTestHarness.timeout"] = options.timeout
+
+        # browser-chrome tests use a fairly short default timeout of 45 seconds;
+        # this is sometimes too short on asan and debug, where we expect reduced
+        # performance.
+        if (mozinfo.info["asan"] or mozinfo.info["debug"]) and \
+                options.flavor == 'browser' and options.timeout is None:
+            self.log.info("Increasing default timeout to 90 seconds")
+            prefs["testing.browserTestHarness.timeout"] = 90
+
+        if getattr(self, 'testRootAbs', None):
+            prefs['mochitest.testRoot'] = self.testRootAbs
+
+        # See if we should use fake media devices.
+        if options.useTestMediaDevices:
+            prefs['media.audio_loopback_dev'] = self.mediaDevices['audio']
+            prefs['media.video_loopback_dev'] = self.mediaDevices['video']
+
+        self.profile.set_preferences(prefs)
+
+        # Extra prefs from --setpref
+        self.profile.set_preferences(self.extraPrefs(options.extraPrefs))
         return manifest
 
     def getGMPPluginPath(self, options):
         if options.gmp_path:
             return options.gmp_path
 
         gmp_parentdirs = [
             # For local builds, GMP plugins will be under dist/bin.
--- a/testing/mochitest/tests/python/python.ini
+++ b/testing/mochitest/tests/python/python.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 subsuite = mochitest
-sequential = true
 
 [test_basic_mochitest_plain.py]
+sequential = true
 [test_get_active_tests.py]
+[test_build_profile.py]
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/tests/python/test_build_profile.py
@@ -0,0 +1,77 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import print_function, unicode_literals
+
+import os
+from argparse import Namespace
+
+from mozprofile.prefs import Preferences
+from mozprofile import Profile
+from six import string_types
+
+import mozunit
+import pytest
+from conftest import setup_args
+
+
+@pytest.fixture
+def build_profile(monkeypatch, setup_test_harness, parser):
+    setup_test_harness(*setup_args)
+    runtests = pytest.importorskip('runtests')
+    md = runtests.MochitestDesktop('plain', {'log_tbpl': '-'})
+    monkeypatch.setattr(md, 'fillCertificateDB', lambda *args, **kwargs: None)
+
+    options = parser.parse_args([])
+    options = vars(options)
+
+    def inner(**kwargs):
+        opts = options.copy()
+        opts.update(kwargs)
+
+        return md, md.buildProfile(Namespace(**opts))
+
+    return inner
+
+
+@pytest.fixture
+def profile_data_dir(build_obj):
+    return os.path.join(build_obj.topsrcdir, 'testing', 'profiles')
+
+
+def test_common_prefs_are_all_set(build_profile, profile_data_dir):
+    # We set e10s=False here because MochitestDesktop.buildProfile overwrites
+    # the value defined in the base profile.
+    # TODO stop setting browser.tabs.remote.autostart in the base profile
+    md, result = build_profile(e10s=False)
+
+    # build the expected prefs
+    expected_prefs = {}
+    for profile in md.base_profiles:
+        for name in Profile.preference_file_names:
+            path = os.path.join(profile_data_dir, profile, name)
+            if os.path.isfile(path):
+                expected_prefs.update(Preferences.read_prefs(path))
+
+    # read the actual prefs
+    actual_prefs = {}
+    for name in Profile.preference_file_names:
+        path = os.path.join(md.profile.profile, name)
+        if os.path.isfile(path):
+            actual_prefs.update(Preferences.read_prefs(path))
+
+    # keep this in sync with the values in MochitestDesktop.merge_base_profiles
+    interpolation = {
+        'server': '127.0.0.1:8888',
+    }
+    for k, v in expected_prefs.items():
+        if isinstance(v, string_types):
+            v = v.format(**interpolation)
+
+        assert k in actual_prefs
+        assert k and actual_prefs[k] == v
+
+
+if __name__ == '__main__':
+    mozunit.main()