Blame testing/talos/talos/ffsetup.py

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