blob: 41593be4428f1ae782c6e8f94aafb8a236a9beb7 [file] [log] [blame]
# Copyright 2019 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.
"""Defines the APIs that each supported project must implement."""
import json
import textwrap
from google.appengine.api import taskqueue
from google.appengine.ext import ndb
from findit_v2.services.gerrit_actions import GerritActions
from infra_api_clients.codereview import gerrit
from services import git
class ProjectAPI(object): # pragma: no cover.
# Default auto action messaging templates, override these constants for
# custom/project-specific messages.
REQUEST_REVIEW = textwrap.dedent("""\
Sheriffs, CL owner or CL reviewers:
Please submit this revert if it is correct.
If it is a false positive, please abandon and report it
at {bug_link}
For more information about Findit auto-revert: https://goo.gl/adB34D.""")
REQUEST_CONFIRMATION = textwrap.dedent("""\
Sheriffs, CL owner or CL reviewers:
Please confirm if this revert is correct.
If it is a false positive, please reland the original CL and report this
at {bug_link}
For more information about Findit auto-revert: https://goo.gl/adB34D.""")
ACTION_REASON = textwrap.dedent("""\
Findit (https://goo.gl/kROfz5) {verb} this CL at revision {revision} as
the culprit for failures in the continuous build including:
Sample Failed Build: {build}
Sample Failed Step: {step}
If it is a false positive, please report it at {bug_link}""")
def __init__(self, project_name=None):
self.project_name = project_name
self.gerrit_actions = GerritActions(self)
def ClassifyStepType(self, build, step):
""" Returns the failure type of the given build step.
Args:
build (buildbucket build.proto): ALL info about the build.
step (buildbucket step.proto): ALL info about the build step.
Returns:
findit_v2.services.failure_type.StepTypeEnum
"""
# pylint: disable=unused-argument
raise NotImplementedError
def GetCompileFailures(self, build, compile_steps):
"""Returns the detailed compile failures from a failed build.
Args:
build (buildbucket build.proto): ALL info about the build.
compile_steps (buildbucket step.proto): The failed compile steps.
Returns:
(dict): Information about detailed compile_failures.
{
'step_name': {
'failures': {
frozenset(['target1', 'target2']): {
'first_failed_build': {
'id': 8765432109,
'number': 123,
'commit_id': 654321
},
'last_passed_build': None,
'properties': {
# Arbitrary information about the failure if exists.
}
},
'first_failed_build': {
'id': 8765432109,
'number': 123,
'commit_id': 654321
},
'last_passed_build': None,
'properties': {
# Arbitrary information about the failure if exists.
}
},
}
"""
# pylint: disable=unused-argument
raise NotImplementedError
def GetTestFailures(self, build, test_steps):
"""Returns the detailed test failures from a failed build.
Args:
build (buildbucket build.proto): ALL info about the build.
test_steps (list of buildbucket step.proto): The failed test steps.
Returns:
(dict): Information about detailed test failures.
{
'step_name1': {
'failures': {
frozenset(['test_name']): {
'first_failed_build': {
'id': 8765432109,
'number': 123,
'commit_id': 654321
},
'last_passed_build': None,
'properties': {
# Arbitrary information about the failure if exists.
}
},
...
},
'first_failed_build': {
'id': 8765432109,
'number': 123,
'commit_id': 654321
},
'last_passed_build': None
'properties': None,
},
'step_name2': {
# No test level information.
'failures': {},
'first_failed_build': {
'id': 8765432109,
'number': 123,
'commit_id': 654321
},
'last_passed_build': None
'properties': {
# Arbitrary information about the failure if exists.
},
},
}
"""
# pylint: disable=unused-argument
raise NotImplementedError
def GetRerunDimensions(self, analyzed_build_id):
"""Dimensions for rerun builds.
Some projects may use a single re-run builder supporting multiple
configurations and dimensions, they may override this function to provide
custom dimensions to the rerun job request.
The expected return value is a list of dictionaries where each dictionary
has two key/value pairs describing one dimension. E.g.
[{
'key': 'os',
'value': 'Mac'
}, {
'key': ...,
'value': ...
}, ...]
"""
# pylint: disable=unused-argument
return None
def GetRerunBuilderId(self, build):
"""Gets builder id to run the rerun builds.
Args:
build (buildbucket build.proto): ALL info about the build.
Returns:
(str): Builder id in the format luci_project/luci_bucket/luci_builder
"""
# pylint: disable=unused-argument
raise NotImplementedError
def GetCompileRerunBuildInputProperties(self, failed_targets,
analyzed_build_id):
"""Gets input properties of a rerun build for compile failures.
Args:
failed_targets (list of str): Targets Findit wants to rerun in the build.
analyzed_build_id (int): Buildbucket Id of the analyzed build, may be used
to derive properties for reruns.
Returns:
(dict): input properties of the rerun build."""
# pylint: disable=unused-argument
return NotImplementedError
def GetFailuresWithMatchingCompileFailureGroups(
self, context, build, first_failures_in_current_build):
"""Gets reusable failure groups for given compile failure(s).
Args:
context (findit_v2.services.context.Context): Scope of the analysis.
build (buildbucket build.proto): ALL info about the build.
first_failures_in_current_build (dict): A dict for failures that happened
the first time in current build.
{
'failures': {
'step': {
'atomic_failures': [
frozenset(['target4']),
frozenset(['target1', 'target2'])],
'last_passed_build': {
'id': 8765432109,
'number': 122,
'commit_id': 'git_sha1'
},
},
},
'last_passed_build': {
# In this build all the failures that happened in the build being
# analyzed passed.
'id': 8765432108,
'number': 121,
'commit_id': 'git_sha0'
}
}
"""
# For projects that don't need to group failures (e.g. chromium), this is
# a no-op.
# pylint: disable=unused-argument
return {}
def GetFailuresWithMatchingTestFailureGroups(self, context, build,
first_failures_in_current_build):
"""Gets reusable failure groups for given test failure(s).
This method is a placeholder for projects that might need this feature,
though it's actually a no-op for currently supported projects:
+ For chromium, failure grouping is not required at the moment;
+ For chromeos, All tests are run on the same builder, this grouping is not
needed at all.
Args:
context (findit_v2.services.context.Context): Scope of the analysis.
build (buildbucket build.proto): ALL info about the build.
first_failures_in_current_build (dict): A dict for failures that happened
the first time in current build.
{
'failures': {
'step': {
'atomic_failures': ['test1', 'test2', ...],
'last_passed_build': {
'id': 8765432109,
'number': 122,
'commit_id': 'git_sha1'
},
},
},
'last_passed_build': {
# In this build all the failures that happened in the build being
# analyzed passed.
'id': 8765432108,
'number': 121,
'commit_id': 'git_sha0'
}
}
}
"""
# pylint: disable=unused-argument
return {}
def GetTestRerunBuildInputProperties(self, tests, analyzed_build_id):
"""Gets input properties of a rerun build for test failures.
Args:
tests (dict): Tests Findit wants to rerun in the build.
{
'step': {
'tests': [
{
'name': 'test',
'properties': {
# Properties for this test.
},
},
],
'properties': {
# Properties for this step.
},
},
}
analyzed_build_id (int): Buildbucket Id of the analyzed build, may be used
to derive properties for reruns.
Returns:
(dict): input properties of the rerun build."""
# pylint: disable=unused-argument
raise NotImplementedError
def GetFailureKeysToAnalyzeTestFailures(self, failure_entities):
"""Gets failures that'll actually be analyzed in the analysis."""
return [f.key for f in failure_entities]
def GetCompileFailureInfo(self, context, build,
first_failures_in_current_build):
"""Creates input object required by heuristic analysis for compile."""
# pylint: disable=unused-argument
return {}
def FailureShouldBeAnalyzed(self, failure_entity):
"""Checks if the failure is supposed to be analyzed."""
# pylint: disable=unused-argument
return True
def ClearSkipFlag(self, failure_entities):
"""For failures that were skipped on purpose then require to be analyzed,
updates them to be picked up by an analysis.
So far this is a special case for CrOS: CrOS can tell Findit to skip
analyzing a failed build if there are too many failures. Those failures
will have a flag in properties indicates that they don't need analysis.
But a following build with failures that need analysis might be merged into
some of the skipped failures, if so those particular failures need to update
to be analyzed.
"""
# pylint: disable=unused-argument
return
def GetTestFailureInfo(self, context, build, first_failures_in_current_build):
"""Creates input object required by heuristic analysis for test."""
# pylint: disable=unused-argument
return {}
def AsyncCommitRevert(self, culprit, revert_description,
request_confirmation_message):
"""Enqueues a task to revert a culprit and land it.
This is needed so that the wrapping transaction can complete without waiting
for an external RPC.
"""
self._ScheduleTask(
'CommitRevert', culprit, {
'revert_description': revert_description,
'request_confirmation_message': request_confirmation_message,
})
def AsyncRequestReview(self, culprit, revert_description,
request_review_message):
"""Enqueues a task to revert a culprit and request it to be reviewed.
This is needed so that the wrapping transaction can complete without waiting
for an external RPC.
"""
self._ScheduleTask(
'RequestReview', culprit, {
'revert_description': revert_description,
'request_review_message': request_review_message,
})
def AsyncNotifyCulprit(self, culprit, message, silent_notification=True):
"""Enqueues a task to notify a culprit.
This is needed so that the wrapping transaction can complete without waiting
for an external RPC.
"""
self._ScheduleTask('NotifyCulprit', culprit, {
'message': message,
'silent_notification': silent_notification,
})
def GetAutoRevertReviewers(self):
"""Returns a list of reviewers to be notified of automated reverts."""
# pylint: disable=unused-argument
return []
def _ScheduleTask(self, action, culprit, action_params):
"""Transactionally schedules a task to take an action later.
Args:
action (str): An action understood by findit_v2/handlers/async_action.py
culprit: Datastore entity for the culprit to take action on.
action_params: Parameters specific to the type of action as a
json-serializable dict.
"""
assert ndb.in_transaction()
payload = {
'project': self.project_name,
'action': action,
'culprit_key': culprit.key.urlsafe(),
}
payload.update(action_params)
taskqueue.add(
queue_name='findit-v2-actions',
url='/findit/internal/v2/task/async-action',
method='POST',
target='findit-backend',
payload=json.dumps(payload),
transactional=True,
)