blob: 26cf395df4035df202af93607d23accc1db37973 [file] [log] [blame]
# Copyright (c) 2009, Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import collections
import re
import urllib2
from webkitpy.common.memoized import memoized
from webkitpy.common.net.layout_test_results import LayoutTestResults
from webkitpy.common.net.network_transaction import NetworkTransaction
RESULTS_URL_BASE = 'https://storage.googleapis.com/chromium-layout-test-archives'
class Build(collections.namedtuple('Build', ('builder_name', 'build_number'))):
"""Represents a combination of builder and build number.
If build number is None, this represents the latest build
for a given builder.
"""
def __new__(cls, builder_name, build_number=None):
return super(Build, cls).__new__(cls, builder_name, build_number)
class BuildBot(object):
"""This class represents an interface to BuildBot-related functionality.
This includes fetching layout test results from Google Storage;
for more information about the layout test result format, see:
https://www.chromium.org/developers/the-json-test-results-format
"""
def results_url(self, builder_name, build_number=None):
"""Returns a URL for one set of archived layout test results.
If a build number is given, this will be results for a particular run;
otherwise it will be the accumulated results URL, which should have
the latest results.
"""
if build_number:
url_base = self.builder_results_url_base(builder_name)
return '%s/%s/layout-test-results' % (url_base, build_number)
return self.accumulated_results_url_base(builder_name)
def builder_results_url_base(self, builder_name):
"""Returns the URL for the given builder's directory in Google Storage.
Each builder has a directory in the GS bucket, and the directory
name is the builder name transformed to be more URL-friendly by
replacing all spaces, periods and parentheses with underscores.
"""
return '%s/%s' % (RESULTS_URL_BASE, re.sub('[ .()]', '_', builder_name))
@memoized
def fetch_retry_summary_json(self, build):
"""Fetches and returns the text of the archived retry_summary file.
This file is expected to contain the results of retrying layout tests
with and without a patch in a try job. It includes lists of tests
that failed only with the patch ("failures"), and tests that failed
both with and without ("ignored").
"""
url_base = '%s/%s' % (self.builder_results_url_base(build.builder_name), build.build_number)
return NetworkTransaction(return_none_on_404=True).run(
lambda: self._fetch_file(url_base, 'retry_summary.json'))
def accumulated_results_url_base(self, builder_name):
return self.builder_results_url_base(builder_name) + '/results/layout-test-results'
@memoized
def latest_layout_test_results(self, builder_name):
return self.fetch_layout_test_results(self.accumulated_results_url_base(builder_name))
@memoized
def fetch_results(self, build):
return self.fetch_layout_test_results(self.results_url(build.builder_name, build.build_number))
@memoized
def fetch_layout_test_results(self, results_url):
"""Returns a LayoutTestResults object for results fetched from a given URL."""
results_file = NetworkTransaction(return_none_on_404=True).run(
lambda: self._fetch_file(results_url, 'failing_results.json'))
revision = NetworkTransaction(return_none_on_404=True).run(
lambda: self._fetch_file(results_url, 'LAST_CHANGE'))
if not revision:
results_file = None
return LayoutTestResults.results_from_string(results_file, revision)
def _fetch_file(self, url_base, file_name):
# It seems this can return None if the url redirects and then returns 404.
# FIXME: This could use Web instead of using urllib2 directly.
result = urllib2.urlopen('%s/%s' % (url_base, file_name))
if not result:
return None
# urlopen returns a file-like object which sometimes works fine with str()
# but sometimes is a addinfourl object. In either case calling read() is correct.
return result.read()
def current_build_link(host):
"""Returns a link to the current job if running on buildbot, or None."""
master_name = host.environ.get('BUILDBOT_MASTERNAME')
builder_name = host.environ.get('BUILDBOT_BUILDERNAME')
build_number = host.environ.get('BUILDBOT_BUILDNUMBER')
if not (master_name and builder_name and build_number):
return None
return 'https://build.chromium.org/p/%s/builders/%s/builds/%s' % (master_name, builder_name, build_number)
def filter_latest_builds(builds):
"""Filters Build objects to include only the latest for each builder.
Args:
builds: A collection of Build objects.
Returns:
A list of Build objects; only one Build object per builder name. If
there are only Builds with no build number, then one is kept; if there
are Builds with build numbers, then the one with the highest build
number is kept.
"""
latest_builds = {}
for build in builds:
builder = build.builder_name
if builder not in latest_builds or build.build_number > latest_builds[builder].build_number:
latest_builds[builder] = build
return sorted(latest_builds.values())