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