summaryrefslogtreecommitdiffstats
path: root/webkit/tools/layout_tests
diff options
context:
space:
mode:
authordpranke@google.com <dpranke@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-12-03 20:45:22 +0000
committerdpranke@google.com <dpranke@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-12-03 20:45:22 +0000
commitdc65679bf4733c06b71ca035ce05af9d55ba0759 (patch)
tree0172162b97aa5a1da06bc668f806cc83a4b2578c /webkit/tools/layout_tests
parent6c0c202dde2d8f5bbc0a7c857adaff6b3c29b6c4 (diff)
downloadchromium_src-dc65679bf4733c06b71ca035ce05af9d55ba0759.zip
chromium_src-dc65679bf4733c06b71ca035ce05af9d55ba0759.tar.gz
chromium_src-dc65679bf4733c06b71ca035ce05af9d55ba0759.tar.bz2
Rollback r33709, r33640 until we can figure out why the output is getting
interleaved in the buildbots on linux ... BUG=none R=ojan TEST=none Review URL: http://codereview.chromium.org/464022 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@33715 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'webkit/tools/layout_tests')
-rw-r--r--webkit/tools/layout_tests/layout_package/compare_failures.py216
-rw-r--r--webkit/tools/layout_tests/layout_package/compare_failures_unittest.py281
-rw-r--r--webkit/tools/layout_tests/layout_package/json_results_generator.py111
-rw-r--r--webkit/tools/layout_tests/layout_package/test_expectations.py264
-rw-r--r--webkit/tools/layout_tests/layout_package/test_failures.py56
-rw-r--r--webkit/tools/layout_tests/layout_package/test_shell_thread.py16
-rwxr-xr-xwebkit/tools/layout_tests/run_webkit_tests.py650
-rw-r--r--webkit/tools/layout_tests/update_expectations_from_dashboard.py6
8 files changed, 1006 insertions, 594 deletions
diff --git a/webkit/tools/layout_tests/layout_package/compare_failures.py b/webkit/tools/layout_tests/layout_package/compare_failures.py
new file mode 100644
index 0000000..010a584
--- /dev/null
+++ b/webkit/tools/layout_tests/layout_package/compare_failures.py
@@ -0,0 +1,216 @@
+# Copyright (c) 2006-2008 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.
+
+"""A helper class for comparing the failures and crashes from a test run
+against what we expected to happen (as specified in test_expectations.txt)."""
+
+import errno
+import os
+
+import path_utils
+import test_failures
+import test_expectations
+
+class CompareFailures:
+ # A list of which TestFailure classes count as a failure vs a crash.
+ FAILURE_TYPES = (test_failures.FailureTextMismatch,
+ test_failures.FailureImageHashMismatch)
+ TEXT_FAILURE_TYPES = (test_failures.FailureTextMismatch,)
+ IMAGE_FAILURE_TYPES = (test_failures.FailureImageHashMismatch,)
+ CRASH_TYPES = (test_failures.FailureCrash,)
+ HANG_TYPES = (test_failures.FailureTimeout,)
+ MISSING_TYPES = (test_failures.FailureMissingResult,
+ test_failures.FailureMissingImage,
+ test_failures.FailureMissingImageHash)
+
+
+ def __init__(self, test_files, test_failures, expectations, is_flaky):
+ """Calculate the regressions in this test run.
+
+ Args:
+ test_files is a set of the filenames of all the test cases we ran
+ test_failures is a dictionary mapping the test filename to a list of
+ TestFailure objects if the test failed
+ expectations is a TestExpectations object representing the
+ current test status
+ is_flaky is whether this set of failures represents tests that failed
+ the first time around, but passed on a subsequent run
+ """
+ self._test_files = test_files
+ self._test_failures = test_failures
+ self._expectations = expectations
+ self._is_flaky = is_flaky
+ self._CalculateRegressions()
+
+
+ def PrintFilesFromSet(self, filenames, header_text, output,
+ opt_expectations=None):
+ """A helper method to print a list of files to output.
+
+ Args:
+ filenames: a list of absolute filenames
+ header_text: a string to display before the list of filenames
+ output: file descriptor to write the results to.
+ opt_expectations: expectations that failed for this test
+ """
+ if not len(filenames):
+ return
+
+ filenames = list(filenames)
+ filenames.sort()
+
+ # Print real regressions.
+ if opt_expectations:
+ if self._is_flaky:
+ header_text = "Unexpected flakiness: " + header_text
+ else:
+ header_text = "Regressions: Unexpected " + header_text
+
+ output.write("\n%s (%d):\n" % (header_text, len(filenames)))
+ for filename in filenames:
+ output_string = " %s" % path_utils.RelativeTestFilename(filename)
+ if opt_expectations:
+ if self._is_flaky:
+ opt_expectations += (" " +
+ self._expectations.GetExpectationsString(filename))
+ output_string += " = %s" % opt_expectations
+
+ output.write(output_string + "\n")
+
+ def PrintRegressions(self, output):
+ """Write the regressions computed by _CalculateRegressions() to output. """
+
+ # Print unexpected passes by category.
+ passes = self._regressed_passes
+ self.PrintFilesFromSet(passes & self._expectations.GetFixableFailures(),
+ "Expected to fail, but passed",
+ output)
+ self.PrintFilesFromSet(passes & self._expectations.GetFixableTimeouts(),
+ "Expected to timeout, but passed",
+ output)
+ self.PrintFilesFromSet(passes & self._expectations.GetFixableCrashes(),
+ "Expected to crash, but passed",
+ output)
+
+ self.PrintFilesFromSet(passes & self._expectations.GetWontFixFailures(),
+ "Expected to fail (ignored), but passed",
+ output)
+ self.PrintFilesFromSet(passes & self._expectations.GetWontFixTimeouts(),
+ "Expected to timeout (ignored), but passed",
+ output)
+ # Crashes should never be deferred.
+ self.PrintFilesFromSet(passes & self._expectations.GetDeferredFailures(),
+ "Expected to fail (deferred), but passed",
+ output)
+ self.PrintFilesFromSet(passes & self._expectations.GetDeferredTimeouts(),
+ "Expected to timeout (deferred), but passed",
+ output)
+
+ self.PrintFilesFromSet(self._regressed_text_failures,
+ "text failures",
+ output,
+ 'TEXT')
+ self.PrintFilesFromSet(self._regressed_image_failures,
+ "image failures",
+ output,
+ 'IMAGE')
+ self.PrintFilesFromSet(self._regressed_image_plus_text_failures,
+ "image + text failures",
+ output,
+ 'IMAGE+TEXT')
+ self.PrintFilesFromSet(self._regressed_hangs,
+ "timeouts",
+ output,
+ 'TIMEOUT')
+ self.PrintFilesFromSet(self._regressed_crashes,
+ "crashes",
+ output,
+ 'CRASH')
+ self.PrintFilesFromSet(self._missing,
+ "missing expected results",
+ output,
+ 'MISSING')
+
+ def _CalculateRegressions(self):
+ """Calculate regressions from this run through the layout tests."""
+ worklist = self._test_files.copy()
+
+ passes = set()
+ crashes = set()
+ hangs = set()
+ missing = set()
+ image_failures = set()
+ text_failures = set()
+ image_plus_text_failures = set()
+
+ for test, failure_type_instances in self._test_failures.iteritems():
+ # Although each test can have multiple test_failures, we only put them
+ # into one list (either the crash list or the failure list). We give
+ # priority to a crash/timeout over others, and to missing results over
+ # a text mismatch.
+ failure_types = [type(f) for f in failure_type_instances]
+ is_crash = [True for f in failure_types if f in self.CRASH_TYPES]
+ is_hang = [True for f in failure_types if f in self.HANG_TYPES]
+ is_missing = [True for f in failure_types if f in self.MISSING_TYPES]
+ is_failure = [True for f in failure_types if f in self.FAILURE_TYPES]
+ is_text_failure = [True for f in failure_types if f in
+ self.TEXT_FAILURE_TYPES]
+ is_image_failure = [True for f in failure_types if f in
+ self.IMAGE_FAILURE_TYPES]
+
+ expectations = self._expectations.GetExpectations(test)
+ if is_crash:
+ if not test_expectations.CRASH in expectations: crashes.add(test)
+ elif is_hang:
+ if not test_expectations.TIMEOUT in expectations: hangs.add(test)
+ # Do not add to the missing list if a test is rebaselining and missing
+ # expected files.
+ elif (is_missing and
+ not self._expectations.IsRebaselining(test) and
+ not self._expectations.IsIgnored(test) and
+ (test_expectations.MISSING not in
+ self._expectations.GetExpectations(test))):
+ missing.add(test)
+ elif is_image_failure and is_text_failure:
+ if (not test_expectations.FAIL in expectations and
+ not test_expectations.IMAGE_PLUS_TEXT in expectations):
+ image_plus_text_failures.add(test)
+ elif is_image_failure:
+ if (not test_expectations.FAIL in expectations and
+ not test_expectations.IMAGE in expectations):
+ image_failures.add(test)
+ elif is_text_failure:
+ if (not test_expectations.FAIL in expectations and
+ not test_expectations.TEXT in expectations):
+ text_failures.add(test)
+ elif is_failure:
+ raise ValueError('unexpected failure type:' + f)
+ worklist.remove(test)
+
+ # When processing flaky failures, the list of actual failures is excluded,
+ # so don't look for unexpected passes.
+ if not self._is_flaky:
+ for test in worklist:
+ # Check that all passing tests are expected to pass.
+ expectations = self._expectations.GetExpectations(test)
+ if not test_expectations.PASS in expectations:
+ passes.add(test)
+
+ self._regressed_passes = passes
+ self._regressed_crashes = crashes
+ self._regressed_hangs = hangs
+ self._missing = missing
+ self._regressed_image_failures = image_failures
+ self._regressed_text_failures = text_failures
+ self._regressed_image_plus_text_failures = image_plus_text_failures
+
+ def GetRegressions(self):
+ """Returns a set of regressions from the test expectations. This is
+ used to determine which tests to list in results.html and the
+ right script exit code for the build bots. The list does not
+ include the unexpected passes."""
+ return (self._regressed_text_failures | self._regressed_image_failures |
+ self._regressed_image_plus_text_failures | self._regressed_hangs |
+ self._regressed_crashes | self._missing)
+
diff --git a/webkit/tools/layout_tests/layout_package/compare_failures_unittest.py b/webkit/tools/layout_tests/layout_package/compare_failures_unittest.py
new file mode 100644
index 0000000..202b195f7
--- /dev/null
+++ b/webkit/tools/layout_tests/layout_package/compare_failures_unittest.py
@@ -0,0 +1,281 @@
+#!/usr/bin/env python
+# Copyright (c) 2006-2008 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.
+
+"""Unittests to make sure we generate and update the expected-*.txt files
+properly after running layout tests."""
+
+import os
+import shutil
+import tempfile
+import unittest
+
+import compare_failures
+import path_utils
+import test_failures
+
+class CompareFailuresUnittest(unittest.TestCase):
+ def setUp(self):
+ """Makes a temporary results directory and puts expected-failures.txt and
+ expected-crashes.txt into it."""
+ self._tempdir = tempfile.mkdtemp()
+ # copy over expected-*.txt files
+ testdatadir = self.GetTestDataDir()
+ filenames = ("expected-passing.txt", "expected-failures.txt",
+ "expected-crashes.txt")
+ for filename in filenames:
+ # copyfile doesn't copy file permissions so we can delete the files later
+ shutil.copyfile(os.path.join(testdatadir, filename),
+ os.path.join(self._tempdir, filename))
+
+ def tearDown(self):
+ """Remove temp directory."""
+ shutil.rmtree(self._tempdir)
+ self._tempdir = None
+
+ ###########################################################################
+ # Tests
+ def testGenerateNewBaseline(self):
+ """Test the generation of new expected-*.txt files when either they don't
+ exist or the user explicitly asks to make new files."""
+ failures = self.GetTestFailures()
+
+ # Test to make sure we generate baseline files if the file doesn't exist.
+ os.remove(os.path.join(self.GetTmpDir(), 'expected-passing.txt'))
+ os.remove(os.path.join(self.GetTmpDir(), 'expected-failures.txt'))
+ os.remove(os.path.join(self.GetTmpDir(), 'expected-crashes.txt'))
+
+ # Test force generation of new baseline files with a new failure and one
+ # less passing.
+ pass_file = os.path.join(path_utils.LayoutTestsDir(), 'fast', 'pass1.html')
+ failures[pass_file] = [test_failures.FailureTextMismatch(None)]
+
+ cf = compare_failures.CompareFailures(self.GetTestFiles(), failures,
+ set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing-new-baseline.txt',
+ 'expected-failures-added.txt',
+ 'expected-crashes.txt')
+
+ def testPassingToFailure(self):
+ """When there's a new failure, we don't add it to the baseline."""
+ failures = self.GetTestFailures()
+
+ # Test case where we don't update new baseline. We have a new failure,
+ # but it shouldn't be added to the expected-failures.txt file.
+ pass_file = os.path.join(path_utils.LayoutTestsDir(), 'fast', 'pass1.html')
+ failures[pass_file] = [test_failures.FailureTextMismatch(None)]
+ self.CheckNoChanges(failures)
+
+ # Same thing as before: pass -> crash
+ failures[pass_file] = [test_failures.FailureCrash()]
+ self.CheckNoChanges(failures)
+
+ def testFailureToCrash(self):
+ """When there's a new crash, we don't add it to the baseline or remove it
+ from the failure list."""
+ failures = self.GetTestFailures()
+
+ # Test case where we don't update new baseline. A failure moving to a
+ # crash shouldn't be added to the expected-crashes.txt file.
+ failure_file = os.path.join(path_utils.LayoutTestsDir(),
+ 'fast', 'foo', 'fail1.html')
+ failures[failure_file] = [test_failures.FailureCrash()]
+ self.CheckNoChanges(failures)
+
+ def testFailureToPassing(self):
+ """This is better than before, so we should update the failure list."""
+ failures = self.GetTestFailures()
+
+ # Remove one of the failing test cases from the failures dictionary. This
+ # makes failure_file considered to be passing.
+ failure_file = os.path.join(path_utils.LayoutTestsDir(),
+ 'fast', 'bar', 'fail2.html')
+ del failures[failure_file]
+
+ cf = compare_failures.CompareFailures(self.GetTestFiles(), failures,
+ set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing-new-passing2.txt',
+ 'expected-failures-new-passing.txt',
+ 'expected-crashes.txt')
+
+ def testCrashToPassing(self):
+ """This is better than before, so we update the crashes file."""
+ failures = self.GetTestFailures()
+
+ crash_file = os.path.join(path_utils.LayoutTestsDir(),
+ 'fast', 'bar', 'betz', 'crash3.html')
+ del failures[crash_file]
+ cf = compare_failures.CompareFailures(self.GetTestFiles(), failures,
+ set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing-new-passing.txt',
+ 'expected-failures.txt',
+ 'expected-crashes-new-passing.txt')
+
+ def testCrashToFailure(self):
+ """This is better than before, so we should update both lists."""
+ failures = self.GetTestFailures()
+
+ crash_file = os.path.join(path_utils.LayoutTestsDir(),
+ 'fast', 'bar', 'betz', 'crash3.html')
+ failures[crash_file] = [test_failures.FailureTextMismatch(None)]
+ cf = compare_failures.CompareFailures(self.GetTestFiles(), failures,
+ set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing.txt',
+ 'expected-failures-new-crash.txt',
+ 'expected-crashes-new-passing.txt')
+
+ def testNewTestPass(self):
+ """After a merge, we need to update new passing tests properly."""
+ files = self.GetTestFiles()
+ new_test_file = os.path.join(path_utils.LayoutTestsDir(), "new-test.html")
+ files.add(new_test_file)
+ failures = self.GetTestFailures()
+
+ # New test file passing
+ cf = compare_failures.CompareFailures(files, failures, set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing-new-test.txt',
+ 'expected-failures.txt',
+ 'expected-crashes.txt')
+
+ def testNewTestFail(self):
+ """After a merge, we need to update new failing tests properly."""
+ files = self.GetTestFiles()
+ new_test_file = os.path.join(path_utils.LayoutTestsDir(), "new-test.html")
+ files.add(new_test_file)
+ failures = self.GetTestFailures()
+
+ # New test file failing
+ failures[new_test_file] = [test_failures.FailureTextMismatch(None)]
+ cf = compare_failures.CompareFailures(files, failures, set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing.txt',
+ 'expected-failures-new-test.txt',
+ 'expected-crashes.txt')
+
+ def testNewTestCrash(self):
+ """After a merge, we need to update new crashing tests properly."""
+ files = self.GetTestFiles()
+ new_test_file = os.path.join(path_utils.LayoutTestsDir(), "new-test.html")
+ files.add(new_test_file)
+ failures = self.GetTestFailures()
+
+ # New test file crashing
+ failures[new_test_file] = [test_failures.FailureCrash()]
+ cf = compare_failures.CompareFailures(files, failures, set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing.txt',
+ 'expected-failures.txt',
+ 'expected-crashes-new-test.txt')
+
+ def testHasNewFailures(self):
+ files = self.GetTestFiles()
+ failures = self.GetTestFailures()
+
+ # no changes, no new failures
+ cf = compare_failures.CompareFailures(files, failures, set(), set(),
+ self.GetTmpDir(), False)
+ self.failUnless(not cf.HasNewFailures())
+
+ # test goes from passing to failing
+ pass_file = os.path.join(path_utils.LayoutTestsDir(), 'fast', 'pass1.html')
+ failures[pass_file] = [test_failures.FailureTextMismatch(None)]
+ cf = compare_failures.CompareFailures(files, failures, set(), set(),
+ self.GetTmpDir(), False)
+ self.failUnless(cf.HasNewFailures())
+
+ # Failing to passing
+ failures = self.GetTestFailures()
+ failure_file = os.path.join(path_utils.LayoutTestsDir(),
+ 'fast', 'bar', 'fail2.html')
+ del failures[failure_file]
+ cf = compare_failures.CompareFailures(files, failures, set(), set(),
+ self.GetTmpDir(), False)
+ self.failUnless(not cf.HasNewFailures())
+
+ # A new test that fails, this doesn't count as a new failure.
+ new_test_file = os.path.join(path_utils.LayoutTestsDir(), "new-test.html")
+ files.add(new_test_file)
+ failures = self.GetTestFailures()
+ failures[new_test_file] = [test_failures.FailureCrash()]
+ cf = compare_failures.CompareFailures(files, failures, set(), set(),
+ self.GetTmpDir(), False)
+ self.failUnless(not cf.HasNewFailures())
+
+
+ ###########################################################################
+ # Helper methods
+ def CheckOutputEqualsExpectedFile(self, output, expected):
+ """Compares a file in our output dir against a file from the testdata
+ directory."""
+ output = os.path.join(self.GetTmpDir(), output)
+ expected = os.path.join(self.GetTestDataDir(), expected)
+ self.failUnlessEqual(open(output).read(), open(expected).read())
+
+ def CheckOutputWithExpectedFiles(self, passing, failing, crashing):
+ """Compare all three output files against three provided expected
+ files."""
+ self.CheckOutputEqualsExpectedFile('expected-passing.txt', passing)
+ self.CheckOutputEqualsExpectedFile('expected-failures.txt', failing)
+ self.CheckOutputEqualsExpectedFile('expected-crashes.txt', crashing)
+
+ def CheckNoChanges(self, failures):
+ """Verify that none of the expected-*.txt files have changed."""
+ cf = compare_failures.CompareFailures(self.GetTestFiles(), failures,
+ set(), set(),
+ self.GetTmpDir(), False)
+ cf.UpdateFailuresOnDisk()
+ self.CheckOutputWithExpectedFiles('expected-passing.txt',
+ 'expected-failures.txt',
+ 'expected-crashes.txt')
+
+ def GetTestDataDir(self):
+ return os.path.abspath('testdata')
+
+ def GetTmpDir(self):
+ return self._tempdir
+
+ def GetTestFiles(self):
+ """Get a set of files that includes the expected crashes and failures
+ along with two passing tests."""
+ layout_dir = path_utils.LayoutTestsDir()
+ files = [
+ 'fast\\pass1.html',
+ 'fast\\foo\\pass2.html',
+ 'fast\\foo\\crash1.html',
+ 'fast\\bar\\crash2.html',
+ 'fast\\bar\\betz\\crash3.html',
+ 'fast\\foo\\fail1.html',
+ 'fast\\bar\\fail2.html',
+ 'fast\\bar\\betz\\fail3.html',
+ ]
+
+ return set([os.path.join(layout_dir, f) for f in files])
+
+ def GetTestFailures(self):
+ """Get a dictionary representing the crashes and failures in the
+ expected-*.txt files."""
+ failures = {}
+ for filename in self.GetTestFiles():
+ if filename.find('crash') != -1:
+ failures[filename] = [test_failures.FailureCrash()]
+ elif filename.find('fail') != -1:
+ failures[filename] = [test_failures.FailureTextMismatch(None)]
+
+ return failures
+
+if '__main__' == __name__:
+ unittest.main()
+
diff --git a/webkit/tools/layout_tests/layout_package/json_results_generator.py b/webkit/tools/layout_tests/layout_package/json_results_generator.py
index b2bf2ae5..e88957f 100644
--- a/webkit/tools/layout_tests/layout_package/json_results_generator.py
+++ b/webkit/tools/layout_tests/layout_package/json_results_generator.py
@@ -12,7 +12,6 @@ import urllib2
import xml.dom.minidom
from layout_package import path_utils
-from layout_package import test_expectations
from layout_package import test_failures
sys.path.append(path_utils.PathFromBase('third_party'))
@@ -42,44 +41,41 @@ class JSONResultsGenerator:
WONTFIX = "wontfixCounts"
DEFERRED = "deferredCounts"
FIXABLE = "fixableCounts"
+ ALL_FIXABLE_COUNT = "allFixableCount"
FIXABLE_COUNT = "fixableCount"
-
- # Note that we omit test_expectations.FAIL from this list because
- # it should never show up (it's a legacy input expectation, never
- # an output expectation).
- FAILURE_TO_CHAR = {
- test_expectations.CRASH : "C",
- test_expectations.TIMEOUT : "T",
- test_expectations.IMAGE : "I",
- test_expectations.TEXT : "F",
- test_expectations.MISSING : "O",
- test_expectations.IMAGE_PLUS_TEXT : "Z"
- }
- FAILURE_CHARS = FAILURE_TO_CHAR.values()
-
+ """
+ C = CRASH
+ T = TIMEOUT
+ I = IMAGE
+ F = TEXT
+ O = OTHER
+ Z = TEXT+IMAGE
+ """
+ FAILURE_CHARS = ("C", "T", "I", "F", "O", "Z")
BUILDER_BASE_URL = "http://build.chromium.org/buildbot/layout_test_results/"
RESULTS_FILENAME = "results.json"
- def __init__(self, options, result_summary, individual_test_timings,
- results_file_base_path, all_tests):
+ def __init__(self, options, failures, individual_test_timings,
+ results_file_base_path, all_tests, result_summary):
"""Modifies the results.json file. Grabs it off the archive directory if it
is not found locally.
Args
options: a dictionary of command line options
- result_summary: ResultsSummary object containing failure counts for
- different groups of tests.
+ failures: Map of test name to list of failures.
individual_test_times: Map of test name to a tuple containing the
test_run-time.
results_file_base_path: Absolute path to the directory containing the
results json file.
all_tests: List of all the tests that were run.
+ result_summary: ResultsSummary object containing failure counts for
+ different groups of tests.
"""
# Make sure all test paths are relative to the layout test root directory.
self._failures = {}
- for test in result_summary.failures:
+ for test in failures:
test_path = self._GetPathRelativeToLayoutTestRoot(test)
- self._failures[test_path] = result_summary.failures[test]
+ self._failures[test_path] = failures[test]
self._all_tests = [self._GetPathRelativeToLayoutTestRoot(test)
for test in all_tests]
@@ -265,46 +261,43 @@ class JSONResultsGenerator:
builder.
"""
self._InsertItemIntoRawList(results_for_builder,
- len(set(self._result_summary.failures.keys()) &
- self._result_summary.tests_by_timeline[test_expectations.NOW]),
+ self._result_summary.fixable_count,
self.FIXABLE_COUNT)
self._InsertItemIntoRawList(results_for_builder,
- self._GetFailureSummaryEntry(test_expectations.DEFER),
+ self._result_summary.all_fixable_count,
+ self.ALL_FIXABLE_COUNT)
+
+ self._InsertItemIntoRawList(results_for_builder,
+ self._GetFailureSummaryEntry(self._result_summary.deferred),
self.DEFERRED)
self._InsertItemIntoRawList(results_for_builder,
- self._GetFailureSummaryEntry(test_expectations.WONTFIX),
+ self._GetFailureSummaryEntry(self._result_summary.wontfix),
self.WONTFIX)
self._InsertItemIntoRawList(results_for_builder,
- self._GetFailureSummaryEntry(test_expectations.NOW),
+ self._GetFailureSummaryEntry(self._result_summary.fixable),
self.FIXABLE)
- def _GetFailureSummaryEntry(self, timeline):
+ def _GetFailureSummaryEntry(self, result_summary_entry):
"""Creates a summary object to insert into the JSON.
Args:
- summary ResultSummary object with test results
- timeline current test_expectations timeline to build entry for (e.g.,
- test_expectations.NOW, etc.)
+ result_summary_entry: ResultSummaryEntry for a group of tests
+ (e.g. deferred tests).
"""
entry = {}
- summary = self._result_summary
- timeline_tests = summary.tests_by_timeline[timeline]
- entry[self.SKIP_RESULT] = len(
- summary.tests_by_expectation[test_expectations.SKIP] & timeline_tests)
- entry[self.SKIP_RESULT] = len(
- summary.tests_by_expectation[test_expectations.PASS] & timeline_tests)
- for failure_type in summary.tests_by_expectation.keys():
- if failure_type not in self.FAILURE_TO_CHAR:
- continue
- count = len(summary.tests_by_expectation[failure_type] & timeline_tests)
- entry[self.FAILURE_TO_CHAR[failure_type]] = count
+ entry[self.SKIP_RESULT] = result_summary_entry.skip_count
+ entry[self.PASS_RESULT] = result_summary_entry.pass_count
+ for char in self.FAILURE_CHARS:
+ # There can be multiple failures that map to "O", so keep existing entry
+ # values if they already exist.
+ count = entry.get(char, 0)
+
+ for failure in result_summary_entry.failure_counts:
+ if char == self._GetResultsCharForFailure([failure]):
+ count = result_summary_entry.failure_counts[failure]
+ entry[char] = count
return entry
- def _GetResultsCharForTest(self, test):
- """Returns a single character description of the test result."""
- result = test_failures.DetermineResultType(self._failures[test])
- return self.FAILURE_TO_CHAR[result]
-
def _InsertItemIntoRawList(self, results_for_builder, item, key):
"""Inserts the item into the list with the given key in the results for
this builder. Creates the list if no such list exists.
@@ -362,6 +355,32 @@ class JSONResultsGenerator:
results_for_builder[self.TESTS] = {}
return results_for_builder
+ def _GetResultsCharForTest(self, test):
+ """Returns the worst failure from the list of failures for this test
+ since we can only show one failure per run for each test on the dashboard.
+ """
+ failures = [failure.__class__ for failure in self._failures[test]]
+ return self._GetResultsCharForFailure(failures)
+
+ def _GetResultsCharForFailure(self, failures):
+ """Returns the worst failure from the list of failures
+ since we can only show one failure per run for each test on the dashboard.
+ """
+ has_text_failure = test_failures.FailureTextMismatch in failures
+
+ if test_failures.FailureCrash in failures:
+ return "C"
+ elif test_failures.FailureTimeout in failures:
+ return "T"
+ elif test_failures.FailureImageHashMismatch in failures:
+ if has_text_failure:
+ return "Z"
+ return "I"
+ elif has_text_failure:
+ return "F"
+ else:
+ return "O"
+
def _RemoveItemsOverMaxNumberOfBuilds(self, encoded_list):
"""Removes items from the run-length encoded list after the final item that
exceeds the max number of builds to track.
diff --git a/webkit/tools/layout_tests/layout_package/test_expectations.py b/webkit/tools/layout_tests/layout_package/test_expectations.py
index 1578a57..ca78647 100644
--- a/webkit/tools/layout_tests/layout_package/test_expectations.py
+++ b/webkit/tools/layout_tests/layout_package/test_expectations.py
@@ -12,13 +12,14 @@ import re
import sys
import time
import path_utils
+import compare_failures
sys.path.append(path_utils.PathFromBase('third_party'))
import simplejson
# Test expectation and modifier constants.
(PASS, FAIL, TEXT, IMAGE, IMAGE_PLUS_TEXT, TIMEOUT, CRASH, SKIP, WONTFIX,
- DEFER, SLOW, REBASELINE, MISSING, FLAKY, NOW, NONE) = range(16)
+ DEFER, SLOW, REBASELINE, MISSING, NONE) = range(14)
# Test expectation file update action constants
(NO_CHANGE, REMOVE_TEST, REMOVE_PLATFORM, ADD_PLATFORMS_EXCEPT_THIS) = range(4)
@@ -40,6 +41,85 @@ class TestExpectations:
def GetExpectationsJsonForAllPlatforms(self):
return self._expected_failures.GetExpectationsJsonForAllPlatforms()
+ def GetFixable(self):
+ return self._expected_failures.GetTestSet(NONE)
+
+ def GetFixableFailures(self):
+ # Avoid including TIMEOUT CRASH FAIL tests in the fail numbers since
+ # crashes and timeouts are higher priority.
+ return ((self._expected_failures.GetTestSet(NONE, FAIL) |
+ self._expected_failures.GetTestSet(NONE, IMAGE) |
+ self._expected_failures.GetTestSet(NONE, TEXT) |
+ self._expected_failures.GetTestSet(NONE, IMAGE_PLUS_TEXT)) -
+ self._expected_failures.GetTestSet(NONE, TIMEOUT) -
+ self._expected_failures.GetTestSet(NONE, CRASH))
+
+ def GetFixableTimeouts(self):
+ # Avoid including TIMEOUT CRASH tests in the timeout numbers since crashes
+ # are higher priority.
+ return (self._expected_failures.GetTestSet(NONE, TIMEOUT) -
+ self._expected_failures.GetTestSet(NONE, CRASH))
+
+ def GetFixableCrashes(self):
+ return self._expected_failures.GetTestSet(NONE, CRASH)
+
+ def GetFixableSkipped(self):
+ return (self._expected_failures.GetTestSet(SKIP) -
+ self._expected_failures.GetTestSet(WONTFIX) -
+ self._expected_failures.GetTestSet(DEFER))
+
+ def GetDeferred(self):
+ return self._expected_failures.GetTestSet(DEFER, include_skips=False)
+
+ def GetDeferredSkipped(self):
+ return (self._expected_failures.GetTestSet(SKIP) &
+ self._expected_failures.GetTestSet(DEFER))
+
+ def GetDeferredFailures(self):
+ return (
+ self._expected_failures.GetTestSet(DEFER, FAIL, include_skips=False) |
+ self._expected_failures.GetTestSet(DEFER, IMAGE, include_skips=False) |
+ self._expected_failures.GetTestSet(DEFER, TEXT, include_skips=False) |
+ self._expected_failures.GetTestSet(DEFER, IMAGE_PLUS_TEXT,
+ include_skips=False))
+
+
+ def GetDeferredTimeouts(self):
+ return self._expected_failures.GetTestSet(DEFER, TIMEOUT,
+ include_skips=False)
+
+ def GetWontFix(self):
+ return self._expected_failures.GetTestSet(WONTFIX, include_skips=False)
+
+ def GetWontFixSkipped(self):
+ return (self._expected_failures.GetTestSet(WONTFIX) &
+ self._expected_failures.GetTestSet(SKIP))
+
+ def GetWontFixFailures(self):
+ # Avoid including TIMEOUT CRASH FAIL tests in the fail numbers since
+ # crashes and timeouts are higher priority.
+ return (
+ (self._expected_failures.GetTestSet(WONTFIX, FAIL,
+ include_skips=False) |
+ self._expected_failures.GetTestSet(WONTFIX, IMAGE,
+ include_skips=False) |
+ self._expected_failures.GetTestSet(WONTFIX, TEXT,
+ include_skips=False) |
+ self._expected_failures.GetTestSet(WONTFIX, IMAGE_PLUS_TEXT,
+ include_skips=False)) -
+ self._expected_failures.GetTestSet(WONTFIX, TIMEOUT,
+ include_skips=False) -
+ self._expected_failures.GetTestSet(WONTFIX, CRASH,
+ include_skips=False))
+
+ def GetWontFixTimeouts(self):
+ # Avoid including TIMEOUT CRASH tests in the timeout numbers since crashes
+ # are higher priority.
+ return (self._expected_failures.GetTestSet(WONTFIX, TIMEOUT,
+ include_skips=False) -
+ self._expected_failures.GetTestSet(WONTFIX, CRASH,
+ include_skips=False))
+
def GetRebaseliningFailures(self):
return (self._expected_failures.GetTestSet(REBASELINE, FAIL) |
self._expected_failures.GetTestSet(REBASELINE, IMAGE) |
@@ -47,7 +127,12 @@ class TestExpectations:
self._expected_failures.GetTestSet(REBASELINE, IMAGE_PLUS_TEXT))
def GetExpectations(self, test):
- return self._expected_failures.GetExpectations(test)
+ if self._expected_failures.Contains(test):
+ return self._expected_failures.GetExpectations(test)
+
+ # If the test file is not listed in any of the expectations lists
+ # we expect it to pass (and nothing else).
+ return set([PASS])
def GetExpectationsString(self, test):
"""Returns the expectatons for the given test as an uppercase string.
@@ -63,22 +148,19 @@ class TestExpectations:
return " ".join(retval).upper()
- def GetTimelineForTest(self, test):
- return self._expected_failures.GetTimelineForTest(test)
+ def GetModifiers(self, test):
+ if self._expected_failures.Contains(test):
+ return self._expected_failures.GetModifiers(test)
+ return []
- def GetTestsWithResultType(self, result_type):
- return self._expected_failures.GetTestsWithResultType(result_type)
+ def IsDeferred(self, test):
+ return self._expected_failures.HasModifier(test, DEFER)
- def GetTestsWithTimeline(self, timeline):
- return self._expected_failures.GetTestsWithTimeline(timeline)
+ def IsFixable(self, test):
+ return self._expected_failures.HasModifier(test, NONE)
- def MatchesAnExpectedResult(self, test, result):
- """Returns whether we got one of the expected results for this test."""
- return (result in self._expected_failures.GetExpectations(test) or
- (result in (IMAGE, TEXT, IMAGE_PLUS_TEXT) and
- FAIL in self._expected_failures.GetExpectations(test)) or
- result == MISSING and self.IsRebaselining(test) or
- result == SKIP and self._expected_failures.HasModifier(test, SKIP))
+ def IsIgnored(self, test):
+ return self._expected_failures.HasModifier(test, WONTFIX)
def IsRebaselining(self, test):
return self._expected_failures.HasModifier(test, REBASELINE)
@@ -169,27 +251,10 @@ class TestExpectationsFile:
'crash': CRASH,
'missing': MISSING }
- EXPECTATION_DESCRIPTIONS = { SKIP : ('Skipped', 'Skipped'),
- PASS : ('passes', 'passes'),
- FAIL : ('failure', 'failures'),
- TEXT : ('text diff mismatch',
- 'text diff mismatch'),
- IMAGE : ('image mismatch', 'image mismatch'),
- IMAGE_PLUS_TEXT : ('image and text mismatch',
- 'image and text mismatch'),
- CRASH : ('test shell crash',
- 'test shell crashes'),
- TIMEOUT : ('test timed out', 'tests timed out'),
- MISSING : ('No expected result found',
- 'No expected results found') }
-
- EXPECTATION_ORDER = ( PASS, CRASH, TIMEOUT, MISSING, IMAGE_PLUS_TEXT,
- TEXT, IMAGE, FAIL, SKIP )
-
- BASE_PLATFORMS = ( 'linux', 'mac', 'win' )
- PLATFORMS = BASE_PLATFORMS + ( 'win-xp', 'win-vista', 'win-7' )
+ BASE_PLATFORMS = [ 'linux', 'mac', 'win' ]
+ PLATFORMS = BASE_PLATFORMS + [ 'win-xp', 'win-vista', 'win-7' ]
- BUILD_TYPES = ( 'debug', 'release' )
+ BUILD_TYPES = [ 'debug', 'release' ]
MODIFIERS = { 'skip': SKIP,
'wontfix': WONTFIX,
@@ -198,16 +263,6 @@ class TestExpectationsFile:
'rebaseline': REBASELINE,
'none': NONE }
- TIMELINES = { 'wontfix': WONTFIX,
- 'now': NOW,
- 'defer': DEFER }
-
- RESULT_TYPES = { 'skip': SKIP,
- 'pass': PASS,
- 'fail': FAIL,
- 'flaky': FLAKY }
-
-
def __init__(self, path, full_test_list, platform, is_debug_mode,
is_lint_mode, expectations_as_str=None, suppress_errors=False):
"""
@@ -243,30 +298,23 @@ class TestExpectationsFile:
# Maps a test to its list of expectations.
self._test_to_expectations = {}
- # Maps a test to its list of options (string values)
- self._test_to_options = {}
-
- # Maps a test to its list of modifiers: the constants associated with
- # the options minus any bug or platform strings
+ # Maps a test to its list of modifiers.
self._test_to_modifiers = {}
# Maps a test to the base path that it was listed with in the test list.
self._test_list_paths = {}
- self._modifier_to_tests = self._DictOfSets(self.MODIFIERS)
- self._expectation_to_tests = self._DictOfSets(self.EXPECTATIONS)
- self._timeline_to_tests = self._DictOfSets(self.TIMELINES)
- self._result_type_to_tests = self._DictOfSets(self.RESULT_TYPES)
+ # Maps a modifier to a set of tests.
+ self._modifier_to_tests = {}
+ for modifier in self.MODIFIERS.itervalues():
+ self._modifier_to_tests[modifier] = set()
- self._Read(self._GetIterableExpectations())
+ # Maps an expectation to a set of tests.
+ self._expectation_to_tests = {}
+ for expectation in self.EXPECTATIONS.itervalues():
+ self._expectation_to_tests[expectation] = set()
- def _DictOfSets(self, strings_to_constants):
- """Takes a dict of strings->constants and returns a dict mapping
- each constant to an empty set."""
- d = {}
- for c in strings_to_constants.values():
- d[c] = set()
- return d
+ self._Read(self._GetIterableExpectations())
def _GetIterableExpectations(self):
"""Returns an object that can be iterated over. Allows for not caring about
@@ -304,15 +352,12 @@ class TestExpectationsFile:
return tests
- def GetTestsWithResultType(self, result_type):
- return self._result_type_to_tests[result_type]
-
- def GetTestsWithTimeline(self, timeline):
- return self._timeline_to_tests[timeline]
-
def HasModifier(self, test, modifier):
return test in self._modifier_to_tests[modifier]
+ def GetModifiers(self, test):
+ return self._test_to_modifiers[test]
+
def GetExpectations(self, test):
return self._test_to_expectations[test]
@@ -405,7 +450,9 @@ class TestExpectationsFile:
def ParseExpectationsLine(self, line):
"""Parses a line from test_expectations.txt and returns a tuple with the
- test path, options as a list, expectations as a list."""
+ test path, options as a list, expectations as a list, options as a string
+ and expectations as a string.
+ """
line = StripComments(line)
if not line:
return (None, None, None)
@@ -577,7 +624,7 @@ class TestExpectationsFile:
tests = self._ExpandTests(test_list_path)
self._AddTests(tests, expectations, test_list_path, lineno,
- modifiers, options)
+ modifiers, options)
if not self._suppress_errors and (
len(self._errors) or len(self._non_fatal_errors)):
@@ -593,14 +640,6 @@ class TestExpectationsFile:
if len(self._errors):
raise SyntaxError('\n'.join(map(str, self._errors)))
- # Now add in the tests that weren't present in the expectations file
- expectations = set([PASS])
- options = []
- modifiers = []
- for test in self._full_test_list:
- if not test in self._test_list_paths:
- self._AddTest(test, modifiers, expectations, options)
-
def _GetOptionsList(self, listString):
return [part.strip().lower() for part in listString.strip().split(' ')]
@@ -632,77 +671,42 @@ class TestExpectationsFile:
return result
def _AddTests(self, tests, expectations, test_list_path, lineno, modifiers,
- options):
+ options):
for test in tests:
if self._AlreadySeenTest(test, test_list_path, lineno):
continue
self._ClearExpectationsForTest(test, test_list_path)
- self._AddTest(test, modifiers, expectations, options)
+ self._test_to_expectations[test] = expectations
+ self._test_to_modifiers[test] = options
- def _AddTest(self, test, modifiers, expectations, options):
- """Sets the expected state for a given test.
-
- This routine assumes the test has not been added before. If it has,
- use _ClearExpectationsForTest() to reset the state prior to calling this.
+ if len(modifiers) is 0:
+ self._AddTest(test, NONE, expectations)
+ else:
+ for modifier in modifiers:
+ self._AddTest(test, self.MODIFIERS[modifier], expectations)
- Args:
- test: test to add
- modifiers: sequence of modifier keywords ('wontfix', 'slow', etc.)
- expectations: sequence of expectations (PASS, IMAGE, etc.)
- options: sequence of keywords and bug identifiers."""
- self._test_to_expectations[test] = expectations
+ def _AddTest(self, test, modifier, expectations):
+ self._modifier_to_tests[modifier].add(test)
for expectation in expectations:
self._expectation_to_tests[expectation].add(test)
- self._test_to_options[test] = options
- self._test_to_modifiers[test] = set()
- for modifier in modifiers:
- mod_value = self.MODIFIERS[modifier]
- self._modifier_to_tests[mod_value].add(test)
- self._test_to_modifiers[test].add(mod_value)
-
- if 'wontfix' in modifiers:
- self._timeline_to_tests[WONTFIX].add(test)
- elif 'defer' in modifiers:
- self._timeline_to_tests[DEFER].add(test)
- else:
- self._timeline_to_tests[NOW].add(test)
-
- if 'skip' in modifiers:
- self._result_type_to_tests[SKIP].add(test)
- elif expectations == set([PASS]):
- self._result_type_to_tests[PASS].add(test)
- elif len(expectations) > 1:
- self._result_type_to_tests[FLAKY].add(test)
- else:
- self._result_type_to_tests[FAIL].add(test)
-
-
def _ClearExpectationsForTest(self, test, test_list_path):
- """Remove prexisting expectations for this test.
+ """Remove prexisiting expectations for this test.
This happens if we are seeing a more precise path
than a previous listing.
"""
if test in self._test_list_paths:
self._test_to_expectations.pop(test, '')
- self._RemoveFromSets(test, self._expectation_to_tests)
- self._RemoveFromSets(test, self._modifier_to_tests)
- self._RemoveFromSets(test, self._timeline_to_tests)
- self._RemoveFromSets(test, self._result_type_to_tests)
+ for expectation in self.EXPECTATIONS.itervalues():
+ if test in self._expectation_to_tests[expectation]:
+ self._expectation_to_tests[expectation].remove(test)
+ for modifier in self.MODIFIERS.itervalues():
+ if test in self._modifier_to_tests[modifier]:
+ self._modifier_to_tests[modifier].remove(test)
self._test_list_paths[test] = os.path.normpath(test_list_path)
- def _RemoveFromSets(self, test, dict):
- """Removes the given test from the sets in the dictionary.
-
- Args:
- test: test to look for
- dict: dict of sets of files"""
- for set_of_tests in dict.itervalues():
- if test in set_of_tests:
- set_of_tests.remove(test)
-
def _AlreadySeenTest(self, test, test_list_path, lineno):
"""Returns true if we've already seen a more precise path for this test
than the test_list_path.
diff --git a/webkit/tools/layout_tests/layout_package/test_failures.py b/webkit/tools/layout_tests/layout_package/test_failures.py
index e590492..6f1d807 100644
--- a/webkit/tools/layout_tests/layout_package/test_failures.py
+++ b/webkit/tools/layout_tests/layout_package/test_failures.py
@@ -5,39 +5,29 @@
"""Classes for failures that occur during tests."""
import os
-import test_expectations
-
-def DetermineResultType(failure_list):
- """Takes a set of test_failures and returns which result type best fits
- the list of failures. "Best fits" means we use the worst type of failure.
-
- Returns:
- one of the test_expectations result types - PASS, TEXT, CRASH, etc."""
-
- if not failure_list or len(failure_list) == 0:
- return test_expectations.PASS
-
- failure_types = [type(f) for f in failure_list]
- if FailureCrash in failure_types:
- return test_expectations.CRASH
- elif FailureTimeout in failure_types:
- return test_expectations.TIMEOUT
- elif (FailureMissingResult in failure_types or
- FailureMissingImage in failure_types or
- FailureMissingImageHash in failure_types):
- return test_expectations.MISSING
- else:
- is_text_failure = FailureTextMismatch in failure_types
- is_image_failure = (FailureImageHashIncorrect in failure_types or
- FailureImageHashMismatch in failure_types)
- if is_text_failure and is_image_failure:
- return test_expectations.IMAGE_PLUS_TEXT
- elif is_text_failure:
- return test_expectations.TEXT
- elif is_image_failure:
- return test_expectations.IMAGE
- else:
- raise ValueError("unclassifiable set of failures: " + str(failure_types))
+
+class FailureSort(object):
+ """A repository for failure sort orders and tool to facilitate sorting."""
+
+ # Each failure class should have an entry in this dictionary. Sort order 1
+ # will be sorted first in the list. Failures with the same numeric sort
+ # order will be sorted alphabetically by Message().
+ SORT_ORDERS = {
+ 'FailureTextMismatch': 1,
+ 'FailureImageHashMismatch': 2,
+ 'FailureTimeout': 3,
+ 'FailureCrash': 4,
+ 'FailureMissingImageHash': 5,
+ 'FailureMissingImage': 6,
+ 'FailureMissingResult': 7,
+ 'FailureImageHashIncorrect': 8,
+ }
+
+ @staticmethod
+ def SortOrder(failure_type):
+ """Returns a tuple of the class's numeric sort order and its message."""
+ order = FailureSort.SORT_ORDERS.get(failure_type.__name__, -1)
+ return (order, failure_type.Message())
class TestFailure(object):
diff --git a/webkit/tools/layout_tests/layout_package/test_shell_thread.py b/webkit/tools/layout_tests/layout_package/test_shell_thread.py
index a8c5fa9..3c7e52f 100644
--- a/webkit/tools/layout_tests/layout_package/test_shell_thread.py
+++ b/webkit/tools/layout_tests/layout_package/test_shell_thread.py
@@ -185,15 +185,13 @@ class SingleTestThread(threading.Thread):
class TestShellThread(threading.Thread):
- def __init__(self, filename_list_queue, result_queue, test_shell_command,
- test_types, test_args, shell_args, options):
+ def __init__(self, filename_list_queue, test_shell_command, test_types,
+ test_args, shell_args, options):
"""Initialize all the local state for this test shell thread.
Args:
filename_list_queue: A thread safe Queue class that contains lists of
tuples of (filename, uri) pairs.
- result_queue: A thread safe Queue class that will contain tuples of
- (test, failure lists) for the test results.
test_shell_command: A list specifying the command+args for test_shell
test_types: A list of TestType objects to run the test output against.
test_args: A TestArguments object to pass to each TestType.
@@ -204,7 +202,6 @@ class TestShellThread(threading.Thread):
"""
threading.Thread.__init__(self)
self._filename_list_queue = filename_list_queue
- self._result_queue = result_queue
self._filename_list = []
self._test_shell_command = test_shell_command
self._test_types = test_types
@@ -212,6 +209,7 @@ class TestShellThread(threading.Thread):
self._test_shell_proc = None
self._shell_args = shell_args
self._options = options
+ self._failures = {}
self._canceled = False
self._exception_info = None
self._directory_timing_stats = {}
@@ -227,6 +225,11 @@ class TestShellThread(threading.Thread):
# Time at which we started running tests from self._current_dir.
self._current_dir_start_time = None
+ def GetFailures(self):
+ """Returns a dictionary mapping test filename to a list of
+ TestFailures."""
+ return self._failures
+
def GetDirectoryTimingStats(self):
"""Returns a dictionary mapping test directory to a tuple of
(number of tests in that directory, time to run the tests)"""
@@ -337,9 +340,10 @@ class TestShellThread(threading.Thread):
error_str = '\n'.join([' ' + f.Message() for f in failures])
logging.error("%s failed:\n%s" %
(path_utils.RelativeTestFilename(filename), error_str))
+ # Group the errors for reporting.
+ self._failures[filename] = failures
else:
logging.debug(path_utils.RelativeTestFilename(filename) + " passed")
- self._result_queue.put((filename, failures))
if batch_size > 0 and batch_count > batch_size:
# Bounce the shell and reset count.
diff --git a/webkit/tools/layout_tests/run_webkit_tests.py b/webkit/tools/layout_tests/run_webkit_tests.py
index b9244de..33ec3bd 100755
--- a/webkit/tools/layout_tests/run_webkit_tests.py
+++ b/webkit/tools/layout_tests/run_webkit_tests.py
@@ -36,6 +36,7 @@ import time
import traceback
from layout_package import apache_http_server
+from layout_package import compare_failures
from layout_package import test_expectations
from layout_package import http_server
from layout_package import json_results_generator
@@ -49,8 +50,6 @@ from test_types import image_diff
from test_types import test_type_base
from test_types import text_diff
-TestExpectationsFile = test_expectations.TestExpectationsFile
-
class TestInfo:
"""Groups information about a test for easy passing of data."""
def __init__(self, filename, timeout):
@@ -71,40 +70,43 @@ class TestInfo:
self.image_hash = None
-class ResultSummary(object):
- """A class for partitioning the test results we get into buckets.
-
- This class is basically a glorified struct and it's private to this file
- so we don't bother with any information hiding."""
- def __init__(self, expectations, test_files):
- self.total = len(test_files)
- self.expectations = expectations
- self.tests_by_expectation = {}
- self.tests_by_timeline = {}
- self.results = {}
- self.unexpected_results = {}
- self.failures = {}
- self.tests_by_expectation[test_expectations.SKIP] = set()
- for expectation in TestExpectationsFile.EXPECTATIONS.values():
- self.tests_by_expectation[expectation] = set()
- for timeline in TestExpectationsFile.TIMELINES.values():
- self.tests_by_timeline[timeline] = expectations.GetTestsWithTimeline(
- timeline)
-
- def Add(self, test, failures, result):
- """Add a result into the appropriate bin.
+class ResultSummaryEntry:
+ def __init__(self, all, failed, failure_counts, skipped):
+ """Resolves result counts.
Args:
- test: test file name
- failures: list of failure objects from test execution
- result: result of test (PASS, IMAGE, etc.)."""
+ all: list of all tests in this category
+ failed: list of failing tests in this category
+ skipped: list of skipped tests
+ failure_counts: dictionary of (TestFailure -> frequency)
+ """
+ self.skip_count = len(skipped)
+ self.total_count = len(all | skipped)
+ self.pass_count = self.total_count - self.skip_count - len(failed)
+ self.failure_counts = failure_counts;
+
- self.tests_by_expectation[result].add(test)
- self.results[test] = result
- if len(failures):
- self.failures[test] = failures
- if not self.expectations.MatchesAnExpectedResult(test, result):
- self.unexpected_results[test] = result
+class ResultSummary:
+ """Container for all result summaries for this test run.
+
+ Args:
+ deferred: ResultSummary object for deferred tests.
+ wontfix: ResultSummary object for wontfix tests.
+ fixable: ResultSummary object for tests marked in test_expectations.txt as
+ needing fixing but are not deferred/wontfix (i.e. should be fixed
+ for the next release).
+ fixable_count: Count of all fixable and skipped tests. This is essentially
+ a deduped sum of all the failures that are not deferred/wontfix.
+ all_fixable_count: Count of all the tests that are not deferred/wontfix.
+ This includes passing tests.
+ """
+ def __init__(self, deferred, wontfix, fixable, fixable_count,
+ all_fixable_count):
+ self.deferred = deferred
+ self.wontfix = wontfix
+ self.fixable = fixable
+ self.fixable_count = fixable_count
+ self.all_fixable_count = all_fixable_count
class TestRunner:
@@ -157,7 +159,6 @@ class TestRunner:
self._test_files = set()
self._test_files_list = None
self._file_dir = path_utils.GetAbsolutePath(os.path.dirname(sys.argv[0]))
- self._result_queue = Queue.Queue()
def __del__(self):
logging.info("flushing stdout")
@@ -174,7 +175,7 @@ class TestRunner:
def GatherFilePaths(self, paths):
"""Find all the files to test.
- Args:
+ args:
paths: a list of globs to use instead of the defaults."""
self._test_files = test_files.GatherTestFiles(paths)
logging.info('Found: %d tests' % len(self._test_files))
@@ -201,19 +202,21 @@ class TestRunner:
raise err
def PrepareListsAndPrintOutput(self):
- """Create appropriate subsets of test lists and returns a ResultSummary
- object. Also prints expected test counts."""
-
- result_summary = ResultSummary(self._expectations, self._test_files)
+ """Create appropriate subsets of test lists and print test counts.
+ Create appropriate subsets of self._tests_files in
+ self._ignored_failures, self._fixable_failures, and self._fixable_crashes.
+ Also remove skipped files from self._test_files, extract a subset of tests
+ if desired, and create the sorted self._test_files_list.
+ """
# Remove skipped - both fixable and ignored - files from the
# top-level list of files to test.
skipped = set()
+ # If there was only one test file, we'll run it even if it was skipped.
if len(self._test_files) > 1 and not self._options.force:
- skipped = self._expectations.GetTestsWithResultType(
- test_expectations.SKIP)
- for test in skipped:
- result_summary.Add(test, [], test_expectations.SKIP)
+ skipped = (self._expectations.GetFixableSkipped() |
+ self._expectations.GetDeferredSkipped() |
+ self._expectations.GetWontFixSkipped())
self._test_files -= skipped
if self._options.force:
@@ -305,15 +308,23 @@ class TestRunner:
else:
logging.info('Run: %d tests' % len(self._test_files))
- self._PrintExpectedResultsOfType(result_summary, test_expectations.PASS,
- "passes")
- self._PrintExpectedResultsOfType(result_summary, test_expectations.FAIL,
- "failures")
- self._PrintExpectedResultsOfType(result_summary, test_expectations.FLAKY,
- "flaky")
- self._PrintExpectedResultsOfType(result_summary, test_expectations.SKIP,
- "skipped")
- return result_summary
+ logging.info('Deferred: %d tests' % len(self._expectations.GetDeferred()))
+ logging.info('Expected passes: %d tests' %
+ len(self._test_files -
+ self._expectations.GetFixable() -
+ self._expectations.GetWontFix()))
+ logging.info(('Expected failures: %d fixable, %d ignored '
+ 'and %d deferred tests') %
+ (len(self._expectations.GetFixableFailures()),
+ len(self._expectations.GetWontFixFailures()),
+ len(self._expectations.GetDeferredFailures())))
+ logging.info(('Expected timeouts: %d fixable, %d ignored '
+ 'and %d deferred tests') %
+ (len(self._expectations.GetFixableTimeouts()),
+ len(self._expectations.GetWontFixTimeouts()),
+ len(self._expectations.GetDeferredTimeouts())))
+ logging.info('Expected crashes: %d fixable tests' %
+ len(self._expectations.GetFixableCrashes()))
def AddTestType(self, test_type):
"""Add a TestType to the TestRunner."""
@@ -373,7 +384,6 @@ class TestRunner:
Return:
The Queue of lists of TestInfo objects.
"""
-
if self._options.experimental_fully_parallel:
filename_queue = Queue.Queue()
for test_file in test_files:
@@ -474,7 +484,6 @@ class TestRunner:
test_args, shell_args = self._GetTestShellArgs(i)
thread = test_shell_thread.TestShellThread(filename_queue,
- self._result_queue,
test_shell_command,
test_types,
test_args,
@@ -493,7 +502,7 @@ class TestRunner:
proc.stdin.close()
proc.wait()
- def _RunTests(self, test_shell_binary, file_list, result_summary):
+ def _RunTests(self, test_shell_binary, file_list):
"""Runs the tests in the file_list.
Return: A tuple (failures, thread_timings, test_timings,
@@ -505,7 +514,6 @@ class TestRunner:
form [time, directory_name, num_tests]
individual_test_timings is a list of run times for each test in the form
{filename:filename, test_run_time:test_run_time}
- result_summary: summary object to populate with the results
"""
threads = self._InstantiateTestShellThreads(test_shell_binary, file_list)
@@ -522,7 +530,7 @@ class TestRunner:
# suffices to not use an indefinite blocking join for it to
# be interruptible by KeyboardInterrupt.
thread.join(1.0)
- self._UpdateSummary(result_summary)
+ failures.update(thread.GetFailures())
thread_timings.append({ 'name': thread.getName(),
'num_tests': thread.GetNumTests(),
'total_time': thread.GetTotalTime()});
@@ -542,19 +550,14 @@ class TestRunner:
# would be assumed to have passed.
raise exception_info[0], exception_info[1], exception_info[2]
- # Make sure we pick up any remaining tests.
- self._UpdateSummary(result_summary)
- return (thread_timings, test_timings, individual_test_timings)
+ return (failures, thread_timings, test_timings, individual_test_timings)
- def Run(self, result_summary):
+ def Run(self):
"""Run all our tests on all our test files.
For each test file, we run each test type. If there are any failures, we
collect them for reporting.
- Args:
- result_summary: a summary object tracking the test results.
-
Return:
We return nonzero if there are regressions compared to the last run.
"""
@@ -597,36 +600,25 @@ class TestRunner:
self._websocket_server.Start()
# self._websocket_secure_server.Start()
- thread_timings, test_timings, individual_test_timings = (
- self._RunTests(test_shell_binary, self._test_files_list,
- result_summary))
- original_results_by_test, original_results_by_type = self._CompareResults(
- result_summary)
- final_results_by_test = original_results_by_test
- final_results_by_type = original_results_by_type
- failed_results_by_test, failed_results_by_type = self._OnlyFailures(
- original_results_by_test)
- num_passes = len(original_results_by_test) - len(failed_results_by_test)
+ original_failures, thread_timings, test_timings, individual_test_timings = (
+ self._RunTests(test_shell_binary, self._test_files_list))
retries = 0
- while (retries < self.NUM_RETRY_ON_UNEXPECTED_FAILURE and
- len(failed_results_by_test)):
- logging.info("Retrying %d unexpected failure(s)" %
- len(failed_results_by_test))
+ final_failures = original_failures
+ original_regressions = self._CompareFailures(final_failures)
+ regressions = original_regressions
+
+ while retries < self.NUM_RETRY_ON_UNEXPECTED_FAILURE and len(regressions):
+ logging.info("Retrying %d unexpected failure(s)" % len(regressions))
retries += 1
- retry_summary = ResultSummary(self._expectations,
- failed_results_by_test.keys())
- self._RunTests(test_shell_binary, failed_results_by_test.keys(),
- retry_summary)
- final_results_by_test, final_results_by_type = self._CompareResults(
- retry_summary)
- failed_results_by_test, failed_results_by_type = self._OnlyFailures(
- final_results_by_test)
+ final_failures = self._RunTests(test_shell_binary, list(regressions))[0]
+ regressions = self._CompareFailures(final_failures)
self._StopLayoutTestHelper(layout_test_helper_proc)
print
end_time = time.time()
+
logging.info("%6.2f total testing time" % (end_time - start_time))
cuml_time = 0
logging.debug("Thread timing:")
@@ -641,20 +633,20 @@ class TestRunner:
print
self._PrintTimingStatistics(test_timings, individual_test_timings,
- result_summary)
+ original_failures)
- self._PrintUnexpectedResults(original_results_by_test,
- original_results_by_type,
- final_results_by_test,
- final_results_by_type)
+ self._PrintRegressions(original_failures, original_regressions,
+ final_failures)
# Write summaries to stdout.
- # The summaries should include flaky tests, so use the original summary,
- # not the final one.
+ # The summary should include flaky tests, so use original_failures, not
+ # final_failures.
+ result_summary = self._GetResultSummary(original_failures)
self._PrintResultSummary(result_summary, sys.stdout)
if self._options.verbose:
- self._WriteJSONFiles(result_summary, individual_test_timings);
+ self._WriteJSONFiles(original_failures, individual_test_timings,
+ result_summary);
# Write the same data to a log file.
out_filename = os.path.join(self._options.results_directory, "score.txt")
@@ -664,62 +656,40 @@ class TestRunner:
# Write the summary to disk (results.html) and maybe open the test_shell
# to this file.
- wrote_results = self._WriteResultsHtmlFile(result_summary)
+ wrote_results = self._WriteResultsHtmlFile(original_failures,
+ original_regressions)
if not self._options.noshow_results and wrote_results:
self._ShowResultsHtmlFile()
sys.stdout.flush()
sys.stderr.flush()
- # Ignore flaky failures and unexpected passes so we don't turn the
- # bot red for those.
- return len(failed_results_by_test)
-
- def _UpdateSummary(self, result_summary):
- """Update the summary while running tests."""
- while True:
- try:
- (test, fail_list) = self._result_queue.get_nowait()
- result = test_failures.DetermineResultType(fail_list)
- result_summary.Add(test, fail_list, result)
- except Queue.Empty:
- return
-
- def _CompareResults(self, result_summary):
- """Determine if the results in this test run are unexpected.
+ # Ignore flaky failures so we don't turn the bot red for those.
+ return len(regressions)
- Returns:
- A dict of files -> results and a dict of results -> sets of files
+ def _PrintRegressions(self, original_failures, original_regressions,
+ final_failures):
+ """Prints the regressions from the test run.
+ Args:
+ original_failures: Failures from the first test run.
+ original_regressions: Regressions from the first test run.
+ final_failures: Failures after retrying the failures from the first run.
"""
- results_by_test = {}
- results_by_type = {}
- for result in TestExpectationsFile.EXPECTATIONS.values():
- results_by_type[result] = set()
- for test, result in result_summary.unexpected_results.iteritems():
- results_by_test[test] = result
- results_by_type[result].add(test)
- return results_by_test, results_by_type
-
- def _OnlyFailures(self, results_by_test):
- """Filters a dict of results and returns only the failures.
+ print "-" * 78
- Args:
- results_by_test: a dict of files -> results
+ flaky_failures = {}
+ non_flaky_failures = {}
+ for failure in original_failures:
+ if failure not in original_regressions or failure in final_failures:
+ non_flaky_failures[failure] = original_failures[failure]
+ else:
+ flaky_failures[failure] = original_failures[failure]
- Returns:
- a dict of files -> results and results -> sets of files.
- """
- failed_results_by_test = {}
- failed_results_by_type = {}
- for test, result in results_by_test.iteritems():
- if result != test_expectations.PASS:
- failed_results_by_test[test] = result
- if result in failed_results_by_type:
- failed_results_by_type.add(test)
- else:
- failed_results_by_type = set([test])
- return failed_results_by_test, failed_results_by_type
+ self._CompareFailures(non_flaky_failures, print_regressions=True)
+ self._CompareFailures(flaky_failures, print_regressions=True, is_flaky=True)
- def _WriteJSONFiles(self, result_summary, individual_test_timings):
+ print "-" * 78
+
+ def _WriteJSONFiles(self, failures, individual_test_timings, result_summary):
logging.debug("Writing JSON files in %s." % self._options.results_directory)
# Write a json file of the test_expectations.txt file for the layout tests
# dashboard.
@@ -729,47 +699,16 @@ class TestRunner:
expectations_file.write(("ADD_EXPECTATIONS(" + expectations_json + ");"))
expectations_file.close()
- json_results_generator.JSONResultsGenerator(self._options,
- result_summary, individual_test_timings,
- self._options.results_directory, self._test_files_list)
+ json_results_generator.JSONResultsGenerator(self._options, failures,
+ individual_test_timings, self._options.results_directory,
+ self._test_files_list, result_summary)
logging.debug("Finished writing JSON files.")
- def _PrintExpectedResultsOfType(self, result_summary, result_type,
- result_type_str):
- """Print the number of the tests in a given result class.
-
- Args:
- result_summary - the object containg all the results to report on
- result_type - the particular result type to report in the summary.
- result_type_str - a string description of the result_type.
- """
- tests = self._expectations.GetTestsWithResultType(result_type)
- now = result_summary.tests_by_timeline[test_expectations.NOW]
- wontfix = result_summary.tests_by_timeline[test_expectations.WONTFIX]
- defer = result_summary.tests_by_timeline[test_expectations.DEFER]
-
- # We use a fancy format string in order to print the data out in a
- # nicely-aligned table.
- fmtstr = ("Expect: %%5d %%-8s (%%%dd now, %%%dd defer, %%%dd wontfix)" %
- (self._NumDigits(now), self._NumDigits(defer),
- self._NumDigits(wontfix)))
- logging.info(fmtstr %
- (len(tests), result_type_str, len(tests & now), len(tests & defer),
- len(tests & wontfix)))
-
- def _NumDigits(self, num):
- """Returns the number of digits needed to represent the length of a
- sequence."""
- ndigits = 1
- if len(num):
- ndigits = int(math.log10(len(num))) + 1
- return ndigits
-
def _PrintTimingStatistics(self, directory_test_timings,
- individual_test_timings, result_summary):
+ individual_test_timings, failures):
self._PrintAggregateTestStatistics(individual_test_timings)
- self._PrintIndividualTestTimes(individual_test_timings, result_summary)
+ self._PrintIndividualTestTimes(individual_test_timings, failures)
self._PrintDirectoryTimings(directory_test_timings)
def _PrintAggregateTestStatistics(self, individual_test_timings):
@@ -803,12 +742,12 @@ class TestRunner:
"PER TEST TIMES BY TEST TYPE: %s" % test_type,
times_per_test_type[test_type])
- def _PrintIndividualTestTimes(self, individual_test_timings, result_summary):
+ def _PrintIndividualTestTimes(self, individual_test_timings, failures):
"""Prints the run times for slow, timeout and crash tests.
Args:
individual_test_timings: List of test_shell_thread.TestStats for all
tests.
- result_summary: Object containing the results of all the tests.
+ failures: Dictionary mapping test filenames to list of test_failures.
"""
# Reverse-sort by the time spent in test_shell.
individual_test_timings.sort(lambda a, b:
@@ -825,10 +764,13 @@ class TestRunner:
is_timeout_crash_or_slow = True
slow_tests.append(test_tuple)
- if (result_summary.results[filename] == test_expectations.TIMEOUT or
- result_summary.results[filename] == test_expectations.CRASH):
- is_timeout_crash_or_slow = True
- timeout_or_crash_tests.append(test_tuple)
+ if filename in failures:
+ for failure in failures[filename]:
+ if (failure.__class__ == test_failures.FailureTimeout or
+ failure.__class__ == test_failures.FailureCrash):
+ is_timeout_crash_or_slow = True
+ timeout_or_crash_tests.append(test_tuple)
+ break
if (not is_timeout_crash_or_slow and
num_printed < self._options.num_slow_tests_to_log):
@@ -896,225 +838,181 @@ class TestRunner:
"99th percentile: %s, Standard deviation: %s\n" % (
median, mean, percentile90, percentile99, std_deviation)))
- def _PrintResultSummary(self, result_summary, output):
- """Print a short summary to the output file about how many tests passed.
+ def _GetResultSummary(self, failures):
+ """Returns a ResultSummary object with failure counts.
Args:
- output: a file or stream to write to
+ failures: dictionary mapping the test filename to a list of
+ TestFailure objects if the test failed
"""
- failed = len(result_summary.failures)
- skipped = len(result_summary.tests_by_expectation[test_expectations.SKIP])
- total = result_summary.total
- passed = total - failed - skipped
- pct_passed = 0.0
- if total > 0:
- pct_passed = float(passed) * 100 / total
-
- output.write("\n");
- output.write("=> Total: %d/%d tests passed (%.1f%%)\n" %
- (passed, total, pct_passed))
-
- output.write("\n");
- self._PrintResultSummaryEntry(result_summary, test_expectations.NOW,
- "Tests to be fixed for the current release", output)
-
- output.write("\n");
- self._PrintResultSummaryEntry(result_summary, test_expectations.DEFER,
- "Tests we'll fix in the future if they fail (DEFER)", output)
-
- output.write("\n");
- self._PrintResultSummaryEntry(result_summary, test_expectations.WONTFIX,
- "Tests that won't be fixed if they fail (WONTFIX)", output)
-
- def _PrintResultSummaryEntry(self, result_summary, timeline, heading, output):
- """Print a summary block of results for a particular timeline of test.
+ fixable_counts = {}
+ deferred_counts = {}
+ wontfix_counts = {}
+
+ fixable_failures = set()
+ deferred_failures = set()
+ wontfix_failures = set()
+
+ # Aggregate failures in a dictionary (TestFailure -> frequency),
+ # with known (fixable and wontfix) failures separated out.
+ def AddFailure(dictionary, key):
+ if key in dictionary:
+ dictionary[key] += 1
+ else:
+ dictionary[key] = 1
+
+ for test, failure_types in failures.iteritems():
+ for failure_type in failure_types:
+ # TODO(ojan): Now that we have IMAGE+TEXT, IMAGE and TEXT,
+ # we can avoid adding multiple failures per test since there should
+ # be a unique type of failure for each test. This would make the
+ # statistics printed at the end easier to grok.
+ if self._expectations.IsDeferred(test):
+ count_group = deferred_counts
+ failure_group = deferred_failures
+ elif self._expectations.IsIgnored(test):
+ count_group = wontfix_counts
+ failure_group = wontfix_failures
+ else:
+ count_group = fixable_counts
+ failure_group = fixable_failures
+
+ AddFailure(count_group, failure_type.__class__)
+ failure_group.add(test)
+
+ # Here and below, use the prechuncked expectations object for numbers of
+ # skipped tests. Chunking removes the skipped tests before creating the
+ # expectations file.
+ #
+ # This is a bit inaccurate, since the number of skipped tests will be
+ # duplicated across all shard, but it's the best we can reasonably do.
+
+ deduped_fixable_count = len(fixable_failures |
+ self.prechunk_expectations.GetFixableSkipped())
+ all_fixable_count = len(self._test_files -
+ self._expectations.GetWontFix() - self._expectations.GetDeferred())
+
+ # Breakdown of tests we need to fix and want to pass.
+ # Include skipped fixable tests in the statistics.
+ fixable_result_summary = ResultSummaryEntry(
+ self._expectations.GetFixable() | fixable_failures,
+ fixable_failures,
+ fixable_counts,
+ self.prechunk_expectations.GetFixableSkipped())
+
+ deferred_result_summary = ResultSummaryEntry(
+ self._expectations.GetDeferred(),
+ deferred_failures,
+ deferred_counts,
+ self.prechunk_expectations.GetDeferredSkipped())
+
+ wontfix_result_summary = ResultSummaryEntry(
+ self._expectations.GetWontFix(),
+ wontfix_failures,
+ wontfix_counts,
+ self.prechunk_expectations.GetWontFixSkipped())
+
+ return ResultSummary(deferred_result_summary, wontfix_result_summary,
+ fixable_result_summary, deduped_fixable_count, all_fixable_count)
- Args:
- result_summary: summary to print results for
- timeline: the timeline to print results for (NOT, WONTFIX, etc.)
- heading: a textual description of the timeline
- output: a stream to write to
- """
- total = len(result_summary.tests_by_timeline[timeline])
- not_passing = (total -
- len(result_summary.tests_by_expectation[test_expectations.PASS] &
- result_summary.tests_by_timeline[timeline]))
- output.write("=> %s (%d):\n" % (heading, not_passing))
- for result in TestExpectationsFile.EXPECTATION_ORDER:
- if result == test_expectations.PASS:
- continue
- results = (result_summary.tests_by_expectation[result] &
- result_summary.tests_by_timeline[timeline])
- desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result]
- plural = ["", "s"]
- if not_passing and len(results):
- pct = len(results) * 100.0 / not_passing
- output.write("%d test case%s (%4.1f%%) %s\n" %
- (len(results), plural[len(results) != 1], pct,
- desc[len(results) != 1]))
-
- def _PrintUnexpectedResults(self, original_results_by_test,
- original_results_by_type,
- final_results_by_test,
- final_results_by_type):
- """Print unexpected results (regressions) to stdout and a file.
+ def _PrintResultSummary(self, result_summary, output):
+ """Print a short summary to stdout about how many tests passed.
Args:
- original_results_by_test: dict mapping tests -> results for the first
- (original) test run
- original_results_by_type: dict mapping results -> sets of tests for the
- first test run
- final_results_by_test: dict of tests->results after the retries
- eliminated any flakiness
- final_results_by_type: dict of results->tests after the retries
- eliminated any flakiness
+ result_summary: ResultSummary object with failure counts.
+ output: file descriptor to write the results to. For example, sys.stdout.
"""
- print "-" * 78
+ failed = result_summary.fixable_count
+ total = result_summary.all_fixable_count
+ passed = 0
+ if total > 0:
+ passed = float(total - failed) * 100 / total
+ output.write(
+ "\nTest summary: %.1f%% Passed | %s Failures | %s Tests to pass for "
+ "this release\n" % (
+ passed, failed, total))
+
+ self._PrintResultSummaryEntry(
+ "Tests to be fixed for the current release",
+ result_summary.fixable,
+ output)
+ self._PrintResultSummaryEntry(
+ "Tests to be fixed for a future release (DEFER)",
+ result_summary.deferred,
+ output)
+ self._PrintResultSummaryEntry("Tests never to be fixed (WONTFIX)",
+ result_summary.wontfix,
+ output)
print
- flaky_results_by_type = {}
- non_flaky_results_by_type = {}
-
- for test, result in original_results_by_test.iteritems():
- if (result == test_expectations.PASS or
- (test in final_results_by_test and
- result == final_results_by_test[test])):
- if result in non_flaky_results_by_type:
- non_flaky_results_by_type[result].add(test)
- else:
- non_flaky_results_by_type[result] = set([test])
- else:
- if result in flaky_results_by_type:
- flaky_results_by_type[result].add(test)
- else:
- flaky_results_by_type[result] = set([test])
- self._PrintUnexpectedResultsByType(non_flaky_results_by_type, False,
- sys.stdout)
- self._PrintUnexpectedResultsByType(flaky_results_by_type, True, sys.stdout)
-
- out_filename = os.path.join(self._options.results_directory,
- "regressions.txt")
- output_file = open(out_filename, "w")
- self._PrintUnexpectedResultsByType(non_flaky_results_by_type, False,
- output_file)
- output_file.close()
-
- print "-" * 78
-
- def _PrintUnexpectedResultsByType(self, results_by_type, is_flaky, output):
- """Helper method to print a set of unexpected results to an output stream
- sorted by the result type.
+ def _PrintResultSummaryEntry(self, heading, result_summary, output):
+ """Print a summary block of results for a particular category of test.
Args:
- results_by_type: dict(result_type -> list of files)
- is_flaky: where these results flaky or not (changes the output)
- output: stream to write output to
+ heading: text to print before the block, followed by the total count
+ result_summary: ResultSummaryEntry object with the result counts
+ output: file descriptor to write the results to
"""
- descriptions = TestExpectationsFile.EXPECTATION_DESCRIPTIONS
- keywords = {}
- for k, v in TestExpectationsFile.EXPECTATIONS.iteritems():
- keywords[v] = k.upper()
-
- for result in TestExpectationsFile.EXPECTATION_ORDER:
- if result in results_by_type:
- self._PrintUnexpectedResultSet(output, results_by_type[result],
- is_flaky, descriptions[result][1],
- keywords[result])
-
- def _PrintUnexpectedResultSet(self, output, filenames, is_flaky,
- header_text, keyword):
- """A helper method to print one set of results (all of the same type)
- to a stream.
+ output.write("\n=> %s (%d):\n" % (heading, result_summary.total_count))
+ self._PrintResultLine(result_summary.pass_count, result_summary.total_count,
+ "Passed", output)
+ self._PrintResultLine(result_summary.skip_count, result_summary.total_count,
+ "Skipped", output)
+ sorted_keys = sorted(result_summary.failure_counts.keys(),
+ key=test_failures.FailureSort.SortOrder)
+ for failure in sorted_keys:
+ self._PrintResultLine(result_summary.failure_counts[failure],
+ result_summary.total_count,
+ failure.Message(),
+ output)
+
+ def _PrintResultLine(self, count, total, message, output):
+ if count == 0: return
+ output.write(
+ ("%(count)d test case%(plural)s (%(percent).1f%%) %(message)s\n" %
+ { 'count' : count,
+ 'plural' : ('s', '')[count == 1],
+ 'percent' : float(count) * 100 / total,
+ 'message' : message }))
+
+ def _CompareFailures(self, failures, print_regressions=False, is_flaky=False):
+ """Determine if the failures in this test run are unexpected.
Args:
- output: a stream or file object to write() to
- filenames: a list of absolute filenames
- header_text: a string to display before the list of filenames
- keyword: expectation keyword
- """
- filenames = list(filenames)
- filenames.sort()
- if not is_flaky and keyword == 'PASS':
- self._PrintUnexpectedPasses(output, filenames)
- return
-
- if is_flaky:
- prefix = "Unexpected flakiness:"
- else:
- prefix = "Regressions: Unexpected"
- output.write("%s %s (%d):\n" % (prefix, header_text, len(filenames)))
- for filename in filenames:
- flaky_text = ""
- if is_flaky:
- flaky_text = " " + self._expectations.GetExpectationsString(filename)
-
- filename = path_utils.RelativeTestFilename(filename)
- output.write(" %s = %s%s\n" % (filename, keyword, flaky_text))
- output.write("\n")
-
- def _PrintUnexpectedPasses(self, output, filenames):
- """Prints the list of files that passed unexpectedly.
-
- TODO(dpranke): This routine is a bit of a hack, since it's not clear
- what the best way to output this is. Each unexpected pass might have
- multiple expected results, and printing all the combinations would
- be pretty clunky. For now we sort them into three buckets, crashes,
- timeouts, and everything else.
+ failures: a dictionary mapping the test filename to a list of
+ TestFailure objects if the test failed
+ print_regressions: whether to print the regressions to stdout
+ is_flaky: whether this set of failures represents tests that failed
+ the first time around, but passed on a subsequent run
- Args:
- output: a stream to write to
- filenames: list of files that passed
+ Return:
+ A set of regressions (unexpected failures, hangs, or crashes)
"""
- crashes = []
- timeouts = []
- failures = []
- for filename in filenames:
- expectations = self._expectations.GetExpectations(filename)
- if test_expectations.CRASH in expectations:
- crashes.append(filename)
- elif test_expectations.TIMEOUT in expectations:
- timeouts.append(filename)
- else:
- failures.append(filename)
+ cf = compare_failures.CompareFailures(self._test_files,
+ failures,
+ self._expectations,
+ is_flaky)
+ if print_regressions:
+ cf.PrintRegressions(sys.stdout)
- self._PrintPassSet(output, "crash", crashes)
- self._PrintPassSet(output, "timeout", timeouts)
- self._PrintPassSet(output, "fail", failures)
+ return cf.GetRegressions()
- def _PrintPassSet(self, output, expected_str, filenames):
- """Print a set of unexpected passes.
-
- Args:
- output: stream to write to
- expected_str: worst expectation for the given file
- filenames: list of files in that set
- """
- if len(filenames):
- filenames.sort()
- output.write("Expected to %s, but passed: (%d)\n" %
- (expected_str, len(filenames)))
- for filename in filenames:
- filename = path_utils.RelativeTestFilename(filename)
- output.write(" %s\n" % filename)
- output.write("\n")
-
- def _WriteResultsHtmlFile(self, result_summary):
+ def _WriteResultsHtmlFile(self, failures, regressions):
"""Write results.html which is a summary of tests that failed.
Args:
- result_summary: a summary of the results :)
+ failures: a dictionary mapping the test filename to a list of
+ TestFailure objects if the test failed
+ regressions: a set of test filenames that regressed
Returns:
True if any results were written (since expected failures may be omitted)
"""
# test failures
- failures = result_summary.failures
- failed_results_by_test, failed_results_by_type = self._OnlyFailures(
- result_summary.unexpected_results)
if self._options.full_results_html:
test_files = failures.keys()
else:
- test_files = failed_results_by_test.keys()
+ test_files = list(regressions)
if not len(test_files):
return False
@@ -1264,15 +1162,15 @@ def main(options, args):
if options.lint_test_files:
# Creating the expecations for each platform/target pair does all the
# test list parsing and ensures it's correct syntax (e.g. no dupes).
- for platform in TestExpectationsFile.PLATFORMS:
+ for platform in test_expectations.TestExpectationsFile.PLATFORMS:
test_runner.ParseExpectations(platform, is_debug_mode=True)
test_runner.ParseExpectations(platform, is_debug_mode=False)
print ("If there are no fail messages, errors or exceptions, then the "
"lint succeeded.")
- sys.exit(0)
+ return
else:
test_runner.ParseExpectations(options.platform, options.target == 'Debug')
- result_summary = test_runner.PrepareListsAndPrintOutput()
+ test_runner.PrepareListsAndPrintOutput()
if options.find_baselines:
# Record where we found each baseline, then exit.
@@ -1326,7 +1224,7 @@ def main(options, args):
test_runner.AddTestType(image_diff.ImageDiff)
if options.fuzzy_pixel_tests:
test_runner.AddTestType(fuzzy_image_diff.FuzzyImageDiff)
- has_new_failures = test_runner.Run(result_summary)
+ has_new_failures = test_runner.Run()
logging.info("Exit status: %d" % has_new_failures)
sys.exit(has_new_failures)
diff --git a/webkit/tools/layout_tests/update_expectations_from_dashboard.py b/webkit/tools/layout_tests/update_expectations_from_dashboard.py
index 5d0b14e..5bfb8cc 100644
--- a/webkit/tools/layout_tests/update_expectations_from_dashboard.py
+++ b/webkit/tools/layout_tests/update_expectations_from_dashboard.py
@@ -368,7 +368,7 @@ class ExpectationsUpdater(test_expectations.TestExpectationsFile):
if not has_non_pass_expectation:
expectations = ["pass"]
- missing_build_types = list(self.BUILD_TYPES)
+ missing_build_types = self.BUILD_TYPES[:]
sentinal = None
for build_type in update.build_info:
if not sentinal:
@@ -406,7 +406,7 @@ class ExpectationsUpdater(test_expectations.TestExpectationsFile):
platforms: List of lower-case platform names.
"""
platforms.sort()
- if platforms == list(self.BASE_PLATFORMS):
+ if platforms == self.BASE_PLATFORMS:
return ""
else:
return " ".join(platforms)
@@ -449,4 +449,4 @@ def main():
open(path_to_expectations, 'w').write(new_expectations)
if '__main__' == __name__:
- main()
+ main() \ No newline at end of file