• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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