| # Copyright 2017 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Module to manage stage failure messages.""" |
| |
| import collections |
| import json |
| import logging |
| import re |
| |
| |
| # Currently, an exception is reported to CIDB failureTabe using the exception |
| # class name as the exception_type. failure_message_lib.FailureMessageManager |
| # uses the exception_type to decide which StageFailureMessage class to use |
| # to rebuild the failure message. Whenever you need to change the names of these |
| # classes, please add the new class names to their corresponding type lists, |
| # and DO NOT remove the old class names from the type lists. |
| # TODO (nxia): instead of using the class name as the exception type when |
| # reporting an exception to CIDB, we need to have an attribute like |
| # EXCEPTION_CATEGORY (say EXCEPTION_TYPE) and this type cannot be changed or |
| # removed from EXCEPTION_TYPE_LIST. But we can add new types to the list. |
| BUILD_SCRIPT_FAILURE_TYPES = ("BuildScriptFailure",) |
| PACKAGE_BUILD_FAILURE_TYPES = ("PackageBuildFailure",) |
| |
| |
| # These keys must exist as column names from failureView in cidb. |
| FAILURE_KEYS = ( |
| "id", |
| "build_stage_id", |
| "outer_failure_id", |
| "exception_type", |
| "exception_message", |
| "exception_category", |
| "extra_info", |
| "timestamp", |
| "stage_name", |
| "board", |
| "stage_status", |
| "build_id", |
| "master_build_id", |
| "builder_name", |
| "build_number", |
| "build_config", |
| "build_status", |
| "important", |
| "buildbucket_id", |
| ) |
| |
| |
| # A namedtuple containing values fetched from CIDB failureView. |
| _StageFailure = collections.namedtuple("_StageFailure", FAILURE_KEYS) |
| |
| |
| class StageFailure(_StageFailure): |
| """A class presenting values of a failure fetched from CIDB failureView.""" |
| |
| @classmethod |
| def GetStageFailureFromMessage(cls, stage_failure_message): |
| """Create StageFailure from a StageFailureMessage instance. |
| |
| Args: |
| stage_failure_message: An instance of StageFailureMessage. |
| |
| Returns: |
| An instance of StageFailure. |
| """ |
| return StageFailure( |
| stage_failure_message.failure_id, |
| stage_failure_message.build_stage_id, |
| stage_failure_message.outer_failure_id, |
| stage_failure_message.exception_type, |
| stage_failure_message.exception_message, |
| stage_failure_message.exception_category, |
| stage_failure_message.extra_info, |
| None, |
| stage_failure_message.stage_name, |
| None, |
| None, |
| None, |
| None, |
| None, |
| None, |
| None, |
| None, |
| None, |
| None, |
| ) |
| |
| @classmethod |
| def GetStageFailureFromDicts(cls, failure_dict, stage_dict, build_dict): |
| """Get StageFailure from value dictionaries. |
| |
| Args: |
| failure_dict: A dict presenting values of a tuple from failureTable. |
| stage_dict: A dict presenting values of a tuple from |
| buildStageTable. |
| build_dict: A dict presenting values of a tuple from buildTable. |
| |
| Returns: |
| An instance of StageFailure. |
| """ |
| return StageFailure( |
| failure_dict["id"], |
| failure_dict["build_stage_id"], |
| failure_dict["outer_failure_id"], |
| failure_dict["exception_type"], |
| failure_dict["exception_message"], |
| failure_dict["exception_category"], |
| failure_dict["extra_info"], |
| failure_dict["timestamp"], |
| stage_dict["name"], |
| stage_dict["board"], |
| stage_dict["status"], |
| build_dict["id"], |
| build_dict["master_build_id"], |
| build_dict["builder_name"], |
| build_dict["build_number"], |
| build_dict["build_config"], |
| build_dict["status"], |
| build_dict["important"], |
| build_dict["buildbucket_id"], |
| ) |
| |
| |
| class StageFailureMessage: |
| """Message class contains information of a general stage failure. |
| |
| Failed stages report stage failures to CIDB failureTable (see more details |
| in failures_lib.ReportStageFailure). This class constructs a failure |
| message instance from the stage failure information stored in CIDB. |
| """ |
| |
| def __init__( |
| self, stage_failure, extra_info=None, stage_prefix_name=None |
| ) -> None: |
| """Construct a StageFailureMessage instance. |
| |
| Args: |
| stage_failure: An instance of StageFailure. |
| extra_info: The extra info of the origin failure, default to None. |
| stage_prefix_name: The prefix name (string) of the failed stage, |
| default to None. |
| """ |
| self.failure_id = stage_failure.id |
| self.build_stage_id = stage_failure.build_stage_id |
| self.stage_name = stage_failure.stage_name |
| self.exception_type = stage_failure.exception_type |
| self.exception_message = stage_failure.exception_message |
| self.exception_category = stage_failure.exception_category |
| self.outer_failure_id = stage_failure.outer_failure_id |
| |
| if extra_info is not None: |
| self.extra_info = extra_info |
| else: |
| # No extra_info provided, decode extra_info from stage_failure. |
| self.extra_info = self._DecodeExtraInfo(stage_failure.extra_info) |
| |
| if stage_prefix_name is not None: |
| self.stage_prefix_name = stage_prefix_name |
| else: |
| # No stage_prefix_name provided, extra prefix name from |
| # stage_failure. |
| self.stage_prefix_name = self._ExtractStagePrefixName( |
| self.stage_name |
| ) |
| |
| def __str__(self) -> str: |
| return ( |
| "[failure id] %s [stage name] %s [stage prefix name] %s " |
| "[exception type] %s [exception category] %s [exception message] %s" |
| " [extra info] %s" |
| % ( |
| self.failure_id, |
| self.stage_name, |
| self.stage_prefix_name, |
| self.exception_type, |
| self.exception_category, |
| self.exception_message, |
| self.extra_info, |
| ) |
| ) |
| |
| def _DecodeExtraInfo(self, extra_info): |
| """Decode extra info json into dict. |
| |
| Args: |
| extra_info: The extra_info of the origin exception, default to None. |
| |
| Returns: |
| An empty dict if extra_info is None; extra_info itself if extra_info |
| is a dict; else, load the json string into a dict and return it. |
| """ |
| if not extra_info: |
| return {} |
| elif isinstance(extra_info, dict): |
| return extra_info |
| else: |
| try: |
| return json.loads(extra_info) |
| except ValueError as e: |
| logging.error("Cannot decode extra_info: %s", e) |
| return {} |
| |
| # TODO(nxia): Force format checking on stage names when they're created |
| def _ExtractStagePrefixName(self, stage_name): |
| """Extract stage prefix name given a full stage name. |
| |
| Format examples in our current CIDB buildStageTable: |
| HWTest [bvt-arc] -> HWTest |
| HWTest -> HWTest |
| ImageTest -> ImageTest |
| ImageTest [amd64-generic] -> ImageTest |
| VMTest (attempt 1) -> VMTest |
| VMTest [amd64-generic] (attempt 1) -> VMTest |
| |
| Args: |
| stage_name: The full stage name (string) recorded in CIDB. |
| |
| Returns: |
| The prefix stage name (string). |
| """ |
| pattern = r"([^ ]+)( +\[([^]]+)\])?( +\(([^)]+)\))?" |
| m = re.compile(pattern).match(stage_name) |
| if m is not None: |
| return m.group(1) |
| else: |
| return stage_name |
| |
| |
| class BuildScriptFailureMessage(StageFailureMessage): |
| """Message class contains information of a BuildScriptFailure.""" |
| |
| def GetShortname(self): |
| """Return the short name (string) of the run command.""" |
| return self.extra_info.get("shortname") |
| |
| |
| class PackageBuildFailureMessage(StageFailureMessage): |
| """Message class contains information of a PackagebuildFailure.""" |
| |
| def GetShortname(self): |
| """Return the short name (string) of the run command.""" |
| return self.extra_info.get("shortname") |
| |
| def GetFailedPackages(self): |
| """Return a list of packages (strings) that failed to build.""" |
| return self.extra_info.get("failed_packages", []) |
| |
| |
| class CompoundFailureMessage(StageFailureMessage): |
| """Message class contains information of a CompoundFailureMessage.""" |
| |
| def __init__(self, stage_failure, **kwargs) -> None: |
| """Construct a CompoundFailureMessage instance. |
| |
| Args: |
| stage_failure: An instance of StageFailure. |
| **kwargs: Extra message information to pass to StageFailureMessage. |
| """ |
| super().__init__(stage_failure, **kwargs) |
| |
| self.inner_failures = [] |
| |
| def __str__(self) -> str: |
| msg_str = super().__str__() |
| |
| for failure in self.inner_failures: |
| msg_str += "(Inner Stage Failure Message) %s" % str(failure) |
| |
| return msg_str |
| |
| @staticmethod |
| def GetFailureMessage(failure_message): |
| """Convert a regular failure message instance to CompoundFailureMessage. |
| |
| Args: |
| failure_message: An instance of StageFailureMessage. |
| |
| Returns: |
| A CompoundFailureMessage instance. |
| """ |
| return CompoundFailureMessage( |
| StageFailure.GetStageFailureFromMessage(failure_message), |
| extra_info=failure_message.extra_info, |
| stage_prefix_name=failure_message.stage_prefix_name, |
| ) |
| |
| def HasEmptyList(self): |
| """Check whether the inner failure list is empty. |
| |
| Returns: |
| True if self.inner_failures is empty; else, False. |
| """ |
| return not bool(self.inner_failures) |
| |
| def HasExceptionCategories(self, exception_categories): |
| """Check if any of the inner failures matches the exception categories. |
| |
| Args: |
| exception_categories: A set of exception categories (members of |
| constants.EXCEPTION_CATEGORY_ALL_CATEGORIES). |
| |
| Returns: |
| True if any of the inner failures matches a member in |
| exception_categories; else, False. |
| """ |
| return any( |
| x.exception_category in exception_categories |
| for x in self.inner_failures |
| ) |
| |
| def MatchesExceptionCategories(self, exception_categories): |
| """Check if all the inner failures matches the exception categories. |
| |
| Args: |
| exception_categories: A set of exception categories (members of |
| constants.EXCEPTION_CATEGORY_ALL_CATEGORIES). |
| |
| Returns: |
| True if all the inner failures match a member in |
| exception_categories; else, False. |
| """ |
| return not self.HasEmptyList() and all( |
| x.exception_category in exception_categories |
| for x in self.inner_failures |
| ) |
| |
| |
| class FailureMessageManager: |
| """Manager class to create a failure message or reconstruct messages.""" |
| |
| @classmethod |
| def CreateMessage(cls, stage_failure, **kwargs): |
| """Create a failure message instance depending on the exception type. |
| |
| Args: |
| stage_failure: An instance of StageFailure. |
| **kwargs: Extra message information to pass to StageFailureMessage. |
| |
| Returns: |
| A failure message instance of StageFailureMessage class (or its |
| subclass) |
| """ |
| if stage_failure.exception_type in BUILD_SCRIPT_FAILURE_TYPES: |
| return BuildScriptFailureMessage(stage_failure, **kwargs) |
| elif stage_failure.exception_type in PACKAGE_BUILD_FAILURE_TYPES: |
| return PackageBuildFailureMessage(stage_failure, **kwargs) |
| else: |
| return StageFailureMessage(stage_failure, **kwargs) |
| |
| @classmethod |
| def ReconstructMessages(cls, failure_messages): |
| """Reconstruct failure messages by nesting messages. |
| |
| A failure message with not none outer_failure_id is an inner failure of |
| its outer failure message(failure_id == outer_failure_id). This method |
| takes a list of failure messages, reconstructs the list by 1) converting |
| the outer failure message into a CompoundFailureMessage instance 2) |
| insert the inner failure messages to the inner_failures list of their |
| outer failure messages. |
| CompoundFailures in CIDB aren't nested |
| (see failures_lib.ReportStageFailure), so there isn't another |
| inner failure list layer in a inner failure message and there are no |
| circular dependencies. |
| |
| For example, given failure_messages list |
| [A(failure_id=1), |
| B(failure_id=2, outer_failure_id=1), |
| C(failure_id=3, outer_failure_id=1), |
| D(failure_id=4), |
| E(failure_id=5, outer_failure_id=4), |
| F(failure_id=6)] |
| this method returns a reconstructed list: |
| [ |
| A(failure_id=1, inner_failures=[ |
| B(failure_id=2, outer_failure_id=1), |
| C(failure_id=3, outer_failure_id=1) |
| ]), |
| D(failure_id=4, inner_failures=[ |
| E(failure_id=5, outer_failure_id=4) |
| ]), |
| F(failure_id=6) |
| ] |
| |
| Args: |
| failure_messages: A list a failure message instances not nested. |
| |
| Returns: |
| A list of failure message instances of StageFailureMessage class (or |
| its subclass). Failure messages with not None outer_failure_id are |
| nested into the inner_failures list of their outer failure messages. |
| """ |
| failure_message_dict = {x.failure_id: x for x in failure_messages} |
| |
| for failure in failure_messages: |
| if failure.outer_failure_id is not None: |
| assert failure.outer_failure_id in failure_message_dict |
| outer_failure = failure_message_dict[failure.outer_failure_id] |
| if not isinstance(outer_failure, CompoundFailureMessage): |
| outer_failure = CompoundFailureMessage.GetFailureMessage( |
| outer_failure |
| ) |
| failure_message_dict[ |
| outer_failure.failure_id |
| ] = outer_failure |
| |
| outer_failure.inner_failures.append(failure) |
| del failure_message_dict[failure.failure_id] |
| |
| return list(failure_message_dict.values()) |
| |
| @classmethod |
| def ConstructStageFailureMessages(cls, stage_failures): |
| """Construct stage failure messages from failure entries from CIDB. |
| |
| Args: |
| stage_failures: A list of StageFailure instances. |
| |
| Returns: |
| A list of stage failure message instances of StageFailureMessage |
| class (or its subclass). See return type of ReconstructMessages(). |
| """ |
| failure_messages = [cls.CreateMessage(f) for f in stage_failures] |
| |
| return cls.ReconstructMessages(failure_messages) |