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