diff options
Diffstat (limited to 'testing/talos/talos/ffsetup.py')
-rw-r--r-- | testing/talos/talos/ffsetup.py | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/testing/talos/talos/ffsetup.py b/testing/talos/talos/ffsetup.py new file mode 100644 index 0000000000..4ac5ed688b --- /dev/null +++ b/testing/talos/talos/ffsetup.py @@ -0,0 +1,351 @@ +# 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/. + +""" +Set up a browser environment before running a test. +""" +import json +import os +import shutil +import tempfile + +import mozfile +import mozinfo +import mozrunner +import six +from mozlog import get_proxy_logger +from mozprofile.profile import Profile +from talos import heavy, utils +from talos.gecko_profile import GeckoProfile +from talos.utils import TalosError, run_in_debug_mode + +here = os.path.abspath(os.path.dirname(__file__)) + +LOG = get_proxy_logger() + + +class FFSetup(object): + """ + Initialize the browser environment before running a test. + + This prepares: + - the environment vars for running the test in the browser, + available via the instance member *env*. + - the profile used to run the test, available via the + instance member *profile_dir*. + - Gecko profiling, available via the instance member *gecko_profile* + of type :class:`GeckoProfile` or None if not used. + + Note that the browser will be run once with the profile, to ensure + this is basically working and negate any performance noise with the + real test run (installing the profile the first time takes time). + + This class should be used as a context manager:: + + with FFSetup(browser_config, test_config) as setup: + # setup.env is initialized, and setup.profile_dir created + pass + # here the profile is removed + """ + + def __init__(self, browser_config, test_config): + self.browser_config, self.test_config = browser_config, test_config + self._tmp_dir = tempfile.mkdtemp() + self.env = None + # The profile dir must be named 'profile' because of xperf analysis + # (in etlparser.py). TODO fix that ? + self.profile_dir = os.path.join(self._tmp_dir, "profile") + self.gecko_profile = None + self.debug_mode = run_in_debug_mode(browser_config) + + @property + def profile_data_dir(self): + if "MOZ_DEVELOPER_REPO_DIR" in os.environ: + return os.path.join( + os.environ["MOZ_DEVELOPER_REPO_DIR"], "testing", "profiles" + ) + return os.path.join(here, "profile_data") + + def _init_env(self): + self.env = dict(os.environ) + for k, v in six.iteritems(self.browser_config["env"]): + self.env[k] = str(v) + self.env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" + if self.browser_config["symbols_path"]: + self.env["MOZ_CRASHREPORTER"] = "1" + else: + self.env["MOZ_CRASHREPORTER_DISABLE"] = "1" + + self.env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" + + self.env["LD_LIBRARY_PATH"] = os.path.dirname( + self.browser_config["browser_path"] + ) + + def _init_profile(self): + extensions = self.browser_config["extensions"][:] + if self.test_config.get("extensions"): + extensions.extend(self.test_config["extensions"]) + + # downloading a profile instead of using the empty one + if self.test_config["profile"] is not None: + path = heavy.download_profile(self.test_config["profile"]) + self.test_config["profile_path"] = path + + profile_path = os.path.normpath(self.test_config["profile_path"]) + LOG.info("Cloning profile located at %s" % profile_path) + + def _feedback(directory, content): + # Called by shutil.copytree on each visited directory. + # Used here to display info. + # + # Returns the items that should be ignored by + # shutil.copytree when copying the tree, so always returns + # an empty list. + sub = directory.split(profile_path)[-1].lstrip("/") + if sub: + LOG.info("=> %s" % sub) + return [] + + profile = Profile.clone( + profile_path, self.profile_dir, ignore=_feedback, restore=False + ) + + # build pref interpolation context + webserver = self.browser_config["webserver"] + if "://" not in webserver: + webserver = "http://" + webserver + + interpolation = { + "webserver": webserver, + } + + # merge base profiles + with open(os.path.join(self.profile_data_dir, "profiles.json"), "r") as fh: + base_profiles = json.load(fh)["talos"] + + for name in base_profiles: + path = os.path.join(self.profile_data_dir, name) + LOG.info("Merging profile: {}".format(path)) + profile.merge(path, interpolation=interpolation) + + # set test preferences + preferences = self.browser_config.get("preferences", {}).copy() + if self.test_config.get("preferences"): + test_prefs = dict( + [ + (i, utils.parse_pref(j)) + for i, j in self.test_config["preferences"].items() + ] + ) + preferences.update(test_prefs) + + for name, value in preferences.items(): + if type(value) is str: + value = utils.interpolate(value, **interpolation) + preferences[name] = value + profile.set_preferences(preferences) + + # installing addons + LOG.info("Installing Add-ons:") + LOG.info(extensions) + profile.addons.install(extensions) + + # installing webextensions + webextensions_to_install = [] + webextensions_folder = self.test_config.get("webextensions_folder", None) + if isinstance(webextensions_folder, six.string_types): + folder = utils.interpolate(webextensions_folder) + for file in os.listdir(folder): + if file.endswith(".xpi"): + webextensions_to_install.append(os.path.join(folder, file)) + + webextensions = self.test_config.get("webextensions", None) + if isinstance(webextensions, six.string_types): + webextensions_to_install.append(webextensions) + + if webextensions_to_install is not None: + LOG.info("Installing Webextensions:") + for webext in webextensions_to_install: + filename = utils.interpolate(webext) + if mozinfo.os == "win": + filename = filename.replace("/", "\\") + if not filename.endswith(".xpi"): + continue + if not os.path.exists(filename): + continue + LOG.info(filename) + profile.addons.install(filename) + + def _run_profile(self): + runner_cls = mozrunner.runners.get( + mozinfo.info.get("appname", "firefox"), mozrunner.Runner + ) + + args = list(self.browser_config["extra_args"]) + args.append(self.browser_config["init_url"]) + + runner = runner_cls( + profile=self.profile_dir, + binary=self.browser_config["browser_path"], + cmdargs=args, + env=self.env, + ) + + runner.start(outputTimeout=30) + proc = runner.process_handler + LOG.process_start( + proc.pid, "%s %s" % (self.browser_config["browser_path"], " ".join(args)) + ) + + try: + exit_code = proc.wait() + except Exception: + proc.kill() + raise TalosError("Browser Failed to close properly during warmup") + + LOG.process_exit(proc.pid, exit_code) + + def _init_gecko_profile(self): + upload_dir = os.getenv("MOZ_UPLOAD_DIR") + if self.test_config.get("gecko_profile") and not upload_dir: + LOG.critical("Profiling ignored because MOZ_UPLOAD_DIR was not" " set") + if upload_dir and self.test_config.get("gecko_profile"): + self.gecko_profile = GeckoProfile( + upload_dir, self.browser_config, self.test_config + ) + self.gecko_profile.update_env(self.env) + + def clean(self): + try: + mozfile.remove(self._tmp_dir) + except Exception as e: + LOG.info("Exception while removing profile directory: %s" % self._tmp_dir) + LOG.info(e) + + if self.gecko_profile: + self.gecko_profile.clean() + + def collect_or_clean_ccov(self, clean=False): + # NOTE: Currently only supported when running in production + if not self.browser_config.get("develop", False): + # first see if we an find any ccov files at the ccov output dirs + if clean: + LOG.info("Cleaning ccov files before starting the talos test") + else: + LOG.info( + "Collecting ccov files that were generated during the talos test" + ) + gcov_prefix = os.getenv("GCOV_PREFIX", None) + js_ccov_dir = os.getenv("JS_CODE_COVERAGE_OUTPUT_DIR", None) + gcda_archive_folder_name = "gcda-archive" + _gcda_files_found = [] + + for _ccov_env in [gcov_prefix, js_ccov_dir]: + if _ccov_env is not None: + # ccov output dir env vars exist; now search for gcda files to remove + _ccov_path = os.path.abspath(_ccov_env) + if os.path.exists(_ccov_path): + # now walk through and look for gcda files + LOG.info("Recursive search for gcda files in: %s" % _ccov_path) + for root, dirs, files in os.walk(_ccov_path): + for next_file in files: + if next_file.endswith(".gcda"): + # don't want to move or delete files in our 'gcda-archive' + if root.find(gcda_archive_folder_name) == -1: + _gcda_files_found.append( + os.path.join(root, next_file) + ) + else: + LOG.info( + "The ccov env var path doesn't exist: %s" % str(_ccov_path) + ) + + # now clean or collect gcda files accordingly + if clean: + # remove ccov data + LOG.info( + "Found %d gcda files to clean. Deleting..." + % (len(_gcda_files_found)) + ) + for _gcda in _gcda_files_found: + try: + mozfile.remove(_gcda) + except Exception as e: + LOG.info("Exception while removing file: %s" % _gcda) + LOG.info(e) + LOG.info("Finished cleaning ccov gcda files") + else: + # copy gcda files to archive folder to be collected later + gcda_archive_top = os.path.join( + gcov_prefix, gcda_archive_folder_name, self.test_config["name"] + ) + LOG.info( + "Found %d gcda files to collect. Moving to gcda archive %s" + % (len(_gcda_files_found), str(gcda_archive_top)) + ) + if not os.path.exists(gcda_archive_top): + try: + os.makedirs(gcda_archive_top) + except OSError: + LOG.critical( + "Unable to make gcda archive folder %s" % gcda_archive_top + ) + for _gcda in _gcda_files_found: + # want to copy the existing directory strucutre but put it under archive-dir + # need to remove preceeding '/' from _gcda file name so can join the path + gcda_archive_file = os.path.join( + gcov_prefix, + gcda_archive_folder_name, + self.test_config["name"], + _gcda.strip(gcov_prefix + "//"), + ) + gcda_archive_dest = os.path.dirname(gcda_archive_file) + + # create archive folder, mirroring structure + if not os.path.exists(gcda_archive_dest): + try: + os.makedirs(gcda_archive_dest) + except OSError: + LOG.critical( + "Unable to make archive folder %s" % gcda_archive_dest + ) + # now copy the file there + try: + shutil.copy(_gcda, gcda_archive_dest) + except Exception as e: + LOG.info( + "Error copying %s to %s" + % (str(_gcda), str(gcda_archive_dest)) + ) + LOG.info(e) + LOG.info( + "Finished collecting ccov gcda files. Copied to: %s" + % gcda_archive_top + ) + + def __enter__(self): + LOG.info("Initialising browser for %s test..." % self.test_config["name"]) + self._init_env() + self._init_profile() + try: + if not self.debug_mode and not self.test_config["name"].startswith("damp"): + self._run_profile() + except BaseException: + self.clean() + raise + self._init_gecko_profile() + LOG.info("Browser initialized.") + LOG.info("Fission enabled: %s" % self.browser_config.get("fission", True)) + # remove ccov files before actual tests start + if self.browser_config.get("code_coverage", False): + # if the Firefox build was instrumented for ccov, initializing the browser + # will have caused ccov to output some gcda files; in order to have valid + # ccov data for the talos test we want to remove these files before starting + # the actual talos test(s) + self.collect_or_clean_ccov(clean=True) + return self + + def __exit__(self, type, value, tb): + self.clean() |