xref: /aosp_15_r20/external/autotest/utils/frozen_chromite/lib/failure_message_lib.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# -*- coding: utf-8 -*-
2# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Module to manage stage failure messages."""
7
8from __future__ import print_function
9
10import collections
11import json
12import re
13
14from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging
15
16# Currently, an exception is reported to CIDB failureTabe using the exception
17# class name as the exception_type. failure_message_lib.FailureMessageManager
18# uses the exception_type to decide which StageFailureMessage class to use
19# to rebuild the failure message. Whenever you need to change the names of these
20# classes, please add the new class names to their corresponding type lists,
21# and DO NOT remove the old class names from the type lists.
22# TODO (nxia): instead of using the class name as the exception type when
23# reporting an exception to CIDB, we need to have an attribute like
24# EXCEPTION_CATEGORY (say EXCEPTION_TYPE) and this type cannot be changed or
25# removed from EXCEPTION_TYPE_LIST. But we can add new types to the list.
26BUILD_SCRIPT_FAILURE_TYPES = ('BuildScriptFailure',)
27PACKAGE_BUILD_FAILURE_TYPES = ('PackageBuildFailure',)
28
29
30# These keys must exist as column names from failureView in cidb.
31FAILURE_KEYS = (
32    'id', 'build_stage_id', 'outer_failure_id', 'exception_type',
33    'exception_message', 'exception_category', 'extra_info',
34    'timestamp', 'stage_name', 'board', 'stage_status', 'build_id',
35    'master_build_id', 'builder_name', 'build_number',
36    'build_config', 'build_status', 'important', 'buildbucket_id')
37
38
39# A namedtuple containing values fetched from CIDB failureView.
40_StageFailure = collections.namedtuple('_StageFailure', FAILURE_KEYS)
41
42
43class StageFailure(_StageFailure):
44  """A class presenting values of a failure fetched from CIDB failureView."""
45
46  @classmethod
47  def GetStageFailureFromMessage(cls, stage_failure_message):
48    """Create StageFailure from a StageFailureMessage instance.
49
50    Args:
51      stage_failure_message: An instance of StageFailureMessage.
52
53    Returns:
54      An instance of StageFailure.
55    """
56    return StageFailure(
57        stage_failure_message.failure_id,
58        stage_failure_message.build_stage_id,
59        stage_failure_message.outer_failure_id,
60        stage_failure_message.exception_type,
61        stage_failure_message.exception_message,
62        stage_failure_message.exception_category,
63        stage_failure_message.extra_info, None,
64        stage_failure_message.stage_name, None, None, None, None, None, None,
65        None, None, None, None)
66
67  @classmethod
68  def GetStageFailureFromDicts(cls, failure_dict, stage_dict, build_dict):
69    """Get StageFailure from value dictionaries.
70
71    Args:
72      failure_dict: A dict presenting values of a tuple from failureTable.
73      stage_dict: A dict presenting values of a tuple from buildStageTable.
74      build_dict: A dict presenting values of a tuple from buildTable.
75
76    Returns:
77      An instance of StageFailure.
78    """
79    return StageFailure(
80        failure_dict['id'], failure_dict['build_stage_id'],
81        failure_dict['outer_failure_id'], failure_dict['exception_type'],
82        failure_dict['exception_message'], failure_dict['exception_category'],
83        failure_dict['extra_info'], failure_dict['timestamp'],
84        stage_dict['name'], stage_dict['board'], stage_dict['status'],
85        build_dict['id'], build_dict['master_build_id'],
86        build_dict['builder_name'],
87        build_dict['build_number'], build_dict['build_config'],
88        build_dict['status'], build_dict['important'],
89        build_dict['buildbucket_id'])
90
91
92class StageFailureMessage(object):
93  """Message class contains information of a general stage failure.
94
95  Failed stages report stage failures to CIDB failureTable (see more details
96  in failures_lib.ReportStageFailure). This class constructs a failure
97  message instance from the stage failure information stored in CIDB.
98  """
99
100  def __init__(self, stage_failure, extra_info=None, stage_prefix_name=None):
101    """Construct a StageFailureMessage instance.
102
103    Args:
104      stage_failure: An instance of StageFailure.
105      extra_info: The extra info of the origin failure, default to None.
106      stage_prefix_name: The prefix name (string) of the failed stage,
107        default to None.
108    """
109    self.failure_id = stage_failure.id
110    self.build_stage_id = stage_failure.build_stage_id
111    self.stage_name = stage_failure.stage_name
112    self.exception_type = stage_failure.exception_type
113    self.exception_message = stage_failure.exception_message
114    self.exception_category = stage_failure.exception_category
115    self.outer_failure_id = stage_failure.outer_failure_id
116
117    if extra_info is not None:
118      self.extra_info = extra_info
119    else:
120      # No extra_info provided, decode extra_info from stage_failure.
121      self.extra_info = self._DecodeExtraInfo(stage_failure.extra_info)
122
123    if stage_prefix_name is not None:
124      self.stage_prefix_name = stage_prefix_name
125    else:
126      # No stage_prefix_name provided, extra prefix name from stage_failure.
127      self.stage_prefix_name = self._ExtractStagePrefixName(self.stage_name)
128
129  def __str__(self):
130    return ('[failure id] %s [stage name] %s [stage prefix name] %s '
131            '[exception type] %s [exception category] %s [exception message] %s'
132            ' [extra info] %s' %
133            (self.failure_id, self.stage_name, self.stage_prefix_name,
134             self.exception_type, self.exception_category,
135             self.exception_message, self.extra_info))
136
137  def _DecodeExtraInfo(self, extra_info):
138    """Decode extra info json into dict.
139
140    Args:
141      extra_info: The extra_info of the origin exception, default to None.
142
143    Returns:
144      An empty dict if extra_info is None; extra_info itself if extra_info is
145      a dict; else, load the json string into a dict and return it.
146    """
147    if not extra_info:
148      return {}
149    elif isinstance(extra_info, dict):
150      return extra_info
151    else:
152      try:
153        return  json.loads(extra_info)
154      except ValueError as e:
155        logging.error('Cannot decode extra_info: %s', e)
156        return {}
157
158  # TODO(nxia): Force format checking on stage names when they're created
159  def _ExtractStagePrefixName(self, stage_name):
160    """Extract stage prefix name given a full stage name.
161
162    Format examples in our current CIDB buildStageTable:
163      HWTest [bvt-arc] -> HWTest
164      HWTest -> HWTest
165      ImageTest -> ImageTest
166      ImageTest [amd64-generic] -> ImageTest
167      VMTest (attempt 1) -> VMTest
168      VMTest [amd64-generic] (attempt 1) -> VMTest
169
170    Args:
171      stage_name: The full stage name (string) recorded in CIDB.
172
173    Returns:
174      The prefix stage name (string).
175    """
176    pattern = r'([^ ]+)( +\[([^]]+)\])?( +\(([^)]+)\))?'
177    m = re.compile(pattern).match(stage_name)
178    if m is not None:
179      return m.group(1)
180    else:
181      return stage_name
182
183
184class BuildScriptFailureMessage(StageFailureMessage):
185  """Message class contains information of a BuildScriptFailure."""
186
187  def GetShortname(self):
188    """Return the short name (string) of the run command."""
189    return self.extra_info.get('shortname')
190
191
192class PackageBuildFailureMessage(StageFailureMessage):
193  """Message class contains information of a PackagebuildFailure."""
194
195  def GetShortname(self):
196    """Return the short name (string) of the run command."""
197    return self.extra_info.get('shortname')
198
199  def GetFailedPackages(self):
200    """Return a list of packages (strings) that failed to build."""
201    return self.extra_info.get('failed_packages', [])
202
203
204class CompoundFailureMessage(StageFailureMessage):
205  """Message class contains information of a CompoundFailureMessage."""
206
207  def __init__(self, stage_failure, **kwargs):
208    """Construct a CompoundFailureMessage instance.
209
210    Args:
211      stage_failure: An instance of StageFailure.
212      kwargs: Extra message information to pass to StageFailureMessage.
213    """
214    super(CompoundFailureMessage, self).__init__(stage_failure, **kwargs)
215
216    self.inner_failures = []
217
218  def __str__(self):
219    msg_str = super(CompoundFailureMessage, self).__str__()
220
221    for failure in self.inner_failures:
222      msg_str += ('(Inner Stage Failure Message) %s' % str(failure))
223
224    return msg_str
225
226  @staticmethod
227  def GetFailureMessage(failure_message):
228    """Convert a regular failure message instance to CompoundFailureMessage.
229
230    Args:
231      failure_message: An instance of StageFailureMessage.
232
233    Returns:
234      A CompoundFailureMessage instance.
235    """
236    return CompoundFailureMessage(
237        StageFailure.GetStageFailureFromMessage(failure_message),
238        extra_info=failure_message.extra_info,
239        stage_prefix_name=failure_message.stage_prefix_name)
240
241  def HasEmptyList(self):
242    """Check whether the inner failure list is empty.
243
244    Returns:
245      True if self.inner_failures is empty; else, False.
246    """
247    return not bool(self.inner_failures)
248
249  def HasExceptionCategories(self, exception_categories):
250    """Check whether any of the inner failures matches the exception categories.
251
252    Args:
253      exception_categories: A set of exception categories (members of
254        constants.EXCEPTION_CATEGORY_ALL_CATEGORIES).
255
256    Returns:
257      True if any of the inner failures matches a memeber in
258      exception_categories; else, False.
259    """
260    return any(x.exception_category in exception_categories
261               for x in self.inner_failures)
262
263  def MatchesExceptionCategories(self, exception_categories):
264    """Check whether all of the inner failures matches the exception categories.
265
266    Args:
267      exception_categories: A set of exception categories (members of
268        constants.EXCEPTION_CATEGORY_ALL_CATEGORIES).
269
270    Returns:
271      True if all of the inner failures match a memeber in
272      exception_categories; else, False.
273    """
274    return (not self.HasEmptyList() and
275            all(x.exception_category in exception_categories
276                for x in self.inner_failures))
277
278
279class FailureMessageManager(object):
280  """Manager class to create a failure message or reconstruct messages."""
281
282  @classmethod
283  def CreateMessage(cls, stage_failure, **kwargs):
284    """Create a failure message instance depending on the exception type.
285
286    Args:
287      stage_failure: An instance of StageFailure.
288      kwargs: Extra message information to pass to StageFailureMessage.
289
290    Returns:
291      A failure message instance of StageFailureMessage class (or its
292        sub-class)
293    """
294    if stage_failure.exception_type in BUILD_SCRIPT_FAILURE_TYPES:
295      return BuildScriptFailureMessage(stage_failure, **kwargs)
296    elif stage_failure.exception_type in PACKAGE_BUILD_FAILURE_TYPES:
297      return PackageBuildFailureMessage(stage_failure, **kwargs)
298    else:
299      return StageFailureMessage(stage_failure, **kwargs)
300
301  @classmethod
302  def ReconstructMessages(cls, failure_messages):
303    """Reconstruct failure messages by nesting messages.
304
305    A failure message with not none outer_failure_id is an inner failure of its
306    outer failure message(failure_id == outer_failure_id). This method takes a
307    list of failure messages, reconstructs the list by 1) converting the outer
308    failure message into a CompoundFailureMessage instance 2) insert the inner
309    failure messages to the inner_failures list of their outer failure messages.
310    CompoundFailures in CIDB aren't nested
311    (see failures_lib.ReportStageFailure), so there isn't another
312    inner failure list layer in a inner failure message and there're no circular
313    dependencies.
314
315    For example, given failure_messages list
316      [A(failure_id=1),
317       B(failure_id=2, outer_failure_id=1),
318       C(failure_id=3, outer_failure_id=1),
319       D(failure_id=4),
320       E(failure_id=5, outer_failure_id=4),
321       F(failure_id=6)]
322    this method returns a reconstructed list:
323      [A(failure_id=1, inner_failures=[B(failure_id=2, outer_failure_id=1),
324                                       C(failure_id=3, outer_failure_id=1)]),
325       D(failure_id=4, inner_failures=[E(failure_id=5, outer_failure_id=4)]),
326       F(failure_id=6)]
327
328    Args:
329      failure_messages: A list a failure message instances not nested.
330
331    Returns:
332      A list of failure message instances of StageFailureMessage class (or its
333        sub-class). Failure messages with not None outer_failure_id are nested
334        into the inner_failures list of their outer failure messages.
335    """
336    failure_message_dict = {x.failure_id: x for x in failure_messages}
337
338    for failure in failure_messages:
339      if failure.outer_failure_id is not None:
340        assert failure.outer_failure_id in failure_message_dict
341        outer_failure = failure_message_dict[failure.outer_failure_id]
342        if not isinstance(outer_failure, CompoundFailureMessage):
343          outer_failure = CompoundFailureMessage.GetFailureMessage(
344              outer_failure)
345          failure_message_dict[outer_failure.failure_id] = outer_failure
346
347        outer_failure.inner_failures.append(failure)
348        del failure_message_dict[failure.failure_id]
349
350    return list(failure_message_dict.values())
351
352  @classmethod
353  def ConstructStageFailureMessages(cls, stage_failures):
354    """Construct stage failure messages from failure entries from CIDB.
355
356    Args:
357      stage_failures: A list of StageFailure instances.
358
359    Returns:
360      A list of stage failure message instances of StageFailureMessage class
361      (or its sub-class). See return type of ReconstructMessages().
362    """
363    failure_messages = [cls.CreateMessage(f) for f in stage_failures]
364
365    return cls.ReconstructMessages(failure_messages)
366