blob: 49b4e2870c48284d19904f0e4965eab5996acf1a [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 logging
import json
import re
import urllib
from blinkpy.common.memoized import memoized
from blinkpy.common.net.web import Web
from blinkpy.common.net.web_test_results import WebTestResults
from blinkpy.web_tests.layout_package import json_results_generator
_log = logging.getLogger(__name__)
TEST_RESULTS_SERVER = 'https://test-results.appspot.com'
RESULTS_URL_BASE = '%s/data/layout_results' % TEST_RESULTS_SERVER
RESULTS_SUMMARY_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 TestResultsFetcher(object):
"""This class represents an interface to test results for particular builds.
This includes fetching web test results from Google Storage;
for more information about the web test result format, see:
https://www.chromium.org/developers/the-json-test-results-format
"""
def __init__(self):
self.web = Web()
def results_url(self, builder_name, build_number=None, step_name=None):
"""Returns a URL for one set of archived web 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:
assert str(build_number).isdigit(), \
'expected numeric build number, got %s' % build_number
url_base = self.builder_results_url_base(builder_name)
if step_name is None:
step_name = self.get_layout_test_step_name(
Build(builder_name, build_number))
if step_name:
return '%s/%s/%s/layout-test-results' % (
url_base, build_number, urllib.quote(step_name))
return '%s/%s/layout-test-results' % (url_base, build_number)
return self.accumulated_results_url_base(builder_name)
def get_full_builder_url(self, url_base, builder_name):
""" Returns the url for a builder 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' % (url_base, re.sub('[ .()]', '_', builder_name))
def builder_results_url_base(self, builder_name):
"""Returns the URL for the given builder's directory in Google Storage.
"""
return self.get_full_builder_url(RESULTS_URL_BASE, builder_name)
def builder_retry_results_url_base(self, builder_name):
"""Returns the URL for the given builder's directory in Google Storage.
This is used for fetching the retry data which is now contained in
test_results_summary.json which cannot be fetched from
https://test-results.appspot.com anymore. Migrating this tool to use
resultDB is the ideal solution.
"""
return self.get_full_builder_url(RESULTS_SUMMARY_URL_BASE,
builder_name)
@memoized
def fetch_retry_summary_json(self, build):
"""Fetches and returns the text of the archived test_results_summary.json file.
This file is expected to contain the results of retrying web 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_retry_results_url_base(
build.builder_name), build.build_number)
# NOTE(crbug.com/1082907): We used to fetch retry_with_patch_summary.json from
# test-results.appspot.com. The file has been renamed and can no longer be
# accessed via test-results, so we download it from GCS directly.
# There is still a bug in uploading this json file for other platforms than linux.
# see https://crbug.com/1157202
return self.web.get_binary('%s/%s' %
(url_base, 'test_results_summary.json'),
return_none_on_404=True)
def accumulated_results_url_base(self, builder_name):
return self.builder_results_url_base(
builder_name) + '/results/layout-test-results'
@memoized
def fetch_results(self, build, full=False, step_name=None):
"""Returns a WebTestResults object for results from a given Build.
Uses full_results.json if full is True, otherwise failing_results.json.
"""
if not build.builder_name or not build.build_number:
_log.debug('Builder name or build number is None')
return None
step_name = step_name or self.get_layout_test_step_name(build)
return self.fetch_web_test_results(
self.results_url(
build.builder_name,
build.build_number,
step_name=step_name), full, step_name)
@memoized
def get_layout_test_step_name(self, build):
if not build.builder_name or not build.build_number:
_log.debug('Builder name or build number is None')
return None
url = '%s/testfile?%s' % (
TEST_RESULTS_SERVER,
urllib.urlencode({
'builder': build.builder_name,
'buildnumber': build.build_number,
'name': 'full_results.json',
# This forces the server to gives us JSON rather than an HTML page.
'callback': json_results_generator.JSON_CALLBACK,
}))
data = self.web.get_binary(url, return_none_on_404=True)
if not data:
_log.debug('Got 404 response from:\n%s', url)
return None
# Strip out the callback
data = json.loads(json_results_generator.strip_json_wrapper(data))
suites = [
entry['TestType'] for entry in data
# Some suite names are like 'blink_web_tests on Intel GPU (with
# patch)'. Only make sure it starts with blink_web_tests and
# runs with a patch. This should be changed eventually to use actual
# structured data from the test results server.
if re.match(r'(blink_web_tests|wpt_tests_suite).*\(with patch\)$',
entry['TestType'])
]
# In manual testing, I sometimes saw results where the same suite was
# repeated twice. De-duplicate here to try to catch this.
suites = list(set(suites))
if len(suites) != 1:
raise Exception(
'build %s on builder %s expected to only have one web test '
'step, instead has %s' % (build.build_number,
build.builder_name, suites))
return suites[0]
@memoized
def fetch_web_test_results(self, results_url, full=False, step_name=None):
"""Returns a WebTestResults object for results fetched from a given URL.
Uses full_results.json if full is True, otherwise failing_results.json.
"""
base_filename = 'full_results.json' if full else 'failing_results.json'
results_file = self.web.get_binary(
'%s/%s' % (results_url, base_filename), return_none_on_404=True)
if results_file is None:
_log.debug('Got 404 response from:\n%s/%s', results_url,
base_filename)
return None
return WebTestResults.results_from_string(results_file, step_name)
def fetch_webdriver_test_results(self, build, master):
if not build.builder_name or not build.build_number or not master:
_log.debug('Builder name or build number or master is None')
return None
url = '%s/testfile?%s' % (
TEST_RESULTS_SERVER,
urllib.urlencode({
'builder': build.builder_name,
'buildnumber': build.build_number,
'name': 'full_results.json',
'testtype': 'webdriver_tests_suite (with patch)',
'master': master
}))
data = self.web.get_binary(url, return_none_on_404=True)
if not data:
_log.debug('Got 404 response from:\n%s', url)
return None
return WebTestResults.results_from_string(data)
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())