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 import argparse | |
6 | |
7 from webkitpy.layout_tests.port import builders | |
8 from webkitpy.layout_tests.models.test_expectations import TestExpectations | |
9 from webkitpy.layout_tests.layout_package.bot_test_expectations import BotTestEx pectationsFactory | |
10 | |
11 | |
12 def main(host, argv): | |
13 parser = argparse.ArgumentParser(epilog=(""" | |
14 Scans the TestExpectations file | |
15 and uses results from actual builder bots runs to remove tests that are | |
16 marked as flaky but don't fail in the specified way. | |
17 | |
18 E.g. If a test has this expectation: | |
19 bug(test) fast/test.html [ Failure Pass ] | |
20 | |
21 And all the runs on builders have passed the line will be removed. | |
22 | |
23 Additionally, the runs don't all have to be Passing to remove the line; | |
24 as long as the non-Passing results are of a type not specified in the | |
25 expectation this line will be removed. For example, if this is the | |
26 expectation: | |
27 | |
28 bug(test) fast/test.html [ Crash Pass ] | |
29 | |
30 But the results on the builders show only Passes and Timeouts, the line | |
31 will be removed since there's no Crash results. | |
32 """), formatter_class=argparse.RawTextHelpFormatter) | |
Dirk Pranke
2016/03/29 22:39:01
I would actually move this string up to the top-le
bokan
2016/04/05 12:28:57
Done.
| |
33 args = parser.parse_args(argv) | |
34 | |
35 port = host.port_factory.get() | |
36 bot_test_expectations_factory = BotTestExpectationsFactory() | |
37 | |
38 remove_flakes_o_matic = RemoveFlakesOMatic(host, | |
39 port, | |
40 bot_test_expectations_factory) | |
41 | |
42 test_expectations = remove_flakes_o_matic.get_updated_test_expectations() | |
43 | |
44 if not test_expectations: | |
45 # TODO(bokan): How should we report errors? | |
46 return None | |
47 | |
48 expectations_file = port.path_to_generic_test_expectations_file() | |
49 | |
50 remove_flakes_o_matic.write_test_expectations(test_expectations, | |
51 expectations_file) | |
qyearsley
2016/03/21 21:45:25
Note, the unit test doesn't cover main(), or write
Dirk Pranke
2016/03/29 22:39:01
By passing a MockHost() to main, you can (and shou
bokan
2016/04/05 12:28:56
Added a TODO, will add shortly.
| |
52 | |
53 | |
54 class RemoveFlakesOMatic: | |
Dirk Pranke
2016/03/29 22:39:01
derive this class from object, i.e. RemoveFlakesOM
bokan
2016/04/05 12:28:56
Done.
| |
55 def __init__(self, host, port, bot_test_expectations_factory): | |
56 self._host = host | |
57 self._port = port | |
58 self._expectations_factory = bot_test_expectations_factory | |
59 | |
60 def _can_delete_line(self, test_expectation_line): | |
61 """Returns whether a given line in the expectations can be removed. | |
62 | |
63 Uses results from builder bots to determine if a given line is stale and | |
64 can safely be removed from the TestExpectations file. (i.e. remove if | |
65 the bots show that it's not flaky.) There's also some rules about when | |
66 not to remove lines (e.g. never remove lines with Rebaseline | |
67 expectations, don't remove non-flaky expectations, etc.) | |
68 | |
69 Args: | |
70 test_expectation_line (TestExpectationLine): A line in the test | |
71 expectation file to test for possible removal. | |
72 | |
73 Returns: True if the line can be removed, False otherwise. | |
74 """ | |
75 expectations = test_expectation_line.expectations | |
76 if len(expectations) < 2: | |
77 return False | |
78 | |
79 if self._has_unstrippable_expectations(expectations): | |
80 return False | |
81 | |
82 if not self._has_pass_expectation(expectations): | |
83 return False | |
84 | |
85 # The line can be deleted if the only expectation on the line that appea rs in the actual | |
86 # results is the PASS expectation. | |
87 for config in test_expectation_line.matching_configurations: | |
88 builder_name = builders.builder_name_for_specifiers(config.version, config.build_type) | |
89 | |
90 if not builder_name: | |
91 # TODO(bokan): We probably want to report a problem if we can't | |
92 # turn the specifiers into a builder name. | |
93 # TODO(bokan): Matching configurations often give us bots that d on't have a | |
94 # builder in builders.py's exact_matches. Should we ignore those or be conservative | |
95 # and assume we need these expectations to make a decision? | |
Dirk Pranke
2016/03/29 22:39:01
Are you wondering about cases like Debug bots wher
bokan
2016/04/05 12:28:56
Yah, I think it's mostly debug, but not all. The m
| |
96 return False | |
97 | |
98 if builder_name not in self.builder_results_by_path.keys(): | |
99 # TODO(bokan): We probably want to report a problem if we can't get results for | |
100 # a bot specified in expectatoins. | |
101 return False | |
qyearsley
2016/03/21 21:45:25
For here and above, I agree that logging an error
bokan
2016/04/05 12:28:56
Added logging messages. Will add tests shortly.
| |
102 | |
103 results_by_path = self.builder_results_by_path[builder_name] | |
104 | |
105 # No results means the test was all skipped or all results are passi ng. | |
qyearsley
2016/03/21 21:45:25
test was all skipped -> tests were all skipped
bokan
2016/04/05 12:28:56
Done.
| |
106 if test_expectation_line.path not in results_by_path.keys(): | |
107 continue | |
qyearsley
2016/03/21 21:45:25
This line is not covered by the unit test right no
bokan
2016/04/05 12:28:56
Ack. Added a TODO, will do shortly.
| |
108 | |
109 results_for_single_test = results_by_path[test_expectation_line.path ] | |
110 | |
111 if self._expectations_that_were_met(test_expectation_line, results_f or_single_test) != set(['PASS']): | |
112 return False | |
113 | |
114 return True | |
115 | |
116 def _has_pass_expectation(self, expectations): | |
117 return 'PASS' in expectations | |
118 | |
119 def _expectations_that_were_met(self, test_expectation_line, results_for_sin gle_test): | |
120 """Returns the set of expectations that appear in the given results. | |
121 | |
122 e.g. If the test expectations is: | |
123 bug(test) fast/test.html [Crash Failure Pass] | |
124 | |
125 And the results are ['TEXT', 'PASS', 'PASS', 'TIMEOUT'] | |
126 | |
127 This method would return [Pass Failure] | |
128 | |
129 Args: | |
130 test_expectation_line: A TestExpectationLine object | |
131 results_for_single_test: A list of result strings. | |
132 e.g. ['IMAGE', 'IMAGE', 'PASS'] | |
133 | |
134 Returns: | |
135 A set containing expectations that occured in the results. | |
136 """ | |
137 # TODO(bokan): Does this not exist in a more central place? | |
138 def replace_failing_with_fail(expectation): | |
139 if expectation in ('TEXT', 'IMAGE', 'IMAGE+TEXT', 'AUDIO'): | |
140 return 'FAIL' | |
141 else: | |
142 return expectation | |
143 | |
144 actual_results = {replace_failing_with_fail(r) for r in results_for_sing le_test} | |
145 | |
146 return set(test_expectation_line.expectations) & actual_results | |
147 | |
148 def _has_unstrippable_expectations(self, expectations): | |
149 """ Returns whether any of the given expectations are considered unstrip pable. | |
150 | |
151 Unstrippable expectations are those which should stop a line from being | |
152 removed regardless of builder bot results. | |
153 | |
154 Args: | |
155 expectations: A list of string expectations. | |
156 E.g. ['PASS', 'FAIL' 'CRASH'] | |
157 | |
158 Returns: | |
159 True if at least one of the expectations is unstrippable. False | |
160 otherwise. | |
161 """ | |
162 unstrippable_expectations = ('REBASELINE', 'NEEDSREBASELINE', | |
163 'NEEDSMANUALREBASELINE', 'SLOW') | |
164 return any(s in expectations for s in unstrippable_expectations) | |
165 | |
166 def get_updated_test_expectations(self): | |
167 test_expectations = TestExpectations(self._port, include_overrides=False ).expectations() | |
168 | |
169 self.builder_results_by_path = {} | |
170 for builder_name in builders.all_builder_names(): | |
171 expectations_for_builder = ( | |
172 self._expectations_factory.expectations_for_builder(builder_name ) | |
173 ) | |
174 | |
175 # TODO(bokan): Error message? | |
176 if not expectations_for_builder: | |
177 return None | |
qyearsley
2016/03/21 21:45:25
Logging an error here sounds good to me. Also, thi
bokan
2016/04/05 12:28:56
Added log message. TODO and working on test.
| |
178 | |
179 self.builder_results_by_path[builder_name] = ( | |
180 expectations_for_builder.all_results_by_path() | |
181 ) | |
182 | |
183 expectations_to_remove = [] | |
184 | |
185 for expectation in test_expectations: | |
186 if self._can_delete_line(expectation): | |
187 expectations_to_remove.append(expectation) | |
188 | |
189 for expectation in expectations_to_remove: | |
190 index = test_expectations.index(expectation) | |
191 test_expectations.remove(expectation) | |
192 | |
193 # Remove associated comments and whitespace if we've removed the las t expectation under | |
194 # a comment block. Only remove a comment block if it's not separated from the tests by | |
195 # whitespace. | |
qyearsley
2016/03/21 21:45:25
Would it be correct to rephrase "if it's not separ
bokan
2016/04/05 12:28:56
Done. (replaced with "if it's not separated _from_
| |
196 if index == len(test_expectations) or test_expectations[index].is_wh itespace_or_comment(): | |
197 removed_whitespace = False | |
198 while index and test_expectations[index - 1].is_whitespace(): | |
199 index = index - 1 | |
200 test_expectations.pop(index) | |
201 removed_whitespace = True | |
qyearsley
2016/03/21 21:45:25
I think there may be no test case that covers this
bokan
2016/04/05 12:28:56
It should be covered by test_preserve_comments_and
| |
202 | |
203 if not removed_whitespace: | |
204 while index and test_expectations[index - 1].is_comment(): | |
205 index = index - 1 | |
206 test_expectations.pop(index) | |
207 | |
208 while index and test_expectations[index - 1].is_whitespace() : | |
209 index = index - 1 | |
210 test_expectations.pop(index) | |
211 | |
212 return test_expectations | |
213 | |
214 def write_test_expectations(self, test_expectations, test_expectations_file) : | |
215 self._host.filesystem.write_text_file( | |
216 test_expectations_file, | |
217 TestExpectations.list_to_string(test_expectations, reconstitute_only _these=[])) | |
OLD | NEW |