blob: 115840e9c31e7c8bcad9022c43d1ff5729728b33 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Invokes git bisect to find culprit commit inside Chromium repository."""
from __future__ import print_function
import json
import re
from chromite.cli import flash
from chromite.cros_bisect import git_bisector
from chromite.lib import cros_logging as logging
from chromite.lib import git
from chromite.lib import gs
from chromite.lib import retry_util
REGEX_CROS_VERSION = re.compile(r'[Rr]?(\d+)[.-](\d+)\.(\d+)\.(\d+)$')
class ChromeOnCrosBisector(git_bisector.GitBisector):
"""Bisects offending commit in Chromium repository.
Before bisect, it extracts metric scores for both last-know-good and bad
commits, for verifying regression and for setting threshold telling good from
bad commit. If last-known-good and last-known-bad CrOS version is given, it
first checks if offending commit is in Chrome by deploying last-known-bad
CrOS version's Chrome to DUT with last-known-good CrOS image installed. If it
doesn't have regression, then the offending commit is not on Chrome side.
It finds first bad commit within given good and bad commit using "git bisect"
command. For each commit, it uses builder to build package to verify and
deploy it to DUT (device under test). And asks evaluator to extract score
of the commit. It is treated as good commit i the score is closer to user
specified good commit.
Finally, it outputs git bisect result.
"""
def __init__(self, options, builder, evaluator):
"""Constructor.
Args:
options: An argparse.Namespace to hold command line arguments. Should
contain:
* cros_flash_retry: Max retry for "cros flash" command.
* cros_flash_sleep: #seconds to wait between retry.
* cros_flash_backoff: backoff factor. Must be >=1. If backoff factor
is 1, sleep_duration = sleep * num_retry. Otherwise,
sleep_duration = sleep * (backoff_factor) ** (num_retry - 1)
builder: Builder to build/deploy image. Should contain repo_dir.
evaluator: Evaluator to get score
"""
super(ChromeOnCrosBisector, self).__init__(options, builder, evaluator)
self.cros_flash_retry = max(0, options.cros_flash_retry)
self.cros_flash_sleep = max(0, options.cros_flash_sleep)
self.cros_flash_backoff = max(1, options.cros_flash_backoff)
self.good_cros_version = None
self.bad_cros_version = None
self.bisect_between_cros_version = False
if not (git.IsSHA1(options.good, full=False) and
git.IsSHA1(options.bad, full=False)):
# Postpone commit resolution to Run().
self.good_commit = None
self.bad_commit = None
self.good_cros_version = options.good
self.bad_cros_version = options.bad
self.bisect_between_cros_version = True
# Used to access gs://. Lazy initialization.
self.gs_ctx = None
@staticmethod
def CheckCommitFormat(commit):
"""Checks if commit is the acceptable format.
It accepts either SHA1 or CrOS version.
Args:
commit: commit string.
Returns:
Normalized commit. None if the format is unacceptable.
"""
if git_bisector.GitBisector.CheckCommitFormat(commit):
return commit
match_obj = REGEX_CROS_VERSION.match(commit)
if match_obj:
return 'R%s-%s.%s.%s' % match_obj.groups()
return None
def ObtainBisectBoundaryScoreImpl(self, good_side):
"""The worker of obtaining score of either last-known-good or bad commit.
Instead of deploying Chrome for good/bad commit, it deploys good/bad
CrOS image if self.bisect_between_cros_version is set.
Args:
good_side: True if it evaluates score for last-known-good. False for
last-known-bad commit.
Returns:
Evaluated score.
"""
commit = self.good_commit if good_side else self.bad_commit
commit_label = 'good' if good_side else 'bad'
# Though bisect_between_cros_version uses archived image directly without
# building Chrome, it is necessary because BuildDeployEval() will update
# self.current_commit.
self.Git(['checkout', commit])
eval_label = None
customize_build_deploy = None
if self.bisect_between_cros_version:
cros_version = (self.good_cros_version if good_side else
self.bad_cros_version)
logging.notice('Obtaining score of %s CrOS version: %s', commit_label,
cros_version)
eval_label = 'cros_%s' % cros_version
customize_build_deploy = lambda: self.FlashCrosImage(
self.GetCrosXbuddyPath(cros_version))
else:
logging.notice('Obtaining score of %s commit: %s', commit_label, commit)
return self.BuildDeployEval(eval_label=eval_label,
customize_build_deploy=customize_build_deploy)
def GetCrosXbuddyPath(self, version):
"""Composes xbuddy path.
Args:
version: CrOS version to get.
Returns:
xbuddy path of the CrOS image for board.
"""
return 'xbuddy://remote/%s/%s/test' % (self.board, version)
def ExchangeChromeSanityCheck(self):
"""Exchanges Chrome between good and bad CrOS.
It deploys last-known-good Chrome to last-known-bad CrOS DUT and vice
versa to see if regression culprit is in Chrome.
"""
def FlashBuildDeploy(cros_version):
"""Flashes DUT first then builds/deploys Chrome."""
self.FlashCrosImage(self.GetCrosXbuddyPath(cros_version))
return self.BuildDeploy()
def Evaluate(cros_version, chromium_commit):
self.Git(['checkout', chromium_commit])
score = self.BuildDeployEval(
eval_label='cros_%s_cr_%s' % (cros_version, chromium_commit),
customize_build_deploy=lambda: FlashBuildDeploy(cros_version))
label = self.LabelBuild(score)
logging.notice('Score(mean: %.3f std: %.3f). Marked as %s',
score.mean, score.std, label)
return label
logging.notice('Sanity check: exchange Chrome between good and bad CrOS '
'version.')
# Expect bad result if culprit commit is inside Chrome.
logging.notice('Obtaining score of good CrOS %s with bad Chrome %s',
self.good_cros_version, self.bad_commit)
bad_chrome_on_good_cros_label = Evaluate(self.good_cros_version,
self.bad_commit)
self.current_commit.label = 'good_cros_bad_chrome'
# Expect bad result if culprit commit is inside Chrome.
logging.notice('Obtaining score of bad CrOS %s with good Chrome %s',
self.bad_cros_version, self.good_commit)
good_chrome_on_bad_cros_label = Evaluate(self.bad_cros_version,
self.good_commit)
self.current_commit.label = 'bad_cros_good_chrome'
if (bad_chrome_on_good_cros_label != 'bad' or
good_chrome_on_bad_cros_label != 'good'):
logging.error(
'After exchanging Chrome between good/bad CrOS image, found that '
'culprit commit should not be in Chrome repository.')
logging.notice(
'Bisect log:\n' +
'\n'.join(self.CommitInfoToStr(x) for x in self.bisect_log))
return False
return True
def FlashCrosImage(self, xbuddy_path):
"""Flashes CrOS image to DUT.
It returns True when it successfully flashes image to DUT. Raises exception
when it fails after retry.
Args:
xbuddy_path: xbuddy path to CrOS image to flash.
Returns:
True
Raises:
FlashError: An unrecoverable error occured.
"""
logging.notice('cros flash %s', xbuddy_path)
@retry_util.WithRetry(
self.cros_flash_retry, log_all_retries=True,
sleep=self.cros_flash_sleep,
backoff_factor=self.cros_flash_backoff)
def flash_with_retry():
flash.Flash(self.remote, xbuddy_path, board=self.board,
clobber_stateful=True, clear_tpm_owner=True,
disable_rootfs_verification=True)
flash_with_retry()
return True
def CrosVersionToChromeCommit(self, cros_version):
"""Resolves head commit of the Chrome used by the CrOS version.
Args:
cros_version: ChromeOS version, e.g. R60-9531.0.0.
Returns:
Chrome SHA. None if the ChromeOS version is not found.
"""
metadata_url = ('gs://chromeos-image-archive/%s-release/%s/'
'partial-metadata.json') % (self.board, cros_version)
try:
metadata_content = self.gs_ctx.Cat(metadata_url)
except gs.GSCommandError as e:
logging.error('Cannot load %s: %s', metadata_url, e)
return None
try:
metadata = json.loads(metadata_content)
except ValueError:
logging.error('Unable to parse %s', metadata_url)
return None
if (not metadata or 'version' not in metadata or
'chrome' not in metadata['version']):
logging.error('metadata["version"]["chrome"] does not exist in %s',
metadata_url)
return None
chrome_version = metadata['version']['chrome']
# Commit just before the branch point.
# Second line, first field.
result = self.Git(['log', '--oneline', '-n', '2', chrome_version])
if result.returncode != 0:
logging.error('Failed to run "git log %s": error: %s returncode:%s',
chrome_version, result.error, result.returncode)
return None
return result.output.splitlines()[1].split()[0]
def ResolveChromeBisectRangeFromCrosVersion(self):
"""Resolves Chrome bisect range given good and bad CrOS versions.
It sets up self.good_commit and self.bad_commit, which are derived from
self.good_cros_version and self.bad_cros_version, respectively.
Returns:
False if either good_commit or bad_commit failed to resolve. Otherwise,
True.
"""
self.good_commit = self.CrosVersionToChromeCommit(self.good_cros_version)
if self.good_commit:
logging.info('Latest Chrome commit of good CrOS version %s: %s',
self.good_cros_version, self.good_commit)
else:
logging.error('Cannot find metadata for CrOS version: %s',
self.good_cros_version)
return False
self.bad_commit = self.CrosVersionToChromeCommit(self.bad_cros_version)
if self.bad_commit:
logging.info('Latest Chrome commit of bad CrOS version %s: %s',
self.bad_cros_version, self.bad_commit)
else:
logging.error('Cannot find metadata for CrOS version: %s',
self.bad_cros_version)
return False
return True
def PrepareBisect(self):
"""Performs sanity checks and obtains bisect boundary score before bisect.
Returns:
False if there's something wrong.
"""
if self.bisect_between_cros_version:
# Lazy initialization.
self.gs_ctx = gs.GSContext()
self.builder.SyncToHead(fetch_tags=True)
if not self.ResolveChromeBisectRangeFromCrosVersion():
return None
if not (self.SanityCheck() and
self.ObtainBisectBoundaryScore() and
self.GetThresholdFromUser()):
return False
if self.bisect_between_cros_version:
if not self.ExchangeChromeSanityCheck():
return False
return True