diff options
author | Steve Block <steveblock@google.com> | 2010-02-15 12:23:52 +0000 |
---|---|---|
committer | Steve Block <steveblock@google.com> | 2010-02-16 11:48:32 +0000 |
commit | 8a0914b749bbe7da7768e07a7db5c6d4bb09472b (patch) | |
tree | 73f9065f370435d6fde32ae129d458a8c77c8dff /WebKitTools/Scripts | |
parent | bf14be70295513b8076f3fa47a268a7e42b2c478 (diff) | |
download | external_webkit-8a0914b749bbe7da7768e07a7db5c6d4bb09472b.zip external_webkit-8a0914b749bbe7da7768e07a7db5c6d4bb09472b.tar.gz external_webkit-8a0914b749bbe7da7768e07a7db5c6d4bb09472b.tar.bz2 |
Merge webkit.org at r54731 : Initial merge by git
Change-Id: Ia79977b6cf3b0b00c06ef39419989b28e57e4f4a
Diffstat (limited to 'WebKitTools/Scripts')
57 files changed, 3487 insertions, 2155 deletions
diff --git a/WebKitTools/Scripts/build-webkit b/WebKitTools/Scripts/build-webkit index 8171fba..5ae1aae 100755 --- a/WebKitTools/Scripts/build-webkit +++ b/WebKitTools/Scripts/build-webkit @@ -36,6 +36,7 @@ use FindBin; use Getopt::Long qw(:config pass_through); use lib $FindBin::Bin; use webkitdirs; +use webkitperl::features; use POSIX; sub formatBuildTime($); @@ -52,7 +53,7 @@ my $startTime = time(); my ($threeDCanvasSupport, $threeDRenderingSupport, $channelMessagingSupport, $clientBasedGeolocationSupport, $databaseSupport, $datagridSupport, $datalistSupport, $domStorageSupport, $eventsourceSupport, $filtersSupport, $geolocationSupport, $iconDatabaseSupport, $indexedDatabaseSupport, - $javaScriptDebuggerSupport, $mathmlSupport, $offlineWebApplicationSupport, $sharedWorkersSupport, + $javaScriptDebuggerSupport, $mathmlSupport, $offlineWebApplicationSupport, $rubySupport, $sharedWorkersSupport, $svgSupport, $svgAnimationSupport, $svgAsImageSupport, $svgDOMObjCBindingsSupport, $svgFontsSupport, $svgForeignObjectSupport, $svgUseSupport, $videoSupport, $webSocketsSupport, $wmlSupport, $wcssSupport, $xhtmlmpSupport, $workersSupport, $xpathSupport, $xsltSupport, $coverageSupport, $notificationsSupport); @@ -112,6 +113,9 @@ my @features = ( { option => "offline-web-applications", desc => "Toggle Offline Web Application Support", define => "ENABLE_OFFLINE_WEB_APPLICATIONS", default => 1, value => \$offlineWebApplicationSupport }, + { option => "ruby", desc => "Toggle HTML5 Ruby support", + define => "ENABLE_RUBY", default => 1, value => \$rubySupport }, + { option => "shared-workers", desc => "Toggle SharedWorkers support", define => "ENABLE_SHARED_WORKERS", default => (isAppleWebKit() || isGtk()), value => \$sharedWorkersSupport }, @@ -169,6 +173,9 @@ if (isQt()) { } } +# Additional environment parameters +push @ARGV, split(/ /, $ENV{'BUILD_WEBKIT_ARGS'}) if ($ENV{'BUILD_WEBKIT_ARGS'}); + # Initialize values from defaults foreach (@ARGV) { if ($_ eq '--minimal') { @@ -316,7 +323,7 @@ if (isGtk()) { } # Force re-link of existing libraries if different than expected -removeLibraryDependingOnSVG("WebCore", $svgSupport); +removeLibraryDependingOnFeature("WebCore", "SVG", $svgSupport); if (isInspectorFrontend()) { exit exitStatus(copyInspectorFrontendFiles()); diff --git a/WebKitTools/Scripts/check-for-global-initializers b/WebKitTools/Scripts/check-for-global-initializers index a74f57d..cf83048 100755 --- a/WebKitTools/Scripts/check-for-global-initializers +++ b/WebKitTools/Scripts/check-for-global-initializers @@ -107,7 +107,7 @@ for my $file (sort @files) { next if $shortName eq "Node.o"; next if $shortName eq "Page.o"; next if $shortName eq "Range.o"; - next if $shortName eq "RenderBlockLineLayout.o"; + next if $shortName eq "BidiRun.o"; next if $shortName eq "RenderObject.o"; next if $shortName eq "SubresourceLoader.o"; next if $shortName eq "SVGElementInstance.o"; diff --git a/WebKitTools/Scripts/check-for-weak-vtables b/WebKitTools/Scripts/check-for-weak-vtables-and-externals index a10a236..a3dc364 100755 --- a/WebKitTools/Scripts/check-for-weak-vtables +++ b/WebKitTools/Scripts/check-for-weak-vtables-and-externals @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. +# Copyright (C) 2006, 2007, 2008, 2010 Apple Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -11,7 +11,7 @@ # 2. 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. -# 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of +# 3. Neither the name of Apple Inc. ("Apple") nor the names of # its contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # @@ -26,10 +26,10 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# "check-for-weak-vtables" script for WebKit Open Source Project +# "check-for-weak-vtables-and-externals" script for WebKit Open Source Project # Intended to be invoked from an Xcode build step to check if there are -# any weak vtables in a target. +# any weak vtables or weak externals in a target. use warnings; use strict; @@ -63,18 +63,27 @@ if (!defined $executablePathAge || !defined $buildTimestampAge || $executablePat next; } my @weakVTableClasses = (); + my @weakExternalSymbols = (); while (<NM>) { if (/^STDOUT:/) { - push @weakVTableClasses, $1 if /weak external vtable for (.*)$/; + # Ignore undefined, RTTI and typeinfo symbols. + next if /\bundefined\b/ or /\b__ZT[IS]/; + + if (/weak external vtable for (.*)$/) { + push @weakVTableClasses, $1; + } elsif (/weak external (.*)$/) { + push @weakExternalSymbols, $1; + } } else { print STDERR if $_ ne "nm: no name list\n"; } } close NM; - if (@weakVTableClasses) { - my $shortName = $executablePath; - $shortName =~ s/.*\///; + my $shortName = $executablePath; + $shortName =~ s/.*\///; + + if (@weakVTableClasses) { print "ERROR: $shortName has a weak vtable in it ($executablePath)\n"; print "ERROR: Fix by making sure the first virtual function in each of these classes is not an inline:\n"; for my $class (sort @weakVTableClasses) { @@ -82,6 +91,16 @@ if (!defined $executablePathAge || !defined $buildTimestampAge || $executablePat } $sawError = 1; } + + if (@weakExternalSymbols) { + print "ERROR: $shortName has a weak external symbol in it ($executablePath)\n"; + print "ERROR: A weak external symbol is generated when a symbol is defined in multiple compilation units and is also marked as being exported from the library.\n"; + print "ERROR: A common cause of weak external symbols is when an inline function is listed in the linker export file.\n"; + for my $symbol (sort @weakExternalSymbols) { + print "ERROR: symbol $symbol\n"; + } + $sawError = 1; + } } if ($sawError and !$coverageBuild) { diff --git a/WebKitTools/Scripts/rebaseline-chromium-webkit-tests b/WebKitTools/Scripts/rebaseline-chromium-webkit-tests index 9a8a156..aea0edc 100755 --- a/WebKitTools/Scripts/rebaseline-chromium-webkit-tests +++ b/WebKitTools/Scripts/rebaseline-chromium-webkit-tests @@ -11,7 +11,7 @@ # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. -# * Neither the Chromium name nor the names of its +# * 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. # diff --git a/WebKitTools/Scripts/run-chromium-webkit-tests b/WebKitTools/Scripts/run-chromium-webkit-tests index 9c027b8..221b5aa 100755 --- a/WebKitTools/Scripts/run-chromium-webkit-tests +++ b/WebKitTools/Scripts/run-chromium-webkit-tests @@ -11,7 +11,7 @@ # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. -# * Neither the Chromium name nor the names of its +# * 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. # @@ -33,6 +33,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "webkitpy", "layout_tests")) +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))) import run_chromium_webkit_tests if __name__ == '__main__': diff --git a/WebKitTools/Scripts/run-drawtest b/WebKitTools/Scripts/run-drawtest deleted file mode 100755 index 2cd61de..0000000 --- a/WebKitTools/Scripts/run-drawtest +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/perl -w - -# Copyright (C) 2005 Apple Computer, 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: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. 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. -# 3. Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE AND ITS 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 APPLE OR ITS 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. - -# Simplified "run" script for WebKit Open Source Project. - -use strict; -use FindBin; -use lib $FindBin::Bin; -use webkitdirs; - -setConfiguration(); -my $productDir = productDir(); - -# Check to see that all the frameworks are built (w/ SVG support). -checkFrameworks(); -checkWebCoreSVGSupport(1); - -# Set up DYLD_FRAMEWORK_PATH to point to the product directory. -print "Start DrawTest with DYLD_FRAMEWORK_PATH set to point to built WebKit in $productDir.\n"; -$ENV{DYLD_FRAMEWORK_PATH} = $productDir; -my $drawtestPath = "$productDir/DrawTest.app/Contents/MacOS/DrawTest"; -exec $drawtestPath or die; diff --git a/WebKitTools/Scripts/run-iexploder-tests b/WebKitTools/Scripts/run-iexploder-tests index 1b3976f..7f2f04f 100755 --- a/WebKitTools/Scripts/run-iexploder-tests +++ b/WebKitTools/Scripts/run-iexploder-tests @@ -101,9 +101,13 @@ sub runSafariWithIExploder() print REDIRECT_HTML "</html>\n"; close REDIRECT_HTML; - local %ENV; - $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc; - system "WebKitTools/Scripts/run-safari", "-NSOpen", "$iExploderTestDirectory/redirect.html"; + if (!isAppleWebKit()) { + system "WebKitTools/Scripts/run-launcher", "$iExploderTestDirectory/redirect.html"; + } else { + local %ENV; + $ENV{DYLD_INSERT_LIBRARIES} = "/usr/lib/libgmalloc.dylib" if $guardMalloc; + system "WebKitTools/Scripts/run-safari", "-NSOpen", "$iExploderTestDirectory/redirect.html"; + } } sub configureAndOpenHTTPDIfNeeded() @@ -114,8 +118,9 @@ sub configureAndOpenHTTPDIfNeeded() my $webkitDirectory = getcwd(); my $testDirectory = $webkitDirectory . "/LayoutTests"; my $iExploderDirectory = $webkitDirectory . "/WebKitTools/iExploder"; - my $httpdConfig = "$testDirectory/http/conf/httpd.conf"; - $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|; + + my $httpdConfig = getHTTPDConfigPathForTestDirectory($testDirectory); + my $documentRoot = "$iExploderDirectory/htdocs"; my $typesConfig = "$testDirectory/http/conf/mime.types"; my $sslCertificate = "$testDirectory/http/conf/webkit-httpd.pem"; diff --git a/WebKitTools/Scripts/run-webkit-tests b/WebKitTools/Scripts/run-webkit-tests index 6b21e48..809e078 100755 --- a/WebKitTools/Scripts/run-webkit-tests +++ b/WebKitTools/Scripts/run-webkit-tests @@ -67,6 +67,7 @@ use Time::HiRes qw(time usleep); use List::Util 'shuffle'; use lib $FindBin::Bin; +use webkitperl::features; use webkitperl::httpd; use webkitdirs; use VCSUtils; @@ -154,9 +155,15 @@ my $treatSkipped = "default"; my $useRemoteLinksToTests = 0; my $useValgrind = 0; my $verbose = 0; +my $shouldWaitForHTTPD = 0; my @leaksFilenames; +if (isWindows() || isMsys()) { + print "This script has to be run under Cygwin to function correctly.\n"; + exit 1; +} + # Default to --no-http for wx for now. $testHTTP = 0 if (isWx()); @@ -224,6 +231,7 @@ Usage: $programName [options] [testdir|testpath ...] --exit-after-n-failures N Exit after the first N failures instead of running all tests -h|--help Show this help message --[no-]http Run (or do not run) http tests (default: $httpDefault) + --[no-]wait-for-httpd Wait for httpd if some other test session is using it already (same as WEBKIT_WAIT_FOR_HTTPD=1). (default: $shouldWaitForHTTPD) -i|--ignore-tests Comma-separated list of directories or tests to ignore --iterations n Number of times to run the set of tests (e.g. ABCABCABC) --[no-]launch-safari Launch (or do not launch) Safari to display test results (default: $launchSafariDefault) @@ -267,6 +275,7 @@ my $getOptionsResult = GetOptions( 'guard-malloc|g' => \$guardMalloc, 'help|h' => \$showHelp, 'http!' => \$testHTTP, + 'wait-for-httpd!' => \$shouldWaitForHTTPD, 'ignore-metrics!' => \$ignoreMetrics, 'ignore-tests|i=s' => \$ignoreTests, 'iterations=i' => \$iterations, @@ -308,6 +317,8 @@ my $skippedOnly = $treatSkipped eq "only"; my $configuration = configuration(); +# We need an environment variable to be able to enable the feature per-slave +$shouldWaitForHTTPD = $ENV{"WEBKIT_WAIT_FOR_HTTPD"} unless ($shouldWaitForHTTPD); $verbose = 1 if $testsPerDumpTool == 1; if ($shouldCheckLeaks && $testsPerDumpTool > 1000) { @@ -410,12 +421,12 @@ my %ignoredDirectories = map { $_ => 1 } qw(platform); my %ignoredLocalDirectories = map { $_ => 1 } qw(.svn _svn resources script-tests); my %supportedFileExtensions = map { $_ => 1 } qw(html shtml xml xhtml pl php); -if (!checkWebCoreMathMLSupport(0)) { +if (!checkWebCoreFeatureSupport("MathML", 0)) { $ignoredDirectories{'mathml'} = 1; } -# FIXME: We should fix webkitdirs.pm:hasSVG/WMLSupport() to do the correct feature detection for Cygwin. -if (checkWebCoreSVGSupport(0)) { +# FIXME: We should fix webkitperl/features.pm:hasFeature() to do the correct feature detection for Cygwin. +if (checkWebCoreFeatureSupport("SVG", 0)) { $supportedFileExtensions{'svg'} = 1; } elsif (isCygwin()) { $supportedFileExtensions{'svg'} = 1; @@ -433,20 +444,20 @@ if (!$testMedia) { $ignoredDirectories{'http/tests/media'} = 1; } -if (!checkWebCoreAcceleratedCompositingSupport(0)) { +if (!checkWebCoreFeatureSupport("Accelerated Compositing", 0)) { $ignoredDirectories{'compositing'} = 1; } -if (!checkWebCore3DRenderingSupport(0)) { +if (!checkWebCoreFeatureSupport("3D Rendering", 0)) { $ignoredDirectories{'animations/3d'} = 1; $ignoredDirectories{'transforms/3d'} = 1; } -if (!checkWebCore3DCanvasSupport(0)) { +if (!checkWebCoreFeatureSupport("3D Canvas", 0)) { $ignoredDirectories{'fast/canvas/webgl'} = 1; } -if (checkWebCoreWMLSupport(0)) { +if (checkWebCoreFeatureSupport("WML", 0)) { $supportedFileExtensions{'wml'} = 1; } else { $ignoredDirectories{'http/tests/wml'} = 1; @@ -454,14 +465,10 @@ if (checkWebCoreWMLSupport(0)) { $ignoredDirectories{'wml'} = 1; } -if (!checkWebCoreXHTMLMPSupport(0)) { +if (!checkWebCoreFeatureSupport("XHTMLMP", 0)) { $ignoredDirectories{'fast/xhtmlmp'} = 1; } -if (!checkWebCoreWCSSSupport(0)) { - $ignoredDirectories{'fast/wcss'} = 1; -} - processIgnoreTests($ignoreTests, "ignore-tests") if $ignoreTests; if (!$ignoreSkipped) { if (!$skippedOnly || @ARGV == 0) { @@ -943,7 +950,14 @@ for my $test (@tests) { } } } -printf "\n%0.2fs total testing time\n", (time - $overallStartTime) . ""; +my $totalTestingTime = time - $overallStartTime; +my $waitTime = getWaitTime(); +if ($waitTime > 0.1) { + my $normalizedTestingTime = $totalTestingTime - $waitTime; + printf "\n%0.2fs HTTPD waiting time\n", $waitTime . ""; + printf "%0.2fs normalized testing time", $normalizedTestingTime . ""; +} +printf "\n%0.2fs total testing time\n", $totalTestingTime . ""; !$isDumpToolOpen || die "Failed to close $dumpToolName.\n"; @@ -1360,6 +1374,7 @@ sub configureAndOpenHTTPDIfNeeded() my @defaultArgs = getDefaultConfigForTestDirectory($testDirectory); @args = (@defaultArgs, @args); + waitForHTTPDLock() if $shouldWaitForHTTPD; $isHttpdOpen = openHTTPD(@args); } @@ -1383,6 +1398,7 @@ sub openWebSocketServerIfNeeded() "-d", "$webSocketHandlerDir", "-s", "$webSocketHandlerScanDir", "-m", "$webSocketHandlerMapFile", + "-x", "/websocket/tests/cookies", "-l", "$logFile", "--strict", ); diff --git a/WebKitTools/Scripts/run-webkit-websocketserver b/WebKitTools/Scripts/run-webkit-websocketserver index bbc5af6..64a724d 100755 --- a/WebKitTools/Scripts/run-webkit-websocketserver +++ b/WebKitTools/Scripts/run-webkit-websocketserver @@ -75,6 +75,7 @@ sub openWebSocketServer() "-d", "$webSocketHandlerDir", "-s", "$webSocketHandlerScanDir", "-m", "$webSocketHandlerMapFile", + "-x", "/websocket/tests/cookies", ); $ENV{"PYTHONPATH"} = $webSocketPythonPath; diff --git a/WebKitTools/Scripts/test-webkitpy b/WebKitTools/Scripts/test-webkitpy index ca58b50..cfd3434 100755 --- a/WebKitTools/Scripts/test-webkitpy +++ b/WebKitTools/Scripts/test-webkitpy @@ -43,6 +43,7 @@ from webkitpy.committers_unittest import * from webkitpy.credentials_unittest import * from webkitpy.diff_parser_unittest import * from webkitpy.executive_unittest import * +from webkitpy.grammar_unittest import * from webkitpy.multicommandtool_unittest import * from webkitpy.networktransaction_unittest import * from webkitpy.patchcollection_unittest import * @@ -51,6 +52,7 @@ from webkitpy.steps.steps_unittest import * from webkitpy.steps.closebugforlanddiff_unittest import * from webkitpy.steps.updatechangelogswithreview_unittests import * from webkitpy.style.unittests import * # for check-webkit-style +from webkitpy.user_unittest import * from webkitpy.webkit_logging_unittest import * from webkitpy.webkitport_unittest import * diff --git a/WebKitTools/Scripts/build-drawtest b/WebKitTools/Scripts/webkit-build-directory index fa9b7c2..a85c587 100755 --- a/WebKitTools/Scripts/build-drawtest +++ b/WebKitTools/Scripts/webkit-build-directory @@ -1,6 +1,6 @@ -#!/usr/bin/perl -w +#!/usr/bin/perl -# Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved. +# 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 @@ -26,23 +26,40 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# Simplified build script for WebKit Open Source Project. -# Modified copy of build-dumprendertree. Perhaps these could share code. +# A script to expose WebKit's build directory detection logic to non-perl scripts. -use strict; use FindBin; +use Getopt::Long; + use lib $FindBin::Bin; use webkitdirs; -checkRequiredSystemConfig(); -setConfiguration(); -chdirWebKit(); -my @options = XcodeOptions(); +my $showBaseProductDirectory = 0; +my $showHelp = 0; + +my $programName = basename($0); +my $usage = <<EOF; +Usage: $programName [options] + --base Show the root build directory instead of one corresponding to the current target (e.g. Debug, Release) + --debug Show build directory for the Debug target + -h|--help Show this help message + --release Show build directory for the Release target +EOF + +setConfiguration(); # Figure out from the command line if we're --debug or --release or the default. + +my $getOptionsResult = GetOptions( + 'base' => \$showBaseProductDirectory, + 'help|h' => \$showHelp, +); -# Check to see that all the frameworks are built (w/ SVG support). -checkFrameworks(); -checkWebCoreSVGSupport(1); +if (!$getOptionsResult || $showHelp) { + print STDERR $usage; + exit 1; +} -# Build -chdir "WebKitTools/DrawTest" or die; -exit system "xcodebuild", "-project", "DrawTest.xcodeproj", @options; +if ($showBaseProductDirectory) { + print baseProductDir() . "\n"; +} else { + print productDir() . "\n"; +} diff --git a/WebKitTools/Scripts/webkitdirs.pm b/WebKitTools/Scripts/webkitdirs.pm index 8b2ecc1..a788b3d 100644 --- a/WebKitTools/Scripts/webkitdirs.pm +++ b/WebKitTools/Scripts/webkitdirs.pm @@ -573,222 +573,17 @@ sub builtDylibPathForName } # Check to see that all the frameworks are built. -sub checkFrameworks +sub checkFrameworks # FIXME: This is a poor name since only the Mac calls built WebCore a Framework. { return if isCygwin(); my @frameworks = ("JavaScriptCore", "WebCore"); - push(@frameworks, "WebKit") if isAppleMacWebKit(); + push(@frameworks, "WebKit") if isAppleMacWebKit(); # FIXME: This seems wrong, all ports should have a WebKit these days. for my $framework (@frameworks) { my $path = builtDylibPathForName($framework); die "Can't find built framework at \"$path\".\n" unless -e $path; } } -sub libraryContainsSymbol -{ - my $path = shift; - my $symbol = shift; - - if (isCygwin() or isWindows()) { - # FIXME: Implement this for Windows. - return 0; - } - - my $foundSymbol = 0; - if (-e $path) { - open NM, "-|", "nm", $path or die; - while (<NM>) { - $foundSymbol = 1 if /$symbol/; - } - close NM; - } - return $foundSymbol; -} - -sub hasMathMLSupport -{ - my $path = shift; - - return libraryContainsSymbol($path, "MathMLElement"); -} - -sub checkWebCoreMathMLSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $hasMathML = hasMathMLSupport($path); - if ($required && !$hasMathML) { - die "$framework at \"$path\" does not include MathML Support, please run build-webkit --mathml\n"; - } - return $hasMathML; -} - -sub hasSVGSupport -{ - my $path = shift; - - if (isWx()) { - return 0; - } - - # We used to look for SVGElement but isSVGElement is a valid symbol in --no-svg builds. - return libraryContainsSymbol($path, "SVGDefsElement"); -} - -sub removeLibraryDependingOnSVG -{ - my $frameworkName = shift; - my $shouldHaveSVG = shift; - - my $path = builtDylibPathForName($frameworkName); - return unless -x $path; - - my $hasSVG = hasSVGSupport($path); - system "rm -f $path" if ($shouldHaveSVG xor $hasSVG); -} - -sub checkWebCoreSVGSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $hasSVG = hasSVGSupport($path); - if ($required && !$hasSVG) { - die "$framework at \"$path\" does not include SVG Support, please run build-webkit --svg\n"; - } - return $hasSVG; -} - -sub hasAcceleratedCompositingSupport -{ - # On platforms other than Mac the Skipped files are used to skip compositing tests - return 1 if !isAppleMacWebKit(); - - my $path = shift; - return libraryContainsSymbol($path, "GraphicsLayer"); -} - -sub checkWebCoreAcceleratedCompositingSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $hasAcceleratedCompositing = hasAcceleratedCompositingSupport($path); - if ($required && !$hasAcceleratedCompositing) { - die "$framework at \"$path\" does not use accelerated compositing\n"; - } - return $hasAcceleratedCompositing; -} - -sub has3DRenderingSupport -{ - # On platforms other than Mac the Skipped files are used to skip 3D tests - return 1 if !isAppleMacWebKit(); - - my $path = shift; - return libraryContainsSymbol($path, "WebCoreHas3DRendering"); -} - -sub checkWebCore3DRenderingSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $has3DRendering = has3DRenderingSupport($path); - if ($required && !$has3DRendering) { - die "$framework at \"$path\" does not include 3D rendering Support, please run build-webkit --3d-rendering\n"; - } - return $has3DRendering; -} - -sub has3DCanvasSupport -{ - return 0 if isQt(); - - my $path = shift; - return libraryContainsSymbol($path, "WebGLShader"); -} - -sub checkWebCore3DCanvasSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $has3DCanvas = has3DCanvasSupport($path); - if ($required && !$has3DCanvas) { - die "$framework at \"$path\" does not include 3D Canvas Support, please run build-webkit --3d-canvas\n"; - } - return $has3DCanvas; -} - -sub hasWMLSupport -{ - my $path = shift; - return libraryContainsSymbol($path, "WMLElement"); -} - -sub removeLibraryDependingOnWML -{ - my $frameworkName = shift; - my $shouldHaveWML = shift; - - my $path = builtDylibPathForName($frameworkName); - return unless -x $path; - - my $hasWML = hasWMLSupport($path); - system "rm -f $path" if ($shouldHaveWML xor $hasWML); -} - -sub checkWebCoreWMLSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $hasWML = hasWMLSupport($path); - if ($required && !$hasWML) { - die "$framework at \"$path\" does not include WML Support, please run build-webkit --wml\n"; - } - return $hasWML; -} - -sub hasXHTMLMPSupport -{ - my $path = shift; - return libraryContainsSymbol($path, "isXHTMLMPDocument"); -} - -sub checkWebCoreXHTMLMPSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $hasXHTMLMP = hasXHTMLMPSupport($path); - if ($required && !$hasXHTMLMP) { - die "$framework at \"$path\" does not include XHTML MP Support\n"; - } - return $hasXHTMLMP; -} - -sub hasWCSSSupport -{ - # FIXME: When WCSS support is landed this should be updated to check for WCSS - # being enabled in a manner similar to how we check for XHTML MP above. - return 0; -} - -sub checkWebCoreWCSSSupport -{ - my $required = shift; - my $framework = "WebCore"; - my $path = builtDylibPathForName($framework); - my $hasWCSS = hasWCSSSupport($path); - if ($required && !$hasWCSS) { - die "$framework at \"$path\" does not include WCSS Support\n"; - } - return $hasWCSS; -} - sub isInspectorFrontend() { determineIsInspectorFrontend(); @@ -954,6 +749,11 @@ sub isWindows() return ($^O eq "MSWin32") || 0; } +sub isMsys() +{ + return ($^O eq "msys") || 0; +} + sub isLinux() { return ($^O eq "linux") || 0; @@ -1107,7 +907,7 @@ sub determineWindowsSourceDir() { return if $windowsSourceDir; my $sourceDir = sourceDir(); - chomp($windowsSourceDir = `cygpath -w $sourceDir`); + chomp($windowsSourceDir = `cygpath -w '$sourceDir'`); } sub windowsSourceDir() @@ -1491,27 +1291,21 @@ sub buildQMakeProject($@) my $dsMakefile = "Makefile.DerivedSources"; - print "Calling '$make $makeargs -f $dsMakefile generated_files' in " . $dir . "/JavaScriptCore\n\n"; - if ($make eq "nmake") { - $result = system "pushd JavaScriptCore && $make $makeargs -f $dsMakefile generated_files && popd"; - } else { - $result = system "$make $makeargs -C JavaScriptCore -f $dsMakefile generated_files"; - } - if ($result ne 0) { - die "Failed to generate JavaScriptCore's derived sources!\n"; - } - - print "Calling '$make $makeargs -f $dsMakefile generated_files' in " . $dir . "/WebCore\n\n"; - if ($make eq "nmake") { - $result = system "pushd WebCore && $make $makeargs -f $dsMakefile generated_files && popd"; - } else { - $result = system "$make $makeargs -C WebCore -f $dsMakefile generated_files"; - } - if ($result ne 0) { - die "Failed to generate WebCore's derived sources!\n"; + # Iterate over different source directories manually to workaround a problem with qmake+extraTargets+s60 + for my $subdir ("JavaScriptCore", "WebCore", "WebKit/qt/Api") { + print "Calling '$make $makeargs -f $dsMakefile generated_files' in " . $dir . "/$subdir\n\n"; + if ($make eq "nmake") { + my $subdirWindows = $subdir; + $subdirWindows =~ s:/:\\:g; + $result = system "pushd $subdirWindows && $make $makeargs -f $dsMakefile generated_files && popd"; + } else { + $result = system "$make $makeargs -C $subdir -f $dsMakefile generated_files"; + } + if ($result ne 0) { + die "Failed to generate ${subdir}'s derived sources!\n"; + } } - push @buildArgs, "OUTPUT_DIR=" . baseProductDir() . "/$config"; push @buildArgs, sourceDir() . "/WebKit.pro"; if ($config =~ m/debug/i) { diff --git a/WebKitTools/Scripts/webkitperl/features.pm b/WebKitTools/Scripts/webkitperl/features.pm new file mode 100644 index 0000000..1f88022 --- /dev/null +++ b/WebKitTools/Scripts/webkitperl/features.pm @@ -0,0 +1,104 @@ +# Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved +# Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com) +# Copyright (C) 2010 Andras Becsi (abecsi@inf.u-szeged.hu), University of Szeged +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# 3. Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE AND ITS 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 APPLE OR ITS 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. + +# Module to share code to detect the existance of features in built binaries. + +use strict; +use warnings; + +BEGIN { + use Exporter (); + our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); + $VERSION = 1.00; + @ISA = qw(Exporter); + @EXPORT = qw(&checkWebCoreFeatureSupport + &removeLibraryDependingOnFeature); + %EXPORT_TAGS = ( ); + @EXPORT_OK = (); +} + +sub libraryContainsSymbol($$) +{ + my ($path, $symbol) = @_; + + if (isCygwin() or isWindows()) { + # FIXME: Implement this for Windows. + return 0; + } + + my $foundSymbol = 0; + if (-e $path) { + open NM, "-|", "nm", $path or die; + while (<NM>) { + $foundSymbol = 1 if /$symbol/; # FIXME: This should probably check for word boundaries before/after the symbol name. + } + close NM; + } + return $foundSymbol; +} + +sub hasFeature($$) +{ + my ($featureName, $path) = @_; + my %symbolForFeature = ( + "MathML" => "MathMLElement", + "SVG" => "SVGDefsElement", # We used to look for SVGElement but isSVGElement exists (and would match) in --no-svg builds. + "Accelerated Compositing" => "GraphicsLayer", + "3D Rendering" => "WebCoreHas3DRendering", + "3D Canvas" => "WebGLShader", + "WML" => "WMLElement", + "XHTMLMP" => "isXHTMLMPDocument", + ); + my $symbolName = $symbolForFeature{$featureName}; + die "Unknown feature: $featureName" unless $symbolName; + return libraryContainsSymbol($path, $symbolName); +} + +sub checkWebCoreFeatureSupport($$) +{ + my ($feature, $required) = @_; + my $libraryName = "WebCore"; + my $path = builtDylibPathForName($libraryName); + my $hasFeature = hasFeature($feature, $path); + if ($required && !$hasFeature) { + die "$libraryName at \"$path\" does not include $hasFeature support. See build-webkit --help\n"; + } + return $hasFeature; +} + +sub removeLibraryDependingOnFeature($$$) +{ + my ($libraryName, $featureName, $shouldHaveFeature) = @_; + my $path = builtDylibPathForName($libraryName); + return unless -x $path; + + my $hasFeature = hasFeature($featureName, $path); + system "rm -f $path" if ($shouldHaveFeature xor $hasFeature); +} + +1; diff --git a/WebKitTools/Scripts/webkitperl/httpd.pm b/WebKitTools/Scripts/webkitperl/httpd.pm index d082870..05eb21c 100644 --- a/WebKitTools/Scripts/webkitperl/httpd.pm +++ b/WebKitTools/Scripts/webkitperl/httpd.pm @@ -34,6 +34,7 @@ use warnings; use File::Path; use File::Spec; use File::Spec::Functions; +use Fcntl ':flock'; use IPC::Open2; use webkitdirs; @@ -43,20 +44,32 @@ BEGIN { our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); $VERSION = 1.00; @ISA = qw(Exporter); - @EXPORT = qw(&getHTTPDPath &getDefaultConfigForTestDirectory &openHTTPD &closeHTTPD &getHTTPDPid &setShouldWaitForUserInterrupt); + @EXPORT = qw(&getHTTPDPath + &getHTTPDConfigPathForTestDirectory + &getDefaultConfigForTestDirectory + &openHTTPD + &closeHTTPD + &setShouldWaitForUserInterrupt + &waitForHTTPDLock + &getWaitTime); %EXPORT_TAGS = ( ); @EXPORT_OK = (); } my $tmpDir = "/tmp"; +my $httpdLockPrefix = "WebKitHttpd.lock."; +my $myLockFile; +my $exclusiveLockFile = File::Spec->catfile($tmpDir, "WebKit.lock"); my $httpdPath; my $httpdPidDir = File::Spec->catfile($tmpDir, "WebKit"); my $httpdPidFile = File::Spec->catfile($httpdPidDir, "httpd.pid"); my $httpdPid; my $waitForUserInterrupt = 0; +my $waitBeginTime; +my $waitEndTime; -$SIG{'INT'} = 'cleanup'; -$SIG{'TERM'} = 'cleanup'; +$SIG{'INT'} = 'handleInterrupt'; +$SIG{'TERM'} = 'handleInterrupt'; sub getHTTPDPath { @@ -73,24 +86,7 @@ sub getDefaultConfigForTestDirectory my ($testDirectory) = @_; die "No test directory has been specified." unless ($testDirectory); - my $httpdConfig; - getHTTPDPath(); - if (isCygwin()) { - my $windowsConfDirectory = "$testDirectory/http/conf/"; - unless (-x "/usr/lib/apache/libphp4.dll") { - copy("$windowsConfDirectory/libphp4.dll", "/usr/lib/apache/libphp4.dll"); - chmod(0755, "/usr/lib/apache/libphp4.dll"); - } - $httpdConfig = "$windowsConfDirectory/cygwin-httpd.conf"; - } elsif (isDebianBased()) { - $httpdConfig = "$testDirectory/http/conf/apache2-debian-httpd.conf"; - } elsif (isFedoraBased()) { - $httpdConfig = "$testDirectory/http/conf/fedora-httpd.conf"; - } else { - $httpdConfig = "$testDirectory/http/conf/httpd.conf"; - $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|; - } - + my $httpdConfig = getHTTPDConfigPathForTestDirectory($testDirectory); my $documentRoot = "$testDirectory/http/tests"; my $jsTestResourcesDirectory = $testDirectory . "/fast/js/resources"; my $typesConfig = "$testDirectory/http/conf/mime.types"; @@ -119,6 +115,30 @@ sub getDefaultConfigForTestDirectory } +sub getHTTPDConfigPathForTestDirectory +{ + my ($testDirectory) = @_; + die "No test directory has been specified." unless ($testDirectory); + my $httpdConfig; + getHTTPDPath(); + if (isCygwin()) { + my $windowsConfDirectory = "$testDirectory/http/conf/"; + unless (-x "/usr/lib/apache/libphp4.dll") { + copy("$windowsConfDirectory/libphp4.dll", "/usr/lib/apache/libphp4.dll"); + chmod(0755, "/usr/lib/apache/libphp4.dll"); + } + $httpdConfig = "$windowsConfDirectory/cygwin-httpd.conf"; + } elsif (isDebianBased()) { + $httpdConfig = "$testDirectory/http/conf/apache2-debian-httpd.conf"; + } elsif (isFedoraBased()) { + $httpdConfig = "$testDirectory/http/conf/fedora-httpd.conf"; + } else { + $httpdConfig = "$testDirectory/http/conf/httpd.conf"; + $httpdConfig = "$testDirectory/http/conf/apache2-httpd.conf" if `$httpdPath -v` =~ m|Apache/2|; + } + return $httpdConfig; +} + sub openHTTPD(@) { my (@args) = @_; @@ -141,7 +161,10 @@ sub openHTTPD(@) --$retryCount; } - die "Timed out waiting for httpd to quit" unless $retryCount; + if (!$retryCount) { + cleanUp(); + die "Timed out waiting for httpd to quit"; + } } } @@ -156,7 +179,7 @@ sub openHTTPD(@) } if (!$retryCount) { - rmtree $httpdPidDir; + cleanUp(); die "Timed out waiting for httpd to start"; } @@ -172,20 +195,19 @@ sub openHTTPD(@) sub closeHTTPD { close HTTPDIN; + my $retryCount = 20; if ($httpdPid) { kill 15, $httpdPid; - my $retryCount = 20; while (-f $httpdPidFile && $retryCount) { sleep 1; --$retryCount; } - - if (!$retryCount) { - print STDERR "Timed out waiting for httpd to terminate!\n"; - return 0; - } } - rmdir $httpdPidDir; + cleanUp(); + if (!$retryCount) { + print STDERR "Timed out waiting for httpd to terminate!\n"; + return 0; + } return 1; } @@ -194,9 +216,94 @@ sub setShouldWaitForUserInterrupt $waitForUserInterrupt = 1; } -sub cleanup +sub handleInterrupt { closeHTTPD(); print "\n"; exit(1); } + +sub cleanUp +{ + rmdir $httpdPidDir; + unlink $exclusiveLockFile; + unlink $myLockFile if $myLockFile; +} + +sub extractLockNumber +{ + my ($lockFile) = @_; + return -1 unless $lockFile; + return substr($lockFile, length($httpdLockPrefix)); +} + +sub getLockFiles +{ + opendir(TMPDIR, $tmpDir) or die "Could not open " . $tmpDir . "."; + my @lockFiles = grep {m/^$httpdLockPrefix\d+$/} readdir(TMPDIR); + @lockFiles = sort { extractLockNumber($a) <=> extractLockNumber($b) } @lockFiles; + closedir(TMPDIR); + return @lockFiles; +} + +sub getNextAvailableLockNumber +{ + my @lockFiles = getLockFiles(); + return 0 unless @lockFiles; + return extractLockNumber($lockFiles[-1]) + 1; +} + +sub getLockNumberForCurrentRunning +{ + my @lockFiles = getLockFiles(); + return 0 unless @lockFiles; + return extractLockNumber($lockFiles[0]); +} + +sub waitForHTTPDLock +{ + $waitBeginTime = time; + scheduleHttpTesting(); + # If we are the only one waiting for Apache just run the tests without any further checking + if (scalar getLockFiles() > 1) { + my $currentLockFile = File::Spec->catfile($tmpDir, "$httpdLockPrefix" . getLockNumberForCurrentRunning()); + my $currentLockPid = <SCHEDULER_LOCK> if (-f $currentLockFile && open(SCHEDULER_LOCK, "<$currentLockFile")); + # Wait until we are allowed to run the http tests + while ($currentLockPid && $currentLockPid != $$) { + $currentLockFile = File::Spec->catfile($tmpDir, "$httpdLockPrefix" . getLockNumberForCurrentRunning()); + if ($currentLockFile eq $myLockFile) { + $currentLockPid = <SCHEDULER_LOCK> if open(SCHEDULER_LOCK, "<$currentLockFile"); + if ($currentLockPid != $$) { + print STDERR "\nPID mismatch.\n"; + last; + } + } else { + sleep 1; + } + } + } + $waitEndTime = time; +} + +sub scheduleHttpTesting +{ + # We need an exclusive lock file to avoid deadlocks and starvation and ensure that the scheduler lock numbers are sequential. + # The scheduler locks are used to schedule the running test sessions in first come first served order. + while (!(open(SEQUENTIAL_GUARD_LOCK, ">$exclusiveLockFile") && flock(SEQUENTIAL_GUARD_LOCK, LOCK_EX|LOCK_NB))) {} + $myLockFile = File::Spec->catfile($tmpDir, "$httpdLockPrefix" . getNextAvailableLockNumber()); + open(SCHEDULER_LOCK, ">$myLockFile"); + print SCHEDULER_LOCK "$$"; + print SEQUENTIAL_GUARD_LOCK "$$"; + close(SCHEDULER_LOCK); + close(SEQUENTIAL_GUARD_LOCK); + unlink $exclusiveLockFile; +} + +sub getWaitTime +{ + my $waitTime = 0; + if ($waitBeginTime && $waitEndTime) { + $waitTime = $waitEndTime - $waitBeginTime; + } + return $waitTime; +} diff --git a/WebKitTools/Scripts/webkitpy/bugzilla.py b/WebKitTools/Scripts/webkitpy/bugzilla.py index c1cf41d..4506af2 100644 --- a/WebKitTools/Scripts/webkitpy/bugzilla.py +++ b/WebKitTools/Scripts/webkitpy/bugzilla.py @@ -39,6 +39,7 @@ from datetime import datetime # used in timestamp() from webkitpy.webkit_logging import error, log from webkitpy.committers import CommitterList from webkitpy.credentials import Credentials +from webkitpy.user import User # WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy # so this import should always succeed. @@ -559,7 +560,7 @@ class Bugzilla(object): for name in components: i += 1 log("%2d. %s" % (i, name)) - result = int(raw_input("Enter a number: ")) - 1 + result = int(User.prompt("Enter a number: ")) - 1 return components[result] def _check_create_bug_response(self, response_html): diff --git a/WebKitTools/Scripts/webkitpy/commands/upload.py b/WebKitTools/Scripts/webkitpy/commands/upload.py index 8d23d8b..15bdfbb 100644 --- a/WebKitTools/Scripts/webkitpy/commands/upload.py +++ b/WebKitTools/Scripts/webkitpy/commands/upload.py @@ -41,10 +41,11 @@ from webkitpy.bugzilla import parse_bug_id from webkitpy.commands.abstractsequencedcommand import AbstractSequencedCommand from webkitpy.comments import bug_comment_from_svn_revision from webkitpy.committers import CommitterList -from webkitpy.grammar import pluralize +from webkitpy.grammar import pluralize, join_with_separators from webkitpy.webkit_logging import error, log from webkitpy.mock import Mock from webkitpy.multicommandtool import AbstractDeclarativeCommand +from webkitpy.user import User class CommitMessageForCurrentDiff(AbstractDeclarativeCommand): name = "commit-message" @@ -54,11 +55,45 @@ class CommitMessageForCurrentDiff(AbstractDeclarativeCommand): os.chdir(tool.scm().checkout_root) print "%s" % tool.scm().commit_message_for_this_commit().message() +class CleanPendingCommit(AbstractDeclarativeCommand): + name = "clean-pending-commit" + help_text = "Clear r+ on obsolete patches so they do not appear in the pending-commit list." + + # NOTE: This was designed to be generic, but right now we're only processing patches from the pending-commit list, so only r+ matters. + def _flags_to_clear_on_patch(self, patch): + if not patch.is_obsolete(): + return None + what_was_cleared = [] + if patch.review() == "+": + if patch.reviewer(): + what_was_cleared.append("%s's review+" % patch.reviewer().full_name) + else: + what_was_cleared.append("review+") + return join_with_separators(what_was_cleared) + + 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) + 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) + class AssignToCommitter(AbstractDeclarativeCommand): name = "assign-to-committer" help_text = "Assign bug to whoever attached the most recent r+'d patch" + def _patches_have_commiters(self, reviewed_patches): + for patch in reviewed_patches: + if not patch.committer(): + return False + return True + def _assign_bug_to_last_patch_attacher(self, bug_id): committers = CommitterList() bug = self.tool.bugs.fetch_bug(bug_id) @@ -71,6 +106,12 @@ class AssignToCommitter(AbstractDeclarativeCommand): if not reviewed_patches: log("Bug %s has no non-obsolete patches, ignoring." % bug_id) return + + # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set). + if self._patches_have_commiters(reviewed_patches): + log("All reviewed patches on bug %s already have commit-queue+, ignoring." % bug_id) + return + latest_patch = reviewed_patches[-1] attacher_email = latest_patch.attacher_email() committer = committers.committer_by_email(attacher_email) @@ -383,7 +424,7 @@ class CreateBug(AbstractDeclarativeCommand): bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit) def prompt_for_bug_title_and_comment(self): - bug_title = raw_input("Bug title: ") + bug_title = User.prompt("Bug title: ") print "Bug comment (hit ^D on blank line to end):" lines = sys.stdin.readlines() try: diff --git a/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py b/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py index 33001ac..7fa8797 100644 --- a/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py +++ b/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py @@ -40,9 +40,12 @@ class UploadCommandsTest(CommandsTest): expected_stdout = "Mock message\n" self.assert_execute_outputs(CommitMessageForCurrentDiff(), [], expected_stdout=expected_stdout, tool=tool) + def test_clean_pending_commit(self): + self.assert_execute_outputs(CleanPendingCommit(), []) + def test_assign_to_committer(self): tool = MockBugzillaTool() - expected_stderr = "Bug 77 is already assigned to foo@foo.com (None).\nBug 76 has no non-obsolete patches, ignoring.\n" + expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\nBug 77 is already assigned to foo@foo.com (None).\nBug 76 has no non-obsolete patches, ignoring.\n" self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool) tool.bugs.reassign_bug.assert_called_with(42, "eric@webkit.org", "Attachment 128 was posted by a committer and has review+, assigning to Eric Seidel for commit.") diff --git a/WebKitTools/Scripts/webkitpy/committers.py b/WebKitTools/Scripts/webkitpy/committers.py index 0efb4e7..7af0987 100644 --- a/WebKitTools/Scripts/webkitpy/committers.py +++ b/WebKitTools/Scripts/webkitpy/committers.py @@ -65,6 +65,7 @@ committers_unable_to_review = [ Committer("Aaron Boodman", "aa@chromium.org"), Committer("Adam Langley", "agl@chromium.org"), Committer("Albert J. Wong", "ajwong@chromium.org"), + Committer("Alejandro G. Castro", ["alex@igalia.com", "alex@webkit.org"]), Committer("Alexander Kellett", ["lypanov@mac.com", "a-lists001@lypanov.net", "lypanov@kde.org"]), Committer("Alexander Pavlov", "apavlov@chromium.org"), Committer("Andre Boule", "aboule@apple.com"), diff --git a/WebKitTools/Scripts/webkitpy/credentials.py b/WebKitTools/Scripts/webkitpy/credentials.py index a4d8e34..295c576 100644 --- a/WebKitTools/Scripts/webkitpy/credentials.py +++ b/WebKitTools/Scripts/webkitpy/credentials.py @@ -37,6 +37,7 @@ import re from webkitpy.executive import Executive, ScriptError from webkitpy.webkit_logging import log from webkitpy.scm import Git +from webkitpy.user import User class Credentials(object): @@ -124,7 +125,7 @@ class Credentials(object): (username, password) = self._credentials_from_keychain(username) if not username: - username = raw_input("%s login: " % self.host) + username = User.prompt("%s login: " % self.host) if not password: password = getpass.getpass("%s password for %s: " % (self.host, username)) diff --git a/WebKitTools/Scripts/webkitpy/grammar.py b/WebKitTools/Scripts/webkitpy/grammar.py index 78809e0..651bbc9 100644 --- a/WebKitTools/Scripts/webkitpy/grammar.py +++ b/WebKitTools/Scripts/webkitpy/grammar.py @@ -43,3 +43,11 @@ def pluralize(noun, count): if count != 1: noun = plural(noun) return "%d %s" % (count, noun) + + +def join_with_separators(list_of_strings, separator=', ', last_separator=', and '): + if not list_of_strings: + return "" + if len(list_of_strings) == 1: + return list_of_strings[0] + return "%s%s%s" % (separator.join(list_of_strings[:-1]), last_separator, list_of_strings[-1]) diff --git a/WebKitTools/Scripts/webkitpy/grammar_unittest.py b/WebKitTools/Scripts/webkitpy/grammar_unittest.py new file mode 100644 index 0000000..3d8b179 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/grammar_unittest.py @@ -0,0 +1,38 @@ +# 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 unittest +from webkitpy.grammar import join_with_separators + +class GrammarTest(unittest.TestCase): + + def test_join_with_separators(self): + self.assertEqual(join_with_separators(["one", "two", "three"]), "one, two, and three") + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/driver_test.py b/WebKitTools/Scripts/webkitpy/layout_tests/driver_test.py new file mode 100644 index 0000000..6e4ba99 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/driver_test.py @@ -0,0 +1,77 @@ +#!/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. +# * 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. + +# +# FIXME: this is a poor attempt at a unit tests driver. We should replace +# this with something that actually uses a unit testing framework or +# at least produces output that could be useful. + +"""Simple test client for the port/Driver interface.""" + +import os +import optparse +import port + + +def run_tests(port, options, tests): + # |image_path| is a path to the image capture from the driver. + image_path = 'image_result.png' + driver = port.start_driver(image_path, None) + for t in tests: + uri = port.filename_to_uri(os.path.join(port.layout_tests_dir(), t)) + print "uri: " + uri + crash, timeout, checksum, output, err = \ + driver.run_test(uri, int(options.timeout), None) + print "crash: " + str(crash) + print "timeout: " + str(timeout) + print "checksum: " + str(checksum) + print 'stdout: """' + print ''.join(output) + print '"""' + print 'stderr: """' + print ''.join(err) + print '"""' + print + + +if __name__ == '__main__': + optparser = optparse.OptionParser() + optparser.add_option('-p', '--platform', action='store', default='mac', + help='Platform to test (e.g., "mac", "chromium-mac", etc.') + optparser.add_option('-t', '--target', action='store', default='Release', + help='build type ("Debug" or "Release")') + optparser.add_option('', '--timeout', action='store', default='2000', + help='test timeout in milliseconds (2000 by default)') + optparser.add_option('', '--wrapper', action='store') + optparser.add_option('', '--no-pixel-tests', action='store_true', + default=False, + help='disable pixel-to-pixel PNG comparisons') + options, args = optparser.parse_args() + p = port.get(options.platform, options) + run_tests(p, options, args) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py index f38a7ab..520ab1f 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py @@ -29,9 +29,9 @@ import logging import os +import simplejson from layout_package import json_results_generator -from port import path_utils from layout_package import test_expectations from layout_package import test_failures @@ -45,7 +45,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator): WONTFIX = "wontfixCounts" DEFERRED = "deferredCounts" - def __init__(self, builder_name, build_name, build_number, + def __init__(self, port, builder_name, build_name, build_number, results_file_base_path, builder_base_url, test_timings, expectations, result_summary, all_tests): """Modifies the results.json file. Grabs it off the archive directory @@ -56,7 +56,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator): results. (see the comment of JSONResultsGenerator.__init__ for other Args) """ - + self._port = port self._builder_name = builder_name self._build_name = build_name self._build_number = build_number @@ -153,7 +153,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator): test, test_name, tests) # Remove tests that don't exist anymore. - full_path = os.path.join(path_utils.layout_tests_dir(), test_name) + full_path = os.path.join(self._port.layout_tests_dir(), test_name) full_path = os.path.normpath(full_path) if not os.path.exists(full_path): del tests[test_name] diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py index dc24ade..84be0e1 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py @@ -29,19 +29,15 @@ import logging import os +import simplejson import subprocess import sys import time import urllib2 import xml.dom.minidom -from port import path_utils from layout_package import test_expectations -sys.path.append(path_utils.path_from_base('third_party', 'WebKit', - 'WebKitTools')) -import simplejson - class JSONResultsGenerator(object): @@ -80,7 +76,7 @@ class JSONResultsGenerator(object): RESULTS_FILENAME = "results.json" - def __init__(self, builder_name, build_name, build_number, + def __init__(self, port, builder_name, build_name, build_number, results_file_base_path, builder_base_url, test_timings, failures, passed_tests, skipped_tests, all_tests): """Modifies the results.json file. Grabs it off the archive directory @@ -100,6 +96,7 @@ class JSONResultsGenerator(object): all_tests: List of all the tests that were run. This should not include skipped tests. """ + self._port = port self._builder_name = builder_name self._build_name = build_name self._build_number = build_number @@ -122,22 +119,24 @@ class JSONResultsGenerator(object): results_file.write(json) results_file.close() - def _get_svn_revision(self, in_directory=None): + def _get_svn_revision(self, in_directory): """Returns the svn revision for the given directory. Args: in_directory: The directory where svn is to be run. """ - output = subprocess.Popen(["svn", "info", "--xml"], - cwd=in_directory, - shell=(sys.platform == 'win32'), - stdout=subprocess.PIPE).communicate()[0] - try: - dom = xml.dom.minidom.parseString(output) - return dom.getElementsByTagName('entry')[0].getAttribute( - 'revision') - except xml.parsers.expat.ExpatError: - return "" + if os.path.exists(os.path.join(in_directory, '.svn')): + output = subprocess.Popen(["svn", "info", "--xml"], + cwd=in_directory, + shell=(sys.platform == 'win32'), + stdout=subprocess.PIPE).communicate()[0] + try: + dom = xml.dom.minidom.parseString(output) + return dom.getElementsByTagName('entry')[0].getAttribute( + 'revision') + except xml.parsers.expat.ExpatError: + return "" + return "" def _get_archived_json_results(self): """Reads old results JSON file if it exists. @@ -305,16 +304,19 @@ class JSONResultsGenerator(object): self._insert_item_into_raw_list(results_for_builder, self._build_number, self.BUILD_NUMBERS) - path_to_webkit = path_utils.path_from_base('third_party', 'WebKit', - 'WebCore') - self._insert_item_into_raw_list(results_for_builder, - self._get_svn_revision(path_to_webkit), - self.WEBKIT_SVN) - - path_to_chrome_base = path_utils.path_from_base() - self._insert_item_into_raw_list(results_for_builder, - self._get_svn_revision(path_to_chrome_base), - self.CHROME_SVN) + # These next two branches test to see which source repos we can + # pull revisions from. + if hasattr(self._port, 'path_from_webkit_base'): + path_to_webkit = self._port.path_from_webkit_base() + self._insert_item_into_raw_list(results_for_builder, + self._get_svn_revision(path_to_webkit), + self.WEBKIT_SVN) + + if hasattr(self._port, 'path_from_chromium_base'): + path_to_chrome = self._port.path_from_chromium_base() + self._insert_item_into_raw_list(results_for_builder, + self._get_svn_revision(path_to_chrome), + self.CHROME_SVN) self._insert_item_into_raw_list(results_for_builder, int(time.time()), 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 5b0d186..a3650ed 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py @@ -36,10 +36,7 @@ import os import re import sys import time -from port import path_utils -sys.path.append(path_utils.path_from_base('third_party', 'WebKit', - 'WebKitTools')) import simplejson # Test expectation and modifier constants. @@ -53,12 +50,28 @@ import simplejson class TestExpectations: TEST_LIST = "test_expectations.txt" - def __init__(self, tests, directory, platform, is_debug_mode, is_lint_mode, - tests_are_present=True): - """Reads the test expectations files from the given directory.""" - path = os.path.join(directory, self.TEST_LIST) - self._expected_failures = TestExpectationsFile(path, tests, platform, - is_debug_mode, is_lint_mode, tests_are_present=tests_are_present) + def __init__(self, port, tests, expectations, test_platform_name, + is_debug_mode, is_lint_mode, tests_are_present=True): + """Loads and parses the test expectations given in the string. + Args: + port: handle to object containing platform-specific functionality + test: list of all of the test files + expectations: test expectations as a string + test_platform_name: name of the platform to match expectations + against. Note that this may be different than + port.test_platform_name() when is_lint_mode is True. + is_debug_mode: whether to use the DEBUG or RELEASE modifiers + 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. + """ + self._expected_failures = TestExpectationsFile(port, expectations, + tests, test_platform_name, is_debug_mode, is_lint_mode, + tests_are_present=tests_are_present) # TODO(ojan): Allow for removing skipped tests when getting the list of # tests to run, but not when getting metrics. @@ -230,9 +243,6 @@ class TestExpectationsFile: EXPECTATION_ORDER = (PASS, CRASH, TIMEOUT, MISSING, IMAGE_PLUS_TEXT, TEXT, IMAGE, FAIL, SKIP) - BASE_PLATFORMS = ('linux', 'mac', 'win') - PLATFORMS = BASE_PLATFORMS + ('win-xp', 'win-vista', 'win-7') - BUILD_TYPES = ('debug', 'release') MODIFIERS = {'skip': SKIP, @@ -251,37 +261,34 @@ class TestExpectationsFile: 'fail': FAIL, 'flaky': FLAKY} - def __init__(self, path, full_test_list, platform, is_debug_mode, - is_lint_mode, expectations_as_str=None, suppress_errors=False, + def __init__(self, port, expectations, full_test_list, test_platform_name, + is_debug_mode, is_lint_mode, suppress_errors=False, tests_are_present=True): """ - path: The path to the expectation file. An error is thrown if a test is - listed more than once. + expectations: Contents of the expectations file full_test_list: The list of all tests to be run pending processing of the expections for those tests. - platform: Which platform from self.PLATFORMS to filter tests for. + test_platform_name: name of the platform to match expectations + against. Note that this may be different than + port.test_platform_name() when is_lint_mode is True. is_debug_mode: Whether we testing a test_shell built debug mode. is_lint_mode: Whether this is just linting test_expecatations.txt. - expectations_as_str: Contents of the expectations file. Used instead of - the path. This makes unittesting sane. 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. """ - self._path = path - self._expectations_as_str = expectations_as_str + self._port = port + self._expectations = expectations + self._full_test_list = full_test_list + 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._full_test_list = full_test_list self._suppress_errors = suppress_errors self._errors = [] self._non_fatal_errors = [] - self._platform = self.to_test_platform_name(platform) - if self._platform is None: - raise Exception("Unknown platform '%s'" % (platform)) - self._is_debug_mode = is_debug_mode # Maps relative test paths as listed in the expectations file to a # list of maps containing modifiers and expectations for each time @@ -320,27 +327,13 @@ class TestExpectationsFile: """Returns an object that can be iterated over. Allows for not caring about whether we're iterating over a file or a new-line separated string.""" - if self._expectations_as_str: - iterable = [x + "\n" for x in - self._expectations_as_str.split("\n")] - # Strip final entry if it's empty to avoid added in an extra - # newline. - if iterable[len(iterable) - 1] == "\n": - return iterable[:len(iterable) - 1] - return iterable - else: - return open(self._path) - - def to_test_platform_name(self, name): - """Returns the test expectation platform that will be used for a - given platform name, or None if there is no match.""" - chromium_prefix = 'chromium-' - name = name.lower() - if name.startswith(chromium_prefix): - name = name[len(chromium_prefix):] - if name in self.PLATFORMS: - return name - return None + iterable = [x + "\n" for x in + self._expectations.split("\n")] + # Strip final entry if it's empty to avoid added in an extra + # newline. + if iterable[-1] == "\n": + return iterable[:-1] + return iterable def get_test_set(self, modifier, expectation=None, include_skips=True): if expectation is None: @@ -398,6 +391,12 @@ class TestExpectationsFile: no """ + # FIXME - remove_platform_from file worked by writing a new + # test_expectations.txt file over the old one. Now that we're just + # parsing strings, we need to change this to return the new + # expectations string. + raise NotImplementedException('remove_platform_from_file') + new_file = self._path + '.new' logging.debug('Original file: "%s"', self._path) logging.debug('New file: "%s"', new_file) @@ -430,11 +429,12 @@ class TestExpectationsFile: elif action == ADD_PLATFORMS_EXCEPT_THIS: parts = line.split(':') new_options = parts[0] - for p in self.PLATFORMS: - p = p.upper(); + for p in self._port.test_platform_names(): + p = p.upper() # This is a temp solution for rebaselining tool. # Do not add tags WIN-7 and WIN-VISTA to test expectations - # if the original line does not specify the platform option. + # if the original line does not specify the platform + # option. # TODO(victorw): Remove WIN-VISTA and WIN-7 once we have # reliable Win 7 and Win Vista buildbots setup. if not p in (platform.upper(), 'WIN-VISTA', 'WIN-7'): @@ -517,7 +517,7 @@ class TestExpectationsFile: has_any_platform = False for option in options: - if option in self.PLATFORMS: + if option in self._port.test_platform_names(): has_any_platform = True if not option == platform: return REMOVE_PLATFORM @@ -547,7 +547,7 @@ class TestExpectationsFile: for option in options: if option in self.MODIFIERS: modifiers.add(option) - elif option in self.PLATFORMS: + elif option in self._port.test_platform_names(): has_any_platform = True elif option.startswith('bug'): has_bug_id = True @@ -590,7 +590,7 @@ class TestExpectationsFile: options: list of options """ for opt in options: - if self._platform.startswith(opt): + if self._test_platform_name.startswith(opt): return True return False @@ -632,7 +632,7 @@ class TestExpectationsFile: 'indefinitely, then it should be just timeout.', test_list_path) - full_path = os.path.join(path_utils.layout_tests_dir(), + full_path = os.path.join(self._port.layout_tests_dir(), test_list_path) full_path = os.path.normpath(full_path) # WebKit's way of skipping tests is to add a -disabled suffix. @@ -662,7 +662,7 @@ class TestExpectationsFile: else: build_type = 'RELEASE' print "\nFAILURES FOR PLATFORM: %s, BUILD_TYPE: %s" \ - % (self._platform.upper(), build_type) + % (self._test_platform_name.upper(), build_type) for error in self._non_fatal_errors: logging.error(error) @@ -695,7 +695,7 @@ class TestExpectationsFile: def _expand_tests(self, test_list_path): """Convert the test specification to an absolute, normalized path and make sure directories end with the OS path separator.""" - path = os.path.join(path_utils.layout_tests_dir(), test_list_path) + path = os.path.join(self._port.layout_tests_dir(), test_list_path) path = os.path.normpath(path) path = self._fix_dir(path) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_files.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_files.py index b7e620d..3c087c0 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_files.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_files.py @@ -36,7 +36,6 @@ under that directory.""" import glob import os -from port import path_utils # When collecting test cases, we include any file with these extensions. _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl', @@ -45,7 +44,7 @@ _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl', _skipped_directories = set(['.svn', '_svn', 'resources', 'script-tests']) -def gather_test_files(paths): +def gather_test_files(port, paths): """Generate a set of test files and return them. Args: @@ -57,14 +56,14 @@ def gather_test_files(paths): if paths: for path in paths: # If there's an * in the name, assume it's a glob pattern. - path = os.path.join(path_utils.layout_tests_dir(), path) + path = os.path.join(port.layout_tests_dir(), path) if path.find('*') > -1: filenames = glob.glob(path) paths_to_walk.update(filenames) else: paths_to_walk.add(path) else: - paths_to_walk.add(path_utils.layout_tests_dir()) + paths_to_walk.add(port.layout_tests_dir()) # Now walk all the paths passed in on the command line and get filenames test_files = set() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_shell_thread.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_shell_thread.py index 9f52686..3452035 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_shell_thread.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_shell_thread.py @@ -40,21 +40,22 @@ import logging import os import Queue import signal -import subprocess import sys import thread import threading import time -from port import path_utils import test_failures -def process_output(proc, test_info, test_types, test_args, target, output_dir): +def process_output(port, test_info, test_types, test_args, target, output_dir, + crash, timeout, test_run_time, actual_checksum, + output, error): """Receives the output from a test_shell process, subjects it to a number of tests, and returns a list of failure types the test produced. Args: + port: port-specific hooks proc: an active test_shell process test_info: Object containing the test filename, uri and timeout test_types: list of test types to subject the output to @@ -64,84 +65,39 @@ def process_output(proc, test_info, test_types, test_args, target, output_dir): Returns: a list of failure objects and times for the test being processed """ - outlines = [] - extra_lines = [] failures = [] - crash = False # Some test args, such as the image hash, may be added or changed on a # test-by-test basis. local_test_args = copy.copy(test_args) - start_time = time.time() - - line = proc.stdout.readline() - - # Only start saving output lines once we've loaded the URL for the test. - url = None - test_string = test_info.uri.strip() - - while line.rstrip() != "#EOF": - # Make sure we haven't crashed. - if line == '' and proc.poll() is not None: - failures.append(test_failures.FailureCrash()) - - # This is hex code 0xc000001d, which is used for abrupt - # termination. This happens if we hit ctrl+c from the prompt and - # we happen to be waiting on the test_shell. - # sdoyon: Not sure for which OS and in what circumstances the - # above code is valid. What works for me under Linux to detect - # ctrl+c is for the subprocess returncode to be negative SIGINT. - # And that agrees with the subprocess documentation. - if (-1073741510 == proc.returncode or - - signal.SIGINT == proc.returncode): - raise KeyboardInterrupt - crash = True - break - - # Don't include #URL lines in our output - if line.startswith("#URL:"): - url = line.rstrip()[5:] - if url != test_string: - logging.fatal("Test got out of sync:\n|%s|\n|%s|" % - (url, test_string)) - raise AssertionError("test out of sync") - elif line.startswith("#MD5:"): - local_test_args.hash = line.rstrip()[5:] - elif line.startswith("#TEST_TIMED_OUT"): - # Test timed out, but we still need to read until #EOF. - failures.append(test_failures.FailureTimeout()) - elif url: - outlines.append(line) - else: - extra_lines.append(line) - - line = proc.stdout.readline() - - end_test_time = time.time() - - if len(extra_lines): - extra = "".join(extra_lines) - if crash: - logging.debug("Stacktrace for %s:\n%s" % (test_string, extra)) - # Strip off "file://" since RelativeTestFilename expects - # filesystem paths. - filename = os.path.join(output_dir, - path_utils.relative_test_filename(test_string[7:])) - filename = os.path.splitext(filename)[0] + "-stack.txt" - path_utils.maybe_make_directory(os.path.split(filename)[0]) - open(filename, "wb").write(extra) - else: - logging.debug("Previous test output extra lines after dump:\n%s" % - extra) + local_test_args.hash = actual_checksum + + if crash: + failures.append(test_failures.FailureCrash()) + if timeout: + failures.append(test_failures.FailureTimeout()) + + if crash: + logging.debug("Stacktrace for %s:\n%s" % (test_info.filename, error)) + # Strip off "file://" since RelativeTestFilename expects + # filesystem paths. + filename = os.path.join(output_dir, test_info.filename) + filename = os.path.splitext(filename)[0] + "-stack.txt" + port.maybe_make_directory(os.path.split(filename)[0]) + open(filename, "wb").write(error) + elif error: + logging.debug("Previous test output extra lines after dump:\n%s" % + error) # Check the output and save the results. + start_time = time.time() time_for_diffs = {} for test_type in test_types: start_diff_time = time.time() - new_failures = test_type.compare_output(test_info.filename, - proc, ''.join(outlines), - local_test_args, target) + new_failures = test_type.compare_output(port, test_info.filename, + output, local_test_args, + target) # Don't add any more failures if we already have a crash, so we don't # double-report those tests. We do double-report for timeouts since # we still want to see the text and image output. @@ -150,28 +106,11 @@ def process_output(proc, test_info, test_types, test_args, target, output_dir): time_for_diffs[test_type.__class__.__name__] = ( time.time() - start_diff_time) - total_time_for_all_diffs = time.time() - end_test_time - test_run_time = end_test_time - start_time + total_time_for_all_diffs = time.time() - start_diff_time return TestStats(test_info.filename, failures, test_run_time, total_time_for_all_diffs, time_for_diffs) -def start_test_shell(command, args): - """Returns the process for a new test_shell started in layout-tests mode. - """ - cmd = [] - # Hook for injecting valgrind or other runtime instrumentation, - # used by e.g. tools/valgrind/valgrind_tests.py. - wrapper = os.environ.get("BROWSER_WRAPPER", None) - if wrapper != None: - cmd += [wrapper] - cmd += command + ['--layout-tests'] + args - return subprocess.Popen(cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - - class TestStats: def __init__(self, filename, failures, test_run_time, @@ -186,17 +125,19 @@ class TestStats: class SingleTestThread(threading.Thread): """Thread wrapper for running a single test file.""" - def __init__(self, test_shell_command, shell_args, test_info, test_types, - test_args, target, output_dir): + def __init__(self, port, image_path, shell_args, test_info, + test_types, test_args, target, output_dir): """ Args: + port: object implementing port-specific hooks test_info: Object containing the test filename, uri and timeout output_dir: Directory to put crash stacks into. See TestShellThread for documentation of the remaining arguments. """ threading.Thread.__init__(self) - self._command = test_shell_command + self._port = port + self._image_path = image_path self._shell_args = shell_args self._test_info = test_info self._test_types = test_types @@ -205,10 +146,18 @@ class SingleTestThread(threading.Thread): self._output_dir = output_dir def run(self): - proc = start_test_shell(self._command, self._shell_args + - ["--time-out-ms=" + self._test_info.timeout, self._test_info.uri]) - self._test_stats = process_output(proc, self._test_info, - self._test_types, self._test_args, self._target, self._output_dir) + driver = self._port.start_test_driver(self._image_path, + self._shell_args) + start = time.time() + crash, timeout, actual_checksum, output, error = \ + driver.run_test(test_info.uri.strip(), test_info.timeout, + test_info.image_hash) + end = time.time() + self._test_stats = process_output(self._port, + self._test_info, self._test_types, self._test_args, + self._target, self._output_dir, crash, timeout, end - start, + actual_checksum, output, error) + driver.stop() def get_test_stats(self): return self._test_stats @@ -216,17 +165,16 @@ class SingleTestThread(threading.Thread): class TestShellThread(threading.Thread): - def __init__(self, filename_list_queue, result_queue, test_shell_command, - test_types, test_args, shell_args, options): + def __init__(self, port, filename_list_queue, result_queue, + test_types, test_args, image_path, shell_args, options): """Initialize all the local state for this test shell thread. Args: + port: interface to port-specific hooks filename_list_queue: A thread safe Queue class that contains lists of tuples of (filename, uri) pairs. result_queue: A thread safe Queue class that will contain tuples of (test, failure lists) for the test results. - test_shell_command: A list specifying the command+args for - test_shell test_types: A list of TestType objects to run the test output against. test_args: A TestArguments object to pass to each TestType. @@ -236,13 +184,14 @@ class TestShellThread(threading.Thread): run_webkit_tests; they are typically passed via the run_webkit_tests.TestRunner class.""" threading.Thread.__init__(self) + self._port = port self._filename_list_queue = filename_list_queue self._result_queue = result_queue self._filename_list = [] - self._test_shell_command = test_shell_command self._test_types = test_types self._test_args = test_args - self._test_shell_proc = None + self._driver = None + self._image_path = image_path self._shell_args = shell_args self._options = options self._canceled = False @@ -379,11 +328,11 @@ class TestShellThread(threading.Thread): # Print the error message(s). error_str = '\n'.join([' ' + f.message() for f in failures]) logging.debug("%s %s failed:\n%s" % (self.getName(), - path_utils.relative_test_filename(filename), + self._port.relative_test_filename(filename), error_str)) else: logging.debug("%s %s passed" % (self.getName(), - path_utils.relative_test_filename(filename))) + self._port.relative_test_filename(filename))) self._result_queue.put((filename, failures)) if batch_size > 0 and batch_count > batch_size: @@ -407,7 +356,7 @@ class TestShellThread(threading.Thread): Return: A list of TestFailure objects describing the error. """ - worker = SingleTestThread(self._test_shell_command, + worker = SingleTestThread(self._port, self._image_path, self._shell_args, test_info, self._test_types, @@ -431,7 +380,7 @@ class TestShellThread(threading.Thread): # tradeoff in order to avoid losing the rest of this thread's # results. logging.error('Test thread hung: killing all test_shells') - path_utils.kill_all_test_shells() + worker._driver.stop() try: stats = worker.get_test_stats() @@ -454,32 +403,23 @@ class TestShellThread(threading.Thread): A list of TestFailure objects describing the error. """ self._ensure_test_shell_is_running() - # Args to test_shell is a space-separated list of - # "uri timeout pixel_hash" - # The timeout and pixel_hash are optional. The timeout is used if this - # test has a custom timeout. The pixel_hash is used to avoid doing an - # image dump if the checksums match, so it should be set to a blank - # value if we are generating a new baseline. - # (Otherwise, an image from a previous run will be copied into - # the baseline.) + # The pixel_hash is used to avoid doing an image dump if the + # checksums match, so it should be set to a blank value if we + # are generating a new baseline. (Otherwise, an image from a + # previous run will be copied into the baseline.) image_hash = test_info.image_hash if image_hash and self._test_args.new_baseline: image_hash = "" - self._test_shell_proc.stdin.write(("%s %s %s\n" % - (test_info.uri, test_info.timeout, image_hash))) - - # If the test shell is dead, the above may cause an IOError as we - # try to write onto the broken pipe. If this is the first test for - # this test shell process, than the test shell did not - # successfully start. If this is not the first test, then the - # previous tests have caused some kind of delayed crash. We don't - # try to recover here. - self._test_shell_proc.stdin.flush() - - stats = process_output(self._test_shell_proc, test_info, - self._test_types, self._test_args, - self._options.target, - self._options.results_directory) + start = time.time() + crash, timeout, actual_checksum, output, error = \ + self._driver.run_test(test_info.uri, test_info.timeout, image_hash) + end = time.time() + + stats = process_output(self._port, test_info, self._test_types, + self._test_args, self._options.target, + self._options.results_directory, crash, + timeout, end - start, actual_checksum, + output, error) self._test_stats.append(stats) return stats.failures @@ -489,23 +429,12 @@ class TestShellThread(threading.Thread): running tests singly, since those each start a separate test shell in their own thread. """ - if (not self._test_shell_proc or - self._test_shell_proc.poll() is not None): - self._test_shell_proc = start_test_shell(self._test_shell_command, - self._shell_args) + if (not self._driver or self._driver.poll() is not None): + self._driver = self._port.start_driver( + self._image_path, self._shell_args) def _kill_test_shell(self): """Kill the test shell process if it's running.""" - if self._test_shell_proc: - self._test_shell_proc.stdin.close() - self._test_shell_proc.stdout.close() - if self._test_shell_proc.stderr: - self._test_shell_proc.stderr.close() - if (sys.platform not in ('win32', 'cygwin') and - not self._test_shell_proc.poll()): - # Closing stdin/stdout/stderr hangs sometimes on OS X. - null = open(os.devnull, "w") - subprocess.Popen(["kill", "-9", - str(self._test_shell_proc.pid)], stderr=null) - null.close() - self._test_shell_proc = None + if self._driver: + self._driver.stop() + self._driver = None diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/__init__.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/__init__.py index 1730085..3509675 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/__init__.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/__init__.py @@ -27,24 +27,39 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Platform-specific utilities and pseudo-constants +"""Port-specific entrypoints for the layout tests test infrastructure.""" -Any functions whose implementations or values differ from one platform to -another should be defined in their respective <platform>.py -modules. The appropriate one of those will be imported into this module to -provide callers with a common, platform-independent interface. - -This file should only ever be imported by layout_package.path_utils. -""" import sys -# We may not support the version of Python that a user has installed (Cygwin -# especially has had problems), but we'll allow the platform utils to be -# included in any case so we don't get an import error. -if sys.platform in ('cygwin', 'win32'): - from chromium_win import * -elif sys.platform == 'darwin': - from chromium_mac import * -elif sys.platform in ('linux', 'linux2', 'freebsd7', 'openbsd4'): - from chromium_linux import * + +def get(port_name=None, options=None): + """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 + if port_to_use is None: + if sys.platform == 'win32': + port_to_use = 'chromium-win' + elif sys.platform == 'linux2': + port_to_use = 'chromium-linux' + elif sys.platform == 'darwin': + port_to_use = 'chromium-mac' + + if port_to_use == 'test': + import test + return test.TestPort(port_name, options) + elif port_to_use.startswith('mac'): + import mac + return mac.MacPort(port_name, options) + elif port_to_use.startswith('chromium-mac'): + import chromium_mac + return chromium_mac.ChromiumMacPort(port_name, options) + elif port_to_use.startswith('chromium-linux'): + import chromium_linux + return chromium_linux.ChromiumLinuxPort(port_name, options) + elif port_to_use.startswith('chromium-win'): + import chromium_win + return chromium_win.ChromiumWinPort(port_name, options) + + raise NotImplementedError('unsupported port: %s' % port_to_use) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/apache_http_server.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/apache_http_server.py index 1fb0367..9ff3671 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/apache_http_server.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/apache_http_server.py @@ -37,19 +37,19 @@ import subprocess import sys import http_server_base -import path_utils -import port class LayoutTestApacheHttpd(http_server_base.HttpServerBase): - def __init__(self, output_dir): + def __init__(self, port_obj, output_dir): """Args: + port_obj: handle to the platform-specific routines output_dir: the absolute path to the layout test result directory """ + http_server_base.HttpServerBase.__init__(self, port_obj) self._output_dir = output_dir self._httpd_proc = None - path_utils.maybe_make_directory(output_dir) + port_obj.maybe_make_directory(output_dir) self.mappings = [{'port': 8000}, {'port': 8080}, @@ -59,15 +59,14 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase): # The upstream .conf file assumed the existence of /tmp/WebKit for # placing apache files like the lock file there. self._runtime_path = os.path.join("/tmp", "WebKit") - path_utils.maybe_make_directory(self._runtime_path) + port_obj.maybe_make_directory(self._runtime_path) # The PID returned when Apache is started goes away (due to dropping # privileges?). The proper controlling PID is written to a file in the # apache runtime directory. self._pid_file = os.path.join(self._runtime_path, 'httpd.pid') - test_dir = path_utils.path_from_base('third_party', 'WebKit', - 'LayoutTests') + test_dir = self._port_obj.layout_tests_dir() js_test_resources_dir = self._cygwin_safe_join(test_dir, "fast", "js", "resources") mime_types_path = self._cygwin_safe_join(test_dir, "http", "conf", @@ -78,7 +77,7 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase): error_log = self._cygwin_safe_join(output_dir, "error_log.txt") document_root = self._cygwin_safe_join(test_dir, "http", "tests") - executable = port.apache_executable_path() + executable = self._port_obj._path_to_apache() if self._is_cygwin(): executable = self._get_cygwin_path(executable) @@ -95,7 +94,8 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase): os.environ.get("USER", ""))] if self._is_cygwin(): - cygbin = path_utils.path_from_base('third_party', 'cygwin', 'bin') + cygbin = self._port_obj._path_from_base('third_party', 'cygwin', + 'bin') # Not entirely sure why, but from cygwin we need to run the # httpd command through bash. self._start_cmd = [ @@ -146,7 +146,7 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase): test_dir: absolute path to the LayoutTests directory. output_dir: absolute path to the layout test results directory. """ - httpd_config = port.apache_config_file_path() + httpd_config = self._port_obj._path_to_apache_config_file() httpd_config_copy = os.path.join(output_dir, "httpd.conf") httpd_conf = open(httpd_config).read() if self._is_cygwin(): @@ -156,22 +156,11 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase): # plus the relative paths to the .so files listed in the .conf # file. We have apache/cygwin checked into our tree so # people don't have to install it into their cygwin. - cygusr = path_utils.path_from_base('third_party', 'cygwin', 'usr') + cygusr = self._port_obj._path_from_base('third_party', 'cygwin', + 'usr') httpd_conf = httpd_conf.replace('ServerRoot "/usr"', 'ServerRoot "%s"' % self._get_cygwin_path(cygusr)) - # TODO(ojan): Instead of writing an extra file, checkin a conf file - # upstream. Or, even better, upstream/delete all our chrome http - # tests so we don't need this special-cased DocumentRoot and then - # just use the upstream - # conf file. - chrome_document_root = path_utils.path_from_base('webkit', 'data', - 'layout_tests') - if self._is_cygwin(): - chrome_document_root = self._get_cygwin_path(chrome_document_root) - httpd_conf = (httpd_conf + - self._get_virtual_host_config(chrome_document_root, 8081)) - f = open(httpd_config_copy, 'wb') f.write(httpd_conf) f.close() @@ -226,4 +215,4 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase): httpd_pid = None if os.path.exists(self._pid_file): httpd_pid = int(open(self._pid_file).readline()) - path_utils.shut_down_http_server(httpd_pid) + self._port_obj._shut_down_http_server(httpd_pid) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py new file mode 100644 index 0000000..ce06b44 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py @@ -0,0 +1,645 @@ +#!/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. +# * Neither the Google name 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. + +"""Abstract base class of Port-specific entrypoints for the layout tests +test infrastructure (the Port and Driver classes).""" + +import cgi +import difflib +import errno +import os +import subprocess +import sys + +import apache_http_server +import http_server +import websocket_server + +# Python bug workaround. See Port.wdiff_text() for an explanation. +_wdiff_available = True + + +# FIXME: This class should merge with webkitpy.webkit_port at some point. +class Port(object): + """Abstract class for Port-specific hooks for the layout_test package. + """ + + def __init__(self, port_name=None, options=None): + self._name = port_name + self._options = options + self._helper = None + self._http_server = None + self._webkit_base_dir = None + self._websocket_server = None + + def baseline_path(self): + """Return the absolute path to the directory to store new baselines + in for this port.""" + raise NotImplementedError('Port.baseline_path') + + def baseline_search_path(self): + """Return a list of absolute paths to directories to search under for + baselines. The directories are searched in order.""" + raise NotImplementedError('Port.baseline_search_path') + + def check_sys_deps(self): + """If the port needs to do some runtime checks to ensure that the + tests can be run successfully, they should be done here. + + Returns whether the system is properly configured.""" + raise NotImplementedError('Port.check_sys_deps') + + def compare_text(self, actual_text, expected_text): + """Return whether or not the two strings are *not* equal. This + routine is used to diff text output. + + While this is a generic routine, we include it in the Port + interface so that it can be overriden for testing purposes.""" + return actual_text != expected_text + + def diff_image(self, actual_filename, expected_filename, diff_filename): + """Compare two image files and produce a delta image file. + + Return 1 if the two files are different, 0 if they are the same. + Also produce a delta image of the two images and write that into + |diff_filename|. + + While this is a generic routine, we include it in the Port + interface so that it can be overriden for testing purposes.""" + executable = self._path_to_image_diff() + cmd = [executable, '--diff', actual_filename, expected_filename, + diff_filename] + result = 1 + try: + result = subprocess.call(cmd) + except OSError, e: + if e.errno == errno.ENOENT or e.errno == errno.EACCES: + _compare_available = False + else: + raise e + except ValueError: + # work around a race condition in Python 2.4's implementation + # of subprocess.Popen. See http://bugs.python.org/issue1199282 . + pass + return result + + def diff_text(self, actual_text, expected_text, + actual_filename, expected_filename): + """Returns a string containing the diff of the two text strings + in 'unified diff' format. + + While this is a generic routine, we include it in the Port + interface so that it can be overriden for testing purposes.""" + diff = difflib.unified_diff(expected_text.splitlines(True), + actual_text.splitlines(True), + expected_filename, + actual_filename) + return ''.join(diff) + + def expected_baselines(self, filename, suffix, all_baselines=False): + """Given a test name, finds where the baseline results are located. + + Args: + filename: absolute filename to test file + suffix: file suffix of the expected results, including dot; e.g. + '.txt' or '.png'. This should not be None, but may be an empty + string. + all_baselines: If True, return an ordered list of all baseline paths + for the given platform. If False, return only the first one. + Returns + a list of ( platform_dir, results_filename ), where + platform_dir - abs path to the top of the results tree (or test + tree) + results_filename - relative path from top of tree to the results + file + (os.path.join of the two gives you the full path to the file, + unless None was returned.) + Return values will be in the format appropriate for the current + platform (e.g., "\\" for path separators on Windows). If the results + file is not found, then None will be returned for the directory, + but the expected relative pathname will still be returned. + + This routine is generic but lives here since it is used in + conjunction with the other baseline and filename routines that are + platform specific. + """ + testname = os.path.splitext(self.relative_test_filename(filename))[0] + + baseline_filename = testname + '-expected' + suffix + + baseline_search_path = self.baseline_search_path() + + baselines = [] + for platform_dir in baseline_search_path: + if os.path.exists(os.path.join(platform_dir, baseline_filename)): + baselines.append((platform_dir, baseline_filename)) + + if not all_baselines and baselines: + return baselines + + # If it wasn't found in a platform directory, return the expected + # result in the test directory, even if no such file actually exists. + platform_dir = self.layout_tests_dir() + if os.path.exists(os.path.join(platform_dir, baseline_filename)): + baselines.append((platform_dir, baseline_filename)) + + if baselines: + return baselines + + return [(None, baseline_filename)] + + def expected_filename(self, filename, suffix): + """Given a test name, returns an absolute path to its expected results. + + If no expected results are found in any of the searched directories, + the directory in which the test itself is located will be returned. + The return value is in the format appropriate for the platform + (e.g., "\\" for path separators on windows). + + Args: + filename: absolute filename to test file + suffix: file suffix of the expected results, including dot; e.g. '.txt' + or '.png'. This should not be None, but may be an empty string. + platform: the most-specific directory name to use to build the + search list of directories, e.g., 'chromium-win', or + 'chromium-mac-leopard' (we follow the WebKit format) + + This routine is generic but is implemented here to live alongside + the other baseline and filename manipulation routines. + """ + platform_dir, baseline_filename = self.expected_baselines( + filename, suffix)[0] + if platform_dir: + return os.path.join(platform_dir, baseline_filename) + return os.path.join(self.layout_tests_dir(), baseline_filename) + + def filename_to_uri(self, filename): + """Convert a test file to a URI.""" + LAYOUTTEST_HTTP_DIR = "http/tests/" + LAYOUTTEST_WEBSOCKET_DIR = "websocket/tests/" + + relative_path = self.relative_test_filename(filename) + port = None + use_ssl = False + + if relative_path.startswith(LAYOUTTEST_HTTP_DIR): + # http/tests/ run off port 8000 and ssl/ off 8443 + relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):] + port = 8000 + elif relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR): + # websocket/tests/ run off port 8880 and 9323 + # Note: the root is /, not websocket/tests/ + port = 8880 + + # Make http/tests/local run as local files. This is to mimic the + # logic in run-webkit-tests. + # + # TODO(dpranke): remove the media reference and the SSL reference? + if (port and not relative_path.startswith("local/") and + not relative_path.startswith("media/")): + if relative_path.startswith("ssl/"): + port += 443 + protocol = "https" + else: + protocol = "http" + return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path) + + if sys.platform in ('cygwin', 'win32'): + return "file:///" + self.get_absolute_path(filename) + return "file://" + self.get_absolute_path(filename) + + def get_absolute_path(self, filename): + """Return the absolute path in unix format for the given filename. + + This routine exists so that platforms that don't use unix filenames + can convert accordingly.""" + return os.path.abspath(filename) + + def layout_tests_dir(self): + """Return the absolute path to the top of the LayoutTests directory.""" + return self.path_from_webkit_base('LayoutTests') + + def maybe_make_directory(self, *path): + """Creates the specified directory if it doesn't already exist.""" + try: + os.makedirs(os.path.join(*path)) + except OSError, e: + if e.errno != errno.EEXIST: + raise + + def name(self): + """Return the name of the port (e.g., 'mac', 'chromium-win-xp'). + + Note that this is different from the test_platform_name(), which + may be different (e.g., 'win-xp' instead of 'chromium-win-xp'.""" + return self._name + + def num_cores(self): + """Return the number of cores/cpus available on this machine. + + This routine is used to determine the default amount of parallelism + used by run-chromium-webkit-tests.""" + raise NotImplementedError('Port.num_cores') + + def path_from_webkit_base(self, *comps): + """Returns the full path to path made by joining the top of the + WebKit source tree and the list of path components in |*comps|.""" + if not self._webkit_base_dir: + abspath = os.path.abspath(__file__) + self._webkit_base_dir = abspath[0:abspath.find('WebKitTools')] + return os.path.join(self._webkit_base_dir, *comps) + + def remove_directory(self, *path): + """Recursively removes a directory, even if it's marked read-only. + + Remove the directory located at *path, if it exists. + + shutil.rmtree() doesn't work on Windows if any of the files + or directories are read-only, which svn repositories and + some .svn files are. We need to be able to force the files + to be writable (i.e., deletable) as we traverse the tree. + + Even with all this, Windows still sometimes fails to delete a file, + citing a permission error (maybe something to do with antivirus + scans or disk indexing). The best suggestion any of the user + forums had was to wait a bit and try again, so we do that too. + It's hand-waving, but sometimes it works. :/ + """ + file_path = os.path.join(*path) + if not os.path.exists(file_path): + return + + win32 = False + if sys.platform == 'win32': + win32 = True + # Some people don't have the APIs installed. In that case we'll do + # without. + try: + win32api = __import__('win32api') + win32con = __import__('win32con') + except ImportError: + win32 = False + + def remove_with_retry(rmfunc, path): + os.chmod(path, stat.S_IWRITE) + if win32: + win32api.SetFileAttributes(path, + win32con.FILE_ATTRIBUTE_NORMAL) + try: + return rmfunc(path) + except EnvironmentError, e: + if e.errno != errno.EACCES: + raise + print 'Failed to delete %s: trying again' % repr(path) + time.sleep(0.1) + return rmfunc(path) + else: + + def remove_with_retry(rmfunc, path): + if os.path.islink(path): + return os.remove(path) + else: + return rmfunc(path) + + for root, dirs, files in os.walk(file_path, topdown=False): + # For POSIX: making the directory writable guarantees + # removability. Windows will ignore the non-read-only + # bits in the chmod value. + os.chmod(root, 0770) + for name in files: + remove_with_retry(os.remove, os.path.join(root, name)) + for name in dirs: + remove_with_retry(os.rmdir, os.path.join(root, name)) + + remove_with_retry(os.rmdir, file_path) + + def test_platform_name(self): + return self._name + + def relative_test_filename(self, filename): + """Relative unix-style path for a filename under the LayoutTests + directory. Filenames outside the LayoutTests directory should raise + an error.""" + return filename[len(self.layout_tests_dir()) + 1:] + + def results_directory(self): + """Absolute path to the place to store the test results.""" + raise NotImplemented('Port.results_directory') + + def setup_test_run(self): + """This routine can be overridden to perform any port-specific + work that shouuld be done at the beginning of a test run.""" + pass + + def show_html_results_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') + + def start_driver(self, png_path, options): + """Starts a new test Driver and returns a handle to the object.""" + raise NotImplementedError('Port.start_driver') + + def start_helper(self): + """Start a layout test helper if needed on this port. The test helper + is used to reconfigure graphics settings and do other things that + may be necessary to ensure a known test configuration.""" + raise NotImplementedError('Port.start_helper') + + def start_http_server(self): + """Start a web server if it is available. Do nothing if + it isn't. This routine is allowed to (and may) fail if a server + is already running.""" + if self._options.use_apache: + self._http_server = apache_http_server.LayoutTestApacheHttpd(self, + self._options.results_directory) + else: + self._http_server = http_server.Lighttpd(self, + self._options.results_directory) + self._http_server.start() + + def start_websocket_server(self): + """Start a websocket server if it is available. Do nothing if + it isn't. This routine is allowed to (and may) fail if a server + is already running.""" + self._websocket_server = websocket_server.PyWebSocket(self, + self._options.results_directory) + self._websocket_server.start() + + def stop_helper(self): + """Shut down the test helper if it is running. Do nothing if + it isn't, or it isn't available.""" + raise NotImplementedError('Port.stop_helper') + + def stop_http_server(self): + """Shut down the http server if it is running. Do nothing if + it isn't, or it isn't available.""" + if self._http_server: + self._http_server.stop() + + def stop_websocket_server(self): + """Shut down the websocket server if it is running. Do nothing if + it isn't, or it isn't available.""" + if self._websocket_server: + self._websocket_server.stop() + + def test_expectations(self): + """Returns the test expectations for this port. + + Basically this string should contain the equivalent of a + test_expectations file. See test_expectations.py for more details.""" + raise NotImplementedError('Port.test_expectations') + + def test_base_platform_names(self): + """Return a list of the 'base' platforms on your port. The base + platforms represent different architectures, operating systems, + or implementations (as opposed to different versions of a single + platform). For example, 'mac' and 'win' might be different base + platforms, wherease 'mac-tiger' and 'mac-leopard' might be + different platforms. This routine is used by the rebaselining tool + and the dashboards, and the strings correspond to the identifiers + in your test expectations (*not* necessarily the platform names + themselves).""" + raise NotImplementedError('Port.base_test_platforms') + + def test_platform_name(self): + """Returns the string that corresponds to the given platform name + in the test expectations. This may be the same as name(), or it + may be different. For example, chromium returns 'mac' for + 'chromium-mac'.""" + raise NotImplementedError('Port.test_platform_name') + + def test_platforms(self): + """Returns the list of test platform identifiers as used in the + test_expectations and on dashboards, the rebaselining tool, etc. + + Note that this is not necessarily the same as the list of ports, + which must be globally unique (e.g., both 'chromium-mac' and 'mac' + might return 'mac' as a test_platform name'.""" + raise NotImplementedError('Port.platforms') + + def version(self): + """Returns a string indicating the version of a given platform, e.g. + '-leopard' or '-xp'. + + This is used to help identify the exact port when parsing test + expectations, determining search paths, and logging information.""" + raise NotImplementedError('Port.version') + + def wdiff_text(self, actual_filename, expected_filename): + """Returns a string of HTML indicating the word-level diff of the + contents of the two filenames. Returns an empty string if word-level + diffing isn't available.""" + executable = self._path_to_wdiff() + cmd = [executable, + '--start-delete=##WDIFF_DEL##', + '--end-delete=##WDIFF_END##', + '--start-insert=##WDIFF_ADD##', + '--end-insert=##WDIFF_END##', + expected_filename, + actual_filename] + global _wdiff_available + result = '' + try: + # Python's Popen has a bug that causes any pipes opened to a + # process that can't be executed to be leaked. Since this + # code is specifically designed to tolerate exec failures + # to gracefully handle cases where wdiff is not installed, + # the bug results in a massive file descriptor leak. As a + # workaround, if an exec failure is ever experienced for + # wdiff, assume it's not available. This will leak one + # file descriptor but that's better than leaking each time + # wdiff would be run. + # + # http://mail.python.org/pipermail/python-list/ + # 2008-August/505753.html + # http://bugs.python.org/issue3210 + # + # It also has a threading bug, so we don't output wdiff if + # the Popen raises a ValueError. + # http://bugs.python.org/issue1236 + if _wdiff_available: + try: + wdiff = subprocess.Popen(cmd, + stdout=subprocess.PIPE).communicate()[0] + except ValueError, e: + # Working around a race in Python 2.4's implementation + # of Popen(). + wdiff = '' + wdiff = cgi.escape(wdiff) + wdiff = wdiff.replace('##WDIFF_DEL##', '<span class=del>') + wdiff = wdiff.replace('##WDIFF_ADD##', '<span class=add>') + wdiff = wdiff.replace('##WDIFF_END##', '</span>') + result = '<head><style>.del { background: #faa; } ' + result += '.add { background: #afa; }</style></head>' + result += '<pre>' + wdiff + '</pre>' + except OSError, e: + if (e.errno == errno.ENOENT or e.errno == errno.EACCES or + e.errno == errno.ECHILD): + _wdiff_available = False + else: + raise e + return result + + # + # PROTECTED ROUTINES + # + # The routines below should only be called by routines in this class + # or any of its subclasses. + # + + def _kill_process(self, pid): + """Forcefully kill a process. + + This routine should not be used or needed generically, but can be + used in helper files like http_server.py.""" + raise NotImplementedError('Port.kill_process') + + def _path_to_apache(self): + """Returns the full path to the apache binary. + + This is needed only by ports that use the apache_http_server module.""" + raise NotImplementedError('Port.path_to_apache') + + def _path_to_apache_config_file(self): + """Returns the full path to the apache binary. + + This is needed only by ports that use the apache_http_server module.""" + raise NotImplementedError('Port.path_to_apache_config_file') + + def _path_to_driver(self): + """Returns the full path to the test driver (DumpRenderTree).""" + raise NotImplementedError('Port.path_to_driver') + + def _path_to_helper(self): + """Returns the full path to the layout_test_helper binary, which + is used to help configure the system for the test run, or None + if no helper is needed. + + This is likely only used by start/stop_helper().""" + raise NotImplementedError('Port._path_to_helper') + + def _path_to_image_diff(self): + """Returns the full path to the image_diff binary, or None if it + is not available. + + This is likely used only by diff_image()""" + raise NotImplementedError('Port.path_to_image_diff') + + def _path_to_lighttpd(self): + """Returns the path to the LigHTTPd binary. + + This is needed only by ports that use the http_server.py module.""" + raise NotImplementedError('Port._path_to_lighttpd') + + def _path_to_lighttpd_modules(self): + """Returns the path to the LigHTTPd modules directory. + + This is needed only by ports that use the http_server.py module.""" + raise NotImplementedError('Port._path_to_lighttpd_modules') + + def _path_to_lighttpd_php(self): + """Returns the path to the LigHTTPd PHP executable. + + This is needed only by ports that use the http_server.py module.""" + raise NotImplementedError('Port._path_to_lighttpd_php') + + def _path_to_wdiff(self): + """Returns the full path to the wdiff binary, or None if it is + not available. + + This is likely used only by wdiff_text()""" + raise NotImplementedError('Port._path_to_wdiff') + + def _shut_down_http_server(self, pid): + """Forcefully and synchronously kills the web server. + + This routine should only be called from http_server.py or its + subclasses.""" + raise NotImplementedError('Port._shut_down_http_server') + + def _webkit_baseline_path(self, platform): + """Return the full path to the top of the baseline tree for a + given platform.""" + return os.path.join(self.layout_tests_dir(), 'platform', + platform) + + +class Driver: + """Abstract interface for the DumpRenderTree interface.""" + + def __init__(self, port, png_path, options): + """Initialize a Driver to subsequently run tests. + + Typically this routine will spawn DumpRenderTree in a config + ready for subsequent input. + + port - reference back to the port object. + png_path - an absolute path for the driver to write any image + data for a test (as a PNG). If no path is provided, that + indicates that pixel test results will not be checked. + options - any port-specific driver options.""" + raise NotImplementedError('Driver.__init__') + + def run_test(self, uri, timeout, checksum): + """Run a single test and return the results. + + Note that it is okay if a test times out or crashes and leaves + the driver in an indeterminate state. The upper layers of the program + are responsible for cleaning up and ensuring things are okay. + + uri - a full URI for the given test + timeout - number of milliseconds to wait before aborting this test. + checksum - if present, the expected checksum for the image for this + test + + Returns a tuple of the following: + crash - a boolean indicating whether the driver crashed on the test + timeout - a boolean indicating whehter the test timed out + checksum - a string containing the checksum of the image, if + present + output - any text output + error - any unexpected or additional (or error) text output + + Note that the image itself should be written to the path that was + specified in the __init__() call.""" + raise NotImplementedError('Driver.run_test') + + def poll(self): + """Returns None if the Driver is still running. Returns the returncode + if it has exited.""" + raise NotImplementedError('Driver.poll') + + def returncode(self): + """Returns the system-specific returncode if the Driver has stopped or + exited.""" + raise NotImplementedError('Driver.returncode') + + def stop(self): + raise NotImplementedError('Driver.stop') diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py new file mode 100644 index 0000000..70a8dea --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py @@ -0,0 +1,260 @@ +#!/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. +# * 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. + +"""Chromium implementations of the Port interface.""" + +import logging +import os +import shutil +import signal +import subprocess +import sys +import time + +import base +import http_server +import websocket_server + + +class ChromiumPort(base.Port): + """Abstract base class for Chromium implementations of the Port class.""" + + def __init__(self, port_name=None, options=None): + base.Port.__init__(self, port_name, options) + self._chromium_base_dir = None + + def baseline_path(self): + return self._chromium_baseline_path(self._name) + + def check_sys_deps(self): + result = True + test_shell_binary_path = self._path_to_driver() + if os.path.exists(test_shell_binary_path): + proc = subprocess.Popen([test_shell_binary_path, + '--check-layout-test-sys-deps']) + if proc.wait() != 0: + logging.error("Aborting because system dependencies check " + "failed.") + logging.error("To override, invoke with --nocheck-sys-deps") + result = False + else: + logging.error('test driver is not found at %s' % + test_shell_binary_path) + result = False + + image_diff_path = self._path_to_image_diff() + if (not os.path.exists(image_diff_path) and not + self._options.no_pixel_tests): + logging.error('image diff not found at %s' % image_diff_path) + logging.error("To override, invoke with --no-pixel-tests") + result = False + + return result + + def compare_text(self, actual_text, expected_text): + return actual_text != expected_text + + def path_from_chromium_base(self, *comps): + """Returns the full path to path made by joining the top of the + Chromium source tree and the list of path components in |*comps|.""" + if not self._chromium_base_dir: + abspath = os.path.abspath(__file__) + self._chromium_base_dir = abspath[0:abspath.find('third_party')] + return os.path.join(self._chromium_base_dir, *comps) + + def results_directory(self): + return self.path_from_chromium_base('webkit', self._options.target, + self._options.results_directory) + + def setup_test_run(self): + # Delete the disk cache if any to ensure a clean test run. + test_shell_binary_path = self._path_to_driver() + cachedir = os.path.split(test_shell_binary_path)[0] + cachedir = os.path.join(cachedir, "cache") + if os.path.exists(cachedir): + shutil.rmtree(cachedir) + + def show_results_html_file(self, results_filename): + subprocess.Popen([self._path_to_driver(), + self.filename_to_uri(results_filename)]) + + def start_driver(self, image_path, options): + """Starts a new Driver and returns a handle to it.""" + return ChromiumDriver(self, image_path, options) + + def start_helper(self): + helper_path = self._path_to_helper() + if helper_path: + logging.debug("Starting layout helper %s" % helper_path) + self._helper = subprocess.Popen([helper_path], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None) + is_ready = self._helper.stdout.readline() + if not is_ready.startswith('ready'): + logging.error("layout_test_helper failed to be ready") + + def stop_helper(self): + if self._helper: + logging.debug("Stopping layout test helper") + self._helper.stdin.write("x\n") + self._helper.stdin.close() + self._helper.wait() + + def test_base_platform_names(self): + return ('linux', 'mac', 'win') + + def test_expectations(self, options=None): + """Returns the test expectations for this port. + + Basically this string should contain the equivalent of a + test_expectations file. See test_expectations.py for more details.""" + expectations_file = self.path_from_chromium_base('webkit', 'tools', + 'layout_tests', 'test_expectations.txt') + return file(expectations_file, "r").read() + + def test_platform_names(self): + return self.test_base_platform_names() + ('win-xp', + 'win-vista', 'win-7') + + # + # PROTECTED METHODS + # + # These routines should only be called by other methods in this file + # or any subclasses. + # + + def _chromium_baseline_path(self, platform): + if platform is None: + platform = self.name() + return self.path_from_chromium_base('webkit', 'data', 'layout_tests', + 'platform', platform, 'LayoutTests') + + +class ChromiumDriver(base.Driver): + """Abstract interface for the DumpRenderTree interface.""" + + def __init__(self, port, image_path, options): + self._port = port + self._options = options + self._target = port._options.target + self._image_path = image_path + + cmd = [] + # Hook for injecting valgrind or other runtime instrumentation, + # used by e.g. tools/valgrind/valgrind_tests.py. + wrapper = os.environ.get("BROWSER_WRAPPER", None) + if wrapper != None: + cmd += [wrapper] + if self._port._options.wrapper: + # This split() isn't really what we want -- it incorrectly will + # split quoted strings within the wrapper argument -- but in + # practice it shouldn't come up and the --help output warns + # about it anyway. + cmd += self._options.wrapper.split() + cmd += [port._path_to_driver(), '--layout-tests'] + if options: + cmd += options + self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + def poll(self): + return self._proc.poll() + + def returncode(self): + return self._proc.returncode + + def run_test(self, uri, timeoutms, checksum): + output = [] + error = [] + crash = False + timeout = False + actual_uri = None + actual_checksum = None + + start_time = time.time() + cmd = uri + if timeoutms: + cmd += ' ' + str(timeoutms) + if checksum: + cmd += ' ' + checksum + cmd += "\n" + + self._proc.stdin.write(cmd) + line = self._proc.stdout.readline() + while line.rstrip() != "#EOF": + # Make sure we haven't crashed. + if line == '' and self.poll() is not None: + # This is hex code 0xc000001d, which is used for abrupt + # termination. This happens if we hit ctrl+c from the prompt + # and we happen to be waiting on the test_shell. + # sdoyon: Not sure for which OS and in what circumstances the + # above code is valid. What works for me under Linux to detect + # ctrl+c is for the subprocess returncode to be negative + # SIGINT. And that agrees with the subprocess documentation. + if (-1073741510 == self._proc.returncode or + - signal.SIGINT == self._proc.returncode): + raise KeyboardInterrupt + crash = True + break + + # Don't include #URL lines in our output + if line.startswith("#URL:"): + actual_uri = line.rstrip()[5:] + if uri != actual_uri: + logging.fatal("Test got out of sync:\n|%s|\n|%s|" % + (uri, actual_uri)) + raise AssertionError("test out of sync") + elif line.startswith("#MD5:"): + actual_checksum = line.rstrip()[5:] + elif line.startswith("#TEST_TIMED_OUT"): + timeout = True + # Test timed out, but we still need to read until #EOF. + elif actual_uri: + output.append(line) + else: + error.append(line) + + line = self._proc.stdout.readline() + + return (crash, timeout, actual_checksum, ''.join(output), + ''.join(error)) + + def stop(self): + if self._proc: + self._proc.stdin.close() + self._proc.stdout.close() + if self._proc.stderr: + self._proc.stderr.close() + if (sys.platform not in ('win32', 'cygwin') and + not self._proc.poll()): + # Closing stdin/stdout/stderr hangs sometimes on OS X. + null = open(os.devnull, "w") + subprocess.Popen(["kill", "-9", + str(self._proc.pid)], stderr=null) + null.close() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py index 9ffc401..8fd5343 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py @@ -27,222 +27,123 @@ # (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 is the Linux implementation of the port - package. This file should only be imported by that package.""" +"""Chromium Mac implementation of the Port interface.""" import os +import platform import signal import subprocess -import sys -import logging -import chromium_win -import path_utils +import chromium - -def platform_name(): - """Returns the name of the platform we're currently running on.""" - return 'chromium-linux' + platform_version() - - -def platform_version(): - """Returns the version string for the platform, e.g. '-vista' or - '-snowleopard'. If the platform does not distinguish between - minor versions, it returns ''.""" - return '' - - -def get_num_cores(): - """Returns the number of cores on the machine. For hyperthreaded machines, - this will be double the number of actual processors.""" - num_cores = os.sysconf("SC_NPROCESSORS_ONLN") - if isinstance(num_cores, int) and num_cores > 0: - return num_cores - return 1 - - -def baseline_path(platform=None): - """Returns the path relative to the top of the source tree for the - baselines for the specified platform version. If |platform| is None, - then the version currently in use is used.""" - if platform is None: - platform = platform_name() - return path_utils.path_from_base('webkit', 'data', 'layout_tests', - 'platform', platform, 'LayoutTests') - - -def baseline_search_path(platform=None): - """Returns the list of directories to search for baselines/results, in - order of preference. Paths are relative to the top of the source tree.""" - return [baseline_path(platform), - chromium_win.baseline_path('chromium-win'), - path_utils.webkit_baseline_path('win'), - path_utils.webkit_baseline_path('mac')] - - -def apache_executable_path(): - """Returns the executable path to start Apache""" - path = os.path.join("/usr", "sbin", "apache2") - if os.path.exists(path): - return path - print "Unable to fine Apache executable %s" % path - _missing_apache() - - -def apache_config_file_path(): - """Returns the path to Apache config file""" - return path_utils.path_from_base("third_party", "WebKit", "LayoutTests", - "http", "conf", "apache2-debian-httpd.conf") - - -def lighttpd_executable_path(): - """Returns the executable path to start LigHTTPd""" - binpath = "/usr/sbin/lighttpd" - if os.path.exists(binpath): - return binpath - print "Unable to find LigHTTPd executable %s" % binpath - _missing_lighttpd() - - -def lighttpd_module_path(): - """Returns the library module path for LigHTTPd""" - modpath = "/usr/lib/lighttpd" - if os.path.exists(modpath): - return modpath - print "Unable to find LigHTTPd modules %s" % modpath - _missing_lighttpd() - - -def lighttpd_php_path(): - """Returns the PHP executable path for LigHTTPd""" - binpath = "/usr/bin/php-cgi" - if os.path.exists(binpath): - return binpath - print "Unable to find PHP CGI executable %s" % binpath - _missing_lighttpd() - - -def wdiff_path(): - """Path to the WDiff executable, which we assume is already installed and - in the user's $PATH.""" - return 'wdiff' - - -def image_diff_path(target): - """Path to the image_diff binary. - - Args: - target: Build target mode (debug or release)""" - return _path_from_build_results(target, 'image_diff') - - -def layout_test_helper_path(target): - """Path to the layout_test helper binary, if needed, empty otherwise""" - return '' - - -def test_shell_path(target): - """Return the platform-specific binary path for our TestShell. - - Args: - target: Build target mode (debug or release) """ - if target in ('Debug', 'Release'): - try: - debug_path = _path_from_build_results('Debug', 'test_shell') - release_path = _path_from_build_results('Release', 'test_shell') - - debug_mtime = os.stat(debug_path).st_mtime - release_mtime = os.stat(release_path).st_mtime - - if debug_mtime > release_mtime and target == 'Release' or \ - release_mtime > debug_mtime and target == 'Debug': - logging.info('\x1b[31mWarning: you are not running the most ' - 'recent test_shell binary. You need to pass ' - '--debug or not to select between Debug and ' - 'Release.\x1b[0m') - # This will fail if we don't have both a debug and release binary. - # That's fine because, in this case, we must already be running the - # most up-to-date one. - except path_utils.PathNotFound: - pass - - return _path_from_build_results(target, 'test_shell') - - -def fuzzy_match_path(): - """Return the path to the fuzzy matcher binary.""" - return path_utils.path_from_base('third_party', 'fuzzymatch', 'fuzzymatch') - - -def shut_down_http_server(server_pid): - """Shut down the lighttpd web server. Blocks until it's fully shut down. - - Args: - server_pid: The process ID of the running server. - """ - # server_pid is not set when "http_server.py stop" is run manually. - if server_pid is None: - # This isn't ideal, since it could conflict with web server processes - # not started by http_server.py, but good enough for now. - kill_all_process('lighttpd') - kill_all_process('apache2') - else: - try: - os.kill(server_pid, signal.SIGTERM) - #TODO(mmoss) Maybe throw in a SIGKILL just to be sure? - except OSError: - # Sometimes we get a bad PID (e.g. from a stale httpd.pid file), - # so if kill fails on the given PID, just try to 'killall' web - # servers. - shut_down_http_server(None) - - -def kill_process(pid): - """Forcefully kill the process. - - Args: - pid: The id of the process to be killed. - """ - os.kill(pid, signal.SIGKILL) - - -def kill_all_process(process_name): - null = open(os.devnull) - subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'), - process_name], stderr=null) - null.close() - - -def kill_all_test_shells(): - """Kills all instances of the test_shell binary currently running.""" - kill_all_process('test_shell') - -# -# Private helper functions -# - - -def _missing_lighttpd(): - print 'Please install using: "sudo apt-get install lighttpd php5-cgi"' - print 'For complete Linux build requirements, please see:' - print 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions' - sys.exit(1) - - -def _missing_apache(): - print ('Please install using: "sudo apt-get install apache2 ' - 'libapache2-mod-php5"') - print 'For complete Linux build requirements, please see:' - print 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions' - sys.exit(1) - - -def _path_from_build_results(*pathies): - # FIXME(dkegel): use latest or warn if more than one found? - for dir in ["sconsbuild", "out", "xcodebuild"]: - try: - return path_utils.path_from_base(dir, *pathies) - except: - pass - raise path_utils.PathNotFound("Unable to find %s in build tree" % - (os.path.join(*pathies))) + +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' + chromium.ChromiumPort.__init__(self, port_name, options) + + def baseline_search_path(self): + return [self.baseline_path(), + self._chromium_baseline_path('chromium-win'), + self._webkit_baseline_path('win'), + self._webkit_baseline_path('mac')] + + def check_sys_deps(self): + # We have no platform-specific dependencies to check. + return True + + def num_cores(self): + num_cores = os.sysconf("SC_NPROCESSORS_ONLN") + if isinstance(num_cores, int) and num_cores > 0: + return num_cores + return 1 + + def test_platform_name(self): + # We use 'linux' instead of 'chromium-linux' in test_expectations.txt. + return 'linux' + + def version(self): + # We don't have different versions on linux. + return '' + + # + # PROTECTED METHODS + # + + def _build_path(self, *comps): + base = self.path_from_chromium_base() + if os.path.exists(os.path.join(base, 'sconsbuild')): + return self.path_from_chromium_base('sconsbuild', + self._options.target, *comps) + else: + return self.path_from_chromium_base('out', + self._options.target, *comps) + + def _kill_process(self, pid): + """Forcefully kill the process. + + Args: + pid: The id of the process to be killed. + """ + os.kill(pid, signal.SIGKILL) + + def _kill_all_process(self, process_name): + null = open(os.devnull) + subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'), + process_name], stderr=null) + null.close() + + def _path_to_apache(self): + return '/usr/sbin/apache2' + + def _path_to_apache_config_file(self): + return os.path.join(self.layout_tests_dir(), 'http', 'conf', + 'apache2-debian-httpd.conf') + + def _path_to_lighttpd(self): + return "/usr/sbin/lighttpd" + + def _path_to_lighttpd_modules(self): + return "/usr/lib/lighttpd" + + def _path_to_lighttpd_php(self): + return "/usr/bin/php-cgi" + + def _path_to_driver(self): + return self._build_path('test_shell') + + def _path_to_helper(self): + return None + + def _path_to_image_diff(self): + return self._build_path('image_diff') + + def _path_to_wdiff(self): + return 'wdiff' + + def _shut_down_http_server(self, server_pid): + """Shut down the lighttpd web server. Blocks until it's fully + shut down. + + Args: + server_pid: The process ID of the running server. + """ + # server_pid is not set when "http_server.py stop" is run manually. + if server_pid is None: + # TODO(mmoss) This isn't ideal, since it could conflict with + # lighttpd processes not started by http_server.py, + # but good enough for now. + self._kill_all_process('lighttpd') + self._kill_all_process('apache2') + else: + try: + os.kill(server_pid, signal.SIGTERM) + # TODO(mmoss) Maybe throw in a SIGKILL just to be sure? + except OSError: + # Sometimes we get a bad PID (e.g. from a stale httpd.pid + # file), so if kill fails on the given PID, just try to + # 'killall' web servers. + self._shut_down_http_server(None) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py index d0fbc01..7e7b4ca 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py @@ -27,175 +27,136 @@ # (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 is the Mac implementation of the port interface - package. This file should only be imported by that package.""" +"""Chromium Mac implementation of the Port interface.""" import os import platform import signal import subprocess -import path_utils - - -def platform_name(): - """Returns the name of the platform we're currently running on.""" - # At the moment all chromium mac results are version-independent. At some - # point we may need to return 'chromium-mac' + PlatformVersion() - return 'chromium-mac' - - -def platform_version(): - """Returns the version string for the platform, e.g. '-vista' or - '-snowleopard'. If the platform does not distinguish between - minor versions, it returns ''.""" - os_version_string = platform.mac_ver()[0] # e.g. "10.5.6" - if not os_version_string: - return '-leopard' - - release_version = int(os_version_string.split('.')[1]) - - # we don't support 'tiger' or earlier releases - if release_version == 5: - return '-leopard' - elif release_version == 6: - return '-snowleopard' - - return '' - - -def get_num_cores(): - """Returns the number of cores on the machine. For hyperthreaded machines, - this will be double the number of actual processors.""" - return int(os.popen2("sysctl -n hw.ncpu")[1].read()) - - -def baseline_path(platform=None): - """Returns the path relative to the top of the source tree for the - baselines for the specified platform version. If |platform| is None, - then the version currently in use is used.""" - if platform is None: - platform = platform_name() - return path_utils.path_from_base('webkit', 'data', 'layout_tests', - 'platform', platform, 'LayoutTests') - -# TODO: We should add leopard and snowleopard to the list of paths to check -# once we start running the tests from snowleopard. - - -def baseline_search_path(platform=None): - """Returns the list of directories to search for baselines/results, in - order of preference. Paths are relative to the top of the source tree.""" - return [baseline_path(platform), - path_utils.webkit_baseline_path('mac' + platform_version()), - path_utils.webkit_baseline_path('mac')] - - -def wdiff_path(): - """Path to the WDiff executable, which we assume is already installed and - in the user's $PATH.""" - return 'wdiff' - - -def image_diff_path(target): - """Path to the image_diff executable - - Args: - target: build type - 'Debug','Release',etc.""" - return path_utils.path_from_base('xcodebuild', target, 'image_diff') - - -def layout_test_helper_path(target): - """Path to the layout_test_helper executable, if needed, empty otherwise - - Args: - target: build type - 'Debug','Release',etc.""" - return path_utils.path_from_base('xcodebuild', target, - 'layout_test_helper') - - -def test_shell_path(target): - """Path to the test_shell executable. - - Args: - target: build type - 'Debug','Release',etc.""" - # TODO(pinkerton): make |target| happy with case-sensitive file systems. - return path_utils.path_from_base('xcodebuild', target, 'TestShell.app', - 'Contents', 'MacOS', 'TestShell') - - -def apache_executable_path(): - """Returns the executable path to start Apache""" - return os.path.join("/usr", "sbin", "httpd") - - -def apache_config_file_path(): - """Returns the path to Apache config file""" - return path_utils.path_from_base("third_party", "WebKit", "LayoutTests", - "http", "conf", "apache2-httpd.conf") - - -def lighttpd_executable_path(): - """Returns the executable path to start LigHTTPd""" - return path_utils.path_from_base('third_party', 'lighttpd', 'mac', - 'bin', 'lighttpd') - - -def lighttpd_module_path(): - """Returns the library module path for LigHTTPd""" - return path_utils.path_from_base('third_party', 'lighttpd', 'mac', 'lib') - - -def lighttpd_php_path(): - """Returns the PHP executable path for LigHTTPd""" - return path_utils.path_from_base('third_party', 'lighttpd', 'mac', 'bin', - 'php-cgi') - - -def shut_down_http_server(server_pid): - """Shut down the lighttpd web server. Blocks until it's fully shut down. - - Args: - server_pid: The process ID of the running server. - """ - # server_pid is not set when "http_server.py stop" is run manually. - if server_pid is None: - # TODO(mmoss) This isn't ideal, since it could conflict with lighttpd - # processes not started by http_server.py, but good enough for now. - kill_all_process('lighttpd') - kill_all_process('httpd') - else: - try: - os.kill(server_pid, signal.SIGTERM) - # TODO(mmoss) Maybe throw in a SIGKILL just to be sure? - except OSError: - # Sometimes we get a bad PID (e.g. from a stale httpd.pid file), - # so if kill fails on the given PID, just try to 'killall' web - # servers. - shut_down_http_server(None) - - -def kill_process(pid): - """Forcefully kill the process. - - Args: - pid: The id of the process to be killed. - """ - os.kill(pid, signal.SIGKILL) - - -def kill_all_process(process_name): - # On Mac OS X 10.6, killall has a new constraint: -SIGNALNAME or - # -SIGNALNUMBER must come first. Example problem: - # $ killall -u $USER -TERM lighttpd - # killall: illegal option -- T - # Use of the earlier -TERM placement is just fine on 10.5. - null = open(os.devnull) - subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'), - process_name], stderr=null) - null.close() - - -def kill_all_test_shells(): - """Kills all instances of the test_shell binary currently running.""" - kill_all_process('TestShell') +import chromium + + +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' + chromium.ChromiumPort.__init__(self, port_name, options) + + def baseline_search_path(self): + return [self.baseline_path(), + self._webkit_baseline_path('mac' + self.version()), + self._webkit_baseline_path('mac')] + + def check_sys_deps(self): + # We have no specific platform dependencies. + return True + + def num_cores(self): + return int(subprocess.Popen(['sysctl','-n','hw.ncpu'], + stdout=subprocess.PIPE).stdout.read()) + + def test_platform_name(self): + # We use 'mac' instead of 'chromium-mac' + return 'mac' + + def version(self): + os_version_string = platform.mac_ver()[0] # e.g. "10.5.6" + if not os_version_string: + return '-leopard' + release_version = int(os_version_string.split('.')[1]) + # we don't support 'tiger' or earlier releases + if release_version == 5: + return '-leopard' + elif release_version == 6: + return '-snowleopard' + return '' + + # + # PROTECTED METHODS + # + + def _build_path(self, *comps): + return self.path_from_chromium_base('xcodebuild', self._options.target, + *comps) + + def _lighttpd_path(self, *comps): + return self.path_from_chromium_base('third_party', 'lighttpd', + 'mac', *comps) + + def _kill_process(self, pid): + """Forcefully kill the process. + + Args: + pid: The id of the process to be killed. + """ + os.kill(pid, signal.SIGKILL) + + def _kill_all_process(self, process_name): + """Kill any processes running under this name.""" + # On Mac OS X 10.6, killall has a new constraint: -SIGNALNAME or + # -SIGNALNUMBER must come first. Example problem: + # $ killall -u $USER -TERM lighttpd + # killall: illegal option -- T + # Use of the earlier -TERM placement is just fine on 10.5. + null = open(os.devnull) + subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'), + process_name], stderr=null) + null.close() + + def _path_to_apache(self): + return '/usr/sbin/httpd' + + def _path_to_apache_config_file(self): + return os.path.join(self.layout_tests_dir(), 'http', 'conf', + 'apache2-httpd.conf') + + def _path_to_lighttpd(self): + return self._lighttp_path('bin', 'lighttp') + + def _path_to_lighttpd_modules(self): + return self._lighttp_path('lib') + + def _path_to_lighttpd_php(self): + return self._lighttpd_path('bin', 'php-cgi') + + def _path_to_driver(self): + # TODO(pinkerton): make |target| happy with case-sensitive file + # systems. + return self._build_path('TestShell.app', 'Contents', 'MacOS', + 'TestShell') + + def _path_to_helper(self): + return self._build_path('layout_test_helper') + + def _path_to_image_diff(self): + return self._build_path('image_diff') + + def _path_to_wdiff(self): + return 'wdiff' + + def _shut_down_http_server(self, server_pid): + """Shut down the lighttpd web server. Blocks until it's fully + shut down. + + Args: + server_pid: The process ID of the running server. + """ + # server_pid is not set when "http_server.py stop" is run manually. + if server_pid is None: + # TODO(mmoss) This isn't ideal, since it could conflict with + # lighttpd processes not started by http_server.py, + # but good enough for now. + self._kill_all_process('lighttpd') + self._kill_all_process('httpd') + else: + try: + os.kill(server_pid, signal.SIGTERM) + # TODO(mmoss) Maybe throw in a SIGKILL just to be sure? + except OSError: + # Sometimes we get a bad PID (e.g. from a stale httpd.pid + # file), so if kill fails on the given PID, just try to + # 'killall' web servers. + self._shut_down_http_server(None) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py index 1e0b212..352916c 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py @@ -27,184 +27,129 @@ # (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 is the Linux implementation of the port - package. This file should only be imported by that package.""" +"""Chromium Win implementation of the Port interface.""" import os +import platform +import signal import subprocess import sys -import path_utils +import chromium -def platform_name(): - """Returns the name of the platform we're currently running on.""" - # We're not ready for version-specific results yet. When we uncomment - # this, we also need to add it to the BaselineSearchPath() - return 'chromium-win' + platform_version() - -def platform_version(): - """Returns the version string for the platform, e.g. '-vista' or - '-snowleopard'. If the platform does not distinguish between - minor versions, it returns ''.""" - winver = sys.getwindowsversion() - if winver[0] == 6 and (winver[1] == 1): - return '-7' - if winver[0] == 6 and (winver[1] == 0): - return '-vista' - if winver[0] == 5 and (winver[1] == 1 or winver[1] == 2): - return '-xp' - return '' - - -def get_num_cores(): - """Returns the number of cores on the machine. For hyperthreaded machines, - this will be double the number of actual processors.""" - return int(os.environ.get('NUMBER_OF_PROCESSORS', 1)) - - -def baseline_path(platform=None): - """Returns the path relative to the top of the source tree for the - baselines for the specified platform version. If |platform| is None, - then the version currently in use is used.""" - if platform is None: - platform = platform_name() - return path_utils.path_from_base('webkit', 'data', 'layout_tests', - 'platform', platform, 'LayoutTests') - - -def baseline_search_path(platform=None): - """Returns the list of directories to search for baselines/results, in - order of preference. Paths are relative to the top of the source tree.""" - dirs = [] - if platform is None: - platform = platform_name() - - if platform == 'chromium-win-xp': - dirs.append(baseline_path(platform)) - if platform in ('chromium-win-xp', 'chromium-win-vista'): - dirs.append(baseline_path('chromium-win-vista')) - dirs.append(baseline_path('chromium-win')) - dirs.append(path_utils.webkit_baseline_path('win')) - dirs.append(path_utils.webkit_baseline_path('mac')) - return dirs - - -def wdiff_path(): - """Path to the WDiff executable, whose binary is checked in on Win""" - return path_utils.path_from_base('third_party', 'cygwin', 'bin', - 'wdiff.exe') - - -def image_diff_path(target): - """Return the platform-specific binary path for the image compare util. - We use this if we can't find the binary in the default location - in path_utils. - - Args: - target: Build target mode (debug or release) - """ - return _find_binary(target, 'image_diff.exe') - - -def layout_test_helper_path(target): - """Return the platform-specific binary path for the layout test helper. - We use this if we can't find the binary in the default location - in path_utils. - - Args: - target: Build target mode (debug or release) - """ - return _find_binary(target, 'layout_test_helper.exe') - - -def test_shell_path(target): - """Return the platform-specific binary path for our TestShell. - We use this if we can't find the binary in the default location - in path_utils. - - Args: - target: Build target mode (debug or release) - """ - return _find_binary(target, 'test_shell.exe') - - -def apache_executable_path(): - """Returns the executable path to start Apache""" - path = path_utils.path_from_base('third_party', 'cygwin', "usr", "sbin") - # Don't return httpd.exe since we want to use this from cygwin. - return os.path.join(path, "httpd") - - -def apache_config_file_path(): - """Returns the path to Apache config file""" - return path_utils.path_from_base("third_party", "WebKit", "LayoutTests", - "http", "conf", "cygwin-httpd.conf") - - -def lighttpd_executable_path(): - """Returns the executable path to start LigHTTPd""" - return path_utils.path_from_base('third_party', 'lighttpd', 'win', - 'LightTPD.exe') - - -def lighttpd_module_path(): - """Returns the library module path for LigHTTPd""" - return path_utils.path_from_base('third_party', 'lighttpd', 'win', 'lib') - - -def lighttpd_php_path(): - """Returns the PHP executable path for LigHTTPd""" - return path_utils.path_from_base('third_party', 'lighttpd', 'win', 'php5', - 'php-cgi.exe') - - -def shut_down_http_server(server_pid): - """Shut down the lighttpd web server. Blocks until it's fully shut down. - - Args: - server_pid: The process ID of the running server. - Unused in this implementation of the method. - """ - subprocess.Popen(('taskkill.exe', '/f', '/im', 'LightTPD.exe'), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE).wait() - subprocess.Popen(('taskkill.exe', '/f', '/im', 'httpd.exe'), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE).wait() - - -def kill_process(pid): - """Forcefully kill the process. - - Args: - pid: The id of the process to be killed. - """ - subprocess.call(('taskkill.exe', '/f', '/pid', str(pid)), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - -def kill_all_test_shells(self): - """Kills all instances of the test_shell binary currently running.""" - subprocess.Popen(('taskkill.exe', '/f', '/im', 'test_shell.exe'), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE).wait() - -# -# Private helper functions. -# - - -def _find_binary(target, binary): - """On Windows, we look for binaries that we compile in potentially - two places: src/webkit/$target (preferably, which we get if we - built using webkit_glue.gyp), or src/chrome/$target (if compiled some - other way).""" - try: - return path_utils.path_from_base('webkit', target, binary) - except path_utils.PathNotFound: - try: - return path_utils.path_from_base('chrome', target, binary) - except path_utils.PathNotFound: - return path_utils.path_from_base('build', target, binary) +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() + chromium.ChromiumPort.__init__(self, port_name, options) + + def baseline_search_path(self): + dirs = [] + if self._name == 'chromium-win-xp': + dirs.append(self._chromium_baseline_path(self._name)) + if self._name in ('chromium-win-xp', 'chromium-win-vista'): + dirs.append(self._chromium_baseline_path('chromium-win-vista')) + dirs.append(self._chromium_baseline_path('chromium-win')) + dirs.append(self._webkit_baseline_path('win')) + dirs.append(self._webkit_baseline_path('mac')) + return dirs + + def check_sys_deps(self): + # TODO(dpranke): implement this + return True + + def get_absolute_path(self, filename): + """Return the absolute path in unix format for the given filename.""" + abspath = os.path.abspath(filename) + return abspath.replace('\\', '/') + + def num_cores(self): + return int(os.environ.get('NUMBER_OF_PROCESSORS', 1)) + + def relative_test_filename(self, filename): + path = filename[len(self.layout_tests_dir()) + 1:] + return path.replace('\\', '/') + + def test_platform_name(self): + # We return 'win-xp', not 'chromium-win-xp' here, for convenience. + return 'win' + self.version() + + def version(self): + winver = sys.getwindowsversion() + if winver[0] == 6 and (winver[1] == 1): + return '-7' + if winver[0] == 6 and (winver[1] == 0): + return '-vista' + if winver[0] == 5 and (winver[1] == 1 or winver[1] == 2): + return '-xp' + return '' + + # + # PROTECTED ROUTINES + # + + def _build_path(self, *comps): + # FIXME(dpranke): allow for builds under 'chrome' as well. + return self.path_from_chromium_base('webkit', self._options.target, + *comps) + + def _lighttpd_path(self, *comps): + return self.path_from_chromium_base('third_party', 'lighttpd', 'win', + *comps) + + def _kill_process(self, pid): + """Forcefully kill the process. + + Args: + pid: The id of the process to be killed. + """ + subprocess.call(('taskkill.exe', '/f', '/pid', str(pid)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + def _path_to_apache(self): + return self.path_from_chromium_base('third_party', 'cygwin', 'usr', + 'sbin', 'httpd') + + def _path_to_apache_config_file(self): + return os.path.join(self.layout_tests_dir(), 'http', 'conf', + 'cygwin-httpd.conf') + + def _path_to_lighttpd(self): + return self._lighttpd_path('LightTPD.exe') + + def _path_to_lighttpd_modules(self): + return self._lighttpd_path('lib') + + def _path_to_lighttpd_php(self): + return self._lighttpd_path('php5', 'php-cgi.exe') + + def _path_to_driver(self): + return self._build_path('test_shell.exe') + + def _path_to_helper(self): + return self._build_path('layout_test_helper.exe') + + def _path_to_image_diff(self): + return self._build_path('image_diff.exe') + + def _path_to_wdiff(self): + return self.path_from_chromium_base('third_party', 'cygwin', 'bin', + 'wdiff.exe') + + def _shut_down_http_server(self, server_pid): + """Shut down the lighttpd web server. Blocks until it's fully + shut down. + + Args: + server_pid: The process ID of the running server. + """ + subprocess.Popen(('taskkill.exe', '/f', '/im', 'LightTPD.exe'), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).wait() + subprocess.Popen(('taskkill.exe', '/f', '/im', 'httpd.exe'), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).wait() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server.py index 99e2ea1..0315704 100755 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server.py @@ -41,52 +41,21 @@ import time import urllib import http_server_base -import path_utils -class HttpdNotStarted(Exception): pass -def remove_log_files(folder, starts_with): - files = os.listdir(folder) - for file in files: - if file.startswith(starts_with): - full_path = os.path.join(folder, file) - os.remove(full_path) +class HttpdNotStarted(Exception): + pass class Lighttpd(http_server_base.HttpServerBase): - # Webkit tests - try: - _webkit_tests = path_utils.path_from_base('third_party', 'WebKit', - 'LayoutTests', 'http', - 'tests') - _js_test_resource = path_utils.path_from_base('third_party', 'WebKit', - 'LayoutTests', 'fast', - 'js', 'resources') - except path_utils.PathNotFound: - _webkit_tests = None - _js_test_resource = None - - # Path where we can access all of the tests - _all_tests = path_utils.path_from_base('webkit', 'data', 'layout_tests') - # Self generated certificate for SSL server (for client cert get - # <base-path>\chrome\test\data\ssl\certs\root_ca_cert.crt) - _pem_file = path_utils.path_from_base( - os.path.dirname(os.path.abspath(__file__)), 'httpd2.pem') - # One mapping where we can get to everything - VIRTUALCONFIG = [{'port': 8081, 'docroot': _all_tests}] - - if _webkit_tests: - VIRTUALCONFIG.extend( - # Three mappings (one with SSL enabled) for LayoutTests http tests - [{'port': 8000, 'docroot': _webkit_tests}, - {'port': 8080, 'docroot': _webkit_tests}, - {'port': 8443, 'docroot': _webkit_tests, 'sslcert': _pem_file}]) - - def __init__(self, output_dir, background=False, port=None, + + def __init__(self, port_obj, output_dir, background=False, port=None, root=None, register_cygwin=None, run_background=None): """Args: output_dir: the absolute path to the layout test result directory """ + # Webkit tests + http_server_base.HttpServerBase.__init__(self, port_obj) self._output_dir = output_dir self._process = None self._port = port @@ -96,6 +65,31 @@ class Lighttpd(http_server_base.HttpServerBase): if self._port: self._port = int(self._port) + try: + self._webkit_tests = os.path.join( + self._port_obj.layout_tests_dir(), 'http', 'tests') + self._js_test_resource = os.path.join( + self._port_obj.layout_tests_dir(), 'fast', 'js', 'resources') + except: + self._webkit_tests = None + self._js_test_resource = None + + # Self generated certificate for SSL server (for client cert get + # <base-path>\chrome\test\data\ssl\certs\root_ca_cert.crt) + self._pem_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'httpd2.pem') + + # One mapping where we can get to everything + self.VIRTUALCONFIG = [] + + if self._webkit_tests: + self.VIRTUALCONFIG.extend( + # Three mappings (one with SSL) for LayoutTests http tests + [{'port': 8000, 'docroot': self._webkit_tests}, + {'port': 8080, 'docroot': self._webkit_tests}, + {'port': 8443, 'docroot': self._webkit_tests, + 'sslcert': self._pem_file}]) + def is_running(self): return self._process != None @@ -103,9 +97,8 @@ class Lighttpd(http_server_base.HttpServerBase): if self.is_running(): raise 'Lighttpd already running' - base_conf_file = path_utils.path_from_base('third_party', - 'WebKitTools', 'Scripts', 'webkitpy', 'layout_tests', - 'layout_package', 'lighttpd.conf') + base_conf_file = self._port_obj.path_from_webkit_base('WebKitTools', + 'Scripts', 'webkitpy', 'layout_tests', 'port', 'lighttpd.conf') out_conf_file = os.path.join(self._output_dir, 'lighttpd.conf') time_str = time.strftime("%d%b%Y-%H%M%S") access_file_name = "access.log-" + time_str + ".txt" @@ -114,8 +107,8 @@ class Lighttpd(http_server_base.HttpServerBase): error_log = os.path.join(self._output_dir, log_file_name) # Remove old log files. We only need to keep the last ones. - remove_log_files(self._output_dir, "access.log-") - remove_log_files(self._output_dir, "error.log-") + self.remove_log_files(self._output_dir, "access.log-") + self.remove_log_files(self._output_dir, "error.log-") # Write out the config f = file(base_conf_file, 'rb') @@ -132,7 +125,7 @@ class Lighttpd(http_server_base.HttpServerBase): ' ".pl" => "/usr/bin/env",\n' ' ".asis" => "/bin/cat",\n' ' ".php" => "%s" )\n\n') % - path_utils.lighttpd_php_path()) + self._port_obj._path_to_lighttpd_php()) # Setup log files f.write(('server.errorlog = "%s"\n' @@ -161,7 +154,7 @@ class Lighttpd(http_server_base.HttpServerBase): mappings = [{'port': 8000, 'docroot': self._root}, {'port': 8080, 'docroot': self._root}, {'port': 8443, 'docroot': self._root, - 'sslcert': Lighttpd._pem_file}] + 'sslcert': self._pem_file}] else: mappings = self.VIRTUALCONFIG for mapping in mappings: @@ -176,12 +169,11 @@ class Lighttpd(http_server_base.HttpServerBase): '}\n\n') % (mapping['port'], mapping['docroot'])) f.close() - executable = path_utils.lighttpd_executable_path() - module_path = path_utils.lighttpd_module_path() + executable = self._port_obj._path_to_lighttpd() + module_path = self._port_obj._path_to_lighttpd_modules() start_cmd = [executable, # Newly written config file - '-f', path_utils.path_from_base(self._output_dir, - 'lighttpd.conf'), + '-f', os.path.join(self._output_dir, 'lighttpd.conf'), # Where it can find its module dynamic libraries '-m', module_path] @@ -203,12 +195,13 @@ class Lighttpd(http_server_base.HttpServerBase): env = os.environ if sys.platform in ('cygwin', 'win32'): env['PATH'] = '%s;%s' % ( - path_utils.path_from_base('third_party', 'cygwin', 'bin'), + self._port_obj.path_from_chromium_base('third_party', + 'cygwin', 'bin'), env['PATH']) if sys.platform == 'win32' and self._register_cygwin: - setup_mount = path_utils.path_from_base('third_party', 'cygwin', - 'setup_mount.bat') + setup_mount = port.path_from_chromium_base('third_party', + 'cygwin', 'setup_mount.bat') subprocess.Popen(setup_mount).wait() logging.debug('Starting http server') @@ -235,7 +228,7 @@ class Lighttpd(http_server_base.HttpServerBase): httpd_pid = None if self._process: httpd_pid = self._process.pid - path_utils.shut_down_http_server(httpd_pid) + self._port_obj._shut_down_http_server(httpd_pid) if self._process: self._process.wait() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server_base.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server_base.py index 026e070..e82943e 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server_base.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_server_base.py @@ -30,12 +30,16 @@ """Base class with common routines between the Apache and Lighttpd servers.""" import logging +import os import time import urllib class HttpServerBase(object): + def __init__(self, port_obj): + self._port_obj = port_obj + def wait_for_action(self, action): """Repeat the action for 20 seconds or until it succeeds. Returns whether it succeeded.""" @@ -65,3 +69,10 @@ class HttpServerBase(object): return False return True + + def remove_log_files(self, folder, starts_with): + files = os.listdir(folder) + for file in files: + if file.startswith(starts_with): + full_path = os.path.join(folder, file) + os.remove(full_path) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/mac.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/mac.py new file mode 100644 index 0000000..4b73cec --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/mac.py @@ -0,0 +1,439 @@ +#!/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. +# * Neither the Google name 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. + +"""WebKit Mac implementation of the Port interface.""" + +import fcntl +import logging +import os +import pdb +import platform +import select +import signal +import subprocess +import sys +import time +import webbrowser + +import base + +import webkitpy +from webkitpy import executive + +class MacPort(base.Port): + """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() + base.Port.__init__(self, port_name, options) + self._cached_build_root = None + + def baseline_search_path(self): + dirs = [] + if self._name == 'mac-tiger': + dirs.append(self._webkit_baseline_path(self._name)) + if self._name in ('mac-tiger', 'mac-leopard'): + dirs.append(self._webkit_baseline_path('mac-leopard')) + if self._name in ('mac-tiger', 'mac-leopard', 'mac-snowleopard'): + dirs.append(self._webkit_baseline_path('mac-snowleopard')) + dirs.append(self._webkit_baseline_path('mac')) + return dirs + + def check_sys_deps(self): + # FIXME: This should run build-dumprendertree. + # This should also validate that all of the tool paths are valid. + return True + + def num_cores(self): + return int(os.popen2("sysctl -n hw.ncpu")[1].read()) + + def results_directory(self): + return ('/tmp/run-chromium-webkit-tests-' + + self._options.results_directory) + + def setup_test_run(self): + # This port doesn't require any specific configuration. + pass + + def show_results_html_file(self, results_filename): + uri = self.filename_to_uri(results_filename) + webbrowser.open(uri, new=1) + + def start_driver(self, image_path, options): + """Starts a new Driver and returns a handle to it.""" + return MacDriver(self, image_path, options) + + def start_helper(self): + # This port doesn't use a helper process. + pass + + def stop_helper(self): + # This port doesn't use a helper process. + pass + + def test_base_platform_names(self): + # At the moment we don't use test platform names, but we have + # to return something. + return ('mac',) + + def test_expectations(self): + # + # The WebKit mac port uses 'Skipped' files at the moment. Each + # file contains a list of files or directories to be skipped during + # the test run. The total list of tests to skipped is given by the + # contents of the generic Skipped file found in platform/X plus + # a version-specific file found in platform/X-version. Duplicate + # entries are allowed. This routine reads those files and turns + # contents into the format expected by test_expectations. + expectations = [] + skipped_files = [] + if self._name in ('mac-tiger', 'mac-leopard', 'mac-snowleopard'): + skipped_files.append(os.path.join( + self._webkit_baseline_path(self._name), 'Skipped')) + skipped_files.append(os.path.join(self._webkit_baseline_path('mac'), + 'Skipped')) + for filename in skipped_files: + if os.path.exists(filename): + f = file(filename) + for l in f.readlines(): + l = l.strip() + if not l.startswith('#') and len(l): + l = 'BUG_SKIPPED SKIP : ' + l + ' = FAIL' + if l not in expectations: + expectations.append(l) + f.close() + + # TODO - figure out how to check for these dynamically + expectations.append('BUG_SKIPPED SKIP : fast/wcss = FAIL') + expectations.append('BUG_SKIPPED SKIP : fast/xhtmlmp = FAIL') + expectations.append('BUG_SKIPPED SKIP : http/tests/wml = FAIL') + expectations.append('BUG_SKIPPED SKIP : mathml = FAIL') + expectations.append('BUG_SKIPPED SKIP : platform/chromium = FAIL') + expectations.append('BUG_SKIPPED SKIP : platform/gtk = FAIL') + expectations.append('BUG_SKIPPED SKIP : platform/qt = FAIL') + expectations.append('BUG_SKIPPED SKIP : platform/win = FAIL') + expectations.append('BUG_SKIPPED SKIP : wml = FAIL') + + # TODO - figure out how to handle webarchive tests + expectations.append('BUG_SKIPPED SKIP : webarchive = PASS') + expectations.append('BUG_SKIPPED SKIP : svg/webarchive = PASS') + expectations.append('BUG_SKIPPED SKIP : http/tests/webarchive = PASS') + expectations.append('BUG_SKIPPED SKIP : svg/custom/' + 'image-with-prefix-in-webarchive.svg = PASS') + + expectations_str = '\n'.join(expectations) + return expectations_str + + def test_platform_name(self): + # At the moment we don't use test platform names, but we have + # to return something. + return 'mac' + + def test_platform_names(self): + # At the moment we don't use test platform names, but we have + # to return something. + return ('mac',) + + def version(self): + os_version_string = platform.mac_ver()[0] # e.g. "10.5.6" + if not os_version_string: + return '-leopard' + release_version = int(os_version_string.split('.')[1]) + if release_version == 4: + return '-tiger' + elif release_version == 5: + return '-leopard' + elif release_version == 6: + return '-snowleopard' + return '' + + # + # PROTECTED METHODS + # + + def _build_path(self, *comps): + if not self._cached_build_root: + self._cached_build_root = executive.run_command(["webkit-build-directory", "--base"]).rstrip() + return os.path.join(self._cached_build_root, self._options.target, *comps) + + def _kill_process(self, pid): + """Forcefully kill the process. + + Args: + pid: The id of the process to be killed. + """ + os.kill(pid, signal.SIGKILL) + + def _kill_all_process(self, process_name): + # On Mac OS X 10.6, killall has a new constraint: -SIGNALNAME or + # -SIGNALNUMBER must come first. Example problem: + # $ killall -u $USER -TERM lighttpd + # killall: illegal option -- T + # Use of the earlier -TERM placement is just fine on 10.5. + null = open(os.devnull) + subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'), + process_name], stderr=null) + null.close() + + def _path_to_apache(self): + return '/usr/sbin/httpd' + + def _path_to_apache_config_file(self): + return os.path.join(self.layout_tests_dir(), 'http', 'conf', + 'apache2-httpd.conf') + + def _path_to_driver(self): + return self._build_path('DumpRenderTree') + + def _path_to_helper(self): + return None + + def _path_to_image_diff(self): + return self._build_path('image_diff') # FIXME: This is wrong and should be "ImageDiff", but having the correct path causes other parts of the script to hang. + + def _path_to_wdiff(self): + return 'wdiff' # FIXME: This does not exist on a default Mac OS X Leopard install. + + def _shut_down_http_server(self, server_pid): + """Shut down the lighttpd web server. Blocks until it's fully + shut down. + + Args: + server_pid: The process ID of the running server. + """ + # server_pid is not set when "http_server.py stop" is run manually. + if server_pid is None: + # TODO(mmoss) This isn't ideal, since it could conflict with + # lighttpd processes not started by http_server.py, + # but good enough for now. + self._kill_all_process('httpd') + else: + try: + os.kill(server_pid, signal.SIGTERM) + # TODO(mmoss) Maybe throw in a SIGKILL just to be sure? + except OSError: + # Sometimes we get a bad PID (e.g. from a stale httpd.pid + # file), so if kill fails on the given PID, just try to + # 'killall' web servers. + self._shut_down_http_server(None) + + +class MacDriver(base.Driver): + """implementation of the DumpRenderTree interface.""" + + def __init__(self, port, image_path, driver_options): + self._port = port + self._driver_options = driver_options + self._target = port._options.target + self._image_path = image_path + self._stdout_fd = None + self._cmd = None + self._env = None + self._proc = None + self._read_buffer = '' + + cmd = [] + # Hook for injecting valgrind or other runtime instrumentation, + # used by e.g. tools/valgrind/valgrind_tests.py. + wrapper = os.environ.get("BROWSER_WRAPPER", None) + if wrapper != None: + cmd += [wrapper] + if self._port._options.wrapper: + # This split() isn't really what we want -- it incorrectly will + # split quoted strings within the wrapper argument -- but in + # practice it shouldn't come up and the --help output warns + # about it anyway. + cmd += self._options.wrapper.split() + # FIXME: Using arch here masks any possible file-not-found errors from a non-existant driver executable. + cmd += ['arch', '-i386', port._path_to_driver(), '-'] + + # FIXME: This is a hack around our lack of ImageDiff support for now. + if not self._port._options.no_pixel_tests: + logging.warn("This port does not yet support pixel tests.") + self._port._options.no_pixel_tests = True + #cmd.append('--pixel-tests') + + #if driver_options: + # cmd += driver_options + env = os.environ + env['DYLD_FRAMEWORK_PATH'] = self._port._build_path() + self._cmd = cmd + self._env = env + self.restart() + + def poll(self): + return self._proc.poll() + + def restart(self): + self.stop() + self._proc = subprocess.Popen(self._cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=self._env) + + def returncode(self): + return self._proc.returncode + + def run_test(self, uri, timeoutms, image_hash): + output = [] + error = [] + image = '' + crash = False + timeout = False + actual_uri = None + actual_image_hash = None + + if uri.startswith("file:///"): + cmd = uri[7:] + else: + cmd = uri + + if image_hash: + cmd += "'" + image_hash + cmd += "\n" + + self._proc.stdin.write(cmd) + self._stdout_fd = self._proc.stdout.fileno() + fl = fcntl.fcntl(self._stdout_fd, fcntl.F_GETFL) + fcntl.fcntl(self._stdout_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) + + stop_time = time.time() + (int(timeoutms) / 1000.0) + resp = '' + (timeout, line) = self._read_line(timeout, stop_time) + resp += line + have_seen_content_type = False + while not timeout and line.rstrip() != "#EOF": + # Make sure we haven't crashed. + if line == '' and self.poll() is not None: + # This is hex code 0xc000001d, which is used for abrupt + # termination. This happens if we hit ctrl+c from the prompt + # and we happen to be waiting on the test_shell. + # sdoyon: Not sure for which OS and in what circumstances the + # above code is valid. What works for me under Linux to detect + # ctrl+c is for the subprocess returncode to be negative + # SIGINT. And that agrees with the subprocess documentation. + if (-1073741510 == self.returncode() or + - signal.SIGINT == self.returncode()): + raise KeyboardInterrupt + crash = True + break + + elif (line.startswith('Content-Type:') and not + have_seen_content_type): + have_seen_content_type = True + pass + else: + output.append(line) + + (timeout, line) = self._read_line(timeout, stop_time) + resp += line + + # Now read a second block of text for the optional image data + image_length = 0 + (timeout, line) = self._read_line(timeout, stop_time) + resp += line + HASH_HEADER = 'ActualHash: ' + LENGTH_HEADER = 'Content-Length: ' + while not timeout and not crash and line.rstrip() != "#EOF": + if line == '' and self.poll() is not None: + if (-1073741510 == self.returncode() or + - signal.SIGINT == self.returncode()): + raise KeyboardInterrupt + crash = True + break + elif line.startswith(HASH_HEADER): + actual_image_hash = line[len(HASH_HEADER):].strip() + elif line.startswith('Content-Type:'): + pass + elif line.startswith(LENGTH_HEADER): + image_length = int(line[len(LENGTH_HEADER):]) + elif image_length: + image += line + + (timeout, line) = self._read_line(timeout, stop_time, image_length) + resp += line + + if timeout: + self.restart() + + if self._image_path and len(self._image_path): + image_file = file(self._image_path, "wb") + image_file.write(image) + image_file.close() + + return (crash, timeout, actual_image_hash, + ''.join(output), ''.join(error)) + pass + + def stop(self): + if self._proc: + self._proc.stdin.close() + self._proc.stdout.close() + if self._proc.stderr: + self._proc.stderr.close() + if (sys.platform not in ('win32', 'cygwin') and + not self._proc.poll()): + # Closing stdin/stdout/stderr hangs sometimes on OS X. + null = open(os.devnull, "w") + subprocess.Popen(["kill", "-9", + str(self._proc.pid)], stderr=null) + null.close() + + def _read_line(self, timeout, stop_time, image_length=0): + now = time.time() + read_fds = [] + + # first check to see if we have a line already read or if we've + # read the entire image + if image_length and len(self._read_buffer) >= image_length: + out = self._read_buffer[0:image_length] + self._read_buffer = self._read_buffer[image_length:] + return (timeout, out) + + idx = self._read_buffer.find('\n') + if not image_length and idx != -1: + out = self._read_buffer[0:idx + 1] + self._read_buffer = self._read_buffer[idx + 1:] + return (timeout, out) + + # If we've timed out, return just what we have, if anything + if timeout or now >= stop_time: + out = self._read_buffer + self._read_buffer = '' + return (True, out) + + (read_fds, write_fds, err_fds) = select.select( + [self._stdout_fd], [], [], stop_time - now) + try: + if timeout or len(read_fds) == 1: + self._read_buffer += self._proc.stdout.read() + except IOError, e: + read = [] + return self._read_line(timeout, stop_time) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/path_utils.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/path_utils.py deleted file mode 100644 index 70b8c03..0000000 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/path_utils.py +++ /dev/null @@ -1,395 +0,0 @@ -#!/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. -# * 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. - -"""This package contains utility methods for manipulating paths and -filenames for test results and baselines. It also contains wrappers -of a few routines in port/ so that the port package can -be considered a 'protected' package - i.e., this file should be -the only file that ever includes port. This leads to -us including a few things that don't really have anything to do - with paths, unfortunately.""" - -import errno -import os -import stat -import sys - -import port -import chromium_win -import chromium_mac -import chromium_linux - -# Cache some values so we don't have to recalculate them. _basedir is -# used by PathFromBase() and caches the full (native) path to the top -# of the source tree (/src). _baseline_search_path is used by -# ExpectedBaselines() and caches the list of native paths to search -# for baseline results. -_basedir = None -_baseline_search_path = None - - -class PathNotFound(Exception): - pass - - -def layout_tests_dir(): - """Returns the fully-qualified path to the directory containing the input - data for the specified layout test.""" - return path_from_base('third_party', 'WebKit', 'LayoutTests') - - -def chromium_baseline_path(platform=None): - """Returns the full path to the directory containing expected - baseline results from chromium ports. If |platform| is None, the - currently executing platform is used. - - Note: although directly referencing individual port/* files is - usually discouraged, we allow it here so that the rebaselining tool can - pull baselines for platforms other than the host platform.""" - - # Normalize the platform string. - platform = platform_name(platform) - if platform.startswith('chromium-mac'): - return chromium_mac.baseline_path(platform) - elif platform.startswith('chromium-win'): - return chromium_win.baseline_path(platform) - elif platform.startswith('chromium-linux'): - return chromium_linux.baseline_path(platform) - - return port.baseline_path() - - -def webkit_baseline_path(platform): - """Returns the full path to the directory containing expected - baseline results from WebKit ports.""" - return path_from_base('third_party', 'WebKit', 'LayoutTests', - 'platform', platform) - - -def baseline_search_path(platform=None): - """Returns the list of directories to search for baselines/results for a - given platform, in order of preference. Paths are relative to the top of - the source tree. If parameter platform is None, returns the list for the - current platform that the script is running on. - - Note: although directly referencing individual port/* files is - usually discouraged, we allow it here so that the rebaselining tool can - pull baselines for platforms other than the host platform.""" - - # Normalize the platform name. - platform = platform_name(platform) - if platform.startswith('chromium-mac'): - return chromium_mac.baseline_search_path(platform) - elif platform.startswith('chromium-win'): - return chromium_win.baseline_search_path(platform) - elif platform.startswith('chromium-linux'): - return chromium_linux.baseline_search_path(platform) - return port.baseline_search_path() - - -def expected_baselines(filename, suffix, platform=None, all_baselines=False): - """Given a test name, finds where the baseline results are located. - - Args: - filename: absolute filename to test file - suffix: file suffix of the expected results, including dot; e.g. '.txt' - or '.png'. This should not be None, but may be an empty string. - platform: layout test platform: 'win', 'linux' or 'mac'. Defaults to - the current platform. - all_baselines: If True, return an ordered list of all baseline paths - for the given platform. If False, return only the first - one. - Returns - a list of ( platform_dir, results_filename ), where - platform_dir - abs path to the top of the results tree (or test tree) - results_filename - relative path from top of tree to the results file - (os.path.join of the two gives you the full path to the file, - unless None was returned.) - Return values will be in the format appropriate for the current platform - (e.g., "\\" for path separators on Windows). If the results file is not - found, then None will be returned for the directory, but the expected - relative pathname will still be returned. - """ - global _baseline_search_path - global _search_path_platform - testname = os.path.splitext(relative_test_filename(filename))[0] - - baseline_filename = testname + '-expected' + suffix - - if (_baseline_search_path is None) or (_search_path_platform != platform): - _baseline_search_path = baseline_search_path(platform) - _search_path_platform = platform - - baselines = [] - for platform_dir in _baseline_search_path: - if os.path.exists(os.path.join(platform_dir, baseline_filename)): - baselines.append((platform_dir, baseline_filename)) - - if not all_baselines and baselines: - return baselines - - # If it wasn't found in a platform directory, return the expected result - # in the test directory, even if no such file actually exists. - platform_dir = layout_tests_dir() - if os.path.exists(os.path.join(platform_dir, baseline_filename)): - baselines.append((platform_dir, baseline_filename)) - - if baselines: - return baselines - - return [(None, baseline_filename)] - - -def expected_filename(filename, suffix): - """Given a test name, returns an absolute path to its expected results. - - If no expected results are found in any of the searched directories, the - directory in which the test itself is located will be returned. The return - value is in the format appropriate for the platform (e.g., "\\" for - path separators on windows). - - Args: - filename: absolute filename to test file - suffix: file suffix of the expected results, including dot; e.g. '.txt' - or '.png'. This should not be None, but may be an empty string. - platform: the most-specific directory name to use to build the - search list of directories, e.g., 'chromium-win', or - 'chromium-mac-leopard' (we follow the WebKit format) - """ - platform_dir, baseline_filename = expected_baselines(filename, suffix)[0] - if platform_dir: - return os.path.join(platform_dir, baseline_filename) - return os.path.join(layout_tests_dir(), baseline_filename) - - -def relative_test_filename(filename): - """Provide the filename of the test relative to the layout tests - directory as a unix style path (a/b/c).""" - return _win_path_to_unix(filename[len(layout_tests_dir()) + 1:]) - - -def _win_path_to_unix(path): - """Convert a windows path to use unix-style path separators (a/b/c).""" - return path.replace('\\', '/') - -# -# Routines that are arguably platform-specific but have been made -# generic for now -# - - -def filename_to_uri(full_path): - """Convert a test file to a URI.""" - LAYOUTTEST_HTTP_DIR = "http/tests/" - LAYOUTTEST_WEBSOCKET_DIR = "websocket/tests/" - - relative_path = _win_path_to_unix(relative_test_filename(full_path)) - port = None - use_ssl = False - - if relative_path.startswith(LAYOUTTEST_HTTP_DIR): - # http/tests/ run off port 8000 and ssl/ off 8443 - relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):] - port = 8000 - elif relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR): - # websocket/tests/ run off port 8880 and 9323 - # Note: the root is /, not websocket/tests/ - port = 8880 - - # Make http/tests/local run as local files. This is to mimic the - # logic in run-webkit-tests. - # TODO(jianli): Consider extending this to "media/". - if port and not relative_path.startswith("local/"): - if relative_path.startswith("ssl/"): - port += 443 - protocol = "https" - else: - protocol = "http" - return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path) - - if sys.platform in ('cygwin', 'win32'): - return "file:///" + get_absolute_path(full_path) - return "file://" + get_absolute_path(full_path) - - -def get_absolute_path(path): - """Returns an absolute UNIX path.""" - return _win_path_to_unix(os.path.abspath(path)) - - -def maybe_make_directory(*path): - """Creates the specified directory if it doesn't already exist.""" - try: - os.makedirs(os.path.join(*path)) - except OSError, e: - if e.errno != errno.EEXIST: - raise - - -def path_from_base(*comps): - """Returns an absolute filename from a set of components specified - relative to the top of the source tree. If the path does not exist, - the exception PathNotFound is raised.""" - global _basedir - if _basedir == None: - # We compute the top of the source tree by finding the absolute - # path of this source file, and then climbing up three directories - # as given in subpath. If we move this file, subpath needs to be - # updated. - path = os.path.abspath(__file__) - subpath = os.path.join('third_party', 'WebKit') - _basedir = path[:path.index(subpath)] - path = os.path.join(_basedir, *comps) - if not os.path.exists(path): - raise PathNotFound('could not find %s' % (path)) - return path - - -def remove_directory(*path): - """Recursively removes a directory, even if it's marked read-only. - - Remove the directory located at *path, if it exists. - - shutil.rmtree() doesn't work on Windows if any of the files or directories - are read-only, which svn repositories and some .svn files are. We need to - be able to force the files to be writable (i.e., deletable) as we traverse - the tree. - - Even with all this, Windows still sometimes fails to delete a file, citing - a permission error (maybe something to do with antivirus scans or disk - indexing). The best suggestion any of the user forums had was to wait a - bit and try again, so we do that too. It's hand-waving, but sometimes it - works. :/ - """ - file_path = os.path.join(*path) - if not os.path.exists(file_path): - return - - win32 = False - if sys.platform == 'win32': - win32 = True - # Some people don't have the APIs installed. In that case we'll do - # without. - try: - win32api = __import__('win32api') - win32con = __import__('win32con') - except ImportError: - win32 = False - - def remove_with_retry(rmfunc, path): - os.chmod(path, stat.S_IWRITE) - if win32: - win32api.SetFileAttributes(path, - win32con.FILE_ATTRIBUTE_NORMAL) - try: - return rmfunc(path) - except EnvironmentError, e: - if e.errno != errno.EACCES: - raise - print 'Failed to delete %s: trying again' % repr(path) - time.sleep(0.1) - return rmfunc(path) - else: - - def remove_with_retry(rmfunc, path): - if os.path.islink(path): - return os.remove(path) - else: - return rmfunc(path) - - for root, dirs, files in os.walk(file_path, topdown=False): - # For POSIX: making the directory writable guarantees removability. - # Windows will ignore the non-read-only bits in the chmod value. - os.chmod(root, 0770) - for name in files: - remove_with_retry(os.remove, os.path.join(root, name)) - for name in dirs: - remove_with_retry(os.rmdir, os.path.join(root, name)) - - remove_with_retry(os.rmdir, file_path) - -# -# Wrappers around port/ -# - - -def platform_name(platform=None): - """Returns the appropriate chromium platform name for |platform|. If - |platform| is None, returns the name of the chromium platform on the - currently running system. If |platform| is of the form 'chromium-*', - it is returned unchanged, otherwise 'chromium-' is prepended.""" - if platform == None: - return port.platform_name() - if not platform.startswith('chromium-'): - platform = "chromium-" + platform - return platform - - -def platform_version(): - return port.platform_version() - - -def lighttpd_executable_path(): - return port.lighttpd_executable_path() - - -def lighttpd_module_path(): - return port.lighttpd_module_path() - - -def lighttpd_php_path(): - return port.lighttpd_php_path() - - -def wdiff_path(): - return port.wdiff_path() - - -def test_shell_path(target): - return port.test_shell_path(target) - - -def image_diff_path(target): - return port.image_diff_path(target) - - -def layout_test_helper_path(target): - return port.layout_test_helper_path(target) - - -def fuzzy_match_path(): - return port.fuzzy_match_path() - - -def shut_down_http_server(server_pid): - return port.shut_down_http_server(server_pid) - - -def kill_all_test_shells(): - port.kill_all_test_shells() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py new file mode 100644 index 0000000..0bc6e7c --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py @@ -0,0 +1,144 @@ +#!/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. +# * Neither the Google name 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. + +"""Dummy Port implementation used for testing.""" + +import os +import time + +import base + + +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 base_platforms(self): + return ('test',) + + def baseline_path(self): + curdir = os.path.abspath(__file__) + self.topdir = curdir[0:curdir.index("WebKitTools")] + return os.path.join(self.topdir, 'LayoutTests', 'platform', 'test') + + def baseline_search_path(self): + return [self.baseline_path()] + + def check_sys_deps(self): + return True + + def diff_image(self, actual_filename, expected_filename, diff_filename): + return False + + def compare_text(self, actual_text, expected_text): + return False + + def diff_text(self, actual_text, expected_text, + actual_filename, expected_filename): + return '' + + def name(self): + return self._name + + def num_cores(self): + return int(os.popen2("sysctl -n hw.ncpu")[1].read()) + + def options(self): + return self._options + + def results_directory(self): + return '/tmp' + self._options.results_directory + + def setup_test_run(self): + pass + + def show_results_html_file(self, filename): + pass + + def start_driver(self, image_path, options): + return TestDriver(image_path, options, self) + + def start_http_server(self): + pass + + def start_websocket_server(self): + pass + + def start_helper(self): + pass + + def stop_http_server(self): + pass + + def stop_websocket_server(self): + pass + + def stop_helper(self): + pass + + def test_expectations(self): + return '' + + def test_base_platform_names(self): + return ('test',) + + def test_platform_name(self): + return 'test' + + def test_platform_names(self): + return self.test_base_platform_names() + + def version(): + return '' + + def wdiff_text(self, actual_filename, expected_filename): + return '' + + +class TestDriver(base.Driver): + """Test/Dummy implementation of the DumpRenderTree interface.""" + + def __init__(self, image_path, test_driver_options, port): + self._driver_options = test_driver_options + self._image_path = image_path + self._port = port + + def poll(self): + return True + + def returncode(self): + return 0 + + def run_test(self, uri, timeoutms, image_hash): + return (False, False, image_hash, '', None) + + def stop(self): + pass diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py index c6c7527..ba8a5e9 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py @@ -39,8 +39,6 @@ import tempfile import time import urllib -import path_utils -import port import http_server _WS_LOG_PREFIX = 'pywebsocket.ws.log-' @@ -51,37 +49,29 @@ _DEFAULT_WSS_PORT = 9323 def url_is_alive(url): - """Checks to see if we get an http response from |url|. - We poll the url 5 times with a 1 second delay. If we don't - get a reply in that time, we give up and assume the httpd - didn't start properly. - - Args: - url: The URL to check. - Return: - True if the url is alive. - """ - wait_time = 5 - while wait_time > 0: - try: - response = urllib.urlopen(url) - # Server is up and responding. - return True - except IOError: - pass - wait_time -= 1 - # Wait a second and try again. - time.sleep(1) - - return False - - -def remove_log_files(folder, starts_with): - files = os.listdir(folder) - for file in files: - if file.startswith(starts_with): - full_path = os.path.join(folder, file) - os.remove(full_path) + """Checks to see if we get an http response from |url|. + We poll the url 5 times with a 1 second delay. If we don't + get a reply in that time, we give up and assume the httpd + didn't start properly. + + Args: + url: The URL to check. + Return: + True if the url is alive. + """ + wait_time = 5 + while wait_time > 0: + try: + response = urllib.urlopen(url) + # Server is up and responding. + return True + except IOError: + pass + wait_time -= 1 + # Wait a second and try again. + time.sleep(1) + + return False class PyWebSocketNotStarted(Exception): @@ -94,18 +84,15 @@ class PyWebSocketNotFound(Exception): class PyWebSocket(http_server.Lighttpd): - def __init__(self, output_dir, port=_DEFAULT_WS_PORT, - root=None, - use_tls=False, - private_key=http_server.Lighttpd._pem_file, - certificate=http_server.Lighttpd._pem_file, + def __init__(self, port_obj, output_dir, port=_DEFAULT_WS_PORT, + root=None, use_tls=False, register_cygwin=None, pidfile=None): """Args: output_dir: the absolute path to the layout test result directory """ - http_server.Lighttpd.__init__(self, output_dir, - port=port, + http_server.Lighttpd.__init__(self, port_obj, output_dir, + port=_DEFAULT_WS_PORT, root=root, register_cygwin=register_cygwin) self._output_dir = output_dir @@ -113,8 +100,8 @@ class PyWebSocket(http_server.Lighttpd): self._port = port self._root = root self._use_tls = use_tls - self._private_key = private_key - self._certificate = certificate + self._private_key = self._pem_file + self._certificate = self._pem_file if self._port: self._port = int(self._port) if self._use_tls: @@ -131,12 +118,10 @@ class PyWebSocket(http_server.Lighttpd): os.path.join(self._root, 'websocket', 'tests')) else: try: - self._web_socket_tests = path_utils.path_from_base( - 'third_party', 'WebKit', 'LayoutTests', 'websocket', - 'tests') - self._layout_tests = path_utils.path_from_base( - 'third_party', 'WebKit', 'LayoutTests') - except path_utils.PathNotFound: + self._layout_tests = self._port_obj.layout_tests_dir() + self._web_socket_tests = os.path.join(self._layout_tests, + 'websocket', 'tests') + except: self._web_socket_tests = None def start(self): @@ -155,7 +140,7 @@ class PyWebSocket(http_server.Lighttpd): log_file_name = log_prefix + time_str # Remove old log files. We only need to keep the last ones. - remove_log_files(self._output_dir, log_prefix) + self.remove_log_files(self._output_dir, log_prefix) error_log = os.path.join(self._output_dir, log_file_name + "-err.txt") @@ -163,11 +148,12 @@ class PyWebSocket(http_server.Lighttpd): self._wsout = open(output_log, "w") python_interp = sys.executable - pywebsocket_base = path_utils.path_from_base( - 'third_party', 'WebKit', 'WebKitTools', 'pywebsocket') - pywebsocket_script = path_utils.path_from_base( - 'third_party', 'WebKit', 'WebKitTools', 'pywebsocket', - 'mod_pywebsocket', 'standalone.py') + pywebsocket_base = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname( + os.path.dirname(os.path.dirname( + os.path.abspath(__file__)))))), 'pywebsocket') + pywebsocket_script = os.path.join(pywebsocket_base, 'mod_pywebsocket', + 'standalone.py') start_cmd = [ python_interp, pywebsocket_script, '-p', str(self._port), @@ -193,12 +179,13 @@ class PyWebSocket(http_server.Lighttpd): env = os.environ if sys.platform in ('cygwin', 'win32'): env['PATH'] = '%s;%s' % ( - path_utils.path_from_base('third_party', 'cygwin', 'bin'), + self._port_obj.path_from_chromium_base('third_party', + 'cygwin', 'bin'), env['PATH']) if sys.platform == 'win32' and self._register_cygwin: - setup_mount = path_utils.path_from_base('third_party', 'cygwin', - 'setup_mount.bat') + setup_mount = self._port_obj.path_from_chromium_base( + 'third_party', 'cygwin', 'setup_mount.bat') subprocess.Popen(setup_mount).wait() env['PYTHONPATH'] = (pywebsocket_base + os.path.pathsep + @@ -255,7 +242,7 @@ class PyWebSocket(http_server.Lighttpd): 'Failed to find %s server pid.' % self._server_name) logging.debug('Shutting down %s server %d.' % (self._server_name, pid)) - port.kill_process(pid) + self._port_obj._kill_process(pid) if self._process: self._process.wait() 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 414baaf..83cf99de 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py @@ -54,7 +54,7 @@ import urllib import webbrowser import zipfile -from port import path_utils +from layout_package import path_utils from layout_package import test_expectations from test_types import image_diff from test_types import text_diff @@ -76,6 +76,7 @@ ARCHIVE_DIR_NAME_DICT = {'win': 'webkit-rel', 'linux-canary': 'webkit-rel-linux-webkit-org'} +# FIXME: Should be rolled into webkitpy.Executive def run_shell_with_return_code(command, print_output=False): """Executes a command and returns the output and process return code. @@ -109,6 +110,7 @@ def run_shell_with_return_code(command, print_output=False): return output, p.returncode +# FIXME: Should be rolled into webkitpy.Executive def run_shell(command, print_output=False): """Executes a command and returns the output. diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_chromium_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_chromium_webkit_tests.py index 571ae3f..f0b68ee 100755 --- a/WebKitTools/Scripts/webkitpy/layout_tests/run_chromium_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_chromium_webkit_tests.py @@ -54,31 +54,24 @@ import Queue import random import re import shutil -import subprocess import sys import time import traceback +import simplejson + from layout_package import test_expectations from layout_package import json_layout_results_generator from layout_package import metered_stream from layout_package import test_failures from layout_package import test_shell_thread from layout_package import test_files - -import port -from port import apache_http_server -from port import http_server -from port import path_utils -from port import websocket_server - from test_types import fuzzy_image_diff from test_types import image_diff from test_types import test_type_base from test_types import text_diff -sys.path.append(path_utils.path_from_base('third_party')) -import simplejson +import port # Indicates that we want detailed progress updates in the output (prints # directory-by-directory feedback). @@ -96,17 +89,16 @@ TestExpectationsFile = test_expectations.TestExpectationsFile class TestInfo: """Groups information about a test for easy passing of data.""" - def __init__(self, filename, timeout): + def __init__(self, port, filename, timeout): """Generates the URI and stores the filename and timeout for this test. Args: filename: Full path to the test. timeout: Timeout for running the test in TestShell. """ self.filename = filename - self.uri = path_utils.filename_to_uri(filename) + self.uri = port.filename_to_uri(filename) self.timeout = timeout - expected_hash_file = path_utils.expected_filename(filename, - '.checksum') + expected_hash_file = port.expected_filename(filename, '.checksum') try: self.image_hash = open(expected_hash_file, "r").read() except IOError, e: @@ -175,24 +167,18 @@ class TestRunner: NUM_RETRY_ON_UNEXPECTED_FAILURE = 1 - def __init__(self, options, meter): + def __init__(self, port, options, meter): """Initialize test runner data structures. Args: + port: an object implementing port-specific options: a dictionary of command line options meter: a MeteredStream object to record updates to. """ + self._port = port self._options = options self._meter = meter - if options.use_apache: - self._http_server = apache_http_server.LayoutTestApacheHttpd( - options.results_directory) - else: - self._http_server = http_server.Lighttpd(options.results_directory) - - self._websocket_server = websocket_server.PyWebSocket( - options.results_directory) # disable wss server. need to install pyOpenSSL on buildbots. # self._websocket_secure_server = websocket_server.PyWebSocket( # options.results_directory, use_tls=True, port=9323) @@ -203,8 +189,6 @@ class TestRunner: # a set of test files, and the same tests as a list self._test_files = set() self._test_files_list = None - self._file_dir = path_utils.path_from_base('webkit', 'tools', - 'layout_tests') self._result_queue = Queue.Queue() # These are used for --log detailed-progress to track status by @@ -219,20 +203,18 @@ class TestRunner: logging.debug("flushing stderr") sys.stderr.flush() logging.debug("stopping http server") - # Stop the http server. - self._http_server.stop() - # Stop the Web Socket / Web Socket Secure servers. - self._websocket_server.stop() - # self._websocket_secure_server.Stop() + self._port.stop_http_server() + logging.debug("stopping websocket server") + self._port.stop_websocket_server() def gather_file_paths(self, paths): """Find all the files to test. Args: paths: a list of globs to use instead of the defaults.""" - self._test_files = test_files.gather_test_files(paths) + self._test_files = test_files.gather_test_files(self._port, paths) - def parse_expectations(self, platform, is_debug_mode): + def parse_expectations(self, test_platform_name, is_debug_mode): """Parse the expectations from the test_list files and return a data structure holding them. Throws an error if the test_list files have invalid syntax.""" @@ -242,9 +224,10 @@ class TestRunner: test_files = self._test_files try: - self._expectations = test_expectations.TestExpectations(test_files, - self._file_dir, platform, is_debug_mode, - self._options.lint_test_files) + expectations_str = self._port.test_expectations() + self._expectations = test_expectations.TestExpectations( + self._port, test_files, expectations_str, test_platform_name, + is_debug_mode, self._options.lint_test_files) return self._expectations except Exception, err: if self._options.lint_test_files: @@ -359,7 +342,8 @@ class TestRunner: self._test_files = set(self._test_files_list) self._expectations = self.parse_expectations( - path_utils.platform_name(), self._options.target == 'Debug') + self._port.test_platform_name(), + self._options.target == 'Debug') self._test_files = set(files) self._test_files_list = files @@ -424,8 +408,9 @@ class TestRunner: is used for looking up the timeout value (in ms) to use for the given test.""" if self._expectations.has_modifier(test_file, test_expectations.SLOW): - return TestInfo(test_file, self._options.slow_time_out_ms) - return TestInfo(test_file, self._options.time_out_ms) + return TestInfo(self._port, test_file, + self._options.slow_time_out_ms) + return TestInfo(self._port, test_file, self._options.time_out_ms) def _get_test_file_queue(self, test_files): """Create the thread safe queue of lists of (test filenames, test URIs) @@ -490,6 +475,7 @@ class TestRunner: """Returns the tuple of arguments for tests and for test_shell.""" shell_args = [] test_args = test_type_base.TestArguments() + png_path = None if not self._options.no_pixel_tests: png_path = os.path.join(self._options.results_directory, "png_result%s.png" % index) @@ -506,7 +492,7 @@ class TestRunner: if self._options.gp_fault_error_box: shell_args.append('--gp-fault-error-box') - return (test_args, shell_args) + return test_args, png_path, shell_args def _contains_tests(self, subdir): for test_file in self._test_files_list: @@ -514,23 +500,12 @@ class TestRunner: return True return False - def _instantiate_test_shell_threads(self, test_shell_binary, test_files, - result_summary): + def _instantiate_test_shell_threads(self, test_files, result_summary): """Instantitates and starts the TestShellThread(s). Return: The list of threads. """ - test_shell_command = [test_shell_binary] - - if self._options.wrapper: - # This split() isn't really what we want -- it incorrectly will - # split quoted strings within the wrapper argument -- but in - # practice it shouldn't come up and the --help output warns - # about it anyway. - test_shell_command = (self._options.wrapper.split() + - test_shell_command) - filename_queue = self._get_test_file_queue(test_files) # Instantiate TestShellThreads and start them. @@ -539,15 +514,16 @@ class TestRunner: # Create separate TestTypes instances for each thread. test_types = [] for t in self._test_types: - test_types.append(t(self._options.platform, + test_types.append(t(self._port, self._options.platform, self._options.results_directory)) - test_args, shell_args = self._get_test_shell_args(i) - thread = test_shell_thread.TestShellThread(filename_queue, + test_args, png_path, shell_args = self._get_test_shell_args(i) + thread = test_shell_thread.TestShellThread(self._port, + filename_queue, self._result_queue, - test_shell_command, test_types, test_args, + png_path, shell_args, self._options) if self._is_single_threaded(): @@ -558,19 +534,11 @@ class TestRunner: return threads - def _stop_layout_test_helper(self, proc): - """Stop the layout test helper and closes it down.""" - if proc: - logging.debug("Stopping layout test helper") - proc.stdin.write("x\n") - proc.stdin.close() - proc.wait() - def _is_single_threaded(self): """Returns whether we should run all the tests in the main thread.""" return int(self._options.num_test_shells) == 1 - def _run_tests(self, test_shell_binary, file_list, result_summary): + def _run_tests(self, file_list, result_summary): """Runs the tests in the file_list. Return: A tuple (failures, thread_timings, test_timings, @@ -584,9 +552,8 @@ class TestRunner: in the form {filename:filename, test_run_time:test_run_time} result_summary: summary object to populate with the results """ - threads = self._instantiate_test_shell_threads(test_shell_binary, - file_list, - result_summary) + threads = self._instantiate_test_shell_threads(file_list, + result_summary) # Wait for the threads to finish and collect test failures. failures = {} @@ -612,7 +579,7 @@ class TestRunner: except KeyboardInterrupt: for thread in threads: thread.cancel() - self._stop_layout_test_helper(layout_test_helper_proc) + self._port.stop_helper() raise for thread in threads: # Check whether a TestShellThread died before normal completion. @@ -642,42 +609,20 @@ class TestRunner: if not self._test_files: return 0 start_time = time.time() - test_shell_binary = path_utils.test_shell_path(self._options.target) # Start up any helper needed - layout_test_helper_proc = None if not self._options.no_pixel_tests: - helper_path = path_utils.layout_test_helper_path( - self._options.target) - if len(helper_path): - logging.debug("Starting layout helper %s" % helper_path) - layout_test_helper_proc = subprocess.Popen( - [helper_path], stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=None) - is_ready = layout_test_helper_proc.stdout.readline() - if not is_ready.startswith('ready'): - logging.error("layout_test_helper failed to be ready") - - # Check that the system dependencies (themes, fonts, ...) are correct. - if not self._options.nocheck_sys_deps: - proc = subprocess.Popen([test_shell_binary, - "--check-layout-test-sys-deps"]) - if proc.wait() != 0: - logging.info("Aborting because system dependencies check " - "failed.\n To override, invoke with " - "--nocheck-sys-deps") - sys.exit(1) + self._port.start_helper() if self._contains_tests(self.HTTP_SUBDIR): - self._http_server.start() + self._port.start_http_server() if self._contains_tests(self.WEBSOCKET_SUBDIR): - self._websocket_server.start() + self._port.start_websocket_server() # self._websocket_secure_server.Start() thread_timings, test_timings, individual_test_timings = ( - self._run_tests(test_shell_binary, self._test_files_list, - result_summary)) + self._run_tests(self._test_files_list, result_summary)) # We exclude the crashes from the list of results to retry, because # we want to treat even a potentially flaky crash as an error. @@ -689,10 +634,10 @@ class TestRunner: logging.debug("Retrying %d unexpected failure(s)" % len(failures)) retries += 1 retry_summary = ResultSummary(self._expectations, failures.keys()) - self._run_tests(test_shell_binary, failures.keys(), retry_summary) + self._run_tests(failures.keys(), retry_summary) failures = self._get_failures(retry_summary, include_crashes=True) - self._stop_layout_test_helper(layout_test_helper_proc) + self._port.stop_helper() end_time = time.time() write = create_logging_writer(self._options, 'timing') @@ -777,7 +722,7 @@ class TestRunner: next_test = self._test_files_list[self._current_test_number] next_dir = os.path.dirname( - path_utils.relative_test_filename(next_test)) + self._port.relative_test_filename(next_test)) if self._current_progress_str == "": self._current_progress_str = "%s: " % (next_dir) self._current_dir = next_dir @@ -803,7 +748,7 @@ class TestRunner: next_test = self._test_files_list[self._current_test_number] next_dir = os.path.dirname( - path_utils.relative_test_filename(next_test)) + self._port.relative_test_filename(next_test)) if result_summary.remaining: remain_str = " (%d)" % (result_summary.remaining) @@ -874,7 +819,7 @@ class TestRunner: # Note that if a test crashed in the original run, we ignore # whether or not it crashed when we retried it (if we retried it), # and always consider the result not flaky. - test = path_utils.relative_test_filename(filename) + test = self._port.relative_test_filename(filename) expected = self._expectations.get_expectations_string(filename) actual = [keywords[result]] @@ -943,7 +888,7 @@ class TestRunner: expectations_file.close() json_layout_results_generator.JSONLayoutResultsGenerator( - self._options.builder_name, self._options.build_name, + self._port, self._options.builder_name, self._options.build_name, self._options.build_number, self._options.results_directory, BUILDER_BASE_URL, individual_test_timings, self._expectations, result_summary, self._test_files_list) @@ -1110,7 +1055,7 @@ class TestRunner: write(title) for test_tuple in test_list: filename = test_tuple.filename[len( - path_utils.layout_tests_dir()) + 1:] + self._port.layout_tests_dir()) + 1:] filename = filename.replace('\\', '/') test_run_time = round(test_tuple.test_run_time, 1) write(" %s took %s seconds" % (filename, test_run_time)) @@ -1328,7 +1273,7 @@ class TestRunner: """Prints one unexpected test result line.""" desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result][0] self._meter.write(" %s -> unexpected %s\n" % - (path_utils.relative_test_filename(test), desc)) + (self._port.relative_test_filename(test), desc)) def _write_results_html_file(self, result_summary): """Write results.html which is a summary of tests that failed. @@ -1366,12 +1311,12 @@ class TestRunner: for test_file in test_files: test_failures = result_summary.failures.get(test_file, []) out_file.write("<p><a href='%s'>%s</a><br />\n" - % (path_utils.filename_to_uri(test_file), - path_utils.relative_test_filename(test_file))) + % (self._port.filename_to_uri(test_file), + self._port.relative_test_filename(test_file))) for failure in test_failures: out_file.write(" %s<br/>" % failure.result_html_output( - path_utils.relative_test_filename(test_file))) + self._port.relative_test_filename(test_file))) out_file.write("</p>\n") # footer @@ -1382,8 +1327,7 @@ class TestRunner: """Launches the test shell open to the results.html page.""" results_filename = os.path.join(self._options.results_directory, "results.html") - subprocess.Popen([path_utils.test_shell_path(self._options.target), - path_utils.filename_to_uri(results_filename)]) + self._port.show_results_html_file(results_filename) def _add_to_dict_of_lists(dict, key, value): @@ -1444,19 +1388,19 @@ def main(options, args): else: options.target = "Release" + port_obj = port.get(options.platform, options) + if not options.use_apache: options.use_apache = sys.platform in ('darwin', 'linux2') if options.results_directory.startswith("/"): # Assume it's an absolute path and normalize. - options.results_directory = path_utils.get_absolute_path( + options.results_directory = port_obj.get_absolute_path( options.results_directory) else: # If it's a relative path, make the output directory relative to # Debug or Release. - basedir = path_utils.path_from_base('webkit') - options.results_directory = path_utils.get_absolute_path( - os.path.join(basedir, options.target, options.results_directory)) + options.results_directory = port_obj.results_directory() if options.clobber_old_results: # Just clobber the actual test results directories since the other @@ -1466,12 +1410,9 @@ def main(options, args): if os.path.exists(path): shutil.rmtree(path) - # Ensure platform is valid and force it to the form 'chromium-<platform>'. - options.platform = path_utils.platform_name(options.platform) - if not options.num_test_shells: # TODO(ojan): Investigate perf/flakiness impact of using numcores + 1. - options.num_test_shells = port.get_num_cores() + options.num_test_shells = port_obj.num_cores() write = create_logging_writer(options, 'config') write("Running %s test_shells in parallel" % options.num_test_shells) @@ -1499,62 +1440,46 @@ def main(options, args): paths += read_test_files(options.test_list) # Create the output directory if it doesn't already exist. - path_utils.maybe_make_directory(options.results_directory) + port_obj.maybe_make_directory(options.results_directory) meter.update("Gathering files ...") - test_runner = TestRunner(options, meter) + test_runner = TestRunner(port_obj, options, meter) test_runner.gather_file_paths(paths) if options.lint_test_files: # Creating the expecations for each platform/target pair does all the # test list parsing and ensures it's correct syntax (e.g. no dupes). - for platform in TestExpectationsFile.PLATFORMS: + for platform in port_obj.test_platform_names(): test_runner.parse_expectations(platform, is_debug_mode=True) test_runner.parse_expectations(platform, is_debug_mode=False) print ("If there are no fail messages, errors or exceptions, then the " "lint succeeded.") sys.exit(0) - try: - test_shell_binary_path = path_utils.test_shell_path(options.target) - except path_utils.PathNotFound: - print "\nERROR: test_shell is not found. Be sure that you have built" - print "it and that you are using the correct build. This script" - print "will run the Release one by default. Use --debug to use the" - print "Debug build.\n" - sys.exit(1) + # Check that the system dependencies (themes, fonts, ...) are correct. + if not options.nocheck_sys_deps: + if not port_obj.check_sys_deps(): + sys.exit(1) write = create_logging_writer(options, "config") - write("Using platform '%s'" % options.platform) + write("Using port '%s'" % port_obj.name()) write("Placing test results in %s" % options.results_directory) if options.new_baseline: - write("Placing new baselines in %s" % - path_utils.chromium_baseline_path(options.platform)) - write("Using %s build at %s" % (options.target, test_shell_binary_path)) + write("Placing new baselines in %s" % port_obj.baseline_path()) + write("Using %s build" % options.target) if options.no_pixel_tests: write("Not running pixel tests") write("") meter.update("Parsing expectations ...") - test_runner.parse_expectations(options.platform, options.target == 'Debug') + test_runner.parse_expectations(port_obj.test_platform_name(), + options.target == 'Debug') meter.update("Preparing tests ...") write = create_logging_writer(options, "expected") result_summary = test_runner.prepare_lists_and_print_output(write) - if 'cygwin' == sys.platform: - logging.warn("#" * 40) - logging.warn("# UNEXPECTED PYTHON VERSION") - logging.warn("# This script should be run using the version of python") - logging.warn("# in third_party/python_24/") - logging.warn("#" * 40) - sys.exit(1) - - # Delete the disk cache if any to ensure a clean test run. - cachedir = os.path.split(test_shell_binary_path)[0] - cachedir = os.path.join(cachedir, "cache") - if os.path.exists(cachedir): - shutil.rmtree(cachedir) + port_obj.setup_test_run() test_runner.add_test_type(text_diff.TestTextDiff) if not options.no_pixel_tests: diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py index 100fd0d..89dd192 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py @@ -35,16 +35,14 @@ import errno import logging import os import shutil -import subprocess -from port import path_utils from layout_package import test_failures from test_types import test_type_base class FuzzyImageDiff(test_type_base.TestTypeBase): - def compare_output(self, filename, proc, output, test_args, target): + def compare_output(self, filename, output, test_args, target): """Implementation of CompareOutput that checks the output image and checksum against the expected files from the LayoutTest directory. """ @@ -54,7 +52,7 @@ class FuzzyImageDiff(test_type_base.TestTypeBase): if test_args.hash is None: return failures - expected_png_file = path_utils.expected_filename(filename, '.png') + expected_png_file = self._port.expected_filename(filename, '.png') if test_args.show_sources: logging.debug('Using %s' % expected_png_file) @@ -64,8 +62,7 @@ class FuzzyImageDiff(test_type_base.TestTypeBase): failures.append(test_failures.FailureMissingImage(self)) # Run the fuzzymatcher - r = subprocess.call([path_utils.fuzzy_match_path(), - test_args.png_path, expected_png_file]) + r = port.fuzzy_diff(test_args.png_path, expected_png_file) if r != 0: failures.append(test_failures.FailureFuzzyFailure(self)) 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 86b9659..1df7ca3 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py @@ -38,9 +38,7 @@ import errno import logging import os import shutil -import subprocess -from port import path_utils from layout_package import test_failures from test_types import test_type_base @@ -84,7 +82,7 @@ class ImageDiff(test_type_base.TestTypeBase): self._save_baseline_data(filename, png_data, ".png") self._save_baseline_data(filename, checksum, ".checksum") - def _create_image_diff(self, filename, target): + def _create_image_diff(self, port, filename, target): """Creates the visual diff of the expected/actual PNGs. Args: @@ -98,32 +96,14 @@ class ImageDiff(test_type_base.TestTypeBase): expected_filename = self.output_filename(filename, self.FILENAME_SUFFIX_EXPECTED + '.png') - global _compare_available - cmd = '' - try: - executable = path_utils.image_diff_path(target) - cmd = [executable, '--diff', actual_filename, expected_filename, - diff_filename] - except Exception, e: + _compare_available = True + result = port.diff_image(actual_filename, expected_filename, + diff_filename) + except ValueError: _compare_available = False - result = 1 - if _compare_available: - try: - result = subprocess.call(cmd) - except OSError, e: - if e.errno == errno.ENOENT or e.errno == errno.EACCES: - _compare_available = False - else: - raise e - except ValueError: - # work around a race condition in Python 2.4's implementation - # of subprocess.Popen - pass - global _compare_msg_printed - if not _compare_available and not _compare_msg_printed: _compare_msg_printed = True print('image_diff not found. Make sure you have a ' + target + @@ -131,7 +111,7 @@ class ImageDiff(test_type_base.TestTypeBase): return result - def compare_output(self, filename, proc, output, test_args, target): + def compare_output(self, port, filename, output, test_args, target): """Implementation of CompareOutput that checks the output image and checksum against the expected files from the LayoutTest directory. """ @@ -148,9 +128,9 @@ class ImageDiff(test_type_base.TestTypeBase): return failures # Compare hashes. - expected_hash_file = path_utils.expected_filename(filename, + expected_hash_file = self._port.expected_filename(filename, '.checksum') - expected_png_file = path_utils.expected_filename(filename, '.png') + expected_png_file = self._port.expected_filename(filename, '.png') if test_args.show_sources: logging.debug('Using %s' % expected_hash_file) @@ -166,8 +146,9 @@ class ImageDiff(test_type_base.TestTypeBase): if not os.path.isfile(expected_png_file): # Report a missing expected PNG file. - self.write_output_files(filename, '', '.checksum', test_args.hash, - expected_hash, diff=False, wdiff=False) + self.write_output_files(port, filename, '', '.checksum', + test_args.hash, expected_hash, + diff=False, wdiff=False) self._copy_output_png(filename, test_args.png_path, '-actual.png') failures.append(test_failures.FailureMissingImage(self)) return failures @@ -176,14 +157,15 @@ class ImageDiff(test_type_base.TestTypeBase): return failures - self.write_output_files(filename, '', '.checksum', test_args.hash, - expected_hash, diff=False, wdiff=False) + self.write_output_files(port, filename, '', '.checksum', + test_args.hash, expected_hash, + diff=False, wdiff=False) self._copy_output_png(filename, test_args.png_path, '-actual.png') self._copy_output_png(filename, expected_png_file, '-expected.png') # Even though we only use result in one codepath below but we # still need to call CreateImageDiff for other codepaths. - result = self._create_image_diff(filename, target) + result = self._create_image_diff(port, filename, target) if expected_hash == '': failures.append(test_failures.FailureMissingImageHash(self)) elif test_args.hash != expected_hash: @@ -196,7 +178,7 @@ class ImageDiff(test_type_base.TestTypeBase): return failures - def diff_files(self, file1, file2): + def diff_files(self, port, file1, file2): """Diff two image files. Args: @@ -208,17 +190,8 @@ class ImageDiff(test_type_base.TestTypeBase): """ try: - executable = path_utils.image_diff_path('Debug') - except Exception, e: - logging.warn('Failed to find image diff executable.') - return True - - cmd = [executable, file1, file2] - result = 1 - try: - result = subprocess.call(cmd) - except OSError, e: - logging.warn('Failed to compare image diff: %s', e) + result = port.diff_image(file1, file2) + except ValueError, e: return True return result == 1 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 79b7e34..efa2e8c 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 @@ -33,13 +33,9 @@ Also defines the TestArguments "struct" to pass them additional arguments. """ import cgi -import difflib import errno import logging import os.path -import subprocess - -from port import path_utils class TestArguments(object): @@ -74,7 +70,7 @@ class TestTypeBase(object): FILENAME_SUFFIX_WDIFF = "-wdiff.html" FILENAME_SUFFIX_COMPARE = "-diff.png" - def __init__(self, platform, root_output_dir): + def __init__(self, port, platform, root_output_dir): """Initialize a TestTypeBase object. Args: @@ -83,14 +79,15 @@ class TestTypeBase(object): root_output_dir: The unix style path to the output dir. """ self._root_output_dir = root_output_dir + self._port = port self._platform = platform def _make_output_directory(self, filename): """Creates the output directory (if needed) for a given test filename.""" output_filename = os.path.join(self._root_output_dir, - path_utils.relative_test_filename(filename)) - path_utils.maybe_make_directory(os.path.split(output_filename)[0]) + self._port.relative_test_filename(filename)) + self._port.maybe_make_directory(os.path.split(output_filename)[0]) def _save_baseline_data(self, filename, data, modifier): """Saves a new baseline file into the platform directory. @@ -104,13 +101,13 @@ class TestTypeBase(object): modifier: type of the result file, e.g. ".txt" or ".png" """ relative_dir = os.path.dirname( - path_utils.relative_test_filename(filename)) + self._port.relative_test_filename(filename)) output_dir = os.path.join( - path_utils.chromium_baseline_path(self._platform), relative_dir) + self._port.chromium_baseline_path(self._platform), relative_dir) output_file = os.path.basename(os.path.splitext(filename)[0] + self.FILENAME_SUFFIX_EXPECTED + modifier) - path_utils.maybe_make_directory(output_dir) + self._port.maybe_make_directory(output_dir) output_path = os.path.join(output_dir, output_file) logging.debug('writing new baseline to "%s"' % (output_path)) open(output_path, "wb").write(data) @@ -130,10 +127,10 @@ class TestTypeBase(object): The absolute windows path to the output filename """ output_filename = os.path.join(self._root_output_dir, - path_utils.relative_test_filename(filename)) + self._port.relative_test_filename(filename)) return os.path.splitext(output_filename)[0] + modifier - def compare_output(self, filename, proc, output, test_args, target): + def compare_output(self, port, filename, output, test_args, target): """Method that compares the output from the test with the expected value. @@ -141,7 +138,6 @@ class TestTypeBase(object): Args: filename: absolute filename to test file - proc: a reference to the test_shell process output: a string containing the output of the test test_args: a TestArguments object holding optional additional arguments @@ -152,8 +148,8 @@ class TestTypeBase(object): """ raise NotImplemented - def write_output_files(self, filename, test_type, file_type, output, - expected, diff=True, wdiff=False): + def write_output_files(self, port, filename, test_type, file_type, + output, expected, diff=True, wdiff=False): """Writes the test output, the expected output and optionally the diff between the two to files in the results directory. @@ -186,81 +182,15 @@ class TestTypeBase(object): return if diff: - diff = difflib.unified_diff(expected.splitlines(True), - output.splitlines(True), - expected_filename, - actual_filename) - + diff = port.diff_text(expected, output, expected_filename, + actual_filename) diff_filename = self.output_filename(filename, test_type + self.FILENAME_SUFFIX_DIFF + file_type) - open(diff_filename, "wb").write(''.join(diff)) + open(diff_filename, "wb").write(diff) if wdiff: # Shell out to wdiff to get colored inline diffs. - executable = path_utils.wdiff_path() - cmd = [executable, - '--start-delete=##WDIFF_DEL##', - '--end-delete=##WDIFF_END##', - '--start-insert=##WDIFF_ADD##', - '--end-insert=##WDIFF_END##', - expected_filename, - actual_filename] - filename = self.output_filename(filename, - test_type + self.FILENAME_SUFFIX_WDIFF) - - global _wdiff_available - - try: - # Python's Popen has a bug that causes any pipes opened to a - # process that can't be executed to be leaked. Since this - # code is specifically designed to tolerate exec failures - # to gracefully handle cases where wdiff is not installed, - # the bug results in a massive file descriptor leak. As a - # workaround, if an exec failure is ever experienced for - # wdiff, assume it's not available. This will leak one - # file descriptor but that's better than leaking each time - # wdiff would be run. - # - # http://mail.python.org/pipermail/python-list/ - # 2008-August/505753.html - # http://bugs.python.org/issue3210 - # - # It also has a threading bug, so we don't output wdiff if - # the Popen raises a ValueError. - # http://bugs.python.org/issue1236 - if _wdiff_available: - wdiff = subprocess.Popen( - cmd, stdout=subprocess.PIPE).communicate()[0] - wdiff_failed = False - - except OSError, e: - if (e.errno == errno.ENOENT or e.errno == errno.EACCES or - e.errno == errno.ECHILD): - _wdiff_available = False - else: - raise e - except ValueError, e: - wdiff_failed = True - - out = open(filename, 'wb') - - if not _wdiff_available: - out.write( - "wdiff not installed.<br/> " - "If you're running OS X, you can install via macports." - "<br/>" - "If running Ubuntu linux, you can run " - "'sudo apt-get install wdiff'.") - elif wdiff_failed: - out.write('wdiff failed due to running with multiple ' - 'test_shells in parallel.') - else: - wdiff = cgi.escape(wdiff) - wdiff = wdiff.replace('##WDIFF_DEL##', '<span class=del>') - wdiff = wdiff.replace('##WDIFF_ADD##', '<span class=add>') - wdiff = wdiff.replace('##WDIFF_END##', '</span>') - out.write('<head><style>.del { background: #faa; } ') - out.write('.add { background: #afa; }</style></head>') - out.write('<pre>' + wdiff + '</pre>') - - out.close() + wdiff = port.wdiff_text(expected_filename, actual_filename) + filename = self.output_filename(filename, test_type + + self.FILENAME_SUFFIX_WDIFF) + out = open(filename, 'wb').write(wdiff) 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 3c895af..54b332b 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py @@ -37,7 +37,6 @@ import errno import logging import os.path -from port import path_utils from layout_package import test_failures from test_types import test_type_base @@ -61,8 +60,8 @@ 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 platform-specific expected text. - expected_filename = path_utils.expected_filename(filename, '.txt') + # Read the port-specific expected text. + expected_filename = self._port.expected_filename(filename, '.txt') if show_sources: logging.debug('Using %s' % expected_filename) @@ -79,7 +78,7 @@ class TestTextDiff(test_type_base.TestTypeBase): # Normalize line endings return text.strip("\r\n").replace("\r\n", "\n") + "\n" - def compare_output(self, filename, proc, output, test_args, target): + def compare_output(self, port, filename, output, test_args, target): """Implementation of CompareOutput that checks the output text against the expected text from the LayoutTest directory.""" failures = [] @@ -95,10 +94,10 @@ class TestTextDiff(test_type_base.TestTypeBase): test_args.show_sources) # Write output files for new tests, too. - if output != expected: + if port.compare_text(output, expected): # Text doesn't match, write output files. - self.write_output_files(filename, "", ".txt", output, expected, - diff=True, wdiff=True) + self.write_output_files(port, filename, "", ".txt", output, + expected, diff=True, wdiff=True) if expected == '': failures.append(test_failures.FailureMissingResult(self)) @@ -107,7 +106,7 @@ class TestTextDiff(test_type_base.TestTypeBase): return failures - def diff_files(self, file1, file2): + def diff_files(self, port, file1, file2): """Diff two text files. Args: @@ -118,5 +117,5 @@ class TestTextDiff(test_type_base.TestTypeBase): False otherwise. """ - return (self.get_normalized_text(file1) != - self.get_normalized_text(file2)) + return port.compare_text(self.get_normalized_text(file1), + self.get_normalized_text(file2)) diff --git a/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py b/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py index 1aff53a..f522e40 100644 --- a/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py +++ b/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py @@ -321,7 +321,8 @@ class MockSCM(Mock): class MockUser(object): - def prompt(self, message): + @staticmethod + def prompt(message, repeat=1, raw_input=raw_input): return "Mock user response" def edit(self, files): diff --git a/WebKitTools/Scripts/webkitpy/style/checker.py b/WebKitTools/Scripts/webkitpy/style/checker.py index dc14ea3..fbda8cb 100644 --- a/WebKitTools/Scripts/webkitpy/style/checker.py +++ b/WebKitTools/Scripts/webkitpy/style/checker.py @@ -37,7 +37,8 @@ import sys from .. style_references import parse_patch from error_handlers import DefaultStyleErrorHandler from error_handlers import PatchStyleErrorHandler -from filter import CategoryFilter +from filter import validate_filter_rules +from filter import FilterConfiguration from processors.common import check_no_carriage_return from processors.common import categories as CommonCategories from processors.cpp import CppProcessor @@ -85,6 +86,46 @@ WEBKIT_DEFAULT_FILTER_RULES = [ ] +# FIXME: Change the second value of each tuple from a tuple to a list, +# and alter the filter code so it accepts lists instead. (The +# filter code will need to convert incoming values from a list +# to a tuple prior to caching). This will make this +# configuration setting a bit simpler since tuples have an +# unusual syntax case. +# +# The path-specific filter rules. +# +# This list is order sensitive. Only the first path substring match +# is used. See the FilterConfiguration documentation in filter.py +# for more information on this list. +_PATH_RULES_SPECIFIER = [ + # Files in these directories are consumers of the WebKit + # API and therefore do not follow the same header including + # discipline as WebCore. + (["WebKitTools/WebKitAPITest/", + "WebKit/qt/QGVLauncher/"], + ("-build/include", + "-readability/streams")), + ([# The GTK+ APIs use GTK+ naming style, which includes + # lower-cased, underscore-separated values. + "WebKit/gtk/webkit/", + # There is no clean way to avoid "yy_*" names used by flex. + "WebCore/css/CSSParser.cpp", + # There is no clean way to avoid "xxx_data" methods inside + # Qt's autotests since they are called automatically by the + # QtTest module. + "WebKit/qt/tests/", + "JavaScriptCore/qt/tests"], + ("-readability/naming",)), + # These are test file patterns. + (["_test.cpp", + "_unittest.cpp", + "_regtest.cpp"], + ("-readability/streams", # Many unit tests use cout. + "-runtime/rtti")), +] + + # Some files should be skipped when checking style. For example, # WebKit maintains some files in Mozilla style on purpose to ease # future merges. @@ -201,43 +242,56 @@ Syntax: %(program_name)s [--verbose=#] [--git-commit=<SingleCommit>] [--output=v return usage +# FIXME: Eliminate support for "extra_flag_values". +# +# FIXME: Remove everything from ProcessorOptions except for the +# information that can be passed via the command line, and +# rename to something like CheckWebKitStyleOptions. This +# includes, but is not limited to, removing the +# max_reports_per_error attribute and the is_reportable() +# method. See also the FIXME below to create a new class +# called something like CheckerConfiguration. +# # This class should not have knowledge of the flag key names. class ProcessorOptions(object): - """A container to store options to use when checking style. + """A container to store options passed via the command line. Attributes: - output_format: A string that is the output format. The supported - output formats are "emacs" which emacs can parse - and "vs7" which Microsoft Visual Studio 7 can parse. - - verbosity: An integer between 1-5 inclusive that restricts output - to errors with a confidence score at or above this value. - The default is 1, which displays all errors. + extra_flag_values: A string-string dictionary of all flag key-value + pairs that are not otherwise represented by this + class. The default is the empty dictionary. - filter: A CategoryFilter instance. The default is the empty filter, - which means that all categories should be checked. + filter_configuration: A FilterConfiguration instance. The default + is the "empty" filter configuration, which + means that all errors should be checked. git_commit: A string representing the git commit to check. The default is None. - extra_flag_values: A string-string dictionary of all flag key-value - pairs that are not otherwise represented by this - class. The default is the empty dictionary. + max_reports_per_error: The maximum number of errors to report + per file, per category. - """ + output_format: A string that is the output format. The supported + output formats are "emacs" which emacs can parse + and "vs7" which Microsoft Visual Studio 7 can parse. + + verbosity: An integer between 1-5 inclusive that restricts output + to errors with a confidence score at or above this value. + The default is 1, which reports all errors. + """ def __init__(self, - output_format="emacs", - verbosity=1, - filter=None, - max_reports_per_category=None, + extra_flag_values=None, + filter_configuration = None, git_commit=None, - extra_flag_values=None): + max_reports_per_category=None, + output_format="emacs", + verbosity=1): if extra_flag_values is None: extra_flag_values = {} - if filter is None: - filter = CategoryFilter() + if filter_configuration is None: + filter_configuration = FilterConfiguration() if max_reports_per_category is None: max_reports_per_category = {} @@ -252,7 +306,7 @@ class ProcessorOptions(object): 'Value given: "%s".' % verbosity) self.extra_flag_values = extra_flag_values - self.filter = filter + self.filter_configuration = filter_configuration self.git_commit = git_commit self.max_reports_per_category = max_reports_per_category self.output_format = output_format @@ -263,7 +317,7 @@ class ProcessorOptions(object): """Return whether this ProcessorOptions instance is equal to another.""" if self.extra_flag_values != other.extra_flag_values: return False - if self.filter != other.filter: + if self.filter_configuration != other.filter_configuration: return False if self.git_commit != other.git_commit: return False @@ -278,15 +332,16 @@ class ProcessorOptions(object): # Useful for unit testing. def __ne__(self, other): - # Python does not automatically deduce from __eq__(). - return not (self == other) + # Python does not automatically deduce this from __eq__(). + return not self.__eq__(other) - def is_reportable(self, category, confidence_in_error): + def is_reportable(self, category, confidence_in_error, path): """Return whether an error is reportable. An error is reportable if the confidence in the error is at least the current verbosity level, and if the current - filter says that the category should be checked. + filter says that the category should be checked for the + given path. Args: category: A string that is a style category. @@ -294,15 +349,13 @@ class ProcessorOptions(object): represents the application's confidence in the error. A higher number signifies greater confidence. + path: The path of the file being checked """ if confidence_in_error < self.verbosity: return False - if self.filter is None: - return True # All categories should be checked by default. - - return self.filter.should_check(category) + return self.filter_configuration.should_check(category, path) # This class should not have knowledge of the flag key names. @@ -313,16 +366,16 @@ class ArgumentDefaults(object): Attributes: output_format: A string that is the default output format. verbosity: An integer that is the default verbosity level. - filter_rules: A list of strings that are boolean filter rules - to prepend to any user-specified rules. + base_filter_rules: A list of strings that are boolean filter rules + to prepend to any user-specified rules. """ def __init__(self, default_output_format, default_verbosity, - default_filter_rules): + default_base_filter_rules): self.output_format = default_output_format self.verbosity = default_verbosity - self.filter_rules = default_filter_rules + self.base_filter_rules = default_base_filter_rules class ArgumentPrinter(object): @@ -345,11 +398,10 @@ class ArgumentPrinter(object): flags['output'] = options.output_format flags['verbose'] = options.verbosity - if options.filter: - # Only include the filter flag if rules are present. - filter_string = str(options.filter) - if filter_string: - flags['filter'] = filter_string + # Only include the filter flag if user-provided rules are present. + user_rules = options.filter_configuration.user_rules + if user_rules: + flags['filter'] = ",".join(user_rules) if options.git_commit: flags['git-commit'] = options.git_commit @@ -409,7 +461,7 @@ class ArgumentParser(object): self.doc_print(' ' + category + '\n') self.doc_print('\nDefault filter rules**:\n') - for filter_rule in sorted(self.defaults.filter_rules): + for filter_rule in sorted(self.defaults.base_filter_rules): self.doc_print(' ' + filter_rule + '\n') self.doc_print('\n**The command always evaluates the above rules, ' 'and before any --filter flag.\n\n') @@ -417,10 +469,7 @@ class ArgumentParser(object): sys.exit(0) def _parse_filter_flag(self, flag_value): - """Parse the value of the --filter flag. - - These filters are applied when deciding whether to emit a given - error message. + """Parse the --filter flag, and return a list of filter rules. Args: flag_value: A string of comma-separated filter rules, for @@ -457,7 +506,7 @@ class ArgumentParser(object): output_format = self.defaults.output_format verbosity = self.defaults.verbosity - filter_rules = self.defaults.filter_rules + base_rules = self.defaults.base_filter_rules # The flags already supported by the ProcessorOptions class. flags = ['help', 'output=', 'verbose=', 'filter=', 'git-commit='] @@ -480,6 +529,7 @@ class ArgumentParser(object): extra_flag_values = {} git_commit = None + user_rules = [] for (opt, val) in opts: if opt == '--help': @@ -494,7 +544,7 @@ class ArgumentParser(object): if not val: self._exit_with_categories() # Prepend the defaults. - filter_rules = filter_rules + self._parse_filter_flag(val) + user_rules = self._parse_filter_flag(val) else: extra_flag_values[opt] = val @@ -508,15 +558,20 @@ class ArgumentParser(object): 'allowed output formats are emacs and vs7.' % output_format) + all_categories = style_categories() + validate_filter_rules(user_rules, all_categories) + verbosity = int(verbosity) if (verbosity < 1) or (verbosity > 5): raise ValueError('Invalid --verbose value %s: value must ' 'be between 1-5.' % verbosity) - filter = CategoryFilter(filter_rules) + filter_configuration = FilterConfiguration(base_rules=base_rules, + path_specific=_PATH_RULES_SPECIFIER, + user_rules=user_rules) options = ProcessorOptions(extra_flag_values=extra_flag_values, - filter=filter, + filter_configuration=filter_configuration, git_commit=git_commit, max_reports_per_category=MAX_REPORTS_PER_CATEGORY, output_format=output_format, @@ -623,6 +678,18 @@ class ProcessorDispatcher(object): return processor +# FIXME: When creating the new CheckWebKitStyleOptions class as +# described in a FIXME above, add a new class here called +# something like CheckerConfiguration. The class should contain +# attributes for options needed to process a file. This includes +# a subset of the CheckWebKitStyleOptions attributes, a +# FilterConfiguration attribute, an stderr_write attribute, a +# max_reports_per_category attribute, etc. It can also include +# the is_reportable() method. The StyleChecker should accept +# an instance of this class rather than a ProcessorOptions +# instance. + + class StyleChecker(object): """Supports checking style in files and patches. diff --git a/WebKitTools/Scripts/webkitpy/style/checker_unittest.py b/WebKitTools/Scripts/webkitpy/style/checker_unittest.py index 814bd41..e1c9baf 100755 --- a/WebKitTools/Scripts/webkitpy/style/checker_unittest.py +++ b/WebKitTools/Scripts/webkitpy/style/checker_unittest.py @@ -37,10 +37,13 @@ import unittest import checker as style +from checker import _PATH_RULES_SPECIFIER as PATH_RULES_SPECIFIER +from checker import style_categories from checker import ProcessorDispatcher from checker import ProcessorOptions from checker import StyleChecker -from filter import CategoryFilter +from filter import validate_filter_rules +from filter import FilterConfiguration from processors.cpp import CppProcessor from processors.text import TextProcessor @@ -54,7 +57,7 @@ class ProcessorOptionsTest(unittest.TestCase): # Check default parameters. options = ProcessorOptions() self.assertEquals(options.extra_flag_values, {}) - self.assertEquals(options.filter, CategoryFilter()) + self.assertEquals(options.filter_configuration, FilterConfiguration()) self.assertEquals(options.git_commit, None) self.assertEquals(options.max_reports_per_category, {}) self.assertEquals(options.output_format, "emacs") @@ -70,14 +73,15 @@ class ProcessorOptionsTest(unittest.TestCase): ProcessorOptions(verbosity=5) # works # Check attributes. + filter_configuration = FilterConfiguration(base_rules=["+"]) options = ProcessorOptions(extra_flag_values={"extra_value" : 2}, - filter=CategoryFilter(["+"]), + filter_configuration=filter_configuration, git_commit="commit", max_reports_per_category={"category": 3}, output_format="vs7", verbosity=3) self.assertEquals(options.extra_flag_values, {"extra_value" : 2}) - self.assertEquals(options.filter, CategoryFilter(["+"])) + self.assertEquals(options.filter_configuration, filter_configuration) self.assertEquals(options.git_commit, "commit") self.assertEquals(options.max_reports_per_category, {"category": 3}) self.assertEquals(options.output_format, "vs7") @@ -88,15 +92,18 @@ class ProcessorOptionsTest(unittest.TestCase): # == calls __eq__. self.assertTrue(ProcessorOptions() == ProcessorOptions()) - # Verify that a difference in any argument cause equality to fail. + # Verify that a difference in any argument causes equality to fail. + filter_configuration = FilterConfiguration(base_rules=["+"]) options = ProcessorOptions(extra_flag_values={"extra_value" : 1}, - filter=CategoryFilter(["+"]), + filter_configuration=filter_configuration, git_commit="commit", max_reports_per_category={"category": 3}, output_format="vs7", verbosity=1) self.assertFalse(options == ProcessorOptions(extra_flag_values={"extra_value" : 2})) - self.assertFalse(options == ProcessorOptions(filter=CategoryFilter(["-"]))) + new_config = FilterConfiguration(base_rules=["-"]) + self.assertFalse(options == + ProcessorOptions(filter_configuration=new_config)) self.assertFalse(options == ProcessorOptions(git_commit="commit2")) self.assertFalse(options == ProcessorOptions(max_reports_per_category= {"category": 2})) @@ -113,36 +120,41 @@ class ProcessorOptionsTest(unittest.TestCase): def test_is_reportable(self): """Test is_reportable().""" - filter = CategoryFilter(["-xyz"]) - options = ProcessorOptions(filter=filter, verbosity=3) + filter_configuration = FilterConfiguration(base_rules=["-xyz"]) + options = ProcessorOptions(filter_configuration=filter_configuration, + verbosity=3) # Test verbosity - self.assertTrue(options.is_reportable("abc", 3)) - self.assertFalse(options.is_reportable("abc", 2)) + self.assertTrue(options.is_reportable("abc", 3, "foo.h")) + self.assertFalse(options.is_reportable("abc", 2, "foo.h")) # Test filter - self.assertTrue(options.is_reportable("xy", 3)) - self.assertFalse(options.is_reportable("xyz", 3)) + self.assertTrue(options.is_reportable("xy", 3, "foo.h")) + self.assertFalse(options.is_reportable("xyz", 3, "foo.h")) class GlobalVariablesTest(unittest.TestCase): """Tests validity of the global variables.""" + def _all_categories(self): + return style.style_categories() + def defaults(self): return style.webkit_argument_defaults() def test_filter_rules(self): defaults = self.defaults() already_seen = [] - all_categories = style.style_categories() - for rule in defaults.filter_rules: + validate_filter_rules(defaults.base_filter_rules, + self._all_categories()) + # Also do some additional checks. + for rule in defaults.base_filter_rules: # Check no leading or trailing white space. self.assertEquals(rule, rule.strip()) # All categories are on by default, so defaults should # begin with -. self.assertTrue(rule.startswith('-')) - self.assertTrue(rule[1:] in all_categories) # Check no rule occurs twice. self.assertFalse(rule in already_seen) already_seen.append(rule) @@ -158,11 +170,29 @@ class GlobalVariablesTest(unittest.TestCase): # on valid arguments elsewhere. parser.parse([]) # arguments valid: no error or SystemExit + def test_path_rules_specifier(self): + all_categories = style_categories() + for (sub_paths, path_rules) in PATH_RULES_SPECIFIER: + self.assertTrue(isinstance(path_rules, tuple), + "Checking: " + str(path_rules)) + validate_filter_rules(path_rules, self._all_categories()) + + # Try using the path specifier (as an "end-to-end" check). + config = FilterConfiguration(path_specific=PATH_RULES_SPECIFIER) + self.assertTrue(config.should_check("xxx_any_category", + "xxx_non_matching_path")) + self.assertTrue(config.should_check("xxx_any_category", + "WebKitTools/WebKitAPITest/")) + self.assertFalse(config.should_check("build/include", + "WebKitTools/WebKitAPITest/")) + self.assertFalse(config.should_check("readability/naming", + "WebKit/qt/tests/qwebelement/tst_qwebelement.cpp")) + def test_max_reports_per_category(self): """Check that MAX_REPORTS_PER_CATEGORY is valid.""" - categories = style.style_categories() + all_categories = self._all_categories() for category in style.MAX_REPORTS_PER_CATEGORY.iterkeys(): - self.assertTrue(category in categories, + self.assertTrue(category in all_categories, 'Key "%s" is not a category' % category) @@ -172,12 +202,15 @@ class ArgumentPrinterTest(unittest.TestCase): _printer = style.ArgumentPrinter() - def _create_options(self, output_format='emacs', verbosity=3, - filter_rules=[], git_commit=None, + def _create_options(self, + output_format='emacs', + verbosity=3, + user_rules=[], + git_commit=None, extra_flag_values={}): - filter = CategoryFilter(filter_rules) + filter_configuration = FilterConfiguration(user_rules=user_rules) return style.ProcessorOptions(extra_flag_values=extra_flag_values, - filter=filter, + filter_configuration=filter_configuration, git_commit=git_commit, output_format=output_format, verbosity=verbosity) @@ -255,8 +288,8 @@ class ArgumentParserTest(unittest.TestCase): parse(['--output=vs7']) # works # Pass a filter rule not beginning with + or -. - self.assertRaises(ValueError, parse, ['--filter=foo']) - parse(['--filter=+foo']) # works + self.assertRaises(ValueError, parse, ['--filter=build']) + parse(['--filter=+build']) # works # Pass files and git-commit at the same time. self.assertRaises(SystemExit, parse, ['--git-commit=', 'file.txt']) # Pass an extra flag already supported. @@ -277,8 +310,9 @@ class ArgumentParserTest(unittest.TestCase): self.assertEquals(options.output_format, 'vs7') self.assertEquals(options.verbosity, 3) - self.assertEquals(options.filter, - CategoryFilter(["-", "+whitespace"])) + self.assertEquals(options.filter_configuration, + FilterConfiguration(base_rules=["-", "+whitespace"], + path_specific=PATH_RULES_SPECIFIER)) self.assertEquals(options.git_commit, None) def test_parse_explicit_arguments(self): @@ -291,13 +325,21 @@ class ArgumentParserTest(unittest.TestCase): self.assertEquals(options.verbosity, 4) (files, options) = parse(['--git-commit=commit']) self.assertEquals(options.git_commit, 'commit') - (files, options) = parse(['--filter=+foo,-bar']) - self.assertEquals(options.filter, - CategoryFilter(["-", "+whitespace", "+foo", "-bar"])) - # Spurious white space in filter rules. - (files, options) = parse(['--filter=+foo ,-bar']) - self.assertEquals(options.filter, - CategoryFilter(["-", "+whitespace", "+foo", "-bar"])) + + # Pass user_rules. + (files, options) = parse(['--filter=+build,-whitespace']) + config = options.filter_configuration + self.assertEquals(options.filter_configuration, + FilterConfiguration(base_rules=["-", "+whitespace"], + path_specific=PATH_RULES_SPECIFIER, + user_rules=["+build", "-whitespace"])) + + # Pass spurious white space in user rules. + (files, options) = parse(['--filter=+build, -whitespace']) + self.assertEquals(options.filter_configuration, + FilterConfiguration(base_rules=["-", "+whitespace"], + path_specific=PATH_RULES_SPECIFIER, + user_rules=["+build", "-whitespace"])) # Pass extra flag values. (files, options) = parse(['--extra'], ['extra']) diff --git a/WebKitTools/Scripts/webkitpy/style/error_handlers.py b/WebKitTools/Scripts/webkitpy/style/error_handlers.py index 31140de..1940e03 100644 --- a/WebKitTools/Scripts/webkitpy/style/error_handlers.py +++ b/WebKitTools/Scripts/webkitpy/style/error_handlers.py @@ -83,7 +83,7 @@ class DefaultStyleErrorHandler(object): # A string to integer dictionary cache of the number of reportable # errors per category passed to this instance. - self._category_totals = { } + self._category_totals = {} def _add_reportable_error(self, category): """Increment the error count and return the new category total.""" @@ -109,7 +109,9 @@ class DefaultStyleErrorHandler(object): See the docstring of this module for more information. """ - if not self._options.is_reportable(category, confidence): + if not self._options.is_reportable(category, + confidence, + self._file_path): return category_total = self._add_reportable_error(category) diff --git a/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py b/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py index 83bdbb9..1d7e998 100644 --- a/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py +++ b/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py @@ -48,11 +48,12 @@ class DefaultStyleErrorHandlerTest(StyleErrorHandlerTestBase): """Tests DefaultStyleErrorHandler class.""" + _file_path = "foo.h" + _category = "whitespace/tab" def _error_handler(self, options): - file_path = "foo.h" - return DefaultStyleErrorHandler(file_path, + return DefaultStyleErrorHandler(self._file_path, options, self._mock_increment_error_count, self._mock_stderr_write) @@ -81,7 +82,9 @@ class DefaultStyleErrorHandlerTest(StyleErrorHandlerTestBase): self._check_initialized() # Confirm the error is not reportable. - self.assertFalse(options.is_reportable(self._category, confidence)) + self.assertFalse(options.is_reportable(self._category, + confidence, + self._file_path)) self._call_error_handler(options, confidence) @@ -147,9 +150,9 @@ class PatchStyleErrorHandlerTest(StyleErrorHandlerTestBase): """Tests PatchStyleErrorHandler class.""" - file_path = "__init__.py" + _file_path = "__init__.py" - patch_string = """diff --git a/__init__.py b/__init__.py + _patch_string = """diff --git a/__init__.py b/__init__.py index ef65bee..e3db70e 100644 --- a/__init__.py +++ b/__init__.py @@ -160,13 +163,13 @@ index ef65bee..e3db70e 100644 """ def test_call(self): - patch_files = parse_patch(self.patch_string) - diff = patch_files[self.file_path] + patch_files = parse_patch(self._patch_string) + diff = patch_files[self._file_path] options = ProcessorOptions(verbosity=3) handle_error = PatchStyleErrorHandler(diff, - self.file_path, + self._file_path, options, self._mock_increment_error_count, self._mock_stderr_write) @@ -176,7 +179,9 @@ index ef65bee..e3db70e 100644 message = "message" # Confirm error is reportable. - self.assertTrue(options.is_reportable(category, confidence)) + self.assertTrue(options.is_reportable(category, + confidence, + self._file_path)) # Confirm error count initialized to zero. self.assertEquals(0, self._error_count) diff --git a/WebKitTools/Scripts/webkitpy/style/filter.py b/WebKitTools/Scripts/webkitpy/style/filter.py index 1b41424..19c2f4d 100644 --- a/WebKitTools/Scripts/webkitpy/style/filter.py +++ b/WebKitTools/Scripts/webkitpy/style/filter.py @@ -23,20 +23,47 @@ """Contains filter-related code.""" -class CategoryFilter(object): +def validate_filter_rules(filter_rules, all_categories): + """Validate the given filter rules, and raise a ValueError if not valid. + + Args: + filter_rules: A list of boolean filter rules, for example-- + ["-whitespace", "+whitespace/braces"] + all_categories: A list of all available category names, for example-- + ["whitespace/tabs", "whitespace/braces"] + + Raises: + ValueError: An error occurs if a filter rule does not begin + with "+" or "-" or if a filter rule does not match + the beginning of some category name in the list + of all available categories. + + """ + for rule in filter_rules: + if not (rule.startswith('+') or rule.startswith('-')): + raise ValueError('Invalid filter rule "%s": every rule ' + "must start with + or -." % rule) + + for category in all_categories: + if category.startswith(rule[1:]): + break + else: + raise ValueError('Suspected incorrect filter rule "%s": ' + "the rule does not match the beginning " + "of any category name." % rule) + + +class _CategoryFilter(object): """Filters whether to check style categories.""" def __init__(self, filter_rules=None): """Create a category filter. - This method performs argument validation but does not strip - leading or trailing white space. - Args: filter_rules: A list of strings that are filter rules, which are strings beginning with the plus or minus - symbol (+/-). The list should include any + symbol (+/-). The list should include any default filter rules at the beginning. Defaults to the empty list. @@ -48,12 +75,6 @@ class CategoryFilter(object): if filter_rules is None: filter_rules = [] - for rule in filter_rules: - if not (rule.startswith('+') or rule.startswith('-')): - raise ValueError('Invalid filter rule "%s": every rule ' - 'rule in the --filter flag must start ' - 'with + or -.' % rule) - self._filter_rules = filter_rules self._should_check_category = {} # Cached dictionary of category to True/False @@ -74,13 +95,13 @@ class CategoryFilter(object): """Return whether the category should be checked. The rules for determining whether a category should be checked - are as follows. By default all categories should be checked. + are as follows. By default all categories should be checked. Then apply the filter rules in order from first to last, with later flags taking precedence. A filter rule applies to a category if the string after the leading plus/minus (+/-) matches the beginning of the category - name. A plus (+) means the category should be checked, while a + name. A plus (+) means the category should be checked, while a minus (-) means the category should not be checked. """ @@ -95,3 +116,159 @@ class CategoryFilter(object): self._should_check_category[category] = should_check # Update cache. return should_check + +class FilterConfiguration(object): + + """Supports filtering with path-specific and user-specified rules.""" + + def __init__(self, base_rules=None, path_specific=None, user_rules=None): + """Create a FilterConfiguration instance. + + Args: + base_rules: The starting list of filter rules to use for + processing. The default is the empty list, which + by itself would mean that all categories should be + checked. + + path_specific: A list of (sub_paths, path_rules) pairs + that stores the path-specific filter rules for + appending to the base rules. + The "sub_paths" value is a list of path + substrings. If a file path contains one of the + substrings, then the corresponding path rules + are appended. The first substring match takes + precedence, i.e. only the first match triggers + an append. + The "path_rules" value is the tuple of filter + rules that can be appended to the base rules. + The value is a tuple rather than a list so it + can be used as a dictionary key. The dictionary + is for caching purposes in the implementation of + this class. + + user_rules: A list of filter rules that is always appended + to the base rules and any path rules. In other + words, the user rules take precedence over the + everything. In practice, the user rules are + provided by the user from the command line. + + """ + if base_rules is None: + base_rules = [] + if path_specific is None: + path_specific = [] + if user_rules is None: + user_rules = [] + + self._base_rules = base_rules + self._path_specific = path_specific + self._path_specific_lower = None + """The backing store for self._get_path_specific_lower().""" + + # FIXME: Make user rules internal after the FilterConfiguration + # attribute is removed from ProcessorOptions (since at + # that point ArgumentPrinter will no longer need to + # access FilterConfiguration.user_rules). + self.user_rules = user_rules + + self._path_rules_to_filter = {} + """Cached dictionary of path rules to CategoryFilter instance.""" + + # The same CategoryFilter instance can be shared across + # multiple keys in this dictionary. This allows us to take + # greater advantage of the caching done by + # CategoryFilter.should_check(). + self._path_to_filter = {} + """Cached dictionary of file path to CategoryFilter instance.""" + + # Useful for unit testing. + def __eq__(self, other): + """Return whether this FilterConfiguration is equal to another.""" + if self._base_rules != other._base_rules: + return False + if self._path_specific != other._path_specific: + return False + if self.user_rules != other.user_rules: + return False + + return True + + # Useful for unit testing. + def __ne__(self, other): + # Python does not automatically deduce this from __eq__(). + return not self.__eq__(other) + + # We use the prefix "_get" since the name "_path_specific_lower" + # is already taken up by the data attribute backing store. + def _get_path_specific_lower(self): + """Return a copy of self._path_specific with the paths lower-cased.""" + if self._path_specific_lower is None: + self._path_specific_lower = [] + for (sub_paths, path_rules) in self._path_specific: + sub_paths = map(str.lower, sub_paths) + self._path_specific_lower.append((sub_paths, path_rules)) + return self._path_specific_lower + + def _path_rules_from_path(self, path): + """Determine the path-specific rules to use, and return as a tuple.""" + path = path.lower() + for (sub_paths, path_rules) in self._get_path_specific_lower(): + for sub_path in sub_paths: + if path.find(sub_path) > -1: + return path_rules + return () # Default to the empty tuple. + + def _filter_from_path_rules(self, path_rules): + """Return the CategoryFilter associated to a path rules tuple.""" + # We reuse the same CategoryFilter where possible to take + # advantage of the caching they do. + if path_rules not in self._path_rules_to_filter: + rules = list(self._base_rules) # Make a copy + rules.extend(path_rules) + rules.extend(self.user_rules) + self._path_rules_to_filter[path_rules] = _CategoryFilter(rules) + + return self._path_rules_to_filter[path_rules] + + def _filter_from_path(self, path): + """Return the CategoryFilter associated to a path.""" + if path not in self._path_to_filter: + path_rules = self._path_rules_from_path(path) + filter = self._filter_from_path_rules(path_rules) + self._path_to_filter[path] = filter + + return self._path_to_filter[path] + + def should_check(self, category, path): + """Return whether the given category should be checked. + + This method determines whether a category should be checked + by checking the category name against the filter rules for + the given path. + + For a given path, the filter rules are the combination of + the base rules, the path-specific rules, and the user-provided + rules -- in that order. As we will describe below, later rules + in the list take precedence. The path-specific rules are the + rules corresponding to the first element of the "path_specific" + parameter that contains a string case-insensitively matching + some substring of the path. If there is no such element, + there are no path-specific rules for that path. + + Given a list of filter rules, the logic for determining whether + a category should be checked is as follows. By default all + categories should be checked. Then apply the filter rules in + order from first to last, with later flags taking precedence. + + A filter rule applies to a category if the string after the + leading plus/minus (+/-) matches the beginning of the category + name. A plus (+) means the category should be checked, while a + minus (-) means the category should not be checked. + + Args: + category: The category name. + path: The path of the file being checked. + + """ + return self._filter_from_path(path).should_check(category) + diff --git a/WebKitTools/Scripts/webkitpy/style/filter_unittest.py b/WebKitTools/Scripts/webkitpy/style/filter_unittest.py index 0b12123..84760a5 100644 --- a/WebKitTools/Scripts/webkitpy/style/filter_unittest.py +++ b/WebKitTools/Scripts/webkitpy/style/filter_unittest.py @@ -22,10 +22,62 @@ """Unit tests for filter.py.""" - import unittest -from filter import CategoryFilter +from filter import _CategoryFilter as CategoryFilter +from filter import validate_filter_rules +from filter import FilterConfiguration + +# On Testing __eq__() and __ne__(): +# +# In the tests below, we deliberately do not use assertEquals() or +# assertNotEquals() to test __eq__() or __ne__(). We do this to be +# very explicit about what we are testing, especially in the case +# of assertNotEquals(). +# +# Part of the reason is that it is not immediately clear what +# expression the unittest module uses to assert "not equals" -- the +# negation of __eq__() or __ne__(), which are not necessarily +# equivalent expresions in Python. For example, from Python's "Data +# Model" documentation-- +# +# "There are no implied relationships among the comparison +# operators. The truth of x==y does not imply that x!=y is +# false. Accordingly, when defining __eq__(), one should +# also define __ne__() so that the operators will behave as +# expected." +# +# (from http://docs.python.org/reference/datamodel.html#object.__ne__ ) + +class ValidateFilterRulesTest(unittest.TestCase): + + """Tests validate_filter_rules() function.""" + + def test_validate_filter_rules(self): + all_categories = ["tabs", "whitespace", "build/include"] + + bad_rules = [ + "tabs", + "*tabs", + " tabs", + " +tabs", + "+whitespace/newline", + "+xxx", + ] + + good_rules = [ + "+tabs", + "-tabs", + "+build" + ] + + for rule in bad_rules: + self.assertRaises(ValueError, validate_filter_rules, + [rule], all_categories) + + for rule in good_rules: + # This works: no error. + validate_filter_rules([rule], all_categories) class CategoryFilterTest(unittest.TestCase): @@ -33,11 +85,15 @@ class CategoryFilterTest(unittest.TestCase): """Tests CategoryFilter class.""" def test_init(self): - """Test __init__ constructor.""" - self.assertRaises(ValueError, CategoryFilter, ["no_prefix"]) - CategoryFilter() # No ValueError: works - CategoryFilter(["+"]) # No ValueError: works - CategoryFilter(["-"]) # No ValueError: works + """Test __init__ method.""" + # Test that the attributes are getting set correctly. + filter = CategoryFilter(["+"]) + self.assertEquals(["+"], filter._filter_rules) + + def test_init_default_arguments(self): + """Test __init__ method default arguments.""" + filter = CategoryFilter() + self.assertEquals([], filter._filter_rules) def test_str(self): """Test __str__ "to string" operator.""" @@ -50,17 +106,20 @@ class CategoryFilterTest(unittest.TestCase): filter2 = CategoryFilter(["+a", "+b"]) filter3 = CategoryFilter(["+b", "+a"]) - # == calls __eq__. - self.assertTrue(filter1 == filter2) - self.assertFalse(filter1 == filter3) # Cannot test with assertNotEqual. + # See the notes at the top of this module about testing + # __eq__() and __ne__(). + self.assertTrue(filter1.__eq__(filter2)) + self.assertFalse(filter1.__eq__(filter3)) def test_ne(self): """Test __ne__ inequality function.""" - # != calls __ne__. # By default, __ne__ always returns true on different objects. # Thus, just check the distinguishing case to verify that the # code defines __ne__. - self.assertFalse(CategoryFilter() != CategoryFilter()) + # + # Also, see the notes at the top of this module about testing + # __eq__() and __ne__(). + self.assertFalse(CategoryFilter().__ne__(CategoryFilter())) def test_should_check(self): """Test should_check() method.""" @@ -82,3 +141,116 @@ class CategoryFilterTest(unittest.TestCase): self.assertFalse(filter.should_check("abc")) self.assertTrue(filter.should_check("a")) + +class FilterConfigurationTest(unittest.TestCase): + + """Tests FilterConfiguration class.""" + + def _config(self, base_rules, path_specific, user_rules): + """Return a FilterConfiguration instance.""" + return FilterConfiguration(base_rules=base_rules, + path_specific=path_specific, + user_rules=user_rules) + + def test_init(self): + """Test __init__ method.""" + # Test that the attributes are getting set correctly. + # We use parameter values that are different from the defaults. + base_rules = ["-"] + path_specific = [(["path"], ("+a",))] + user_rules = ["+"] + + config = self._config(base_rules, path_specific, user_rules) + + self.assertEquals(base_rules, config._base_rules) + self.assertEquals(path_specific, config._path_specific) + self.assertEquals(user_rules, config.user_rules) + + def test_default_arguments(self): + # Test that the attributes are getting set correctly to the defaults. + config = FilterConfiguration() + + self.assertEquals([], config._base_rules) + self.assertEquals([], config._path_specific) + self.assertEquals([], config.user_rules) + + def test_eq(self): + """Test __eq__ method.""" + # See the notes at the top of this module about testing + # __eq__() and __ne__(). + self.assertTrue(FilterConfiguration().__eq__(FilterConfiguration())) + + # Verify that a difference in any argument causes equality to fail. + config = FilterConfiguration() + + # These parameter values are different from the defaults. + base_rules = ["-"] + path_specific = [(["path"], ("+a",))] + user_rules = ["+"] + + self.assertFalse(config.__eq__(FilterConfiguration( + base_rules=base_rules))) + self.assertFalse(config.__eq__(FilterConfiguration( + path_specific=path_specific))) + self.assertFalse(config.__eq__(FilterConfiguration( + user_rules=user_rules))) + + def test_ne(self): + """Test __ne__ method.""" + # By default, __ne__ always returns true on different objects. + # Thus, just check the distinguishing case to verify that the + # code defines __ne__. + # + # Also, see the notes at the top of this module about testing + # __eq__() and __ne__(). + self.assertFalse(FilterConfiguration().__ne__(FilterConfiguration())) + + def test_base_rules(self): + """Test effect of base_rules on should_check().""" + base_rules = ["-b"] + path_specific = [] + user_rules = [] + + config = self._config(base_rules, path_specific, user_rules) + + self.assertTrue(config.should_check("a", "path")) + self.assertFalse(config.should_check("b", "path")) + + def test_path_specific(self): + """Test effect of path_rules_specifier on should_check().""" + base_rules = ["-"] + path_specific = [(["path1"], ("+b",)), + (["path2"], ("+c",))] + user_rules = [] + + config = self._config(base_rules, path_specific, user_rules) + + self.assertFalse(config.should_check("c", "path1")) + self.assertTrue(config.should_check("c", "path2")) + # Test that first match takes precedence. + self.assertFalse(config.should_check("c", "path2/path1")) + + def test_path_with_different_case(self): + """Test a path that differs only in case.""" + base_rules = ["-"] + path_specific = [(["Foo/"], ("+whitespace",))] + user_rules = [] + + config = self._config(base_rules, path_specific, user_rules) + + self.assertFalse(config.should_check("whitespace", "Fooo/bar.txt")) + self.assertTrue(config.should_check("whitespace", "Foo/bar.txt")) + # Test different case. + self.assertTrue(config.should_check("whitespace", "FOO/bar.txt")) + + def test_user_rules(self): + """Test effect of user_rules on should_check().""" + base_rules = ["-"] + path_specific = [] + user_rules = ["+b"] + + config = self._config(base_rules, path_specific, user_rules) + + self.assertFalse(config.should_check("a", "path")) + self.assertTrue(config.should_check("b", "path")) + diff --git a/WebKitTools/Scripts/webkitpy/style/processors/cpp.py b/WebKitTools/Scripts/webkitpy/style/processors/cpp.py index e1f41a4..182c967 100644 --- a/WebKitTools/Scripts/webkitpy/style/processors/cpp.py +++ b/WebKitTools/Scripts/webkitpy/style/processors/cpp.py @@ -2030,22 +2030,6 @@ def _drop_common_suffixes(filename): return os.path.splitext(filename)[0] -def _is_test_filename(filename): - """Determines if the given filename has a suffix that identifies it as a test. - - Args: - filename: The input filename. - - Returns: - True if 'filename' looks like a test, False otherwise. - """ - if (filename.endswith('_test.cpp') - or filename.endswith('_unittest.cpp') - or filename.endswith('_regtest.cpp')): - return True - return False - - def _classify_include(filename, include, is_system, include_state): """Figures out what kind of header 'include' is. @@ -2110,7 +2094,6 @@ def _classify_include(filename, include, is_system, include_state): return _OTHER_HEADER - def check_include_line(filename, file_extension, clean_lines, line_number, include_state, error): """Check rules that are applicable to #include lines. @@ -2126,14 +2109,12 @@ def check_include_line(filename, file_extension, clean_lines, line_number, inclu include_state: An _IncludeState instance in which the headers are inserted. error: The function to call with any errors found. """ - - if (filename.find('WebKitTools/WebKitAPITest/') >= 0 - or filename.find('WebKit/qt/QGVLauncher/') >= 0): - # Files in this directory are consumers of the WebKit API and - # therefore do not follow the same header including discipline as - # WebCore. - return - + # FIXME: For readability or as a possible optimization, consider + # exiting early here by checking whether the "build/include" + # category should be checked for the given filename. This + # may involve having the error handler classes expose a + # should_check() method, in addition to the usual __call__ + # method. line = clean_lines.lines[line_number] matched = _RE_PATTERN_INCLUDE.search(line) @@ -2145,10 +2126,8 @@ def check_include_line(filename, file_extension, clean_lines, line_number, inclu # Look for any of the stream classes that are part of standard C++. if match(r'(f|ind|io|i|o|parse|pf|stdio|str|)?stream$', include): - # Many unit tests use cout, so we exempt them. - if not _is_test_filename(filename): - error(line_number, 'readability/streams', 3, - 'Streams are highly discouraged.') + error(line_number, 'readability/streams', 3, + 'Streams are highly discouraged.') # Look for specific includes to fix. if include.startswith('wtf/') and not is_system: @@ -2291,7 +2270,7 @@ def check_language(filename, clean_lines, line_number, file_extension, include_s (matched.group(1), matched.group(2))) # Check that we're not using RTTI outside of testing code. - if search(r'\bdynamic_cast<', line) and not _is_test_filename(filename): + if search(r'\bdynamic_cast<', line): error(line_number, 'runtime/rtti', 5, 'Do not use dynamic_cast<>. If you need to cast within a class ' "hierarchy, use static_cast<> to upcast. Google doesn't support " @@ -2502,7 +2481,6 @@ def check_identifier_name_in_declaration(filename, line_number, line, error): if modified_identifier.find('_') >= 0: # Various exceptions to the rule: JavaScript op codes functions, const_iterator. if (not (filename.find('JavaScriptCore') >= 0 and modified_identifier.find('_op_') >= 0) - and not filename.find('WebKit/gtk/webkit/') >= 0 and not modified_identifier.startswith('tst_') and not modified_identifier.startswith('webkit_dom_object_') and not modified_identifier.startswith('qt_') diff --git a/WebKitTools/Scripts/webkitpy/style/processors/cpp_unittest.py b/WebKitTools/Scripts/webkitpy/style/processors/cpp_unittest.py index e556cd3..fb5a487 100644 --- a/WebKitTools/Scripts/webkitpy/style/processors/cpp_unittest.py +++ b/WebKitTools/Scripts/webkitpy/style/processors/cpp_unittest.py @@ -381,10 +381,6 @@ class CppStyleTest(CppStyleTestBase): # dynamic_cast is disallowed in most files. self.assert_language_rules_check('foo.cpp', statement, error_message) self.assert_language_rules_check('foo.h', statement, error_message) - # It is explicitly allowed in tests, however. - self.assert_language_rules_check('foo_test.cpp', statement, '') - self.assert_language_rules_check('foo_unittest.cpp', statement, '') - self.assert_language_rules_check('foo_regtest.cpp', statement, '') # We cannot test this functionality because of difference of # function definitions. Anyway, we may never enable this. @@ -2056,16 +2052,6 @@ class OrderOfIncludesTest(CppStyleTestBase): '#include <assert.h>\n', '') - def test_webkit_api_test_excluded(self): - self.assert_language_rules_check('WebKitTools/WebKitAPITest/Test.h', - '#include "foo.h"\n', - '') - - def test_webkit_api_test_excluded(self): - self.assert_language_rules_check('WebKit/qt/QGVLauncher/main.cpp', - '#include "foo.h"\n', - '') - def test_check_line_break_after_own_header(self): self.assert_language_rules_check('foo.cpp', '#include "config.h"\n' @@ -3603,9 +3589,6 @@ class WebKitStyleTest(CppStyleTestBase): self.assert_lint('void webkit_dom_object_init();', '') self.assert_lint('void webkit_dom_object_class_init();', '') - # The GTK+ APIs use GTK+ naming style, which includes lower-cased, _-separated values. - self.assert_lint('void this_is_a_gtk_style_name(int var1, int var2)', '', 'WebKit/gtk/webkit/foo.cpp') - # There is an exception for some unit tests that begin with "tst_". self.assert_lint('void tst_QWebFrame::arrayObjectEnumerable(int var1, int var2)', '') diff --git a/WebKitTools/Scripts/webkitpy/user.py b/WebKitTools/Scripts/webkitpy/user.py index 8dbf74c..b2ec19e 100644 --- a/WebKitTools/Scripts/webkitpy/user.py +++ b/WebKitTools/Scripts/webkitpy/user.py @@ -32,8 +32,13 @@ import subprocess import webbrowser class User(object): - def prompt(self, message): - return raw_input(message) + @staticmethod + def prompt(message, repeat=1, raw_input=raw_input): + response = None + while (repeat and not response): + repeat -= 1 + response = raw_input(message) + return response def edit(self, files): editor = os.environ.get("EDITOR") or "vi" diff --git a/WebKitTools/Scripts/webkitpy/user_unittest.py b/WebKitTools/Scripts/webkitpy/user_unittest.py new file mode 100644 index 0000000..34d9983 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/user_unittest.py @@ -0,0 +1,53 @@ +# Copyright (C) 2010 Research in Motion Ltd. 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 Research in Motion Ltd. 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 unittest +from webkitpy.user import User + +class UserTest(unittest.TestCase): + + example_user_response = "example user response" + + def test_prompt_repeat(self): + self.repeatsRemaining = 2 + def mock_raw_input(message): + self.repeatsRemaining -= 1 + if not self.repeatsRemaining: + return UserTest.example_user_response + return None + self.assertEqual(User.prompt("input", repeat=self.repeatsRemaining, raw_input=mock_raw_input), UserTest.example_user_response) + + def test_prompt_when_exceeded_repeats(self): + self.repeatsRemaining = 2 + def mock_raw_input(message): + self.repeatsRemaining -= 1 + return None + self.assertEqual(User.prompt("input", repeat=self.repeatsRemaining, raw_input=mock_raw_input), None) + +if __name__ == '__main__': + unittest.main() |