普通文本  |  445行  |  12.41 KB

# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import collections
import cPickle
import json
import logging
import os
import re
import socket
import time
import urllib
import urllib2


PENDING = None
SUCCESS = 0
WARNING = 1
FAILURE = 2
EXCEPTION = 4
SLAVE_LOST = 5


BASE_URL = 'http://build.chromium.org/p'
CACHE_FILE_NAME = 'cache.dat'


StackTraceLine = collections.namedtuple(
    'StackTraceLine', ('file', 'function', 'line', 'source'))


def _FetchData(master, url):
  url = '%s/%s/json/%s' % (BASE_URL, master, url)
  try:
    logging.info('Retrieving ' + url)
    return json.load(urllib2.urlopen(url))
  except (urllib2.HTTPError, socket.error):
    # Could be intermittent; try again.
    try:
      return json.load(urllib2.urlopen(url))
    except (urllib2.HTTPError, socket.error):
      logging.error('Error retrieving URL ' + url)
      raise
  except:
    logging.error('Error retrieving URL ' + url)
    raise


def Builders(master):
  builders = {}

  # Load builders from cache file.
  if os.path.exists(master):
    start_time = time.time()
    for builder_name in os.listdir(master):
      cache_file_path = os.path.join(master, builder_name, CACHE_FILE_NAME)
      if os.path.exists(cache_file_path):
        with open(cache_file_path, 'rb') as cache_file:
          try:
            builders[builder_name] = cPickle.load(cache_file)
          except EOFError:
            logging.error('File is corrupted: %s', cache_file_path)
            raise
    logging.info('Loaded builder caches in %0.2f seconds.',
                 time.time() - start_time)

  return builders


def Update(master, builders):
  # Update builders with latest information.
  builder_data = _FetchData(master, 'builders')
  for builder_name, builder_info in builder_data.iteritems():
    if builder_name in builders:
      builders[builder_name].Update(builder_info)
    else:
      builders[builder_name] = Builder(master, builder_name, builder_info)

  return builders


class Builder(object):
  # pylint: disable=too-many-instance-attributes

  def __init__(self, master, name, data):
    self._master = master
    self._name = name

    self.Update(data)

    self._builds = {}

  def __setstate__(self, state):
    self.__dict__ = state  # pylint: disable=attribute-defined-outside-init
    if not hasattr(self, '_builds'):
      self._builds = {}

  def __lt__(self, other):
    return self.name < other.name

  def __str__(self):
    return self.name

  def __getitem__(self, key):
    if not isinstance(key, int):
      raise TypeError('build numbers must be integers, not %s' %
                      type(key).__name__)

    self._FetchBuilds(key)
    return self._builds[key]

  def _FetchBuilds(self, *build_numbers):
    """Download build details, if not already cached.

    Returns:
      A tuple of downloaded build numbers.
    """
    build_numbers = tuple(build_number for build_number in build_numbers
                          if not (build_number in self._builds and
                                  self._builds[build_number].complete))
    if not build_numbers:
      return ()

    for build_number in build_numbers:
      if build_number < 0:
        raise ValueError('Invalid build number: %d' % build_number)

    build_query = urllib.urlencode(
        [('select', build) for build in build_numbers])
    url = 'builders/%s/builds/?%s' % (urllib.quote(self.name), build_query)
    builds = _FetchData(self.master, url)
    for build_info in builds.itervalues():
      self._builds[build_info['number']] = Build(self.master, build_info)

    self._Cache()

    return build_numbers

  def FetchRecentBuilds(self, number_of_builds):
    min_build = max(self.last_build - number_of_builds, -1)
    return self._FetchBuilds(*xrange(self.last_build, min_build, -1))

  def Update(self, data=None):
    if not data:
      data = _FetchData(self.master, 'builders/%s' % urllib.quote(self.name))
    self._state = data['state']
    self._pending_build_count = data['pendingBuilds']
    self._current_builds = tuple(data['currentBuilds'])
    self._cached_builds = tuple(data['cachedBuilds'])
    self._slaves = tuple(data['slaves'])

    self._Cache()

  def _Cache(self):
    cache_dir_path = os.path.join(self.master, self.name)
    if not os.path.exists(cache_dir_path):
      os.makedirs(cache_dir_path)
    cache_file_path = os.path.join(cache_dir_path, CACHE_FILE_NAME)
    with open(cache_file_path, 'wb') as cache_file:
      cPickle.dump(self, cache_file, -1)

  def LastBuilds(self, count):
    min_build = max(self.last_build - count, -1)
    for build_number in xrange(self.last_build, min_build, -1):
      yield self._builds[build_number]

  @property
  def master(self):
    return self._master

  @property
  def name(self):
    return self._name

  @property
  def state(self):
    return self._state

  @property
  def pending_build_count(self):
    return self._pending_build_count

  @property
  def current_builds(self):
    """List of build numbers currently building.

    There may be multiple entries if there are multiple build slaves."""
    return self._current_builds

  @property
  def cached_builds(self):
    """Builds whose data are visible on the master in increasing order.

    More builds may be available than this."""
    return self._cached_builds

  @property
  def last_build(self):
    """Last completed build."""
    for build_number in reversed(self.cached_builds):
      if build_number not in self.current_builds:
        return build_number
    return None

  @property
  def slaves(self):
    return self._slaves


class Build(object):
  def __init__(self, master, data):
    self._master = master
    self._builder_name = data['builderName']
    self._number = data['number']
    self._complete = not ('currentStep' in data and data['currentStep'])
    self._start_time, self._end_time = data['times']

    self._steps = {
        step_info['name']:
            Step(self._master, self._builder_name, self._number, step_info)
        for step_info in data['steps']
    }

  def __str__(self):
    return str(self.number)

  def __lt__(self, other):
    return self.number < other.number

  @property
  def builder_name(self):
    return self._builder_name

  @property
  def number(self):
    return self._number

  @property
  def complete(self):
    return self._complete

  @property
  def start_time(self):
    return self._start_time

  @property
  def end_time(self):
    return self._end_time

  @property
  def steps(self):
    return self._steps


