diff options
Diffstat (limited to 'WebKitTools/Scripts')
120 files changed, 2112 insertions, 905 deletions
diff --git a/WebKitTools/Scripts/build-webkit b/WebKitTools/Scripts/build-webkit index cd43499..bc1e8ad 100755 --- a/WebKitTools/Scripts/build-webkit +++ b/WebKitTools/Scripts/build-webkit @@ -106,7 +106,7 @@ my @features = ( define => "ENABLE_EVENTSOURCE", default => 1, value => \$eventsourceSupport }, { option => "filters", desc => "Toggle Filters support", - define => "ENABLE_FILTERS", default => (isAppleWebKit() || isGtk() || isQt()), value => \$filtersSupport }, + define => "ENABLE_FILTERS", default => (isAppleWebKit() || isGtk() || isQt() || isEfl()), value => \$filtersSupport }, { option => "geolocation", desc => "Toggle Geolocation support", define => "ENABLE_GEOLOCATION", default => (isAppleWebKit() || isGtk()), value => \$geolocationSupport }, @@ -245,13 +245,14 @@ Usage: $programName [options] [options to pass to build system] --chromium Build the Chromium port on Mac/Win/Linux --gtk Build the GTK+ port --qt Build the Qt port + --efl Build the EFL port --inspector-frontend Copy changes to the inspector front-end files to the build directory --install-headers=<path> Set installation path for the headers (Qt only) --install-libs=<path> Set installation path for the libraries (Qt only) --v8 Use V8 as JavaScript engine (Qt only) - --prefix=<path> Set installation prefix to the given path (Gtk only) + --prefix=<path> Set installation prefix to the given path (Gtk/Efl only) --makeargs=<arguments> Optional Makefile flags --minimal No optional features, unless explicitly enabled. @@ -428,6 +429,22 @@ if (isChromium()) { exit exitStatus($result) if exitStatus($result); } +if (isEfl()) { + @options = (); + @projects = (); + foreach (@features) { + my $featureName = $_->{define}; + if ($featureName) { + my $featureEnabled = ${$_->{value}} ? "ON" : "OFF"; + push @options, "-D$featureName=$featureEnabled"; + } + } + push @options, "--makeargs=" . $makeArgs if defined($makeArgs); + push @options, "--prefix=" . $prefixPath if defined($prefixPath); + my $result = buildCMakeEflProject($clean, @options); + exit exitStatus($result) if exitStatus($result); +} + # Build, and abort if the build fails. for my $dir (@projects) { chdir $dir or die; diff --git a/WebKitTools/Scripts/old-run-webkit-tests b/WebKitTools/Scripts/old-run-webkit-tests index 886b4a8..80801dc 100755 --- a/WebKitTools/Scripts/old-run-webkit-tests +++ b/WebKitTools/Scripts/old-run-webkit-tests @@ -236,7 +236,7 @@ if (isAppleMacWebKit()) { } } -if (isQt() || isGtk() || isCygwin()) { +if (isQt() || isCygwin()) { my $testfontPath = $ENV{"WEBKIT_TESTFONTS"}; if (!$testfontPath || !-d "$testfontPath") { print "The WEBKIT_TESTFONTS environment variable is not defined or not set properly\n"; @@ -528,6 +528,7 @@ if (!$has3DRendering) { if (!checkWebCoreFeatureSupport("3D Canvas", 0)) { $ignoredDirectories{'fast/canvas/webgl'} = 1; $ignoredDirectories{'compositing/webgl'} = 1; + $ignoredDirectories{'http/tests/canvas/webgl'} = 1; } if (checkWebCoreFeatureSupport("WML", 0)) { diff --git a/WebKitTools/Scripts/prepare-ChangeLog b/WebKitTools/Scripts/prepare-ChangeLog index c2adaf5..608c9ce 100755 --- a/WebKitTools/Scripts/prepare-ChangeLog +++ b/WebKitTools/Scripts/prepare-ChangeLog @@ -269,7 +269,7 @@ if ($bugNumber) { if (`curl --version | grep ^Protocols` !~ /\bhttps\b/) { print STDERR " Could not get description for bug $bugNumber.\n"; print STDERR " It looks like your version of curl does not support ssl.\n"; - print STDERR " If you are using macports, this can be fixed with sudo port install curl+ssl.\n"; + print STDERR " If you are using macports, this can be fixed with sudo port install curl +ssl.\n"; } else { print STDERR " Bug $bugNumber has no bug description. Maybe you set wrong bug ID?\n"; print STDERR " The bug URL: $bugXMLURL\n"; diff --git a/WebKitTools/Scripts/run-chromium-webkit-unit-tests b/WebKitTools/Scripts/run-chromium-webkit-unit-tests new file mode 100755 index 0000000..62646af --- /dev/null +++ b/WebKitTools/Scripts/run-chromium-webkit-unit-tests @@ -0,0 +1,51 @@ +#!/usr/bin/perl -w +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use strict; +use File::Spec; +use FindBin; +use lib $FindBin::Bin; +use webkitdirs; + +# Allow running this script from any directory. +my $sourceRootDir = File::Spec->catfile($FindBin::Bin, "../.."); +chdir($sourceRootDir); + +setConfiguration(); + +my $pathToBinary; +if (isDarwin()) { + $pathToBinary = "WebKit/chromium/xcodebuild/" . configuration() . "/webkit_unit_tests"; +} elsif (isCygwin() || isWindows()) { + $pathToBinary = "WebKit/chromium/" . configuration() . "/webkit_unit_tests.exe"; +} elsif (isLinux()) { + $pathToBinary = "out/" . configuration() . "/webkit_unit_tests"; +} + +exit system ($pathToBinary, @ARGV); diff --git a/WebKitTools/Scripts/test-webkitpy b/WebKitTools/Scripts/test-webkitpy index e35c6e6..be7e870 100755 --- a/WebKitTools/Scripts/test-webkitpy +++ b/WebKitTools/Scripts/test-webkitpy @@ -137,8 +137,9 @@ def _clean_pyc_files(dir_to_clean, paths_not_to_log): # As a substitute for a unit test, this method tests _clean_pyc_files() # in addition to calling it. We chose not to use the unittest module # because _clean_pyc_files() is called only once and is not used elsewhere. -def _clean_webkitpy_with_test(): +def _clean_packages_with_test(external_package_paths): webkitpy_dir = os.path.join(os.path.dirname(__file__), "webkitpy") + package_paths = [webkitpy_dir] + external_package_paths # The test .pyc file is-- # webkitpy/python24/TEMP_test-webkitpy_test_pyc_file.pyc. @@ -156,13 +157,14 @@ def _clean_webkitpy_with_test(): if not os.path.exists(test_path): raise Exception("Test .pyc file not created: %s" % test_path) - _clean_pyc_files(webkitpy_dir, [test_path]) + for path in package_paths: + _clean_pyc_files(path, [test_path]) if os.path.exists(test_path): raise Exception("Test .pyc file not deleted: %s" % test_path) -def init(command_args): +def init(command_args, external_package_paths): """Execute code prior to importing from webkitpy.unittests. Args: @@ -186,8 +188,8 @@ def init(command_args): configure_logging(is_verbose_logging) _log.debug("Verbose WebKit logging enabled.") - # We clean orphaned *.pyc files from webkitpy prior to importing from - # webkitpy to make sure that no import statements falsely succeed. + # We clean orphaned *.pyc files from the packages prior to importing from + # them to make sure that no import statements falsely succeed. # This helps to check that import statements have been updated correctly # after any file moves. Otherwise, incorrect import statements can # be masked. @@ -208,7 +210,7 @@ def init(command_args): # # Deleting the orphaned .pyc file prior to importing, however, would # cause an ImportError to occur on import as desired. - _clean_webkitpy_with_test() + _clean_packages_with_test(external_package_paths) import webkitpy.python24.versioning as versioning @@ -227,7 +229,8 @@ def init(command_args): if __name__ == "__main__": - init(sys.argv[1:]) + external_package_paths = [os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'WebKit2', 'Scripts', 'webkit2')] + init(sys.argv[1:], external_package_paths) # We import the unit test code after init() to ensure that any # Python version warnings are displayed in case an error occurs @@ -237,4 +240,4 @@ if __name__ == "__main__": # running the unit tests. from webkitpy.test.main import Tester - Tester().run_tests(sys.argv) + Tester().run_tests(sys.argv, external_package_paths) diff --git a/WebKitTools/Scripts/webkitdirs.pm b/WebKitTools/Scripts/webkitdirs.pm index 2980750..08e14ab 100644 --- a/WebKitTools/Scripts/webkitdirs.pm +++ b/WebKitTools/Scripts/webkitdirs.pm @@ -1408,6 +1408,72 @@ sub buildAutotoolsProject($@) return $result; } +sub buildCMakeProject($@) +{ + my ($port, $clean, @buildParams) = @_; + my $dir = File::Spec->canonpath(baseProductDir()); + my $config = configuration(); + my $result; + my $makeArgs = ""; + my @buildArgs; + + $makeArgs .= " -j" . numberOfCPUs() if ($makeArgs !~ m/-j\s*\d+/); + + if ($clean) { + print "Cleaning the build directory '$dir'\n"; + $dir = File::Spec->catfile($dir, $config); + File::Path::remove_tree($dir, {keep_root => 1}); + $result = 0; + } else { + my $cmakebin = "cmake"; + my $make = "make"; + + push @buildArgs, "-DPORT=$port"; + + for my $i (0 .. $#buildParams) { + my $opt = $buildParams[$i]; + if ($opt =~ /^--makeargs=(.*)/i ) { + $makeArgs = $1; + } elsif ($opt =~ /^--prefix=(.*)/i ) { + push @buildArgs, "-DCMAKE_INSTALL_PREFIX=$1"; + } else { + push @buildArgs, $opt; + } + } + + if ($config =~ m/debug/i) { + push @buildArgs, "-DCMAKE_BUILD_TYPE=Debug"; + } elsif ($config =~ m/release/i) { + push @buildArgs, "-DCMAKE_BUILD_TYPE=Release"; + } + + push @buildArgs, sourceDir(); + + $dir = File::Spec->catfile($dir, $config); + File::Path::mkpath($dir); + chdir $dir or die "Failed to cd into " . $dir . "\n"; + + print "Calling '$cmakebin @buildArgs' in " . $dir . "\n\n"; + my $result = system "$cmakebin @buildArgs"; + if ($result ne 0) { + die "Failed while running $cmakebin to generate makefiles!\n"; + } + + print "Calling '$make $makeArgs' in " . $dir . "\n\n"; + $result = system "$make $makeArgs"; + + chdir ".." or die; + } + + return $result; +} + +sub buildCMakeEflProject($@) +{ + my ($clean, @buildArgs) = @_; + return buildCMakeProject("Efl", $clean, @buildArgs); +} + sub buildQMakeProject($@) { my ($clean, @buildParams) = @_; @@ -1480,7 +1546,6 @@ sub buildQMakeProject($@) } } - push @buildArgs, sourceDir() . "/WebKit.pro"; if ($config =~ m/debug/i) { push @buildArgs, "CONFIG-=release"; push @buildArgs, "CONFIG+=debug"; @@ -1495,6 +1560,8 @@ sub buildQMakeProject($@) } } + push @buildArgs, sourceDir() . "/WebKit.pro"; + print "Calling '$qmakebin @buildArgs' in " . $dir . "\n\n"; print "Installation headers directory: $installHeaders\n" if(defined($installHeaders)); print "Installation libraries directory: $installLibs\n" if(defined($installLibs)); @@ -1504,6 +1571,16 @@ sub buildQMakeProject($@) die "Failed to setup build environment using $qmakebin!\n"; } + # Manually create makefiles for the examples so we don't build by default + my $examplesDir = $dir . "/WebKit/qt/examples"; + File::Path::mkpath($examplesDir); + $buildArgs[-1] = sourceDir() . "/WebKit/qt/examples/examples.pro"; + chdir $examplesDir or die; + print "Calling '$qmakebin @buildArgs' in " . $examplesDir . "\n\n"; + $result = system "$qmakebin @buildArgs"; + die "Failed to create makefiles for the examples!\n" if $result ne 0; + chdir $dir or die; + if ($clean) { print "Calling '$make $makeargs distclean' in " . $dir . "\n\n"; $result = system "$make $makeargs distclean"; diff --git a/WebKitTools/Scripts/webkitpy/common/config/committers.py b/WebKitTools/Scripts/webkitpy/common/config/committers.py index 113131f..2d07158 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/committers.py +++ b/WebKitTools/Scripts/webkitpy/common/config/committers.py @@ -111,6 +111,7 @@ committers_unable_to_review = [ Committer("Graham Dennis", ["Graham.Dennis@gmail.com", "gdennis@webkit.org"]), Committer("Greg Bolsinga", "bolsinga@apple.com"), Committer("Hans Wennborg", "hans@chromium.org", "hwennborg"), + Committer("Hayato Ito", "hayato@chromium.org", "hayato"), Committer("Hin-Chung Lam", ["hclam@google.com", "hclam@chromium.org"]), Committer("Ilya Tikhonovsky", "loislo@chromium.org", "loislo"), Committer("Jakob Petsovits", ["jpetsovits@rim.com", "jpetso@gmx.at"], "jpetso"), @@ -136,6 +137,7 @@ committers_unable_to_review = [ Committer("Kent Hansen", "kent.hansen@nokia.com", "khansen"), Committer("Kinuko Yasuda", "kinuko@chromium.org", "kinuko"), Committer("Krzysztof Kowalczyk", "kkowalczyk@gmail.com"), + Committer("Kwang Yul Seo", ["kwangyul.seo@gmail.com", "skyul@company100.net", "kseo@webkit.org"], "kwangseo"), Committer("Leandro Pereira", ["leandro@profusion.mobi", "leandro@webkit.org"], "acidx"), Committer("Levi Weintraub", "lweintraub@apple.com"), Committer("Lucas De Marchi", ["lucas.demarchi@profusion.mobi", "demarchi@webkit.org"], "demarchi"), @@ -158,6 +160,7 @@ committers_unable_to_review = [ Committer("Noam Rosenthal", "noam.rosenthal@nokia.com", "noamr"), Committer("Pam Greene", "pam@chromium.org", "pamg"), Committer("Patrick Gansterer", ["paroga@paroga.com", "paroga@webkit.org"], "paroga"), + Committer("Pavel Podivilov", "podivilov@chromium.org", "podivilov"), Committer("Peter Kasting", ["pkasting@google.com", "pkasting@chromium.org"], "pkasting"), Committer("Philippe Normand", ["pnormand@igalia.com", "philn@webkit.org"], "philn-tp"), Committer("Pierre d'Herbemont", ["pdherbemont@free.fr", "pdherbemont@apple.com"], "pdherbemont"), diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py index 593ebc1..17f6c7a 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py @@ -34,6 +34,8 @@ import urllib import urllib2 import xmlrpclib +from webkitpy.common.net.failuremap import FailureMap +from webkitpy.common.net.regressionwindow import RegressionWindow from webkitpy.common.system.logutils import get_logger from webkitpy.thirdparty.autoinstalled.mechanize import Browser from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup @@ -145,9 +147,9 @@ class Builder(object): ) return build - def find_failure_transition(self, red_build, look_back_limit=30): + def find_regression_window(self, red_build, look_back_limit=30): if not red_build or red_build.is_green(): - return (None, None) + return RegressionWindow(None, None) common_failures = None current_build = red_build build_after_current_build = None @@ -172,34 +174,25 @@ class Builder(object): break look_back_count += 1 if look_back_count > look_back_limit: - return (None, current_build) + return RegressionWindow(None, current_build, common_failures=common_failures) build_after_current_build = current_build current_build = current_build.previous_build() # We must iterate at least once because red_build is red. assert(build_after_current_build) # Current build must either be green or have no failures in common # with red build, so we've found our failure transition. - return (current_build, build_after_current_build) + return RegressionWindow(current_build, build_after_current_build, common_failures=common_failures) - # FIXME: This likely does not belong on Builder - def suspect_revisions_for_transition(self, last_good_build, first_bad_build): - suspect_revisions = range(first_bad_build.revision(), - last_good_build.revision(), - -1) - suspect_revisions.reverse() - return suspect_revisions - - def blameworthy_revisions(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True): + def find_blameworthy_regression_window(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True): red_build = self.build(red_build_number) - (last_good_build, first_bad_build) = \ - self.find_failure_transition(red_build, look_back_limit) - if not last_good_build: - return [] # We ran off the limit of our search + regression_window = self.find_regression_window(red_build, look_back_limit) + if not regression_window.build_before_failure(): + return None # We ran off the limit of our search # If avoid_flakey_tests, require at least 2 bad builds before we # suspect a real failure transition. - if avoid_flakey_tests and first_bad_build == red_build: - return [] - return self.suspect_revisions_for_transition(last_good_build, first_bad_build) + if avoid_flakey_tests and regression_window.failing_build() == red_build: + return None + return regression_window # FIXME: This should be unified with all the layout test results code in the layout_tests package @@ -414,20 +407,27 @@ class BuildBot(object): build_status_url = "http://%s/one_box_per_builder" % self.buildbot_host return urllib2.urlopen(build_status_url) + def _file_cell_text(self, file_cell): + """Traverses down through firstChild elements until one containing a string is found, then returns that string""" + element = file_cell + while element.string is None and element.contents: + element = element.contents[0] + return element.string + def _parse_twisted_file_row(self, file_row): - string_or_empty = lambda soup: unicode(soup.string) if soup.string else u"" + string_or_empty = lambda string: unicode(string) if string else u"" file_cells = file_row.findAll('td') return { - "filename": string_or_empty(file_cells[0].find("a")), - "size": string_or_empty(file_cells[1]), - "type": string_or_empty(file_cells[2]), - "encoding": string_or_empty(file_cells[3]), + "filename": string_or_empty(self._file_cell_text(file_cells[0])), + "size": string_or_empty(self._file_cell_text(file_cells[1])), + "type": string_or_empty(self._file_cell_text(file_cells[2])), + "encoding": string_or_empty(self._file_cell_text(file_cells[3])), } def _parse_twisted_directory_listing(self, page): soup = BeautifulSoup(page) # HACK: Match only table rows with a class to ignore twisted header/footer rows. - file_rows = soup.find('table').findAll('tr', { "class" : True }) + file_rows = soup.find('table').findAll('tr', {'class': re.compile(r'\b(?:directory|file)\b')}) return [self._parse_twisted_file_row(file_row) for file_row in file_rows] # FIXME: There should be a better way to get this information directly from twisted. @@ -452,19 +452,17 @@ class BuildBot(object): self._builder_by_name[name] = builder return builder - def revisions_causing_failures(self, only_core_builders=True): + def failure_map(self, only_core_builders=True): builder_statuses = self.core_builder_statuses() if only_core_builders else self.builder_statuses() + failure_map = FailureMap() revision_to_failing_bots = {} for builder_status in builder_statuses: if builder_status["is_green"]: continue builder = self.builder_with_name(builder_status["name"]) - revisions = builder.blameworthy_revisions(builder_status["build_number"]) - for revision in revisions: - failing_bots = revision_to_failing_bots.get(revision, []) - failing_bots.append(builder) - revision_to_failing_bots[revision] = failing_bots - return revision_to_failing_bots + regression_window = builder.find_blameworthy_regression_window(builder_status["build_number"]) + failure_map.add_regression_window(builder, regression_window) + return failure_map # This makes fewer requests than calling Builder.latest_build would. It grabs all builder # statuses in one request using self.builder_statuses (fetching /one_box_per_builder instead of builder pages). diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py index b48f0e4..c99ab32 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py @@ -54,53 +54,53 @@ class BuilderTest(unittest.TestCase): self.builder = Builder(u"Test Builder \u2661", self.buildbot) self._install_fetch_build(lambda build_number: ["test1", "test2"]) - def test_find_failure_transition(self): - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1003) - self.assertEqual(red_build.revision(), 1004) + def test_find_regression_window(self): + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1003) + self.assertEqual(regression_window.failing_build().revision(), 1004) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10), look_back_limit=2) - self.assertEqual(green_build, None) - self.assertEqual(red_build.revision(), 1008) + regression_window = self.builder.find_regression_window(self.builder.build(10), look_back_limit=2) + self.assertEqual(regression_window.build_before_failure(), None) + self.assertEqual(regression_window.failing_build().revision(), 1008) def test_none_build(self): self.builder._fetch_build = lambda build_number: None - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build, None) - self.assertEqual(red_build, None) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure(), None) + self.assertEqual(regression_window.failing_build(), None) def test_flaky_tests(self): self._install_fetch_build(lambda build_number: ["test1"] if build_number % 2 else ["test2"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1009) - self.assertEqual(red_build.revision(), 1010) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1009) + self.assertEqual(regression_window.failing_build().revision(), 1010) def test_failure_and_flaky(self): self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1003) - self.assertEqual(red_build.revision(), 1004) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1003) + self.assertEqual(regression_window.failing_build().revision(), 1004) def test_no_results(self): self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1003) - self.assertEqual(red_build.revision(), 1004) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1003) + self.assertEqual(regression_window.failing_build().revision(), 1004) def test_failure_after_flaky(self): self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number > 6 else ["test3"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1006) - self.assertEqual(red_build.revision(), 1007) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1006) + self.assertEqual(regression_window.failing_build().revision(), 1007) - def test_blameworthy_revisions(self): - self.assertEqual(self.builder.blameworthy_revisions(10), [1004]) - self.assertEqual(self.builder.blameworthy_revisions(10, look_back_limit=2), []) + def test_find_blameworthy_regression_window(self): + self.assertEqual(self.builder.find_blameworthy_regression_window(10).revisions(), [1004]) + self.assertEqual(self.builder.find_blameworthy_regression_window(10, look_back_limit=2), None) # Flakey test avoidance requires at least 2 red builds: - self.assertEqual(self.builder.blameworthy_revisions(4), []) - self.assertEqual(self.builder.blameworthy_revisions(4, avoid_flakey_tests=False), [1004]) + self.assertEqual(self.builder.find_blameworthy_regression_window(4), None) + self.assertEqual(self.builder.find_blameworthy_regression_window(4, avoid_flakey_tests=False).revisions(), [1004]) # Green builder: - self.assertEqual(self.builder.blameworthy_revisions(3), []) + self.assertEqual(self.builder.find_blameworthy_regression_window(3), None) def test_build_caching(self): self.assertEqual(self.builder.build(10), self.builder.build(10)) @@ -361,22 +361,19 @@ class BuildBotTest(unittest.TestCase): <h1>Directory listing for /results/SnowLeopard Intel Leaks/</h1> <table> - <thead> - <tr> + <tr class="alt"> <th>Filename</th> <th>Size</th> <th>Content type</th> <th>Content encoding</th> </tr> - </thead> - <tbody> -<tr class="odd"> - <td><a href="r47483%20%281%29/">r47483 (1)/</a></td> - <td></td> - <td>[Directory]</td> - <td></td> +<tr class="directory "> + <td><a href="r47483%20%281%29/"><b>r47483 (1)/</b></a></td> + <td><b></b></td> + <td><b>[Directory]</b></td> + <td><b></b></td> </tr> -<tr class="odd"> +<tr class="file alt"> <td><a href="r47484%20%282%29.zip">r47484 (2).zip</a></td> <td>89K</td> <td>[application/zip]</td> diff --git a/WebKitTools/Scripts/webkitpy/common/net/credentials.py b/WebKitTools/Scripts/webkitpy/common/net/credentials.py index 1d5f83d..1c3e6c0 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/credentials.py +++ b/WebKitTools/Scripts/webkitpy/common/net/credentials.py @@ -39,14 +39,23 @@ from webkitpy.common.system.executive import Executive, ScriptError from webkitpy.common.system.user import User from webkitpy.common.system.deprecated_logging import log +try: + # Use keyring, a cross platform keyring interface, as a fallback: + # http://pypi.python.org/pypi/keyring + import keyring +except ImportError: + keyring = None + class Credentials(object): - def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd()): + def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd(), + keyring=keyring): self.host = host self.git_prefix = "%s." % git_prefix if git_prefix else "" self.executive = executive or Executive() self.cwd = cwd + self._keyring = keyring def _credentials_from_git(self): return [Git.read_git_config(self.git_prefix + "username"), @@ -117,10 +126,19 @@ class Credentials(object): if not username or not password: (username, password) = self._credentials_from_keychain(username) + if username and not password and self._keyring: + password = self._keyring.get_password(self.host, username) + if not username: username = User.prompt("%s login: " % self.host) if not password: password = getpass.getpass("%s password for %s: " % (self.host, username)) + if self._keyring: + store_password = User().confirm( + "Store password in system keyring?", User.DEFAULT_NO) + if store_password: + self._keyring.set_password(self.host, username, password) + return [username, password] diff --git a/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py index 9a42bdd..d30291b 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py @@ -113,5 +113,28 @@ password: "SECRETSAUCE" self.assertEqual(credentials.read_credentials(), ["test@webkit.org", "SECRETSAUCE"]) os.rmdir(temp_dir_path) + def test_keyring_without_git_repo(self): + class MockKeyring(object): + def get_password(self, host, username): + return "NOMNOMNOM" + + class FakeCredentials(Credentials): + def __init__(self, cwd): + Credentials.__init__(self, "fake.hostname", cwd=cwd, + keyring=MockKeyring()) + + def _is_mac_os_x(self): + return True + + def _credentials_from_keychain(self, username): + return ("test@webkit.org", None) + + temp_dir_path = tempfile.mkdtemp(suffix="not_a_git_repo") + credentials = FakeCredentials(temp_dir_path) + try: + self.assertEqual(credentials.read_credentials(), ["test@webkit.org", "NOMNOMNOM"]) + finally: + os.rmdir(temp_dir_path) + if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py new file mode 100644 index 0000000..98e4b8f --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py @@ -0,0 +1,48 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +class FailureMap(object): + def __init__(self): + self._failures = [] + + def add_regression_window(self, builder, regression_window): + self._failures.append({ + 'builder': builder, + 'regression_window': regression_window, + }) + + def revisions_causing_failures(self): + revision_to_failing_bots = {} + for failure_info in self._failures: + revisions = failure_info['regression_window'].revisions() + for revision in revisions: + failing_bots = revision_to_failing_bots.get(revision, []) + failing_bots.append(failure_info['builder']) + revision_to_failing_bots[revision] = failing_bots + return revision_to_failing_bots diff --git a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py new file mode 100644 index 0000000..231459f --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py @@ -0,0 +1,48 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +class RegressionWindow(object): + def __init__(self, build_before_failure, failing_build, common_failures=None): + self._build_before_failure = build_before_failure + self._failing_build = failing_build + self._common_failures = common_failures + + def build_before_failure(self): + return self._build_before_failure + + def failing_build(self): + return self._failing_build + + def common_failures(self): + return self._common_failures + + def revisions(self): + revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1) + revisions.reverse() + return revisions diff --git a/WebKitTools/Scripts/webkitpy/common/system/user.py b/WebKitTools/Scripts/webkitpy/common/system/user.py index 9444c00..240b67b 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user.py @@ -28,6 +28,7 @@ import logging import os +import re import shlex import subprocess import sys @@ -51,6 +52,9 @@ except ImportError: class User(object): + DEFAULT_NO = 'n' + DEFAULT_YES = 'y' + # FIXME: These are @classmethods because bugzilla.py doesn't have a Tool object (thus no User instance). @classmethod def prompt(cls, message, repeat=1, raw_input=raw_input): @@ -61,14 +65,30 @@ class User(object): return response @classmethod - def prompt_with_list(cls, list_title, list_items): + def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input): print list_title i = 0 for item in list_items: i += 1 print "%2d. %s" % (i, item) - result = int(cls.prompt("Enter a number: ")) - 1 - return list_items[result] + + # Loop until we get valid input + while True: + if can_choose_multiple: + response = cls.prompt("Enter one or more numbers (comma-separated), or \"all\": ", raw_input=raw_input) + if not response.strip() or response == "all": + return list_items + try: + indices = [int(r) - 1 for r in re.split("\s*,\s*", response)] + except ValueError, err: + continue + return [list_items[i] for i in indices] + else: + try: + result = int(cls.prompt("Enter a number: ", raw_input=raw_input)) - 1 + except ValueError, err: + continue + return list_items[result] def edit(self, files): editor = os.environ.get("EDITOR") or "vi" @@ -98,11 +118,14 @@ class User(object): except IOError, e: pass - def confirm(self, message=None): + def confirm(self, message=None, default=DEFAULT_YES, raw_input=raw_input): if not message: message = "Continue?" - response = raw_input("%s [Y/n]: " % message) - return not response or response.lower() == "y" + choice = {'y': 'Y/n', 'n': 'y/N'}[default] + response = raw_input("%s [%s]: " % (message, choice)) + if not response: + response = default + return response.lower() == 'y' def can_open_url(self): try: diff --git a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py index dadead3..ae1bad5 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py @@ -28,6 +28,7 @@ import unittest +from webkitpy.common.system.outputcapture import OutputCapture from webkitpy.common.system.user import User class UserTest(unittest.TestCase): @@ -50,5 +51,51 @@ class UserTest(unittest.TestCase): return None self.assertEqual(User.prompt("input", repeat=self.repeatsRemaining, raw_input=mock_raw_input), None) + def test_prompt_with_list(self): + def run_prompt_test(inputs, expected_result, can_choose_multiple=False): + def mock_raw_input(message): + return inputs.pop(0) + output_capture = OutputCapture() + actual_result = output_capture.assert_outputs( + self, + User.prompt_with_list, + args=["title", ["foo", "bar"]], + kwargs={"can_choose_multiple": can_choose_multiple, "raw_input": mock_raw_input}, + expected_stdout="title\n 1. foo\n 2. bar\n") + self.assertEqual(actual_result, expected_result) + self.assertEqual(len(inputs), 0) + + run_prompt_test(["1"], "foo") + run_prompt_test(["badinput", "2"], "bar") + + run_prompt_test(["1,2"], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test([" 1, 2 "], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test(["all"], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test([""], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test([" "], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test(["badinput", "all"], ["foo", "bar"], can_choose_multiple=True) + + def test_confirm(self): + test_cases = ( + (("Continue? [Y/n]: ", True), (User.DEFAULT_YES, 'y')), + (("Continue? [Y/n]: ", False), (User.DEFAULT_YES, 'n')), + (("Continue? [Y/n]: ", True), (User.DEFAULT_YES, '')), + (("Continue? [Y/n]: ", False), (User.DEFAULT_YES, 'q')), + (("Continue? [y/N]: ", True), (User.DEFAULT_NO, 'y')), + (("Continue? [y/N]: ", False), (User.DEFAULT_NO, 'n')), + (("Continue? [y/N]: ", False), (User.DEFAULT_NO, '')), + (("Continue? [y/N]: ", False), (User.DEFAULT_NO, 'q')), + ) + for test_case in test_cases: + expected, inputs = test_case + + def mock_raw_input(message): + self.assertEquals(expected[0], message) + return inputs[1] + + result = User().confirm(default=inputs[0], + raw_input=mock_raw_input) + self.assertEquals(expected[1], result) + if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.checksum b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.checksum deleted file mode 100644 index 5890112..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.checksum +++ /dev/null @@ -1 +0,0 @@ -checksum-checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.png deleted file mode 100644 index 83a5de3..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.png +++ /dev/null @@ -1 +0,0 @@ -checksum-png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.txt deleted file mode 100644 index 5628d69..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum-expected.txt +++ /dev/null @@ -1 +0,0 @@ -checksum-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum.html deleted file mode 100644 index 2b78d31..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/checksum.html +++ /dev/null @@ -1 +0,0 @@ -image_checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/crash.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/crash.html deleted file mode 100644 index 0bc3798..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/crash.html +++ /dev/null @@ -1 +0,0 @@ -crash diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/exception.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/exception.html deleted file mode 100644 index 38c54e3..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/exception.html +++ /dev/null @@ -1 +0,0 @@ -exception diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/hang.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/hang.html deleted file mode 100644 index 4e0de08..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/hang.html +++ /dev/null @@ -1 +0,0 @@ -timeout-thread diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.checksum b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.checksum deleted file mode 100644 index 24b887a..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.checksum +++ /dev/null @@ -1 +0,0 @@ -image-checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.png deleted file mode 100644 index 4c23996..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.png +++ /dev/null @@ -1 +0,0 @@ -image-png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.txt deleted file mode 100644 index c6ee718..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image-expected.txt +++ /dev/null @@ -1 +0,0 @@ -image-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image.html deleted file mode 100644 index 53e4b27..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image.html +++ /dev/null @@ -1 +0,0 @@ -image_failure diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.checksum b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.checksum deleted file mode 100644 index 8fa0851..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.checksum +++ /dev/null @@ -1 +0,0 @@ -image_checksum-checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.png deleted file mode 100644 index d677d2e..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.png +++ /dev/null @@ -1 +0,0 @@ -image_checksum-png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.txt deleted file mode 100644 index 453f213..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum-expected.txt +++ /dev/null @@ -1 +0,0 @@ -image_checksum-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum.html deleted file mode 100644 index 2b78d31..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/image_checksum.html +++ /dev/null @@ -1 +0,0 @@ -image_checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/keyboard.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/keyboard.html deleted file mode 100644 index c253983..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/keyboard.html +++ /dev/null @@ -1 +0,0 @@ -keyboard diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check-expected.png deleted file mode 100644 index e45c7af..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check-expected.png +++ /dev/null @@ -1 +0,0 @@ -missing_check-png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check-expected.txt deleted file mode 100644 index 0ea9227..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check-expected.txt +++ /dev/null @@ -1 +0,0 @@ -missing_check-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check.html deleted file mode 100644 index 0af8000..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_check.html +++ /dev/null @@ -1 +0,0 @@ -missing_image diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_image.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_image.html deleted file mode 100644 index 0af8000..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_image.html +++ /dev/null @@ -1 +0,0 @@ -missing_image diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_text.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_text.html deleted file mode 100644 index 47b8ad6..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/missing_text.html +++ /dev/null @@ -1 +0,0 @@ -missing_text diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/text-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/text-expected.txt deleted file mode 100644 index e21ea45..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/text-expected.txt +++ /dev/null @@ -1 +0,0 @@ -text_failures-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/text.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/text.html deleted file mode 100644 index 91f5fc7..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/text.html +++ /dev/null @@ -1 +0,0 @@ -text_failure diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/timeout.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/timeout.html deleted file mode 100644 index 790851a..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/expected/timeout.html +++ /dev/null @@ -1 +0,0 @@ -timeout diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.checksum b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.checksum deleted file mode 100644 index 0c4f6da..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.checksum +++ /dev/null @@ -1 +0,0 @@ -fail_checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.png deleted file mode 100644 index db483ee..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.png +++ /dev/null @@ -1 +0,0 @@ -fail_png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.txt deleted file mode 100644 index a1f3c24..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum-expected.txt +++ /dev/null @@ -1 +0,0 @@ -fail_output diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum.html deleted file mode 100644 index b325924..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/failures/unexpected/text-image-checksum.html +++ /dev/null @@ -1 +0,0 @@ -Google diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/passes/text-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/passes/text-expected.txt deleted file mode 100644 index 2b38a06..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/passes/text-expected.txt +++ /dev/null @@ -1 +0,0 @@ -text-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/passes/text.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/passes/text.html deleted file mode 100644 index 8e27be7..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/passes/text.html +++ /dev/null @@ -1 +0,0 @@ -text diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/ssl/text-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/ssl/text-expected.txt deleted file mode 100644 index 2b38a06..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/ssl/text-expected.txt +++ /dev/null @@ -1 +0,0 @@ -text-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/ssl/text.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/ssl/text.html deleted file mode 100644 index 8e27be7..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/http/tests/ssl/text.html +++ /dev/null @@ -1 +0,0 @@ -text diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/error-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/error-expected.txt deleted file mode 100644 index 9427269..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/error-expected.txt +++ /dev/null @@ -1 +0,0 @@ -error-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/error.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/error.html deleted file mode 100644 index 8276753..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/error.html +++ /dev/null @@ -1 +0,0 @@ -error diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.checksum b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.checksum deleted file mode 100644 index 24b887a..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.checksum +++ /dev/null @@ -1 +0,0 @@ -image-checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.png deleted file mode 100644 index 4c23996..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.png +++ /dev/null @@ -1 +0,0 @@ -image-png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.txt deleted file mode 100644 index c6ee718..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image-expected.txt +++ /dev/null @@ -1 +0,0 @@ -image-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image.html deleted file mode 100644 index 773b222..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/image.html +++ /dev/null @@ -1 +0,0 @@ -image diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.checksum b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.checksum deleted file mode 100644 index 52038ae..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.checksum +++ /dev/null @@ -1 +0,0 @@ -platform_image-generic-checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.png deleted file mode 100644 index 087872b..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.png +++ /dev/null @@ -1 +0,0 @@ -platform_image-generic-png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.txt deleted file mode 100644 index f71680c..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image-expected.txt +++ /dev/null @@ -1 +0,0 @@ -platform_image-generic-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image.html deleted file mode 100644 index ca48a7b..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/platform_image.html +++ /dev/null @@ -1 +0,0 @@ -platform_image diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/text-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/text-expected.txt deleted file mode 100644 index 2b38a06..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/text-expected.txt +++ /dev/null @@ -1 +0,0 @@ -text-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/text.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/text.html deleted file mode 100644 index 8e27be7..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/passes/text.html +++ /dev/null @@ -1 +0,0 @@ -text diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.checksum b/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.checksum deleted file mode 100644 index ea557cf..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.checksum +++ /dev/null @@ -1 +0,0 @@ -platform_image-checksum diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.png b/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.png deleted file mode 100644 index ec42fc1..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.png +++ /dev/null @@ -1 +0,0 @@ -platform_image-png diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.txt deleted file mode 100644 index ff8bf43..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/passes/platform_image-expected.txt +++ /dev/null @@ -1 +0,0 @@ -platform_image-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/test_expectations.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/test_expectations.txt deleted file mode 100644 index 0619fde..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/platform/test/test_expectations.txt +++ /dev/null @@ -1,13 +0,0 @@ -WONTFIX : failures/expected/checksum.html = IMAGE -WONTFIX : failures/expected/crash.html = CRASH -// This one actually passes because the checksums will match. -WONTFIX : failures/expected/image.html = PASS -WONTFIX : failures/expected/image_checksum.html = IMAGE -WONTFIX : failures/expected/missing_check.html = MISSING PASS -WONTFIX : failures/expected/missing_image.html = MISSING PASS -WONTFIX : failures/expected/missing_text.html = MISSING PASS -WONTFIX : failures/expected/text.html = TEXT -WONTFIX : failures/expected/timeout.html = TIMEOUT -WONTFIX SKIP : failures/expected/hang.html = TIMEOUT -WONTFIX SKIP : failures/expected/keyboard.html = CRASH -WONTFIX SKIP : failures/expected/exception.html = CRASH diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/resources/README.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/resources/README.txt deleted file mode 100644 index b806b06..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/resources/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -This directory exists solely to make sure that when we gather the lists of -tests, we skip over directories named 'resources'. diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/websocket/tests/passes/text-expected.txt b/WebKitTools/Scripts/webkitpy/layout_tests/data/websocket/tests/passes/text-expected.txt deleted file mode 100644 index 2b38a06..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/websocket/tests/passes/text-expected.txt +++ /dev/null @@ -1 +0,0 @@ -text-txt diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/data/websocket/tests/passes/text.html b/WebKitTools/Scripts/webkitpy/layout_tests/data/websocket/tests/passes/text.html deleted file mode 100644 index 8e27be7..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/data/websocket/tests/passes/text.html +++ /dev/null @@ -1 +0,0 @@ -text diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py index d420631..00ff211 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py @@ -351,11 +351,20 @@ class Printer(object): filename = result.filename test_name = self._port.relative_test_filename(filename) self._write('trace: %s' % test_name) - self._write(' txt: %s' % - self._port.relative_test_filename( - self._port.expected_filename(filename, '.txt'))) + txt_file = self._port.expected_filename(filename, '.txt') + if self._port.path_exists(txt_file): + self._write(' txt: %s' % + self._port.relative_test_filename(txt_file)) + else: + self._write(' txt: <none>') + checksum_file = self._port.expected_filename(filename, '.checksum') + if self._port.path_exists(checksum_file): + self._write(' sum: %s' % + self._port.relative_test_filename(checksum_file)) + else: + self._write(' sum: <none>') png_file = self._port.expected_filename(filename, '.png') - if os.path.exists(png_file): + if self._port.path_exists(png_file): self._write(' png: %s' % self._port.relative_test_filename(png_file)) else: diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing_unittest.py index 29139d0..0344aa7 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing_unittest.py @@ -151,7 +151,7 @@ class Testprinter(unittest.TestCase): expectations = test_expectations.TestExpectations( self._port, test_paths, expectations_str, self._port.test_platform_name(), is_debug_mode=False, - is_lint_mode=False, tests_are_present=False) + is_lint_mode=False) rs = run_webkit_tests.ResultSummary(expectations, test_paths) return test_paths, rs, expectations @@ -318,6 +318,16 @@ class Testprinter(unittest.TestCase): self.assertFalse(err.empty()) printer, err, out = self.get_printer(['--print', 'trace-everything']) + result = self.get_result('passes/image.html') + printer.print_test_result(result, expected=True, exp_str='', + got_str='') + result = self.get_result('failures/expected/missing_text.html') + printer.print_test_result(result, expected=True, exp_str='', + got_str='') + result = self.get_result('failures/expected/missing_check.html') + printer.print_test_result(result, expected=True, exp_str='', + got_str='') + result = self.get_result('failures/expected/missing_image.html') printer.print_test_result(result, expected=True, exp_str='', got_str='') self.assertFalse(err.empty()) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py index 3d8349b..508a6ad 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py @@ -87,8 +87,7 @@ class TestExpectations: TEST_LIST = "test_expectations.txt" def __init__(self, port, tests, expectations, test_platform_name, - is_debug_mode, is_lint_mode, tests_are_present=True, - overrides=None): + is_debug_mode, is_lint_mode, overrides=None): """Loads and parses the test expectations given in the string. Args: port: handle to object containing platform-specific functionality @@ -101,10 +100,6 @@ class TestExpectations: in the expectations is_lint_mode: If True, just parse the expectations string looking for errors. - tests_are_present: whether the test files exist in the file - system and can be probed for. This is useful for distinguishing - test files from directories, and is needed by the LTTF - dashboard, where the files aren't actually locally present. overrides: test expectations that are allowed to override any entries in |expectations|. This is used by callers that need to manage two sets of expectations (e.g., upstream @@ -112,7 +107,7 @@ class TestExpectations: """ self._expected_failures = TestExpectationsFile(port, expectations, tests, test_platform_name, is_debug_mode, is_lint_mode, - tests_are_present=tests_are_present, overrides=overrides) + overrides=overrides) # TODO(ojan): Allow for removing skipped tests when getting the list of # tests to run, but not when getting metrics. @@ -302,8 +297,7 @@ class TestExpectationsFile: 'flaky': FLAKY} def __init__(self, port, expectations, full_test_list, test_platform_name, - is_debug_mode, is_lint_mode, suppress_errors=False, - tests_are_present=True, overrides=None): + is_debug_mode, is_lint_mode, suppress_errors=False, overrides=None): """ expectations: Contents of the expectations file full_test_list: The list of all tests to be run pending processing of @@ -314,9 +308,6 @@ class TestExpectationsFile: is_debug_mode: Whether we testing a test_shell built debug mode. is_lint_mode: Whether this is just linting test_expecatations.txt. suppress_errors: Whether to suppress lint errors. - tests_are_present: Whether the test files are present in the local - filesystem. The LTTF Dashboard uses False here to avoid having to - keep a local copy of the tree. overrides: test expectations that are allowed to override any entries in |expectations|. This is used by callers that need to manage two sets of expectations (e.g., upstream @@ -329,7 +320,6 @@ class TestExpectationsFile: self._test_platform_name = test_platform_name self._is_debug_mode = is_debug_mode self._is_lint_mode = is_lint_mode - self._tests_are_present = tests_are_present self._overrides = overrides self._suppress_errors = suppress_errors self._errors = [] @@ -462,7 +452,7 @@ class TestExpectationsFile: def remove_platform_from_expectations(self, tests, platform): """Returns a copy of the expectations with the tests matching the - platform remove. + platform removed. If a test is in the test list and has an option that matches the given platform, remove the matching platform and save the updated test back @@ -699,8 +689,8 @@ class TestExpectationsFile: # WebKit's way of skipping tests is to add a -disabled suffix. # So we should consider the path existing if the path or the # -disabled version exists. - if (self._tests_are_present and not os.path.exists(full_path) - and not os.path.exists(full_path + '-disabled')): + if (not self._port.path_exists(full_path) + and not self._port.path_exists(full_path + '-disabled')): # Log a non fatal error here since you hit this case any # time you update test_expectations.txt without syncing # the LayoutTests directory @@ -735,7 +725,8 @@ class TestExpectationsFile: path and make sure directories end with the OS path separator.""" path = os.path.join(self._port.layout_tests_dir(), test_list_path) path = os.path.normpath(path) - path = self._fix_dir(path) + if self._port.path_isdir(path): + path = os.path.join(path, '') result = [] for test in self._full_test_list: @@ -743,20 +734,6 @@ class TestExpectationsFile: result.append(test) return result - def _fix_dir(self, path): - """Check to see if the path points to a directory, and if so, append - the directory separator if necessary.""" - if self._tests_are_present: - if os.path.isdir(path): - path = os.path.join(path, '') - else: - # If we can't check the filesystem to see if this is a directory, - # we assume that files w/o an extension are directories. - # TODO(dpranke): What happens w/ LayoutTests/css2.1 ? - if os.path.splitext(path)[1] == '': - path = os.path.join(path, '') - return path - def _add_tests(self, tests, expectations, test_list_path, lineno, modifiers, options, overrides_allowed): for test in tests: diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations_unittest.py index 26eb18d..2e1b6ec 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations_unittest.py @@ -106,14 +106,13 @@ BUG_TEST WONTFIX WIN : failures/expected/image.html = IMAGE """ def parse_exp(self, expectations, overrides=None, is_lint_mode=False, - is_debug_mode=False, tests_are_present=True): + is_debug_mode=False): self._exp = TestExpectations(self._port, tests=self.get_basic_tests(), expectations=expectations, test_platform_name=self._port.test_platform_name(), is_debug_mode=is_debug_mode, is_lint_mode=is_lint_mode, - tests_are_present=tests_are_present, overrides=overrides) def assert_exp(self, test, result): @@ -151,10 +150,6 @@ BUGX DEFER : failures/expected = IMAGE self.assert_exp('failures/expected/text.html', TEXT) self.assert_exp('failures/expected/crash.html', IMAGE) - self.parse_exp(exp_str, tests_are_present=False) - self.assert_exp('failures/expected/text.html', TEXT) - self.assert_exp('failures/expected/crash.html', IMAGE) - def test_release_mode(self): self.parse_exp('BUGX DEBUG : failures/expected/text.html = TEXT', is_debug_mode=True) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py index 70beac3..6a5d43b 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py @@ -42,11 +42,13 @@ import sys import time import apache_http_server +import test_files import http_server import websocket_server from webkitpy.common.system import logutils from webkitpy.common.system.executive import Executive, ScriptError +from webkitpy.common.system.user import User _log = logutils.get_logger(__file__) @@ -81,14 +83,15 @@ class Port(object): } return flags_by_configuration[configuration] - def __init__(self, port_name=None, options=None, executive=Executive()): - self._name = port_name - self._options = options + def __init__(self, **kwargs): + self._name = kwargs.get('port_name', None) + self._options = kwargs.get('options', None) + self._executive = kwargs.get('executive', Executive()) + self._user = kwargs.get('user', User()) self._helper = None self._http_server = None self._webkit_base_dir = None self._websocket_server = None - self._executive = executive def default_child_processes(self): """Return the number of DumpRenderTree instances to use for this @@ -130,11 +133,11 @@ class Port(object): interface so that it can be overriden for testing purposes.""" return expected_text != actual_text - def diff_image(self, expected_filename, actual_filename, + def diff_image(self, expected_contents, actual_contents, diff_filename=None, tolerance=0): - """Compare two image files and produce a delta image file. + """Compare two images and produce a delta image file. - Return True if the two files are different, False if they are the same. + Return True if the two images are different, False if they are the same. Also produce a delta image of the two images and write that into |diff_filename| if it is not None. @@ -252,6 +255,31 @@ class Port(object): return os.path.join(platform_dir, baseline_filename) return os.path.join(self.layout_tests_dir(), baseline_filename) + def _expected_file_contents(self, test, extension, encoding): + path = self.expected_filename(test, extension) + if not os.path.exists(path): + return None + with codecs.open(path, 'r', encoding) as file: + return file.read() + + def expected_checksum(self, test): + """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test.""" + return self._expected_file_contents(test, '.checksum', 'ascii') + + def expected_image(self, test): + """Returns the image we expect the test to produce.""" + return self._expected_file_contents(test, '.png', None) + + def expected_text(self, test): + """Returns the text output we expect the test to produce.""" + # NOTE: -expected.txt files are ALWAYS utf-8. However, + # we do not decode the output from DRT, so we should not + # decode the -expected.txt values either to allow comparisons. + text = self._expected_file_contents(test, '.txt', None) + if not text: + return '' + return text.strip("\r\n").replace("\r\n", "\n") + "\n" + def filename_to_uri(self, filename): """Convert a test file to a URI.""" LAYOUTTEST_HTTP_DIR = "http/tests/" @@ -287,6 +315,73 @@ class Port(object): return "file:///" + self.get_absolute_path(filename) return "file://" + self.get_absolute_path(filename) + def tests(self, paths): + """Return the list of tests found (relative to layout_tests_dir().""" + return test_files.find(self, paths) + + def test_dirs(self): + """Returns the list of top-level test directories. + + Used by --clobber-old-results.""" + layout_tests_dir = self.layout_tests_dir() + return filter(lambda x: os.path.isdir(os.path.join(layout_tests_dir, x)), + os.listdir(layout_tests_dir)) + + def path_isdir(self, path): + """Returns whether the path refers to a directory of tests. + + Used by test_expectations.py to apply rules to whole directories.""" + return os.path.isdir(path) + + def path_exists(self, path): + """Returns whether the path refers to an existing test or baseline.""" + # Used by test_expectations.py to determine if an entry refers to a + # valid test and by printing.py to determine if baselines exist.""" + return os.path.exists(path) + + def update_baseline(self, path, data, encoding): + """Updates the baseline for a test. + + Args: + path: the actual path to use for baseline, not the path to + the test. This function is used to update either generic or + platform-specific baselines, but we can't infer which here. + data: contents of the baseline. + encoding: file encoding to use for the baseline. + """ + with codecs.open(path, "w", encoding=encoding) as file: + file.write(data) + + def uri_to_test_name(self, uri): + """Return the base layout test name for a given URI. + + This returns the test name for a given URI, e.g., if you passed in + "file:///src/LayoutTests/fast/html/keygen.html" it would return + "fast/html/keygen.html". + + """ + test = uri + if uri.startswith("file:///"): + if sys.platform == 'win32': + test = test.replace('file:///', '') + test = test.replace('/', '\\') + else: + test = test.replace('file://', '') + return self.relative_test_filename(test) + + if uri.startswith("http://127.0.0.1:8880/"): + # websocket tests + return test.replace('http://127.0.0.1:8880/', '') + + if uri.startswith("http://"): + # regular HTTP test + return test.replace('http://127.0.0.1:8000/', 'http/tests/') + + if uri.startswith("https://"): + return test.replace('https://127.0.0.1:8443/', 'http/tests/') + + raise NotImplementedError('unknown url type: %s' % uri) + def get_absolute_path(self, filename): """Return the absolute path in unix format for the given filename. @@ -369,10 +464,10 @@ class Port(object): """ return os.environ.copy() - def show_html_results_file(self, results_filename): + def show_results_html_file(self, results_filename): """This routine should display the HTML file pointed at by results_filename in a users' browser.""" - raise NotImplementedError('Port.show_html_results_file') + return self._user.open_url(results_filename) def create_driver(self, image_path, options): """Return a newly created base.Driver subclass for starting/stopping @@ -588,7 +683,7 @@ class Port(object): try: with self._open_configuration_file() as file: return file.readline().rstrip() - except IOError, e: + except: return None # FIXME: This list may be incomplete as Apple has some sekret configs. diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py index 780cd22..71877b3 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py @@ -57,16 +57,17 @@ class MockExecutive(): class UnitTestPort(base.Port): """Subclass of base.Port used for unit testing.""" - def __init__(self, configuration_contents=None, executive_exception=None): + def __init__(self, configuration_contents=None, configuration_exception=IOError, executive_exception=None): base.Port.__init__(self) self._configuration_contents = configuration_contents + self._configuration_exception = configuration_exception if executive_exception: self._executive = MockExecutive(executive_exception) def _open_configuration_file(self): if self._configuration_contents: return NewStringIO(self._configuration_contents) - raise IOError + raise self._configuration_exception class PortTest(unittest.TestCase): @@ -191,9 +192,14 @@ class PortTest(unittest.TestCase): self.assertFalse('nosuchthing' in diff) def test_default_configuration_notfound(self): + # Regular IOError thrown while trying to get the configuration. port = UnitTestPort() self.assertEqual(port.default_configuration(), "Release") + # More exotic OSError thrown. + port = UnitTestPort(configuration_exception=OSError) + self.assertEqual(port.default_configuration(), "Release") + def test_layout_tests_skipping(self): port = base.Port() port.skipped_layout_tests = lambda: ['foo/bar.html', 'media'] @@ -214,6 +220,11 @@ class PortTest(unittest.TestCase): # This routine is a no-op. We just test it for coverage. port.setup_test_run() + def test_test_dirs(self): + port = base.Port() + dirs = port.test_dirs() + self.assertTrue('canvas' in dirs) + self.assertTrue('css2.1' in dirs) class VirtualTest(unittest.TestCase): """Tests that various methods expected to be virtual are.""" @@ -231,7 +242,6 @@ class VirtualTest(unittest.TestCase): self.assertVirtual(port.path_to_test_expectations_file) self.assertVirtual(port.test_platform_name) self.assertVirtual(port.results_directory) - self.assertVirtual(port.show_html_results_file, None) self.assertVirtual(port.test_expectations) self.assertVirtual(port.test_base_platform_names) self.assertVirtual(port.test_platform_name) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py index 3fc4613..a72627a 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py @@ -39,6 +39,7 @@ import shutil import signal import subprocess import sys +import tempfile import time import webbrowser @@ -46,7 +47,6 @@ import base import http_server from webkitpy.common.system.executive import Executive -from webkitpy.layout_tests.layout_package import test_files from webkitpy.layout_tests.layout_package import test_expectations # Chromium DRT on OSX uses WebKitDriver. @@ -82,8 +82,13 @@ def check_file_exists(path_to_file, file_description, override_step=None, class ChromiumPort(base.Port): """Abstract base class for Chromium implementations of the Port class.""" - def __init__(self, port_name=None, options=None, **kwargs): - base.Port.__init__(self, port_name, options, **kwargs) + def __init__(self, **kwargs): + base.Port.__init__(self, **kwargs) + if 'options' in kwargs: + options = kwargs['options'] + if (options and (not hasattr(options, 'configuration') or + options.configuration is None)): + options.configuration = self.default_configuration() self._chromium_base_dir = None def baseline_path(self): @@ -126,14 +131,18 @@ class ChromiumPort(base.Port): return check_file_exists(image_diff_path, 'image diff exe', override_step, logging) - def diff_image(self, expected_filename, actual_filename, + def diff_image(self, expected_contents, actual_contents, diff_filename=None, tolerance=0): executable = self._path_to_image_diff() + expected_tmpfile = tempfile.NamedTemporaryFile() + expected_tmpfile.write(expected_contents) + actual_tmpfile = tempfile.NamedTemporaryFile() + actual_tmpfile.write(actual_contents) if diff_filename: - cmd = [executable, '--diff', expected_filename, actual_filename, - diff_filename] + cmd = [executable, '--diff', expected_tmpfile.name, + actual_tmpfile.name, diff_filename] else: - cmd = [executable, expected_filename, actual_filename] + cmd = [executable, expected_tmpfile.name, actual_tmpfile.name] result = True try: @@ -144,6 +153,9 @@ class ChromiumPort(base.Port): _compare_available = False else: raise e + finally: + expected_tmpfile.close() + actual_tmpfile.close() return result def driver_name(self): @@ -183,15 +195,6 @@ class ChromiumPort(base.Port): if os.path.exists(cachedir): shutil.rmtree(cachedir) - def show_results_html_file(self, results_filename): - uri = self.get_absolute_path(results_filename) - if self._options.use_drt: - # FIXME: This should use User.open_url - webbrowser.open(uri, new=1) - else: - # Note: Not thread safe: http://bugs.python.org/issue2320 - subprocess.Popen([self._path_to_driver(), uri]) - def create_driver(self, image_path, options): """Starts a new Driver and returns a handle to it.""" if options.use_drt and sys.platform == 'darwin': @@ -236,7 +239,7 @@ class ChromiumPort(base.Port): # FIXME: This drt_overrides handling should be removed when we switch # from tes_shell to DRT. drt_overrides = '' - if self._options.use_drt: + if self._options and self._options.use_drt: drt_overrides_path = self.path_from_webkit_base('LayoutTests', 'platform', 'chromium', 'drt_expectations.txt') if os.path.exists(drt_overrides_path): @@ -259,14 +262,13 @@ class ChromiumPort(base.Port): test_platform_name = self.test_platform_name() is_debug_mode = False - all_test_files = test_files.gather_test_files(self, '*') + all_test_files = self.tests([]) if extra_test_files: all_test_files.update(extra_test_files) expectations = test_expectations.TestExpectations( self, all_test_files, expectations_str, test_platform_name, - is_debug_mode, is_lint_mode=True, - tests_are_present=False, overrides=overrides_str) + is_debug_mode, is_lint_mode=True, overrides=overrides_str) tests_dir = self.layout_tests_dir() return [self.relative_test_filename(test) for test in expectations.get_tests_with_result_type(test_expectations.SKIP)] @@ -354,6 +356,12 @@ class ChromiumDriver(base.Driver): if self._options.gp_fault_error_box: driver_args.append('--gp-fault-error-box') + + if self._options.accelerated_compositing: + driver_args.append('--enable-accelerated-compositing') + + if self._options.accelerated_2d_canvas: + driver_args.append('--enable-accelerated-2d-canvas') return driver_args def start(self): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py new file mode 100644 index 0000000..80602d9 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import with_statement + +import codecs +import os +import sys + +import chromium_linux +import chromium_mac +import chromium_win + + +def get(**kwargs): + """Some tests have slightly different results when run while using + hardware acceleration. In those cases, we prepend an additional directory + to the baseline paths.""" + port_name = kwargs.get('port_name', None) + if port_name == 'chromium-gpu': + if sys.platform in ('cygwin', 'win32'): + port_name = 'chromium-gpu-win' + elif sys.platform == 'linux2': + port_name = 'chromium-gpu-linux' + elif sys.platform == 'darwin': + port_name = 'chromium-gpu-mac' + else: + raise NotImplementedError('unsupported platform: %s' % + sys.platform) + + if port_name == 'chromium-gpu-linux': + return ChromiumGpuLinuxPort(**kwargs) + + if port_name.startswith('chromium-gpu-mac'): + return ChromiumGpuMacPort(**kwargs) + + if port_name.startswith('chromium-gpu-win'): + return ChromiumGpuWinPort(**kwargs) + + raise NotImplementedError('unsupported port: %s' % port_name) + + +def _set_gpu_options(options): + if options: + if options.accelerated_compositing is None: + options.accelerated_composting = True + if options.accelerated_2d_canvas is None: + options.accelerated_2d_canvas = True + + +def _gpu_overrides(port): + try: + overrides_path = port.path_from_chromium_base('webkit', 'tools', + 'layout_tests', 'test_expectations_gpu.txt') + except AssertionError: + return None + if not os.path.exists(overrides_path): + return None + with codecs.open(overrides_path, "r", "utf-8") as file: + return file.read() + + +class ChromiumGpuLinuxPort(chromium_linux.ChromiumLinuxPort): + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'chromium-gpu-linux') + _set_gpu_options(kwargs.get('options')) + chromium_linux.ChromiumLinuxPort.__init__(self, **kwargs) + + def baseline_search_path(self): + return ([self._webkit_baseline_path('chromium-gpu-linux')] + + chromium_linux.ChromiumLinuxPort.baseline_search_path(self)) + + def path_to_test_expectations_file(self): + return self.path_from_webkit_base('LayoutTests', 'platform', + 'chromium-gpu', 'test_expectations.txt') + + def test_expectations_overrides(self): + return _gpu_overrides(self) + + +class ChromiumGpuMacPort(chromium_mac.ChromiumMacPort): + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'chromium-gpu-mac') + _set_gpu_options(kwargs.get('options')) + chromium_mac.ChromiumMacPort.__init__(self, **kwargs) + + def baseline_search_path(self): + return ([self._webkit_baseline_path('chromium-gpu-mac')] + + chromium_mac.ChromiumMacPort.baseline_search_path(self)) + + def path_to_test_expectations_file(self): + return self.path_from_webkit_base('LayoutTests', 'platform', + 'chromium-gpu', 'test_expectations.txt') + + def test_expectations_overrides(self): + return _gpu_overrides(self) + + +class ChromiumGpuWinPort(chromium_win.ChromiumWinPort): + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'chromium-gpu-win' + self.version()) + _set_gpu_options(kwargs.get('options')) + chromium_win.ChromiumWinPort.__init__(self, **kwargs) + + def baseline_search_path(self): + return ([self._webkit_baseline_path('chromium-gpu-win')] + + chromium_win.ChromiumWinPort.baseline_search_path(self)) + + def path_to_test_expectations_file(self): + return self.path_from_webkit_base('LayoutTests', 'platform', + 'chromium-gpu', 'test_expectations.txt') + + def test_expectations_overrides(self): + return _gpu_overrides(self) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py new file mode 100644 index 0000000..5c79a3f --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import unittest +import chromium_gpu + + +class ChromiumGpuTest(unittest.TestCase): + def test_get_chromium_gpu_linux(self): + self.assertOverridesWorked('chromium-gpu-linux') + + def test_get_chromium_gpu_mac(self): + self.assertOverridesWorked('chromium-gpu-mac') + + def test_get_chromium_gpu_win(self): + self.assertOverridesWorked('chromium-gpu-win') + + def assertOverridesWorked(self, port_name): + # test that we got the right port + port = chromium_gpu.get(port_name=port_name, options=None) + + # we use startswith() instead of Equal to gloss over platform versions. + self.assertTrue(port.name().startswith(port_name)) + + # test that it has the right directory in front of the search path. + path = port.baseline_search_path()[0] + self.assertEqual(port._webkit_baseline_path(port_name), path) + + # test that we have the right expectations file. + self.assertTrue('chromium-gpu' in + port.path_to_test_expectations_file()) + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py index 4df43e0..176991b 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py @@ -41,12 +41,9 @@ _log = logging.getLogger("webkitpy.layout_tests.port.chromium_linux") class ChromiumLinuxPort(chromium.ChromiumPort): """Chromium Linux implementation of the Port class.""" - def __init__(self, port_name=None, options=None): - if port_name is None: - port_name = 'chromium-linux' - if options and not hasattr(options, 'configuration'): - options.configuration = 'Release' - chromium.ChromiumPort.__init__(self, port_name, options) + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'chromium-linux') + chromium.ChromiumPort.__init__(self, **kwargs) def baseline_search_path(self): port_names = ["chromium-linux", "chromium-win", "chromium", "win", "mac"] diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py index abd84ae..64016ab 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py @@ -44,12 +44,9 @@ _log = logging.getLogger("webkitpy.layout_tests.port.chromium_mac") class ChromiumMacPort(chromium.ChromiumPort): """Chromium Mac implementation of the Port class.""" - def __init__(self, port_name=None, options=None): - if port_name is None: - port_name = 'chromium-mac' - if options and not hasattr(options, 'configuration'): - options.configuration = 'Release' - chromium.ChromiumPort.__init__(self, port_name, options) + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'chromium-mac') + chromium.ChromiumPort.__init__(self, **kwargs) def baseline_search_path(self): port_names = ["chromium-mac", "chromium", "mac" + self.version(), "mac"] diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py index 7a005b1..a4a9ea6 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py @@ -83,17 +83,39 @@ class ChromiumDriverTest(unittest.TestCase): self.driver._proc.stdout.readline = mock_readline self._assert_write_command_and_read_line(expected_crash=True) + +class ChromiumPortTest(unittest.TestCase): + class TestMacPort(chromium_mac.ChromiumMacPort): + def __init__(self, options): + chromium_mac.ChromiumMacPort.__init__(self, + port_name='test-port', + options=options) + + def default_configuration(self): + self.default_configuration_called = True + return 'default' + + class TestLinuxPort(chromium_linux.ChromiumLinuxPort): + def __init__(self, options): + chromium_linux.ChromiumLinuxPort.__init__(self, + port_name='test-port', + options=options) + + def default_configuration(self): + self.default_configuration_called = True + return 'default' + def test_path_to_image_diff(self): class MockOptions: def __init__(self): self.use_drt = True - port = chromium_linux.ChromiumLinuxPort('test-port', options=MockOptions()) + port = ChromiumPortTest.TestLinuxPort(options=MockOptions()) self.assertTrue(port._path_to_image_diff().endswith( - '/out/Release/ImageDiff')) - port = chromium_mac.ChromiumMacPort('test-port', options=MockOptions()) + '/out/default/ImageDiff'), msg=port._path_to_image_diff()) + port = ChromiumPortTest.TestMacPort(options=MockOptions()) self.assertTrue(port._path_to_image_diff().endswith( - '/xcodebuild/Release/ImageDiff')) + '/xcodebuild/default/ImageDiff')) # FIXME: Figure out how this is going to work on Windows. #port = chromium_win.ChromiumWinPort('test-port', options=MockOptions()) @@ -102,16 +124,37 @@ class ChromiumDriverTest(unittest.TestCase): def __init__(self): self.use_drt = True - port = chromium_linux.ChromiumLinuxPort('test-port', options=MockOptions()) + port = ChromiumPortTest.TestLinuxPort(options=MockOptions()) fake_test = os.path.join(port.layout_tests_dir(), "fast/js/not-good.js") port.test_expectations = lambda: """BUG_TEST SKIP : fast/js/not-good.js = TEXT DEFER LINUX WIN : fast/js/very-good.js = TIMEOUT PASS""" port.test_expectations_overrides = lambda: '' + port.tests = lambda paths: set() + port.path_exists = lambda test: True skipped_tests = port.skipped_layout_tests(extra_test_files=[fake_test, ]) self.assertTrue("fast/js/not-good.js" in skipped_tests) + def test_default_configuration(self): + class EmptyOptions: + def __init__(self): + pass + + options = EmptyOptions() + port = ChromiumPortTest.TestLinuxPort(options) + self.assertEquals(options.configuration, 'default') + self.assertTrue(port.default_configuration_called) + + class OptionsWithUnsetConfiguration: + def __init__(self): + self.configuration = None + + options = OptionsWithUnsetConfiguration() + port = ChromiumPortTest.TestLinuxPort(options) + self.assertEquals(options.configuration, 'default') + self.assertTrue(port.default_configuration_called) + if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py index e9a81e7..d2b0265 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py @@ -41,12 +41,9 @@ _log = logging.getLogger("webkitpy.layout_tests.port.chromium_win") class ChromiumWinPort(chromium.ChromiumPort): """Chromium Win implementation of the Port class.""" - def __init__(self, port_name=None, options=None): - if port_name is None: - port_name = "chromium-win" + self.version() - if options and not hasattr(options, "configuration"): - options.configuration = "Release" - chromium.ChromiumPort.__init__(self, port_name, options) + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'chromium-win' + self.version()) + chromium.ChromiumPort.__init__(self, **kwargs) def setup_environ_for_server(self): env = chromium.ChromiumPort.setup_environ_for_server(self) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py index 4940e4c..648ccad 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py @@ -46,48 +46,24 @@ from __future__ import with_statement +import os import sys import base import factory -def _read_file(path, mode='r'): - """Return the contents of a file as a string. - - Returns '' if anything goes wrong, instead of throwing an IOError. - - """ - contents = '' - try: - with open(path, mode) as f: - contents = f.read() - except IOError: - pass - return contents - - -def _write_file(path, contents, mode='w'): - """Write the string to the specified path. - - Writes should never fail, so we may raise IOError. - - """ - with open(path, mode) as f: - f.write(contents) - - class DryRunPort(object): """DryRun implementation of the Port interface.""" - def __init__(self, port_name=None, options=None): + def __init__(self, **kwargs): pfx = 'dryrun-' - if port_name.startswith(pfx): - port_name = port_name[len(pfx):] - else: - port_name = None - self._options = options - self.__delegate = factory.get(port_name, options) + if 'port_name' in kwargs: + if kwargs['port_name'].startswith(pfx): + kwargs['port_name'] = kwargs['port_name'][len(pfx):] + else: + kwargs['port_name'] = None + self.__delegate = factory.get(**kwargs) def __getattr__(self, name): return getattr(self.__delegate, name) @@ -134,19 +110,16 @@ class DryrunDriver(base.Driver): return None def run_test(self, uri, timeoutms, image_hash): - test_name = self._uri_to_test(uri) - - text_filename = self._port.expected_filename(test_name, '.txt') - text_output = _read_file(text_filename) + test_name = self._port.uri_to_test_name(uri) + path = os.path.join(self._port.layout_tests_dir(), test_name) + text_output = self._port.expected_text(path) if image_hash is not None: - image_filename = self._port.expected_filename(test_name, '.png') - image = _read_file(image_filename, 'rb') - if self._image_path: - _write_file(self._image_path, image) - hash_filename = self._port.expected_filename(test_name, - '.checksum') - hash = _read_file(hash_filename) + image = self._port.expected_image(path) + if image and self._image_path: + with open(self._image_path, 'w') as f: + f.write(image) + hash = self._port.expected_checksum(path) else: hash = None return (False, False, hash, text_output, None) @@ -156,39 +129,3 @@ class DryrunDriver(base.Driver): def stop(self): pass - - def _uri_to_test(self, uri): - """Return the base layout test name for a given URI. - - This returns the test name for a given URI, e.g., if you passed in - "file:///src/LayoutTests/fast/html/keygen.html" it would return - "fast/html/keygen.html". - - """ - if not self._layout_tests_dir: - self._layout_tests_dir = self._port.layout_tests_dir() - test = uri - - if uri.startswith("file:///"): - if sys.platform == 'win32': - test = test.replace('file:///', '') - test = test.replace('/', '\\') - else: - test = test.replace('file://', '') - return test - elif uri.startswith("http://127.0.0.1:8880/"): - # websocket tests - test = test.replace('http://127.0.0.1:8880/', - self._layout_tests_dir + '/') - return test - elif uri.startswith("http://"): - # regular HTTP test - test = test.replace('http://127.0.0.1:8000/', - self._layout_tests_dir + '/http/tests/') - return test - elif uri.startswith("https://"): - test = test.replace('https://127.0.0.1:8443/', - self._layout_tests_dir + '/http/tests/') - return test - else: - raise NotImplementedError('unknown url type: %s' % uri) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py index 5704f65..6935744 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py @@ -37,11 +37,21 @@ ALL_PORT_NAMES = ['test', 'dryrun', 'mac', 'win', 'gtk', 'qt', 'chromium-mac', 'google-chrome-mac', 'google-chrome-linux32', 'google-chrome-linux64'] -def get(port_name=None, options=None): +def get(port_name=None, options=None, **kwargs): """Returns an object implementing the Port interface. If port_name is None, this routine attempts to guess at the most appropriate port on this platform.""" - port_to_use = port_name + # Wrapped for backwards-compatibility + if port_name: + kwargs['port_name'] = port_name + if options: + kwargs['options'] = options + return _get_kwargs(**kwargs) + + +def _get_kwargs(**kwargs): + port_to_use = kwargs.get('port_name', None) + options = kwargs.get('options', None) if port_to_use is None: if sys.platform == 'win32' or sys.platform == 'cygwin': if options and hasattr(options, 'chromium') and options.chromium: @@ -62,37 +72,40 @@ def get(port_name=None, options=None): if port_to_use == 'test': import test - return test.TestPort(port_name, options) + maker = test.TestPort elif port_to_use.startswith('dryrun'): import dryrun - return dryrun.DryRunPort(port_name, options) + maker = dryrun.DryRunPort elif port_to_use.startswith('mac'): import mac - return mac.MacPort(port_name, options) + maker = mac.MacPort elif port_to_use.startswith('win'): import win - return win.WinPort(port_name, options) + maker = win.WinPort elif port_to_use.startswith('gtk'): import gtk - return gtk.GtkPort(port_name, options) + maker = gtk.GtkPort elif port_to_use.startswith('qt'): import qt - return qt.QtPort(port_name, options) + maker = qt.QtPort + elif port_to_use.startswith('chromium-gpu'): + import chromium_gpu + maker = chromium_gpu.get elif port_to_use.startswith('chromium-mac'): import chromium_mac - return chromium_mac.ChromiumMacPort(port_name, options) + maker = chromium_mac.ChromiumMacPort elif port_to_use.startswith('chromium-linux'): import chromium_linux - return chromium_linux.ChromiumLinuxPort(port_name, options) + maker = chromium_linux.ChromiumLinuxPort elif port_to_use.startswith('chromium-win'): import chromium_win - return chromium_win.ChromiumWinPort(port_name, options) + maker = chromium_win.ChromiumWinPort elif port_to_use.startswith('google-chrome'): import google_chrome - return google_chrome.GetGoogleChromePort(port_name, options) - - raise NotImplementedError('unsupported port: %s' % port_to_use) - + maker = google_chrome.GetGoogleChromePort + else: + raise NotImplementedError('unsupported port: %s' % port_to_use) + return maker(**kwargs) def get_all(options=None): """Returns all the objects implementing the Port interface.""" diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py index c0a4c5e..81c3732 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py @@ -29,6 +29,7 @@ import sys import unittest +import chromium_gpu import chromium_linux import chromium_mac import chromium_win @@ -133,6 +134,15 @@ class FactoryTest(unittest.TestCase): def test_qt(self): self.assert_port("qt", qt.QtPort) + def test_chromium_gpu_linux(self): + self.assert_port("chromium-gpu-linux", chromium_gpu.ChromiumGpuLinuxPort) + + def test_chromium_gpu_mac(self): + self.assert_port("chromium-gpu-mac", chromium_gpu.ChromiumGpuMacPort) + + def test_chromium_gpu_win(self): + self.assert_port("chromium-gpu-win", chromium_gpu.ChromiumGpuWinPort) + def test_chromium_mac(self): self.assert_port("chromium-mac", chromium_mac.ChromiumMacPort) self.assert_platform_port("darwin", self.chromium_options, diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py index 46ab3ed..bffc860 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py @@ -25,10 +25,12 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -def GetGoogleChromePort(port_name, options): +def GetGoogleChromePort(**kwargs): """Some tests have slightly different results when compiled as Google Chrome vs Chromium. In those cases, we prepend an additional directory to to the baseline paths.""" + port_name = kwargs['port_name'] + del kwargs['port_name'] if port_name == 'google-chrome-linux32': import chromium_linux @@ -39,7 +41,7 @@ def GetGoogleChromePort(port_name, options): paths.insert(0, self._webkit_baseline_path( 'google-chrome-linux32')) return paths - return GoogleChromeLinux32Port(None, options) + return GoogleChromeLinux32Port(**kwargs) elif port_name == 'google-chrome-linux64': import chromium_linux @@ -50,7 +52,7 @@ def GetGoogleChromePort(port_name, options): paths.insert(0, self._webkit_baseline_path( 'google-chrome-linux64')) return paths - return GoogleChromeLinux64Port(None, options) + return GoogleChromeLinux64Port(**kwargs) elif port_name.startswith('google-chrome-mac'): import chromium_mac @@ -61,7 +63,7 @@ def GetGoogleChromePort(port_name, options): paths.insert(0, self._webkit_baseline_path( 'google-chrome-mac')) return paths - return GoogleChromeMacPort(None, options) + return GoogleChromeMacPort(**kwargs) elif port_name.startswith('google-chrome-win'): import chromium_win @@ -72,5 +74,5 @@ def GetGoogleChromePort(port_name, options): paths.insert(0, self._webkit_baseline_path( 'google-chrome-win')) return paths - return GoogleChromeWinPort(None, options) + return GoogleChromeWinPort(**kwargs) raise NotImplementedError('unsupported port: %s' % port_name) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py index a2d7056..85e9338 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py @@ -41,6 +41,7 @@ class GetGoogleChromePortTest(unittest.TestCase): self._verify_baseline_path('google-chrome-win', 'google-chrome-win-vista') def _verify_baseline_path(self, expected_path, port_name): - port = google_chrome.GetGoogleChromePort(port_name, None) + port = google_chrome.GetGoogleChromePort(port_name=port_name, + options=None) path = port.baseline_search_path()[0] self.assertEqual(expected_path, os.path.split(path)[1]) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/gtk.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/gtk.py index 59dc1d9..c60909e 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/gtk.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/gtk.py @@ -39,10 +39,9 @@ _log = logging.getLogger("webkitpy.layout_tests.port.gtk") class GtkPort(WebKitPort): """WebKit Gtk implementation of the Port class.""" - def __init__(self, port_name=None, options=None): - if port_name is None: - port_name = 'gtk' - WebKitPort.__init__(self, port_name, options) + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'gtk') + WebKitPort.__init__(self, **kwargs) def _tests_for_other_platforms(self): # FIXME: This list could be dynamic based on platform name and diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/mac.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/mac.py index 413b5f2..696e339 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/mac.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/mac.py @@ -43,10 +43,9 @@ _log = logging.getLogger("webkitpy.layout_tests.port.mac") class MacPort(WebKitPort): """WebKit Mac implementation of the Port class.""" - def __init__(self, port_name=None, options=None): - if port_name is None: - port_name = 'mac' + self.version() - WebKitPort.__init__(self, port_name, options) + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'mac' + self.version()) + WebKitPort.__init__(self, **kwargs) def default_child_processes(self): # FIXME: new-run-webkit-tests is unstable on Mac running more than diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py index 2d650f5..47597d6 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py @@ -68,14 +68,20 @@ class PortTestCase(unittest.TestCase): dir = port.layout_tests_dir() file1 = os.path.join(dir, 'fast', 'css', 'button_center.png') + fh1 = file(file1) + contents1 = fh1.read() file2 = os.path.join(dir, 'fast', 'css', 'remove-shorthand-expected.png') + fh2 = file(file2) + contents2 = fh2.read() tmpfile = tempfile.mktemp() - self.assertFalse(port.diff_image(file1, file1)) - self.assertTrue(port.diff_image(file1, file2)) + self.assertFalse(port.diff_image(contents1, contents1)) + self.assertTrue(port.diff_image(contents1, contents2)) - self.assertTrue(port.diff_image(file1, file2, tmpfile)) + self.assertTrue(port.diff_image(contents1, contents2, tmpfile)) + fh1.close() + fh2.close() # FIXME: this may not be being written? # self.assertTrue(os.path.exists(tmpfile)) # os.remove(tmpfile) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/qt.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/qt.py index 158c633..4c8fa0a 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/qt.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/qt.py @@ -42,10 +42,9 @@ _log = logging.getLogger("webkitpy.layout_tests.port.qt") class QtPort(WebKitPort): """QtWebKit implementation of the Port class.""" - def __init__(self, port_name=None, options=None): - if port_name is None: - port_name = 'qt' - WebKitPort.__init__(self, port_name, options) + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'qt') + WebKitPort.__init__(self, **kwargs) def _tests_for_other_platforms(self): # FIXME: This list could be dynamic based on platform name and diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/server_process.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/server_process.py index 8e0bc11..5a0a40c 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/server_process.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/server_process.py @@ -179,7 +179,7 @@ class ServerProcess: elif size == 0: index = self._output.find('\n') + 1 - if index or self.crashed or self.timed_out: + if index > 0 or self.crashed or self.timed_out: output = self._output[0:index] self._output = self._output[index:] return output diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py index 2ccddb0..3b81167 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py @@ -31,17 +31,100 @@ from __future__ import with_statement import codecs +import fnmatch import os +import sys import time import base +# This sets basic expectations for a test. Each individual expectation +# can be overridden by a keyword argument in TestList.add(). +class TestInstance: + def __init__(self, name): + self.name = name + self.base = name[(name.rfind("/") + 1):name.rfind(".html")] + self.crash = False + self.exception = False + self.hang = False + self.keyboard = False + self.error = '' + self.timeout = False + self.actual_text = self.base + '-txt\n' + self.actual_checksum = self.base + '-checksum\n' + self.actual_image = self.base + '-png\n' + self.expected_text = self.actual_text + self.expected_checksum = self.actual_checksum + self.expected_image = self.actual_image + + +# This is an in-memory list of tests, what we want them to produce, and +# what we want to claim are the expected results. +class TestList: + def __init__(self, port): + self.port = port + self.tests = {} + + def add(self, name, **kwargs): + test = TestInstance(name) + for key, value in kwargs.items(): + test.__dict__[key] = value + self.tests[name] = test + + def keys(self): + return self.tests.keys() + + def __contains__(self, item): + return item in self.tests + + def __getitem__(self, item): + return self.tests[item] + + class TestPort(base.Port): """Test implementation of the Port interface.""" - def __init__(self, port_name=None, options=None): - base.Port.__init__(self, port_name, options) + def __init__(self, **kwargs): + base.Port.__init__(self, **kwargs) + tests = TestList(self) + tests.add('passes/image.html') + tests.add('passes/text.html') + tests.add('failures/expected/checksum.html', + actual_checksum='checksum_fail-checksum') + tests.add('failures/expected/crash.html', crash=True) + tests.add('failures/expected/exception.html', exception=True) + tests.add('failures/expected/timeout.html', timeout=True) + tests.add('failures/expected/hang.html', hang=True) + tests.add('failures/expected/missing_text.html', + expected_text=None) + tests.add('failures/expected/image.html', + actual_image='image_fail-png', + expected_image='image-png') + tests.add('failures/expected/image_checksum.html', + actual_checksum='image_checksum_fail-checksum', + actual_image='image_checksum_fail-png') + tests.add('failures/expected/keyboard.html', + keyboard=True) + tests.add('failures/expected/missing_check.html', + expected_checksum=None) + tests.add('failures/expected/missing_image.html', + expected_image=None) + tests.add('failures/expected/missing_text.html', + expected_text=None) + tests.add('failures/expected/text.html', + actual_text='text_fail-png') + tests.add('failures/unexpected/text-image-checksum.html', + actual_text='text-image-checksum_fail-txt', + actual_checksum='text-image-checksum_fail-checksum') + tests.add('http/tests/passes/text.html') + tests.add('http/tests/ssl/text.html') + tests.add('passes/error.html', error='stuff going to stderr') + tests.add('passes/image.html') + tests.add('passes/platform_image.html') + tests.add('passes/text.html') + tests.add('websocket/tests/passes/text.html') + self._tests = tests def baseline_path(self): return os.path.join(self.layout_tests_dir(), 'platform', @@ -53,12 +136,8 @@ class TestPort(base.Port): def check_build(self, needs_http): return True - def diff_image(self, expected_filename, actual_filename, + def diff_image(self, expected_contents, actual_contents, diff_filename=None, tolerance=0): - with codecs.open(actual_filename, "r", "utf-8") as actual_fh: - actual_contents = actual_fh.read() - with codecs.open(expected_filename, "r", "utf-8") as expected_fh: - expected_contents = expected_fh.read() diffed = actual_contents != expected_contents if diffed and diff_filename: with codecs.open(diff_filename, "w", "utf-8") as diff_fh: @@ -66,24 +145,79 @@ class TestPort(base.Port): (expected_contents, actual_contents)) return diffed + def expected_checksum(self, test): + test = self.relative_test_filename(test) + return self._tests[test].expected_checksum + + def expected_image(self, test): + test = self.relative_test_filename(test) + return self._tests[test].expected_image + + def expected_text(self, test): + test = self.relative_test_filename(test) + text = self._tests[test].expected_text + if not text: + text = '' + return text + + def tests(self, paths): + # Test the idea of port-specific overrides for test lists. Also + # keep in memory to speed up the test harness. + if not paths: + paths = ['*'] + + matched_tests = [] + for p in paths: + if self.path_isdir(p): + matched_tests.extend(fnmatch.filter(self._tests.keys(), p + '*')) + else: + matched_tests.extend(fnmatch.filter(self._tests.keys(), p)) + layout_tests_dir = self.layout_tests_dir() + return set([os.path.join(layout_tests_dir, p) for p in matched_tests]) + + def path_exists(self, path): + # used by test_expectations.py and printing.py + rpath = self.relative_test_filename(path) + if rpath in self._tests: + return True + if self.path_isdir(rpath): + return True + if rpath.endswith('-expected.txt'): + test = rpath.replace('-expected.txt', '.html') + return (test in self._tests and + self._tests[test].expected_text) + if rpath.endswith('-expected.checksum'): + test = rpath.replace('-expected.checksum', '.html') + return (test in self._tests and + self._tests[test].expected_checksum) + if rpath.endswith('-expected.png'): + test = rpath.replace('-expected.png', '.html') + return (test in self._tests and + self._tests[test].expected_image) + return False + def layout_tests_dir(self): return self.path_from_webkit_base('WebKitTools', 'Scripts', 'webkitpy', 'layout_tests', 'data') + def path_isdir(self, path): + # Used by test_expectations.py + # + # We assume that a path is a directory if we have any tests that + # whose prefix matches the path plus a directory modifier. + if path[-1] != '/': + path += '/' + return any([t.startswith(path) for t in self._tests.keys()]) + + def test_dirs(self): + return ['passes', 'failures'] + def name(self): return self._name def options(self): return self._options - def skipped_layout_tests(self): - return [] - - def path_to_test_expectations_file(self): - return self.path_from_webkit_base('WebKitTools', 'Scripts', - 'webkitpy', 'layout_tests', 'data', 'platform', 'test', - 'test_expectations.txt') - def _path_to_wdiff(self): return None @@ -93,9 +227,6 @@ class TestPort(base.Port): def setup_test_run(self): pass - def show_results_html_file(self, filename): - pass - def create_driver(self, image_path, options): return TestDriver(self, image_path, options, executive=None) @@ -116,9 +247,21 @@ class TestPort(base.Port): Basically this string should contain the equivalent of a test_expectations file. See test_expectations.py for more details.""" - expectations_path = self.path_to_test_expectations_file() - with codecs.open(expectations_path, "r", "utf-8") as file: - return file.read() + return """ +WONTFIX : failures/expected/checksum.html = IMAGE +WONTFIX : failures/expected/crash.html = CRASH +// This one actually passes because the checksums will match. +WONTFIX : failures/expected/image.html = PASS +WONTFIX : failures/expected/image_checksum.html = IMAGE +WONTFIX : failures/expected/missing_check.html = MISSING PASS +WONTFIX : failures/expected/missing_image.html = MISSING PASS +WONTFIX : failures/expected/missing_text.html = MISSING PASS +WONTFIX : failures/expected/text.html = TEXT +WONTFIX : failures/expected/timeout.html = TIMEOUT +WONTFIX SKIP : failures/expected/hang.html = TIMEOUT +WONTFIX SKIP : failures/expected/keyboard.html = CRASH +WONTFIX SKIP : failures/expected/exception.html = CRASH +""" def test_base_platform_names(self): return ('mac', 'win') @@ -150,68 +293,21 @@ class TestDriver(base.Driver): return True def run_test(self, uri, timeoutms, image_hash): - basename = uri[(uri.rfind("/") + 1):uri.rfind(".html")] - - if 'error' in basename: - error = basename + "_error\n" - else: - error = '' - checksum = None - # There are four currently supported types of tests: text, image, - # image hash (checksum), and stderr output. The fake output - # is the basename of the file + "-" plus the type of test output - # (or a blank string for stderr). - # - # If 'image' or 'check' appears in the basename, we assume this is - # simulating a pixel test. - # - # If 'failures' appears in the URI, then we assume this test should - # fail. Which type of failures are determined by which strings appear - # in the basename of the test. For failures that produce outputs, - # we change the fake output to basename + "_failed-". - # - # The fact that each test produces (more or less) unique output data - # will allow us to see if any results get crossed by the rest of the - # program. - if 'failures' in uri: - if 'keyboard' in basename: - raise KeyboardInterrupt - if 'exception' in basename: - raise ValueError('exception from ' + basename) - - crash = 'crash' in basename - timeout = 'timeout' in basename or 'hang' in basename - timeout = 'timeout' in basename - if 'text' in basename: - output = basename + '_failed-txt\n' - else: - output = basename + '-txt\n' - if self._port.options().pixel_tests: - if ('image' in basename or 'check' in basename): - checksum = basename + "-checksum\n" - - if 'image' in basename: - with open(self._image_path, "w") as f: - f.write(basename + "_failed-png\n") - elif 'check' in basename: - with open(self._image_path, "w") as f: - f.write(basename + "-png\n") - if 'checksum' in basename: - checksum = basename + "_failed-checksum\n" - - if 'hang' in basename: - time.sleep((float(timeoutms) * 4) / 1000.0) - else: - crash = False - timeout = False - output = basename + '-txt\n' - if self._options.pixel_tests and ( - 'image' in basename or 'check' in basename): - checksum = basename + '-checksum\n' - with open(self._image_path, "w") as f: - f.write(basename + "-png") - - return (crash, timeout, checksum, output, error) + test_name = self._port.uri_to_test_name(uri) + test = self._port._tests[test_name] + if test.keyboard: + raise KeyboardInterrupt + if test.exception: + raise ValueError('exception from ' + test_name) + if test.hang: + time.sleep((float(timeoutms) * 4) / 1000.0) + + if self._port.options().pixel_tests and test.actual_image: + with open(self._image_path, 'w') as file: + file.write(test.actual_image) + + return (test.crash, test.timeout, test.actual_checksum, + test.actual_text, test.error) def start(self): pass diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_files.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/test_files.py index 8f79505..3fa0fb3 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_files.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/test_files.py @@ -27,11 +27,11 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""This module is used to find all of the layout test files used by Chromium -(across all platforms). It exposes one public function - GatherTestFiles() - +"""This module is used to find all of the layout test files used by +run-webkit-tests. It exposes one public function - find() - which takes an optional list of paths. If a list is passed in, the returned list of test files is constrained to those found under the paths passed in, -i.e. calling GatherTestFiles(["LayoutTests/fast"]) will only return files +i.e. calling find(["LayoutTests/fast"]) will only return files under that directory.""" import glob @@ -51,12 +51,12 @@ _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.xhtmlmp _skipped_directories = set(['.svn', '_svn', 'resources', 'script-tests']) -def gather_test_files(port, paths): - """Generate a set of test files and return them. +def find(port, paths): + """Finds the set of tests under port.layout_tests_dir(). Args: - paths: a list of command line paths relative to the webkit/tests - directory. glob patterns are ok. + paths: a list of command line paths relative to the layout_tests_dir() + to limit the search to. glob patterns are ok. """ gather_start_time = time.time() paths_to_walk = set() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/test_files_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/test_files_unittest.py new file mode 100644 index 0000000..c37eb92 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/test_files_unittest.py @@ -0,0 +1,68 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import unittest + +import base +import test_files + + +class TestFilesTest(unittest.TestCase): + def test_find_no_paths_specified(self): + port = base.Port() + layout_tests_dir = port.layout_tests_dir() + port.layout_tests_dir = lambda: os.path.join(layout_tests_dir, + 'fast', 'html') + tests = test_files.find(port, []) + self.assertNotEqual(tests, 0) + + def test_find_one_test(self): + port = base.Port() + # This is just a test picked at random but known to exist. + tests = test_files.find(port, ['fast/html/keygen.html']) + self.assertEqual(len(tests), 1) + + def test_find_glob(self): + port = base.Port() + tests = test_files.find(port, ['fast/html/key*']) + self.assertEqual(len(tests), 1) + + def test_find_with_skipped_directories(self): + port = base.Port() + tests = port.tests('userscripts') + self.assertTrue('userscripts/resources/frame1.html' not in tests) + + def test_find_with_skipped_directories_2(self): + port = base.Port() + tests = test_files.find(port, ['userscripts/resources']) + self.assertEqual(tests, set([])) + + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py index 88c9bdf..ed19c09 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py @@ -58,16 +58,16 @@ _log = logging.getLogger("webkitpy.layout_tests.port.webkit") class WebKitPort(base.Port): """WebKit implementation of the Port class.""" - def __init__(self, port_name=None, options=None, **kwargs): - base.Port.__init__(self, port_name, options, **kwargs) + def __init__(self, **kwargs): + base.Port.__init__(self, **kwargs) self._cached_build_root = None self._cached_apache_path = None # FIXME: disable pixel tests until they are run by default on the # build machines. - if options and (not hasattr(options, "pixel_tests") or - options.pixel_tests is None): - options.pixel_tests = False + if self._options and (not hasattr(self._options, "pixel_tests") or + self._options.pixel_tests is None): + self._options.pixel_tests = False def baseline_path(self): return self._webkit_baseline_path(self._name) @@ -84,10 +84,14 @@ class WebKitPort(base.Port): return '' def _build_driver(self): - return not self._executive.run_command([ + exit_code = self._executive.run_command([ self.script_path("build-dumprendertree"), self.flag_from_configuration(self._options.configuration), ], return_exit_code=True) + if exit_code != 0: + _log.error("Failed to build DumpRenderTree") + return False + return True def _check_driver(self): driver_path = self._path_to_driver() @@ -119,7 +123,7 @@ class WebKitPort(base.Port): return False return True - def diff_image(self, expected_filename, actual_filename, + def diff_image(self, expected_contents, actual_contents, diff_filename=None, tolerance=0.1): """Return True if the two files are different. Also write a delta image of the two images into |diff_filename| if it is not None.""" @@ -128,31 +132,24 @@ class WebKitPort(base.Port): # parameter, or make it go away and always use exact matches. # Handle the case where the test didn't actually generate an image. - actual_length = os.stat(actual_filename).st_size - if actual_length == 0: - if diff_filename: - shutil.copyfile(actual_filename, expected_filename) + if not actual_contents: return True - sp = self._diff_image_request(expected_filename, actual_filename, tolerance) - return self._diff_image_reply(sp, expected_filename, diff_filename) + sp = self._diff_image_request(expected_contents, actual_contents, + tolerance) + return self._diff_image_reply(sp, diff_filename) - def _diff_image_request(self, expected_filename, actual_filename, tolerance): + def _diff_image_request(self, expected_contents, actual_contents, tolerance): command = [self._path_to_image_diff(), '--tolerance', str(tolerance)] sp = server_process.ServerProcess(self, 'ImageDiff', command) - actual_length = os.stat(actual_filename).st_size - with open(actual_filename) as file: - actual_file = file.read() - expected_length = os.stat(expected_filename).st_size - with open(expected_filename) as file: - expected_file = file.read() sp.write('Content-Length: %d\n%sContent-Length: %d\n%s' % - (actual_length, actual_file, expected_length, expected_file)) + (len(actual_contents), actual_contents, + len(expected_contents), expected_contents)) return sp - def _diff_image_reply(self, sp, expected_filename, diff_filename): + def _diff_image_reply(self, sp, diff_filename): timeout = 2.0 deadline = time.time() + timeout output = sp.read_line(timeout) @@ -178,7 +175,7 @@ class WebKitPort(base.Port): with open(diff_filename, 'w') as file: file.write(output) elif sp.timed_out: - _log.error("ImageDiff timed out on %s" % expected_filename) + _log.error("ImageDiff timed out") elif sp.crashed: _log.error("ImageDiff crashed") sp.stop() @@ -193,11 +190,6 @@ class WebKitPort(base.Port): # This port doesn't require any specific configuration. pass - def show_results_html_file(self, results_filename): - uri = self.filename_to_uri(results_filename) - # FIXME: We should open results in the version of WebKit we built. - webbrowser.open(uri, new=1) - def create_driver(self, image_path, options): return WebKitDriver(self, image_path, options, executive=self._executive) @@ -255,7 +247,7 @@ class WebKitPort(base.Port): "MathMLElement": ["mathml"], "GraphicsLayer": ["compositing"], "WebCoreHas3DRendering": ["animations/3d", "transforms/3d"], - "WebGLShader": ["fast/canvas/webgl"], + "WebGLShader": ["fast/canvas/webgl", "compositing/webgl", "http/tests/canvas/webgl"], "WMLElement": ["http/tests/wml", "fast/wml", "wml"], "parseWCSSInputProperty": ["fast/wcss"], "isXHTMLMPDocument": ["fast/xhtmlmp"], @@ -418,12 +410,17 @@ class WebKitDriver(base.Driver): def _driver_args(self): driver_args = [] + if self._image_path: driver_args.append('--pixel-tests') - # These are used by the Chromium DRT port if self._options.use_drt: - driver_args.append('--test-shell') + if self._options.accelerated_compositing: + driver_args.append('--enable-accelerated-compositing') + + if self._options.accelerated_2d_canvas: + driver_args.append('--enable-accelerated-2d-canvas') + return driver_args def start(self): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py index fbfadc3..7b68310 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py @@ -53,7 +53,7 @@ class WebKitPortTest(unittest.TestCase): def test_skipped_directories_for_symbols(self): supported_symbols = ["GraphicsLayer", "WebCoreHas3DRendering", "isXHTMLMPDocument", "fooSymbol"] - expected_directories = set(["mathml", "fast/canvas/webgl", "http/tests/wml", "fast/wml", "wml", "fast/wcss"]) + expected_directories = set(["mathml", "fast/canvas/webgl", "compositing/webgl", "http/tests/canvas/webgl", "http/tests/wml", "fast/wml", "wml", "fast/wcss"]) result_directories = set(TestWebKitPort(supported_symbols, None)._skipped_tests_for_unsupported_features()) self.assertEqual(result_directories, expected_directories) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/win.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/win.py index e05a69d..9e30155 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/win.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/win.py @@ -39,10 +39,9 @@ _log = logging.getLogger("webkitpy.layout_tests.port.win") class WinPort(WebKitPort): """WebKit Win implementation of the Port class.""" - def __init__(self, port_name=None, options=None): - if port_name is None: - port_name = 'win' - WebKitPort.__init__(self, port_name, options) + def __init__(self, **kwargs): + kwargs.setdefault('port_name', 'win') + WebKitPort.__init__(self, **kwargs) def baseline_search_path(self): # Based on code from old-run-webkit-tests expectedDirectoryForTest() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py index 3a9f923..e57ceb2 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py @@ -55,9 +55,9 @@ import sys import tempfile import time import urllib -import webbrowser import zipfile +from webkitpy.common.system import user from webkitpy.common.system.executive import run_command, ScriptError import webkitpy.common.checkout.scm as scm @@ -518,8 +518,15 @@ class Rebaseliner(object): fallback_fullpath = os.path.normpath( os.path.join(fallback_dir, fallback_file)) if fallback_fullpath.lower() != baseline_path.lower(): - if not self._diff_baselines(new_baseline, - fallback_fullpath): + with codecs.open(new_baseline, "r", + None) as file_handle1: + new_output = file_handle1.read() + with codecs.open(fallback_fullpath, "r", + None) as file_handle2: + fallback_output = file_handle2.read() + is_image = baseline_path.lower().endswith('.png') + if not self._diff_baselines(new_output, fallback_output, + is_image): _log.info(' Found same baseline at %s', fallback_fullpath) return True @@ -528,31 +535,20 @@ class Rebaseliner(object): return False - def _diff_baselines(self, file1, file2): + def _diff_baselines(self, output1, output2, is_image): """Check whether two baselines are different. Args: - file1, file2: full paths of the baselines to compare. + output1, output2: contents of the baselines to compare. Returns: True if two files are different or have different extensions. False otherwise. """ - ext1 = os.path.splitext(file1)[1].upper() - ext2 = os.path.splitext(file2)[1].upper() - if ext1 != ext2: - _log.warn('Files to compare have different ext. ' - 'File1: %s; File2: %s', file1, file2) - return True - - if ext1 == '.PNG': - return self._port.diff_image(file1, file2) + if is_image: + return self._port.diff_image(output1, output2) else: - with codecs.open(file1, "r", "utf8") as file_handle1: - output1 = file_handle1.read() - with codecs.open(file2, "r", "utf8") as file_handle2: - output2 = file_handle2.read() return self._port.compare_text(output1, output2) def _delete_baseline(self, filename): @@ -593,7 +589,7 @@ class Rebaseliner(object): # Or is new_expectations always a byte array? with open(path, "w") as file: file.write(new_expectations) - self._scm.add(path) + # self._scm.add(path) else: _log.info('No test was rebaselined so nothing to remove.') @@ -737,10 +733,7 @@ class HtmlGenerator(object): """Launch the rebaselining html in brwoser.""" _log.info('Launching html: "%s"', self._html_file) - - html_uri = self._target_port.filename_to_uri(self._html_file) - webbrowser.open(html_uri, 1) - + user.User().open_url(self._html_file) _log.info('Html launched.') def _generate_baseline_links(self, test_basename, suffix, platform): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py index dbb2b91..9ba3d6b 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py @@ -103,15 +103,20 @@ class TestRebaseliner(unittest.TestCase): def test_diff_baselines_txt(self): rebaseliner = self.make_rebaseliner() - path = os.path.join(rebaseliner._port.layout_tests_dir(), - "passes", "text-expected.txt") - self.assertFalse(rebaseliner._diff_baselines(path, path)) + output = rebaseliner._port.expected_text( + os.path.join(rebaseliner._port.layout_tests_dir(), + 'passes/text.html')) + self.assertFalse(rebaseliner._diff_baselines(output, output, + is_image=False)) def test_diff_baselines_png(self): + return rebaseliner = self.make_rebaseliner() - path = os.path.join(rebaseliner._port.layout_tests_dir(), - "passes", "image-expected.png") - self.assertFalse(rebaseliner._diff_baselines(path, path)) + image = rebaseliner._port.expected_image( + os.path.join(rebaseliner._port.layout_tests_dir(), + 'passes/image.html')) + self.assertFalse(rebaseliner._diff_baselines(image, image, + is_image=True)) if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py index 14d4f0e..e9c6d2c 100755 --- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py @@ -68,12 +68,12 @@ from layout_package import json_layout_results_generator from layout_package import printing from layout_package import test_expectations from layout_package import test_failures -from layout_package import test_files from layout_package import test_results_uploader from test_types import image_diff from test_types import text_diff from test_types import test_type_base +from webkitpy.common.system import user from webkitpy.thirdparty import simplejson import port @@ -96,28 +96,21 @@ class TestInfo: timeout: Timeout for running the test in TestShell. """ self.filename = filename + self._port = port self.uri = port.filename_to_uri(filename) self.timeout = timeout - # FIXME: Confusing that the file is .checksum and we call it "hash" - self._expected_hash_path = port.expected_filename(filename, '.checksum') - self._have_read_expected_hash = False - self._image_hash = None - - def _read_image_hash(self): - if not os.path.exists(self._expected_hash_path): - return None - - with codecs.open(self._expected_hash_path, "r", "ascii") as hash_file: - return hash_file.read() + self._image_checksum = -1 def image_hash(self): # Read the image_hash lazily to reduce startup time. # This class is accessed across threads, but only one thread should # ever be dealing with any given TestInfo so no locking is needed. - if not self._have_read_expected_hash: - self._have_read_expected_hash = True - self._image_hash = self._read_image_hash() - return self._image_hash + # + # Note that we use -1 to indicate that we haven't read the value, + # because expected_checksum() returns a string or None. + if self._image_checksum == -1: + self._image_checksum = self._port.expected_checksum(self.filename) + return self._image_checksum class ResultSummary(object): @@ -292,7 +285,7 @@ class TestRunner: paths += last_unexpected_results if self._options.test_list: paths += read_test_files(self._options.test_list) - self._test_files = test_files.gather_test_files(self._port, paths) + self._test_files = self._port.tests(paths) def lint(self): # Creating the expecations for each platform/configuration pair does @@ -321,7 +314,7 @@ class TestRunner: self._expectations = test_expectations.TestExpectations( self._port, test_files, expectations_str, test_platform_name, is_debug_mode, self._options.lint_test_files, - tests_are_present=True, overrides=overrides_str) + overrides=overrides_str) return self._expectations except SyntaxError, err: if self._options.lint_test_files: @@ -865,7 +858,7 @@ class TestRunner: self._printer.print_update("Clobbering old results in %s" % self._options.results_directory) layout_tests_dir = self._port.layout_tests_dir() - possible_dirs = os.listdir(layout_tests_dir) + possible_dirs = self._port.test_dirs() for dirname in possible_dirs: if os.path.isdir(os.path.join(layout_tests_dir, dirname)): shutil.rmtree(os.path.join(self._options.results_directory, @@ -1408,6 +1401,7 @@ def run(port, options, args, regular_output=sys.stderr, printer.print_update("Checking build ...") if not port.check_build(test_runner.needs_http()): + _log.error("Build check failed") return -1 result_summary = test_runner.set_up_run() @@ -1515,6 +1509,20 @@ def parse_args(args=None): optparse.make_option("--use-drt", action="store_true", default=False, help="Use DumpRenderTree instead of test_shell"), + optparse.make_option("--accelerated-compositing", + action="store_true", + help="Use hardware-accelated compositing for rendering"), + optparse.make_option("--no-accelerated-compositing", + action="store_false", + dest="accelerated_compositing", + help="Don't use hardware-accelerated compositing for rendering"), + optparse.make_option("--accelerated-2d-canvas", + action="store_true", + help="Use hardware-accelerated 2D Canvas calls"), + optparse.make_option("--no-accelerated-2d-canvas", + action="store_false", + dest="accelerated_2d_canvas", + help="Don't use hardware-accelerated 2D Canvas calls"), ] # Missing Mac-specific old-run-webkit-tests options: diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py index aa96962..6fe99d6 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py @@ -42,6 +42,7 @@ import unittest from webkitpy.common import array_stream from webkitpy.common.system import outputcapture +from webkitpy.common.system import user from webkitpy.layout_tests import port from webkitpy.layout_tests import run_webkit_tests from webkitpy.layout_tests.layout_package import dump_render_tree_thread @@ -49,6 +50,14 @@ from webkitpy.layout_tests.layout_package import dump_render_tree_thread from webkitpy.thirdparty.mock import Mock +class MockUser(): + def __init__(self): + self.url = None + + def open_url(self, url): + self.url = url + + def passing_run(args=[], port_obj=None, record_results=False, tests_included=False): new_args = ['--print', 'nothing'] @@ -65,7 +74,8 @@ def passing_run(args=[], port_obj=None, record_results=False, 'failures/expected/*']) options, parsed_args = run_webkit_tests.parse_args(new_args) if port_obj is None: - port_obj = port.get(options.platform, options) + port_obj = port.get(port_name=options.platform, options=options, + user=MockUser()) res = run_webkit_tests.run(port_obj, options, parsed_args) return res == 0 @@ -77,20 +87,31 @@ def logging_run(args=[], tests_included=False): new_args.extend(args) if not tests_included: new_args.extend(['passes', - 'http/tests' + 'http/tests', 'websocket/tests', 'failures/expected/*']) options, parsed_args = run_webkit_tests.parse_args(new_args) - port_obj = port.get(options.platform, options) + user = MockUser() + port_obj = port.get(port_name=options.platform, options=options, user=user) buildbot_output = array_stream.ArrayStream() regular_output = array_stream.ArrayStream() res = run_webkit_tests.run(port_obj, options, parsed_args, buildbot_output=buildbot_output, regular_output=regular_output) - return (res, buildbot_output, regular_output) + return (res, buildbot_output, regular_output, user) class MainTest(unittest.TestCase): + def test_accelerated_compositing(self): + # This just tests that we recognize the command line args + self.assertTrue(passing_run(['--accelerated-compositing'])) + self.assertTrue(passing_run(['--no-accelerated-compositing'])) + + def test_accelerated_2d_canvas(self): + # This just tests that we recognize the command line args + self.assertTrue(passing_run(['--accelerated-2d-canvas'])) + self.assertTrue(passing_run(['--no-accelerated-2d-canvas'])) + def test_basic(self): self.assertTrue(passing_run()) @@ -99,13 +120,13 @@ class MainTest(unittest.TestCase): self.assertTrue(passing_run(['--batch-size', '2'])) def test_child_process_1(self): - (res, buildbot_output, regular_output) = logging_run( + (res, buildbot_output, regular_output, user) = logging_run( ['--print', 'config', '--child-processes', '1']) self.assertTrue('Running one DumpRenderTree\n' in regular_output.get()) def test_child_processes_2(self): - (res, buildbot_output, regular_output) = logging_run( + (res, buildbot_output, regular_output, user) = logging_run( ['--print', 'config', '--child-processes', '2']) self.assertTrue('Running 2 DumpRenderTrees in parallel\n' in regular_output.get()) @@ -119,15 +140,15 @@ class MainTest(unittest.TestCase): self.assertTrue(passing_run(['--full-results-html'])) def test_help_printing(self): - res, out, err = logging_run(['--help-printing']) + res, out, err, user = logging_run(['--help-printing']) self.assertEqual(res, 0) self.assertTrue(out.empty()) self.assertFalse(err.empty()) def test_hung_thread(self): - res, out, err = logging_run(['--run-singly', '--time-out-ms=50', - 'failures/expected/hang.html'], - tests_included=True) + res, out, err, user = logging_run(['--run-singly', '--time-out-ms=50', + 'failures/expected/hang.html'], + tests_included=True) self.assertEqual(res, 0) self.assertFalse(out.empty()) self.assertFalse(err.empty()) @@ -140,26 +161,27 @@ class MainTest(unittest.TestCase): def test_last_results(self): passing_run(['--clobber-old-results'], record_results=True) - (res, buildbot_output, regular_output) = logging_run( + (res, buildbot_output, regular_output, user) = logging_run( ['--print-last-failures']) self.assertEqual(regular_output.get(), ['\n\n']) self.assertEqual(buildbot_output.get(), []) def test_lint_test_files(self): # FIXME: add errors? - res, out, err = logging_run(['--lint-test-files'], tests_included=True) + res, out, err, user = logging_run(['--lint-test-files'], + tests_included=True) self.assertEqual(res, 0) self.assertTrue(out.empty()) self.assertTrue(any(['lint succeeded' in msg for msg in err.get()])) def test_no_tests_found(self): - res, out, err = logging_run(['resources'], tests_included=True) + res, out, err, user = logging_run(['resources'], tests_included=True) self.assertEqual(res, -1) self.assertTrue(out.empty()) self.assertTrue('No tests to run.\n' in err.get()) def test_no_tests_found_2(self): - res, out, err = logging_run(['foo'], tests_included=True) + res, out, err, user = logging_run(['foo'], tests_included=True) self.assertEqual(res, -1) self.assertTrue(out.empty()) self.assertTrue('No tests to run.\n' in err.get()) @@ -196,17 +218,19 @@ class MainTest(unittest.TestCase): self.assertTrue(passing_run(['--test-list=%s' % filename], tests_included=True)) os.remove(filename) - res, out, err = logging_run(['--test-list=%s' % filename], - tests_included=True) + res, out, err, user = logging_run(['--test-list=%s' % filename], + tests_included=True) self.assertEqual(res, -1) self.assertFalse(err.empty()) def test_unexpected_failures(self): # Run tests including the unexpected failures. - res, out, err = logging_run(tests_included=True) + self._url_opened = None + res, out, err, user = logging_run(tests_included=True) self.assertEqual(res, 1) self.assertFalse(out.empty()) self.assertFalse(err.empty()) + self.assertEqual(user.url, '/tmp/layout-test-results/results.html') def _mocked_open(original_open, file_list): @@ -269,6 +293,7 @@ class RebaselineTest(unittest.TestCase): finally: codecs.open = original_open + class TestRunnerTest(unittest.TestCase): def test_results_html(self): mock_port = Mock() @@ -306,11 +331,8 @@ class DryrunTest(unittest.TestCase): 'fast/html'])) def test_test(self): - res, out, err = logging_run(['--platform', 'dryrun-test', - '--pixel-tests']) - self.assertEqual(res, 2) - self.assertFalse(out.empty()) - self.assertFalse(err.empty()) + self.assertTrue(passing_run(['--platform', 'dryrun-test', + '--pixel-tests'])) class TestThread(dump_render_tree_thread.WatchableThread): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py index 879646c..1ad0fe6 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py @@ -103,7 +103,11 @@ class ImageDiff(test_type_base.TestTypeBase): expected_filename = self.output_filename(filename, self.FILENAME_SUFFIX_EXPECTED + '.png') - result = port.diff_image(expected_filename, actual_filename, + expected_image = port.expected_image(filename) + with codecs.open(actual_filename, 'r', None) as file: + actual_image = file.read() + + result = port.diff_image(expected_image, actual_image, diff_filename) return result @@ -124,19 +128,12 @@ class ImageDiff(test_type_base.TestTypeBase): return failures # Compare hashes. - expected_hash_file = self._port.expected_filename(filename, - '.checksum') - expected_png_file = self._port.expected_filename(filename, '.png') - - # FIXME: We repeat this pattern often, we should share code. - expected_hash = '' - if os.path.exists(expected_hash_file): - with codecs.open(expected_hash_file, "r", "ascii") as file: - expected_hash = file.read() + expected_hash = self._port.expected_checksum(filename) + expected_png = self._port.expected_image(filename) - if not os.path.isfile(expected_png_file): + if not expected_png: # Report a missing expected PNG file. - self.write_output_files(port, filename, '.checksum', + self.write_output_files(filename, '.checksum', test_args.hash, expected_hash, encoding="ascii", print_text_diffs=False) @@ -147,17 +144,21 @@ class ImageDiff(test_type_base.TestTypeBase): # Hash matched (no diff needed, okay to return). return failures - self.write_output_files(port, filename, '.checksum', + self.write_output_files(filename, '.checksum', test_args.hash, expected_hash, encoding="ascii", print_text_diffs=False) + + # FIXME: combine next two lines self._copy_output_png(filename, test_args.png_path, '-actual.png') - self._copy_output_png(filename, expected_png_file, '-expected.png') + self.write_output_files(filename, '.png', output=None, + expected=expected_png, + encoding=None, print_text_diffs=False) # Even though we only use the result in one codepath below but we # still need to call CreateImageDiff for other codepaths. images_are_different = self._create_image_diff(port, filename, configuration) - if expected_hash == '': + if not expected_hash: failures.append(test_failures.FailureMissingImageHash()) elif test_args.hash != expected_hash: if images_are_different: diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py index 753dbee..3a6e92b 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py @@ -120,7 +120,7 @@ class TestTypeBase(object): output_path = self._port.expected_filename(filename, modifier) _log.debug('resetting baseline result "%s"' % output_path) - self._write_into_file_at_path(output_path, data, encoding) + self._port.update_baseline(output_path, data, encoding) def output_filename(self, filename, modifier): """Returns a filename inside the output dir that contains modifier. @@ -164,7 +164,7 @@ class TestTypeBase(object): with codecs.open(file_path, "w", encoding=encoding) as file: file.write(contents) - def write_output_files(self, port, filename, file_type, + def write_output_files(self, filename, file_type, output, expected, encoding, print_text_diffs=False): """Writes the test output, the expected output and optionally the diff @@ -201,16 +201,16 @@ class TestTypeBase(object): # Note: We pass encoding=None for all diff writes, as we treat diff # output as binary. Diff output may contain multiple files in # conflicting encodings. - diff = port.diff_text(expected, output, expected_filename, actual_filename) + diff = self._port.diff_text(expected, output, expected_filename, actual_filename) diff_filename = self.output_filename(filename, self.FILENAME_SUFFIX_DIFF + file_type) self._write_into_file_at_path(diff_filename, diff, encoding=None) # Shell out to wdiff to get colored inline diffs. - wdiff = port.wdiff_text(expected_filename, actual_filename) + wdiff = self._port.wdiff_text(expected_filename, actual_filename) wdiff_filename = self.output_filename(filename, self.FILENAME_SUFFIX_WDIFF) self._write_into_file_at_path(wdiff_filename, wdiff, encoding=None) # Use WebKit's PrettyPatch.rb to get an HTML diff. - pretty_patch = port.pretty_patch_text(diff_filename) + pretty_patch = self._port.pretty_patch_text(diff_filename) pretty_patch_filename = self.output_filename(filename, self.FILENAME_SUFFIX_PRETTY_PATCH) self._write_into_file_at_path(pretty_patch_filename, pretty_patch, encoding=None) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py index 50a9995..b1f621e 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py @@ -59,24 +59,7 @@ class TestTextDiff(test_type_base.TestTypeBase): """Given the filename of the test, read the expected output from a file and normalize the text. Returns a string with the expected text, or '' if the expected output file was not found.""" - # Read the port-specific expected text. - expected_filename = self._port.expected_filename(filename, '.txt') - return self._get_normalized_text(expected_filename) - - def _get_normalized_text(self, filename): - # FIXME: We repeat this pattern often, we should share code. - if not os.path.exists(filename): - return '' - - # NOTE: -expected.txt files are ALWAYS utf-8. However, - # we do not decode the output from DRT, so we should not - # decode the -expected.txt values either to allow comparisons. - with codecs.open(filename, "r", encoding=None) as file: - text = file.read() - # We could assert that the text is valid utf-8. - - # Normalize line endings - return text.strip("\r\n").replace("\r\n", "\n") + "\n" + return self._port.expected_text(filename) def compare_output(self, port, filename, output, test_args, configuration): """Implementation of CompareOutput that checks the output text against @@ -99,7 +82,7 @@ class TestTextDiff(test_type_base.TestTypeBase): # Write output files for new tests, too. if port.compare_text(output, expected): # Text doesn't match, write output files. - self.write_output_files(port, filename, ".txt", output, + self.write_output_files(filename, ".txt", output, expected, encoding=None, print_text_diffs=True) diff --git a/WebKitTools/Scripts/webkitpy/style/checkers/test_expectations.py b/WebKitTools/Scripts/webkitpy/style/checkers/test_expectations.py index ddc3983..d2d67f3 100644 --- a/WebKitTools/Scripts/webkitpy/style/checkers/test_expectations.py +++ b/WebKitTools/Scripts/webkitpy/style/checkers/test_expectations.py @@ -93,8 +93,7 @@ class TestExpectationsChecker(object): expectations = test_expectations.TestExpectationsFile( port=self._port_obj, expectations=expectations_str, full_test_list=tests, test_platform_name=self._port_to_check, is_debug_mode=False, - is_lint_mode=True, suppress_errors=False, tests_are_present=True, - overrides=overrides) + is_lint_mode=True, suppress_errors=False, overrides=overrides) except SyntaxError, error: errors = str(error).splitlines() diff --git a/WebKitTools/Scripts/webkitpy/test/main.py b/WebKitTools/Scripts/webkitpy/test/main.py index daf255f..9351768 100644 --- a/WebKitTools/Scripts/webkitpy/test/main.py +++ b/WebKitTools/Scripts/webkitpy/test/main.py @@ -78,7 +78,7 @@ class Tester(object): return modules - def run_tests(self, sys_argv): + def run_tests(self, sys_argv, external_package_paths=None): """Run the unit tests in all *_unittest.py modules in webkitpy. This method excludes "webkitpy.common.checkout.scm_unittest" unless @@ -88,6 +88,11 @@ class Tester(object): sys_argv: A reference to sys.argv. """ + if external_package_paths is None: + external_package_paths = [] + else: + sys.path.extend(set(os.path.dirname(path) for path in external_package_paths)) + if len(sys_argv) > 1 and not sys_argv[-1].startswith("-"): # Then explicit modules or test names were provided, which # the unittest module is equipped to handle. @@ -97,9 +102,10 @@ class Tester(object): # Otherwise, auto-detect all unit tests. webkitpy_dir = os.path.dirname(webkitpy.__file__) - unittest_paths = self._find_unittest_files(webkitpy_dir) - modules = self._modules_from_paths(webkitpy_dir, unittest_paths) + modules = [] + for path in [webkitpy_dir] + external_package_paths: + modules.extend(self._modules_from_paths(path, self._find_unittest_files(path))) modules.sort() # This is a sanity check to ensure that the unit-test discovery diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py new file mode 100644 index 0000000..a347972 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py @@ -0,0 +1,158 @@ +# Copyright (c) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from webkitpy.common.system.executive import ScriptError + + +class CommitQueueTask(object): + def __init__(self, tool, commit_queue, patch): + self._tool = tool + self._commit_queue = commit_queue + self._patch = patch + self._script_error = None + + def _validate(self): + # Bugs might get closed, or patches might be obsoleted or r-'d while the + # commit-queue is processing. + self._patch = self._tool.bugs.fetch_attachment(self._patch.id()) + if self._patch.is_obsolete(): + return False + if self._patch.bug().is_closed(): + return False + if not self._patch.committer(): + return False + # Reviewer is not required. Missing reviewers will be caught during + # the ChangeLog check during landing. + return True + + def _run_command(self, command, success_message, failure_message): + try: + self._commit_queue.run_webkit_patch(command) + self._commit_queue.command_passed(success_message, patch=self._patch) + return True + except ScriptError, e: + self._script_error = e + self.failure_status_id = self._commit_queue.command_failed(failure_message, script_error=self._script_error, patch=self._patch) + return False + + def _apply(self): + return self._run_command([ + "apply-attachment", + "--force-clean", + "--non-interactive", + "--quiet", + self._patch.id(), + ], + "Applied patch", + "Patch does not apply") + + def _build(self): + return self._run_command([ + "build", + "--no-clean", + "--no-update", + "--build", + "--build-style=both", + "--quiet", + ], + "Built patch", + "Patch does not build") + + def _build_without_patch(self): + return self._run_command([ + "build", + "--force-clean", + "--no-update", + "--build", + "--build-style=both", + "--quiet", + ], + "Able to build without patch", + "Unable to build without patch") + + def _test(self): + return self._run_command([ + "build-and-test", + "--no-clean", + "--no-update", + # Notice that we don't pass --build, which means we won't build! + "--test", + "--quiet", + "--non-interactive", + ], + "Passed tests", + "Patch does not pass tests") + + def _build_and_test_without_patch(self): + return self._run_command([ + "build-and-test", + "--force-clean", + "--no-update", + "--build", + "--test", + "--quiet", + "--non-interactive", + ], + "Able to pass tests without patch", + "Unable to pass tests without patch (tree is red?)") + + def _land(self): + return self._run_command([ + "land-attachment", + "--force-clean", + "--ignore-builders", + "--quiet", + "--non-interactive", + "--parent-command=commit-queue", + self._patch.id(), + ], + "Landed patch", + "Unable to land patch") + + def run(self): + if not self._validate(): + return False + if not self._apply(): + raise self._script_error + if not self._build(): + if not self._build_without_patch(): + return False + raise self._script_error + if not self._patch.is_rollout(): + if not self._test(): + if not self._test(): + if not self._build_and_test_without_patch(): + return False + raise self._script_error + # Make sure the patch is still valid before landing (e.g., make sure + # no one has set commit-queue- since we started working on the patch.) + if not self._validate(): + return False + if not self._land(): + raise self._script_error + return True diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py new file mode 100644 index 0000000..8b46146 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py @@ -0,0 +1,194 @@ +# Copyright (c) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from datetime import datetime +import unittest + +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.bot.commitqueuetask import * +from webkitpy.tool.mocktool import MockTool + + +class MockCommitQueue: + def __init__(self, error_plan): + self._error_plan = error_plan + + def run_webkit_patch(self, command): + log("run_webkit_patch: %s" % command) + if self._error_plan: + error = self._error_plan.pop(0) + if error: + raise error + + def command_passed(self, success_message, patch): + log("command_passed: success_message='%s' patch='%s'" % ( + success_message, patch.id())) + + def command_failed(self, failure_message, script_error, patch): + log("command_failed: failure_message='%s' script_error='%s' patch='%s'" % ( + failure_message, script_error, patch.id())) + return 3947 + + +class CommitQueueTaskTest(unittest.TestCase): + def _run_through_task(self, commit_queue, expected_stderr, expected_exception=None): + tool = MockTool(log_executive=True) + patch = tool.bugs.fetch_attachment(197) + task = CommitQueueTask(tool, commit_queue, patch) + OutputCapture().assert_outputs(self, task.run, expected_stderr=expected_stderr, expected_exception=expected_exception) + + def test_success_case(self): + commit_queue = MockCommitQueue([]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_passed: success_message='Applied patch' patch='197' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_passed: success_message='Built patch' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_passed: success_message='Passed tests' patch='197' +run_webkit_patch: ['land-attachment', '--force-clean', '--ignore-builders', '--quiet', '--non-interactive', '--parent-command=commit-queue', 197] +command_passed: success_message='Landed patch' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_apply_failure(self): + commit_queue = MockCommitQueue([ + ScriptError("MOCK apply failure"), + ]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_failed: failure_message='Patch does not apply' script_error='MOCK apply failure' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr, ScriptError) + + def test_build_failure(self): + commit_queue = MockCommitQueue([ + None, + ScriptError("MOCK build failure"), + ]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_passed: success_message='Applied patch' patch='197' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='197' +run_webkit_patch: ['build', '--force-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_passed: success_message='Able to build without patch' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr, ScriptError) + + def test_red_build_failure(self): + commit_queue = MockCommitQueue([ + None, + ScriptError("MOCK build failure"), + ScriptError("MOCK clean build failure"), + ]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_passed: success_message='Applied patch' patch='197' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='197' +run_webkit_patch: ['build', '--force-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_failed: failure_message='Unable to build without patch' script_error='MOCK clean build failure' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_flaky_test_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + ScriptError("MOCK tests failure"), + ]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_passed: success_message='Applied patch' patch='197' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_passed: success_message='Built patch' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_failed: failure_message='Patch does not pass tests' script_error='MOCK tests failure' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_passed: success_message='Passed tests' patch='197' +run_webkit_patch: ['land-attachment', '--force-clean', '--ignore-builders', '--quiet', '--non-interactive', '--parent-command=commit-queue', 197] +command_passed: success_message='Landed patch' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_test_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + ScriptError("MOCK test failure"), + ScriptError("MOCK test failure again"), + ]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_passed: success_message='Applied patch' patch='197' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_passed: success_message='Built patch' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='197' +run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--quiet', '--non-interactive'] +command_passed: success_message='Able to pass tests without patch' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr, ScriptError) + + def test_red_test_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + ScriptError("MOCK test failure"), + ScriptError("MOCK test failure again"), + ScriptError("MOCK clean test failure"), + ]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_passed: success_message='Applied patch' patch='197' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_passed: success_message='Built patch' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='197' +run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--quiet', '--non-interactive'] +command_failed: failure_message='Unable to pass tests without patch (tree is red?)' script_error='MOCK clean test failure' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_land_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + ScriptError("MOCK land failure"), + ]) + expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +command_passed: success_message='Applied patch' patch='197' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +command_passed: success_message='Built patch' patch='197' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +command_passed: success_message='Passed tests' patch='197' +run_webkit_patch: ['land-attachment', '--force-clean', '--ignore-builders', '--quiet', '--non-interactive', '--parent-command=commit-queue', 197] +command_failed: failure_message='Unable to land patch' script_error='MOCK land failure' patch='197' +""" + self._run_through_task(commit_queue, expected_stderr, ScriptError) diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py b/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py new file mode 100644 index 0000000..15eaaf3 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py @@ -0,0 +1,73 @@ +# Copyright (c) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from webkitpy.common.system.deprecated_logging import log +from webkitpy.common.net.bugzilla import CommitterValidator + + +class AbstractFeeder(object): + def __init__(self, tool): + self._tool = tool + + def feed(tool): + raise NotImplementedError, "subclasses must implement" + + def update_work_items(self, item_ids): + self._tool.status_server.update_work_items(self.queue_name, item_ids) + log("Feeding %s items %s" % (self.queue_name, item_ids)) + + +class CommitQueueFeeder(AbstractFeeder): + queue_name = "commit-queue" + + def __init__(self, tool): + AbstractFeeder.__init__(self, tool) + self.committer_validator = CommitterValidator(self._tool.bugs) + + def feed(self): + patches = self._validate_patches() + patches = sorted(patches, self._patch_cmp) + patch_ids = [patch.id() for patch in patches] + self.update_work_items(patch_ids) + + def _patches_for_bug(self, bug_id): + return self._tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True) + + def _validate_patches(self): + # Not using BugzillaQueries.fetch_patches_from_commit_queue() so we can reject patches with invalid committers/reviewers. + bug_ids = self._tool.bugs.queries.fetch_bug_ids_from_commit_queue() + all_patches = sum([self._patches_for_bug(bug_id) for bug_id in bug_ids], []) + return self.committer_validator.patches_after_rejecting_invalid_commiters_and_reviewers(all_patches) + + def _patch_cmp(self, a, b): + # Sort first by is_rollout, then by attach_date. + # Reversing the order so that is_rollout is first. + rollout_cmp = cmp(b.is_rollout(), a.is_rollout()) + if rollout_cmp != 0: + return rollout_cmp + return cmp(a.attach_date(), b.attach_date()) diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/feeders_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/feeders_unittest.py new file mode 100644 index 0000000..5ce00b4 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/bot/feeders_unittest.py @@ -0,0 +1,70 @@ +# Copyright (c) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from datetime import datetime +import unittest + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.bot.feeders import * +from webkitpy.tool.mocktool import MockTool + + +class FeedersTest(unittest.TestCase): + def test_commit_queue_feeder(self): + feeder = CommitQueueFeeder(MockTool()) + expected_stderr = u"""Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) +Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) +MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py. + +- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags. + +- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.' +MOCK: update_work_items: commit-queue [106, 197] +Feeding commit-queue items [106, 197] +""" + OutputCapture().assert_outputs(self, feeder.feed, expected_stderr=expected_stderr) + + def _mock_attachment(self, is_rollout, attach_date): + attachment = Mock() + attachment.is_rollout = lambda: is_rollout + attachment.attach_date = lambda: attach_date + return attachment + + def test_patch_cmp(self): + long_ago_date = datetime(1900, 1, 21) + recent_date = datetime(2010, 1, 21) + attachment1 = self._mock_attachment(is_rollout=False, attach_date=recent_date) + attachment2 = self._mock_attachment(is_rollout=False, attach_date=long_ago_date) + attachment3 = self._mock_attachment(is_rollout=True, attach_date=recent_date) + attachment4 = self._mock_attachment(is_rollout=True, attach_date=long_ago_date) + attachments = [attachment1, attachment2, attachment3, attachment4] + expected_sort = [attachment4, attachment3, attachment2, attachment1] + queue = CommitQueueFeeder(MockTool()) + attachments.sort(queue._patch_cmp) + self.assertEqual(attachments, expected_sort) diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py index 289dc4a..8118653 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py @@ -141,6 +141,8 @@ class QueueEngine: def _open_work_log(self, work_item): work_item_log_path = self._delegate.work_item_log_path(work_item) + if not work_item_log_path: + return self._work_log = self._output_tee.add_log(work_item_log_path) def _ensure_work_log_closed(self): diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/download.py b/WebKitTools/Scripts/webkitpy/tool/commands/download.py index ed0e3d6..9916523 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/download.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/download.py @@ -289,7 +289,7 @@ class AbstractRolloutPrepCommand(AbstractSequencedCommand): argument_names = "REVISION REASON" def _commit_info(self, revision): - commit_info = self.tool.checkout().commit_info_for_revision(revision) + commit_info = self._tool.checkout().commit_info_for_revision(revision) if commit_info and commit_info.bug_id(): # Note: Don't print a bug URL here because it will confuse the # SheriffBot because the SheriffBot just greps the output diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py b/WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py index 5da5bbb..1b51c9f 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py @@ -41,8 +41,8 @@ class OpenBugs(AbstractDeclarativeCommand): def _open_bugs(self, bug_ids): for bug_id in bug_ids: - bug_url = self.tool.bugs.bug_url_for_bug_id(bug_id) - self.tool.user.open_url(bug_url) + bug_url = self._tool.bugs.bug_url_for_bug_id(bug_id) + self._tool.user.open_url(bug_url) # _find_bugs_in_string mostly exists for easy unit testing. def _find_bugs_in_string(self, string): diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queries.py b/WebKitTools/Scripts/webkitpy/tool/commands/queries.py index 9b8d162..c6e45aa 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queries.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queries.py @@ -33,6 +33,7 @@ from optparse import make_option from webkitpy.common.checkout.commitinfo import CommitInfo from webkitpy.common.config.committers import CommitterList from webkitpy.common.net.buildbot import BuildBot +from webkitpy.common.net.regressionwindow import RegressionWindow from webkitpy.common.system.user import User from webkitpy.tool.grammar import pluralize from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand @@ -112,7 +113,7 @@ class LastGreenRevision(AbstractDeclarativeCommand): help_text = "Prints the last known good revision" def execute(self, options, args, tool): - print self.tool.buildbot.last_green_revision() + print self._tool.buildbot.last_green_revision() class WhatBroke(AbstractDeclarativeCommand): @@ -122,29 +123,26 @@ class WhatBroke(AbstractDeclarativeCommand): def _print_builder_line(self, builder_name, max_name_width, status_message): print "%s : %s" % (builder_name.ljust(max_name_width), status_message) - # FIXME: This is slightly different from Builder.suspect_revisions_for_green_to_red_transition - # due to needing to detect the "hit the limit" case an print a special message. def _print_blame_information_for_builder(self, builder_status, name_width, avoid_flakey_tests=True): - builder = self.tool.buildbot.builder_with_name(builder_status["name"]) + builder = self._tool.buildbot.builder_with_name(builder_status["name"]) red_build = builder.build(builder_status["build_number"]) - (last_green_build, first_red_build) = builder.find_failure_transition(red_build) - if not first_red_build: + regression_window = builder.find_regression_window(red_build) + if not regression_window.failing_build(): self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)") return - if not last_green_build: - self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % first_red_build.revision()) + if not regression_window.build_before_failure(): + self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % regression_window.failing_build().revision()) return - suspect_revisions = range(first_red_build.revision(), last_green_build.revision(), -1) - suspect_revisions.reverse() + revisions = regression_window.revisions() first_failure_message = "" - if (first_red_build == builder.build(builder_status["build_number"])): + if (regression_window.failing_build() == builder.build(builder_status["build_number"])): first_failure_message = " FIRST FAILURE, possibly a flaky test" - self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (suspect_revisions, first_failure_message)) - for revision in suspect_revisions: - commit_info = self.tool.checkout().commit_info_for_revision(revision) + self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (revisions, first_failure_message)) + for revision in revisions: + commit_info = self._tool.checkout().commit_info_for_revision(revision) if commit_info: - print commit_info.blame_string(self.tool.bugs) + print commit_info.blame_string(self._tool.bugs) else: print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision @@ -169,7 +167,7 @@ class WhoBrokeIt(AbstractDeclarativeCommand): help_text = "Print a list of revisions causing failures on %s" % BuildBot.default_host def execute(self, options, args, tool): - for revision, builders in self.tool.buildbot.revisions_causing_failures(False).items(): + for revision, builders in self._tool.buildbot.failure_map(False).revisions_causing_failures().items(): print "r%s appears to have broken %s" % (revision, [builder.name() for builder in builders]) @@ -188,7 +186,7 @@ class ResultsFor(AbstractDeclarativeCommand): print " %s" % filename def execute(self, options, args, tool): - builders = self.tool.buildbot.builders() + builders = self._tool.buildbot.builders() for builder in builders: print "%s:" % builder.name() build = builder.build_for_revision(args[0], allow_failed_lookups=True) @@ -200,13 +198,14 @@ class FailureReason(AbstractDeclarativeCommand): help_text = "Lists revisions where individual test failures started at %s" % BuildBot.default_host def _print_blame_information_for_transition(self, green_build, red_build, failing_tests): - suspect_revisions = green_build.builder().suspect_revisions_for_transition(green_build, red_build) + regression_window = RegressionWindow(green_build, red_build) + revisions = regression_window.revisions() print "SUCCESS: Build %s (r%s) was the first to show failures: %s" % (red_build._number, red_build.revision(), failing_tests) print "Suspect revisions:" - for revision in suspect_revisions: - commit_info = self.tool.checkout().commit_info_for_revision(revision) + for revision in revisions: + commit_info = self._tool.checkout().commit_info_for_revision(revision) if commit_info: - print commit_info.blame_string(self.tool.bugs) + print commit_info.blame_string(self._tool.bugs) else: print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision @@ -255,7 +254,7 @@ class FailureReason(AbstractDeclarativeCommand): return 0 def _builder_to_explain(self): - builder_statuses = self.tool.buildbot.builder_statuses() + builder_statuses = self._tool.buildbot.builder_statuses() red_statuses = [status for status in builder_statuses if not status["is_green"]] print "%s failing" % (pluralize("builder", len(red_statuses))) builder_choices = [status["name"] for status in red_statuses] @@ -264,11 +263,11 @@ class FailureReason(AbstractDeclarativeCommand): # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object. for status in red_statuses: if status["name"] == chosen_name: - return (self.tool.buildbot.builder_with_name(chosen_name), status["built_revision"]) + return (self._tool.buildbot.builder_with_name(chosen_name), status["built_revision"]) def execute(self, options, args, tool): (builder, latest_revision) = self._builder_to_explain() - start_revision = self.tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision + start_revision = self._tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision if not start_revision: print "Revision required." return 1 diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queues.py b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py index bc9ee42..80fd2ea 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queues.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py @@ -27,6 +27,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import time import traceback import os @@ -39,6 +40,8 @@ from webkitpy.common.net.statusserver import StatusServer from webkitpy.common.system.executive import ScriptError from webkitpy.common.system.deprecated_logging import error, log from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler +from webkitpy.tool.bot.commitqueuetask import CommitQueueTask +from webkitpy.tool.bot.feeders import CommitQueueFeeder from webkitpy.tool.bot.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate from webkitpy.tool.grammar import pluralize @@ -50,6 +53,7 @@ class AbstractQueue(Command, QueueEngineDelegate): _pass_status = "Pass" _fail_status = "Fail" + _retry_status = "Retry" _error_status = "Error" def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations @@ -62,20 +66,20 @@ class AbstractQueue(Command, QueueEngineDelegate): def _cc_watchers(self, bug_id): try: - self.tool.bugs.add_cc_to_bug(bug_id, self.watchers) + self._tool.bugs.add_cc_to_bug(bug_id, self.watchers) except Exception, e: traceback.print_exc() log("Failed to CC watchers.") def run_webkit_patch(self, args): - webkit_patch_args = [self.tool.path()] + webkit_patch_args = [self._tool.path()] # FIXME: This is a hack, we should have a more general way to pass global options. # FIXME: We must always pass global options and their value in one argument # because our global option code looks for the first argument which does # not begin with "-" and assumes that is the command name. - webkit_patch_args += ["--status-host=%s" % self.tool.status_server.host] + webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host] webkit_patch_args.extend(args) - return self.tool.executive.run_and_throw_if_fail(webkit_patch_args) + return self._tool.executive.run_and_throw_if_fail(webkit_patch_args) def _log_directory(self): return "%s-logs" % self.name @@ -89,16 +93,16 @@ class AbstractQueue(Command, QueueEngineDelegate): raise NotImplementedError, "subclasses must implement" def begin_work_queue(self): - log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root)) + log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root)) if self.options.confirm: - response = self.tool.user.prompt("Are you sure? Type \"yes\" to continue: ") + response = self._tool.user.prompt("Are you sure? Type \"yes\" to continue: ") if (response != "yes"): error("User declined.") log("Running WebKit %s." % self.name) - self.tool.status_server.update_status(self.name, "Starting Queue") + self._tool.status_server.update_status(self.name, "Starting Queue") def stop_work_queue(self, reason): - self.tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason) + self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason) def should_continue_work_queue(self): self._iteration_count += 1 @@ -120,8 +124,8 @@ class AbstractQueue(Command, QueueEngineDelegate): def execute(self, options, args, tool, engine=QueueEngine): self.options = options # FIXME: This code is wrong. Command.options is a list, this assumes an Options element! - self.tool = tool # FIXME: This code is wrong too! Command.bind_to_tool handles this! - return engine(self.name, self, self.tool.wakeup_event).run() + self._tool = tool # FIXME: This code is wrong too! Command.bind_to_tool handles this! + return engine(self.name, self, self._tool.wakeup_event).run() @classmethod def _log_from_script_error_for_upload(cls, script_error, output_limit=None): @@ -144,20 +148,47 @@ class AbstractQueue(Command, QueueEngineDelegate): return tool.status_server.update_status(cls.name, message, state["patch"], failure_log) +class FeederQueue(AbstractQueue): + name = "feeder-queue" + + _sleep_duration = 30 # seconds + + # AbstractPatchQueue methods + + def begin_work_queue(self): + AbstractQueue.begin_work_queue(self) + self.feeders = [ + CommitQueueFeeder(self._tool), + ] + + def next_work_item(self): + # This really show inherit from some more basic class that doesn't + # understand work items, but the base class in the heirarchy currently + # understands work items. + return "synthetic-work-item" + + def should_proceed_with_work_item(self, work_item): + return True + + def process_work_item(self, work_item): + for feeder in self.feeders: + feeder.feed() + time.sleep(self._sleep_duration) + return True + + def work_item_log_path(self, work_item): + return None + + def handle_unexpected_error(self, work_item, message): + log(message) + + class AbstractPatchQueue(AbstractQueue): def _update_status(self, message, patch=None, results_file=None): - self.tool.status_server.update_status(self.name, message, patch, results_file) - - # Note, eventually this will be done by a separate "feeder" queue - # whose job it is to poll bugzilla and feed work items into the - # status server for other queues to munch on. - def _update_work_items(self, patch_ids): - self.tool.status_server.update_work_items(self.name, patch_ids) - if patch_ids: - self.log_progress(patch_ids) + return self._tool.status_server.update_status(self.name, message, patch, results_file) def _fetch_next_work_item(self): - return self.tool.status_server.next_work_item(self.name) + return self._tool.status_server.next_work_item(self.name) def _did_pass(self, patch): self._update_status(self._pass_status, patch) @@ -165,6 +196,9 @@ class AbstractPatchQueue(AbstractQueue): def _did_fail(self, patch): self._update_status(self._fail_status, patch) + def _did_retry(self, patch): + self._update_status(self._retry_status, patch) + def _did_error(self, patch, reason): message = "%s: %s" % (self._error_status, reason) self._update_status(message, patch) @@ -172,178 +206,63 @@ class AbstractPatchQueue(AbstractQueue): def work_item_log_path(self, patch): return os.path.join(self._log_directory(), "%s.log" % patch.bug_id()) - def log_progress(self, patch_ids): - log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids)))) - class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): name = "commit-queue" - def __init__(self): - AbstractPatchQueue.__init__(self) # AbstractPatchQueue methods def begin_work_queue(self): AbstractPatchQueue.begin_work_queue(self) - self.committer_validator = CommitterValidator(self.tool.bugs) - - def _validate_patches_in_commit_queue(self): - # Not using BugzillaQueries.fetch_patches_from_commit_queue() so we can reject patches with invalid committers/reviewers. - bug_ids = self.tool.bugs.queries.fetch_bug_ids_from_commit_queue() - all_patches = sum([self.tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True) for bug_id in bug_ids], []) - return self.committer_validator.patches_after_rejecting_invalid_commiters_and_reviewers(all_patches) - - def _patch_cmp(self, a, b): - # Sort first by is_rollout, then by attach_date. - # Reversing the order so that is_rollout is first. - rollout_cmp = cmp(b.is_rollout(), a.is_rollout()) - if (rollout_cmp != 0): - return rollout_cmp - return cmp(a.attach_date(), b.attach_date()) - - def _feed_work_items_to_server(self): - # Grab the set of patches from bugzilla, sort them, and update the status server. - # Eventually this will all be done by a separate feeder queue. - patches = self._validate_patches_in_commit_queue() - patches = sorted(patches, self._patch_cmp) - self._update_work_items([patch.id() for patch in patches]) + self.committer_validator = CommitterValidator(self._tool.bugs) def next_work_item(self): - self._feed_work_items_to_server() - # The grab the next patch to work on back from the status server. patch_id = self._fetch_next_work_item() if not patch_id: return None - return self.tool.bugs.fetch_attachment(patch_id) - - def _can_build_and_test_without_patch(self): - try: - self.run_webkit_patch([ - "build-and-test", - "--force-clean", - "--build", - "--test", - "--non-interactive", - "--no-update", - "--build-style=both", - "--quiet"]) - return True - except ScriptError, e: - failure_log = self._log_from_script_error_for_upload(e) - self._update_status("Unable to build and test without patch", results_file=failure_log) - return False + return self._tool.bugs.fetch_attachment(patch_id) def should_proceed_with_work_item(self, patch): patch_text = "rollout patch" if patch.is_rollout() else "patch" - self._update_status("Landing %s" % patch_text, patch) + self._update_status("Processing %s" % patch_text, patch) return True - def _build_and_test_patch(self, patch, first_run=False): + def process_work_item(self, patch): + self._cc_watchers(patch.bug_id()) + task = CommitQueueTask(self._tool, self, patch) try: - args = [ - "build-and-test-attachment", - "--force-clean", - "--build", - "--non-interactive", - "--build-style=both", - "--quiet", - patch.id() - ] - # We don't bother to run tests for rollouts as that makes them too slow. - if not patch.is_rollout(): - args.append("--test") - if not first_run: - # The first time through, we don't reject the patch from the - # commit queue because we want to make sure we can build and - # test ourselves. However, the second time through, we - # register ourselves as the parent-command so we can reject - # the patch on failure. - args.append("--parent-command=commit-queue") - # The second time through, we also don't want to update so we - # know we're testing the same revision that we successfully - # built and tested. - args.append("--no-update") - self.run_webkit_patch(args) - return True + if task.run(): + self._did_pass(patch) + return True + self._did_retry(patch) except ScriptError, e: - failure_log = self._log_from_script_error_for_upload(e) - self._update_status("Unable to build and test patch", patch=patch, results_file=failure_log) - if first_run: - return False + validator = CommitterValidator(self._tool.bugs) + validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task.failure_status_id, e)) self._did_fail(patch) - raise - - def _revalidate_patch(self, patch): - # Bugs might get closed, or patches might be obsoleted or r-'d while the - # commit-queue is processing. Do one last minute check before landing. - patch = self.tool.bugs.fetch_attachment(patch.id()) - if patch.is_obsolete(): - return None - if patch.bug().is_closed(): - return None - if not patch.committer(): - return None - # Reviewer is not required. Misisng reviewers will be caught during the ChangeLog check during landing. - return patch - - def _land(self, patch): - try: - args = [ - "land-attachment", - "--force-clean", - "--non-interactive", - "--ignore-builders", - "--quiet", - "--parent-command=commit-queue", - patch.id(), - ] - self.run_webkit_patch(args) - self._did_pass(patch) - except ScriptError, e: - failure_log = self._log_from_script_error_for_upload(e) - self._update_status("Unable to land patch", patch=patch, results_file=failure_log) - self._did_fail(patch) - raise - - def process_work_item(self, patch): - self._cc_watchers(patch.bug_id()) - if not self._build_and_test_patch(patch, first_run=True): - self._update_status("Building and testing without the patch as a sanity check", patch) - # The patch failed to build and test. It's possible that the - # tree is busted. To check that case, we try to build and test - # without the patch. - if not self._can_build_and_test_without_patch(): - return False - self._update_status("Build and test succeeded, trying again with patch", patch) - # Hum, looks like the patch is actually bad. Of course, we could - # have been bitten by a flaky test the first time around. We try - # to build and test again. If it fails a second time, we're pretty - # sure its a bad test and re can reject it outright. - self._build_and_test_patch(patch) - # Do one last check to catch any bug changes (cq-, closed, reviewer changed, etc.) - # This helps catch races between the bots if locks expire. - patch = self._revalidate_patch(patch) - if not patch: - return False - self._land(patch) - return True def handle_unexpected_error(self, patch, message): self.committer_validator.reject_patch_from_commit_queue(patch.id(), message) - # StepSequenceErrorHandler methods - @staticmethod - def _error_message_for_bug(tool, status_id, script_error): + def command_passed(self, message, patch): + self._update_status(message, patch=patch) + + def command_failed(self, message, script_error, patch): + failure_log = self._log_from_script_error_for_upload(script_error) + return self._update_status(message, patch=patch, results_file=failure_log) + + def _error_message_for_bug(self, status_id, script_error): if not script_error.output: return script_error.message_with_output() - results_link = tool.status_server.results_url_for_status(status_id) + results_link = self._tool.status_server.results_url_for_status(status_id) return "%s\nFull output: %s" % (script_error.message_with_output(), results_link) - @classmethod + # StepSequenceErrorHandler methods + def handle_script_error(cls, tool, state, script_error): - status_id = cls._update_status_for_script_error(tool, state, script_error) - validator = CommitterValidator(tool.bugs) - validator.reject_patch_from_commit_queue(state["patch"].id(), cls._error_message_for_bug(tool, status_id, script_error)) + # Hitting this error handler should be pretty rare. It does occur, + # however, when a patch no longer applies to top-of-tree in the final + # land step. + log(script_error.message_with_output()) @classmethod def handle_checkout_needs_update(cls, tool, state, options, error): @@ -368,7 +287,7 @@ class RietveldUploadQueue(AbstractPatchQueue, StepSequenceErrorHandler): # AbstractPatchQueue methods def next_work_item(self): - patch_id = self.tool.bugs.queries.fetch_first_patch_from_rietveld_queue() + patch_id = self._tool.bugs.queries.fetch_first_patch_from_rietveld_queue() if patch_id: return patch_id self._update_status("Empty queue") @@ -393,7 +312,7 @@ class RietveldUploadQueue(AbstractPatchQueue, StepSequenceErrorHandler): def handle_unexpected_error(self, patch, message): log(message) - self._reject_patch(self.tool, patch.id()) + self._reject_patch(self._tool, patch.id()) # StepSequenceErrorHandler methods @@ -417,10 +336,10 @@ class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, return self.name def fetch_potential_patch_ids(self): - return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue() + return self._tool.bugs.queries.fetch_attachment_ids_from_review_queue() def status_server(self): - return self.tool.status_server + return self._tool.status_server def is_terminal_status(self, status): return status == "Pass" or status == "Fail" or status.startswith("Error:") @@ -434,7 +353,7 @@ class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, def next_work_item(self): patch_id = self._patches.next() if patch_id: - return self.tool.bugs.fetch_attachment(patch_id) + return self._tool.bugs.fetch_attachment(patch_id) def should_proceed_with_work_item(self, patch): raise NotImplementedError, "subclasses must implement" diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py index 2deee76..029814e 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py @@ -47,20 +47,16 @@ class TestReviewQueue(AbstractReviewQueue): name = "test-review-queue" +class TestFeederQueue(FeederQueue): + _sleep_duration = 0 + + class MockRolloutPatch(MockPatch): def is_rollout(self): return True class AbstractQueueTest(CommandsTest): - def _assert_log_progress_output(self, patch_ids, progress_output): - OutputCapture().assert_outputs(self, TestQueue().log_progress, [patch_ids], expected_stderr=progress_output) - - def test_log_progress(self): - self._assert_log_progress_output([1,2,3], "3 patches in test-queue [1, 2, 3]\n") - self._assert_log_progress_output(["1","2","3"], "3 patches in test-queue [1, 2, 3]\n") - self._assert_log_progress_output([1], "1 patch in test-queue [1]\n") - def test_log_directory(self): self.assertEquals(TestQueue()._log_directory(), "test-queue-logs") @@ -115,6 +111,29 @@ class AbstractQueueTest(CommandsTest): self._assert_log_message(script_error, expected_output) +class FeederQueueTest(QueuesTest): + def test_feeder_queue(self): + queue = TestFeederQueue() + tool = MockTool(log_executive=True) + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("feeder-queue", MockSCM.fake_checkout_root), + "should_proceed_with_work_item": "", + "next_work_item": "", + "process_work_item": """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) +Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) +MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py. + +- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags. + +- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.' +MOCK: update_work_items: commit-queue [106, 197] +Feeding commit-queue items [106, 197] +""", + "handle_unexpected_error": "Mock error message\n", + } + self.assert_queue_outputs(queue, tool=tool, expected_stderr=expected_stderr) + + class AbstractPatchQueueTest(CommandsTest): def test_fetch_next_work_item(self): queue = AbstractPatchQueue() @@ -167,7 +186,7 @@ class SecondThoughtsCommitQueue(CommitQueue): "attacher_email": "Contributer1", } patch = Attachment(attachment_dictionary, None) - self.tool.bugs.set_override_patch(patch) + self._tool.bugs.set_override_patch(patch) return True @@ -175,40 +194,58 @@ class CommitQueueTest(QueuesTest): def test_commit_queue(self): expected_stderr = { "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root), - "should_proceed_with_work_item": "MOCK: update_status: commit-queue Landing patch\n", - # FIXME: The commit-queue warns about bad committers twice. This is due to the fact that we access Attachment.reviewer() twice and it logs each time. - "next_work_item": """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) -Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) -MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py.\n\n- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.\n\n- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.' -MOCK: update_work_items: commit-queue [106, 197] -2 patches in commit-queue [106, 197] + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Applied patch +MOCK: update_status: commit-queue Built patch +MOCK: update_status: commit-queue Passed tests +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass """, - "process_work_item": "MOCK: update_status: commit-queue Pass\n", "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'Mock error message'\n", - "handle_script_error": "MOCK: update_status: commit-queue ScriptError error message\nMOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'ScriptError error message'\n", + "handle_script_error": "ScriptError error message\n", } self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr) + def test_commit_queue_failure(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Patch does not apply +MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'MOCK script error' +MOCK: update_status: commit-queue Fail +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + queue = CommitQueue() + + def mock_run_webkit_patch(command): + raise ScriptError('MOCK script error') + + queue.run_webkit_patch = mock_run_webkit_patch + self.assert_queue_outputs(queue, expected_stderr=expected_stderr) + def test_rollout(self): tool = MockTool(log_executive=True) tool.buildbot.light_tree_on_fire() expected_stderr = { "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root), - "should_proceed_with_work_item": "MOCK: update_status: commit-queue Landing patch\n", - # FIXME: The commit-queue warns about bad committers twice. This is due to the fact that we access Attachment.reviewer() twice and it logs each time. - "next_work_item": """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) -Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) -MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py. - -- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags. - -- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.' -MOCK: update_work_items: commit-queue [106, 197] -2 patches in commit-queue [106, 197] + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +MOCK: update_status: commit-queue Applied patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +MOCK: update_status: commit-queue Built patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +MOCK: update_status: commit-queue Passed tests +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--ignore-builders', '--quiet', '--non-interactive', '--parent-command=commit-queue', 197] +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass """, - "process_work_item": "MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test-attachment', '--force-clean', '--build', '--non-interactive', '--build-style=both', '--quiet', 197, '--test']\nMOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--ignore-builders', '--quiet', '--parent-command=commit-queue', 197]\nMOCK: update_status: commit-queue Pass\n", "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'Mock error message'\n", - "handle_script_error": "MOCK: update_status: commit-queue ScriptError error message\nMOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'ScriptError error message'\n", + "handle_script_error": "ScriptError error message\n", } self.assert_queue_outputs(CommitQueue(), tool=tool, expected_stderr=expected_stderr) @@ -218,52 +255,23 @@ MOCK: update_work_items: commit-queue [106, 197] rollout_patch = MockRolloutPatch() expected_stderr = { "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root), - "should_proceed_with_work_item": "MOCK: update_status: commit-queue Landing rollout patch\n", - # FIXME: The commit-queue warns about bad committers twice. This is due to the fact that we access Attachment.reviewer() twice and it logs each time. - "next_work_item": """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) -Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com) -MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py. - -- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags. - -- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.' -MOCK: update_work_items: commit-queue [106, 197] -2 patches in commit-queue [106, 197] + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing rollout patch\n", + "next_work_item": "", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] +MOCK: update_status: commit-queue Applied patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +MOCK: update_status: commit-queue Built patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] +MOCK: update_status: commit-queue Passed tests +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--ignore-builders', '--quiet', '--non-interactive', '--parent-command=commit-queue', 197] +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass """, - "process_work_item": "MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test-attachment', '--force-clean', '--build', '--non-interactive', '--build-style=both', '--quiet', 197]\nMOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--ignore-builders', '--quiet', '--parent-command=commit-queue', 197]\nMOCK: update_status: commit-queue Pass\n", "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'Mock error message'\n", - "handle_script_error": "MOCK: update_status: commit-queue ScriptError error message\nMOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'ScriptError error message'\n", + "handle_script_error": "ScriptError error message\n", } self.assert_queue_outputs(CommitQueue(), tool=tool, work_item=rollout_patch, expected_stderr=expected_stderr) - def test_can_build_and_test(self): - queue = CommitQueue() - tool = MockTool() - tool.executive = Mock() - queue.bind_to_tool(tool) - self.assertTrue(queue._can_build_and_test_without_patch()) - expected_run_args = ["echo", "--status-host=example.com", "build-and-test", "--force-clean", "--build", "--test", "--non-interactive", "--no-update", "--build-style=both", "--quiet"] - tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args) - - def _mock_attachment(self, is_rollout, attach_date): - attachment = Mock() - attachment.is_rollout = lambda: is_rollout - attachment.attach_date = lambda: attach_date - return attachment - - def test_patch_cmp(self): - long_ago_date = datetime(1900, 1, 21) - recent_date = datetime(2010, 1, 21) - attachment1 = self._mock_attachment(is_rollout=False, attach_date=recent_date) - attachment2 = self._mock_attachment(is_rollout=False, attach_date=long_ago_date) - attachment3 = self._mock_attachment(is_rollout=True, attach_date=recent_date) - attachment4 = self._mock_attachment(is_rollout=True, attach_date=long_ago_date) - attachments = [attachment1, attachment2, attachment3, attachment4] - expected_sort = [attachment4, attachment3, attachment2, attachment1] - queue = CommitQueue() - attachments.sort(queue._patch_cmp) - self.assertEqual(attachments, expected_sort) - def test_auto_retry(self): queue = CommitQueue() options = Mock() @@ -282,7 +290,13 @@ MOCK: update_work_items: commit-queue [106, 197] def test_manual_reject_during_processing(self): queue = SecondThoughtsCommitQueue() queue.bind_to_tool(MockTool()) - queue.process_work_item(MockPatch()) + expected_stderr = """MOCK: update_status: commit-queue Applied patch +MOCK: update_status: commit-queue Built patch +MOCK: update_status: commit-queue Passed tests +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass +""" + OutputCapture().assert_outputs(self, queue.process_work_item, [MockPatch()], expected_stderr=expected_stderr) class RietveldUploadQueueTest(QueuesTest): diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py index aa3cef4..9f3583d 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py @@ -32,6 +32,7 @@ from webkitpy.common.net.bugzilla import Attachment from webkitpy.common.system.outputcapture import OutputCapture from webkitpy.common.system.executive import ScriptError from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler from webkitpy.tool.mocktool import MockTool @@ -100,4 +101,6 @@ class QueuesTest(unittest.TestCase): self.assert_outputs(queue.should_proceed_with_work_item, "should_proceed_with_work_item", [work_item], expected_stdout, expected_stderr, expected_exceptions) self.assert_outputs(queue.process_work_item, "process_work_item", [work_item], expected_stdout, expected_stderr, expected_exceptions) self.assert_outputs(queue.handle_unexpected_error, "handle_unexpected_error", [work_item, "Mock error message"], expected_stdout, expected_stderr, expected_exceptions) - self.assert_outputs(queue.handle_script_error, "handle_script_error", [tool, {"patch": MockPatch()}, ScriptError(message="ScriptError error message", script_args="MockErrorCommand")], expected_stdout, expected_stderr, expected_exceptions) + # Should we have a different function for testing StepSequenceErrorHandlers? + if isinstance(queue, StepSequenceErrorHandler): + self.assert_outputs(queue.handle_script_error, "handle_script_error", [tool, {"patch": MockPatch()}, ScriptError(message="ScriptError error message", script_args="MockErrorCommand")], expected_stdout, expected_stderr, expected_exceptions) diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py b/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py index 78e06c6..abfa850 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py @@ -72,15 +72,15 @@ class Rebaseline(AbstractDeclarativeCommand): # FIXME: This should share more code with FailureReason._builder_to_explain def _builder_to_pull_from(self): - builder_statuses = self.tool.buildbot.builder_statuses() + builder_statuses = self._tool.buildbot.builder_statuses() red_statuses = [status for status in builder_statuses if not status["is_green"]] print "%s failing" % (pluralize("builder", len(red_statuses))) builder_choices = [status["name"] for status in red_statuses] - chosen_name = self.tool.user.prompt_with_list("Which builder to pull results from:", builder_choices) + chosen_name = self._tool.user.prompt_with_list("Which builder to pull results from:", builder_choices) # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object. for status in red_statuses: if status["name"] == chosen_name: - return (self.tool.buildbot.builder_with_name(chosen_name), status["build_number"]) + return (self._tool.buildbot.builder_with_name(chosen_name), status["build_number"]) def _replace_expectation_with_remote_result(self, local_file, remote_file): (downloaded_file, headers) = urllib.urlretrieve(remote_file) @@ -90,7 +90,8 @@ class Rebaseline(AbstractDeclarativeCommand): parsed_results = build.layout_test_results().parsed_results() # FIXME: This probably belongs as API on LayoutTestResults # but .failing_tests() already means something else. - return parsed_results[LayoutTestResults.fail_key] + failing_tests = parsed_results[LayoutTestResults.fail_key] + return self._tool.user.prompt_with_list("Which test(s) to rebaseline:", failing_tests, can_choose_multiple=True) def _results_url_for_test(self, build, test): test_base = os.path.splitext(test)[0] diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py index 24c8517..23d013d 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py @@ -50,9 +50,9 @@ class SheriffBot(AbstractQueue, StepSequenceErrorHandler): def begin_work_queue(self): AbstractQueue.begin_work_queue(self) - self._sheriff = Sheriff(self.tool, self) - self._irc_bot = SheriffIRCBot(self.tool, self._sheriff) - self.tool.ensure_irc_connected(self._irc_bot.irc_delegate()) + self._sheriff = Sheriff(self._tool, self) + self._irc_bot = SheriffIRCBot(self._tool, self._sheriff) + self._tool.ensure_irc_connected(self._irc_bot.irc_delegate()) def work_item_log_path(self, new_failures): return os.path.join("%s-logs" % self.name, "%s.log" % new_failures.keys()[0]) @@ -86,12 +86,12 @@ class SheriffBot(AbstractQueue, StepSequenceErrorHandler): self._update() # We do one read from buildbot to ensure a consistent view. - revisions_causing_failures = self.tool.buildbot.revisions_causing_failures() + revisions_causing_failures = self._tool.buildbot.failure_map().revisions_causing_failures() # Similarly, we read once from our the status_server. old_failing_svn_revisions = [] for svn_revision in revisions_causing_failures.keys(): - if self.tool.status_server.svn_revision(svn_revision): + if self._tool.status_server.svn_revision(svn_revision): old_failing_svn_revisions.append(svn_revision) new_failures = self._new_failures(revisions_causing_failures, @@ -108,7 +108,7 @@ class SheriffBot(AbstractQueue, StepSequenceErrorHandler): blame_list = new_failures.keys() for svn_revision, builders in new_failures.items(): try: - commit_info = self.tool.checkout().commit_info_for_revision(svn_revision) + commit_info = self._tool.checkout().commit_info_for_revision(svn_revision) if not commit_info: print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision continue @@ -120,7 +120,7 @@ class SheriffBot(AbstractQueue, StepSequenceErrorHandler): builders) finally: for builder in builders: - self.tool.status_server.update_svn_revision(svn_revision, + self._tool.status_server.update_svn_revision(svn_revision, builder.name()) return True diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/upload.py b/WebKitTools/Scripts/webkitpy/tool/commands/upload.py index 4a15ed6..107d8db 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/upload.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/upload.py @@ -82,14 +82,14 @@ class CleanPendingCommit(AbstractDeclarativeCommand): def execute(self, options, args, tool): committers = CommitterList() for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list(): - bug = self.tool.bugs.fetch_bug(bug_id) + bug = self._tool.bugs.fetch_bug(bug_id) patches = bug.patches(include_obsolete=True) for patch in patches: flags_to_clear = self._flags_to_clear_on_patch(patch) if not flags_to_clear: continue message = "Cleared %s from obsolete attachment %s so that this bug does not appear in http://webkit.org/pending-commit." % (flags_to_clear, patch.id()) - self.tool.bugs.obsolete_attachment(patch.id(), message) + self._tool.bugs.obsolete_attachment(patch.id(), message) class AssignToCommitter(AbstractDeclarativeCommand): @@ -104,7 +104,7 @@ class AssignToCommitter(AbstractDeclarativeCommand): def _assign_bug_to_last_patch_attacher(self, bug_id): committers = CommitterList() - bug = self.tool.bugs.fetch_bug(bug_id) + bug = self._tool.bugs.fetch_bug(bug_id) if not bug.is_unassigned(): assigned_to_email = bug.assigned_to_email() log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email))) @@ -128,7 +128,7 @@ class AssignToCommitter(AbstractDeclarativeCommand): return reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch.id(), committer.full_name) - self.tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message) + self._tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message) def execute(self, options, args, tool): for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list(): diff --git a/WebKitTools/Scripts/webkitpy/tool/mocktool.py b/WebKitTools/Scripts/webkitpy/tool/mocktool.py index 8a6188a..277bd08 100644 --- a/WebKitTools/Scripts/webkitpy/tool/mocktool.py +++ b/WebKitTools/Scripts/webkitpy/tool/mocktool.py @@ -350,6 +350,16 @@ class MockBuilder(object): self._name, username, comments)) +class MockFailureMap(): + def __init__(self, buildbot): + self._buildbot = buildbot + + def revisions_causing_failures(self): + return { + "29837": [self._buildbot.builder_with_name("Builder1")], + } + + class MockBuildBot(object): buildbot_host = "dummy_buildbot_host" def __init__(self): @@ -394,10 +404,8 @@ class MockBuildBot(object): def light_tree_on_fire(self): self._mock_builder2_status["is_green"] = False - def revisions_causing_failures(self): - return { - "29837": [self.builder_with_name("Builder1")], - } + def failure_map(self): + return MockFailureMap(self) class MockSCM(Mock): @@ -483,8 +491,8 @@ class MockUser(object): def page(self, message): pass - def confirm(self, message=None): - return True + def confirm(self, message=None, default='y'): + return default == 'y' def can_open_url(self): return True diff --git a/WebKitTools/Scripts/webkitpy/tool/multicommandtool.py b/WebKitTools/Scripts/webkitpy/tool/multicommandtool.py index 12ede2e..4848ae5 100644 --- a/WebKitTools/Scripts/webkitpy/tool/multicommandtool.py +++ b/WebKitTools/Scripts/webkitpy/tool/multicommandtool.py @@ -53,7 +53,7 @@ class Command(object): self.required_arguments = self._parse_required_arguments(argument_names) self.options = options self.requires_local_commits = requires_local_commits - self.tool = None + self._tool = None # option_parser can be overriden by the tool using set_option_parser # This default parser will be used for standalone_help printing. self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options) @@ -73,9 +73,9 @@ class Command(object): # The tool calls bind_to_tool on each Command after adding it to its list. def bind_to_tool(self, tool): # Command instances can only be bound to one tool at a time. - if self.tool and tool != self.tool: + if self._tool and tool != self._tool: raise Exception("Command already bound to tool!") - self.tool = tool + self._tool = tool @staticmethod def _parse_required_arguments(argument_names): @@ -179,17 +179,17 @@ class HelpCommand(AbstractDeclarativeCommand): # Only show commands which are relevant to this checkout's SCM system. Might this be confusing to some users? if self.show_all_commands: epilog = "All %prog commands:\n" - relevant_commands = self.tool.commands[:] + relevant_commands = self._tool.commands[:] else: epilog = "Common %prog commands:\n" - relevant_commands = filter(self.tool.should_show_in_main_help, self.tool.commands) + relevant_commands = filter(self._tool.should_show_in_main_help, self._tool.commands) longest_name_length = max(map(lambda command: len(command.name), relevant_commands)) relevant_commands.sort(lambda a, b: cmp(a.name, b.name)) command_help_texts = map(lambda command: " %s %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands) epilog += "%s\n" % "".join(command_help_texts) epilog += "See '%prog help --all-commands' to list all commands.\n" epilog += "See '%prog help COMMAND' for more information on a specific command.\n" - return epilog.replace("%prog", self.tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name(). + return epilog.replace("%prog", self._tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name(). # FIXME: This is a hack so that we don't show --all-commands as a global option: def _remove_help_options(self): @@ -198,7 +198,7 @@ class HelpCommand(AbstractDeclarativeCommand): def execute(self, options, args, tool): if args: - command = self.tool.command_by_name(args[0]) + command = self._tool.command_by_name(args[0]) if command: print command.standalone_help() return 0 |