OLD | NEW |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Updates TestExpectations based on results in builder bots. | 5 """Updates TestExpectations based on results in builder bots. |
6 | 6 |
7 Scans the TestExpectations file and uses results from actual builder bots runs | 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. | 8 to remove tests that are marked as flaky but don't fail in the specified way. |
9 | 9 |
10 E.g. If a test has this expectation: | 10 E.g. If a test has this expectation: |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
53 remove_flakes_o_matic.write_test_expectations(test_expectations, | 53 remove_flakes_o_matic.write_test_expectations(test_expectations, |
54 expectations_file) | 54 expectations_file) |
55 return 0 | 55 return 0 |
56 | 56 |
57 | 57 |
58 class RemoveFlakesOMatic(object): | 58 class RemoveFlakesOMatic(object): |
59 def __init__(self, host, port, bot_test_expectations_factory): | 59 def __init__(self, host, port, bot_test_expectations_factory): |
60 self._host = host | 60 self._host = host |
61 self._port = port | 61 self._port = port |
62 self._expectations_factory = bot_test_expectations_factory | 62 self._expectations_factory = bot_test_expectations_factory |
63 self.builder_results_by_path = {} | 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 are 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: |
| 79 True if the line can be removed, False otherwise. |
| 80 """ |
| 81 expectations = test_expectation_line.expectations |
| 82 if len(expectations) < 2: |
| 83 return False |
| 84 |
| 85 # Don't check lines that have expectations like NeedsRebaseline or Skip. |
| 86 if self._has_unstrippable_expectations(expectations): |
| 87 return False |
| 88 |
| 89 # Don't check lines unless they're flaky. i.e. At least one expectation
is a PASS. |
| 90 if not self._has_pass_expectation(expectations): |
| 91 return False |
| 92 |
| 93 # The line can be deleted if the only expectation on the line that appea
rs in the actual |
| 94 # results is the PASS expectation. |
| 95 for config in test_expectation_line.matching_configurations: |
| 96 builder_name = self._host.builders.builder_name_for_specifiers(confi
g.version, config.build_type) |
| 97 |
| 98 if not builder_name: |
| 99 _log.error('Failed to get builder for config [%s, %s, %s]', |
| 100 config.version, config.architecture, config.build_typ
e) |
| 101 # TODO(bokan): Matching configurations often give us bots that d
on't have a |
| 102 # builder in builders.py's exact_matches. Should we ignore those
or be conservative |
| 103 # and assume we need these expectations to make a decision? |
| 104 return False |
| 105 |
| 106 if builder_name not in self._builder_results_by_path.keys(): |
| 107 _log.error('Failed to find results for builder "%s"' % builder_n
ame) |
| 108 return False |
| 109 |
| 110 results_by_path = self._builder_results_by_path[builder_name] |
| 111 |
| 112 # No results means the tests were all skipped or all results are pas
sing. |
| 113 if test_expectation_line.path not in results_by_path.keys(): |
| 114 continue |
| 115 |
| 116 results_for_single_test = results_by_path[test_expectation_line.path
] |
| 117 |
| 118 if self._expectations_that_were_met(test_expectation_line, results_f
or_single_test) != set(['PASS']): |
| 119 return False |
| 120 |
| 121 return True |
| 122 |
| 123 def _has_pass_expectation(self, expectations): |
| 124 return 'PASS' in expectations |
| 125 |
| 126 def _expectations_that_were_met(self, test_expectation_line, results_for_sin
gle_test): |
| 127 """Returns the set of expectations that appear in the given results. |
| 128 |
| 129 e.g. If the test expectations is "bug(test) fast/test.html [Crash Failur
e Pass]" |
| 130 and the results are ['TEXT', 'PASS', 'PASS', 'TIMEOUT'], then this metho
d would |
| 131 return [Pass Failure] since the Failure expectation is satisfied by 'TEX
T', Pass |
| 132 by 'PASS' but Crash doesn't appear in the results. |
| 133 |
| 134 Args: |
| 135 test_expectation_line: A TestExpectationLine object |
| 136 results_for_single_test: A list of result strings. |
| 137 e.g. ['IMAGE', 'IMAGE', 'PASS'] |
| 138 |
| 139 Returns: |
| 140 A set containing expectations that occured in the results. |
| 141 """ |
| 142 # TODO(bokan): Does this not exist in a more central place? |
| 143 def replace_failing_with_fail(expectation): |
| 144 if expectation in ('TEXT', 'IMAGE', 'IMAGE+TEXT', 'AUDIO'): |
| 145 return 'FAIL' |
| 146 else: |
| 147 return expectation |
| 148 |
| 149 actual_results = {replace_failing_with_fail(r) for r in results_for_sing
le_test} |
| 150 |
| 151 return set(test_expectation_line.expectations) & actual_results |
| 152 |
| 153 def _has_unstrippable_expectations(self, expectations): |
| 154 """Returns whether any of the given expectations are considered unstripp
able. |
| 155 |
| 156 Unstrippable expectations are those which should stop a line from being |
| 157 removed regardless of builder bot results. |
| 158 |
| 159 Args: |
| 160 expectations: A list of string expectations. |
| 161 E.g. ['PASS', 'FAIL' 'CRASH'] |
| 162 |
| 163 Returns: |
| 164 True if at least one of the expectations is unstrippable. False |
| 165 otherwise. |
| 166 """ |
| 167 unstrippable_expectations = ('REBASELINE', 'NEEDSREBASELINE', |
| 168 'NEEDSMANUALREBASELINE', 'SLOW', |
| 169 'SKIP') |
| 170 return any(s in expectations for s in unstrippable_expectations) |
| 171 |
| 172 def _get_builder_results_by_path(self): |
| 173 """Returns a dictionary of results for each builder. |
| 174 |
| 175 Returns a dictionary where each key is a builder and value is a dictiona
ry containing |
| 176 the distinct results for each test. E.g. |
| 177 |
| 178 { |
| 179 'WebKit Linux': { |
| 180 'test1.html': ['PASS', 'IMAGE'], |
| 181 'test2.html': ['PASS'], |
| 182 }, |
| 183 'WebKit Mac10.10': { |
| 184 'test1.html': ['PASS', 'IMAGE'], |
| 185 'test2.html': ['PASS', 'TEXT'], |
| 186 } |
| 187 } |
| 188 """ |
| 189 builder_results_by_path = {} |
| 190 for builder_name in self._host.builders.all_builder_names(): |
| 191 expectations_for_builder = ( |
| 192 self._expectations_factory.expectations_for_builder(builder_name
) |
| 193 ) |
| 194 |
| 195 if not expectations_for_builder: |
| 196 # This is not fatal since we may not need to check these |
| 197 # results. If we do need these results we'll log an error later |
| 198 # when trying to check against them. |
| 199 _log.warn('Downloaded results are missing results for builder "%
s"' % builder_name) |
| 200 continue |
| 201 |
| 202 builder_results_by_path[builder_name] = ( |
| 203 expectations_for_builder.all_results_by_path() |
| 204 ) |
| 205 return builder_results_by_path |
| 206 |
| 207 def _remove_associated_comments_and_whitespace(self, expectations, removed_i
ndex): |
| 208 """Removes comments and whitespace from an empty expectation block. |
| 209 |
| 210 If the removed expectation was the last in a block of expectations, this
method |
| 211 will remove any associated comments and whitespace. |
| 212 |
| 213 Args: |
| 214 expectations: A list of TestExpectationLine objects to be modified. |
| 215 removed_index: The index in the above list that was just removed. |
| 216 """ |
| 217 was_last_expectation_in_block = (removed_index == len(expectations) |
| 218 or expectations[removed_index].is_white
space() |
| 219 or expectations[removed_index].is_comme
nt()) |
| 220 |
| 221 # If the line immediately below isn't another expectation, then the bloc
k of |
| 222 # expectations definitely isn't empty so we shouldn't remove their assoc
iated comments. |
| 223 if not was_last_expectation_in_block: |
| 224 return |
| 225 |
| 226 did_remove_whitespace = False |
| 227 |
| 228 # We may have removed the last expectation in a block. Remove any whites
pace above. |
| 229 while removed_index > 0 and expectations[removed_index - 1].is_whitespac
e(): |
| 230 removed_index -= 1 |
| 231 expectations.pop(removed_index) |
| 232 did_remove_whitespace = True |
| 233 |
| 234 # If we did remove some whitespace then we shouldn't remove any comments
above it |
| 235 # since those won't have belonged to this expectation block. For example
, commented |
| 236 # out expectations, or a section header. |
| 237 if did_remove_whitespace: |
| 238 return |
| 239 |
| 240 # Remove all comments above the removed line. |
| 241 while removed_index > 0 and expectations[removed_index - 1].is_comment()
: |
| 242 removed_index -= 1 |
| 243 expectations.pop(removed_index) |
| 244 |
| 245 # Remove all whitespace above the comments. |
| 246 while removed_index > 0 and expectations[removed_index - 1].is_whitespac
e(): |
| 247 removed_index -= 1 |
| 248 expectations.pop(removed_index) |
64 | 249 |
65 def get_updated_test_expectations(self): | 250 def get_updated_test_expectations(self): |
66 """Filters out passing lines from TestExpectations file. | 251 """Filters out passing lines from TestExpectations file. |
67 | 252 |
68 Reads the current TestExpectations file and, using results from the | 253 Reads the current TestExpectations file and, using results from the |
69 build bots, removes lines that are passing. That is, removes lines that | 254 build bots, removes lines that are passing. That is, removes lines that |
70 were not needed to keep the bots green. | 255 were not needed to keep the bots green. |
71 | 256 |
72 Returns: | 257 Returns: |
73 A TestExpectations object with the passing lines filtered out. | 258 A TestExpectations object with the passing lines filtered out. |
74 """ | 259 """ |
| 260 |
| 261 self._builder_results_by_path = self._get_builder_results_by_path() |
| 262 |
| 263 expectations_to_remove = [] |
75 test_expectations = TestExpectations(self._port, include_overrides=False
).expectations() | 264 test_expectations = TestExpectations(self._port, include_overrides=False
).expectations() |
76 # TODO: Filter the expectations based on results. | 265 |
| 266 for expectation in test_expectations: |
| 267 if self._can_delete_line(expectation): |
| 268 expectations_to_remove.append(expectation) |
| 269 |
| 270 for expectation in expectations_to_remove: |
| 271 index = test_expectations.index(expectation) |
| 272 test_expectations.remove(expectation) |
| 273 |
| 274 # Remove associated comments and whitespace if we've removed the las
t expectation under |
| 275 # a comment block. Only remove a comment block if it's not separated
from the test |
| 276 # expectation line by whitespace. |
| 277 self._remove_associated_comments_and_whitespace(test_expectations, i
ndex) |
| 278 |
77 return test_expectations | 279 return test_expectations |
78 | 280 |
79 def write_test_expectations(self, test_expectations, test_expectations_file)
: | 281 def write_test_expectations(self, test_expectations, test_expectations_file)
: |
80 """Writes the given TestExpectations object to the filesystem. | 282 """Writes the given TestExpectations object to the filesystem. |
81 | 283 |
82 Args: | 284 Args: |
83 test_expectations: The TestExpectations object to write. | 285 test_expectations: The TestExpectations object to write. |
84 test_expectations_file: The full file path of the Blink | 286 test_expectations_file: The full file path of the Blink |
85 TestExpectations file. This file will be overwritten. | 287 TestExpectations file. This file will be overwritten. |
86 """ | 288 """ |
87 self._host.filesystem.write_text_file( | 289 self._host.filesystem.write_text_file( |
88 test_expectations_file, | 290 test_expectations_file, |
89 TestExpectations.list_to_string(test_expectations, reconstitute_only
_these=[])) | 291 TestExpectations.list_to_string(test_expectations, reconstitute_only
_these=[])) |
OLD | NEW |