blob: 572b999322e18f436c037920380c19f0a7d08a34 [file] [log] [blame]
# Copyright (c) 2020, the Dart project authors. All rights reserved. Use of this
# source code is governed by a BSD-style license that can be found in the
# LICENSE file.
from recipe_engine import recipe_api
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
from PB.go.chromium.org.luci.buildbucket.proto import builds_service as builds_service_pb2
class BisectApi(recipe_api.RecipeApi):
REASON_SUCCESS = 'SUCCESS'
FETCH_BUILDS_STEP_NAME = 'list previous builds'
FIND_BASE_BUILD_STEP_NAME = 'find bisection base build'
def _get_reason(self):
return self.m.properties['bisect_reason']
def get_base_build(self):
return self.m.properties['bisect_base_build']
def is_bisecting(self):
return 'bisect_reason' in self.m.properties
@property
def is_enabled(self): # pragma: no cover
return self.m.properties.get('bisection_enabled', False)
def schedule(self, repo_url, reason, is_experimental=False):
if self.is_bisecting():
base_build = self.get_base_build()
bisect_reason = self._get_reason()
assert bisect_reason != self.REASON_SUCCESS
if bisect_reason == reason:
self._bisect_older(bisect_reason, is_experimental, base_build)
elif reason == self.REASON_SUCCESS:
self._bisect_newer(bisect_reason, is_experimental, base_build)
else:
self._bisect_newer(bisect_reason, is_experimental, base_build)
# The build failed for a different reason, fan out to find the root
# cause of that failure as well.
self._bisect_older(reason, is_experimental, base_build)
else:
self._start_bisection(repo_url, reason, is_experimental)
def _find_base_build(self):
""" Returns the most recent regular build if that build was successful,
and `None` otherwise. """
builder = self.m.buildbucket.build.builder
create_time = common_pb2.TimeRange(
end_time=self.m.buildbucket.build.create_time)
search_predicate = builds_service_pb2.BuildPredicate(
builder=builder, create_time=create_time)
result = self.m.buildbucket.search(
search_predicate, limit=100, step_name=BisectApi.FETCH_BUILDS_STEP_NAME)
if not result:
# No builds found.
return None
for candidate in result:
if 'bisect_reason' not in candidate.output.properties:
if candidate.status == common_pb2.SUCCESS:
# Found a matching build.
return candidate
# The previous regular build was not successful, do not start a
# bisection.
return None
return None
def _start_bisection(self, repo_url, reason, is_experimental):
current_rev = self.m.buildbucket.gitiles_commit.id
if not current_rev:
return
with self.m.step.nest(BisectApi.FIND_BASE_BUILD_STEP_NAME):
previous_build = self._find_base_build()
if not previous_build:
return
base_build = previous_build.number
previous_rev = previous_build.output.properties['got_revision']
assert previous_rev, "Build does not have a commit"
commits, _ = self.m.gitiles.log(
url=repo_url, ref='%s..%s' % (previous_rev, current_rev))
commits = commits[1:] # The first commit is the current_rev
commits = [commit['commit'] for commit in commits]
self._bisect(commits, reason, is_experimental, base_build)
def _bisect(self, commits, reason, is_experimental, base_build):
if len(commits) == 0:
# Nothing more to bisect.
return
middle_index = len(commits) // 2
newer = commits[:middle_index]
middle = commits[middle_index]
older = commits[middle_index + 1:]
commit = self.m.buildbucket.gitiles_commit
middle_commit = common_pb2.GitilesCommit()
middle_commit.CopyFrom(commit)
middle_commit.id = middle
builder = self.m.buildbucket.build.builder
properties = {
'bisection_enabled': True,
'bisect_newer': newer,
'bisect_older': older,
'bisect_reason': reason,
'bisect_base_build': base_build,
}
schedule_experimental = (
True if is_experimental else self.m.buildbucket.INHERIT)
request = self.m.buildbucket.schedule_request(
builder=builder.builder,
project=builder.project,
bucket=builder.bucket,
properties=properties,
gitiles_commit=middle_commit,
inherit_buildsets=False,
experimental=schedule_experimental,
)
self.m.buildbucket.schedule([request],
step_name='schedule bisect (%s)' % middle)
def _bisect_newer(self, reason, is_experimental, base_build):
self._bisect(
list(self.m.properties.get('bisect_newer', [])), reason,
is_experimental, base_build)
def _bisect_older(self, reason, is_experimental, base_build):
self._bisect(
list(self.m.properties.get('bisect_older', [])), reason,
is_experimental, base_build)