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