blob: 72c9be581f3f4ba546e0a9ff713f044997134da6 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""API for the bisect recipe module.
This API is meant to enable the bisect recipe to bisect any chromium-supported
platform for any test that can be run via buildbot, perf or otherwise.
"""
import os
from recipe_engine import recipe_api
from . import bisector
from . import depot_config
from . import revision_state
from . import local_bisect
BISECT_CONFIG_FILE = 'tools/auto_bisect/bisect.cfg'
class AutoBisectApi(recipe_api.RecipeApi):
"""A module for bisect specific functions."""
# Number of seconds to wait between polls for test results.
POLLING_INTERVAL = 60
# GS bucket to use for communicating results and job state between bisector
# and tester bots.
BUCKET = 'chrome-perf'
# Directory within the above bucket to store results.
RESULTS_GS_DIR = 'bisect-results'
GS_RESULTS_URL = 'gs://%s/%s/' % (BUCKET, RESULTS_GS_DIR)
# Email to send on try jobs (for build requests) since git try will not
# necessarily rely on a local checkout for that information.
BOT_EMAIL = 'chrome_bot@chromium.org'
SERVICE_ACCOUNT = 'chromium-bisect'
def __init__(self, *args, **kwargs):
super(AutoBisectApi, self).__init__(*args, **kwargs)
self.override_poll_interval = None
self.bot_db = None
# Repo for triggering build jobs.
self.svn_repo_url = 'svn://svn.chromium.org/chrome-try/try-perf'
# The variable below are set and used for the internal bisects.
self.buildurl_gs_prefix = None
self.internal_bisect = False
self.builder_bot = None
self.full_deploy_script = None
# Keep track of working directory (which contains the checkout).
# None means "default value".
self._working_dir = None
@property
def working_dir(self):
if not self._working_dir:
self._working_dir = self.m.chromium_checkout.get_checkout_dir({})
return self._working_dir or self.m.path['slave_build']
def perform_bisect(self, **flags):
return local_bisect.perform_bisect(self, **flags)
def create_bisector(self, bisect_config_dict, dummy_mode=False, **flags):
"""Passes the api and the config dictionary to the Bisector constructor.
For details about the keys in the bisect config dictionary go to:
http://chromium.org/developers/speed-infra/perf-try-bots-bisect-bots/config
Args:
bisect_config_dict (dict): Contains the configuration for the bisect job.
dummy_mode (bool): In dummy mode we prevent the bisector for expanding
the revision range at construction, to avoid the need for lots of step
data when performing certain tests (such as results output).
Returns:
An instance of bisector.Bisector.
"""
self.override_poll_interval = bisect_config_dict.get('poll_sleep')
return bisector.Bisector(self, bisect_config_dict,
revision_state.RevisionState,
init_revisions=not dummy_mode, **flags)
def set_platform_gs_prefix(self, gs_url):
"""Sets GS path for the platform."""
self.buildurl_gs_prefix = gs_url # pragma: no cover
def set_builder_bot(self, builder_bot):
"""Sets builder name for building binaries."""
self.builder_bot = builder_bot # pragma: no cover
def set_internal(self):
"""Sets bisector as internal only to process android-chrome."""
self.internal_bisect = True # pragma: no cover
def set_additional_depot_info(self, depot_info):
"""Adds additional depot info to the global depot variables."""
depot_config.add_addition_depot_into(depot_info) # pragma: no cover
def set_deploy_script(self, path): # pragma: no cover
"""Sets apk deployment script path for android-chrome."""
self.full_deploy_script = path
def set_svn_repo(self, svn_repo_url): # pragma: no cover
"""Sets SVN repo url for triggering build jobs."""
self.svn_repo_url = svn_repo_url
def gsutil_file_exists(self, path):
"""Returns True if a file exists at the given GS path."""
try:
self.m.gsutil(['ls', path])
except self.m.step.StepFailure: # pragma: no cover
# A step failure here simply means that the file does not exist, and
# should not be treated as an error.
self.m.step.active_result.presentation.status = self.m.step.SUCCESS
return False
return True
def query_revision_info(self, revision):
"""Gathers information on a particular revision.
Args:
revision (str): A git commit hash.
Returns:
A dict with the keys "author", "email", "date", "subject" and "body",
as output by fetch_revision_info.py.
"""
step_name = 'Reading culprit cl information.'
# Use gitiles to get commit information.
if revision.depot_name == 'android-chrome': # pragma: no cover
commit_url = depot_config.DEPOT_DEPS_NAME[revision.depot_name]['url']
return self._commit_info(revision.commit_hash, commit_url, step_name)
result = self.m.python(
step_name,
self.resource('fetch_revision_info.py'),
[revision.commit_hash, revision.depot_name],
stdout=self.m.json.output())
return result.stdout
def _commit_info(self, commit_hash, url, step_name=None): # pragma: no cover
"""Fetches information about a given commit.
Args:
commit_hash (str): A git commit hash.
url (str): The URL of a repository, e.g.
"https://chromium.googlesource.com/chromium/src".
step_name (str): Optional step name.
Returns:
A dict with commit information.
"""
try:
step_name = step_name or 'gitiles commit info: %s' % commit_hash
commit_info = self.m.gitiles.commit_log(
url, commit_hash, step_name=step_name)
if commit_info:
message = commit_info.get('message', '').splitlines()
subject = message[0]
body = '\n'.join(message[1:])
return {
'author': commit_info['author']['name'],
'email': commit_info['author']['email'],
'subject': subject,
'body': body,
'date': commit_info['committer']['time'],
}
return None
except self.m.step.StepFailure: # pragma: no cover
self.surface_result('BAD_REV')
raise
def run_bisect_script(self, **kwargs):
"""Executes src/tools/run-perf-bisect-regression.py to perform bisection."""
self.m.python(
'Preparing for Bisection',
script=self.m.path['checkout'].join(
'tools', 'prepare-bisect-perf-regression.py'),
args=['-w', self.m.path['cache'].join('bisect')])
args = []
kwargs['allow_subannotations'] = True
# TODO(prasadv): Remove this once bisect runs are no longer running
# against revisions from February 2016 or earlier.
if self.internal_bisect: # pragma: no cover
kwargs['env'] = {'CHROMIUM_OUTPUT_DIR': self.m.chromium.output_dir}
if kwargs.get('extra_src'):
args = args + ['--extra_src', kwargs.pop('extra_src')]
if kwargs.get('path_to_config'):
args = args + ['--path_to_config', kwargs.pop('path_to_config')]
if self.m.chromium.c.TARGET_PLATFORM != 'android':
goma_dir = self.m.goma.ensure_goma()
args += ['--path_to_goma', goma_dir]
args += [
'--build-properties',
self.m.json.dumps(dict(self.m.properties.legacy())),
]
self.m.chromium.runtest(
self.m.path['checkout'].join('tools', 'run-bisect-perf-regression.py'),
['-w', self.m.path['cache'].join('bisect')] + args,
name='Running Bisection',
xvfb=True, **kwargs)
def run_local_test_run(self, test_config_params,
skip_download=False): # pragma: no cover
"""Starts a test run on the same machine.
This is for the merged director/tester flow.
"""
if self.m.platform.is_win:
self.m.chromium.taskkill()
if skip_download:
update_step = None
else:
update_step = self._SyncRevisionToTest(test_config_params)
self.start_test_run_for_bisect(update_step, self.bot_db,
test_config_params, run_locally=True,
skip_download=skip_download)
def ensure_checkout(self, *args, **kwargs):
if self.working_dir:
kwargs.setdefault('cwd', self.working_dir)
return self.m.bot_update.ensure_checkout(*args, **kwargs)
def _SyncRevisionToTest(self, test_config_params): # pragma: no cover
if not self.internal_bisect:
return self.ensure_checkout(
root_solution_revision=test_config_params['revision'])
else:
return self._SyncRevisionsForAndroidChrome(
test_config_params['revision_ladder'])
def _SyncRevisionsForAndroidChrome(self, revision_ladder): # pragma: no cover
"""Syncs android-chrome and chromium repos to particular revision."""
revisions = []
for d, r in revision_ladder.iteritems():
revisions.append('%s@%s' % (depot_config.DEPOT_DEPS_NAME[d]['src'], r))
params = ['sync', '--verbose', '--nohooks', '--force',
'--delete_unversioned_trees']
for revision in revisions:
params.extend(['--revision', revision])
self.m.gclient('sync %s' % '-'.join(revisions), params)
return None
def start_test_run_for_bisect(self, update_step, bot_db,
test_config_params, run_locally=False,
skip_download=False):
mastername = self.m.properties.get('mastername')
buildername = self.m.properties.get('buildername')
bot_config = bot_db.get_bot_config(mastername, buildername)
build_archive_url = test_config_params['parent_build_archive_url']
if not run_locally:
self.m.bisect_tester.upload_job_url()
if not skip_download:
if self.m.chromium.c.TARGET_PLATFORM == 'android':
# The best way to ensure the old build directory is not used is to
# remove it.
build_dir = self.m.chromium.c.build_dir.join(
self.m.chromium.c.build_config_fs)
self.m.file.rmtree('build directory', build_dir)
# The way android builders on tryserver.chromium.perf are archived is
# different from builders on chromium.perf. In order to support both
# forms of archives, we added this temporary hack until builders are
# fixed. See http://crbug.com/535218.
zip_dir = self.m.path.join(self.m.path['checkout'], 'full-build-linux')
if self.m.path.exists(zip_dir): # pragma: no cover
self.m.file.rmtree('full-build-linux directory', zip_dir)
gs_bucket = 'gs://%s/' % bot_config['bucket']
archive_path = build_archive_url[len(gs_bucket):]
self.m.chromium_android.download_build(
bucket=bot_config['bucket'],
path=archive_path)
# The way android builders on tryserver.chromium.perf are archived is
# different from builders on chromium.perf. In order to support both
# forms of archives, we added this temporary hack until builders are
# fixed. See http://crbug.com/535218.
if self.m.path.exists(zip_dir): # pragma: no cover
self.m.python.inline(
'moving full-build-linux to out/Release',
"""
import shutil
import sys
shutil.move(sys.argv[1], sys.argv[2])
""",
args=[zip_dir, build_dir])
else:
self.m.chromium_tests.download_and_unzip_build(
mastername, buildername, update_step, bot_db,
build_archive_url=build_archive_url,
build_revision=test_config_params['parent_got_revision'],
override_bot_type='tester')
tests = [self.m.chromium_tests.steps.BisectTest(test_config_params)]
if not tests: # pragma: no cover
return
self.m.chromium_swarming.configure_swarming(
'chromium', precommit=False, mastername=mastername)
test_runner = self.m.chromium_tests.create_test_runner(self.m, tests)
bot_config_object = self.m.chromium_tests.create_bot_config_object(
mastername, buildername)
with self.m.chromium_tests.wrap_chromium_tests(bot_config_object, tests):
if self.m.chromium.c.TARGET_PLATFORM == 'android' and not skip_download:
deploy_apks = []
deploy_args = []
if self.internal_bisect: # pragma: no cover
deploy_args = list(bot_config['deploy_args'])
deploy_apks = bot_config.get('deploy_apks')
if deploy_apks:
deploy_args.extend([
'--apks',
','.join(str(self.m.chromium_android.apk_path(apk))
for apk in deploy_apks)])
else:
if bot_config.get('webview'):
deploy_apks = ['SystemWebView.apk', 'SystemWebViewShell.apk']
else:
deploy_apks = ['ChromePublic.apk']
self.deploy_apk_on_device(
self.full_deploy_script, deploy_apks, deploy_args)
test_runner()
def deploy_apk_on_device(self, deploy_script, deploy_apks, deploy_args):
"""Installs apk on the android device."""
if deploy_script: # pragma: no cover
self.full_deploy_on_device(deploy_script, deploy_args)
else:
for apk in deploy_apks:
self.m.chromium_android.adb_install_apk(apk)
def full_deploy_on_device(self, deploy_script, args=None): # pragma: no cover
"""Install android-chrome apk on device."""
full_deploy_flags = [
'-v',
'--blacklist-file', self.m.chromium_android.blacklist_file,
'--perfbot',
'--release',
]
if args:
full_deploy_flags += args
self.m.python(
'Deploy on Device',
deploy_script,
full_deploy_flags,
infra_step=True,
env=self.m.chromium.get_env())
def start_try_job(self, api, update_step=None, bot_db=None, **kwargs):
"""Starts a recipe bisect job, perf test run, or legacy bisect run.
This function is an entry point for:
1. A legacy bisect job run (in this case, there will be a patch
with a bisect config file).
2. A recipe bisect job run (in this case, there will be a property
called bisect_config which contains the config parameters).
3. A single test run for a recipe bisect job (there will be a
bisect_config property but it won't contain good/bad revisions).
4. A perf try job run.
Args:
api: The recipe api object.
update_step: Extra update_step to, used for some job types.
bot_db: A BotConfigAndTestDB object, used for some job types.
kwargs: Args to use only for legacy bisect.
"""
flags = {}
if kwargs.get('do_not_nest_wait_for_revision'):
flags['do_not_nest_wait_for_revision'] = kwargs.pop(
'do_not_nest_wait_for_revision')
if bot_db is None: # pragma: no cover
self.bot_db = api.chromium_tests.create_bot_db_from_master_dict(
'', None, None)
else:
self.bot_db = bot_db
context = {}
if self.working_dir:
context['cwd'] = self.working_dir
with api.step.context(context):
affected_files = self.m.tryserver.get_files_affected_by_patch()
# Skip device setup for internal bisect as it is taken care in
# internal recipes.
if (api.chromium.c.TARGET_PLATFORM == 'android' and
not self.internal_bisect):
api.chromium_android.common_tests_setup_steps(
perf_setup=True, remove_system_webview=True)
api.chromium.runhooks()
try:
# Run legacy bisect script if the patch contains bisect.cfg.
if BISECT_CONFIG_FILE in affected_files:
api.step('***LEGACY BISECT (deprecated)***', [])
self.run_bisect_script(**kwargs)
elif api.properties.get('bisect_config'):
# We can distinguish between a config for a full bisect vs a single
# test by checking for the presence of the good_revision key.
if api.properties.get('bisect_config').get('good_revision'):
api.step('***BISECT***', [])
local_bisect.perform_bisect(self, **flags) # pragma: no cover
else:
api.step('***SINGLE TEST (deprecated)***', [])
self.start_test_run_for_bisect(update_step, self.bot_db,
api.properties)
else:
api.step('***PERF TRYJOB***', [])
self.m.perf_try.start_perf_try_job(
api, affected_files, update_step, self.bot_db)
finally:
if api.chromium.c.TARGET_PLATFORM == 'android':
if self.internal_bisect: # pragma: no cover
api.chromium_android.init_and_sync(
gclient_config=api.chromium_android.c.internal_dir_name,
use_bot_update=True)
else:
self.ensure_checkout()
api.chromium_android.common_tests_final_steps()