| # Copyright 2021 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Utility functions common to multiple recipes that don't fit elsewhere.""" |
| |
| from __future__ import annotations |
| |
| import dataclasses |
| import json |
| import re |
| from collections.abc import Sequence |
| from typing import Any |
| |
| from google.protobuf import json_format |
| |
| from recipe_engine import recipe_api |
| |
| from PB.go.chromium.org.luci.buildbucket.proto import build as build_pb |
| from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb |
| |
| |
| @dataclasses.dataclass |
| class ChangeWithComments: |
| change: common_pb.GerritChange |
| details: dict[str, Any] |
| commit_message: str |
| comments: Sequence[str] |
| |
| |
| class UtilApi(recipe_api.RecipeApi): |
| ChangeWithComments = ChangeWithComments |
| |
| def get_change_with_comments(self) -> ChangeWithComments: |
| input_: build_pb.Build.Input = self.m.buildbucket.build.input |
| change: common_pb.GerritChange = input_.gerrit_changes[0] |
| change_id: str = str(change.change) |
| details: dict[str, Any] = self.m.gerrit.change_details( |
| 'change details', |
| change_id=change_id, |
| host=input_.gerrit_changes[0].host, |
| query_params=['ALL_COMMITS', 'ALL_REVISIONS', 'ALL_FILES'], |
| test_data=self.m.json.test_api.output( |
| { |
| 'owner': { |
| 'email': 'coder@example.com', |
| }, |
| 'current_revision': 'a' * 40, |
| 'revisions': { |
| 'a' * 40: { |
| 'files': [], |
| 'commit': { |
| 'message': '', |
| }, |
| 'description': 'description', |
| } |
| }, |
| 'revert_of': 0, |
| } |
| ), |
| ).json.output |
| |
| current_revision: str = details['revisions'][ |
| details['current_revision'] |
| ] |
| commit_message: str = current_revision['commit']['message'] |
| |
| comments: list[str] = [] |
| |
| for revision in details['revisions'].values(): |
| if revision.get('description'): |
| comments.append(revision['description']) |
| |
| comments_result: dict[str, Any] = self.m.gerrit.list_change_comments( |
| 'list change comments', |
| change_id, |
| test_data=self.m.json.test_api.output( |
| { |
| '/PATCHSET_LEVEL': [{'message': ''}], |
| } |
| ), |
| ).json.output |
| |
| for _, comment_data in comments_result.items(): |
| comments.extend(x['message'] for x in comment_data) |
| |
| return ChangeWithComments(change, details, commit_message, comments) |
| |
| def find_matching_comment( |
| self, rx: re.Pattern, comments: Sequence[str] |
| ) -> str | None: |
| """Find a comment in comments that matches regex object rx.""" |
| result: str | None = None |
| with self.m.step.nest('checking comments'): |
| for i, comment in enumerate(comments): |
| with self.m.step.nest(f'comment ({i})') as pres: |
| pres.step_summary_text = comment |
| match: re.Match = re.search(rx, comment) |
| if match: |
| pres.step_summary_text = f'MATCH: {comment}' |
| result: str = match.group(0) |
| break |
| |
| if result: |
| with self.m.step.nest('found'): |
| pass |
| |
| return result |
| |
| def build_metadata(self) -> dict[str, Any]: |
| return { |
| 'bb_id': self.m.buildbucket.build.id, |
| 'swarming_id': self.m.swarming.task_id, |
| 'builder': self.m.buildbucket_util.full_builder_name(), |
| 'url': self.m.buildbucket.build_url(), |
| 'triggers': [ |
| json.loads(json_format.MessageToJson(x)) |
| for x in self.m.scheduler.triggers |
| ], |
| } |
| |
| def on_last_iteration( |
| self, |
| poll_interval_secs: int, |
| *, |
| debug: bool = False, |
| ) -> bool: |
| remaining = self._secs_remaining_in_build() |
| if debug: |
| pres = self.m.step.empty('secs remaining in build').presentation |
| pres.step_summary_text = f'{remaining}' |
| |
| pres = self.m.step.empty('poll interval secs').presentation |
| pres.step_summary_text = f'{poll_interval_secs}' |
| |
| return remaining < poll_interval_secs * 3 // 2 |
| |
| def _secs_remaining_in_build(self) -> float: |
| current_time = self.m.time.time() |
| elapsed_time = ( |
| current_time - self.m.buildbucket.build.start_time.seconds |
| ) |
| remaining = ( |
| self.m.buildbucket.build.execution_timeout.seconds - elapsed_time |
| ) |
| |
| # If a test isn't fully specified we might get some inconsistent results. |
| # Ignore those cases. |
| if remaining < -1337000000: |
| remaining = 10000 |
| |
| return remaining |