| OLD | NEW |
| (Empty) |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Updates TestExpectations based on results in builder bots. | |
| 6 | |
| 7 Scans the TestExpectations file and uses results from actual builder bots runs | |
| 8 to remove tests that are marked as flaky but don't fail in the specified way. | |
| 9 | |
| 10 E.g. If a test has this expectation: | |
| 11 bug(test) fast/test.html [ Failure Pass ] | |
| 12 | |
| 13 And all the runs on builders have passed the line will be removed. | |
| 14 | |
| 15 Additionally, the runs don't all have to be Passing to remove the line; | |
| 16 as long as the non-Passing results are of a type not specified in the | |
| 17 expectation this line will be removed. For example, if this is the | |
| 18 expectation: | |
| 19 | |
| 20 bug(test) fast/test.html [ Crash Pass ] | |
| 21 | |
| 22 But the results on the builders show only Passes and Timeouts, the line | |
| 23 will be removed since there's no Crash results. | |
| 24 """ | |
| 25 | |
| 26 import argparse | |
| 27 import logging | |
| 28 import webbrowser | |
| 29 | |
| 30 from webkitpy.layout_tests.models.test_expectations import TestExpectations | |
| 31 from webkitpy.tool.commands.flaky_tests import FlakyTests | |
| 32 | |
| 33 _log = logging.getLogger(__name__) | |
| 34 | |
| 35 | |
| 36 def main(host, bot_test_expectations_factory, argv): | |
| 37 parser = argparse.ArgumentParser(epilog=__doc__, formatter_class=argparse.Ra
wTextHelpFormatter) | |
| 38 parser.add_argument('--verbose', '-v', action='store_true', default=False, h
elp='enable more verbose logging') | |
| 39 parser.add_argument('--show-results', | |
| 40 '-s', | |
| 41 action='store_true', | |
| 42 default=False, | |
| 43 help='Open results dashboard for all removed lines') | |
| 44 args = parser.parse_args(argv) | |
| 45 | |
| 46 logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, f
ormat='%(levelname)s: %(message)s') | |
| 47 | |
| 48 port = host.port_factory.get() | |
| 49 expectations_file = port.path_to_generic_test_expectations_file() | |
| 50 if not host.filesystem.isfile(expectations_file): | |
| 51 _log.warning("Didn't find generic expectations file at: " + expectations
_file) | |
| 52 return 1 | |
| 53 | |
| 54 remove_flakes_o_matic = RemoveFlakesOMatic(host, | |
| 55 port, | |
| 56 bot_test_expectations_factory, | |
| 57 webbrowser) | |
| 58 | |
| 59 test_expectations = remove_flakes_o_matic.get_updated_test_expectations() | |
| 60 | |
| 61 if args.show_results: | |
| 62 remove_flakes_o_matic.show_removed_results() | |
| 63 | |
| 64 remove_flakes_o_matic.write_test_expectations(test_expectations, | |
| 65 expectations_file) | |
| 66 return 0 | |
| 67 | |
| 68 | |
| 69 class RemoveFlakesOMatic(object): | |
| 70 | |
| 71 def __init__(self, host, port, bot_test_expectations_factory, browser): | |
| 72 self._host = host | |
| 73 self._port = port | |
| 74 self._expectations_factory = bot_test_expectations_factory | |
| 75 self._builder_results_by_path = {} | |
| 76 self._browser = browser | |
| 77 self._expectations_to_remove_list = None | |
| 78 | |
| 79 def _can_delete_line(self, test_expectation_line): | |
| 80 """Returns whether a given line in the expectations can be removed. | |
| 81 | |
| 82 Uses results from builder bots to determine if a given line is stale and | |
| 83 can safely be removed from the TestExpectations file. (i.e. remove if | |
| 84 the bots show that it's not flaky.) There are also some rules about when | |
| 85 not to remove lines (e.g. never remove lines with Rebaseline | |
| 86 expectations, don't remove non-flaky expectations, etc.) | |
| 87 | |
| 88 Args: | |
| 89 test_expectation_line (TestExpectationLine): A line in the test | |
| 90 expectation file to test for possible removal. | |
| 91 | |
| 92 Returns: | |
| 93 True if the line can be removed, False otherwise. | |
| 94 """ | |
| 95 expectations = test_expectation_line.expectations | |
| 96 if len(expectations) < 2: | |
| 97 return False | |
| 98 | |
| 99 # Don't check lines that have expectations like NeedsRebaseline or Skip. | |
| 100 if self._has_unstrippable_expectations(expectations): | |
| 101 return False | |
| 102 | |
| 103 # Don't check lines unless they're flaky. i.e. At least one expectation
is a PASS. | |
| 104 if not self._has_pass_expectation(expectations): | |
| 105 return False | |
| 106 | |
| 107 # Don't check lines that have expectations for directories, since | |
| 108 # the flakiness of all sub-tests isn't as easy to check. | |
| 109 if self._port.test_isdir(test_expectation_line.name): | |
| 110 return False | |
| 111 | |
| 112 # The line can be deleted if the only expectation on the line that appea
rs in the actual | |
| 113 # results is the PASS expectation. | |
| 114 builders_checked = [] | |
| 115 for config in test_expectation_line.matching_configurations: | |
| 116 builder_name = self._host.builders.builder_name_for_specifiers(confi
g.version, config.build_type) | |
| 117 | |
| 118 if not builder_name: | |
| 119 _log.debug('No builder with config %s', config) | |
| 120 # For many configurations, there is no matching builder in | |
| 121 # webkitpy/common/config/builders.py. We ignore these | |
| 122 # configurations and make decisions based only on configurations | |
| 123 # with actual builders. | |
| 124 continue | |
| 125 | |
| 126 builders_checked.append(builder_name) | |
| 127 | |
| 128 if builder_name not in self._builder_results_by_path.keys(): | |
| 129 _log.error('Failed to find results for builder "%s"', builder_na
me) | |
| 130 return False | |
| 131 | |
| 132 results_by_path = self._builder_results_by_path[builder_name] | |
| 133 | |
| 134 # No results means the tests were all skipped, or all results are pa
ssing. | |
| 135 if test_expectation_line.path not in results_by_path.keys(): | |
| 136 continue | |
| 137 | |
| 138 results_for_single_test = results_by_path[test_expectation_line.path
] | |
| 139 | |
| 140 if self._expectations_that_were_met(test_expectation_line, results_f
or_single_test) != set(['PASS']): | |
| 141 return False | |
| 142 | |
| 143 if builders_checked: | |
| 144 _log.debug('Checked builders:\n %s', '\n '.join(builders_checked)) | |
| 145 else: | |
| 146 _log.warning('No matching builders for line, deleting line.') | |
| 147 _log.info('Deleting line "%s"', test_expectation_line.original_string.st
rip()) | |
| 148 return True | |
| 149 | |
| 150 def _has_pass_expectation(self, expectations): | |
| 151 return 'PASS' in expectations | |
| 152 | |
| 153 def _expectations_that_were_met(self, test_expectation_line, results_for_sin
gle_test): | |
| 154 """Returns the set of expectations that appear in the given results. | |
| 155 | |
| 156 e.g. If the test expectations is "bug(test) fast/test.html [Crash Failur
e Pass]" | |
| 157 and the results are ['TEXT', 'PASS', 'PASS', 'TIMEOUT'], then this metho
d would | |
| 158 return [Pass Failure] since the Failure expectation is satisfied by 'TEX
T', Pass | |
| 159 by 'PASS' but Crash doesn't appear in the results. | |
| 160 | |
| 161 Args: | |
| 162 test_expectation_line: A TestExpectationLine object | |
| 163 results_for_single_test: A list of result strings. | |
| 164 e.g. ['IMAGE', 'IMAGE', 'PASS'] | |
| 165 | |
| 166 Returns: | |
| 167 A set containing expectations that occurred in the results. | |
| 168 """ | |
| 169 # TODO(bokan): Does this not exist in a more central place? | |
| 170 def replace_failing_with_fail(expectation): | |
| 171 if expectation in ('TEXT', 'IMAGE', 'IMAGE+TEXT', 'AUDIO'): | |
| 172 return 'FAIL' | |
| 173 else: | |
| 174 return expectation | |
| 175 | |
| 176 actual_results = {replace_failing_with_fail(r) for r in results_for_sing
le_test} | |
| 177 | |
| 178 return set(test_expectation_line.expectations) & actual_results | |
| 179 | |
| 180 def _has_unstrippable_expectations(self, expectations): | |
| 181 """Returns whether any of the given expectations are considered unstripp
able. | |
| 182 | |
| 183 Unstrippable expectations are those which should stop a line from being | |
| 184 removed regardless of builder bot results. | |
| 185 | |
| 186 Args: | |
| 187 expectations: A list of string expectations. | |
| 188 E.g. ['PASS', 'FAIL' 'CRASH'] | |
| 189 | |
| 190 Returns: | |
| 191 True if at least one of the expectations is unstrippable. False | |
| 192 otherwise. | |
| 193 """ | |
| 194 unstrippable_expectations = ('REBASELINE', 'NEEDSREBASELINE', | |
| 195 'NEEDSMANUALREBASELINE', 'SLOW', | |
| 196 'SKIP') | |
| 197 return any(s in expectations for s in unstrippable_expectations) | |
| 198 | |
| 199 def _get_builder_results_by_path(self): | |
| 200 """Returns a dictionary of results for each builder. | |
| 201 | |
| 202 Returns a dictionary where each key is a builder and value is a dictiona
ry containing | |
| 203 the distinct results for each test. E.g. | |
| 204 | |
| 205 { | |
| 206 'WebKit Linux Precise': { | |
| 207 'test1.html': ['PASS', 'IMAGE'], | |
| 208 'test2.html': ['PASS'], | |
| 209 }, | |
| 210 'WebKit Mac10.10': { | |
| 211 'test1.html': ['PASS', 'IMAGE'], | |
| 212 'test2.html': ['PASS', 'TEXT'], | |
| 213 } | |
| 214 } | |
| 215 """ | |
| 216 builder_results_by_path = {} | |
| 217 for builder_name in self._host.builders.all_continuous_builder_names(): | |
| 218 expectations_for_builder = ( | |
| 219 self._expectations_factory.expectations_for_builder(builder_name
) | |
| 220 ) | |
| 221 | |
| 222 if not expectations_for_builder: | |
| 223 # This is not fatal since we may not need to check these | |
| 224 # results. If we do need these results we'll log an error later | |
| 225 # when trying to check against them. | |
| 226 _log.warning('Downloaded results are missing results for builder
"%s"', builder_name) | |
| 227 continue | |
| 228 | |
| 229 builder_results_by_path[builder_name] = ( | |
| 230 expectations_for_builder.all_results_by_path() | |
| 231 ) | |
| 232 return builder_results_by_path | |
| 233 | |
| 234 def _remove_associated_comments_and_whitespace(self, expectations, removed_i
ndex): | |
| 235 """Removes comments and whitespace from an empty expectation block. | |
| 236 | |
| 237 If the removed expectation was the last in a block of expectations, this
method | |
| 238 will remove any associated comments and whitespace. | |
| 239 | |
| 240 Args: | |
| 241 expectations: A list of TestExpectationLine objects to be modified. | |
| 242 removed_index: The index in the above list that was just removed. | |
| 243 """ | |
| 244 was_last_expectation_in_block = (removed_index == len(expectations) | |
| 245 or expectations[removed_index].is_white
space() | |
| 246 or expectations[removed_index].is_comme
nt()) | |
| 247 | |
| 248 # If the line immediately below isn't another expectation, then the bloc
k of | |
| 249 # expectations definitely isn't empty so we shouldn't remove their assoc
iated comments. | |
| 250 if not was_last_expectation_in_block: | |
| 251 return | |
| 252 | |
| 253 did_remove_whitespace = False | |
| 254 | |
| 255 # We may have removed the last expectation in a block. Remove any whites
pace above. | |
| 256 while removed_index > 0 and expectations[removed_index - 1].is_whitespac
e(): | |
| 257 removed_index -= 1 | |
| 258 expectations.pop(removed_index) | |
| 259 did_remove_whitespace = True | |
| 260 | |
| 261 # If we did remove some whitespace then we shouldn't remove any comments
above it | |
| 262 # since those won't have belonged to this expectation block. For example
, commented | |
| 263 # out expectations, or a section header. | |
| 264 if did_remove_whitespace: | |
| 265 return | |
| 266 | |
| 267 # Remove all comments above the removed line. | |
| 268 while removed_index > 0 and expectations[removed_index - 1].is_comment()
: | |
| 269 removed_index -= 1 | |
| 270 expectations.pop(removed_index) | |
| 271 | |
| 272 # Remove all whitespace above the comments. | |
| 273 while removed_index > 0 and expectations[removed_index - 1].is_whitespac
e(): | |
| 274 removed_index -= 1 | |
| 275 expectations.pop(removed_index) | |
| 276 | |
| 277 def _expectations_to_remove(self): | |
| 278 """Computes and returns the expectation lines that should be removed. | |
| 279 | |
| 280 Returns: | |
| 281 A list of TestExpectationLine objects for lines that can be removed | |
| 282 from the test expectations file. The result is memoized so that | |
| 283 subsequent calls will not recompute the result. | |
| 284 """ | |
| 285 if self._expectations_to_remove_list is not None: | |
| 286 return self._expectations_to_remove_list | |
| 287 | |
| 288 self._builder_results_by_path = self._get_builder_results_by_path() | |
| 289 self._expectations_to_remove_list = [] | |
| 290 test_expectations = TestExpectations(self._port, include_overrides=False
).expectations() | |
| 291 | |
| 292 for expectation in test_expectations: | |
| 293 if self._can_delete_line(expectation): | |
| 294 self._expectations_to_remove_list.append(expectation) | |
| 295 | |
| 296 return self._expectations_to_remove_list | |
| 297 | |
| 298 def get_updated_test_expectations(self): | |
| 299 """Filters out passing lines from TestExpectations file. | |
| 300 | |
| 301 Reads the current TestExpectations file and, using results from the | |
| 302 build bots, removes lines that are passing. That is, removes lines that | |
| 303 were not needed to keep the bots green. | |
| 304 | |
| 305 Returns: | |
| 306 A TestExpectations object with the passing lines filtered out. | |
| 307 """ | |
| 308 | |
| 309 test_expectations = TestExpectations(self._port, include_overrides=False
).expectations() | |
| 310 for expectation in self._expectations_to_remove(): | |
| 311 index = test_expectations.index(expectation) | |
| 312 test_expectations.remove(expectation) | |
| 313 | |
| 314 # Remove associated comments and whitespace if we've removed the las
t expectation under | |
| 315 # a comment block. Only remove a comment block if it's not separated
from the test | |
| 316 # expectation line by whitespace. | |
| 317 self._remove_associated_comments_and_whitespace(test_expectations, i
ndex) | |
| 318 | |
| 319 return test_expectations | |
| 320 | |
| 321 def show_removed_results(self): | |
| 322 """Opens removed lines in the results dashboard. | |
| 323 | |
| 324 Opens the results dashboard in the browser, showing all the tests for li
nes that the script | |
| 325 removed from the TestExpectations file and allowing the user to manually
confirm the | |
| 326 results. | |
| 327 """ | |
| 328 removed_test_names = ','.join(x.name for x in self._expectations_to_remo
ve()) | |
| 329 url = FlakyTests.FLAKINESS_DASHBOARD_URL % removed_test_names | |
| 330 | |
| 331 _log.info('Opening results dashboard: ' + url) | |
| 332 self._browser.open(url) | |
| 333 | |
| 334 def write_test_expectations(self, test_expectations, test_expectations_file)
: | |
| 335 """Writes the given TestExpectations object to the filesystem. | |
| 336 | |
| 337 Args: | |
| 338 test_expectations: The TestExpectations object to write. | |
| 339 test_expectations_file: The full file path of the Blink | |
| 340 TestExpectations file. This file will be overwritten. | |
| 341 """ | |
| 342 self._host.filesystem.write_text_file( | |
| 343 test_expectations_file, | |
| 344 TestExpectations.list_to_string(test_expectations, reconstitute_only
_these=[])) | |
| OLD | NEW |