| # 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 |