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 |