1# -*- coding: utf-8 -*- 2# Copyright (c) 2011 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"""Classes for collecting results of our BuildStages as they run.""" 7 8from __future__ import print_function 9 10import collections 11import datetime 12import math 13import os 14 15from autotest_lib.utils.frozen_chromite.lib import constants 16from autotest_lib.utils.frozen_chromite.lib import failures_lib 17from autotest_lib.utils.frozen_chromite.lib import cros_build_lib 18from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging 19 20def _GetCheckpointFile(buildroot): 21 return os.path.join(buildroot, '.completed_stages') 22 23 24def WriteCheckpoint(buildroot): 25 """Drops a completed stages file with current state.""" 26 completed_stages_file = _GetCheckpointFile(buildroot) 27 with open(completed_stages_file, 'w+') as save_file: 28 Results.SaveCompletedStages(save_file) 29 30 31def LoadCheckpoint(buildroot): 32 """Restore completed stage info from checkpoint file.""" 33 completed_stages_file = _GetCheckpointFile(buildroot) 34 if not os.path.exists(completed_stages_file): 35 logging.warning('Checkpoint file not found in buildroot %s', buildroot) 36 return 37 38 with open(completed_stages_file, 'r') as load_file: 39 Results.RestoreCompletedStages(load_file) 40 41 42class RecordedTraceback(object): 43 """This class represents a traceback recorded in the list of results.""" 44 45 def __init__(self, failed_stage, failed_prefix, exception, traceback): 46 """Construct a RecordedTraceback object. 47 48 Args: 49 failed_stage: The stage that failed during the build. E.g., HWTest [bvt] 50 failed_prefix: The prefix of the stage that failed. E.g., HWTest 51 exception: The raw exception object. 52 traceback: The full stack trace for the failure, as a string. 53 """ 54 self.failed_stage = failed_stage 55 self.failed_prefix = failed_prefix 56 self.exception = exception 57 self.traceback = traceback 58 59 60_result_fields = ['name', 'result', 'description', 'prefix', 'board', 'time'] 61Result = collections.namedtuple('Result', _result_fields) 62 63 64class _Results(object): 65 """Static class that collects the results of our BuildStages as they run.""" 66 67 SUCCESS = 'Stage was successful' 68 FORGIVEN = 'Stage failed but was optional' 69 SKIPPED = 'Stage was skipped' 70 NON_FAILURE_TYPES = (SUCCESS, FORGIVEN, SKIPPED) 71 72 SPLIT_TOKEN = r'\_O_/' 73 74 def __init__(self): 75 # List of results for all stages that's built up as we run. Members are of 76 # the form ('name', SUCCESS | FORGIVEN | Exception, None | description) 77 self._results_log = [] 78 79 # A list of instances of failure_message_lib.StageFailureMessage to present 80 # the exceptions threw by failed stages. 81 self._failure_message_results = [] 82 83 # Stages run in a previous run and restored. Stored as a dictionary of 84 # names to previous records. 85 self._previous = {} 86 87 self.start_time = datetime.datetime.now() 88 89 def Clear(self): 90 """Clear existing stage results.""" 91 self.__init__() 92 93 def PreviouslyCompletedRecord(self, name): 94 """Check to see if this stage was previously completed. 95 96 Returns: 97 A boolean showing the stage was successful in the previous run. 98 """ 99 return self._previous.get(name) 100 101 def BuildSucceededSoFar(self, buildstore=None, buildbucket_id=None, 102 name=None): 103 """Return true if all stages so far have passing states. 104 105 This method returns true if all was successful or forgiven or skipped. 106 107 Args: 108 buildstore: A BuildStore instance to make DB calls. 109 buildbucket_id: buildbucket_id of the build to check. 110 name: stage name of current stage. 111 """ 112 build_succeess = all(entry.result in self.NON_FAILURE_TYPES 113 for entry in self._results_log) 114 115 # When timeout happens and background tasks are killed, the statuses 116 # of the background stage tasks may get lost. BuildSucceededSoFar may 117 # still return build_succeess = True when the killed stage tasks were 118 # failed. Add one more verification step in _BuildSucceededFromCIDB to 119 # check the stage status in CIDB. 120 return (build_succeess and 121 self._BuildSucceededFromCIDB(buildstore=buildstore, 122 buildbucket_id=buildbucket_id, 123 name=name)) 124 125 def _BuildSucceededFromCIDB(self, buildstore=None, buildbucket_id=None, 126 name=None): 127 """Return True if all stages recorded in buildbucket have passing states. 128 129 Args: 130 buildstore: A BuildStore instance to make DB calls. 131 buildbucket_id: buildbucket_id of the build to check. 132 name: stage name of current stage. 133 """ 134 if (buildstore is not None and buildstore.AreClientsReady() 135 and buildbucket_id is not None): 136 stages = buildstore.GetBuildsStages(buildbucket_ids=[buildbucket_id]) 137 for stage in stages: 138 if name is not None and stage['name'] == name: 139 logging.info("Ignore status of %s as it's the current stage.", 140 stage['name']) 141 continue 142 if stage['status'] not in constants.BUILDER_NON_FAILURE_STATUSES: 143 logging.warning('Failure in previous stage %s with status %s.', 144 stage['name'], stage['status']) 145 return False 146 147 return True 148 149 def StageHasResults(self, name): 150 """Return true if stage has posted results.""" 151 return name in [entry.name for entry in self._results_log] 152 153 def _RecordStageFailureMessage(self, name, exception, prefix=None, 154 build_stage_id=None): 155 self._failure_message_results.append( 156 failures_lib.GetStageFailureMessageFromException( 157 name, build_stage_id, exception, stage_prefix_name=prefix)) 158 159 def Record(self, name, result, description=None, prefix=None, board='', 160 time=0, build_stage_id=None): 161 """Store off an additional stage result. 162 163 Args: 164 name: The name of the stage (e.g. HWTest [bvt]) 165 result: 166 Result should be one of: 167 Results.SUCCESS if the stage was successful. 168 Results.SKIPPED if the stage was skipped. 169 Results.FORGIVEN if the stage had warnings. 170 Otherwise, it should be the exception stage errored with. 171 description: 172 The textual backtrace of the exception, or None 173 prefix: The prefix of the stage (e.g. HWTest). Defaults to 174 the value of name. 175 board: The board associated with the stage, if any. Defaults to ''. 176 time: How long the result took to complete. 177 build_stage_id: The id of the failed build stage to record, default to 178 None. 179 """ 180 if prefix is None: 181 prefix = name 182 183 # Convert exception to stage_failure_message and record it. 184 if isinstance(result, BaseException): 185 self._RecordStageFailureMessage(name, result, prefix=prefix, 186 build_stage_id=build_stage_id) 187 188 result = Result(name, result, description, prefix, board, time) 189 self._results_log.append(result) 190 191 def GetStageFailureMessage(self): 192 return self._failure_message_results 193 194 def Get(self): 195 """Fetch stage results. 196 197 Returns: 198 A list with one entry per stage run with a result. 199 """ 200 return self._results_log 201 202 def GetPrevious(self): 203 """Fetch stage results. 204 205 Returns: 206 A list of stages names that were completed in a previous run. 207 """ 208 return self._previous 209 210 def SaveCompletedStages(self, out): 211 """Save the successfully completed stages to the provided file |out|.""" 212 for entry in self._results_log: 213 if entry.result != self.SUCCESS: 214 break 215 out.write(self.SPLIT_TOKEN.join(str(x) for x in entry) + '\n') 216 217 def RestoreCompletedStages(self, out): 218 """Load the successfully completed stages from the provided file |out|.""" 219 # Read the file, and strip off the newlines. 220 for line in out: 221 record = line.strip().split(self.SPLIT_TOKEN) 222 if len(record) != len(_result_fields): 223 logging.warning('State file does not match expected format, ignoring.') 224 # Wipe any partial state. 225 self._previous = {} 226 break 227 228 self._previous[record[0]] = Result(*record) 229 230 def GetTracebacks(self): 231 """Get a list of the exceptions that failed the build. 232 233 Returns: 234 A list of RecordedTraceback objects. 235 """ 236 tracebacks = [] 237 for entry in self._results_log: 238 # If entry.result is not in NON_FAILURE_TYPES, then the stage failed, and 239 # entry.result is the exception object and entry.description is a string 240 # containing the full traceback. 241 if entry.result not in self.NON_FAILURE_TYPES: 242 traceback = RecordedTraceback(entry.name, entry.prefix, entry.result, 243 entry.description) 244 tracebacks.append(traceback) 245 return tracebacks 246 247 def Report(self, out, current_version=None): 248 """Generate a user friendly text display of the results data. 249 250 Args: 251 out: Output stream to write to (e.g. sys.stdout). 252 current_version: ChromeOS version associated with this report. 253 """ 254 results = self._results_log 255 256 line = '*' * 60 + '\n' 257 edge = '*' * 2 258 259 if current_version: 260 out.write(line) 261 out.write(edge + 262 ' RELEASE VERSION: ' + 263 current_version + 264 '\n') 265 266 out.write(line) 267 out.write(edge + ' Stage Results\n') 268 warnings = False 269 270 for entry in results: 271 name, result, run_time = (entry.name, entry.result, entry.time) 272 timestr = datetime.timedelta(seconds=math.ceil(run_time)) 273 274 # Don't print data on skipped stages. 275 if result == self.SKIPPED: 276 continue 277 278 out.write(line) 279 details = '' 280 if result == self.SUCCESS: 281 status = 'PASS' 282 elif result == self.FORGIVEN: 283 status = 'FAILED BUT FORGIVEN' 284 warnings = True 285 else: 286 status = 'FAIL' 287 if isinstance(result, cros_build_lib.RunCommandError): 288 # If there was a run error, give just the command that failed, not 289 # its full argument list, since those are usually too long. 290 details = ' in %s' % result.result.cmd[0] 291 elif isinstance(result, failures_lib.BuildScriptFailure): 292 # BuildScriptFailure errors publish a 'short' name of the 293 # command that failed. 294 details = ' in %s' % result.shortname 295 else: 296 # There was a normal error. Give the type of exception. 297 details = ' with %s' % type(result).__name__ 298 299 out.write('%s %s %s (%s)%s\n' % (edge, status, name, timestr, details)) 300 301 out.write(line) 302 303 for x in self.GetTracebacks(): 304 if x.failed_stage and x.traceback: 305 out.write('\nFailed in stage %s:\n\n' % x.failed_stage) 306 out.write(x.traceback) 307 out.write('\n') 308 309 if warnings: 310 logging.PrintBuildbotStepWarnings(out) 311 312 313Results = _Results() 314