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 """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=[])) |
OLD | NEW |