# 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