Bug 1371709 - Support passing profile to Firefox wdspec tests, r=ato draft
authorJames Graham <james@hoppipolla.co.uk>
Fri, 09 Jun 2017 16:42:33 +0100
changeset 594751 22d0d6e93cb923c3a9282311f5ce3ae7a06d4bf3
parent 594750 4210c68c56314ed673a004955b48c4553f6f7e2d
child 594752 a9adb30834023b6e724c8d13c18f64d360188e79
push id64122
push userbmo:james@hoppipolla.co.uk
push dateThu, 15 Jun 2017 12:20:31 +0000
reviewersato
bugs1371709
milestone56.0a1
Bug 1371709 - Support passing profile to Firefox wdspec tests, r=ato In order for wdspec tests to run against Firefox with the correct settings we need to ensure that they use a correctly configured profile. We could do this by creating a profile with a -profile argument, but that has the disadvantage that the profile is fixed for all tests and so we don't clean out as much state as we would like. Instead we generate a profile upfront, base64 encode it, and pass it through the profile capability. MozReview-Commit-ID: H1B7CLVwpsp
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
testing/web-platform/tests/tools/wptrunner/wptrunner/products.py
testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -1,41 +1,49 @@
+import base64
 import os
 import platform
+import shutil
 import signal
 import subprocess
 import sys
+import tempfile
+import zipfile
+from cStringIO import StringIO
 
 import mozinfo
 import mozleak
