Chromium Code Reviews| 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 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 55 expectations_file) | 55 expectations_file) |
| 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 | 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 | |
|
qyearsley
2016/07/08 17:55:03
There's also some rules -> There are also some rul
bokan
2016/07/08 21:13:00
Done.
| |
| 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. | |
|
qyearsley
2016/07/08 17:55:04
To be consistent with the docstrings below, this c
bokan
2016/07/08 21:13:00
Done.
| |
| 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 | |
|
qyearsley
2016/07/08 17:55:03
Is "PASS" an "unstrippable" expectation? Would it
bokan
2016/07/08 21:13:00
No, it's a bit subtle but has_unstrippable_expecta
| |
| 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)) | |
|
qyearsley
2016/07/08 17:55:03
For logging methods, if you give more than one arg
bokan
2016/07/08 21:13:00
Done.
| |
| 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] | |
|
qyearsley
2016/07/08 17:55:03
Maybe the "flowiness" of this section could be imp
bokan
2016/07/08 21:13:00
Done.
| |
| 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? | |
|
qyearsley
2016/07/08 17:55:03
Good question, it seems like it should, so if it d
bokan
2016/07/08 21:13:00
Ok, I'll leave this TODO here for now and either f
| |
| 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. | |
|
qyearsley
2016/07/08 17:55:04
Extra space character can be removed.
bokan
2016/07/08 21:13:00
Done.
| |
| 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 | |
| 65 def get_updated_test_expectations(self): | 170 def get_updated_test_expectations(self): |
| 66 """Filters out passing lines from TestExpectations file. | 171 """Filters out passing lines from TestExpectations file. |
| 67 | 172 |
| 68 Reads the current TestExpectatoins file and, using results from the | 173 Reads the current TestExpectatoins file and, using results from the |
| 69 build bots, removes lines that are passing. That is, removes lines that | 174 build bots, removes lines that are passing. That is, removes lines that |
| 70 were not needed to keep the bots green. | 175 were not needed to keep the bots green. |
| 71 | 176 |
| 72 Returns: A TestExpectations object with the passing lines filtered out. | 177 Returns: A TestExpectations object with the passing lines filtered out. |
| 73 """ | 178 """ |
| 74 test_expectations = TestExpectations(self._port, include_overrides=False ).expectations() | 179 test_expectations = TestExpectations(self._port, include_overrides=False ).expectations() |
| 75 # TODO: Filter the expectations based on results. | 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 ) | |
|
qyearsley
2016/07/08 17:55:04
This block of code (lines 181-196), which builds u
bokan
2016/07/08 21:13:00
Done.
| |
| 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 | |
|
qyearsley
2016/07/08 17:55:03
`index = index - 1` can also be written as `index
bokan
2016/07/08 21:13:00
Done.
| |
| 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) | |
|
qyearsley
2016/07/08 17:55:03
This block of code (removing associated comments a
bokan
2016/07/08 21:13:00
Done.
| |
| 226 | |
| 76 return test_expectations | 227 return test_expectations |
| 77 | 228 |
| 78 def write_test_expectations(self, test_expectations, test_expectations_file) : | 229 def write_test_expectations(self, test_expectations, test_expectations_file) : |
| 79 """Writes the given TestExpectations object to the filesystem. | 230 """Writes the given TestExpectations object to the filesystem. |
| 80 | 231 |
| 81 Args: | 232 Args: |
| 82 test_expectatoins: The TestExpectations object to write. | 233 test_expectatoins: The TestExpectations object to write. |
| 83 test_expectations_file: The full file path of the Blink | 234 test_expectations_file: The full file path of the Blink |
| 84 TestExpectations file. This file will be overwritten. | 235 TestExpectations file. This file will be overwritten. |
| 85 """ | 236 """ |
| 86 self._host.filesystem.write_text_file( | 237 self._host.filesystem.write_text_file( |
| 87 test_expectations_file, | 238 test_expectations_file, |
| 88 TestExpectations.list_to_string(test_expectations, reconstitute_only _these=[])) | 239 TestExpectations.list_to_string(test_expectations, reconstitute_only _these=[])) |
| OLD | NEW |