Index: third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/update_test_expectations.py |
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/update_test_expectations.py b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/update_test_expectations.py |
deleted file mode 100644 |
index 0ade6bf2c0acf25754a79f44199b5dcf8039bbf1..0000000000000000000000000000000000000000 |
--- a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/update_test_expectations.py |
+++ /dev/null |
@@ -1,344 +0,0 @@ |
-# Copyright 2016 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""Updates TestExpectations based on results in builder bots. |
- |
-Scans the TestExpectations file and uses results from actual builder bots runs |
-to remove tests that are marked as flaky but don't fail in the specified way. |
- |
-E.g. If a test has this expectation: |
- bug(test) fast/test.html [ Failure Pass ] |
- |
-And all the runs on builders have passed the line will be removed. |
- |
-Additionally, the runs don't all have to be Passing to remove the line; |
-as long as the non-Passing results are of a type not specified in the |
-expectation this line will be removed. For example, if this is the |
-expectation: |
- |
- bug(test) fast/test.html [ Crash Pass ] |
- |
-But the results on the builders show only Passes and Timeouts, the line |
-will be removed since there's no Crash results. |
-""" |
- |
-import argparse |
-import logging |
-import webbrowser |
- |
-from webkitpy.layout_tests.models.test_expectations import TestExpectations |
-from webkitpy.tool.commands.flaky_tests import FlakyTests |
- |
-_log = logging.getLogger(__name__) |
- |
- |
-def main(host, bot_test_expectations_factory, argv): |
- parser = argparse.ArgumentParser(epilog=__doc__, formatter_class=argparse.RawTextHelpFormatter) |
- parser.add_argument('--verbose', '-v', action='store_true', default=False, help='enable more verbose logging') |
- parser.add_argument('--show-results', |
- '-s', |
- action='store_true', |
- default=False, |
- help='Open results dashboard for all removed lines') |
- args = parser.parse_args(argv) |
- |
- logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, format='%(levelname)s: %(message)s') |
- |
- port = host.port_factory.get() |
- expectations_file = port.path_to_generic_test_expectations_file() |
- if not host.filesystem.isfile(expectations_file): |
- _log.warning("Didn't find generic expectations file at: " + expectations_file) |
- return 1 |
- |
- remove_flakes_o_matic = RemoveFlakesOMatic(host, |
- port, |
- bot_test_expectations_factory, |
- webbrowser) |
- |
- test_expectations = remove_flakes_o_matic.get_updated_test_expectations() |
- |
- if args.show_results: |
- remove_flakes_o_matic.show_removed_results() |
- |
- remove_flakes_o_matic.write_test_expectations(test_expectations, |
- expectations_file) |
- return 0 |
- |
- |
-class RemoveFlakesOMatic(object): |
- |
- def __init__(self, host, port, bot_test_expectations_factory, browser): |
- self._host = host |
- self._port = port |
- self._expectations_factory = bot_test_expectations_factory |
- self._builder_results_by_path = {} |
- self._browser = browser |
- self._expectations_to_remove_list = None |
- |
- def _can_delete_line(self, test_expectation_line): |
- """Returns whether a given line in the expectations can be removed. |
- |
- Uses results from builder bots to determine if a given line is stale and |
- can safely be removed from the TestExpectations file. (i.e. remove if |
- the bots show that it's not flaky.) There are also some rules about when |
- not to remove lines (e.g. never remove lines with Rebaseline |
- expectations, don't remove non-flaky expectations, etc.) |
- |
- Args: |
- test_expectation_line (TestExpectationLine): A line in the test |
- expectation file to test for possible removal. |
- |
- Returns: |
- True if the line can be removed, False otherwise. |
- """ |
- expectations = test_expectation_line.expectations |
- if len(expectations) < 2: |
- return False |
- |
- # Don't check lines that have expectations like NeedsRebaseline or Skip. |
- if self._has_unstrippable_expectations(expectations): |
- return False |
- |
- # Don't check lines unless they're flaky. i.e. At least one expectation is a PASS. |
- if not self._has_pass_expectation(expectations): |
- return False |
- |
- # Don't check lines that have expectations for directories, since |
- # the flakiness of all sub-tests isn't as easy to check. |
- if self._port.test_isdir(test_expectation_line.name): |
- return False |
- |
- # The line can be deleted if the only expectation on the line that appears in the actual |
- # results is the PASS expectation. |
- builders_checked = [] |
- for config in test_expectation_line.matching_configurations: |
- builder_name = self._host.builders.builder_name_for_specifiers(config.version, config.build_type) |
- |
- if not builder_name: |
- _log.debug('No builder with config %s', config) |
- # For many configurations, there is no matching builder in |
- # webkitpy/common/config/builders.py. We ignore these |
- # configurations and make decisions based only on configurations |
- # with actual builders. |
- continue |
- |
- builders_checked.append(builder_name) |
- |
- if builder_name not in self._builder_results_by_path.keys(): |
- _log.error('Failed to find results for builder "%s"', builder_name) |
- return False |
- |
- results_by_path = self._builder_results_by_path[builder_name] |
- |
- # No results means the tests were all skipped, or all results are passing. |
- if test_expectation_line.path not in results_by_path.keys(): |
- continue |
- |
- results_for_single_test = results_by_path[test_expectation_line.path] |
- |
- if self._expectations_that_were_met(test_expectation_line, results_for_single_test) != set(['PASS']): |
- return False |
- |
- if builders_checked: |
- _log.debug('Checked builders:\n %s', '\n '.join(builders_checked)) |
- else: |
- _log.warning('No matching builders for line, deleting line.') |
- _log.info('Deleting line "%s"', test_expectation_line.original_string.strip()) |
- return True |
- |
- def _has_pass_expectation(self, expectations): |
- return 'PASS' in expectations |
- |
- def _expectations_that_were_met(self, test_expectation_line, results_for_single_test): |
- """Returns the set of expectations that appear in the given results. |
- |
- e.g. If the test expectations is "bug(test) fast/test.html [Crash Failure Pass]" |
- and the results are ['TEXT', 'PASS', 'PASS', 'TIMEOUT'], then this method would |
- return [Pass Failure] since the Failure expectation is satisfied by 'TEXT', Pass |
- by 'PASS' but Crash doesn't appear in the results. |
- |
- Args: |
- test_expectation_line: A TestExpectationLine object |
- results_for_single_test: A list of result strings. |
- e.g. ['IMAGE', 'IMAGE', 'PASS'] |
- |
- Returns: |
- A set containing expectations that occurred in the results. |
- """ |
- # TODO(bokan): Does this not exist in a more central place? |
- def replace_failing_with_fail(expectation): |
- if expectation in ('TEXT', 'IMAGE', 'IMAGE+TEXT', 'AUDIO'): |
- return 'FAIL' |
- else: |
- return expectation |
- |
- actual_results = {replace_failing_with_fail(r) for r in results_for_single_test} |
- |
- return set(test_expectation_line.expectations) & actual_results |
- |
- def _has_unstrippable_expectations(self, expectations): |
- """Returns whether any of the given expectations are considered unstrippable. |
- |
- Unstrippable expectations are those which should stop a line from being |
- removed regardless of builder bot results. |
- |
- Args: |
- expectations: A list of string expectations. |
- E.g. ['PASS', 'FAIL' 'CRASH'] |
- |
- Returns: |
- True if at least one of the expectations is unstrippable. False |
- otherwise. |
- """ |
- unstrippable_expectations = ('REBASELINE', 'NEEDSREBASELINE', |
- 'NEEDSMANUALREBASELINE', 'SLOW', |
- 'SKIP') |
- return any(s in expectations for s in unstrippable_expectations) |
- |
- def _get_builder_results_by_path(self): |
- """Returns a dictionary of results for each builder. |
- |
- Returns a dictionary where each key is a builder and value is a dictionary containing |
- the distinct results for each test. E.g. |
- |
- { |
- 'WebKit Linux Precise': { |
- 'test1.html': ['PASS', 'IMAGE'], |
- 'test2.html': ['PASS'], |
- }, |
- 'WebKit Mac10.10': { |
- 'test1.html': ['PASS', 'IMAGE'], |
- 'test2.html': ['PASS', 'TEXT'], |
- } |
- } |
- """ |
- builder_results_by_path = {} |
- for builder_name in self._host.builders.all_continuous_builder_names(): |
- expectations_for_builder = ( |
- self._expectations_factory.expectations_for_builder(builder_name) |
- ) |
- |
- if not expectations_for_builder: |
- # This is not fatal since we may not need to check these |
- # results. If we do need these results we'll log an error later |
- # when trying to check against them. |
- _log.warning('Downloaded results are missing results for builder "%s"', builder_name) |
- continue |
- |
- builder_results_by_path[builder_name] = ( |
- expectations_for_builder.all_results_by_path() |
- ) |
- return builder_results_by_path |
- |
- def _remove_associated_comments_and_whitespace(self, expectations, removed_index): |
- """Removes comments and whitespace from an empty expectation block. |
- |
- If the removed expectation was the last in a block of expectations, this method |
- will remove any associated comments and whitespace. |
- |
- Args: |
- expectations: A list of TestExpectationLine objects to be modified. |
- removed_index: The index in the above list that was just removed. |
- """ |
- was_last_expectation_in_block = (removed_index == len(expectations) |
- or expectations[removed_index].is_whitespace() |
- or expectations[removed_index].is_comment()) |
- |
- # If the line immediately below isn't another expectation, then the block of |
- # expectations definitely isn't empty so we shouldn't remove their associated comments. |
- if not was_last_expectation_in_block: |
- return |
- |
- did_remove_whitespace = False |
- |
- # We may have removed the last expectation in a block. Remove any whitespace above. |
- while removed_index > 0 and expectations[removed_index - 1].is_whitespace(): |
- removed_index -= 1 |
- expectations.pop(removed_index) |
- did_remove_whitespace = True |
- |
- # If we did remove some whitespace then we shouldn't remove any comments above it |
- # since those won't have belonged to this expectation block. For example, commented |
- # out expectations, or a section header. |
- if did_remove_whitespace: |
- return |
- |
- # Remove all comments above the removed line. |
- while removed_index > 0 and expectations[removed_index - 1].is_comment(): |
- removed_index -= 1 |
- expectations.pop(removed_index) |
- |
- # Remove all whitespace above the comments. |
- while removed_index > 0 and expectations[removed_index - 1].is_whitespace(): |
- removed_index -= 1 |
- expectations.pop(removed_index) |
- |
- def _expectations_to_remove(self): |
- """Computes and returns the expectation lines that should be removed. |
- |
- Returns: |
- A list of TestExpectationLine objects for lines that can be removed |
- from the test expectations file. The result is memoized so that |
- subsequent calls will not recompute the result. |
- """ |
- if self._expectations_to_remove_list is not None: |
- return self._expectations_to_remove_list |
- |
- self._builder_results_by_path = self._get_builder_results_by_path() |
- self._expectations_to_remove_list = [] |
- test_expectations = TestExpectations(self._port, include_overrides=False).expectations() |
- |
- for expectation in test_expectations: |
- if self._can_delete_line(expectation): |
- self._expectations_to_remove_list.append(expectation) |
- |
- return self._expectations_to_remove_list |
- |
- def get_updated_test_expectations(self): |
- """Filters out passing lines from TestExpectations file. |
- |
- Reads the current TestExpectations file and, using results from the |
- build bots, removes lines that are passing. That is, removes lines that |
- were not needed to keep the bots green. |
- |
- Returns: |
- A TestExpectations object with the passing lines filtered out. |
- """ |
- |
- test_expectations = TestExpectations(self._port, include_overrides=False).expectations() |
- for expectation in self._expectations_to_remove(): |
- index = test_expectations.index(expectation) |
- test_expectations.remove(expectation) |
- |
- # Remove associated comments and whitespace if we've removed the last expectation under |
- # a comment block. Only remove a comment block if it's not separated from the test |
- # expectation line by whitespace. |
- self._remove_associated_comments_and_whitespace(test_expectations, index) |
- |
- return test_expectations |
- |
- def show_removed_results(self): |
- """Opens removed lines in the results dashboard. |
- |
- Opens the results dashboard in the browser, showing all the tests for lines that the script |
- removed from the TestExpectations file and allowing the user to manually confirm the |
- results. |
- """ |
- removed_test_names = ','.join(x.name for x in self._expectations_to_remove()) |
- url = FlakyTests.FLAKINESS_DASHBOARD_URL % removed_test_names |
- |
- _log.info('Opening results dashboard: ' + url) |
- self._browser.open(url) |
- |
- def write_test_expectations(self, test_expectations, test_expectations_file): |
- """Writes the given TestExpectations object to the filesystem. |
- |
- Args: |
- test_expectations: The TestExpectations object to write. |
- test_expectations_file: The full file path of the Blink |
- TestExpectations file. This file will be overwritten. |
- """ |
- self._host.filesystem.write_text_file( |
- test_expectations_file, |
- TestExpectations.list_to_string(test_expectations, reconstitute_only_these=[])) |