Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(8)

Side by Side Diff: third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/update_test_expectations.py

Issue 2895863003: Rename update-test-expectations -> update-flaky-expectations (Closed)
Patch Set: Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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=[]))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698