blob: ae756e0b8d23acc71355d58649bd76c0470a87e9 [file] [log] [blame]
# Copyright 2017 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.
"""Logic related to examine builds and determine regression range."""
from collections import defaultdict
import logging
from common.findit_http_client import FinditHttpClient
from common.waterfall import failure_type
from libs import analysis_status
from model import analysis_approach_type
from model import result_status
from model.wf_analysis import WfAnalysis
from services import monitoring
from services import step_util
from services.parameters import FailureInfoBuild
from services.parameters import FailureInfoBuilds
from waterfall import build_util
from waterfall import buildbot
_MAX_BUILDS_TO_CHECK = 20
_SUPPORTED_FAILURE_TYPE = [failure_type.COMPILE, failure_type.TEST]
# TODO(crbug/842980): Deprecate blame_list in builds.
def _GetBlameListAndRevisionForBuild(build_info):
"""Gets blame list and chromium revision info for a build.
Args:
build_info (BuildInfo): a BuildInfo instance which contains blame list and
chromium revision.
"""
return {
'chromium_revision': build_info.chromium_revision,
'blame_list': build_info.blame_list
}
def _CreateADictOfFailedSteps(build_info):
""" Returns a dict with build number for failed steps.
Args:
failed_steps (list): a list of failed steps.
Returns:
A dict like this:
{
'step_name': {
'current_failure': 555,
'first_failure': 553,
'supported': True
},
}
"""
failed_steps = dict()
for step_name in build_info.failed_steps:
failed_steps[step_name] = {
'current_failure':
build_info.build_number,
'first_failure':
build_info.build_number,
'supported':
step_util.StepIsSupportedForMaster(
build_info.master_name, build_info.builder_name,
build_info.build_number, step_name)
}
return failed_steps
def _UpdateStringTypedBuildKeyToInt(builds):
"""Updates the string keys to int keys."""
updated_builds = FailureInfoBuilds()
for build_number, build in builds.iteritems():
updated_builds[int(build_number)] = build
return updated_builds
def CheckForFirstKnownFailure(master_name, builder_name, build_number,
failure_info):
"""Checks for first known failures of the given failed steps.
Args:
master_name (str): master of the failed build.
builder_name (str): builder of the failed build.
build_number (int): builder number of the current failed build.
failure_info (CompileFailureInfo, TestFailureInfo): information of the build
failure.
Returns:
failure_info (CompileFailureInfo, TestFailureInfo): updated failure_info.
"""
failed_steps = failure_info.failed_steps
failure_info.builds = _UpdateStringTypedBuildKeyToInt(failure_info.builds)
# Look back for first known failures.
for build_info in build_util.IteratePreviousBuildsFrom(
master_name, builder_name, build_number, _MAX_BUILDS_TO_CHECK):
# Extraction should stop when we reach to the first build.
failure_info.builds[build_info.build_number] = (
FailureInfoBuild.FromSerializable(
_GetBlameListAndRevisionForBuild(build_info)))
if build_info.result == buildbot.SUCCESS:
for step_name in failed_steps:
if failed_steps[step_name].last_pass is None:
failed_steps[step_name].last_pass = build_info.build_number
# All steps passed, so stop looking back.
return failure_info
else:
# If a step is not run due to some bot exception, we are not sure
# whether the step could pass or not. So we only check failed/passed
# steps here.
for step_name in build_info.failed_steps:
if (step_name in failed_steps and
failed_steps[step_name].last_pass is None):
failed_steps[step_name].first_failure = build_info.build_number
for step_name in failed_steps:
if (step_name in build_info.passed_steps and
failed_steps[step_name].last_pass is None):
failed_steps[step_name].last_pass = build_info.build_number
if all(step_info.last_pass is not None
for step_info in failed_steps.values()):
# All failed steps passed in this build cycle.
return failure_info
return failure_info
def GetBuildFailureInfo(master_name, builder_name, build_number):
"""Processes build info of a build and gets failure info.
This function will also update wf_analysis about the build's not passed steps
and failure type.
Args:
master_name (str): Master name of the build.
builder_name (str): Builder name of the build.
build_number (int): Number of the build.
Returns:
A dict of failure info and a flag for should start analysis.
"""
status_code, build_info = build_util.GetBuildInfo(master_name, builder_name,
build_number)
analysis = WfAnalysis.Get(master_name, builder_name, build_number)
assert analysis
# TODO(crbug/804617): Remove this when new LUCI API is ready.
if status_code == 404:
# Hits a build number gap. Should skip the analysis.
analysis.status = analysis_status.SKIPPED
analysis.result_status = result_status.NOT_FOUND_UNTRIAGED
analysis.put()
# Findit analysis doesn't start. At this point it doesn't matter which
# step failed, so use 'unknown' for all the step related information.
monitoring.OnWaterfallAnalysisStateChange(
master_name=master_name,
builder_name=builder_name,
failure_type='unknown',
canonical_step_name='unknown',
isolate_target_name='unknown',
status=analysis_status.STATUS_TO_DESCRIPTION[analysis_status.SKIPPED],
analysis_type=analysis_approach_type.STATUS_TO_DESCRIPTION[
analysis_approach_type.PRE_ANALYSIS])
return {}, False
if not build_info:
logging.error('Failed to extract build info for build %s/%s/%d',
master_name, builder_name, build_number)
analysis.status = analysis_status.ERROR
analysis.result_status = result_status.NOT_FOUND_UNTRIAGED
analysis.put()
monitoring.OnWaterfallAnalysisStateChange(
master_name=master_name,
builder_name=builder_name,
failure_type='unknown',
canonical_step_name='unknown',
isolate_target_name='unknown',
status=analysis_status.STATUS_TO_DESCRIPTION[analysis_status.ERROR],
analysis_type=analysis_approach_type.STATUS_TO_DESCRIPTION[
analysis_approach_type.PRE_ANALYSIS])
return {}, False
build_failure_type = build_util.GetFailureType(build_info)
failed = (
build_info.result != buildbot.SUCCESS and bool(build_info.failed_steps))
failure_info = {
'failed': failed,
'master_name': master_name,
'builder_name': builder_name,
'build_number': build_number,
'chromium_revision': build_info.chromium_revision,
'builds': {},
'failed_steps': {},
'failure_type': build_failure_type,
'parent_mastername': build_info.parent_mastername,
'parent_buildername': build_info.parent_buildername,
'is_luci': build_info.is_luci,
'buildbucket_bucket': build_info.buildbucket_bucket,
'buildbucket_id': build_info.buildbucket_id,
}
if (not failed or not build_info.chromium_revision or
build_failure_type not in _SUPPORTED_FAILURE_TYPE):
# No real failure or lack of required information, so no need to start
# an analysis.
analysis.status = analysis_status.COMPLETED
analysis.result_status = result_status.NOT_FOUND_UNTRIAGED
analysis.put()
monitoring.OnWaterfallAnalysisStateChange(
master_name=master_name,
builder_name=builder_name,
failure_type='unknown',
canonical_step_name='unknown',
isolate_target_name='unknown',
status=analysis_status.STATUS_TO_DESCRIPTION[analysis_status.COMPLETED],
analysis_type=analysis_approach_type.STATUS_TO_DESCRIPTION[
analysis_approach_type.PRE_ANALYSIS])
return failure_info, False
failure_info['builds'][build_info.build_number] = (
_GetBlameListAndRevisionForBuild(build_info))
failure_info['failed_steps'] = _CreateADictOfFailedSteps(build_info)
analysis.not_passed_steps = build_info.not_passed_steps
analysis.build_failure_type = build_failure_type
analysis.build_start_time = (
analysis.build_start_time or build_info.build_start_time)
analysis.put()
return failure_info, True
def GetLaterBuildsWithAnySameStepFailure(master_name,
builder_name,
build_number,
failed_steps=None):
"""Gets successive failed builds with the same failure as the referred build.
If failed_steps is provided, this function can drill down to step_level
as well.
The function will stop looking further and abandon all its findings if:
- find a non-failed build (build ends in success or warning) or
- find a build with non-overlapping failures.
"""
latest_build_numbers = buildbot.GetRecentCompletedBuilds(
master_name, builder_name, FinditHttpClient())
builds_with_same_failed_steps = defaultdict(list)
if not latest_build_numbers:
# Failed to get later builds, cannot check their failures. Returns an empty
# dict to skip actions on the culprit.
# This should be rare, possible cause is builder rename.
logging.error(
'Failed to get latest build numbers for builder %s/%s since %d.',
master_name, builder_name, build_number)
return {}
for newer_build_number in latest_build_numbers:
if newer_build_number <= build_number:
break
# Checks all builds after current build.
_, newer_build_info = build_util.GetBuildInfo(master_name, builder_name,
newer_build_number)
if newer_build_info and newer_build_info.result in [
buildbot.SUCCESS, buildbot.WARNINGS
]:
return {}
for failed_step in failed_steps or []:
if failed_step in newer_build_info.failed_steps:
builds_with_same_failed_steps[newer_build_number].append(failed_step)
if not builds_with_same_failed_steps[newer_build_number]:
# No same failed steps.
return {}
return builds_with_same_failed_steps
def GetGoodRevision(failure_info):
"""Gets the earliest passed revision for the failures.
Uses the chromium_revision of the earliest last_pass of failures, ignore the
failures with no last_pass.
Args:
failure_info(CompileFailureInfo, TestFailureInfo): Failure info.
Returns:
(str): chromium_revision of the earliest last_pass Findit found.
"""
earliest_last_pass_build = failure_info.build_number
for step_failure in failure_info.failed_steps.itervalues():
if (step_failure.last_pass and
step_failure.last_pass < earliest_last_pass_build):
earliest_last_pass_build = step_failure.last_pass
if (earliest_last_pass_build < failure_info.build_number and
failure_info.builds.get(earliest_last_pass_build)):
return failure_info.builds[earliest_last_pass_build].chromium_revision
return None