Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(757)

Unified Diff: third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/update_test_expectations.py

Issue 1766583002: Added update_test_expectations script to remove non-flaky test expectations. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: tests++ Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
new file mode 100644
index 0000000000000000000000000000000000000000..a9f1c08f74f31df7d26719b138ee71d0a59c8a64
--- /dev/null
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/update_test_expectations.py
@@ -0,0 +1,172 @@
+# 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.
+
+import argparse
+
+from webkitpy.layout_tests.port import builders
+from webkitpy.layout_tests.models.test_expectations import TestExpectations
+from webkitpy.layout_tests.layout_package.bot_test_expectations import BotTestExpectationsFactory
+
+
+def main(host, argv):
+ parser = argparse.ArgumentParser(epilog=("""
+ 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 (e.g. Crash) this line will be remoed.
qyearsley 2016/03/10 19:13:58 Excellent, this is helpful. I'm not sure, but may
bokan 2016/03/16 20:24:53 Done.
+ """), formatter_class=argparse.RawTextHelpFormatter)
+ args = parser.parse_args(argv)
+
+ port = host.port_factory.get()
+ bot_test_expectations_factory = BotTestExpectationsFactory()
+
+ remove_flakes_o_matic = RemoveFlakesOMatic(host,
+ port,
+ bot_test_expectations_factory)
+
+ test_expectations = remove_flakes_o_matic.get_updated_test_expectations()
+
+ expectations_file = port.path_to_generic_test_expectations_file()
+
+ remove_flakes_o_matic.write_test_expectations(test_expectations,
+ expectations_file)
+
+
+class RemoveFlakesOMatic:
+ def __init__(self, host, port, bot_test_expectations_factory):
+ self._host = host
+ self._port = port
qyearsley 2016/03/10 19:13:58 Host is the host on which the expectations file wi
bokan 2016/03/16 20:24:53 I'm not 100% clear on what Host and Port are but I
+ self._expectations_factory = bot_test_expectations_factory
+
+ def _can_delete_line(self, test_expectation_line):
+ """Returns whether a given line in the expectations can be removed based
+ on its results on the bots (i.e. remove if the bots show its not flaky.)
+
+ Args:
+ test_expectation_line (TestExpectationLine): A line in the test
+ expectation file to test for possible removal.
+
qyearsley 2016/03/10 19:13:58 Optional docstring recommendations: - It's faste
bokan 2016/03/16 20:24:53 Thanks, done.
+ """
+ expectations = test_expectation_line.expectations
+ if len(expectations) < 2:
+ return False
+
+ if self._has_unstrippable_expectations(expectations):
+ return False
+
+ if not self._has_pass_expectation(expectations):
+ return False
+
+ # The line can be deleted if the only expectation on the line that appears in the actual
+ # results is the PASS expectation.
+ for config in test_expectation_line.matching_configurations:
+ builder_name = builders.builder_name_for_specifiers(config.version, config.build_type)
+
+ if not builder_name:
+ # TODO(bokan): We probably want to report a problem if we can't
+ # turn the specifiers into a builder name.
+ # TODO(bokan): Matching configurations often give us bots that don't have a
+ # builder in builders.py's exact_matches. Should we ignore those or be conservative
+ # and assume we need these expectations to make a decision? Also, testing isn't
+ # well suited to changing the list of builders/configurations so if we want to skip
+ # here the tests will have to specify results for all builders.
+ continue
+
+ if builder_name not in self.builder_results_by_path.keys():
+ # TODO(bokan): We probably want to report a problem if we can't get results for
+ # a bot.
qyearsley 2016/03/10 19:13:58 Sounds reasonable -- question is, should it be con
bokan 2016/03/16 20:24:53 I think not because, for example, if we have an ex
+ continue
+
+ results_by_path = self.builder_results_by_path[builder_name]
+
+ # No results means the test was 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._met_expectations(test_expectation_line, results_for_single_test) != set(['PASS']):
+ return False
+
+ return True
+
+ def _has_pass_expectation(self, expectations):
+ return 'PASS' in expectations
+
+ def _met_expectations(self, test_expectation_line, results_for_single_test):
qyearsley 2016/03/10 19:13:58 The name of this function currently suggests it mi
bokan 2016/03/16 20:24:53 I don't really like testing private internals but
+ # 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):
qyearsley 2016/03/10 19:13:58 expectations here is a list of strings, right?
bokan 2016/03/16 20:24:53 Right, docstring added.
+ unstrippable_expectations = ('REBASELINE', 'NEEDSREBASELINE',
+ 'NEEDSMANUALREBASELINE', 'SLOW')
+ return any(s in expectations for s in unstrippable_expectations)
+
+ def get_updated_test_expectations(self):
+ test_expectations = TestExpectations(self._port, include_overrides=False).expectations()
+
+ self.builder_results_by_path = {}
+ for builder_name in builders.all_builder_names():
+ expectations_for_builder = (
+ self._expectations_factory.expectations_for_builder(builder_name)
+ )
+
+ # TODO(bokan): What to do if we can't get the results for a builder?
+ if not expectations_for_builder:
+ continue
+
+ self.builder_results_by_path[builder_name] = (
+ expectations_for_builder.all_results_by_path()
+ )
qyearsley 2016/03/10 19:13:58 Does getting the builder results require making ne
bokan 2016/03/16 20:24:53 Not sure since that all happens elsewhere but my b
+
+ expectations_to_remove = []
+
+ for expectation in test_expectations:
+ if self._can_delete_line(expectation):
+ expectations_to_remove.append(expectation)
+
+ for expectation in 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 tests by
+ # whitespace.
+ if index == len(test_expectations) or test_expectations[index].is_whitespace_or_comment():
+ removed_whitespace = False
+ while index and test_expectations[index - 1].is_whitespace():
+ index = index - 1
+ test_expectations.pop(index)
+ removed_whitespace = True
+
+ if not removed_whitespace:
+ while index and test_expectations[index - 1].is_comment():
+ index = index - 1
+ test_expectations.pop(index)
+
+ while index and test_expectations[index - 1].is_whitespace():
+ index = index - 1
+ test_expectations.pop(index)
+
+ return test_expectations
+
+ def write_test_expectations(self, test_expectations, test_expectations_file):
+ self._host.filesystem.write_text_file(
+ test_expectations_file,
+ TestExpectations.list_to_string(test_expectations, reconstitute_only_these=[]))

Powered by Google App Engine
This is Rietveld 408576698