+import mozlog
 from mozprocess import ProcessHandler
 from mozprofile import FirefoxProfile, Preferences
 from mozprofile.permissions import ServerLocations
 from mozrunner import FirefoxRunner
 from mozrunner.utils import get_stack_fixer_function
 from mozcrash import mozcrash
 
 from .base import (get_free_port,
                    Browser,
                    ExecutorBrowser,
+                   NullBrowser,
                    require_arg,
                    cmd_arg,
                    browser_command)
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executormarionette import (MarionetteTestharnessExecutor,
                                             MarionetteRefTestExecutor,
                                             MarionetteWdspecExecutor)
 from ..environment import hostnames
 
 
 here = os.path.join(os.path.split(__file__)[0])
 
 __wptrunner__ = {"product": "firefox",
                  "check_args": "check_args",
-                 "browser": "FirefoxBrowser",
+                 "browser": {None: "FirefoxBrowser",
+                             "wdspec": "NullBrowser"},
                  "executor": {"testharness": "MarionetteTestharnessExecutor",
                               "reftest": "MarionetteRefTestExecutor",
                               "wdspec": "MarionetteWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
                  "env_options": "env_options",
                  "run_info_extras": "run_info_extras",
@@ -57,31 +65,32 @@ def get_timeout_multiplier(test_type, ru
 
 def check_args(**kwargs):
     require_arg(kwargs, "binary")
     if kwargs["ssl_type"] != "none":
         require_arg(kwargs, "certutil_binary")
 
 
 def browser_kwargs(test_type, run_info_data, **kwargs):
-    return {"binary": kwargs["binary"],
-            "prefs_root": kwargs["prefs_root"],
-            "extra_prefs": kwargs["extra_prefs"],
-            "debug_info": kwargs["debug_info"],
-            "symbols_path": kwargs["symbols_path"],
-            "stackwalk_binary": kwargs["stackwalk_binary"],
-            "certutil_binary": kwargs["certutil_binary"],
-            "ca_certificate_path": kwargs["ssl_env"].ca_cert_path(),
-            "e10s": kwargs["gecko_e10s"],
-            "stackfix_dir": kwargs["stackfix_dir"],
-            "binary_args": kwargs["binary_args"],
-            "timeout_multiplier": get_timeout_multiplier(test_type,
-                                                         run_info_data,
-                                                         **kwargs),
-            "leak_check": kwargs["leak_check"]}
+    rv = {"binary": kwargs["binary"],
+          "prefs_root": kwargs["prefs_root"],
+          "extra_prefs": kwargs["extra_prefs"],
+          "debug_info": kwargs["debug_info"],
+          "symbols_path": kwargs["symbols_path"],
+          "stackwalk_binary": kwargs["stackwalk_binary"],
+          "certutil_binary": kwargs["certutil_binary"],
+          "ca_certificate_path": kwargs["ssl_env"].ca_cert_path(),
+          "e10s": kwargs["gecko_e10s"],
+          "stackfix_dir": kwargs["stackfix_dir"],
+          "binary_args": kwargs["binary_args"],
+          "timeout_multiplier": get_timeout_multiplier(test_type,
+                                                       run_info_data,
+                                                       **kwargs),
+          "leak_check": kwargs["leak_check"]}
+    return rv
 
 
 def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
                     **kwargs):
     executor_kwargs = base_executor_kwargs(test_type, server_config,
                                            cache_manager, **kwargs)
     executor_kwargs["close_after_done"] = test_type != "reftest"
     executor_kwargs["timeout_multiplier"] = get_timeout_multiplier(test_type,
@@ -91,19 +100,28 @@ def executor_kwargs(test_type, server_co
         executor_kwargs["binary"] = kwargs["binary"]
         executor_kwargs["webdriver_binary"] = kwargs.get("webdriver_binary")
         executor_kwargs["webdriver_args"] = kwargs.get("webdriver_args")
         fxOptions = {}
         if kwargs["binary"]:
             fxOptions["binary"] = kwargs["binary"]
         if kwargs["binary_args"]:
             fxOptions["args"] = kwargs["binary_args"]
-        fxOptions["prefs"] = {
-            "network.dns.localDomains": ",".join(hostnames)
-        }
+        else:
+            fxOptions["args"] = []
+
+        profile = Profile(mozlog.get_default_logger(),
+                          kwargs["binary"],
+                          kwargs["prefs_root"],
+                          kwargs["extra_prefs"],
+                          kwargs["gecko_e10s"],
+                          kwargs["ssl_env"].ca_cert_path(),
+                          kwargs["certutil_binary"])
+        profile.setup()
+        fxOptions["profile"] = profile.to_base64_zip()
         capabilities = {"moz:firefoxOptions": fxOptions}
         executor_kwargs["capabilities"] = capabilities
     return executor_kwargs
 
 
 def env_extras(**kwargs):
     return []
 
@@ -119,123 +137,206 @@ def env_options():
 def run_info_extras(**kwargs):
     return {"e10s": kwargs["gecko_e10s"]}
 
 
 def update_properties():
     return ["debug", "e10s", "os", "version", "processor", "bits"], {"debug", "e10s"}
 
 
-class FirefoxBrowser(Browser):
-    used_ports = set()
-    init_timeout = 60
-    shutdown_timeout = 60
-
-    def __init__(self, logger, binary, prefs_root, extra_prefs=None, debug_info=None,
-                 symbols_path=None, stackwalk_binary=None, certutil_binary=None,
-                 ca_certificate_path=None, e10s=False, stackfix_dir=None,
-                 binary_args=None, timeout_multiplier=None, leak_check=False):
-        Browser.__init__(self, logger)
+class Profile(object):
+    def __init__(self, logger, binary, prefs_root, extra_prefs, e10s, ca_certificate_path, certutil_binary):
+        self.logger = logger
         self.binary = binary
         self.prefs_root = prefs_root
         self.extra_prefs = extra_prefs
-        self.marionette_port = None
-        self.runner = None
-        self.debug_info = debug_info
-        self.profile = None
-        self.symbols_path = symbols_path
-        self.stackwalk_binary = stackwalk_binary
+        self.e10s = e10s
         self.ca_certificate_path = ca_certificate_path
         self.certutil_binary = certutil_binary
         self.e10s = e10s
-        self.binary_args = binary_args
-        if self.symbols_path and stackfix_dir:
-            self.stack_fixer = get_stack_fixer_function(stackfix_dir,
-                                                        self.symbols_path)
-        else:
-            self.stack_fixer = None
-
-        if timeout_multiplier:
-            self.init_timeout = self.init_timeout * timeout_multiplier
-
-        self.leak_report_file = None
-        self.leak_check = leak_check
-
-    def settings(self, test):
-        return {"check_leaks": self.leak_check and not test.leaks}
-
-    def start(self, **kwargs):
-        if self.marionette_port is None:
-            self.marionette_port = get_free_port(2828, exclude=self.used_ports)
-            self.used_ports.add(self.marionette_port)
-
-        env = os.environ.copy()
-        env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
-
-        locations = ServerLocations(filename=os.path.join(here, "server-locations.txt"))
-
-        preferences = self.load_prefs()
-
-        self.profile = FirefoxProfile(locations=locations,
-                                      preferences=preferences)
-        self.profile.set_preferences({"marionette.port": self.marionette_port,
-                                      "dom.disable_open_during_load": False,
-                                      "network.dns.localDomains": ",".join(hostnames),
-                                      "network.proxy.type": 0,
-                                      "places.history.enabled": False})
-        if self.e10s:
-            self.profile.set_preferences({"browser.tabs.remote.autostart": True})
-
-        if self.leak_check and kwargs.get("check_leaks", True):
-            self.leak_report_file = os.path.join(self.profile.profile, "runtests_leaks.log")
-            if os.path.exists(self.leak_report_file):
-                os.remove(self.leak_report_file)
-            env["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
-        else:
-            self.leak_report_file = None
-
-        # Bug 1262954: winxp + e10s, disable hwaccel
-        if (self.e10s and platform.system() in ("Windows", "Microsoft") and
-            '5.1' in platform.version()):
-            self.profile.set_preferences({"layers.acceleration.disabled": True})
-
-        if self.ca_certificate_path is not None:
-            self.setup_ssl()
-
-        debug_args, cmd = browser_command(self.binary,
-                                          self.binary_args if self.binary_args else [] +
-                                          [cmd_arg("marionette"), "about:blank"],
-                                          self.debug_info)
-
-        self.runner = FirefoxRunner(profile=self.profile,
-                                    binary=cmd[0],
-                                    cmdargs=cmd[1:],
-                                    env=env,
-                                    process_class=ProcessHandler,
-                                    process_args={"processOutputLine": [self.on_output]})
-
-        self.logger.debug("Starting Firefox")
-
-        self.runner.start(debug_args=debug_args, interactive=self.debug_info and self.debug_info.interactive)
-        self.logger.debug("Firefox Started")
+        self.profile = None
+        self.profile_dir = None
 
     def load_prefs(self):
         prefs = Preferences()
 
         prefs_path = os.path.join(self.prefs_root, "prefs_general.js")
         if os.path.exists(prefs_path):
             prefs.add(Preferences.read_prefs(prefs_path))
         else:
             self.logger.warning("Failed to find base prefs file in %s" % prefs_path)
 
         # Add any custom preferences
         prefs.add(self.extra_prefs, cast=True)
 
         return prefs()
 
+    def setup(self, marionette_port=None):
+        locations = ServerLocations(filename=os.path.join(here, "server-locations.txt"))
+
+        preferences = self.load_prefs()
+
+        self.profile = FirefoxProfile(locations=locations,
+                                      preferences=preferences)
+        self.profile_dir = self.profile.profile
+
+        extra_prefs = {"dom.disable_open_during_load": False,
+                       "network.dns.localDomains": ",".join(hostnames),
+                       "network.proxy.type": 0,
+                       "places.history.enabled": False}
+
+        if marionette_port is not None:
+            extra_prefs["marionette.port"] = marionette_port
+        if self.e10s:
+            extra_prefs["browser.tabs.remote.autostart"] = True
+
+        self.profile.set_preferences(extra_prefs)
+
+        # Bug 1262954: winxp + e10s, disable hwaccel
+        if (self.e10s and platform.system() in ("Windows", "Microsoft") and
+            '5.1' in platform.version()):
+            self.profile.set_preferences({"layers.acceleration.disabled": True})
+
+        return self.profile
+
+    def setup_ssl(self):
+        """Create a certificate database to use in the test profile. This is configured
+        to trust the CA Certificate that has signed the web-platform.test server
+        certificate."""
+
+        self.logger.info("Setting up ssl")
+
+        # Make sure the certutil libraries from the source tree are loaded when using a
+        # local copy of certutil
+        # TODO: Maybe only set this if certutil won't launch?
+        env = os.environ.copy()
+        certutil_dir = os.path.dirname(self.binary)
+        if mozinfo.isMac:
+            env_var = "DYLD_LIBRARY_PATH"
+        elif mozinfo.isUnix:
+            env_var = "LD_LIBRARY_PATH"
+        else:
+            env_var = "PATH"
+
+
+        env[env_var] = (os.path.pathsep.join([certutil_dir, env[env_var]])
+                        if env_var in env else certutil_dir).encode(
+                                sys.getfilesystemencoding() or 'utf-8', 'replace')
+
+        def certutil(*args):
+            cmd = [self.certutil_binary] + list(args)
+            self.logger.process_output("certutil",
+                                       subprocess.check_output(cmd,
+                                                               env=env,
+                                                               stderr=subprocess.STDOUT),
+                                       " ".join(cmd))
+
+        pw_path = os.path.join(self.profile_dir, ".crtdbpw")
+        with open(pw_path, "w") as f:
+            # Use empty password for certificate db
+            f.write("\n")
+
+        cert_db_path = self.profile_dir
+
+        # Create a new certificate db
+        certutil("-N", "-d", cert_db_path, "-f", pw_path)
+
+        # Add the CA certificate to the database and mark as trusted to issue server certs
+        certutil("-A", "-d", cert_db_path, "-f", pw_path, "-t", "CT,,",
+                 "-n", "web-platform-tests", "-i", self.ca_certificate_path)
+
+        # List all certs in the database
+        certutil("-L", "-d", cert_db_path)
+
+    def to_base64_zip(self):
+        data = StringIO()
+        zipf = zipfile.ZipFile(data, mode="w", compression=zipfile.ZIP_DEFLATED)
+        for dir_path, dir_names, file_names in os.walk(self.profile_dir):
+            zip_dir = os.path.relpath(dir_path, self.profile_dir)
+            for file_name in file_names:
+                zip_path = os.path.join(zip_dir, file_name)
+                zipf.write(os.path.join(dir_path, file_name),
+                           zip_path)
+        zipf.close()
+        return base64.b64encode(data.getvalue())
+
+
+class FirefoxBrowser(Browser):
+    used_ports = set()
+    init_timeout = 60
+    shutdown_timeout = 60
+
+    def __init__(self, logger, binary, prefs_root, extra_prefs=None, debug_info=None,
+                 symbols_path=None, stackwalk_binary=None, certutil_binary=None,
+                 ca_certificate_path=None, e10s=False, stackfix_dir=None,
+                 binary_args=None, timeout_multiplier=None, leak_check=False,
+                 profile_dir=None):
+        Browser.__init__(self, logger)
+        self.binary = binary
+        self.prefs_root = prefs_root
+        self.extra_prefs = extra_prefs
+        self.marionette_port = None
+        self.runner = None
+        self.debug_info = debug_info
+        self.profile = Profile(logger, binary, prefs_root, extra_prefs, e10s,
+                               ca_certificate_path, certutil_binary)
+        self.symbols_path = symbols_path
+        self.stackwalk_binary = stackwalk_binary
+        self.binary_args = binary_args
+        if self.symbols_path and stackfix_dir:
+            self.stack_fixer = get_stack_fixer_function(stackfix_dir,
+                                                        self.symbols_path)
+        else:
+            self.stack_fixer = None
+
+        if timeout_multiplier:
+            self.init_timeout = self.init_timeout * timeout_multiplier
+
+        self.leak_report_file = None
+        self.leak_check = leak_check
+        self.profile_dir = profile_dir
+
+    def settings(self, test):
+        return {"check_leaks": self.leak_check and not test.leaks}
+
+    def start(self, **kwargs):
+        if self.marionette_port is None:
+            self.marionette_port = get_free_port(2828, exclude=self.used_ports)
+            self.used_ports.add(self.marionette_port)
+
+        profile = self.profile.setup(self.marionette_port)
+
+        env = os.environ.copy()
+        env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
+
+        if self.leak_check and kwargs.get("check_leaks", True):
+            self.leak_report_file = os.path.join(profile, "runtests_leaks.log")
+            if os.path.exists(self.leak_report_file):
+                os.remove(self.leak_report_file)
+            env["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
+        else:
+            self.leak_report_file = None
+
+        debug_args, cmd = browser_command(self.binary,
+                                          self.binary_args if self.binary_args else [] +
+                                          [cmd_arg("marionette"), "about:blank"],
+                                          self.debug_info)
+
+        self.runner = FirefoxRunner(profile=profile,
+                                    binary=cmd[0],
+                                    cmdargs=cmd[1:],
+                                    env=env,
+                                    process_class=ProcessHandler,
+                                    process_args={"processOutputLine": [self.on_output]})
+
+        self.logger.debug("Starting Firefox")
+
+        self.runner.start(debug_args=debug_args, interactive=self.debug_info and self.debug_info.interactive)
+        self.logger.debug("Firefox Started")
+
     def stop(self, force=False):
         if self.runner is not None and self.runner.is_running():
             try:
                 # For Firefox we assume that stopping the runner prompts the
                 # browser to shut down. This allows the leak log to be written
                 for clean, stop_f in [(True, lambda: self.runner.wait(self.shutdown_timeout)),
                                       (False, lambda: self.runner.stop(signal.SIGTERM)),
                                       (False, lambda: self.runner.stop(signal.SIGKILL))]:
@@ -293,65 +394,16 @@ class FirefoxBrowser(Browser):
         self.stop()
         self.process_leaks()
 
     def executor_browser(self):
         assert self.marionette_port is not None
         return ExecutorBrowser, {"marionette_port": self.marionette_port}
 
     def log_crash(self, process, test):
-        dump_dir = os.path.join(self.profile.profile, "minidumps")
+        dump_dir = os.path.join(self.profile.profile_dir, "minidumps")
 
         mozcrash.log_crashes(self.logger,
                              dump_dir,
                              symbols_path=self.symbols_path,
                              stackwalk_binary=self.stackwalk_binary,
                              process=process,
                              test=test)
-
-    def setup_ssl(self):
-        """Create a certificate database to use in the test profile. This is configured
-        to trust the CA Certificate that has signed the web-platform.test server
-        certificate."""
-
-        self.logger.info("Setting up ssl")
-
-        # Make sure the certutil libraries from the source tree are loaded when using a
-        # local copy of certutil
-        # TODO: Maybe only set this if certutil won't launch?
-        env = os.environ.copy()
-        certutil_dir = os.path.dirname(self.binary)
-        if mozinfo.isMac:
-            env_var = "DYLD_LIBRARY_PATH"
-        elif mozinfo.isUnix:
-            env_var = "LD_LIBRARY_PATH"
-        else:
-            env_var = "PATH"
-
-
-        env[env_var] = (os.path.pathsep.join([certutil_dir, env[env_var]])
-                        if env_var in env else certutil_dir).encode(
-                                sys.getfilesystemencoding() or 'utf-8', 'replace')
-
-        def certutil(*args):
-            cmd = [self.certutil_binary] + list(args)
-            self.logger.process_output("certutil",
-                                       subprocess.check_output(cmd,
-                                                               env=env,
-                                                               stderr=subprocess.STDOUT),
-                                       " ".join(cmd))
-
-        pw_path = os.path.join(self.profile.profile, ".crtdbpw")
-        with open(pw_path, "w") as f:
-            # Use empty password for certificate db
-            f.write("\n")
-
-        cert_db_path = self.profile.profile
-
-        # Create a new certificate db
-        certutil("-N", "-d", cert_db_path, "-f", pw_path)
-
-        # Add the CA certificate to the database and mark as trusted to issue server certs
-        certutil("-A", "-d", cert_db_path, "-f", pw_path, "-t", "CT,,",
-                 "-n", "web-platform-tests", "-i", self.ca_certificate_path)
-
-        # List all certs in the database
-        certutil("-L", "-d", cert_db_path)
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/products.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/products.py
@@ -30,23 +30,29 @@ def product_module(config, product):
     return module
 
 
 def load_product(config, product):
     module = product_module(config, product)
     data = module.__wptrunner__
 
     check_args = getattr(module, data["check_args"])
-    browser_cls = getattr(module, data["browser"])
     browser_kwargs = getattr(module, data["browser_kwargs"])
     executor_kwargs = getattr(module, data["executor_kwargs"])
     env_options = getattr(module, data["env_options"])()
     env_extras = getattr(module, data["env_extras"])
     run_info_extras = (getattr(module, data["run_info_extras"])
                        if "run_info_extras" in data else lambda **kwargs:{})
+    browser_cls = {}
+    browser_classes = data["browser"]
+    if isinstance(browser_classes, dict):
+        for test_type, cls_name in browser_classes.iteritems():
+            browser_cls[test_type] = getattr(module, cls_name)
+    else:
+        browser_cls[None] = getattr(module, cls_name)
 
     executor_classes = {}
     for test_type, cls_name in data["executor"].iteritems():
         cls = getattr(module, cls_name)
         executor_classes[test_type] = cls
 
     return (check_args,
             browser_cls, browser_kwargs,
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
@@ -6,17 +6,16 @@ import sys
 
 import environment as env
 import products
 import testloader
 import wptcommandline
 import wptlogging
 import wpttest
 from testrunner import ManagerGroup
-from browsers.base import NullBrowser
 
 here = os.path.split(__file__)[0]
 
 logger = None
 
 """Runner for web-platform-tests
 
 The runner has several design goals:
@@ -107,17 +106,17 @@ def get_pause_after_test(test_loader, **
     return kwargs["pause_after_test"]
 
 
 def run_tests(config, test_paths, product, **kwargs):
     with wptlogging.CaptureIO(logger, not kwargs["no_capture_stdio"]):
         env.do_delayed_imports(logger, test_paths)
 
         (check_args,
-         target_browser_cls, get_browser_kwargs,
+         browser_classes, get_browser_kwargs,
          executor_classes, get_executor_kwargs,
          env_options, get_env_extras, run_info_extras) = products.load_product(config, product)
 
         ssl_env = env.ssl_env(logger, **kwargs)
         env_extras = get_env_extras(**kwargs)
 
         check_args(**kwargs)
 
@@ -169,37 +168,31 @@ def run_tests(config, test_paths, produc
                 elif repeat > 1:
                     logger.info("Repetition %i / %i" % (repeat_count, repeat))
 
                 unexpected_count = 0
                 logger.suite_start(test_loader.test_ids, run_info)
                 for test_type in kwargs["test_types"]:
                     logger.info("Running %s tests" % test_type)
 
-                    # WebDriver tests may create and destroy multiple browser
-                    # processes as part of their expected behavior. These
-                    # processes are managed by a WebDriver server binary. This
-                    # obviates the need for wptrunner to provide a browser, so
-                    # the NullBrowser is used in place of the "target" browser
-                    if test_type == "wdspec":
-                        browser_cls = NullBrowser
-                    else:
-                        browser_cls = target_browser_cls
+                    browser_cls = (browser_classes.get(test_type) or
+                                   browser_classes.get(None))
 
                     browser_kwargs = get_browser_kwargs(test_type,
                                                         run_info,
                                                         ssl_env=ssl_env,
                                                         **kwargs)
 
 
                     executor_cls = executor_classes.get(test_type)
                     executor_kwargs = get_executor_kwargs(test_type,
                                                           test_environment.external_config,
                                                           test_environment.cache_manager,
                                                           run_info,
+                                                          ssl_env=ssl_env,
                                                           **kwargs)
 
                     if executor_cls is None:
                         logger.error("Unsupported test type %s for product %s" %
                                      (test_type, product))
                         continue
 
                     for test in test_loader.disabled_tests[test_type]: