blob: 9b0f5d5dee3f1ecef25fcbbad2adffa6b8c80ce8 [file] [log] [blame]
# Copyright (C) 2013 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 Google name 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.
"""Generates a fake TestExpectations file consisting of flaky tests from the bot
corresponding to the give port."""
import json
import logging
import os.path
import urllib
import urllib2
from webkitpy.layout_tests.port import builders
from webkitpy.layout_tests.models.test_expectations import TestExpectations
from webkitpy.layout_tests.models.test_expectations import TestExpectationLine
_log = logging.getLogger(__name__)
# results.json v4 format:
# {
# 'version': 4,
# 'builder name' : {
# 'blinkRevision': [],
# 'tests': {
# 'directory' { # Each path component is a dictionary.
# 'testname.html': {
# 'expected' : 'FAIL', # expectation name
# 'results': [], # Run-length encoded result.
# 'times': [],
# 'bugs': [], # bug urls
# }
# }
# }
# 'buildNumbers': [],
# 'secondsSinceEpoch': [],
# 'chromeRevision': [],
# 'failure_map': { } # Map from letter code to expectation name.
# },
class ResultsJSON(object):
TESTS_KEY = 'tests'
FAILURE_MAP_KEY = 'failure_map'
RESULTS_KEY = 'results'
EXPECTATIONS_KEY = 'expected'
BUGS_KEY = 'bugs'
RLE_LENGTH = 0
RLE_VALUE = 1
# results.json was originally designed to support
# multiple builders in one json file, so the builder_name
# is needed to figure out which builder this json file
# refers to (and thus where the results are stored)
def __init__(self, builder_name, json_dict):
self.builder_name = builder_name
self._json = json_dict
def _walk_trie(self, trie, parent_path):
for name, value in trie.items():
full_path = os.path.join(parent_path, name)
# FIXME: If we ever have a test directory self.RESULTS_KEY
# ("results"), this logic will break!
if self.RESULTS_KEY not in value:
for path, results in self._walk_trie(value, full_path):
yield path, results
else:
yield full_path, value
def walk_results(self, full_path=''):
tests_trie = self._json[self.builder_name][self.TESTS_KEY]
return self._walk_trie(tests_trie, parent_path='')
def expectation_for_type(self, type_char):
return self._json[self.builder_name][self.FAILURE_MAP_KEY][type_char]
# Knowing how to parse the run-length-encoded values in results.json
# is a detail of this class.
def occurances_and_type_from_result_item(self, item):
return item[self.RLE_LENGTH], item[self.RLE_VALUE]
class BotTestExpectationsFactory(object):
RESULTS_URL_PREFIX = 'http://test-results.appspot.com/testfile?master=ChromiumWebkit&testtype=layout-tests&name=results-small.json&builder='
def _results_json_for_port(self, port_name, builder_category):
if builder_category == 'deps':
builder = builders.deps_builder_name_for_port_name(port_name)
else:
builder = builders.builder_name_for_port_name(port_name)
if not builder:
return None
return self._results_json_for_builder(builder)
def _results_json_for_builder(self, builder):
results_url = self.RESULTS_URL_PREFIX + urllib.quote(builder)
try:
_log.debug('Fetching flakiness data from appengine.')
return ResultsJSON(builder, json.load(urllib2.urlopen(results_url)))
except urllib2.URLError as error:
_log.warning('Could not retrieve flakiness data from the bot. url: %s', results_url)
_log.warning(error)
def expectations_for_port(self, port_name, builder_category='layout'):
# FIXME: This only grabs release builder's flakiness data. If we're running debug,
# when we should grab the debug builder's data.
# FIXME: What should this do if there is no debug builder for a port, e.g. we have
# no debug XP builder? Should it use the release bot or another Windows debug bot?
# At the very least, it should log an error.
results_json = self._results_json_for_port(port_name, builder_category)
if not results_json:
return None
return BotTestExpectations(results_json)
def expectations_for_builder(self, builder):
results_json = self._results_json_for_builder(builder)
if not results_json:
return None
return BotTestExpectations(results_json)
class BotTestExpectations(object):
# FIXME: Get this from the json instead of hard-coding it.
RESULT_TYPES_TO_IGNORE = ['N', 'X', 'Y']
# specifiers arg is used in unittests to avoid the static dependency on builders.
def __init__(self, results_json, specifiers=None):
self.results_json = results_json
self.specifiers = specifiers or set(builders.specifiers_for_builder(results_json.builder_name))
def _line_from_test_and_flaky_types_and_bug_urls(self, test_path, flaky_types, bug_urls):
line = TestExpectationLine()
line.original_string = test_path
line.name = test_path
line.filename = test_path
line.path = test_path # FIXME: Should this be normpath?
line.matching_tests = [test_path]
line.bugs = bug_urls if bug_urls else ["Bug(gardener)"]
line.expectations = sorted(map(self.results_json.expectation_for_type, flaky_types))
line.specifiers = self.specifiers
return line
def flakes_by_path(self, only_ignore_very_flaky):
"""Sets test expectations to bot results if there are at least two distinct results."""
flakes_by_path = {}
for test_path, entry in self.results_json.walk_results():
results_dict = entry[self.results_json.RESULTS_KEY]
flaky_types = self._flaky_types_in_results(results_dict, only_ignore_very_flaky)
if len(flaky_types) <= 1:
continue
flakes_by_path[test_path] = sorted(map(self.results_json.expectation_for_type, flaky_types))
return flakes_by_path
def unexpected_results_by_path(self):
"""For tests with unexpected results, returns original expectations + results."""
def exp_to_string(exp):
return TestExpectations.EXPECTATIONS_TO_STRING.get(exp, None).upper()
def string_to_exp(string):
# Needs a bit more logic than the method above,
# since a PASS is 0 and evaluates to False.
result = TestExpectations.EXPECTATIONS.get(string.lower(), None)
if not result is None:
return result
raise ValueError(string)
unexpected_results_by_path = {}
for test_path, entry in self.results_json.walk_results():
# Expectations for this test. No expectation defaults to PASS.
exp_string = entry.get(self.results_json.EXPECTATIONS_KEY, u'PASS')
# All run-length-encoded results for this test.
results_dict = entry.get(self.results_json.RESULTS_KEY, {})
# Set of expectations for this test.
expectations = set(map(string_to_exp, exp_string.split(' ')))
# Set of distinct results for this test.
result_types = self._flaky_types_in_results(results_dict)
# Distinct results as non-encoded strings.
result_strings = map(self.results_json.expectation_for_type, result_types)
# Distinct resulting expectations.
result_exp = map(string_to_exp, result_strings)
expected = lambda e: TestExpectations.result_was_expected(e, expectations, False)
additional_expectations = set(e for e in result_exp if not expected(e))
# Test did not have unexpected results.
if not additional_expectations:
continue
expectations.update(additional_expectations)
unexpected_results_by_path[test_path] = sorted(map(exp_to_string, expectations))
return unexpected_results_by_path
def expectation_lines(self, only_ignore_very_flaky=False):
lines = []
for test_path, entry in self.results_json.walk_results():
results_array = entry[self.results_json.RESULTS_KEY]
flaky_types = self._flaky_types_in_results(results_array, only_ignore_very_flaky)
if len(flaky_types) > 1:
bug_urls = entry.get(self.results_json.BUGS_KEY)
line = self._line_from_test_and_flaky_types_and_bug_urls(test_path, flaky_types, bug_urls)
lines.append(line)
return lines
def _flaky_types_in_results(self, run_length_encoded_results, only_ignore_very_flaky=False):
results_map = {}
seen_results = {}
for result_item in run_length_encoded_results:
_, result_type = self.results_json.occurances_and_type_from_result_item(result_item)
if result_type in self.RESULT_TYPES_TO_IGNORE:
continue
if only_ignore_very_flaky and result_type not in seen_results:
# Only consider a short-lived result if we've seen it more than once.
# Otherwise, we include lots of false-positives due to tests that fail
# for a couple runs and then start passing.
# FIXME: Maybe we should make this more liberal and consider it a flake
# even if we only see that failure once.
seen_results[result_type] = True
continue
results_map[result_type] = True
return results_map.keys()