1 # Copyright (C) 2013 Google Inc. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions are 5 # met: 6 # 7 # * Redistributions of source code must retain the above copyright 8 # notice, this list of conditions and the following disclaimer. 9 # * Redistributions in binary form must reproduce the above 10 # copyright notice, this list of conditions and the following disclaimer 11 # in the documentation and/or other materials provided with the 12 # distribution. 13 # * Neither the Google name nor the names of its 14 # contributors may be used to endorse or promote products derived from 15 # this software without specific prior written permission. 16 # 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 """Generates a fake TestExpectations file consisting of flaky tests from the bot 30 corresponding to the give port.""" 31 32 import json 33 import logging 34 import os.path 35 import urllib 36 import urllib2 37 38 from webkitpy.layout_tests.port import builders 39 from webkitpy.layout_tests.models.test_expectations import TestExpectations 40 from webkitpy.layout_tests.models.test_expectations import TestExpectationLine 41 42 43 _log = logging.getLogger(__name__) 44 45 46 # results.json v4 format: 47 # { 48 # 'version': 4, 49 # 'builder name' : { 50 # 'blinkRevision': [], 51 # 'tests': { 52 # 'directory' { # Each path component is a dictionary. 53 # 'testname.html': { 54 # 'expected' : 'FAIL', # expectation name 55 # 'results': [], # Run-length encoded result. 56 # 'times': [], 57 # 'bugs': [], # bug urls 58 # } 59 # } 60 # } 61 # 'buildNumbers': [], 62 # 'secondsSinceEpoch': [], 63 # 'chromeRevision': [], 64 # 'failure_map': { } # Map from letter code to expectation name. 65 # }, 66 class ResultsJSON(object): 67 TESTS_KEY = 'tests' 68 FAILURE_MAP_KEY = 'failure_map' 69 RESULTS_KEY = 'results' 70 EXPECTATIONS_KEY = 'expected' 71 BUGS_KEY = 'bugs' 72 RLE_LENGTH = 0 73 RLE_VALUE = 1 74 75 # results.json was originally designed to support 76 # multiple builders in one json file, so the builder_name 77 # is needed to figure out which builder this json file 78 # refers to (and thus where the results are stored) 79 def __init__(self, builder_name, json_dict): 80 self.builder_name = builder_name 81 self._json = json_dict 82 83 def _walk_trie(self, trie, parent_path): 84 for name, value in trie.items(): 85 full_path = os.path.join(parent_path, name) 86 87 # FIXME: If we ever have a test directory self.RESULTS_KEY 88 # ("results"), this logic will break! 89 if self.RESULTS_KEY not in value: 90 for path, results in self._walk_trie(value, full_path): 91 yield path, results 92 else: 93 yield full_path, value 94 95 def walk_results(self, full_path=''): 96 tests_trie = self._json[self.builder_name][self.TESTS_KEY] 97 return self._walk_trie(tests_trie, parent_path='') 98 99 def expectation_for_type(self, type_char): 100 return self._json[self.builder_name][self.FAILURE_MAP_KEY][type_char] 101 102 # Knowing how to parse the run-length-encoded values in results.json 103 # is a detail of this class. 104 def occurances_and_type_from_result_item(self, item): 105 return item[self.RLE_LENGTH], item[self.RLE_VALUE] 106 107 108 class BotTestExpectationsFactory(object): 109 RESULTS_URL_PREFIX = 'http://test-results.appspot.com/testfile?master=ChromiumWebkit&testtype=layout-tests&name=results-small.json&builder=' 110 111 def _results_json_for_port(self, port_name, builder_category): 112 if builder_category == 'deps': 113 builder = builders.deps_builder_name_for_port_name(port_name) 114 else: 115 builder = builders.builder_name_for_port_name(port_name) 116 117 if not builder: 118 return None 119 return self._results_json_for_builder(builder) 120 121 def _results_json_for_builder(self, builder): 122 results_url = self.RESULTS_URL_PREFIX + urllib.quote(builder) 123 try: 124 _log.debug('Fetching flakiness data from appengine.') 125 return ResultsJSON(builder, json.load(urllib2.urlopen(results_url))) 126 except urllib2.URLError as error: 127 _log.warning('Could not retrieve flakiness data from the bot. url: %s', results_url) 128 _log.warning(error) 129 130 def expectations_for_port(self, port_name, builder_category='layout'): 131 # FIXME: This only grabs release builder's flakiness data. If we're running debug, 132 # when we should grab the debug builder's data. 133 # FIXME: What should this do if there is no debug builder for a port, e.g. we have 134 # no debug XP builder? Should it use the release bot or another Windows debug bot? 135 # At the very least, it should log an error. 136 results_json = self._results_json_for_port(port_name, builder_category) 137 if not results_json: 138 return None 139 return BotTestExpectations(results_json) 140 141 def expectations_for_builder(self, builder): 142 results_json = self._results_json_for_builder(builder) 143 if not results_json: 144 return None 145 return BotTestExpectations(results_json) 146 147 class BotTestExpectations(object): 148 # FIXME: Get this from the json instead of hard-coding it. 149 RESULT_TYPES_TO_IGNORE = ['N', 'X', 'Y'] 150 151 # specifiers arg is used in unittests to avoid the static dependency on builders. 152 def __init__(self, results_json, specifiers=None): 153 self.results_json = results_json 154 self.specifiers = specifiers or set(builders.specifiers_for_builder(results_json.builder_name)) 155 156 def _line_from_test_and_flaky_types_and_bug_urls(self, test_path, flaky_types, bug_urls): 157 line = TestExpectationLine() 158 line.original_string = test_path 159 line.name = test_path 160 line.filename = test_path 161 line.path = test_path # FIXME: Should this be normpath? 162 line.matching_tests = [test_path] 163 line.bugs = bug_urls if bug_urls else ["Bug(gardener)"] 164 line.expectations = sorted(map(self.results_json.expectation_for_type, flaky_types)) 165 line.specifiers = self.specifiers 166 return line 167 168 def flakes_by_path(self, only_ignore_very_flaky): 169 """Sets test expectations to bot results if there are at least two distinct results.""" 170 flakes_by_path = {} 171 for test_path, entry in self.results_json.walk_results(): 172 results_dict = entry[self.results_json.RESULTS_KEY] 173 flaky_types = self._flaky_types_in_results(results_dict, only_ignore_very_flaky) 174 if len(flaky_types) <= 1: 175 continue 176 flakes_by_path[test_path] = sorted(map(self.results_json.expectation_for_type, flaky_types)) 177 return flakes_by_path 178 179 def unexpected_results_by_path(self): 180 """For tests with unexpected results, returns original expectations + results.""" 181 def exp_to_string(exp): 182 return TestExpectations.EXPECTATIONS_TO_STRING.get(exp, None).upper() 183 184 def string_to_exp(string): 185 # Needs a bit more logic than the method above, 186 # since a PASS is 0 and evaluates to False. 187 result = TestExpectations.EXPECTATIONS.get(string.lower(), None) 188 if not result is None: 189 return result 190 raise ValueError(string) 191 192 unexpected_results_by_path = {} 193 for test_path, entry in self.results_json.walk_results(): 194 # Expectations for this test. No expectation defaults to PASS. 195 exp_string = entry.get(self.results_json.EXPECTATIONS_KEY, u'PASS') 196 197 # All run-length-encoded results for this test. 198 results_dict = entry.get(self.results_json.RESULTS_KEY, {}) 199 200 # Set of expectations for this test. 201 expectations = set(map(string_to_exp, exp_string.split(' '))) 202 203 # Set of distinct results for this test. 204 result_types = self._flaky_types_in_results(results_dict) 205 206 # Distinct results as non-encoded strings. 207 result_strings = map(self.results_json.expectation_for_type, result_types) 208 209 # Distinct resulting expectations. 210 result_exp = map(string_to_exp, result_strings) 211 212 expected = lambda e: TestExpectations.result_was_expected(e, expectations, False) 213 214 additional_expectations = set(e for e in result_exp if not expected(e)) 215 216 # Test did not have unexpected results. 217 if not additional_expectations: 218 continue 219 220 expectations.update(additional_expectations) 221 unexpected_results_by_path[test_path] = sorted(map(exp_to_string, expectations)) 222 return unexpected_results_by_path 223 224 def expectation_lines(self, only_ignore_very_flaky=False): 225 lines = [] 226 for test_path, entry in self.results_json.walk_results(): 227 results_array = entry[self.results_json.RESULTS_KEY] 228 flaky_types = self._flaky_types_in_results(results_array, only_ignore_very_flaky) 229 if len(flaky_types) > 1: 230 bug_urls = entry.get(self.results_json.BUGS_KEY) 231 line = self._line_from_test_and_flaky_types_and_bug_urls(test_path, flaky_types, bug_urls) 232 lines.append(line) 233 return lines 234 235 def _flaky_types_in_results(self, run_length_encoded_results, only_ignore_very_flaky=False): 236 results_map = {} 237 seen_results = {} 238 239 for result_item in run_length_encoded_results: 240 _, result_type = self.results_json.occurances_and_type_from_result_item(result_item) 241 if result_type in self.RESULT_TYPES_TO_IGNORE: 242 continue 243 244 if only_ignore_very_flaky and result_type not in seen_results: 245 # Only consider a short-lived result if we've seen it more than once. 246 # Otherwise, we include lots of false-positives due to tests that fail 247 # for a couple runs and then start passing. 248 # FIXME: Maybe we should make this more liberal and consider it a flake 249 # even if we only see that failure once. 250 seen_results[result_type] = True 251 continue 252 253 results_map[result_type] = True 254 255 return results_map.keys() 256