| # 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 rpc as rpc_pb2 |
| |
| |
| class BisectApi(recipe_api.RecipeApi): |
| |
| def _get_reason(self): |
| return self.m.properties['bisect_reason'] |
| |
| def is_bisecting(self): |
| return 'bisect_reason' in self.m.properties |
| |
| def schedule(self, repo_url, reason): |
| if self.is_bisecting(): |
| bisect_reason = self._get_reason() |
| if bisect_reason == reason: |
| self._bisect_older(bisect_reason) |
| else: |
| self._bisect_newer(bisect_reason) |
| # The build failed for a different reason, fan out to find the root |
| # cause of that failure as well. |
| self._bisect_older(reason) |
| else: |
| self._start_bisection(repo_url, reason) |
| |
| def _start_bisection(self, repo_url, reason): |
| 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 = rpc_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] |
| self._bisect(commits, reason) |
| |
| def _bisect(self, commits, reason): |
| 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 |
| request = self.m.buildbucket.schedule_request( |
| builder=builder.builder, |
| project=builder.project, |
| bucket=builder.bucket, |
| properties={ |
| 'bisect_newer': newer, |
| 'bisect_older': older, |
| 'bisect_reason': reason |
| }, |
| gitiles_commit=middle_commit, |
| inherit_buildsets=False, |
| ) |
| self.m.buildbucket.schedule([request], |
| step_name='schedule bisect (%s)' % middle) |
| |
| def _bisect_newer(self, reason): |
| self._bisect(list(self.m.properties.get('bisect_newer', [])), reason) |
| |
| def _bisect_older(self, reason): |
| self._bisect(list(self.m.properties.get('bisect_older', [])), reason) |