blob: f2da98afa382246153e348d11617b846aa8f80ee [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'
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 _start_bisection(self, repo_url, reason, is_experimental):
current_rev = self.m.buildbucket.gitiles_commit.id
if not current_rev:
return
# Search for previous builds created by the Luci scheduler (to exclude
# bisection builds). The TimeRange includes all builds from start_time
# (defaults to 0) to end_time (exclusive). Because builds are ordered by
# create_time, the first result will be the previous build.
create_time = common_pb2.TimeRange(
end_time=self.m.buildbucket.build.create_time)
builder = self.m.buildbucket.build.builder
search_predicate = builds_service_pb2.BuildPredicate(
builder=builder,
create_time=create_time,
tags=[common_pb2.StringPair(key='user_agent', value='luci-scheduler')])
result = self.m.buildbucket.search(
search_predicate, limit=1, step_name='fetch previous build')
if not result or len(result) == 0:
# There is no previous build: do not bisect on new builders.
return
previous_build = result[0]
if previous_build.status == common_pb2.FAILURE:
# Do not bisect if the previous build failed.
# TODO(athom): Check if the failure reason is the same by adding
# 'builds.*.summaryMarkdown' to the field mask.
return
previous_rev = previous_build.input.gitiles_commit.id
# We're intentionally not paging through the log to avoid bisecting an
# excessive number of commits.
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]
base_build = previous_build.number
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)