| # 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) |