blob: c3470d297016f5fa9b7c22a6fa79058994218583 [file] [log] [blame]
# 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)