def _ParseTraceFromLog(log):
  """Search the log for a stack trace and return a structured representation.

  This function supports both default Python-style stacks and Telemetry-style
  stacks. It returns the first stack trace found in the log - sometimes a bug
  leads to a cascade of failures, so the first one is usually the root cause.
  """
  log_iterator = iter(log.splitlines())
  for line in log_iterator:
    if line == 'Traceback (most recent call last):':
      break
  else:
    return (None, None)

  stack_trace = []
  while True:
    line = log_iterator.next()
    match1 = re.match(r'\s*File "(?P<file>.+)", line (?P<line>[0-9]+), '
                      'in (?P<function>.+)', line)
    match2 = re.match(r'\s*(?P<function>.+) at '
                      '(?P<file>.+):(?P<line>[0-9]+)', line)
    match = match1 or match2
    if not match:
      exception = line
      break
    trace_line = match.groupdict()
    # Use the base name, because the path will be different
    # across platforms and configurations.
    file_base_name = trace_line['file'].split('/')[-1].split('\\')[-1]
    source = log_iterator.next().strip()
    stack_trace.append(StackTraceLine(
        file_base_name, trace_line['function'], trace_line['line'], source))

  return tuple(stack_trace), exception


class Step(object):
  # pylint: disable=too-many-instance-attributes

  def __init__(self, master, builder_name, build_number, data):
    self._master = master
    self._builder_name = builder_name
    self._build_number = build_number
    self._name = data['name']
    self._result = data['results'][0]
    self._start_time, self._end_time = data['times']

    self._log_link = None
    self._results_link = None
    for link_name, link_url in data['logs']:
      if link_name == 'stdio':
        self._log_link = link_url + '/text'
      elif link_name == 'json.output':
        self._results_link = link_url + '/text'

    self._log = None
    self._results = None
    self._stack_trace = None

  def __getstate__(self):
    return {
        '_master': self._master,
        '_builder_name': self._builder_name,
        '_build_number': self._build_number,
        '_name': self._name,
        '_result': self._result,
        '_start_time': self._start_time,
        '_end_time': self._end_time,
        '_log_link': self._log_link,
        '_results_link': self._results_link,
    }

  def __setstate__(self, state):
    self.__dict__ = state  # pylint: disable=attribute-defined-outside-init
    self._log = None
    self._results = None
    self._stack_trace = None

  def __str__(self):
    return self.name

  @property
  def name(self):
    return self._name

  @property
  def result(self):
    return self._result

  @property
  def start_time(self):
    return self._start_time

  @property
  def end_time(self):
    return self._end_time

  @property
  def log_link(self):
    return self._log_link

  @property
  def results_link(self):
    return self._results_link

  @property
  def log(self):
    if self._log is None:
      if not self.log_link:
        return None
      cache_file_path = os.path.join(
          self._master, self._builder_name,
          str(self._build_number), self._name, 'log')
      if os.path.exists(cache_file_path):
        # Load cache file, if it exists.
        with open(cache_file_path, 'rb') as cache_file:
          self._log = cache_file.read()
      else:
        # Otherwise, download it.
        logging.info('Retrieving ' + self.log_link)
        try:
          data = urllib2.urlopen(self.log_link).read()
        except (urllib2.HTTPError, socket.error):
          # Could be intermittent; try again.
          try:
            data = urllib2.urlopen(self.log_link).read()
          except (urllib2.HTTPError, socket.error):
            logging.error('Error retrieving URL ' + self.log_link)
            raise
        except:
          logging.error('Error retrieving URL ' + self.log_link)
          raise
        # And cache the newly downloaded data.
        cache_dir_path = os.path.dirname(cache_file_path)
        if not os.path.exists(cache_dir_path):
          os.makedirs(cache_dir_path)
        with open(cache_file_path, 'wb') as cache_file:
          cache_file.write(data)
        self._log = data
    return self._log

  @property
  def results(self):
    if self._results is None:
      if not self.results_link:
        return None
      cache_file_path = os.path.join(
          self._master, self._builder_name,
          str(self._build_number), self._name, 'results')
      if os.path.exists(cache_file_path):
        # Load cache file, if it exists.
        try:
          with open(cache_file_path, 'rb') as cache_file:
            self._results = cPickle.load(cache_file)
        except EOFError:
          os.remove(cache_file_path)
          return self.results
      else:
        # Otherwise, download it.
        logging.info('Retrieving ' + self.results_link)
        try:
          data = json.load(urllib2.urlopen(self.results_link))
        except (urllib2.HTTPError, socket.error):
          # Could be intermittent; try again.
          try:
            data = json.load(urllib2.urlopen(self.results_link))
          except (urllib2.HTTPError, socket.error):
            logging.error('Error retrieving URL ' + self.results_link)
            raise
        except ValueError:
          # If the build had an exception, the results might not be valid.
          data = None
        except:
          logging.error('Error retrieving URL ' + self.results_link)
          raise
        # And cache the newly downloaded data.
        cache_dir_path = os.path.dirname(cache_file_path)
        if not os.path.exists(cache_dir_path):
          os.makedirs(cache_dir_path)
        with open(cache_file_path, 'wb') as cache_file:
          cPickle.dump(data, cache_file, -1)
        self._results = data
    return self._results

  @property
  def stack_trace(self):
    if self._stack_trace is None:
      self._stack_trace = _ParseTraceFromLog(self.log)
    return self._stack_trace