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

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

Issue 1766583002: Added update_test_expectations script to remove non-flaky test expectations. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase after long break Created 4 years, 5 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
27 import argparse
28 import logging
29
30 from webkitpy.layout_tests.models.test_expectations import TestExpectations
31
32 _log = logging.getLogger(__name__)
33
34
35 def main(host, bot_test_expectations_factory, argv):
36 parser = argparse.ArgumentParser(epilog=__doc__, formatter_class=argparse.Ra wTextHelpFormatter)
37 parser.parse_args(argv)
38
39 port = host.port_factory.get()
40
41 logging.basicConfig(level=logging.INFO, format="%(message)s")
42
43 expectations_file = port.path_to_generic_test_expectations_file()
44 if not host.filesystem.isfile(expectations_file):
45 _log.warn("Didn't find generic expectations file at: " + expectations_fi le)
46 return None
47
48 remove_flakes_o_matic = RemoveFlakesOMatic(host,
49 port,
50 bot_test_expectations_factory)
51
52 test_expectations = remove_flakes_o_matic.get_updated_test_expectations()
53
54 remove_flakes_o_matic.write_test_expectations(test_expectations,
55 expectations_file)
56
57
58 class RemoveFlakesOMatic(object):
59 def __init__(self, host, port, bot_test_expectations_factory):
60 self._host = host
61 self._port = port
62 self._expectations_factory = bot_test_expectations_factory
63 self.builder_results_by_path = {}
64
65 def _can_delete_line(self, test_expectation_line):
66 """Returns whether a given line in the expectations can be removed.
67
68 Uses results from builder bots to determine if a given line is stale and
69 can safely be removed from the TestExpectations file. (i.e. remove if
70 the bots show that it's not flaky.) There's also some rules about when
71 not to remove lines (e.g. never remove lines with Rebaseline
72 expectations, don't remove non-flaky expectations, etc.)
73
74 Args:
75 test_expectation_line (TestExpectationLine): A line in the test
76 expectation file to test for possible removal.
77
78 Returns: True if the line can be removed, False otherwise.
79 """
80 expectations = test_expectation_line.expectations
81 if len(expectations) < 2:
82 return False
83
84 if self._has_unstrippable_expectations(expectations):
85 return False
86
87 if not self._has_pass_expectation(expectations):
88 return False
89
90 # The line can be deleted if the only expectation on the line that appea rs in the actual
91 # results is the PASS expectation.
92 for config in test_expectation_line.matching_configurations:
93 builder_name = self._host.builders.builder_name_for_specifiers(confi g.version, config.build_type)
94
95 if not builder_name:
96 _log.error('Failed to get builder for config [%s, %s, %s]' % (co nfig.version, config.architecture, config.build_type))
97 # TODO(bokan): Matching configurations often give us bots that d on't have a
98 # builder in builders.py's exact_matches. Should we ignore those or be conservative
99 # and assume we need these expectations to make a decision?
100 return False
101
102 if builder_name not in self.builder_results_by_path.keys():
103 _log.error('Failed to find results for builder "%s"' % builder_n ame)
104 return False
105
106 results_by_path = self.builder_results_by_path[builder_name]
107
108 # No results means the tests were all skipped or all results are pas sing.
109 if test_expectation_line.path not in results_by_path.keys():
110 continue
111
112 results_for_single_test = results_by_path[test_expectation_line.path ]
113
114 if self._expectations_that_were_met(test_expectation_line, results_f or_single_test) != set(['PASS']):
115 return False
116
117 return True
118
119 def _has_pass_expectation(self, expectations):
120 return 'PASS' in expectations
121
122 def _expectations_that_were_met(self, test_expectation_line, results_for_sin gle_test):
123 """Returns the set of expectations that appear in the given results.
124
125 e.g. If the test expectations is:
126 bug(test) fast/test.html [Crash Failure Pass]
127
128 And the results are ['TEXT', 'PASS', 'PASS', 'TIMEOUT']
129
130 This method would return [Pass Failure]
131
132 Args:
133 test_expectation_line: A TestExpectationLine object
134 results_for_single_test: A list of result strings.
135 e.g. ['IMAGE', 'IMAGE', 'PASS']
136
137 Returns:
138 A set containing expectations that occured in the results.
139 """
140 # TODO(bokan): Does this not exist in a more central place?
141 def replace_failing_with_fail(expectation):
142 if expectation in ('TEXT', 'IMAGE', 'IMAGE+TEXT', 'AUDIO'):
143 return 'FAIL'
144 else:
145 return expectation
146
147 actual_results = {replace_failing_with_fail(r) for r in results_for_sing le_test}
148
149 return set(test_expectation_line.expectations) & actual_results
150
151 def _has_unstrippable_expectations(self, expectations):
152 """ Returns whether any of the given expectations are considered unstrip pable.
153
154 Unstrippable expectations are those which should stop a line from being
155 removed regardless of builder bot results.
156
157 Args:
158 expectations: A list of string expectations.
159 E.g. ['PASS', 'FAIL' 'CRASH']
160
161 Returns:
162 True if at least one of the expectations is unstrippable. False
163 otherwise.
164 """
165 unstrippable_expectations = ('REBASELINE', 'NEEDSREBASELINE',
166 'NEEDSMANUALREBASELINE', 'SLOW',
167 'SKIP')
168 return any(s in expectations for s in unstrippable_expectations)
169
170 def get_updated_test_expectations(self):
171 """Filters out passing lines from TestExpectations file.
172
173 Reads the current TestExpectatoins file and, using results from the
174 build bots, removes lines that are passing. That is, removes lines that
175 were not needed to keep the bots green.
176
177 Returns: A TestExpectations object with the passing lines filtered out.
178 """
179 test_expectations = TestExpectations(self._port, include_overrides=False ).expectations()
180
181 self.builder_results_by_path = {}
182 for builder_name in self._host.builders.all_builder_names():
183 expectations_for_builder = (
184 self._expectations_factory.expectations_for_builder(builder_name )
185 )
186
187 if not expectations_for_builder:
188 # This is not fatal since we may not need to check these
189 # results. If we do need these results we'll log an error later
190 # when trying to check against them.
191 _log.warn('Downloaded results are missing results for builder "% s"' % builder_name)
192 continue
193
194 self.builder_results_by_path[builder_name] = (
195 expectations_for_builder.all_results_by_path()
196 )
197
198 expectations_to_remove = []
199
200 for expectation in test_expectations:
201 if self._can_delete_line(expectation):
202 expectations_to_remove.append(expectation)
203
204 for expectation in expectations_to_remove:
205 index = test_expectations.index(expectation)
206 test_expectations.remove(expectation)
207
208 # Remove associated comments and whitespace if we've removed the las t expectation under
209 # a comment block. Only remove a comment block if it's not separated from the test
210 # expectation line by whitespace.
211 if index == len(test_expectations) or test_expectations[index].is_wh itespace() or test_expectations[index].is_comment():
212 removed_whitespace = False
213 while index and test_expectations[index - 1].is_whitespace():
214 index = index - 1
215 test_expectations.pop(index)
216 removed_whitespace = True
217
218 if not removed_whitespace:
219 while index and test_expectations[index - 1].is_comment():
220 index = index - 1
221 test_expectations.pop(index)
222
223 while index and test_expectations[index - 1].is_whitespace() :
224 index = index - 1
225 test_expectations.pop(index)
226
227 return test_expectations
228
229 def write_test_expectations(self, test_expectations, test_expectations_file) :
230 """Writes the given TestExpectations object to the filesystem.
231
232 Args:
233 test_expectatoins: The TestExpectations object to write.
234 test_expectations_file: The full file path of the Blink
235 TestExpectations file. This file will be overwritten.
236 """
237 self._host.filesystem.write_text_file(
238 test_expectations_file,
239 TestExpectations.list_to_string(test_expectations, reconstitute_only _these=[]))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698