| # Copyright 2020 The Fuchsia 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 past.builtins import long | |
| from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2 | |
| from RECIPE_MODULES.fuchsia.utils import pluralize | |
| class DisplayUtilApi(recipe_api.RecipeApi): | |
| """Module to display buildbucket or swarming tasks as steps.""" | |
| def display_subbuilds(self, step_name, subbuilds, raise_on_failure=False): | |
| """Display build links and status for each input build. | |
| Optionally raise on build failure(s). | |
| Args: | |
| step_name (str): Name of build group to display in step name. | |
| builds (seq(buildbucket.v2.Build)): buildbucket Build objects. See | |
| recipe_engine/buildbucket recipe module for more info. | |
| raise_on_failure (bool): Raise InfraFailure or StepFailure on failure. | |
| Raises: | |
| InfraFailure: One or more input builds had infra failure. Takes priority | |
| over step failures. | |
| StepFailure: One or more of input builds failed. | |
| """ | |
| # List of failed builds | |
| infra_failures = [] | |
| failures = [] | |
| # Create per-build display steps. | |
| infra_failure_states = [ | |
| common_pb2.Status.Value('INFRA_FAILURE'), | |
| common_pb2.Status.Value('CANCELED') | |
| ] | |
| with self.m.step.nest(step_name) as presentation: | |
| for id_name, build in subbuilds.items(): | |
| with self.m.step.nest(build.build_name) as display_step: | |
| step_links = display_step.links | |
| step_links[str(build.build_id)] = build.url | |
| if build.build_proto.status == common_pb2.Status.Value('SUCCESS'): | |
| display_step.status = self.m.step.SUCCESS | |
| elif build.build_proto.status in infra_failure_states: | |
| display_step.status = self.m.step.EXCEPTION | |
| infra_failures.append(build) | |
| elif build.build_proto.status == common_pb2.Status.Value('FAILURE'): | |
| display_step.status = self.m.step.FAILURE | |
| failures.append(build) | |
| # For any other status, use warning color. | |
| else: | |
| display_step.status = self.m.step.WARNING | |
| def summary_section(build): | |
| url = self.m.buildbucket.build_url(build_id=build.build_id) | |
| failure_header = "[%s](%s)" % (build.build_name, url) | |
| if build.build_proto.status == common_pb2.INFRA_FAILURE: | |
| failure_header += " (infra failure)" | |
| summary = build.build_proto.summary_markdown.strip() | |
| # Don't include an empty summary. | |
| if not summary: | |
| return failure_header | |
| return failure_header + ":\n\n%s" % summary | |
| failure_message_parts = [] | |
| for b in infra_failures + failures: | |
| failure_message_parts.append(summary_section(b)) | |
| if raise_on_failure: | |
| # If there were any infra failures, raise purple. | |
| if infra_failures: | |
| presentation.status = self.m.step.EXCEPTION | |
| exception_type = self.m.step.InfraFailure | |
| # Otherwise if there were any step failures, raise red. | |
| elif failures: | |
| presentation.status = self.m.step.FAILURE | |
| exception_type = self.m.step.StepFailure | |
| else: | |
| return | |
| num_failed = len(failures) + len(infra_failures) | |
| raise exception_type( | |
| "%s failed:\n\n%s" % ( | |
| pluralize("build", | |
| num_failed), "\n\n".join(failure_message_parts) | |
| ) | |
| ) | |
| def display_builds(self, step_name, builds, raise_on_failure=False): | |
| """Display build links and status for each input build. | |
| Optionally raise on build failure(s). | |
| Args: | |
| step_name (str): Name of build group to display in step name. | |
| builds (seq(buildbucket.v2.Build)): buildbucket Build objects. See | |
| recipe_engine/buildbucket recipe module for more info. | |
| raise_on_failure (bool): Raise InfraFailure or StepFailure on failure. | |
| Raises: | |
| InfraFailure: One or more input builds had infra failure. Takes priority | |
| over step failures. | |
| StepFailure: One or more of input builds failed. | |
| """ | |
| # List of failed builds | |
| infra_failures = [] | |
| failures = [] | |
| # Create per-build display steps. | |
| infra_failure_states = [ | |
| common_pb2.Status.Value('INFRA_FAILURE'), | |
| common_pb2.Status.Value('CANCELED') | |
| ] | |
| with self.m.step.nest(step_name) as presentation: | |
| for k in builds: | |
| build = builds[k] if isinstance(k, long) or isinstance(k, int) else k | |
| with self.m.step.nest(build.builder.builder) as display_step: | |
| step_links = display_step.links | |
| step_links[str(build.id) | |
| ] = self.m.buildbucket.build_url(build_id=build.id) | |
| if build.status == common_pb2.Status.Value('SUCCESS'): | |
| display_step.status = self.m.step.SUCCESS | |
| elif build.status in infra_failure_states: | |
| display_step.status = self.m.step.EXCEPTION | |
| infra_failures.append(build) | |
| elif build.status == common_pb2.Status.Value('FAILURE'): | |
| display_step.status = self.m.step.FAILURE | |
| failures.append(build) | |
| # For any other status, use warning color. | |
| else: | |
| display_step.status = self.m.step.WARNING | |
| def summary_section(build): | |
| url = self.m.buildbucket.build_url(build_id=build.id) | |
| failure_header = "[%s](%s)" % (build.builder.builder, url) | |
| if build.status == common_pb2.INFRA_FAILURE: | |
| failure_header += " (infra failure)" | |
| summary = build.summary_markdown.strip() | |
| # Don't include an empty summary. | |
| if not summary: | |
| return failure_header | |
| return failure_header + ":\n\n%s" % summary | |
| failure_message_parts = [] | |
| for b in infra_failures + failures: | |
| failure_message_parts.append(summary_section(b)) | |
| if raise_on_failure: | |
| # If there were any infra failures, raise purple. | |
| if infra_failures: | |
| presentation.status = self.m.step.EXCEPTION | |
| exception_type = self.m.step.InfraFailure | |
| # Otherwise if there were any step failures, raise red. | |
| elif failures: | |
| presentation.status = self.m.step.FAILURE | |
| exception_type = self.m.step.StepFailure | |
| else: | |
| return | |
| num_failed = len(failures) + len(infra_failures) | |
| raise exception_type( | |
| "%s failed:\n\n%s" % ( | |
| pluralize("build", | |
| num_failed), "\n\n".join(failure_message_parts) | |
| ) | |
| ) | |
| def display_tasks(self, step_name, results, metadata, raise_on_failure=False): | |
| """Display task links and status for each input task. | |
| Optionally raise on build failure(s). | |
| Args: | |
| step_name (str): Name of build group to display in step name. | |
| results (seq(swarming.TaskResult)): swarming TaskResult objects. See | |
| recipe_engine/swarming recipe module for more info. | |
| metadata (seq(swarming.TaskMetadata)): swarming TaskMetadata objects. See | |
| recipe_engine/swarming recipe module for more info. | |
| raise_on_failure (bool): Raise InfraFailure or StepFailure on failure. | |
| Raises: | |
| InfraFailure: One or more input builds had infra failure. Takes priority | |
| over step failures. | |
| StepFailure: One or more of input builds failed. | |
| """ | |
| self._display( | |
| step_name=step_name, | |
| builds=results, | |
| raise_on_failure=raise_on_failure, | |
| process_func=self._process_task, | |
| metadata=metadata, | |
| ) | |
| def _process_task( | |
| self, result, infra_failed_builders, failed_builders, links | |
| ): | |
| """Process a single swarming.TaskResult. | |
| Args: | |
| result (swarming.TaskResult): A swarming TaskResult object. | |
| infra_failed_builders (List(str)): A list of the builder names with infra failures. | |
| failed_builders (List(str)): A list of the builder names with failures. | |
| links (Dict): A dictionary with the task links as values and the task id as keys. | |
| """ | |
| with self.m.step.nest(result.name) as display_step: | |
| step_links = display_step.links | |
| step_links[str(result.id)] = links[result.id] | |
| if (result.state is None or | |
| result.state != self.m.swarming.TaskState.COMPLETED): | |
| display_step.status = self.m.step.EXCEPTION | |
| infra_failed_builders.append(result.name) | |
| elif not result.success: | |
| display_step.status = self.m.step.FAILURE | |
| failed_builders.append(result.name) | |
| else: | |
| display_step.status = self.m.step.WARNING | |
| def _display( | |
| self, | |
| step_name, | |
| builds, | |
| process_func, | |
| raise_on_failure=False, | |
| metadata=None | |
| ): | |
| """Display build links and status for each input build. | |
| Optionally raise on build failure(s). | |
| Args: | |
| step_name (str): Name of build group to display in step name. | |
| builds (seq(buildbucket.v2.Build) or seq(swarming.TaskResult)): buildbucket Build or swarming TaskResult objects. See | |
| recipe_engine/buildbucket or recipe_engine/swarming recipe module for more info. | |
| process_func (Runnable): A function to process a build or task result object. | |
| raise_on_failure (bool): Raise InfraFailure or StepFailure on failure. | |
| metadata (seq(swarming.TaskMetadata)): swarming TaskMetadata objects. See | |
| recipe_engine/swarming recipe module for more info. | |
| Raises: | |
| InfraFailure: One or more input builds had infra failure. Takes priority | |
| over step failures. | |
| StepFailure: One or more of input builds failed. | |
| """ | |
| infra_failed_builders = [] | |
| failed_builders = [] | |
| # Create per-build display steps. | |
| with self.m.step.nest(step_name): | |
| for k in builds: | |
| build = builds[k] if isinstance(k, (long, int)) else k | |
| args = { | |
| "result": build, | |
| "infra_failed_builders": infra_failed_builders, | |
| "failed_builders": failed_builders, | |
| } | |
| if metadata: | |
| args["links"] = {m.id: m.task_ui_link for m in metadata} | |
| process_func(**args) | |
| if raise_on_failure: | |
| # Construct failure header and message. Include both types of failures, | |
| # regardless of whether we raise purple or red. | |
| failure_header = "build(s) failed" | |
| failure_message = [] | |
| if infra_failed_builders: | |
| failure_message.append( | |
| "infra failures: {infra_failed_builders}".format( | |
| infra_failed_builders=", ".join(infra_failed_builders) | |
| ) | |
| ) | |
| if failed_builders: | |
| failure_message.append( | |
| "step failures: {failed_builders}".format( | |
| failed_builders=", ".join(failed_builders) | |
| ) | |
| ) | |
| failure_message = ", ".join(failure_message) | |
| # If there were any infra failures, raise purple. | |
| if infra_failed_builders: | |
| self.m.step.empty( | |
| failure_header, | |
| status=self.m.step.INFRA_FAILURE, | |
| step_text=failure_message, | |
| ) | |
| # Otherwise if there were any step failures, raise red. | |
| if failed_builders: | |
| self.m.step.empty( | |
| failure_header, | |
| status=self.m.step.FAILURE, | |
| step_text=failure_message, | |
| ) |