blob: 7e8ff4b16750ee045770485ded6031587dfcc8b2 [file] [log] [blame]
# Copyright 2011 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Classes for collecting results of our BuildStages as they run."""
import collections
import datetime
import logging
import math
import os
from chromite.cbuildbot import cbuildbot_alerts
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import failures_lib
def _GetCheckpointFile(buildroot):
return os.path.join(buildroot, ".completed_stages")
def WriteCheckpoint(buildroot):
"""Drops a completed stages file with current state."""
completed_stages_file = _GetCheckpointFile(buildroot)
with open(completed_stages_file, "w+", encoding="utf-8") as save_file:
Results.SaveCompletedStages(save_file)
def LoadCheckpoint(buildroot):
"""Restore completed stage info from checkpoint file."""
completed_stages_file = _GetCheckpointFile(buildroot)
if not os.path.exists(completed_stages_file):
logging.warning("Checkpoint file not found in buildroot %s", buildroot)
return
with open(completed_stages_file, "r", encoding="utf-8") as load_file:
Results.RestoreCompletedStages(load_file)
class RecordedTraceback:
"""This class represents a traceback recorded in the list of results."""
def __init__(self, failed_stage, failed_prefix, exception, traceback):
"""Construct a RecordedTraceback object.
Args:
failed_stage: The stage that failed during the build.
E.g., HWTest [bvt]
failed_prefix: The prefix of the stage that failed. E.g., HWTest
exception: The raw exception object.
traceback: The full stack trace for the failure, as a string.
"""
self.failed_stage = failed_stage
self.failed_prefix = failed_prefix
self.exception = exception
self.traceback = traceback
_result_fields = ["name", "result", "description", "prefix", "board", "time"]
Result = collections.namedtuple("Result", _result_fields)
class _Results:
"""Static class that collects the results of our BuildStages as they run."""
SUCCESS = "Stage was successful"
FORGIVEN = "Stage failed but was optional"
SKIPPED = "Stage was skipped"
NON_FAILURE_TYPES = (SUCCESS, FORGIVEN, SKIPPED)
SPLIT_TOKEN = r"\_O_/"
def __init__(self):
# List of results for all stages that's built up as we run. Members are
# of the form:
# ('name', SUCCESS | FORGIVEN | Exception, None | description)
self._results_log = []
# A list of instances of failure_message_lib.StageFailureMessage to
# present the exceptions threw by failed stages.
self._failure_message_results = []
# Stages run in a previous run and restored. Stored as a dictionary of
# names to previous records.
self._previous = {}
self.start_time = datetime.datetime.now()
def Clear(self):
"""Clear existing stage results."""
self.__init__()
def PreviouslyCompletedRecord(self, name):
"""Check to see if this stage was previously completed.
Returns:
A boolean showing the stage was successful in the previous run.
"""
return self._previous.get(name)
def BuildSucceededSoFar(
self, buildstore=None, buildbucket_id=None, name=None
):
"""Return true if all stages so far have passing states.
This method returns true if all was successful or forgiven or skipped.
Args:
buildstore: A BuildStore instance to make DB calls.
buildbucket_id: buildbucket_id of the build to check.
name: stage name of current stage.
"""
build_succeess = all(
entry.result in self.NON_FAILURE_TYPES
for entry in self._results_log
)
# When timeout happens and background tasks are killed, the statuses
# of the background stage tasks may get lost. BuildSucceededSoFar may
# still return build_succeess = True when the killed stage tasks were
# failed. Add one more verification step in _BuildSucceededFromCIDB to
# check the stage status in CIDB.
return build_succeess and self._BuildSucceededFromCIDB(
buildstore=buildstore, buildbucket_id=buildbucket_id, name=name
)
def _BuildSucceededFromCIDB(
self, buildstore=None, buildbucket_id=None, name=None
):
"""Return True if all stages recorded in buildbucket passed.
Args:
buildstore: A BuildStore instance to make DB calls.
buildbucket_id: buildbucket_id of the build to check.
name: stage name of current stage.
"""
if (
buildstore is not None
and buildstore.AreClientsReady()
and buildbucket_id is not None
):
stages = buildstore.GetBuildsStages(
buildbucket_ids=[buildbucket_id]
)
for stage in stages:
if name is not None and stage["name"] == name:
logging.info(
"Ignore status of %s as it's the current stage.",
stage["name"],
)
continue
if (
stage["status"]
not in constants.BUILDER_NON_FAILURE_STATUSES
):
logging.warning(
"Failure in previous stage %s with status %s.",
stage["name"],
stage["status"],
)
return False
return True
def StageHasResults(self, name):
"""Return true if stage has posted results."""
return name in [entry.name for entry in self._results_log]
def _RecordStageFailureMessage(
self, name, exception, prefix=None, build_stage_id=None
):
self._failure_message_results.append(
failures_lib.GetStageFailureMessageFromException(
name, build_stage_id, exception, stage_prefix_name=prefix
)
)
def Record(
self,
name,
result,
description=None,
prefix=None,
board="",
time=0,
build_stage_id=None,
):
"""Store off an additional stage result.
Args:
name: The name of the stage (e.g. HWTest [bvt])
result:
Result should be one of:
Results.SUCCESS if the stage was successful.
Results.SKIPPED if the stage was skipped.
Results.FORGIVEN if the stage had warnings.
Otherwise, it should be the exception stage errored with.
description: The textual backtrace of the exception, or None
prefix: The prefix of the stage (e.g. HWTest). Defaults to
the value of name.
board: The board associated with the stage, if any. Defaults to ''.
time: How long the result took to complete.
build_stage_id: The id of the failed build stage to record, default
None.
"""
if prefix is None:
prefix = name
# Convert exception to stage_failure_message and record it.
if isinstance(result, BaseException):
self._RecordStageFailureMessage(
name, result, prefix=prefix, build_stage_id=build_stage_id
)
result = Result(name, result, description, prefix, board, time)
self._results_log.append(result)
def GetStageFailureMessage(self):
return self._failure_message_results
def Get(self):
"""Fetch stage results.
Returns:
A list with one entry per stage run with a result.
"""
return self._results_log
def GetPrevious(self):
"""Fetch stage results.
Returns:
A list of stages names that were completed in a previous run.
"""
return self._previous
def SaveCompletedStages(self, out):
"""Save the successfully completed stages to the provided file |out|."""
for entry in self._results_log:
if entry.result != self.SUCCESS:
break
out.write(self.SPLIT_TOKEN.join(str(x) for x in entry) + "\n")
def RestoreCompletedStages(self, out):
"""Load the successfully completed stages from |out|."""
# Read the file, and strip off the newlines.
for line in out:
record = line.strip().split(self.SPLIT_TOKEN)
if len(record) != len(_result_fields):
logging.warning(
"State file does not match expected format, ignoring."
)
# Wipe any partial state.
self._previous = {}
break
self._previous[record[0]] = Result(*record)
def GetTracebacks(self):
"""Get a list of the exceptions that failed the build.
Returns:
A list of RecordedTraceback objects.
"""
tracebacks = []
for entry in self._results_log:
# If entry.result is not in NON_FAILURE_TYPES, then the stage
# failed, and entry.result is the exception object and
# entry.description is a string containing the full traceback.
if entry.result not in self.NON_FAILURE_TYPES:
traceback = RecordedTraceback(
entry.name, entry.prefix, entry.result, entry.description
)
tracebacks.append(traceback)
return tracebacks
def Report(self, out, current_version=None):
"""Generate a user-friendly text display of the result data.
Args:
out: Output stream to write to (e.g. sys.stdout).
current_version: Chrome OS version associated with this report.
"""
results = self._results_log
line = "*" * 60 + "\n"
edge = "*" * 2
if current_version:
out.write(line)
out.write(edge + " RELEASE VERSION: " + current_version + "\n")
out.write(line)
out.write(edge + " Stage Results\n")
warnings = False
for entry in results:
name, result, run_time = (entry.name, entry.result, entry.time)
timestr = datetime.timedelta(seconds=math.ceil(run_time))
# Don't print data on skipped stages.
if result == self.SKIPPED:
continue
out.write(line)
details = ""
if result == self.SUCCESS:
status = "PASS"
elif result == self.FORGIVEN:
status = "FAILED BUT FORGIVEN"
warnings = True
else:
status = "FAIL"
if isinstance(result, cros_build_lib.RunCommandError):
# If there was a run error, give just the command that
# failed, not its full argument list, since those are
# usually too long.
details = " in %s" % result.cmd[0]
elif isinstance(result, failures_lib.BuildScriptFailure):
# BuildScriptFailure errors publish a 'short' name of the
# command that failed.
details = " in %s" % result.shortname
else:
# There was a normal error. Give the type of exception.
details = " with %s" % type(result).__name__
out.write(
"%s %s %s (%s)%s\n" % (edge, status, name, timestr, details)
)
out.write(line)
for x in self.GetTracebacks():
if x.failed_stage and x.traceback:
out.write("\nFailed in stage %s:\n\n" % x.failed_stage)
out.write(x.traceback)
out.write("\n")
if warnings:
cbuildbot_alerts.PrintBuildbotStepWarnings(out)
Results = _Results()