diff options
author | Ben Murdoch <benm@google.com> | 2010-10-22 13:02:20 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2010-10-26 15:21:41 +0100 |
commit | a94275402997c11dd2e778633dacf4b7e630a35d (patch) | |
tree | e66f56c67e3b01f22c9c23cd932271ee9ac558ed /WebKitTools/Scripts | |
parent | 09e26c78506587b3f5d930d7bc72a23287ffbec0 (diff) | |
download | external_webkit-a94275402997c11dd2e778633dacf4b7e630a35d.zip external_webkit-a94275402997c11dd2e778633dacf4b7e630a35d.tar.gz external_webkit-a94275402997c11dd2e778633dacf4b7e630a35d.tar.bz2 |
Merge WebKit at r70209: Initial merge by Git
Change-Id: Id23a68efa36e9d1126bcce0b137872db00892c8e
Diffstat (limited to 'WebKitTools/Scripts')
115 files changed, 3650 insertions, 1314 deletions
diff --git a/WebKitTools/Scripts/VCSUtils.pm b/WebKitTools/Scripts/VCSUtils.pm index dd08baa..8d7e766 100644 --- a/WebKitTools/Scripts/VCSUtils.pm +++ b/WebKitTools/Scripts/VCSUtils.pm @@ -1,5 +1,6 @@ # Copyright (C) 2007, 2008, 2009 Apple Inc. All rights reserved. # Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com) +# Copyright (C) Research In Motion Limited 2010. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -136,6 +137,18 @@ sub toWindowsLineEndings return $text; } +# Note, this method will not error if the file corresponding to the $source path does not exist. +sub scmMoveOrRenameFile +{ + my ($source, $destination) = @_; + return if ! -e $source; + if (isSVN()) { + system("svn", "move", $source, $destination); + } elsif (isGit()) { + system("git", "mv", $source, $destination); + } +} + # Note, this method will not error if the file corresponding to the path does not exist. sub scmToggleExecutableBit { @@ -1284,6 +1297,16 @@ sub setChangeLogDateAndReviewer($$$) # context. # # This subroutine has unit tests in VCSUtils_unittest.pl. +# +# Returns $changeLogHashRef: +# $changeLogHashRef: a hash reference representing a change log patch. +# patch: a ChangeLog patch equivalent to the given one, but with the +# newest ChangeLog entry inserted at the top of the file, if possible. +# hasOverlappingLines: the value 1 if the change log entry overlaps +# some lines of another change log entry. This can +# happen when deliberately inserting a new ChangeLog +# entry earlier in the file above an entry with +# the same date and author. sub fixChangeLogPatch($) { my $patch = shift; # $patch will only contain patch fragments for ChangeLog. @@ -1301,10 +1324,12 @@ sub fixChangeLogPatch($) } } my $chunkStartIndex = ++$i; + my %changeLogHashRef; # Optimization: do not process if new lines already begin the chunk. if (substr($lines[$i], 0, 1) eq "+") { - return $patch; + $changeLogHashRef{patch} = $patch; + return \%changeLogHashRef; } # Skip to first line of newly added ChangeLog entry. @@ -1321,10 +1346,12 @@ sub fixChangeLogPatch($) } elsif ($firstChar eq " " or $firstChar eq "+") { next; } - return $patch; # Do not change if, for example, "-" or "@" found. + $changeLogHashRef{patch} = $patch; # Do not change if, for example, "-" or "@" found. + return \%changeLogHashRef; } if ($i >= @lines) { - return $patch; # Do not change if date not found. + $changeLogHashRef{patch} = $patch; # Do not change if date not found. + return \%changeLogHashRef; } my $dateStartIndex = $i; @@ -1367,7 +1394,8 @@ sub fixChangeLogPatch($) my $text = substr($line, 1); my $newLine = pop(@overlappingLines); if ($text ne substr($newLine, 1)) { - return $patch; # Unexpected difference. + $changeLogHashRef{patch} = $patch; # Unexpected difference. + return \%changeLogHashRef; } $lines[$i] = "+$text"; } @@ -1379,7 +1407,8 @@ sub fixChangeLogPatch($) # FIXME: Handle errors differently from ChangeLog files that # are okay but should not be altered. That way we can find out # if improvements to the script ever become necessary. - return $patch; # Error: unexpected patch string format. + $changeLogHashRef{patch} = $patch; # Error: unexpected patch string format. + return \%changeLogHashRef; } my $skippedFirstLineCount = $1 - 1; my $oldSourceLineCount = $2; @@ -1388,7 +1417,9 @@ sub fixChangeLogPatch($) if (@overlappingLines != $skippedFirstLineCount) { # This can happen, for example, when deliberately inserting # a new ChangeLog entry earlier in the file. - return $patch; + $changeLogHashRef{hasOverlappingLines} = 1; + $changeLogHashRef{patch} = $patch; + return \%changeLogHashRef; } # If @overlappingLines > 0, this is where we make use of the # assumption that the beginning of the source file was not modified. @@ -1398,7 +1429,8 @@ sub fixChangeLogPatch($) my $targetLineCount = $oldTargetLineCount + @overlappingLines - $deletedLineCount; $lines[$chunkStartIndex - 1] = "@@ -1,$sourceLineCount +1,$targetLineCount @@"; - return join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline. + $changeLogHashRef{patch} = join($lineEnding, @lines) . "\n"; # patch(1) expects an extra trailing newline. + return \%changeLogHashRef; } # This is a supporting method for runPatchCommand. @@ -1550,7 +1582,12 @@ sub mergeChangeLogs($$$) unlink("${fileNewer}.rej"); open(PATCH, "| patch --force --fuzz=3 --binary $fileNewer > " . File::Spec->devnull()) or die $!; - print PATCH ($traditionalReject ? $patch : fixChangeLogPatch($patch)); + if ($traditionalReject) { + print PATCH $patch; + } else { + my $changeLogHash = fixChangeLogPatch($patch); + print PATCH $changeLogHash->{patch}; + } close(PATCH); my $result = !exitStatus($?); diff --git a/WebKitTools/Scripts/build-api-tests b/WebKitTools/Scripts/build-api-tests new file mode 100755 index 0000000..9db6653 --- /dev/null +++ b/WebKitTools/Scripts/build-api-tests @@ -0,0 +1,70 @@ +#!/usr/bin/perl -w + +# Copyright (C) 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 +# 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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. + +use strict; +use File::Basename; +use FindBin; +use Getopt::Long qw(:config pass_through); +use lib $FindBin::Bin; +use webkitdirs; +use POSIX; + +my $showHelp = 0; +my $clean = 0; + +my $programName = basename($0); +my $usage = <<EOF; +Usage: $programName [options] [options to pass to build system] + --help Show this help message + --clean Clean up the build directory +EOF + +GetOptions( + 'help' => \$showHelp, + 'clean' => \$clean, +); + +if ($showHelp) { + print STDERR $usage; + exit 1; +} + +checkRequiredSystemConfig(); +setConfiguration(); +chdirWebKit(); + +# Build +chdir "WebKitTools/TestWebKitAPI" or die; + +my $result; +if (isAppleMacWebKit()) { + $result = buildXCodeProject("TestWebKitAPI", $clean, XcodeOptions(), @ARGV); +} elsif (isAppleWinWebKit()) { + $result = buildVisualStudioProject("win/TestWebKitAPI.sln", $clean); +} else { + die "TestWebKitAPI is not supported on this platform.\n"; +} + +exit exitStatus($result); diff --git a/WebKitTools/Scripts/build-webkit b/WebKitTools/Scripts/build-webkit index bc1e8ad..e7f9d1f 100755 --- a/WebKitTools/Scripts/build-webkit +++ b/WebKitTools/Scripts/build-webkit @@ -57,7 +57,7 @@ my $prefixPath; my $makeArgs; my $startTime = time(); -my ($linkPrefetchSupport, $threeDCanvasSupport, $threeDRenderingSupport, $channelMessagingSupport, $clientBasedGeolocationSupport, $databaseSupport, $datagridSupport, $datalistSupport, +my ($linkPrefetchSupport, $accelerated2dCanvasSupport, $threeDCanvasSupport, $threeDRenderingSupport, $channelMessagingSupport, $clientBasedGeolocationSupport, $databaseSupport, $datagridSupport, $datalistSupport, $domStorageSupport, $eventsourceSupport, $filtersSupport, $geolocationSupport, $iconDatabaseSupport, $imageResizerSupport, $indexedDatabaseSupport, $inputSpeechSupport, $javaScriptDebuggerSupport, $mathmlSupport, $offlineWebApplicationSupport, $rubySupport, $systemMallocSupport, $sandboxSupport, $sharedWorkersSupport, $svgSupport, $svgAnimationSupport, $svgAsImageSupport, $svgDOMObjCBindingsSupport, $svgFontsSupport, @@ -69,6 +69,9 @@ my @features = ( { option => "link-prefetch", desc => "Toggle pre fetching support", define => "ENABLE_LINK_PREFETCH", default => 0, value => \$linkPrefetchSupport }, + { option => "accelerated-2d-canvas", desc => "Toggle accelerated 2D canvas support", + define => "ENABLE_ACCELERATED_2D_CANVAS", default => 0, value => \$accelerated2dCanvasSupport }, + { option => "3d-canvas", desc => "Toggle 3D canvas support", define => "ENABLE_3D_CANVAS", default => (isAppleMacWebKit() && !isTiger() && !isLeopard()), value => \$threeDCanvasSupport }, @@ -241,7 +244,7 @@ Usage: $programName [options] [options to pass to build system] --help Show this help message --clean Cleanup the build directory --debug Compile in debug mode - --cairo-win32 Build using Cairo (rather than CoreGraphics) on Windows + --wincairo Build using Cairo (rather than CoreGraphics) on Windows --chromium Build the Chromium port on Mac/Win/Linux --gtk Build the GTK+ port --qt Build the Qt port diff --git a/WebKitTools/Scripts/build-webkittestrunner b/WebKitTools/Scripts/build-webkittestrunner index dbc36d1..6cb6ac8 100755 --- a/WebKitTools/Scripts/build-webkittestrunner +++ b/WebKitTools/Scripts/build-webkittestrunner @@ -63,6 +63,9 @@ if (isAppleMacWebKit()) { $result = buildXCodeProject("WebKitTestRunner", $clean, XcodeOptions(), @ARGV); } elsif (isAppleWinWebKit()) { $result = buildVisualStudioProject("WebKitTestRunner.sln", $clean); +} elsif (isQt()) { + # Qt builds everything in one shot. No need to build anything here. + $result = 0; } else { die "WebKitTestRunner is not supported on this platform.\n"; } diff --git a/WebKitTools/Scripts/check-Xcode-source-file-types b/WebKitTools/Scripts/check-Xcode-source-file-types new file mode 100755 index 0000000..57a70b9 --- /dev/null +++ b/WebKitTools/Scripts/check-Xcode-source-file-types @@ -0,0 +1,168 @@ +#!/usr/bin/perl -w + +# Copyright (C) 2007, 2008, 2009, 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 +# 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. +# +# 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. + +# Script to check that source file extensions match file types in Xcode project.pbxproj files. + +# TODO +# - Add support for file types other than source code files. +# - Can't differentiate between sourcecode.c.h and sourcecode.cpp.h. +# (Hint: Use gcc -x c/objective-c/c++/objective-c++ -E. It will +# take time to check each header using gcc, so make it a switch.) + +use strict; + +use File::Basename; +use File::Spec; +use File::Temp qw(tempfile); +use Getopt::Long; + +# Map of Xcode file types to file extensions. +my %typeExtensionMap = qw( + sourcecode.c.c .c + sourcecode.c.h .h + sourcecode.c.objc .m + sourcecode.cpp.h .h + sourcecode.cpp.cpp .cpp + sourcecode.cpp.objcpp .mm + sourcecode.exports .exp + sourcecode.javascript .js + sourcecode.make .make + sourcecode.mig .defs + sourcecode.yacc .y +); + +# Map of file extensions to Xcode file types. +my %extensionTypeMap = map { $typeExtensionMap{$_} => $_ } keys %typeExtensionMap; +$extensionTypeMap{'.h'} = 'sourcecode.c.h'; # See TODO list. + +my $shouldFixIssues = 0; +my $printWarnings = 1; +my $showHelp; + +my $getOptionsResult = GetOptions( + 'f|fix' => \$shouldFixIssues, + 'h|help' => \$showHelp, + 'w|warnings!' => \$printWarnings, +); + +if (scalar(@ARGV) == 0 && !$showHelp) { + print STDERR "ERROR: No Xcode project files (project.pbxproj) listed on command-line.\n"; + undef $getOptionsResult; +} + +if (!$getOptionsResult || $showHelp) { + print STDERR <<__END__; +Usage: @{[ basename($0) ]} [options] path/to/project.pbxproj [path/to/project.pbxproj ...] + -f|--fix fix mismatched types in Xcode project file + -h|--help show this help message + -w|--[no-]warnings show or suppress warnings (default: show warnings) +__END__ + exit 1; +} + +for my $projectFile (@ARGV) { + my $issuesFound = 0; + my $issuesFixed = 0; + + if (basename($projectFile) =~ /\.xcodeproj$/) { + $projectFile = File::Spec->catfile($projectFile, "project.pbxproj"); + } + + if (basename($projectFile) ne "project.pbxproj") { + print STDERR "WARNING: Not an Xcode project file: $projectFile\n" if $printWarnings; + next; + } + + open(IN, "< $projectFile") || die "Could not open $projectFile: $!"; + + my ($OUT, $tempFileName); + if ($shouldFixIssues) { + ($OUT, $tempFileName) = tempfile( + basename($projectFile) . "-XXXXXXXX", + DIR => dirname($projectFile), + UNLINK => 0, + ); + + # Clean up temp file in case of die() + $SIG{__DIE__} = sub { + close(IN); + close($OUT); + unlink($tempFileName); + }; + } + + # Fast-forward to "Begin PBXFileReference section". + while (my $line = <IN>) { + print $OUT $line if $shouldFixIssues; + last if $line =~ m#^\Q/* Begin PBXFileReference section */\E$#; + } + + while (my $line = <IN>) { + if ($line =~ m#^\Q/* End PBXFileReference section */\E$#) { + print $OUT $line if $shouldFixIssues; + last; + } + + if ($line =~ m#^\s*[A-Z0-9]{24} /\* (.+) \*/\s+=\s+\{.*\s+explicitFileType = (sourcecode[^;]*);.*\s+path = ([^;]+);.*\};$#) { + my $fileName = $1; + my $fileType = $2; + my $filePath = $3; + my (undef, undef, $fileExtension) = map { lc($_) } fileparse(basename($filePath), qr{\.[^.]+$}); + + if (!exists $typeExtensionMap{$fileType}) { + $issuesFound++; + print STDERR "WARNING: Unknown file type '$fileType' for file '$filePath'.\n" if $printWarnings; + } elsif ($typeExtensionMap{$fileType} ne $fileExtension) { + $issuesFound++; + print STDERR "WARNING: Incorrect file type '$fileType' for file '$filePath'.\n" if $printWarnings; + $line =~ s/(\s+)explicitFileType( = )(sourcecode[^;]*);/$1lastKnownFileType$2$extensionTypeMap{$fileExtension};/; + $issuesFixed++ if $shouldFixIssues; + } + } + + print $OUT $line if $shouldFixIssues; + } + + # Output the rest of the file. + print $OUT <IN> if $shouldFixIssues; + + close(IN); + + if ($shouldFixIssues) { + close($OUT); + + unlink($projectFile) || die "Could not delete $projectFile: $!"; + rename($tempFileName, $projectFile) || die "Could not rename $tempFileName to $projectFile: $!"; + } + + if ($printWarnings) { + printf STDERR "%s issues found for $projectFile.\n", ($issuesFound ? $issuesFound : "No"); + print STDERR "$issuesFixed issues fixed for $projectFile.\n" if $issuesFixed && $shouldFixIssues; + print STDERR "NOTE: Open $projectFile in Xcode to let it have its way with the file.\n" if $issuesFixed; + print STDERR "\n"; + } +} + +exit 0; diff --git a/WebKitTools/Scripts/do-file-rename b/WebKitTools/Scripts/do-file-rename index ac5099e..b81b9dc 100755 --- a/WebKitTools/Scripts/do-file-rename +++ b/WebKitTools/Scripts/do-file-rename @@ -29,10 +29,11 @@ # Script to do file renaming. use strict; +use File::Find; use FindBin; use lib $FindBin::Bin; use webkitdirs; -use File::Find; +use VCSUtils; setConfiguration(); chdirWebKit(); @@ -86,7 +87,7 @@ for my $file (sort @paths) { if ($newFile{$file}) { my $newFile = $newFile{$file}; print "Renaming $file to $newFile\n"; - system "svn move $file $newFile"; + scmMoveOrRenameFile($file, $newFile); } } diff --git a/WebKitTools/Scripts/do-webcore-rename b/WebKitTools/Scripts/do-webcore-rename index a1674de..6dcb719 100755 --- a/WebKitTools/Scripts/do-webcore-rename +++ b/WebKitTools/Scripts/do-webcore-rename @@ -207,18 +207,11 @@ for my $file (sort @paths) { } } - -my $isGit = isGit(); - for my $file (sort @paths) { if ($newFile{$file}) { my $newFile = $newFile{$file}; print "Renaming $file to $newFile\n"; - if ($isGit) { - system "git mv $file $newFile"; - } else { - system "svn move $file $newFile"; - } + scmMoveOrRenameFile($file, $newFile); } } diff --git a/WebKitTools/Scripts/generate-forwarding-headers.pl b/WebKitTools/Scripts/generate-forwarding-headers.pl new file mode 100755 index 0000000..ed58702 --- /dev/null +++ b/WebKitTools/Scripts/generate-forwarding-headers.pl @@ -0,0 +1,99 @@ +#!/usr/bin/perl -w +# Copyright (C) 2010 Andras Becsi (abecsi@inf.u-szeged.hu), University of Szeged +# 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. +# +# THIS SOFTWARE IS PROVIDED BY UNIVERSITY OF SZEGED ``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 UNIVERSITY OF SZEGED 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. +# +# A script which searches for headers included by WebKit2 files +# and generates forwarding headers for these headers. + +use strict; +use Cwd qw(abs_path realpath); +use File::Find; +use File::Basename; +use File::Spec::Functions; + +my $srcRoot = realpath(File::Spec->catfile(dirname(abs_path($0)), "../..")); +my $incFromRoot = abs_path($ARGV[0]); +my @platformPrefixes = ("android", "brew", "cf", "chromium", "curl", "efl", "gtk", "haiku", "mac", "qt", "soup", "v8", "win", "wx"); +my @frameworks = ( "JavaScriptCore", "WebCore", "WebKit2"); +my @skippedPrefixes; +my @frameworkHeaders; +my $framework; +my %neededHeaders; + +shift; +my $outputDirectory = $ARGV[0]; +shift; +my $platform = $ARGV[0]; + +foreach my $prefix (@platformPrefixes) { + push(@skippedPrefixes, $prefix) unless ($prefix =~ $platform); +} + +foreach (@frameworks) { + $framework = $_; + find(\&collectNeededHeaders, $incFromRoot); + find(\&collectFameworkHeaderPaths, File::Spec->catfile($srcRoot, $framework)); + createForwardingHeadersForFramework(); +} + +sub collectNeededHeaders { + my $filePath = $File::Find::name; + my $file = $_; + if ($filePath =~ '\.h$|\.cpp$') { + open(FILE, "<$file") or die "Could not open $filePath.\n"; + while (<FILE>) { + if (m/^#.*<$framework\/(.*\.h)/) { + $neededHeaders{$1} = 1; + } + } + close(FILE); + } +} + +sub collectFameworkHeaderPaths { + my $filePath = $File::Find::name; + my $file = $_; + if ($filePath =~ '\.h$' && $filePath !~ "ForwardingHeaders" && grep{$file eq $_} keys %neededHeaders) { + my $headerPath = substr($filePath, length("$srcRoot/$framework/")); + push(@frameworkHeaders, $headerPath) unless (grep($headerPath =~ "$_/", @skippedPrefixes)); + } +} + +sub createForwardingHeadersForFramework { + foreach my $header (@frameworkHeaders) { + my $forwardingHeaderPath = File::Spec->catfile($outputDirectory, $framework, basename($header)); + my $expectedIncludeStatement = "#include \"$header\""; + my $foundIncludeStatement = 0; + $foundIncludeStatement = <EXISTING_HEADER> if open(EXISTING_HEADER, "<$forwardingHeaderPath"); + chomp($foundIncludeStatement); + if (! $foundIncludeStatement || $foundIncludeStatement ne $expectedIncludeStatement) { + print "[Creating forwarding header for $framework/$header]\n"; + open(FORWARDING_HEADER, ">$forwardingHeaderPath") or die "Could not open $forwardingHeaderPath."; + print FORWARDING_HEADER "$expectedIncludeStatement\n"; + close(FORWARDING_HEADER); + } + close(EXISTING_HEADER); + } +} + diff --git a/WebKitTools/Scripts/old-run-webkit-tests b/WebKitTools/Scripts/old-run-webkit-tests index 80801dc..a468b4d 100755 --- a/WebKitTools/Scripts/old-run-webkit-tests +++ b/WebKitTools/Scripts/old-run-webkit-tests @@ -49,6 +49,7 @@ use strict; use warnings; +use Config; use Cwd; use Data::Dumper; use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK); @@ -157,7 +158,7 @@ my $testHTTP = 1; my $testWebSocket = 1; my $testMedia = 1; my $tmpDir = "/tmp"; -my $testResultsDirectory = File::Spec->catfile($tmpDir, "layout-test-results"); +my $testResultsDirectory = File::Spec->catdir($tmpDir, "layout-test-results"); my $testsPerDumpTool = 1000; my $threaded = 0; # DumpRenderTree has an internal timeout of 30 seconds, so this must be > 30. @@ -180,6 +181,8 @@ if (isWindows() || isMsys()) { # Default to --no-http for wx for now. $testHTTP = 0 if (isWx()); +my $perlInterpreter = "perl"; + my $expectedTag = "expected"; my $actualTag = "actual"; my $prettyDiffTag = "pretty-diff"; @@ -224,7 +227,7 @@ if (isAppleMacWebKit()) { $platform = "gtk"; } elsif (isWx()) { $platform = "wx"; -} elsif (isCygwin()) { +} elsif (isCygwin() || isWindows()) { if (isWindowsXP()) { $platform = "win-xp"; } elsif (isWindowsVista()) { @@ -236,7 +239,7 @@ if (isAppleMacWebKit()) { } } -if (isQt() || isCygwin()) { +if (isQt() || isAppleWinWebKit()) { my $testfontPath = $ENV{"WEBKIT_TESTFONTS"}; if (!$testfontPath || !-d "$testfontPath") { print "The WEBKIT_TESTFONTS environment variable is not defined or not set properly\n"; @@ -364,6 +367,9 @@ if ($useWebKitTestRunner) { $stripEditingCallbacks = 0 unless defined $stripEditingCallbacks; $realPlatform = $platform; $platform = "win-wk2"; + } elsif (isQt()) { + $realPlatform = $platform; + $platform = "qt-wk2"; } } @@ -409,8 +415,11 @@ if (!defined($root)) { my $dumpToolName = $useWebKitTestRunner ? "WebKitTestRunner" : "DumpRenderTree"; -$dumpToolName .= "_debug" if isCygwin() && configurationForVisualStudio() !~ /^Release|Debug_Internal$/; -my $dumpTool = "$productDir/$dumpToolName"; +if (isAppleWinWebKit()) { + $dumpToolName .= "_debug" if configurationForVisualStudio() !~ /^Release|Debug_Internal$/; + $dumpToolName .= $Config{_exe}; +} +my $dumpTool = File::Spec->catfile($productDir, $dumpToolName); die "can't find executable $dumpToolName (looked in $productDir)\n" unless -x $dumpTool; my $imageDiffTool = "$productDir/ImageDiff"; @@ -501,7 +510,7 @@ my $supportedFeaturesResult = ""; if (isCygwin()) { # Collect supported features list setPathForRunningWebKitApp(\%ENV); - my $supportedFeaturesCommand = $dumpTool . " --print-supported-features 2>&1"; + my $supportedFeaturesCommand = "\"$dumpTool\" --print-supported-features 2>&1"; $supportedFeaturesResult = `$supportedFeaturesCommand 2>&1`; } @@ -681,7 +690,7 @@ for my $test (@tests) { my $suffixExpectedHash = ""; if ($pixelTests && !$resetResults) { my $expectedPixelDir = expectedDirectoryForTest($base, 0, "png"); - if (open EXPECTEDHASH, "$expectedPixelDir/$base-$expectedTag.checksum") { + if (open EXPECTEDHASH, File::Spec->catfile($expectedPixelDir, "$base-$expectedTag.checksum")) { my $expectedHash = <EXPECTEDHASH>; chomp($expectedHash); close EXPECTEDHASH; @@ -693,7 +702,34 @@ for my $test (@tests) { if ($test =~ /^http\//) { configureAndOpenHTTPDIfNeeded(); - if ($test !~ /^http\/tests\/local\// && $test !~ /^http\/tests\/ssl\// && $test !~ /^http\/tests\/wml\// && $test !~ /^http\/tests\/media\//) { + if ($test =~ /^http\/tests\/websocket\//) { + if ($test =~ /^websocket\/tests\/local\//) { + my $testPath = "$testDirectory/$test"; + if (isCygwin()) { + $testPath = toWindowsPath($testPath); + } else { + $testPath = canonpath($testPath); + } + print OUT "$testPath\n"; + } else { + if (openWebSocketServerIfNeeded()) { + my $path = canonpath($test); + if ($test =~ /^http\/tests\/websocket\/tests\/ssl\//) { + # wss is disabled until all platforms support pyOpenSSL. + print STDERR "Error: wss is disabled until all platforms support pyOpenSSL."; + } else { + $path =~ s/^http\/tests\///; + print OUT "http://127.0.0.1:$httpdPort/$path\n"; + } + } else { + # We failed to launch the WebSocket server. Display a useful error message rather than attempting + # to run tests that expect the server to be available. + my $errorMessagePath = "$testDirectory/http/tests/websocket/resources/server-failed-to-start.html"; + $errorMessagePath = isCygwin() ? toWindowsPath($errorMessagePath) : canonpath($errorMessagePath); + print OUT "$errorMessagePath\n"; + } + } + } elsif ($test !~ /^http\/tests\/local\// && $test !~ /^http\/tests\/ssl\// && $test !~ /^http\/tests\/wml\// && $test !~ /^http\/tests\/media\//) { my $path = canonpath($test); $path =~ s/^http\/tests\///; print OUT "http://127.0.0.1:$httpdPort/$path$suffixExpectedHash\n"; @@ -710,33 +746,6 @@ for my $test (@tests) { } print OUT "$testPath$suffixExpectedHash\n"; } - } elsif ($test =~ /^websocket\//) { - if ($test =~ /^websocket\/tests\/local\//) { - my $testPath = "$testDirectory/$test"; - if (isCygwin()) { - $testPath = toWindowsPath($testPath); - } else { - $testPath = canonpath($testPath); - } - print OUT "$testPath\n"; - } else { - if (openWebSocketServerIfNeeded()) { - my $path = canonpath($test); - if ($test =~ /^websocket\/tests\/ssl\//) { - # wss is disabled until all platforms support pyOpenSSL. - print STDERR "Error: wss is disabled until all platforms support pyOpenSSL."; - # print OUT "https://127.0.0.1:$webSocketSecurePort/$path\n"; - } else { - print OUT "http://127.0.0.1:$webSocketPort/$path\n"; - } - } else { - # We failed to launch the WebSocket server. Display a useful error message rather than attempting - # to run tests that expect the server to be available. - my $errorMessagePath = "$testDirectory/websocket/resources/server-failed-to-start.html"; - $errorMessagePath = isCygwin() ? toWindowsPath($errorMessagePath) : canonpath($errorMessagePath); - print OUT "$errorMessagePath\n"; - } - } } else { my $testPath = "$testDirectory/$test"; if (isCygwin()) { @@ -763,7 +772,7 @@ for my $test (@tests) { my $isText = isTextOnlyTest($actual); my $expectedDir = expectedDirectoryForTest($base, $isText, $expectedExtension); - $expectedResultPaths{$base} = "$expectedDir/$expectedFileName"; + $expectedResultPaths{$base} = File::Spec->catfile($expectedDir, $expectedFileName); unless ($readResults->{status} eq "success") { my $crashed = $readResults->{status} eq "crashed"; @@ -777,7 +786,7 @@ for my $test (@tests) { my $expected; - if (!$resetResults && open EXPECTED, "<", "$expectedDir/$expectedFileName") { + if (!$resetResults && open EXPECTED, "<", $expectedResultPaths{$base}) { $expected = ""; while (<EXPECTED>) { next if $stripEditingCallbacks && $_ =~ /^EDITING DELEGATE:/; @@ -827,12 +836,13 @@ for my $test (@tests) { if ($actualPNGSize > 0) { my $expectedPixelDir = expectedDirectoryForTest($base, 0, "png"); + my $expectedPNGPath = File::Spec->catfile($expectedPixelDir, "$base-$expectedTag.png"); if (!$resetResults && ($expectedHash ne $actualHash || ($actualHash eq "" && $expectedHash eq ""))) { - if (-f "$expectedPixelDir/$base-$expectedTag.png") { - my $expectedPNGSize = -s "$expectedPixelDir/$base-$expectedTag.png"; + if (-f $expectedPNGPath) { + my $expectedPNGSize = -s $expectedPNGPath; my $expectedPNG = ""; - open EXPECTEDPNG, "$expectedPixelDir/$base-$expectedTag.png"; + open EXPECTEDPNG, $expectedPNGPath; read(EXPECTEDPNG, $expectedPNG, $expectedPNGSize); openDiffTool(); @@ -863,13 +873,14 @@ for my $test (@tests) { } } - if ($resetResults || !-f "$expectedPixelDir/$base-$expectedTag.png") { + if ($resetResults || !-f $expectedPNGPath) { mkpath catfile($expectedPixelDir, dirname($base)) if $testDirectory ne $expectedPixelDir; - writeToFile("$expectedPixelDir/$base-$expectedTag.png", $actualPNG); + writeToFile($expectedPNGPath, $actualPNG); } - if ($actualHash ne "" && ($resetResults || !-f "$expectedPixelDir/$base-$expectedTag.checksum")) { - writeToFile("$expectedPixelDir/$base-$expectedTag.checksum", $actualHash); + my $expectedChecksumPath = File::Spec->catfile($expectedPixelDir, "$base-$expectedTag.checksum"); + if ($actualHash ne "" && ($resetResults || !-f $expectedChecksumPath)) { + writeToFile($expectedChecksumPath, $actualHash); } } @@ -1006,11 +1017,10 @@ for my $test (@tests) { } if ($error) { - my $dir = "$testResultsDirectory/$base"; - $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n"; + my $dir = dirname(File::Spec->catdir($testResultsDirectory, $base)); mkpath $dir; - writeToFile("$testResultsDirectory/$base-$errorTag.txt", $error); + writeToFile(File::Spec->catfile($testResultsDirectory, "$base-$errorTag.txt"), $error); $counts{error}++; push @{$tests{error}}, $test; @@ -1118,6 +1128,8 @@ if (isGtk()) { system "WebKitTools/Scripts/run-launcher", @configurationArgs, "file://".$testResults if $launchSafari; } elsif (isCygwin()) { system "cygstart", $testResults if $launchSafari; +} elsif (isWindows()) { + system "start", $testResults if $launchSafari; } else { system "WebKitTools/Scripts/run-safari", @configurationArgs, "-NSOpen", $testResults if $launchSafari; } @@ -1319,7 +1331,7 @@ sub launchWithEnv(\@\%) unshift @{$args}, "\"$allEnvVars\""; my $execScript = File::Spec->catfile(sourceDir(), qw(WebKitTools Scripts execAppWithEnv)); - unshift @{$args}, $execScript; + unshift @{$args}, $perlInterpreter, $execScript; return @{$args}; } @@ -1361,7 +1373,7 @@ sub buildDumpTool($) } my @args = argumentsForConfiguration(); - my $buildProcess = open3($childIn, $childOut, $childErr, "WebKitTools/Scripts/$dumpToolBuildScript", @args) or die "Failed to run build-dumprendertree"; + my $buildProcess = open3($childIn, $childOut, $childErr, $perlInterpreter, File::Spec->catfile(qw(WebKitTools Scripts), $dumpToolBuildScript), @args) or die "Failed to run build-dumprendertree"; close($childIn); waitpid $buildProcess, 0; my $buildResult = $?; @@ -1504,7 +1516,7 @@ sub configureAndOpenHTTPDIfNeeded() sub checkPythonVersion() { # we have not chdir to sourceDir yet. - system sourceDir() . "/WebKitTools/Scripts/ensure-valid-python", "--check-only"; + system $perlInterpreter, File::Spec->catfile(sourceDir(), qw(WebKitTools Scripts ensure-valid-python)), "--check-only"; return exitStatus($?) == 0; } @@ -1607,12 +1619,12 @@ sub expectedDirectoryForTest($;$;$) my ($base, $isText, $expectedExtension) = @_; my @directories = @platformResultHierarchy; - push @directories, map { catdir($platformBaseDirectory, $_) } qw(mac-snowleopard mac) if isCygwin(); + push @directories, map { catdir($platformBaseDirectory, $_) } qw(mac-snowleopard mac) if isAppleWinWebKit(); push @directories, $expectedDirectory; # If we already have expected results, just return their location. foreach my $directory (@directories) { - return $directory if (-f "$directory/$base-$expectedTag.$expectedExtension"); + return $directory if -f File::Spec->catfile($directory, "$base-$expectedTag.$expectedExtension"); } # For cross-platform tests, text-only results should go in the cross-platform directory, @@ -1628,9 +1640,9 @@ sub countFinishedTest($$$$) if ($shouldCheckLeaks) { my $fileName; if ($testsPerDumpTool == 1) { - $fileName = "$testResultsDirectory/$base-leaks.txt"; + $fileName = File::Spec->catfile($testResultsDirectory, "$base-leaks.txt"); } else { - $fileName = "$testResultsDirectory/" . fileNameWithNumber($dumpToolName, $leaksOutputFileNumber) . "-leaks.txt"; + $fileName = File::Spec->catfile($testResultsDirectory, fileNameWithNumber($dumpToolName, $leaksOutputFileNumber) . "-leaks.txt"); } my $leakCount = countAndPrintLeaks($dumpToolName, $dumpToolPID, $fileName); $totalLeaks += $leakCount; @@ -1653,14 +1665,13 @@ sub testCrashedOrTimedOut($$$$$) sampleDumpTool() unless $didCrash; - my $dir = "$testResultsDirectory/$base"; - $dir =~ s|/([^/]+)$|| or die "Failed to find test name from base\n"; + my $dir = dirname(File::Spec->catdir($testResultsDirectory, $base)); mkpath $dir; deleteExpectedAndActualResults($base); if (defined($error) && length($error)) { - writeToFile("$testResultsDirectory/$base-$errorTag.txt", $error); + writeToFile(File::Spec->catfile($testResultsDirectory, "$base-$errorTag.txt"), $error); } recordActualResultsAndDiff($base, $actual); @@ -1898,8 +1909,8 @@ sub recordActualResultsAndDiff($$) my $expectedResultPath = $expectedResultPaths{$base}; my ($expectedResultFileNameMinusExtension, $expectedResultDirectoryPath, $expectedResultExtension) = fileparse($expectedResultPath, qr{\.[^.]+$}); - my $actualResultsPath = "$testResultsDirectory/$base-$actualTag$expectedResultExtension"; - my $copiedExpectedResultsPath = "$testResultsDirectory/$base-$expectedTag$expectedResultExtension"; + my $actualResultsPath = File::Spec->catfile($testResultsDirectory, "$base-$actualTag$expectedResultExtension"); + my $copiedExpectedResultsPath = File::Spec->catfile($testResultsDirectory, "$base-$expectedTag$expectedResultExtension"); mkpath(dirname($actualResultsPath)); writeToFile("$actualResultsPath", $actualResults); @@ -1911,7 +1922,7 @@ sub recordActualResultsAndDiff($$) close EMPTY; } - my $diffOuputBasePath = "$testResultsDirectory/$base"; + my $diffOuputBasePath = File::Spec->catfile($testResultsDirectory, $base); my $diffOutputPath = "$diffOuputBasePath-$diffsTag.txt"; system "diff -u \"$copiedExpectedResultsPath\" \"$actualResultsPath\" > \"$diffOutputPath\""; @@ -2271,7 +2282,7 @@ sub findTestsToRun my @testsToRun = (); for my $test (@ARGV) { - $test =~ s/^($layoutTestsName|$testDirectory)\///; + $test =~ s/^(\Q$layoutTestsName\E|\Q$testDirectory\E)\///; my $fullPath = catfile($testDirectory, $test); if (file_name_is_absolute($test)) { print "can't run test $test outside $testDirectory\n"; diff --git a/WebKitTools/Scripts/run-api-tests b/WebKitTools/Scripts/run-api-tests new file mode 100755 index 0000000..3d08013 --- /dev/null +++ b/WebKitTools/Scripts/run-api-tests @@ -0,0 +1,246 @@ +#!/usr/bin/perl -w + +# Copyright (C) 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 +# 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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. + +# Features to add: +# - Command line option to run a single test. +# - Command line option to run all tests in a suite. + +use strict; +use warnings; + +use File::Basename; +use FindBin; +use Getopt::Long qw(:config pass_through); +use IPC::Open3; +use lib $FindBin::Bin; +use webkitdirs; +use Term::ANSIColor qw(:constants); + +sub dumpAllTests(); +sub runAllTests(); +sub runAllTestsInSuite($); +sub runTest($$); +sub populateTests(); +sub buildTestTool(); + +my $showHelp = 0; +my $quiet = 0; +my $dump = 0; + +my $programName = basename($0); +my $usage = <<EOF; +Usage: $programName [options] + --help Show this help message + -q|--quite Less verbose output + -d|--dump-tests Dump the names of testcases without running them +EOF + +GetOptions( + 'help' => \$showHelp, + 'quiet|q' => \$quiet, + 'dump|d' => \$dump, +); + +if ($showHelp) { + print STDERR $usage; + exit 1; +} + +setConfiguration(); +buildTestTool(); +setPathForRunningWebKitApp(\%ENV); +my %testsToRun = populateTests(); + +if ($dump) { + dumpAllTests(); + exit 0; +} + +runAllTests(); + +sub dumpAllTests() +{ + print "Dumping test cases\n"; + print "------------------\n"; + for my $suite (keys %testsToRun) { + print $suite . ":\n"; + print map { " " . $_ . "\n" } @{ $testsToRun{$suite} }; + } + print "------------------\n"; +} + +sub runAllTests() +{ + my $anyFailures = 0; + for my $suite (keys %testsToRun) { + my $failed = runAllTestsInSuite($suite); + if ($failed) { + $anyFailures = 1; + } + } + return $anyFailures; +} + +sub runAllTestsInSuite($) +{ + my ($suite) = @_; + print "Suite: $suite\n"; + + my $anyFailures = 0; + for my $test (@{$testsToRun{$suite}}) { + my $failed = runTest($suite, $test); + if ($failed) { + $anyFailures = 1; + } + } + + return $anyFailures; +} + +sub runTest($$) +{ + my ($suite, $testName) = @_; + my $test = $suite . "/" . $testName; + + print " Test: $testName -> "; + + my $result = 0; + if (isAppleMacWebKit()) { + my $productDir = productDir(); + $ENV{DYLD_FRAMEWORK_PATH} = $productDir; + $ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES"; + my $apiTesterPath = "$productDir/TestWebKitAPI"; + if (architecture()) { + $result = system "arch", "-" . architecture(), $apiTesterPath, $test, @ARGV; + } else { + $result = system $apiTesterPath, $test, @ARGV; + } + } elsif (isAppleWinWebKit()) { + my $apiTesterNameSuffix; + if (configurationForVisualStudio() =~ /^Release|Debug_Internal$/) { + $apiTesterNameSuffix = ""; + } else { + $apiTesterNameSuffix = "_debug"; + } + my $apiTesterPath = File::Spec->catfile(productDir(), "TestWebKitAPI$apiTesterNameSuffix.exe"); + $result = system $apiTesterPath, $test, @ARGV; + } else { + die "run-api-tests is not supported on this platform.\n" + } + + if ($result == 0) { + print BOLD GREEN, "Passed", RESET, "\n"; + } else { + print BOLD RED, "Failed", RESET, "\n"; + } +} + + +sub populateTests() +{ + my @tests; + + if (isAppleMacWebKit()) { + my $productDir = productDir(); + $ENV{DYLD_FRAMEWORK_PATH} = $productDir; + $ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES"; + my $apiTesterPath = "$productDir/TestWebKitAPI"; + + my ($pid, $childIn, $childOut); + if (architecture()) { + $pid = open3($childIn, $childOut, ">&STDERR", "arch", "-" . architecture(), $apiTesterPath, "--dump-tests") or die "Failed to build list of tests!"; + } else { + $pid = open3($childIn, $childOut, ">&STDERR", $apiTesterPath, "--dump-tests") or die "Failed to build list of tests!"; + } + close($childIn); + @tests = <$childOut>; + close($childOut); + + waitpid($pid, 0); + my $result = $?; + + if ($result) { + print STDERR "Failed to build list of tests!\n"; + exit exitStatus($result); + } + } elsif (isAppleWinWebKit()) { + my $apiTesterNameSuffix; + if (configurationForVisualStudio() =~ /^Release|Debug_Internal$/) { + $apiTesterNameSuffix = ""; + } else { + $apiTesterNameSuffix = "_debug"; + } + my $apiTesterPath = File::Spec->catfile(productDir(), "TestWebKitAPI$apiTesterNameSuffix.exe"); + open(TESTS, "-|", $apiTesterPath, "--dump-tests") or die $!; + @tests = <TESTS>; + close(TESTS) or die $!; + } else { + die "run-api-tests is not supported on this platform.\n" + } + + my %keyedTests = (); + for my $test (@tests) { + $test =~ s/[\r\n]*$//; + my ($suite, $testName) = split(/\//, $test); + push @{$keyedTests{$suite}}, $testName; + } + + return %keyedTests; +} + +sub buildTestTool() +{ + chdirWebKit(); + + my $buildTestTool = "build-api-tests"; + print STDERR "Running $buildTestTool\n"; + + local *DEVNULL; + my ($childIn, $childOut, $childErr); + if ($quiet) { + open(DEVNULL, ">", File::Spec->devnull()) or die "Failed to open /dev/null"; + $childOut = ">&DEVNULL"; + $childErr = ">&DEVNULL"; + } else { + # When not quiet, let the child use our stdout/stderr. + $childOut = ">&STDOUT"; + $childErr = ">&STDERR"; + } + + my @args = argumentsForConfiguration(); + my $buildProcess = open3($childIn, $childOut, $childErr, "WebKitTools/Scripts/$buildTestTool", @args) or die "Failed to run " . $buildTestTool; + close($childIn); + waitpid $buildProcess, 0; + my $buildResult = $?; + close($childOut); + close($childErr); + + close DEVNULL if ($quiet); + + if ($buildResult) { + print STDERR "Compiling TestWebKitAPI failed!\n"; + exit exitStatus($buildResult); + } +} diff --git a/WebKitTools/Scripts/run-test-webkit-api b/WebKitTools/Scripts/run-test-webkit-api new file mode 100755 index 0000000..dfd85d5 --- /dev/null +++ b/WebKitTools/Scripts/run-test-webkit-api @@ -0,0 +1,38 @@ +#!/usr/bin/perl -w + +# Copyright (C) 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 +# 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 launching the WebKit2 estWebKitAPI. + +use strict; +use FindBin; +use lib $FindBin::Bin; +use webkitdirs; + +setConfiguration(); + +exit exitStatus(runTestWebKitAPI()); diff --git a/WebKitTools/Scripts/run-webkit-tests b/WebKitTools/Scripts/run-webkit-tests index 8fe8360..6b530e1 100755 --- a/WebKitTools/Scripts/run-webkit-tests +++ b/WebKitTools/Scripts/run-webkit-tests @@ -41,6 +41,7 @@ use strict; use warnings; +use File::Spec; use FindBin; use lib $FindBin::Bin; use webkitdirs; @@ -79,5 +80,5 @@ if (useNewRunWebKitTests()) { } } -my $harnessPath = sprintf("%s/%s", relativeScriptsDir(), $harnessName); +my $harnessPath = File::Spec->catfile(relativeScriptsDir(), $harnessName); exec $harnessPath ($harnessPath, @ARGV) or die "Failed to execute $harnessPath"; diff --git a/WebKitTools/Scripts/sort-Xcode-project-file b/WebKitTools/Scripts/sort-Xcode-project-file index 044186f..705b41d 100755 --- a/WebKitTools/Scripts/sort-Xcode-project-file +++ b/WebKitTools/Scripts/sort-Xcode-project-file @@ -1,6 +1,6 @@ #!/usr/bin/perl -w -# Copyright (C) 2007, 2008 Apple Inc. All rights reserved. +# Copyright (C) 2007, 2008, 2009, 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 @@ -31,6 +31,7 @@ use strict; use File::Basename; +use File::Spec; use File::Temp qw(tempfile); use Getopt::Long; @@ -54,7 +55,7 @@ my $getOptionsResult = GetOptions( 'w|warnings!' => \$printWarnings, ); -if (scalar(@ARGV) == 0) { +if (scalar(@ARGV) == 0 && !$showHelp) { print STDERR "ERROR: No Xcode project files (project.pbxproj) listed on command-line.\n"; undef $getOptionsResult; } @@ -69,6 +70,10 @@ __END__ } for my $projectFile (@ARGV) { + if (basename($projectFile) =~ /\.xcodeproj$/) { + $projectFile = File::Spec->catfile($projectFile, "project.pbxproj"); + } + if (basename($projectFile) ne "project.pbxproj") { print STDERR "WARNING: Not an Xcode project file: $projectFile\n" if $printWarnings; next; diff --git a/WebKitTools/Scripts/sunspider-compare-results b/WebKitTools/Scripts/sunspider-compare-results index 8c3f7f5..193ee8f 100755 --- a/WebKitTools/Scripts/sunspider-compare-results +++ b/WebKitTools/Scripts/sunspider-compare-results @@ -55,7 +55,7 @@ Usage: $programName [options] FILE FILE --parse-only Use the parse-only benchmark suite. Same as --suite=parse-only EOF -GetOptions('root=s' => sub { my ($argName, $value) = @_; setConfigurationProductDir(Cwd::abs_path($value)); }, +GetOptions('root=s' => sub { my ($argName, $value) = @_; setConfigurationProductDir(Cwd::abs_path($value)); $root = $value; }, 'suite=s' => \$suite, 'ubench' => \$ubench, 'v8' => \$v8, diff --git a/WebKitTools/Scripts/svn-apply b/WebKitTools/Scripts/svn-apply index 1cf9c01..cab7fb4 100755 --- a/WebKitTools/Scripts/svn-apply +++ b/WebKitTools/Scripts/svn-apply @@ -316,7 +316,8 @@ sub patch($) # Standard patch, patch tool can handle this. if (basename($fullPath) eq "ChangeLog") { my $changeLogDotOrigExisted = -f "${fullPath}.orig"; - my $newPatch = setChangeLogDateAndReviewer(fixChangeLogPatch($patch), $reviewer, $epochTime); + my $changeLogHash = fixChangeLogPatch($patch); + my $newPatch = setChangeLogDateAndReviewer($changeLogHash->{patch}, $reviewer, $epochTime); applyPatch($newPatch, $fullPath, ["--fuzz=3"]); unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted); } else { diff --git a/WebKitTools/Scripts/svn-create-patch b/WebKitTools/Scripts/svn-create-patch index 5aead2e..863998d 100755 --- a/WebKitTools/Scripts/svn-create-patch +++ b/WebKitTools/Scripts/svn-create-patch @@ -232,7 +232,10 @@ sub generateDiff($$) $patch .= $_; } close DIFF; - $patch = fixChangeLogPatch($patch) if basename($file) eq "ChangeLog"; + if (basename($file) eq "ChangeLog") { + my $changeLogHash = fixChangeLogPatch($patch); + $patch = $changeLogHash->{patch}; + } print $patch; if ($fileData->{isBinary}) { print "\n" if ($patch && $patch =~ m/\n\S+$/m); diff --git a/WebKitTools/Scripts/svn-unapply b/WebKitTools/Scripts/svn-unapply index 53ab1b5..1dca11c 100755 --- a/WebKitTools/Scripts/svn-unapply +++ b/WebKitTools/Scripts/svn-unapply @@ -158,7 +158,8 @@ sub patch($) # Standard patch, patch tool can handle this. if (basename($fullPath) eq "ChangeLog") { my $changeLogDotOrigExisted = -f "${fullPath}.orig"; - unapplyPatch(unsetChangeLogDate($fullPath, fixChangeLogPatch($patch)), $fullPath, ["--fuzz=3"]); + my $changeLogHash = fixChangeLogPatch($patch); + unapplyPatch(unsetChangeLogDate($fullPath, $changeLogHash->{patch}), $fullPath, ["--fuzz=3"]); unlink("${fullPath}.orig") if (! $changeLogDotOrigExisted); } else { unapplyPatch($patch, $fullPath); diff --git a/WebKitTools/Scripts/test-webkitpy b/WebKitTools/Scripts/test-webkitpy index be7e870..fcff4b4 100755 --- a/WebKitTools/Scripts/test-webkitpy +++ b/WebKitTools/Scripts/test-webkitpy @@ -227,9 +227,30 @@ def init(command_args, external_package_paths): _log.warn(message) -if __name__ == "__main__": +def _path_from_webkit_root(*components): + webkit_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + return os.path.join(webkit_root, *components) + + +def _test_import(module_path): + try: + sys.path.append(os.path.dirname(module_path)) + module_name = os.path.basename(module_path) + __import__(module_name) + return True + except Exception, e: + message = "Skipping tests in %s due to failure (%s)." % (module_path, e) + if module_name.endswith("QueueStatusServer"): + message += " This module is optional. The failure is likely due to a missing Google AppEngine install. (http://code.google.com/appengine/downloads.html)" + _log.warn(message) + return False - external_package_paths = [os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'WebKit2', 'Scripts', 'webkit2')] +if __name__ == "__main__": + # FIXME: We should probably test each package separately to avoid naming conflicts. + external_package_paths = [ + _path_from_webkit_root('WebKit2', 'Scripts', 'webkit2'), + _path_from_webkit_root('WebKitTools', 'QueueStatusServer'), + ] init(sys.argv[1:], external_package_paths) # We import the unit test code after init() to ensure that any @@ -240,4 +261,6 @@ if __name__ == "__main__": # running the unit tests. from webkitpy.test.main import Tester + external_package_paths = filter(_test_import, external_package_paths) + Tester().run_tests(sys.argv, external_package_paths) diff --git a/WebKitTools/Scripts/update-webkit-support-libs b/WebKitTools/Scripts/update-webkit-support-libs index 7065293..fa2afd0 100755 --- a/WebKitTools/Scripts/update-webkit-support-libs +++ b/WebKitTools/Scripts/update-webkit-support-libs @@ -38,43 +38,33 @@ use FindBin; use lib $FindBin::Bin; use webkitdirs; -my $expectedMD5 = "a1341aadbcce1ef26dad2b2895457314"; - my $sourceDir = sourceDir(); my $file = "WebKitSupportLibrary"; my $zipFile = "$file.zip"; my $zipDirectory = toUnixPath($ENV{'WEBKITSUPPORTLIBRARIESZIPDIR'}) || $sourceDir; my $pathToZip = File::Spec->catfile($zipDirectory, $zipFile); my $webkitLibrariesDir = toUnixPath($ENV{'WEBKITLIBRARIESDIR'}) || "$sourceDir/WebKitLibraries/win"; +my $versionFile = $file . "Version"; +my $pathToVersionFile = File::Spec->catfile($webkitLibrariesDir, $versionFile); my $tmpDir = File::Spec->rel2abs(File::Temp::tempdir("webkitlibsXXXXXXX", TMPDIR => 1, CLEANUP => 1)); -# Make sure the file zipfile exists and matches the expected MD5 before doing anything. - --f $pathToZip or dieAndInstructToDownload("$zipFile could not be find in your root source directory."); - -`md5sum "$pathToZip"` =~ /^([0-9a-fA-F]{32}).*/ or die "Error running md5sum on \"$pathToZip\""; -my $actualMD5 = $1; -$actualMD5 eq $expectedMD5 or dieAndInstructToDownload("$zipFile is out of date."); - -print "Checking mod-date of $zipFile...\n"; -open MOD, ">$tmpDir/$file.modified" or die "Couldn't open $tmpDir/$file.modified for writing"; -print MOD (stat $pathToZip)[9] . "\n"; -close MOD; +chomp(my $expectedVersion = `curl -s http://developer.apple.com/opensource/internet/$versionFile`); -if (open NEW, "$tmpDir/$file.modified") { - my $new = <NEW>; - close NEW; - - if (open OLD, "$webkitLibrariesDir/$file.modified") { - my $old = <OLD>; - close OLD; - if ($old eq $new) { - print "Current $file is up to date\n"; - exit 0; - } +# Check whether the extracted library is up-to-date. If it is, we don't have anything to do. +if (open VERSION, "<", $pathToVersionFile) { + chomp(my $extractedVersion = <VERSION>); + close VERSION; + if ($extractedVersion eq $expectedVersion) { + print "$file is up-to-date.\n"; + exit; } } +# Check whether the downloaded library is up-to-date. If it isn't, the user needs to download it. +-f $pathToZip or dieAndInstructToDownload("$zipFile could not be found in $zipDirectory."); +chomp(my $zipFileVersion = `unzip -p "$pathToZip" $file/win/$versionFile`); +dieAndInstructToDownload("$zipFile is out-of-date.") if $zipFileVersion ne $expectedVersion; + my $result = system "unzip", "-q", "-d", $tmpDir, $pathToZip; die "Couldn't unzip $zipFile." if $result; @@ -95,9 +85,6 @@ sub wanted File::Find::find(\&wanted, "$tmpDir/$file"); -$result = system "mv", "$tmpDir/$file.modified", $webkitLibrariesDir; -print STDERR "Couldn't move $file.modified to $webkitLibrariesDir" . ".\n" if $result; - print "The $file has been sucessfully installed in\n $webkitLibrariesDir\n"; exit; diff --git a/WebKitTools/Scripts/webkitdirs.pm b/WebKitTools/Scripts/webkitdirs.pm index 08e14ab..fa85667 100644 --- a/WebKitTools/Scripts/webkitdirs.pm +++ b/WebKitTools/Scripts/webkitdirs.pm @@ -71,7 +71,6 @@ my $isInspectorFrontend; # Variables for Win32 support my $vcBuildPath; -my $windowsTmpPath; my $windowsSourceDir; my $winVersion; my $willUseVCExpressWhenBuilding = 0; @@ -173,9 +172,6 @@ sub determineBaseProductDir my $dosBuildPath = `cygpath --windows \"$baseProductDir\"`; chomp $dosBuildPath; $ENV{"WEBKITOUTPUTDIR"} = $dosBuildPath; - } - - if (isAppleWinWebKit()) { my $unixBuildPath = `cygpath --unix \"$baseProductDir\"`; chomp $unixBuildPath; $baseProductDir = $unixBuildPath; @@ -289,8 +285,9 @@ sub determineConfigurationForVisualStudio $configurationForVisualStudio = $configuration; return unless $configuration eq "Debug"; setupCygwinEnv(); - chomp(my $dir = `cygpath -ua '$ENV{WEBKITLIBRARIESDIR}'`); - $configurationForVisualStudio = "Debug_Internal" if -f "$dir/bin/CoreFoundation_debug.dll"; + my $dir = $ENV{WEBKITLIBRARIESDIR}; + chomp($dir = `cygpath -ua '$dir'`) if isCygwin(); + $configurationForVisualStudio = "Debug_Internal" if -f File::Spec->catfile($dir, "bin", "CoreFoundation_debug.dll"); } sub determineConfigurationProductDir @@ -299,7 +296,7 @@ sub determineConfigurationProductDir determineBaseProductDir(); determineConfiguration(); if (isAppleWinWebKit() && !isWx()) { - $configurationProductDir = "$baseProductDir/bin"; + $configurationProductDir = File::Spec->catdir($baseProductDir, "bin"); } else { # [Gtk][Efl] We don't have Release/Debug configurations in straight # autotool builds (non build-webkit). In this case and if @@ -415,7 +412,7 @@ sub determinePassedConfiguration return if $searchedForPassedConfiguration; $searchedForPassedConfiguration = 1; - my $isWinCairo = checkForArgumentAndRemoveFromARGV("--cairo-win32"); + my $isWinCairo = checkForArgumentAndRemoveFromARGV("--wincairo"); for my $i (0 .. $#ARGV) { my $opt = $ARGV[$i]; @@ -529,7 +526,7 @@ sub installedSafariPath } elsif (isAppleWinWebKit()) { $safariBundle = `"$configurationProductDir/FindSafari.exe"`; $safariBundle =~ s/[\r\n]+$//; - $safariBundle = `cygpath -u '$safariBundle'`; + $safariBundle = `cygpath -u '$safariBundle'` if isCygwin(); $safariBundle =~ s/[\r\n]+$//; $safariBundle .= "Safari.exe"; } @@ -624,7 +621,7 @@ sub builtDylibPathForName # Check to see that all the frameworks are built. sub checkFrameworks # FIXME: This is a poor name since only the Mac calls built WebCore a Framework. { - return if isCygwin(); + return if isCygwin() || isWindows(); my @frameworks = ("JavaScriptCore", "WebCore"); push(@frameworks, "WebKit") if isAppleMacWebKit(); # FIXME: This seems wrong, all ports should have a WebKit these days. for my $framework (@frameworks) { @@ -873,7 +870,7 @@ sub isAppleMacWebKit() sub isAppleWinWebKit() { - return isAppleWebKit() && isCygwin(); + return isAppleWebKit() && (isCygwin() || isWindows()); } sub isPerianInstalled() @@ -1009,8 +1006,8 @@ sub checkRequiredSystemConfig sub determineWindowsSourceDir() { return if $windowsSourceDir; - my $sourceDir = sourceDir(); - chomp($windowsSourceDir = `cygpath -w '$sourceDir'`); + $windowsSourceDir = sourceDir(); + chomp($windowsSourceDir = `cygpath -w '$windowsSourceDir'`) if isCygwin(); } sub windowsSourceDir() @@ -1070,25 +1067,25 @@ sub setupAppleWinEnv() sub setupCygwinEnv() { - return if !isCygwin(); + return if !isCygwin() && !isWindows(); return if $vcBuildPath; my $vsInstallDir; - my $programFilesPath = $ENV{'PROGRAMFILES'} || "C:\\Program Files"; + my $programFilesPath = $ENV{'PROGRAMFILES(X86)'} || $ENV{'PROGRAMFILES'} || "C:\\Program Files"; if ($ENV{'VSINSTALLDIR'}) { $vsInstallDir = $ENV{'VSINSTALLDIR'}; } else { - $vsInstallDir = "$programFilesPath/Microsoft Visual Studio 8"; + $vsInstallDir = File::Spec->catdir($programFilesPath, "Microsoft Visual Studio 8"); } - $vsInstallDir = `cygpath "$vsInstallDir"`; - chomp $vsInstallDir; - $vcBuildPath = "$vsInstallDir/Common7/IDE/devenv.com"; + chomp($vsInstallDir = `cygpath "$vsInstallDir"`) if isCygwin(); + $vcBuildPath = File::Spec->catfile($vsInstallDir, qw(Common7 IDE devenv.com)); if (-e $vcBuildPath) { # Visual Studio is installed; we can use pdevenv to build. - $vcBuildPath = File::Spec->catfile(sourceDir(), qw(WebKitTools Scripts pdevenv)); + # FIXME: Make pdevenv work with non-Cygwin Perl. + $vcBuildPath = File::Spec->catfile(sourceDir(), qw(WebKitTools Scripts pdevenv)) if isCygwin(); } else { # Visual Studio not found, try VC++ Express - $vcBuildPath = "$vsInstallDir/Common7/IDE/VCExpress.exe"; + $vcBuildPath = File::Spec->catfile($vsInstallDir, qw(Common7 IDE VCExpress.exe)); if (! -e $vcBuildPath) { print "*************************************************************\n"; print "Cannot find '$vcBuildPath'\n"; @@ -1101,7 +1098,7 @@ sub setupCygwinEnv() $willUseVCExpressWhenBuilding = 1; } - my $qtSDKPath = "$programFilesPath/QuickTime SDK"; + my $qtSDKPath = File::Spec->catdir($programFilesPath, "QuickTime SDK"); if (0 && ! -e $qtSDKPath) { print "*************************************************************\n"; print "Cannot find '$qtSDKPath'\n"; @@ -1111,10 +1108,11 @@ sub setupCygwinEnv() die; } - chomp($ENV{'WEBKITLIBRARIESDIR'} = `cygpath -wa "$sourceDir/WebKitLibraries/win"`) unless $ENV{'WEBKITLIBRARIESDIR'}; + unless ($ENV{WEBKITLIBRARIESDIR}) { + $ENV{'WEBKITLIBRARIESDIR'} = File::Spec->catdir($sourceDir, "WebKitLibraries", "win"); + chomp($ENV{WEBKITLIBRARIESDIR} = `cygpath -wa $ENV{WEBKITLIBRARIESDIR}`) if isCygwin(); + } - $windowsTmpPath = `cygpath -w /tmp`; - chomp $windowsTmpPath; print "Building results into: ", baseProductDir(), "\n"; print "WEBKITOUTPUTDIR is set to: ", $ENV{"WEBKITOUTPUTDIR"}, "\n"; print "WEBKITLIBRARIESDIR is set to: ", $ENV{"WEBKITLIBRARIESDIR"}, "\n"; @@ -1197,14 +1195,14 @@ sub buildVisualStudioProject dieIfWindowsPlatformSDKNotInstalled() if $willUseVCExpressWhenBuilding; - chomp(my $winProjectPath = `cygpath -w "$project"`); + chomp($project = `cygpath -w "$project"`) if isCygwin(); my $action = "/build"; if ($clean) { $action = "/clean"; } - my @command = ($vcBuildPath, $winProjectPath, $action, $config); + my @command = ($vcBuildPath, $project, $action, $config); print join(" ", @command), "\n"; return system @command; @@ -1461,6 +1459,9 @@ sub buildCMakeProject($@) print "Calling '$make $makeArgs' in " . $dir . "\n\n"; $result = system "$make $makeArgs"; + if ($result ne 0) { + die "Failed to build $port port\n"; + } chdir ".." or die; } @@ -1530,6 +1531,7 @@ sub buildQMakeProject($@) my @subdirs = ("JavaScriptCore", "WebCore", "WebKit/qt/Api"); if (grep { $_ eq "CONFIG+=webkit2"} @buildArgs) { push @subdirs, "WebKit2"; + push @subdirs, "WebKitTools/WebKitTestRunner"; } for my $subdir (@subdirs) { @@ -1831,4 +1833,22 @@ sub debugWebKitTestRunner return 1; } +sub runTestWebKitAPI +{ + if (isAppleMacWebKit()) { + my $productDir = productDir(); + print "Starting TestWebKitAPI with DYLD_FRAMEWORK_PATH set to point to $productDir.\n"; + $ENV{DYLD_FRAMEWORK_PATH} = $productDir; + $ENV{WEBKIT_UNSET_DYLD_FRAMEWORK_PATH} = "YES"; + my $testWebKitAPIPath = "$productDir/TestWebKitAPI"; + if (!isTiger() && architecture()) { + return system "arch", "-" . architecture(), $testWebKitAPIPath, @ARGV; + } else { + return system $testWebKitAPIPath, @ARGV; + } + } + + return 1; +} + 1; diff --git a/WebKitTools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatch.pl b/WebKitTools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatch.pl index ee258da..a7282c7 100644 --- a/WebKitTools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatch.pl +++ b/WebKitTools/Scripts/webkitperl/VCSUtils_unittest/fixChangeLogPatch.pl @@ -1,6 +1,7 @@ #!/usr/bin/perl # # Copyright (C) 2009, 2010 Chris Jerdonek (chris.jerdonek@gmail.com) +# Copyright (C) Research In Motion 2010. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are @@ -30,7 +31,10 @@ # Unit tests of VCSUtils::fixChangeLogPatch(). -use Test::Simple tests => 12; +use strict; +use warnings; + +use Test::More; use VCSUtils; # The source ChangeLog for these tests is the following: @@ -53,14 +57,10 @@ use VCSUtils; # * File: # * File2: -my $title; -my $in; -my $out; - -# New test -$title = "fixChangeLogPatch: [no change] In-place change."; - -$in = <<'END'; +my @testCaseHashRefs = ( +{ # New test + diffName => "fixChangeLogPatch: [no change] In-place change.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -1,5 +1,5 @@ @@ -71,13 +71,23 @@ $in = <<'END'; Changed some code on 2010-12-22. END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: [no change] Remove first entry."; - -$in = <<'END'; + expectedReturn => { + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@ -1,5 +1,5 @@ + 2010-12-22 Bob <bob@email.address> + +- Reviewed by Sue. ++ Reviewed by Ray. + + Changed some code on 2010-12-22. +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: [no change] Remove first entry.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -1,11 +1,3 @@ @@ -93,13 +103,28 @@ $in = <<'END'; Reviewed by Ray. END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: [no change] Remove entry in the middle."; - -$in = <<'END'; + expectedReturn => { + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@ -1,11 +1,3 @@ +-2010-12-22 Bob <bob@email.address> +- +- Reviewed by Ray. +- +- Changed some code on 2010-12-22. +- +- * File: +- + 2010-12-22 Alice <alice@email.address> + + Reviewed by Ray. +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: [no change] Remove entry in the middle.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@@ -7,10 +7,6 @@ @@ -114,13 +139,27 @@ $in = <<'END'; Reviewed by Ray. END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: [no change] Far apart changes (i.e. more than one chunk)."; - -$in = <<'END'; + expectedReturn => { + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@@ -7,10 +7,6 @@ + + * File: + +-2010-12-22 Bob <bob@email.address> +- +- Changed some code on 2010-12-22. +- + 2010-12-22 Alice <alice@email.address> + + Reviewed by Ray. +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: [no change] Far apart changes (i.e. more than one chunk).", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -7,7 +7,7 @@ @@ -141,13 +180,33 @@ $in = <<'END'; Changed some code on 2010-12-21. END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: [no change] First line is new line."; - -$in = <<'END'; + expectedReturn => { + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@ -7,7 +7,7 @@ + + * File: + +-2010-12-22 Bob <bob@email.address> ++2010-12-22 Bobby <bob@email.address> + + Changed some code on 2010-12-22. + +@@ -21,7 +21,7 @@ + + * File2: + +-2010-12-21 Bob <bob@email.address> ++2010-12-21 Bobby <bob@email.address> + + Changed some code on 2010-12-21. +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: [no change] First line is new line.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -1,3 +1,11 @@ @@ -163,13 +222,28 @@ $in = <<'END'; Reviewed by Ray. END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: [no change] No date string."; - -$in = <<'END'; + expectedReturn => { + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@ -1,3 +1,11 @@ ++2009-12-22 Bob <bob@email.address> ++ ++ Reviewed by Ray. ++ ++ Changed some more code on 2009-12-22. ++ ++ * File: ++ + 2009-12-22 Alice <alice@email.address> + + Reviewed by Ray. +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: [no change] No date string.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -6,6 +6,7 @@ @@ -181,13 +255,24 @@ $in = <<'END'; 2009-12-21 Alice <alice@email.address> END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: [no change] New entry inserted in middle."; - -$in = <<'END'; + expectedReturn => { + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@ -6,6 +6,7 @@ + + * File: + * File2: ++ * File3: + + 2009-12-21 Alice <alice@email.address> + +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: [no change] New entry inserted in middle.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -11,6 +11,14 @@ @@ -206,13 +291,32 @@ $in = <<'END'; * File: END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: [no change] New entry inserted earlier in the file, but after an entry with the same author and date."; - -$in = <<'END'; + expectedReturn => { + hasOverlappingLines => 1, + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@ -11,6 +11,14 @@ + + Reviewed by Ray. + ++ Changed some more code on 2009-12-21. ++ ++ * File: ++ ++2009-12-21 Alice <alice@email.address> ++ ++ Reviewed by Ray. ++ + Changed some code on 2009-12-21. + + * File: +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: [no change] New entry inserted earlier in the file, but after an entry with the same author and date.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -70,6 +70,14 @@ @@ -231,13 +335,32 @@ $in = <<'END'; Changed some code on 2009-12-22. END - -ok(fixChangeLogPatch($in) eq $in, $title); - -# New test -$title = "fixChangeLogPatch: Leading context includes first line."; - -$in = <<'END'; + expectedReturn => { + hasOverlappingLines => 1, + patch => <<'END', +--- ChangeLog ++++ ChangeLog +@@ -70,6 +70,14 @@ + + 2009-12-22 Alice <alice@email.address> + ++ Reviewed by Sue. ++ ++ Changed some more code on 2009-12-22. ++ ++ * File: ++ ++2009-12-22 Alice <alice@email.address> ++ + Reviewed by Ray. + + Changed some code on 2009-12-22. +END + } +}, +{ # New test + diffName => "fixChangeLogPatch: Leading context includes first line.", + inputText => <<'END', --- ChangeLog +++ ChangeLog @@ -1,5 +1,13 @@ @@ -255,8 +378,8 @@ $in = <<'END'; Changed some code on 2009-12-22. END - -$out = <<'END'; + expectedReturn => { + patch => <<'END', --- ChangeLog +++ ChangeLog @@ -1,3 +1,11 @@ @@ -272,13 +395,11 @@ $out = <<'END'; Reviewed by Ray. END - -ok(fixChangeLogPatch($in) eq $out, $title); - -# New test -$title = "fixChangeLogPatch: Leading context does not include first line."; - -$in = <<'END'; + } +}, +{ # New test + diffName => "fixChangeLogPatch: Leading context does not include first line.", + inputText => <<'END', @@ -2,6 +2,14 @@ Reviewed by Ray. @@ -295,8 +416,8 @@ $in = <<'END'; * File: END - -$out = <<'END'; + expectedReturn => { + patch => <<'END', @@ -1,3 +1,11 @@ +2009-12-22 Alice <alice@email.address> + @@ -310,18 +431,17 @@ $out = <<'END'; Reviewed by Ray. END - -ok(fixChangeLogPatch($in) eq $out, $title); - -# New test -$title = "fixChangeLogPatch: Non-consecutive line additions."; + } +}, +{ # New test + diffName => "fixChangeLogPatch: Non-consecutive line additions.", # This can occur, for example, if the new ChangeLog entry includes # trailing white space in the first blank line but not the second. # A diff command can then match the second blank line of the new # ChangeLog entry with the first blank line of the old. # The svn diff command with the default --diff-cmd has done this. -$in = <<'END'; + inputText => <<'END', @@ -1,5 +1,11 @@ 2009-12-22 Alice <alice@email.address> + <pretend-whitespace> @@ -335,8 +455,8 @@ $in = <<'END'; Changed some code on 2009-12-22. END - -$out = <<'END'; + expectedReturn => { + patch => <<'END', @@ -1,3 +1,9 @@ +2009-12-22 Alice <alice@email.address> + <pretend-whitespace> @@ -348,13 +468,11 @@ $out = <<'END'; Reviewed by Ray. END - -ok(fixChangeLogPatch($in) eq $out, $title); - -# New test -$title = "fixChangeLogPatch: Additional edits after new entry."; - -$in = <<'END'; + } +}, +{ # New test + diffName => "fixChangeLogPatch: Additional edits after new entry.", + inputText => <<'END', @@ -2,10 +2,17 @@ Reviewed by Ray. @@ -375,8 +493,8 @@ $in = <<'END'; 2009-12-21 Alice <alice@email.address> END - -$out = <<'END'; + expectedReturn => { + patch => <<'END', @@ -1,11 +1,18 @@ +2009-12-22 Alice <alice@email.address> + @@ -398,5 +516,18 @@ $out = <<'END'; 2009-12-21 Alice <alice@email.address> END + } +}, +); + +my $testCasesCount = @testCaseHashRefs; +plan(tests => $testCasesCount); # Total number of assertions. -ok(fixChangeLogPatch($in) eq $out, $title); +foreach my $testCase (@testCaseHashRefs) { + my $testNameStart = "fixChangeLogPatch(): $testCase->{diffName}: comparing"; + + my $got = VCSUtils::fixChangeLogPatch($testCase->{inputText}); + my $expectedReturn = $testCase->{expectedReturn}; + + is_deeply($got, $expectedReturn, "$testNameStart return value."); +} diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/api.py b/WebKitTools/Scripts/webkitpy/common/checkout/api.py index ca28e32..72cad8d 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/api.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/api.py @@ -83,13 +83,20 @@ class Checkout(object): def bug_id_for_revision(self, revision): return self.commit_info_for_revision(revision).bug_id() - def modified_changelogs(self, git_commit): + def _modified_files_matching_predicate(self, git_commit, predicate, changed_files=None): # SCM returns paths relative to scm.checkout_root # Callers (especially those using the ChangeLog class) may # expect absolute paths, so this method returns absolute paths. - changed_files = self._scm.changed_files(git_commit) + if not changed_files: + changed_files = self._scm.changed_files(git_commit) absolute_paths = [os.path.join(self._scm.checkout_root, path) for path in changed_files] - return [path for path in absolute_paths if self._is_path_to_changelog(path)] + return [path for path in absolute_paths if predicate(path)] + + def modified_changelogs(self, git_commit, changed_files=None): + return self._modified_files_matching_predicate(git_commit, self._is_path_to_changelog, changed_files=changed_files) + + def modified_non_changelogs(self, git_commit, changed_files=None): + return self._modified_files_matching_predicate(git_commit, lambda path: not self._is_path_to_changelog(path), changed_files=changed_files) def commit_message_for_this_commit(self, git_commit): changelog_paths = self.modified_changelogs(git_commit) @@ -109,6 +116,17 @@ class Checkout(object): # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does. return CommitMessage("".join(changelog_messages).splitlines()) + def recent_commit_infos_for_files(self, paths): + revisions = set(sum(map(self._scm.revisions_changing_file, paths), [])) + return set(map(self.commit_info_for_revision, revisions)) + + def suggested_reviewers(self, git_commit): + changed_files = self.modified_non_changelogs(git_commit) + commit_infos = self.recent_commit_infos_for_files(changed_files) + reviewers = [commit_info.reviewer() for commit_info in commit_infos if commit_info.reviewer()] + reviewers.extend([commit_info.author() for commit_info in commit_infos if commit_info.author() and commit_info.author().can_review]) + return sorted(set(reviewers)) + def bug_id_for_this_commit(self, git_commit): try: return parse_bug_id(self.commit_message_for_this_commit(git_commit).message()) diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py b/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py index fdfd879..d7bd95e 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py @@ -173,3 +173,24 @@ class CheckoutTest(unittest.TestCase): checkout = Checkout(scm) expected_changlogs = ["/foo/bar/ChangeLog", "/foo/bar/relative/path/ChangeLog"] self.assertEqual(checkout.modified_changelogs(git_commit=None), expected_changlogs) + + def test_suggested_reviewers(self): + def mock_changelog_entries_for_revision(revision): + if revision % 2 == 0: + return [ChangeLogEntry(_changelog1entry1)] + return [ChangeLogEntry(_changelog1entry2)] + + def mock_revisions_changing_file(path, limit=5): + if path.endswith("ChangeLog"): + return [3] + return [4, 8] + + scm = Mock() + scm.checkout_root = "/foo/bar" + scm.changed_files = lambda git_commit: ["file1", "file2", "relative/path/ChangeLog"] + scm.revisions_changing_file = mock_revisions_changing_file + checkout = Checkout(scm) + checkout.changelog_entries_for_revision = mock_changelog_entries_for_revision + reviewers = checkout.suggested_reviewers(git_commit=None) + reviewer_names = [reviewer.full_name for reviewer in reviewers] + self.assertEqual(reviewer_names, [u'Tor Arne Vestb\xf8']) diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py b/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py index d8ebae6..a6ea756 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py @@ -33,9 +33,13 @@ import re _log = logging.getLogger("webkitpy.common.checkout.diff_parser") + +# FIXME: This is broken. We should compile our regexps up-front +# instead of using a custom cache. _regexp_compile_cache = {} +# FIXME: This function should be removed. def match(pattern, string): """Matches the string with the pattern, caching the compiled regexp.""" if not pattern in _regexp_compile_cache: @@ -43,12 +47,15 @@ def match(pattern, string): return _regexp_compile_cache[pattern].match(string) +# FIXME: This belongs on DiffParser (e.g. as to_svn_diff()). def git_diff_to_svn_diff(line): """Converts a git formatted diff line to a svn formatted line. Args: line: A string representing a line of the diff. """ + # FIXME: This list should be a class member on DiffParser. + # These regexp patterns should be compiled once instead of every time. conversion_patterns = (("^diff --git \w/(.+) \w/(?P<FilePath>.+)", lambda matched: "Index: " + matched.group('FilePath') + "\n"), ("^new file.*", lambda matched: "\n"), ("^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}", lambda matched: "===================================================================\n"), @@ -62,6 +69,7 @@ def git_diff_to_svn_diff(line): return line +# FIXME: This method belongs on DiffParser def get_diff_converter(first_diff_line): """Gets a converter function of diff lines. @@ -80,7 +88,7 @@ _DECLARED_FILE_PATH = 2 _PROCESSING_CHUNK = 3 -class DiffFile: +class DiffFile(object): """Contains the information for one file in a patch. The field "lines" is a list which contains tuples in this format: @@ -88,6 +96,13 @@ class DiffFile: If deleted_line_number is zero, it means this line is newly added. If new_line_number is zero, it means this line is deleted. """ + # FIXME: Tuples generally grow into classes. We should consider + # adding a DiffLine object. + + def added_or_modified_line_numbers(self): + # This logic was moved from patchreader.py, but may not be + # the right API for this object long-term. + return [line[1] for line in self.lines if not line[0]] def __init__(self, filename): self.filename = filename @@ -103,13 +118,14 @@ class DiffFile: self.lines.append((deleted_line_number, new_line_number, line)) -class DiffParser: +class DiffParser(object): """A parser for a patch file. The field "files" is a dict whose key is the filename and value is a DiffFile object. """ + # FIXME: This function is way too long and needs to be broken up. def __init__(self, diff_input): """Parses a diff. diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py index 793d96d..4bd9ed6 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py @@ -245,7 +245,10 @@ class SCM: def changed_files(self, git_commit=None): self._subclass_must_implement() - def changed_files_for_revision(self): + def changed_files_for_revision(self, revision): + self._subclass_must_implement() + + def revisions_changing_file(self, path, limit=5): self._subclass_must_implement() def added_files(self): @@ -257,7 +260,7 @@ class SCM: def display_name(self): self._subclass_must_implement() - def create_patch(self, git_commit=None): + def create_patch(self, git_commit=None, changed_files=[]): self._subclass_must_implement() def committer_email_for_revision(self, revision): @@ -427,6 +430,16 @@ class SVN(SCM): status_command = ["svn", "diff", "--summarize", "-c", revision] return self.run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR")) + def revisions_changing_file(self, path, limit=5): + revisions = [] + log_command = ['svn', 'log', '--quiet', '--limit=%s' % limit, path] + for line in self.run(log_command, cwd=self.checkout_root).splitlines(): + match = re.search('^r(?P<revision>\d+) ', line) + if not match: + continue + revisions.append(int(match.group('revision'))) + return revisions + def conflicted_files(self): return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("C")) @@ -444,11 +457,11 @@ class SVN(SCM): return "svn" # FIXME: This method should be on Checkout. - def create_patch(self, git_commit=None): + def create_patch(self, git_commit=None, changed_files=[]): """Returns a byte array (str()) representing the patch file. Patch files are effectively binary since they may contain files of multiple different encodings.""" - return self.run([self.script_path("svn-create-patch")], + return self.run([self.script_path("svn-create-patch")] + changed_files, cwd=self.checkout_root, return_stderr=False, decode_output=False) @@ -653,6 +666,10 @@ class Git(SCM): commit_id = self.git_commit_from_svn_revision(revision) return self._changes_files_for_commit(commit_id) + def revisions_changing_file(self, path, limit=5): + commit_ids = self.run(["git", "log", "--pretty=format:%H", "-%s" % limit, path]).splitlines() + return filter(lambda revision: revision, map(self.svn_revision_from_git_commit, commit_ids)) + def conflicted_files(self): # We do not need to pass decode_output for this diff command # as we're passing --name-status which does not output any data. @@ -672,12 +689,12 @@ class Git(SCM): def display_name(self): return "git" - def create_patch(self, git_commit=None): + def create_patch(self, git_commit=None, changed_files=[]): """Returns a byte array (str()) representing the patch file. Patch files are effectively binary since they may contain files of multiple different encodings.""" # FIXME: This should probably use cwd=self.checkout_root - return self.run(['git', 'diff', '--binary', "--no-ext-diff", "--full-index", "-M", self.merge_base(git_commit)], decode_output=False) + return self.run(['git', 'diff', '--binary', "--no-ext-diff", "--full-index", "-M", self.merge_base(git_commit), "--"] + changed_files, decode_output=False) @classmethod def git_commit_from_svn_revision(cls, revision): @@ -688,6 +705,12 @@ class Git(SCM): raise ScriptError(message='Failed to find git commit for revision %s, your checkout likely needs an update.' % revision) return git_commit + def svn_revision_from_git_commit(self, commit_id): + try: + return int(self.run(['git', 'svn', 'find-rev', commit_id]).rstrip()) + except ValueError, e: + return None + def contents_at_revision(self, path, revision): """Returns a byte array (str()) containing the contents of path @ revision in the repository.""" diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py b/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py index 87d5539..4aa5279 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py @@ -352,6 +352,10 @@ class SCMTest(unittest.TestCase): self.assertRaises(ScriptError, self.scm.contents_at_revision, "test_file2", 2) self.assertRaises(ScriptError, self.scm.contents_at_revision, "does_not_exist", 2) + def _shared_test_revisions_changing_file(self): + self.assertEqual(self.scm.revisions_changing_file("test_file"), [5, 4, 3, 2]) + self.assertRaises(ScriptError, self.scm.revisions_changing_file, "non_existent_file") + def _shared_test_committer_email_for_revision(self): self.assertEqual(self.scm.committer_email_for_revision(3), getpass.getuser()) # Committer "email" will be the current user @@ -696,6 +700,9 @@ Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA== def test_contents_at_revision(self): self._shared_test_contents_at_revision() + def test_revisions_changing_file(self): + self._shared_test_revisions_changing_file() + def test_committer_email_for_revision(self): self._shared_test_committer_email_for_revision() @@ -964,6 +971,10 @@ class GitSVNTest(SCMTest): self.scm.commit_locally_with_message("another test commit") self._two_local_commits() + def test_revisions_changing_files_with_local_commit(self): + self._one_local_commit() + self.assertEquals(self.scm.revisions_changing_file('test_file_commit1'), []) + def test_commit_with_message(self): self._one_local_commit_plus_working_copy_changes() scm = detect_scm_system(self.git_checkout_path) @@ -1087,6 +1098,20 @@ class GitSVNTest(SCMTest): self.assertTrue(re.search(r'test_file_commit2', patch)) self.assertTrue(re.search(r'test_file_commit1', patch)) + def test_create_patch_with_changed_files(self): + self._one_local_commit_plus_working_copy_changes() + scm = detect_scm_system(self.git_checkout_path) + patch = scm.create_patch(changed_files=['test_file_commit2']) + self.assertTrue(re.search(r'test_file_commit2', patch)) + + def test_create_patch_with_rm_and_changed_files(self): + self._one_local_commit_plus_working_copy_changes() + scm = detect_scm_system(self.git_checkout_path) + os.remove('test_file_commit1') + patch = scm.create_patch() + patch_with_changed_files = scm.create_patch(changed_files=['test_file_commit1', 'test_file_commit2']) + self.assertEquals(patch, patch_with_changed_files) + def test_create_patch_git_commit(self): self._two_local_commits() scm = detect_scm_system(self.git_checkout_path) @@ -1199,6 +1224,9 @@ class GitSVNTest(SCMTest): def test_contents_at_revision(self): self._shared_test_contents_at_revision() + def test_revisions_changing_file(self): + self._shared_test_revisions_changing_file() + def test_added_files(self): self._shared_test_added_files() diff --git a/WebKitTools/Scripts/webkitpy/common/config/committers.py b/WebKitTools/Scripts/webkitpy/common/config/committers.py index 2d07158..f768cf9 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/committers.py +++ b/WebKitTools/Scripts/webkitpy/common/config/committers.py @@ -73,6 +73,7 @@ committers_unable_to_review = [ Committer("Andre Boule", "aboule@apple.com"), Committer("Andrei Popescu", "andreip@google.com", "andreip"), Committer("Andrew Wellington", ["andrew@webkit.org", "proton@wiretapped.net"], "proton"), + Committer("Andrey Kosyakov", "caseq@chromium.org", "caseq"), Committer("Andras Becsi", "abecsi@webkit.org", "bbandix"), Committer("Andy Estes", "aestes@apple.com", "estes"), Committer("Anthony Ricaud", "rik@webkit.org", "rik"), @@ -104,6 +105,7 @@ committers_unable_to_review = [ Committer("Eric Roman", "eroman@chromium.org", "eroman"), Committer("Evan Martin", "evan@chromium.org", "evmar"), Committer("Evan Stade", "estade@chromium.org", "estade"), + Committer("Fady Samuel", "fsamuel@chromium.org", "fsamuel"), Committer("Feng Qian", "feng@chromium.org"), Committer("Fumitoshi Ukai", "ukai@chromium.org", "ukai"), Committer("Gabor Loki", "loki@webkit.org", "loki04"), @@ -144,10 +146,12 @@ committers_unable_to_review = [ Committer("Luiz Agostini", ["luiz@webkit.org", "luiz.agostini@openbossa.org"], "lca"), Committer("Mads Ager", "ager@chromium.org"), Committer("Marcus Voltis Bulach", "bulach@chromium.org"), + Committer("Matt Delaney", "mdelaney@apple.com"), Committer("Matt Lilek", ["webkit@mattlilek.com", "pewtermoose@webkit.org"]), Committer("Matt Perry", "mpcomplete@chromium.org"), Committer("Maxime Britto", ["maxime.britto@gmail.com", "britto@apple.com"]), Committer("Maxime Simon", ["simon.maxime@gmail.com", "maxime.simon@webkit.org"], "maxime.simon"), + Committer("Michael Nordman", "michaeln@google.com", "michaeln"), Committer("Michael Saboff", "msaboff@apple.com"), Committer("Michelangelo De Simone", "michelangelo@webkit.org", "michelangelo"), Committer("Mihai Parparita", "mihaip@chromium.org", "mihaip"), @@ -183,6 +187,7 @@ committers_unable_to_review = [ Committer("Yaar Schnitman", ["yaar@chromium.org", "yaar@google.com"]), Committer("Yong Li", ["yong.li.webkit@gmail.com", "yong.li@torchmobile.com"], "yong"), Committer("Yongjun Zhang", "yongjun.zhang@nokia.com"), + Committer("Yuta Kitamura", "yutak@chromium.org", "yutak"), Committer("Yuzo Fujishima", "yuzo@google.com", "yuzo"), Committer("Zhenyao Mo", "zmo@google.com", "zhenyao"), Committer("Zoltan Herczeg", "zherczeg@webkit.org", "zherczeg"), @@ -205,7 +210,7 @@ reviewers_list = [ Reviewer("Alice Liu", "alice.liu@apple.com", "aliu"), Reviewer("Alp Toker", ["alp@nuanti.com", "alp@atoker.com", "alp@webkit.org"], "alp"), Reviewer("Anders Carlsson", ["andersca@apple.com", "acarlsson@apple.com"], "andersca"), - Reviewer("Andreas Kling", "andreas.kling@nokia.com", "kling"), + Reviewer("Andreas Kling", ["kling@webkit.org", "andreas.kling@nokia.com"], "kling"), Reviewer("Antonio Gomes", ["tonikitoo@webkit.org", "agomes@rim.com"], "tonikitoo"), Reviewer("Antti Koivisto", ["koivisto@iki.fi", "antti@apple.com", "antti.j.koivisto@nokia.com"], "anttik"), Reviewer("Ariya Hidayat", ["ariya@sencha.com", "ariya.hidayat@gmail.com", "ariya@webkit.org"], "ariya"), @@ -256,7 +261,7 @@ reviewers_list = [ Reviewer("Laszlo Gombos", "laszlo.1.gombos@nokia.com", "lgombos"), Reviewer("Maciej Stachowiak", "mjs@apple.com", "othermaciej"), Reviewer("Mark Rowe", "mrowe@apple.com", "bdash"), - Reviewer("Martin Robinson", ["mrobinson@igalia.com", "mrobinson@webkit.org", "martin.james.robinson@gmail.com"], "mrobinson"), + Reviewer("Martin Robinson", ["mrobinson@webkit.org", "mrobinson@igalia.com", "martin.james.robinson@gmail.com"], "mrobinson"), Reviewer("Nate Chapin", "japhet@chromium.org", "japhet"), Reviewer("Nikolas Zimmermann", ["zimmermann@kde.org", "zimmermann@physik.rwth-aachen.de", "zimmermann@webkit.org"], "wildfox"), Reviewer("Ojan Vafai", "ojan@chromium.org", "ojan"), diff --git a/WebKitTools/Scripts/webkitpy/common/config/ports.py b/WebKitTools/Scripts/webkitpy/common/config/ports.py index ebd88b1..d268865 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/ports.py +++ b/WebKitTools/Scripts/webkitpy/common/config/ports.py @@ -45,6 +45,7 @@ class WebKitPort(object): def port(port_name): ports = { "chromium": ChromiumPort, + "chromium-xvfb": ChromiumXVFBPort, "gtk": GtkPort, "mac": MacPort, "win": WinPort, @@ -102,6 +103,10 @@ class WebKitPort(object): def run_perl_unittests_command(cls): return [cls.script_path("test-webkitperl")] + @classmethod + def layout_tests_results_path(cls): + return "/tmp/layout-test-results/results.html" + class MacPort(WebKitPort): @@ -217,3 +222,28 @@ class ChromiumPort(WebKitPort): command = WebKitPort.build_webkit_command(build_style=build_style) command.append("--chromium") return command + + @classmethod + def run_webkit_tests_command(cls): + return [ + cls.script_path("new-run-webkit-tests"), + "--chromium", + "--use-drt", + "--no-pixel-tests", + ] + + @classmethod + def run_javascriptcore_tests_command(cls): + return None + + +class ChromiumXVFBPort(ChromiumPort): + + @classmethod + def flag(cls): + return "--port=chromium-xvfb" + + @classmethod + def run_webkit_tests_command(cls): + # FIXME: We should find a better way to do this. + return ["xvfb-run"] + ChromiumPort.run_webkit_tests_command() diff --git a/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py b/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py index 42c4f2d..3bdf0e6 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py @@ -29,7 +29,7 @@ import unittest -from webkitpy.common.config.ports import WebKitPort, MacPort, GtkPort, QtPort, ChromiumPort +from webkitpy.common.config.ports import * class WebKitPortTest(unittest.TestCase): @@ -64,11 +64,13 @@ class WebKitPortTest(unittest.TestCase): def test_chromium_port(self): self.assertEquals(ChromiumPort.name(), "Chromium") self.assertEquals(ChromiumPort.flag(), "--port=chromium") - self.assertEquals(ChromiumPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")]) + self.assertEquals(ChromiumPort.run_webkit_tests_command(), [WebKitPort.script_path("new-run-webkit-tests"), "--chromium", "--use-drt", "--no-pixel-tests"]) self.assertEquals(ChromiumPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--chromium"]) self.assertEquals(ChromiumPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--chromium"]) self.assertEquals(ChromiumPort.update_webkit_command(), [WebKitPort.script_path("update-webkit"), "--chromium"]) + def test_chromium_xvfb_port(self): + self.assertEquals(ChromiumXVFBPort.run_webkit_tests_command(), ["xvfb-run", "WebKitTools/Scripts/new-run-webkit-tests", "--chromium", "--use-drt", "--no-pixel-tests"]) if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py index cc64fac..94519a7 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py +++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py @@ -301,15 +301,14 @@ class BugzillaQueries(object): review_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review?" return self._fetch_bug_ids_advanced_query(review_queue_url) + # This method will make several requests to bugzilla. def fetch_patches_from_review_queue(self, limit=None): # [:None] returns the whole array. return sum([self._fetch_bug(bug_id).unreviewed_patches() for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], []) - # FIXME: Why do we have both fetch_patches_from_review_queue and - # fetch_attachment_ids_from_review_queue?? - # NOTE: This is also the only client of _fetch_attachment_ids_request_query - + # NOTE: This is the only client of _fetch_attachment_ids_request_query + # This method only makes one request to bugzilla. def fetch_attachment_ids_from_review_queue(self): review_queue_url = "request.cgi?action=queue&type=review&group=type" return self._fetch_attachment_ids_request_query(review_queue_url) diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py index 32f23cd..3a454d6 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py @@ -33,24 +33,11 @@ import datetime from webkitpy.common.config.committers import CommitterList, Reviewer, Committer from webkitpy.common.net.bugzilla import Bugzilla, BugzillaQueries, parse_bug_id, CommitterValidator, Bug from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.mocktool import MockBrowser from webkitpy.thirdparty.mock import Mock from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup -class MockBrowser(object): - def open(self, url): - pass - - def select_form(self, name): - pass - - def __setitem__(self, key, value): - pass - - def submit(self): - pass - - class BugTest(unittest.TestCase): def test_is_unassigned(self): for email in Bug.unassigned_emails: diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py index 17f6c7a..a14bc7f 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py @@ -35,12 +35,12 @@ import urllib2 import xmlrpclib from webkitpy.common.net.failuremap import FailureMap +from webkitpy.common.net.layouttestresults import LayoutTestResults from webkitpy.common.net.regressionwindow import RegressionWindow from webkitpy.common.system.logutils import get_logger from webkitpy.thirdparty.autoinstalled.mechanize import Browser from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup - _log = get_logger(__file__) @@ -108,6 +108,7 @@ class Builder(object): def _fetch_revision_to_build_map(self): # All _fetch requests go through _buildbot for easier mocking + # FIXME: This should use NetworkTransaction's 404 handling instead. try: # FIXME: This method is horribly slow due to the huge network load. # FIXME: This is a poor way to do revision -> build mapping. @@ -166,22 +167,23 @@ class Builder(object): failures = set(results.failing_tests()) if common_failures == None: common_failures = failures - common_failures = common_failures.intersection(failures) - if not common_failures: - # current_build doesn't have any failures in common with - # the red build we're worried about. We assume that any - # failures in current_build were due to flakiness. - break + else: + common_failures = common_failures.intersection(failures) + if not common_failures: + # current_build doesn't have any failures in common with + # the red build we're worried about. We assume that any + # failures in current_build were due to flakiness. + break look_back_count += 1 if look_back_count > look_back_limit: - return RegressionWindow(None, current_build, common_failures=common_failures) + return RegressionWindow(None, current_build, failing_tests=common_failures) build_after_current_build = current_build current_build = current_build.previous_build() # We must iterate at least once because red_build is red. assert(build_after_current_build) # Current build must either be green or have no failures in common # with red build, so we've found our failure transition. - return RegressionWindow(current_build, build_after_current_build, common_failures=common_failures) + return RegressionWindow(current_build, build_after_current_build, failing_tests=common_failures) def find_blameworthy_regression_window(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True): red_build = self.build(red_build_number) @@ -195,66 +197,6 @@ class Builder(object): return regression_window -# FIXME: This should be unified with all the layout test results code in the layout_tests package -class LayoutTestResults(object): - stderr_key = u'Tests that had stderr output:' - fail_key = u'Tests where results did not match expected results:' - timeout_key = u'Tests that timed out:' - crash_key = u'Tests that caused the DumpRenderTree tool to crash:' - missing_key = u'Tests that had no expected results (probably new):' - - expected_keys = [ - stderr_key, - fail_key, - crash_key, - timeout_key, - missing_key, - ] - - @classmethod - def _parse_results_html(cls, page): - parsed_results = {} - tables = BeautifulSoup(page).findAll("table") - for table in tables: - table_title = unicode(table.findPreviousSibling("p").string) - if table_title not in cls.expected_keys: - # This Exception should only ever be hit if run-webkit-tests changes its results.html format. - raise Exception("Unhandled title: %s" % table_title) - # We might want to translate table titles into identifiers before storing. - parsed_results[table_title] = [unicode(row.find("a").string) for row in table.findAll("tr")] - - return parsed_results - - @classmethod - def _fetch_results_html(cls, base_url): - results_html = "%s/results.html" % base_url - # FIXME: We need to move this sort of 404 logic into NetworkTransaction or similar. - try: - page = urllib2.urlopen(results_html) - return cls._parse_results_html(page) - except urllib2.HTTPError, error: - if error.code != 404: - raise - - @classmethod - def results_from_url(cls, base_url): - parsed_results = cls._fetch_results_html(base_url) - if not parsed_results: - return None - return cls(base_url, parsed_results) - - def __init__(self, base_url, parsed_results): - self._base_url = base_url - self._parsed_results = parsed_results - - def parsed_results(self): - return self._parsed_results - - def failing_tests(self): - failing_keys = [self.fail_key, self.crash_key, self.timeout_key] - return sorted(sum([tests for key, tests in self._parsed_results.items() if key in failing_keys], [])) - - class Build(object): def __init__(self, builder, build_number, revision, is_green): self._builder = builder @@ -274,9 +216,19 @@ class Build(object): results_directory = "r%s (%s)" % (self.revision(), self._number) return "%s/%s" % (self._builder.results_url(), urllib.quote(results_directory)) + def _fetch_results_html(self): + results_html = "%s/results.html" % (self.results_url()) + # FIXME: This should use NetworkTransaction's 404 handling instead. + try: + return urllib2.urlopen(results_html) + except urllib2.HTTPError, error: + if error.code != 404: + raise + def layout_test_results(self): if not self._layout_test_results: - self._layout_test_results = LayoutTestResults.results_from_url(self.results_url()) + # FIXME: This should cache that the result was a 404 and stop hitting the network. + self._layout_test_results = LayoutTestResults.results_from_string(self._fetch_results_html()) return self._layout_test_results def builder(self): @@ -461,7 +413,8 @@ class BuildBot(object): continue builder = self.builder_with_name(builder_status["name"]) regression_window = builder.find_blameworthy_regression_window(builder_status["build_number"]) - failure_map.add_regression_window(builder, regression_window) + if regression_window: + failure_map.add_regression_window(builder, regression_window) return failure_map # This makes fewer requests than calling Builder.latest_build would. It grabs all builder diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py index c99ab32..afc9a39 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py @@ -29,7 +29,6 @@ import unittest from webkitpy.common.net.buildbot import BuildBot, Builder, Build, LayoutTestResults - from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup @@ -42,10 +41,8 @@ class BuilderTest(unittest.TestCase): revision=build_number + 1000, is_green=build_number < 4 ) - build._layout_test_results = LayoutTestResults( - "http://buildbot.example.com/foo", { - LayoutTestResults.fail_key: failure(build_number), - }) + parsed_results = {LayoutTestResults.fail_key: failure(build_number)} + build._layout_test_results = LayoutTestResults(parsed_results) return build self.builder._fetch_build = _mock_fetch_build @@ -114,45 +111,6 @@ class BuilderTest(unittest.TestCase): self.assertEqual(self.builder._revision_and_build_for_filename(filename), revision_and_build) -class LayoutTestResultsTest(unittest.TestCase): - _example_results_html = """ -<html> -<head> -<title>Layout Test Results</title> -</head> -<body> -<p>Tests that had stderr output:</p> -<table> -<tr> -<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/accessibility/aria-activedescendant-crash.html">accessibility/aria-activedescendant-crash.html</a></td> -<td><a href="accessibility/aria-activedescendant-crash-stderr.txt">stderr</a></td> -</tr> -<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/http/tests/security/canvas-remote-read-svg-image.html">http/tests/security/canvas-remote-read-svg-image.html</a></td> -<td><a href="http/tests/security/canvas-remote-read-svg-image-stderr.txt">stderr</a></td> -</tr> -</table><p>Tests that had no expected results (probably new):</p> -<table> -<tr> -<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/fast/repaint/no-caret-repaint-in-non-content-editable-element.html">fast/repaint/no-caret-repaint-in-non-content-editable-element.html</a></td> -<td><a href="fast/repaint/no-caret-repaint-in-non-content-editable-element-actual.txt">result</a></td> -</tr> -</table></body> -</html> -""" - - _expected_layout_test_results = { - 'Tests that had stderr output:' : [ - 'accessibility/aria-activedescendant-crash.html' - ], - 'Tests that had no expected results (probably new):' : [ - 'fast/repaint/no-caret-repaint-in-non-content-editable-element.html' - ] - } - def test_parse_layout_test_results(self): - results = LayoutTestResults._parse_results_html(self._example_results_html) - self.assertEqual(self._expected_layout_test_results, results) - - class BuildBotTest(unittest.TestCase): _example_one_box_status = ''' diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py index 98e4b8f..e2d53ae 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py +++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py @@ -37,12 +37,48 @@ class FailureMap(object): 'regression_window': regression_window, }) - def revisions_causing_failures(self): - revision_to_failing_bots = {} - for failure_info in self._failures: - revisions = failure_info['regression_window'].revisions() - for revision in revisions: - failing_bots = revision_to_failing_bots.get(revision, []) - failing_bots.append(failure_info['builder']) - revision_to_failing_bots[revision] = failing_bots - return revision_to_failing_bots + def is_empty(self): + return not self._failures + + def failing_revisions(self): + failing_revisions = [failure_info['regression_window'].revisions() + for failure_info in self._failures] + return sorted(set(sum(failing_revisions, []))) + + def builders_failing_for(self, revision): + return self._builders_failing_because_of([revision]) + + def tests_failing_for(self, revision): + tests = [failure_info['regression_window'].failing_tests() + for failure_info in self._failures + if revision in failure_info['regression_window'].revisions() + and failure_info['regression_window'].failing_tests()] + result = set() + for test in tests: + result = result.union(test) + return sorted(result) + + def _old_failures(self, is_old_failure): + return filter(lambda revision: is_old_failure(revision), + self.failing_revisions()) + + def _builders_failing_because_of(self, revisions): + revision_set = set(revisions) + return [failure_info['builder'] for failure_info in self._failures + if revision_set.intersection( + failure_info['regression_window'].revisions())] + + # FIXME: We should re-process old failures after some time delay. + # https://bugs.webkit.org/show_bug.cgi?id=36581 + def filter_out_old_failures(self, is_old_failure): + old_failures = self._old_failures(is_old_failure) + old_failing_builder_names = set([builder.name() + for builder in self._builders_failing_because_of(old_failures)]) + + # We filter out all the failing builders that could have been caused + # by old_failures. We could miss some new failures this way, but + # emperically, this reduces the amount of spam we generate. + failures = self._failures + self._failures = [failure_info for failure_info in failures + if failure_info['builder'].name() not in old_failing_builder_names] + self._cache = {} diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py new file mode 100644 index 0000000..2f0b49d --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py @@ -0,0 +1,76 @@ +# 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.common.net.buildbot import Build +from webkitpy.common.net.failuremap import * +from webkitpy.common.net.regressionwindow import RegressionWindow +from webkitpy.tool.mocktool import MockBuilder + + +class FailureMapTest(unittest.TestCase): + builder1 = MockBuilder("Builder1") + builder2 = MockBuilder("Builder2") + + build1a = Build(builder1, build_number=22, revision=1233, is_green=True) + build1b = Build(builder1, build_number=23, revision=1234, is_green=False) + build2a = Build(builder2, build_number=89, revision=1233, is_green=True) + build2b = Build(builder2, build_number=90, revision=1235, is_green=False) + + regression_window1 = RegressionWindow(build1a, build1b, failing_tests=[u'test1', u'test1']) + regression_window2 = RegressionWindow(build2a, build2b, failing_tests=[u'test1']) + + def _make_failure_map(self): + failure_map = FailureMap() + failure_map.add_regression_window(self.builder1, self.regression_window1) + failure_map.add_regression_window(self.builder2, self.regression_window2) + return failure_map + + def test_failing_revisions(self): + failure_map = self._make_failure_map() + self.assertEquals(failure_map.failing_revisions(), [1234, 1235]) + + def test_new_failures(self): + failure_map = self._make_failure_map() + failure_map.filter_out_old_failures(lambda revision: False) + self.assertEquals(failure_map.failing_revisions(), [1234, 1235]) + + def test_new_failures_with_old_revisions(self): + failure_map = self._make_failure_map() + failure_map.filter_out_old_failures(lambda revision: revision == 1234) + self.assertEquals(failure_map.failing_revisions(), []) + + def test_new_failures_with_more_old_revisions(self): + failure_map = self._make_failure_map() + failure_map.filter_out_old_failures(lambda revision: revision == 1235) + self.assertEquals(failure_map.failing_revisions(), [1234]) + + def test_tests_failing_for(self): + failure_map = self._make_failure_map() + self.assertEquals(failure_map.tests_failing_for(1234), [u'test1']) diff --git a/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py new file mode 100644 index 0000000..2f7b3e6 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py @@ -0,0 +1,88 @@ +# 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. +# +# A module for parsing results.html files generated by old-run-webkit-tests + +from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, SoupStrainer + + +# FIXME: This should be unified with all the layout test results code in the layout_tests package +# This doesn't belong in common.net, but we don't have a better place for it yet. +def path_for_layout_test(test_name): + return "LayoutTests/%s" % test_name + + +# FIXME: This should be unified with all the layout test results code in the layout_tests package +# This doesn't belong in common.net, but we don't have a better place for it yet. +class LayoutTestResults(object): + """This class knows how to parse old-run-webkit-tests results.html files.""" + + stderr_key = u'Tests that had stderr output:' + fail_key = u'Tests where results did not match expected results:' + timeout_key = u'Tests that timed out:' + crash_key = u'Tests that caused the DumpRenderTree tool to crash:' + missing_key = u'Tests that had no expected results (probably new):' + + expected_keys = [ + stderr_key, + fail_key, + crash_key, + timeout_key, + missing_key, + ] + + @classmethod + def _parse_results_html(cls, page): + parsed_results = {} + tables = BeautifulSoup(page).findAll("table") + for table in tables: + table_title = unicode(table.findPreviousSibling("p").string) + if table_title not in cls.expected_keys: + # This Exception should only ever be hit if run-webkit-tests changes its results.html format. + raise Exception("Unhandled title: %s" % table_title) + # We might want to translate table titles into identifiers before storing. + parsed_results[table_title] = [unicode(row.find("a").string) for row in table.findAll("tr")] + + return parsed_results + + @classmethod + def results_from_string(cls, string): + parsed_results = cls._parse_results_html(string) + if not parsed_results: + return None + return cls(parsed_results) + + def __init__(self, parsed_results): + self._parsed_results = parsed_results + + def parsed_results(self): + return self._parsed_results + + def failing_tests(self): + failing_keys = [self.fail_key, self.crash_key, self.timeout_key] + return sorted(sum([tests for key, tests in self._parsed_results.items() if key in failing_keys], [])) diff --git a/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py new file mode 100644 index 0000000..44e4dbc --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py @@ -0,0 +1,76 @@ +# 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.common.net.layouttestresults import LayoutTestResults + + +class LayoutTestResultsTest(unittest.TestCase): + _example_results_html = """ +<html> +<head> +<title>Layout Test Results</title> +</head> +<body> +<p>Tests that had stderr output:</p> +<table> +<tr> +<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/accessibility/aria-activedescendant-crash.html">accessibility/aria-activedescendant-crash.html</a></td> +<td><a href="accessibility/aria-activedescendant-crash-stderr.txt">stderr</a></td> +</tr> +<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/http/tests/security/canvas-remote-read-svg-image.html">http/tests/security/canvas-remote-read-svg-image.html</a></td> +<td><a href="http/tests/security/canvas-remote-read-svg-image-stderr.txt">stderr</a></td> +</tr> +</table><p>Tests that had no expected results (probably new):</p> +<table> +<tr> +<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/fast/repaint/no-caret-repaint-in-non-content-editable-element.html">fast/repaint/no-caret-repaint-in-non-content-editable-element.html</a></td> +<td><a href="fast/repaint/no-caret-repaint-in-non-content-editable-element-actual.txt">result</a></td> +</tr> +</table></body> +</html> +""" + + _expected_layout_test_results = { + 'Tests that had stderr output:': [ + 'accessibility/aria-activedescendant-crash.html', + ], + 'Tests that had no expected results (probably new):': [ + 'fast/repaint/no-caret-repaint-in-non-content-editable-element.html', + ], + } + + def test_parse_layout_test_results(self): + results = LayoutTestResults._parse_results_html(self._example_results_html) + self.assertEqual(self._expected_layout_test_results, results) + + def test_results_from_string(self): + self.assertEqual(LayoutTestResults.results_from_string(""), None) + results = LayoutTestResults.results_from_string(self._example_results_html) + self.assertEqual(len(results.failing_tests()), 0) diff --git a/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py b/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py index c82fc6f..de19e94 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py +++ b/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py @@ -29,7 +29,7 @@ import logging import time -from webkitpy.thirdparty.autoinstalled.mechanize import HTTPError +from webkitpy.thirdparty.autoinstalled import mechanize from webkitpy.common.system.deprecated_logging import log @@ -41,10 +41,11 @@ class NetworkTimeout(Exception): class NetworkTransaction(object): - def __init__(self, initial_backoff_seconds=10, grown_factor=1.5, timeout_seconds=10*60): + def __init__(self, initial_backoff_seconds=10, grown_factor=1.5, timeout_seconds=(10 * 60), convert_404_to_None=False): self._initial_backoff_seconds = initial_backoff_seconds self._grown_factor = grown_factor self._timeout_seconds = timeout_seconds + self._convert_404_to_None = convert_404_to_None def run(self, request): self._total_sleep = 0 @@ -52,7 +53,10 @@ class NetworkTransaction(object): while True: try: return request() - except HTTPError, e: + # FIXME: We should catch urllib2.HTTPError here too. + except mechanize.HTTPError, e: + if self._convert_404_to_None and e.code == 404: + return None self._check_for_timeout() _log.warn("Received HTTP status %s from server. Retrying in " "%s seconds..." % (e.code, self._backoff_seconds)) diff --git a/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py index cd0702b..49aaeed 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py @@ -56,29 +56,36 @@ class NetworkTransactionTest(LoggingTestCase): self.assertTrue(did_throw_exception) self.assertTrue(did_process_exception) - def _raise_http_error(self): + def _raise_500_error(self): self._run_count += 1 if self._run_count < 3: - raise HTTPError("http://example.com/", 500, "inteneral server error", None, None) + raise HTTPError("http://example.com/", 500, "internal server error", None, None) return 42 + def _raise_404_error(self): + raise HTTPError("http://foo.com/", 404, "not found", None, None) + def test_retry(self): self._run_count = 0 transaction = NetworkTransaction(initial_backoff_seconds=0) - self.assertEqual(transaction.run(lambda: self._raise_http_error()), 42) + self.assertEqual(transaction.run(lambda: self._raise_500_error()), 42) self.assertEqual(self._run_count, 3) self.assertLog(['WARNING: Received HTTP status 500 from server. ' 'Retrying in 0 seconds...\n', 'WARNING: Received HTTP status 500 from server. ' 'Retrying in 0.0 seconds...\n']) + def test_convert_404_to_None(self): + transaction = NetworkTransaction(convert_404_to_None=True) + self.assertEqual(transaction.run(lambda: self._raise_404_error()), None) + def test_timeout(self): self._run_count = 0 transaction = NetworkTransaction(initial_backoff_seconds=60*60, timeout_seconds=60) did_process_exception = False did_throw_exception = True try: - transaction.run(lambda: self._raise_http_error()) + transaction.run(lambda: self._raise_500_error()) did_throw_exception = False except NetworkTimeout, e: did_process_exception = True diff --git a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py index 231459f..ad89815 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py +++ b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py @@ -28,10 +28,11 @@ class RegressionWindow(object): - def __init__(self, build_before_failure, failing_build, common_failures=None): + def __init__(self, build_before_failure, failing_build, failing_tests=None): self._build_before_failure = build_before_failure self._failing_build = failing_build - self._common_failures = common_failures + self._failing_tests = failing_tests + self._revisions = None def build_before_failure(self): return self._build_before_failure @@ -39,10 +40,12 @@ class RegressionWindow(object): def failing_build(self): return self._failing_build - def common_failures(self): - return self._common_failures + def failing_tests(self): + return self._failing_tests def revisions(self): - revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1) - revisions.reverse() - return revisions + # Cache revisions to avoid excessive allocations. + if not self._revisions: + self._revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1) + self._revisions.reverse() + return self._revisions diff --git a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py index 57390b8..3d03dcd 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py +++ b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py @@ -41,14 +41,18 @@ _log = logging.getLogger("webkitpy.common.net.statusserver") class StatusServer: default_host = "queues.webkit.org" - def __init__(self, host=default_host): + def __init__(self, host=default_host, browser=None, bot_id=None): self.set_host(host) - self.browser = Browser() + self._browser = browser or Browser() + self.set_bot_id(bot_id) def set_host(self, host): self.host = host self.url = "http://%s" % self.host + def set_bot_id(self, bot_id): + self.bot_id = bot_id + def results_url_for_status(self, status_id): return "%s/results/%s" % (self.url, status_id) @@ -56,14 +60,14 @@ class StatusServer: if not patch: return if patch.bug_id(): - self.browser["bug_id"] = unicode(patch.bug_id()) + self._browser["bug_id"] = unicode(patch.bug_id()) if patch.id(): - self.browser["patch_id"] = unicode(patch.id()) + self._browser["patch_id"] = unicode(patch.id()) def _add_results_file(self, results_file): if not results_file: return - self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file') + self._browser.add_file(results_file, "text/plain", "results.txt", 'results_file') def _post_status_to_server(self, queue_name, status, patch, results_file): if results_file: @@ -71,36 +75,61 @@ class StatusServer: results_file.seek(0) update_status_url = "%s/update-status" % self.url - self.browser.open(update_status_url) - self.browser.select_form(name="update_status") - self.browser["queue_name"] = queue_name + self._browser.open(update_status_url) + self._browser.select_form(name="update_status") + self._browser["queue_name"] = queue_name + if self.bot_id: + self._browser["bot_id"] = self.bot_id self._add_patch(patch) - self.browser["status"] = status + self._browser["status"] = status self._add_results_file(results_file) - return self.browser.submit().read() # This is the id of the newly created status object. + return self._browser.submit().read() # This is the id of the newly created status object. def _post_svn_revision_to_server(self, svn_revision_number, broken_bot): update_svn_revision_url = "%s/update-svn-revision" % self.url - self.browser.open(update_svn_revision_url) - self.browser.select_form(name="update_svn_revision") - self.browser["number"] = unicode(svn_revision_number) - self.browser["broken_bot"] = broken_bot - return self.browser.submit().read() + self._browser.open(update_svn_revision_url) + self._browser.select_form(name="update_svn_revision") + self._browser["number"] = unicode(svn_revision_number) + self._browser["broken_bot"] = broken_bot + return self._browser.submit().read() def _post_work_items_to_server(self, queue_name, work_items): update_work_items_url = "%s/update-work-items" % self.url - self.browser.open(update_work_items_url) - self.browser.select_form(name="update_work_items") - self.browser["queue_name"] = queue_name + self._browser.open(update_work_items_url) + self._browser.select_form(name="update_work_items") + self._browser["queue_name"] = queue_name work_items = map(unicode, work_items) # .join expects strings - self.browser["work_items"] = " ".join(work_items) - return self.browser.submit().read() + self._browser["work_items"] = " ".join(work_items) + return self._browser.submit().read() + + def _post_work_item_to_ews(self, attachment_id): + submit_to_ews_url = "%s/submit-to-ews" % self.url + self._browser.open(submit_to_ews_url) + self._browser.select_form(name="submit_to_ews") + self._browser["attachment_id"] = unicode(attachment_id) + self._browser.submit() + + def submit_to_ews(self, attachment_id): + _log.info("Submitting attachment %s to EWS queues" % attachment_id) + return NetworkTransaction().run(lambda: self._post_work_item_to_ews(attachment_id)) def next_work_item(self, queue_name): _log.debug("Fetching next work item for %s" % queue_name) patch_status_url = "%s/next-patch/%s" % (self.url, queue_name) return self._fetch_url(patch_status_url) + def _post_release_work_item(self, queue_name, patch): + release_patch_url = "%s/release-patch" % (self.url) + self._browser.open(release_patch_url) + self._browser.select_form(name="release_patch") + self._browser["queue_name"] = queue_name + self._browser["attachment_id"] = unicode(patch.id()) + self._browser.submit() + + def release_work_item(self, queue_name, patch): + _log.debug("Releasing work item %s from %s" % (patch.id(), queue_name)) + return NetworkTransaction(convert_404_to_None=True).run(lambda: self._post_release_work_item(queue_name, patch)) + def update_work_items(self, queue_name, work_items): _log.debug("Recording work items: %s for %s" % (work_items, queue_name)) return NetworkTransaction().run(lambda: self._post_work_items_to_server(queue_name, work_items)) @@ -114,6 +143,7 @@ class StatusServer: return NetworkTransaction().run(lambda: self._post_svn_revision_to_server(svn_revision_number, broken_bot)) def _fetch_url(self, url): + # FIXME: This should use NetworkTransaction's 404 handling instead. try: return urllib2.urlopen(url).read() except urllib2.HTTPError, e: diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/patchcollection_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py index 4ec6e25..1169ba0 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/patchcollection_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py @@ -1,9 +1,9 @@ -# Copyright (c) 2009 Google 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 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 @@ -13,7 +13,7 @@ # * 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 @@ -28,25 +28,16 @@ import unittest -from webkitpy.tool.bot.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate -from webkitpy.thirdparty.mock import Mock - - -class TestPersistentPatchCollectionDelegate(PersistentPatchCollectionDelegate): - def collection_name(self): - return "test-collection" - - def fetch_potential_patch_ids(self): - return [42, 192, 87] - - def status_server(self): - return Mock() - - def is_terminal_status(self, status): - return False +from webkitpy.common.net.statusserver import StatusServer +from webkitpy.common.system.outputcapture import OutputCaptureTestCaseBase +from webkitpy.tool.mocktool import MockBrowser -class PersistentPatchCollectionTest(unittest.TestCase): - def test_next(self): - collection = PersistentPatchCollection(TestPersistentPatchCollectionDelegate()) - collection.next() +class StatusServerTest(OutputCaptureTestCaseBase): + def test_url_for_issue(self): + mock_browser = MockBrowser() + status_server = StatusServer(browser=mock_browser, bot_id='123') + status_server.update_status('queue name', 'the status') + self.assertEqual('queue name', mock_browser.params['queue_name']) + self.assertEqual('the status', mock_browser.params['status']) + self.assertEqual('123', mock_browser.params['bot_id']) diff --git a/WebKitTools/Scripts/webkitpy/common/system/executive.py b/WebKitTools/Scripts/webkitpy/common/system/executive.py index 7c00f22..216cf58 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/executive.py +++ b/WebKitTools/Scripts/webkitpy/common/system/executive.py @@ -103,6 +103,13 @@ class Executive(object): def _run_command_with_teed_output(self, args, teed_output): args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) + if sys.platform == 'cygwin': + # Cygwin's Python's os.execv doesn't support unicode command + # arguments, and neither does Cygwin's execv itself. + # FIXME: Using UTF-8 here will confuse Windows-native commands + # which will expect arguments to be encoded using the current code + # page. + args = [arg.encode('utf-8') for arg in args] child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -281,6 +288,13 @@ class Executive(object): assert(isinstance(args, list) or isinstance(args, tuple)) start_time = time.time() args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) + if sys.platform == 'cygwin': + # Cygwin's Python's os.execv doesn't support unicode command + # arguments, and neither does Cygwin's execv itself. + # FIXME: Using UTF-8 here will confuse Windows-native commands + # which will expect arguments to be encoded using the current code + # page. + args = [arg.encode('utf-8') for arg in args] stdin, string_to_communicate = self._compute_stdin(input) stderr = subprocess.STDOUT if return_stderr else None diff --git a/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py b/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py index 68a3919..45e0e3f 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py +++ b/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py @@ -29,6 +29,7 @@ # Class for unittest support. Used for capturing stderr/stdout. import sys +import unittest from StringIO import StringIO class OutputCapture(object): @@ -37,7 +38,9 @@ class OutputCapture(object): def _capture_output_with_name(self, output_name): self.saved_outputs[output_name] = getattr(sys, output_name) - setattr(sys, output_name, StringIO()) + captured_output = StringIO() + setattr(sys, output_name, captured_output) + return captured_output def _restore_output_with_name(self, output_name): captured_output = getattr(sys, output_name).getvalue() @@ -46,8 +49,7 @@ class OutputCapture(object): return captured_output def capture_output(self): - self._capture_output_with_name("stdout") - self._capture_output_with_name("stderr") + return (self._capture_output_with_name("stdout"), self._capture_output_with_name("stderr")) def restore_output(self): return (self._restore_output_with_name("stdout"), self._restore_output_with_name("stderr")) @@ -63,3 +65,22 @@ class OutputCapture(object): testcase.assertEqual(stderr_string, expected_stderr) # This is a little strange, but I don't know where else to return this information. return return_value + + +class OutputCaptureTestCaseBase(unittest.TestCase): + def setUp(self): + unittest.TestCase.setUp(self) + self.output_capture = OutputCapture() + (self.__captured_stdout, self.__captured_stderr) = self.output_capture.capture_output() + + def tearDown(self): + del self.__captured_stdout + del self.__captured_stderr + self.output_capture.restore_output() + unittest.TestCase.tearDown(self) + + def assertStdout(self, expected_stdout): + self.assertEquals(expected_stdout, self.__captured_stdout.getvalue()) + + def assertStderr(self, expected_stderr): + self.assertEquals(expected_stderr, self.__captured_stderr.getvalue()) diff --git a/WebKitTools/Scripts/webkitpy/common/system/path.py b/WebKitTools/Scripts/webkitpy/common/system/path.py new file mode 100644 index 0000000..43c6410 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/system/path.py @@ -0,0 +1,134 @@ +# 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. + +"""generic routines to convert platform-specific paths to URIs.""" +from __future__ import with_statement + +import atexit +import subprocess +import sys +import threading +import urllib + + +def abspath_to_uri(path, platform=None): + """Converts a platform-specific absolute path to a file: URL.""" + if platform is None: + platform = sys.platform + return "file:" + _escape(_convert_path(path, platform)) + + +def cygpath(path): + """Converts a cygwin path to Windows path.""" + return _CygPath.convert_using_singleton(path) + + +# Note that this object is not threadsafe and must only be called +# from multiple threads under protection of a lock (as is done in cygpath()) +class _CygPath(object): + """Manages a long-running 'cygpath' process for file conversion.""" + _lock = None + _singleton = None + + @staticmethod + def stop_cygpath_subprocess(): + if not _CygPath._lock: + return + + with _CygPath._lock: + if _CygPath._singleton: + _CygPath._singleton.stop() + + @staticmethod + def convert_using_singleton(path): + if not _CygPath._lock: + _CygPath._lock = threading.Lock() + + with _CygPath._lock: + if not _CygPath._singleton: + _CygPath._singleton = _CygPath() + # Make sure the cygpath subprocess always gets shutdown cleanly. + atexit.register(_CygPath.stop_cygpath_subprocess) + + return _CygPath._singleton.convert(path) + + def __init__(self): + self._child_process = None + + def start(self): + assert(self._child_process is None) + args = ['cygpath', '-f', '-', '-wa'] + self._child_process = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + + def is_running(self): + if not self._child_process: + return False + return self._child_process.returncode is None + + def stop(self): + if self._child_process: + self._child_process.stdin.close() + self._child_process.wait() + self._child_process = None + + def convert(self, path): + if not self.is_running(): + self.start() + self._child_process.stdin.write("%s\r\n" % path) + self._child_process.stdin.flush() + return self._child_process.stdout.readline().rstrip() + + +def _escape(path): + """Handle any characters in the path that should be escaped.""" + # FIXME: web browsers don't appear to blindly quote every character + # when converting filenames to files. Instead of using urllib's default + # rules, we allow a small list of other characters through un-escaped. + # It's unclear if this is the best possible solution. + return urllib.quote(path, safe='/+:') + + +def _convert_path(path, platform): + """Handles any os-specific path separators, mappings, etc.""" + if platform == 'win32': + return _winpath_to_uri(path) + if platform == 'cygwin': + return _winpath_to_uri(cygpath(path)) + return _unixypath_to_uri(path) + + +def _winpath_to_uri(path): + """Converts a window absolute path to a file: URL.""" + return "///" + path.replace("\\", "/") + + +def _unixypath_to_uri(path): + """Converts a unix-style path to a file: URL.""" + return "//" + path diff --git a/WebKitTools/Scripts/webkitpy/common/system/path_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/path_unittest.py new file mode 100644 index 0000000..4dbd38a --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/system/path_unittest.py @@ -0,0 +1,105 @@ +# 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 +import sys + +import path + +class AbspathTest(unittest.TestCase): + def assertMatch(self, test_path, expected_uri, + platform=None): + if platform == 'cygwin' and sys.platform != 'cygwin': + return + self.assertEqual(path.abspath_to_uri(test_path, platform=platform), + expected_uri) + + def test_abspath_to_uri_cygwin(self): + if sys.platform != 'cygwin': + return + + self.assertMatch('/cygdrive/c/foo/bar.html', + 'file:///C:/foo/bar.html', + platform='cygwin') + self.assertEqual(path.abspath_to_uri('/cygdrive/c/foo/bar.html', + platform='cygwin'), + 'file:///C:/foo/bar.html') + + def test_abspath_to_uri_darwin(self): + self.assertMatch('/foo/bar.html', + 'file:///foo/bar.html', + platform='darwin') + self.assertEqual(path.abspath_to_uri("/foo/bar.html", + platform='darwin'), + "file:///foo/bar.html") + + def test_abspath_to_uri_linux2(self): + self.assertMatch('/foo/bar.html', + 'file:///foo/bar.html', + platform='darwin') + self.assertEqual(path.abspath_to_uri("/foo/bar.html", + platform='linux2'), + "file:///foo/bar.html") + + def test_abspath_to_uri_win(self): + self.assertMatch('c:\\foo\\bar.html', + 'file:///c:/foo/bar.html', + platform='win32') + self.assertEqual(path.abspath_to_uri("c:\\foo\\bar.html", + platform='win32'), + "file:///c:/foo/bar.html") + + def test_abspath_to_uri_escaping(self): + self.assertMatch('/foo/bar + baz%?.html', + 'file:///foo/bar%20+%20baz%25%3F.html', + platform='darwin') + self.assertMatch('/foo/bar + baz%?.html', + 'file:///foo/bar%20+%20baz%25%3F.html', + platform='linux2') + + # Note that you can't have '?' in a filename on windows. + self.assertMatch('/cygdrive/c/foo/bar + baz%.html', + 'file:///C:/foo/bar%20+%20baz%25.html', + platform='cygwin') + + def test_stop_cygpath_subprocess(self): + if sys.platform != 'cygwin': + return + + # Call cygpath to ensure the subprocess is running. + path.cygpath("/cygdrive/c/foo.txt") + self.assertTrue(path._CygPath._singleton.is_running()) + + # Stop it. + path._CygPath.stop_cygpath_subprocess() + + # Ensure that it is stopped. + self.assertFalse(path._CygPath._singleton.is_running()) + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/common/system/user.py b/WebKitTools/Scripts/webkitpy/common/system/user.py index 240b67b..8917137 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user.py @@ -96,6 +96,10 @@ class User(object): # Note: Not thread safe: http://bugs.python.org/issue2320 subprocess.call(args + files) + def _warn_if_application_is_xcode(self, edit_application): + if "Xcode" in edit_application: + print "Instead of using Xcode.app, consider using EDITOR=\"xed --wait\"." + def edit_changelog(self, files): edit_application = os.environ.get("CHANGE_LOG_EDIT_APPLICATION") if edit_application and sys.platform == "darwin": @@ -103,8 +107,7 @@ class User(object): args = shlex.split(edit_application) print "Using editor in the CHANGE_LOG_EDIT_APPLICATION environment variable." print "Please quit the editor application when done editing." - if edit_application.find("Xcode.app"): - print "Instead of using Xcode.app, consider using EDITOR=\"xed --wait\"." + self._warn_if_application_is_xcode(edit_application) subprocess.call(["open", "-W", "-n", "-a"] + args + files) return self.edit(files) diff --git a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py index ae1bad5..7ec9b34 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py @@ -97,5 +97,13 @@ class UserTest(unittest.TestCase): raw_input=mock_raw_input) self.assertEquals(expected[1], result) -if __name__ == '__main__': - unittest.main() + def test_warn_if_application_is_xcode(self): + output = OutputCapture() + user = User() + output.assert_outputs(self, user._warn_if_application_is_xcode, ["TextMate"]) + output.assert_outputs(self, user._warn_if_application_is_xcode, ["/Applications/TextMate.app"]) + output.assert_outputs(self, user._warn_if_application_is_xcode, ["XCode"]) # case sensitive matching + + xcode_warning = "Instead of using Xcode.app, consider using EDITOR=\"xed --wait\".\n" + output.assert_outputs(self, user._warn_if_application_is_xcode, ["Xcode"], expected_stdout=xcode_warning) + output.assert_outputs(self, user._warn_if_application_is_xcode, ["/Developer/Applications/Xcode.app"], expected_stdout=xcode_warning) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py index bb9604f..309bf8d 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py @@ -205,3 +205,6 @@ class ListDuplicatesTest(unittest.TestCase): for expected, inputs in test_cases: self.assertEquals(expected, deduplicate_tests.get_relative_test_path(*inputs)) + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py index 970de60..e0fd1b6 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # Copyright (C) 2010 Google Inc. All rights reserved. +# Copyright (C) 2010 Gabor Rapcsanyi (rgabor@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 @@ -271,23 +272,26 @@ class TestShellThread(WatchableThread): self._test_types = test_types self._test_args = test_args self._driver = None - self._directory_timing_stats = {} + self._test_group_timing_stats = {} self._test_results = [] self._num_tests = 0 self._start_time = 0 self._stop_time = 0 - - # Current directory of tests we're running. - self._current_dir = None - # Number of tests in self._current_dir. - self._num_tests_in_current_dir = None - # Time at which we started running tests from self._current_dir. - self._current_dir_start_time = None - - def get_directory_timing_stats(self): - """Returns a dictionary mapping test directory to a tuple of - (number of tests in that directory, time to run the tests)""" - return self._directory_timing_stats + self._have_http_lock = False + self._http_lock_wait_begin = 0 + self._http_lock_wait_end = 0 + + # Current group of tests we're running. + self._current_group = None + # Number of tests in self._current_group. + self._num_tests_in_current_group = None + # Time at which we started running tests from self._current_group. + self._current_group_start_time = None + + def get_test_group_timing_stats(self): + """Returns a dictionary mapping test group to a tuple of + (number of tests in that group, time to run the tests)""" + return self._test_group_timing_stats def get_test_results(self): """Return the list of all tests run on this thread. @@ -298,7 +302,8 @@ class TestShellThread(WatchableThread): return self._test_results def get_total_time(self): - return max(self._stop_time - self._start_time, 0.0) + return max(self._stop_time - self._start_time - + self._http_lock_wait_time(), 0.0) def get_num_tests(self): return self._num_tests @@ -337,6 +342,25 @@ class TestShellThread(WatchableThread): do multi-threaded debugging.""" self._run(test_runner, result_summary) + def cancel(self): + """Clean up http lock and set a flag telling this thread to quit.""" + self._stop_http_lock() + WatchableThread.cancel(self) + + def next_timeout(self): + """Return the time the test is supposed to finish by.""" + if self._next_timeout: + return self._next_timeout + self._http_lock_wait_time() + return self._next_timeout + + def _http_lock_wait_time(self): + """Return the time what http locking takes.""" + if self._http_lock_wait_begin == 0: + return 0 + if self._http_lock_wait_end == 0: + return time.time() - self._http_lock_wait_begin + return self._http_lock_wait_end - self._http_lock_wait_begin + def _run(self, test_runner, result_summary): """Main work entry point of the thread. Basically we pull urls from the filename queue and run the tests until we run out of urls. @@ -359,21 +383,35 @@ class TestShellThread(WatchableThread): return if len(self._filename_list) is 0: - if self._current_dir is not None: - self._directory_timing_stats[self._current_dir] = \ - (self._num_tests_in_current_dir, - time.time() - self._current_dir_start_time) + if self._current_group is not None: + self._test_group_timing_stats[self._current_group] = \ + (self._num_tests_in_current_group, + time.time() - self._current_group_start_time) try: - self._current_dir, self._filename_list = \ + self._current_group, self._filename_list = \ self._filename_list_queue.get_nowait() except Queue.Empty: + self._stop_http_lock() self._kill_dump_render_tree() tests_run_file.close() return - self._num_tests_in_current_dir = len(self._filename_list) - self._current_dir_start_time = time.time() + if self._options.wait_for_httpd: + if self._current_group == "tests_to_http_lock": + self._http_lock_wait_begin = time.time() + self._port.acquire_http_lock() + + self._port.start_http_server() + self._port.start_websocket_server() + + self._have_http_lock = True + self._http_lock_wait_end = time.time() + elif self._have_http_lock: + self._stop_http_lock() + + self._num_tests_in_current_group = len(self._filename_list) + self._current_group_start_time = time.time() test_info = self._filename_list.pop() @@ -517,6 +555,14 @@ class TestShellThread(WatchableThread): self._options) self._driver.start() + def _stop_http_lock(self): + """Stop the servers and release http lock.""" + if self._have_http_lock: + self._port.stop_http_server() + self._port.stop_websocket_server() + self._port.release_http_lock() + self._have_http_lock = False + def _kill_dump_render_tree(self): """Kill the DumpRenderTree process if it's running.""" if self._driver: diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py index 6a5d43b..cd7d663 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py @@ -42,33 +42,35 @@ import sys import time import apache_http_server -import test_files +import http_lock import http_server +import test_files import websocket_server from webkitpy.common.system import logutils from webkitpy.common.system.executive import Executive, ScriptError +from webkitpy.common.system.path import abspath_to_uri from webkitpy.common.system.user import User _log = logutils.get_logger(__file__) -# 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 -_wdiff_available = True -_pretty_patch_available = True +class DummyOptions(object): + """Fake implementation of optparse.Values. Cloned from + webkitpy.tool.mocktool.MockOptions. + + """ + + def __init__(self, **kwargs): + # The caller can set option values using keyword arguments. We don't + # set any values by default because we don't know how this + # object will be used. Generally speaking unit tests should + # subclass this or provider wrapper functions that set a common + # set of options. + for key, value in kwargs.items(): + self.__dict__[key] = value + # FIXME: This class should merge with webkitpy.webkit_port at some point. class Port(object): @@ -85,13 +87,41 @@ class Port(object): def __init__(self, **kwargs): self._name = kwargs.get('port_name', None) - self._options = kwargs.get('options', None) + self._options = kwargs.get('options') + if self._options is None: + # FIXME: Ideally we'd have a package-wide way to get a + # well-formed options object that had all of the necessary + # options defined on it. + self._options = DummyOptions() self._executive = kwargs.get('executive', Executive()) self._user = kwargs.get('user', User()) self._helper = None self._http_server = None self._webkit_base_dir = None self._websocket_server = None + self._http_lock = None + + # 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 + self._wdiff_available = True + + self._pretty_patch_path = self.path_from_webkit_base("BugsSite", + "PrettyPatch", "prettify.rb") + self._pretty_patch_available = True + self.set_option_default('configuration', None) + if self._options.configuration is None: + self._options.configuration = self.default_configuration() def default_child_processes(self): """Return the number of DumpRenderTree instances to use for this @@ -125,6 +155,27 @@ class Port(object): """This routine is used to check whether image_diff binary exists.""" raise NotImplementedError('Port.check_image_diff') + def check_pretty_patch(self): + """Checks whether we can use the PrettyPatch ruby script.""" + + # check if Ruby is installed + try: + result = self._executive.run_command(['ruby', '--version']) + except OSError, e: + if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: + _log.error("Ruby is not installed; " + "can't generate pretty patches.") + _log.error('') + return False + + if not self.path_exists(self._pretty_patch_path): + _log.error('Unable to find %s .' % self._pretty_patch_path) + _log.error("Can't generate pretty patches.") + _log.error('') + return False + + return True + def compare_text(self, expected_text, actual_text): """Return whether or not the two strings are *not* equal. This routine is used to diff text output. @@ -259,7 +310,10 @@ class Port(object): path = self.expected_filename(test, extension) if not os.path.exists(path): return None - with codecs.open(path, 'r', encoding) as file: + open_mode = 'r' + if encoding is None: + open_mode = 'r+b' + with codecs.open(path, open_mode, encoding) as file: return file.read() def expected_checksum(self, test): @@ -281,22 +335,18 @@ class Port(object): return text.strip("\r\n").replace("\r\n", "\n") + "\n" def filename_to_uri(self, filename): - """Convert a test file to a URI.""" + """Convert a test file (which is an absolute path) to a URI.""" LAYOUTTEST_HTTP_DIR = "http/tests/" - LAYOUTTEST_WEBSOCKET_DIR = "websocket/tests/" + LAYOUTTEST_WEBSOCKET_DIR = "http/tests/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 + if (relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR) + or relative_path.startswith(LAYOUTTEST_HTTP_DIR)): 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. @@ -311,9 +361,7 @@ class Port(object): 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) + return abspath_to_uri(os.path.abspath(filename)) def tests(self, paths): """Return the list of tests found (relative to layout_tests_dir().""" @@ -349,7 +397,10 @@ class Port(object): data: contents of the baseline. encoding: file encoding to use for the baseline. """ - with codecs.open(path, "w", encoding=encoding) as file: + write_mode = "w" + if encoding is None: + write_mode = "wb" + with codecs.open(path, write_mode, encoding=encoding) as file: file.write(data) def uri_to_test_name(self, uri): @@ -362,12 +413,8 @@ class Port(object): """ test = uri if uri.startswith("file:///"): - if sys.platform == 'win32': - test = test.replace('file:///', '') - test = test.replace('/', '\\') - else: - test = test.replace('file://', '') - return self.relative_test_filename(test) + prefix = abspath_to_uri(self.layout_tests_dir()) + "/" + return test[len(prefix):] if uri.startswith("http://127.0.0.1:8880/"): # websocket tests @@ -382,13 +429,6 @@ class Port(object): raise NotImplementedError('unknown url type: %s' % uri) - 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') @@ -420,6 +460,18 @@ class Port(object): may be different (e.g., 'win-xp' instead of 'chromium-win-xp'.""" return self._name + def get_option(self, name, default_value=None): + # FIXME: Eventually we should not have to do a test for + # hasattr(), and we should be able to just do + # self.options.value. See additional FIXME in the constructor. + if hasattr(self._options, name): + return getattr(self._options, name) + return default_value + + def set_option_default(self, name, default_value): + if not hasattr(self._options, name): + return setattr(self._options, name, default_value) + # FIXME: This could be replaced by functions in webkitpy.common.checkout.scm. def path_from_webkit_base(self, *comps): """Returns the full path to path made by joining the top of the @@ -445,7 +497,7 @@ class Port(object): """Relative unix-style path for a filename under the LayoutTests directory. Filenames outside the LayoutTests directory should raise an error.""" - assert(filename.startswith(self.layout_tests_dir())) + #assert(filename.startswith(self.layout_tests_dir())) return filename[len(self.layout_tests_dir()) + 1:] def results_directory(self): @@ -484,12 +536,12 @@ class Port(object): """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: + if self.get_option('use_apache'): self._http_server = apache_http_server.LayoutTestApacheHttpd(self, - self._options.results_directory) + self.get_option('results_directory')) else: self._http_server = http_server.Lighttpd(self, - self._options.results_directory) + self.get_option('results_directory')) self._http_server.start() def start_websocket_server(self): @@ -497,9 +549,13 @@ class Port(object): 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.get_option('results_directory')) self._websocket_server.start() + def acquire_http_lock(self): + self._http_lock = http_lock.HttpLock(None) + self._http_lock.wait_for_httpd_lock() + def stop_helper(self): """Shut down the test helper if it is running. Do nothing if it isn't, or it isn't available. If a port overrides start_helper() @@ -518,6 +574,10 @@ class Port(object): if self._websocket_server: self._websocket_server.stop() + def release_http_lock(self): + if self._http_lock: + self._http_lock.cleanup_http_lock() + def test_expectations(self): """Returns the test expectations for this port. @@ -628,8 +688,7 @@ class Port(object): """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.""" - global _wdiff_available # See explaination at top of file. - if not _wdiff_available: + if not self._wdiff_available: return "" try: # It's possible to raise a ScriptError we pass wdiff invalid paths. @@ -637,33 +696,33 @@ class Port(object): except OSError, e: if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: # Silently ignore cases where wdiff is missing. - _wdiff_available = False + self._wdiff_available = False return "" raise - _pretty_patch_error_html = "Failed to run PrettyPatch, see error console." + # This is a class variable so we can test error output easily. + _pretty_patch_error_html = "Failed to run PrettyPatch, see error log." def pretty_patch_text(self, diff_path): - # FIXME: Much of this function could move to prettypatch.rb - global _pretty_patch_available - if not _pretty_patch_available: + if not self._pretty_patch_available: return self._pretty_patch_error_html - pretty_patch_path = self.path_from_webkit_base("BugsSite", "PrettyPatch") - prettify_path = os.path.join(pretty_patch_path, "prettify.rb") - command = ["ruby", "-I", pretty_patch_path, prettify_path, diff_path] + command = ("ruby", "-I", os.path.dirname(self._pretty_patch_path), + self._pretty_patch_path, diff_path) try: # Diffs are treated as binary (we pass decode_output=False) as they # may contain multiple files of conflicting encodings. return self._executive.run_command(command, decode_output=False) except OSError, e: # If the system is missing ruby log the error and stop trying. - _pretty_patch_available = False + self._pretty_patch_available = False _log.error("Failed to run PrettyPatch (%s): %s" % (command, e)) return self._pretty_patch_error_html except ScriptError, e: - # If ruby failed to run for some reason, log the command output and stop trying. - _pretty_patch_available = False - _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output())) + # If ruby failed to run for some reason, log the command + # output and stop trying. + self._pretty_patch_available = False + _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, + e.message_with_output())) return self._pretty_patch_error_html def _webkit_build_directory(self, args): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py index 71877b3..93f8808 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/base_unittest.py @@ -26,16 +26,19 @@ # (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 base +import optparse import os import StringIO import sys import tempfile import unittest +from webkitpy.common.system.path import abspath_to_uri from webkitpy.common.system.executive import Executive, ScriptError from webkitpy.thirdparty.mock import Mock +from webkitpy.tool import mocktool +import base # FIXME: This makes StringIO objects work with "with". Remove # when we upgrade to 2.6. @@ -139,11 +142,11 @@ class PortTest(unittest.TestCase): expected_wdiff = "<head><style>.del { background: #faa; } .add { background: #afa; }</style></head><pre><span class=del>foo</span><span class=add>bar</span></pre>" self.assertEqual(wdiff, expected_wdiff) # Running the full wdiff_text method should give the same result. - base._wdiff_available = True # In case it's somehow already disabled. + port._wdiff_available = True # In case it's somehow already disabled. wdiff = port.wdiff_text(actual.name, expected.name) self.assertEqual(wdiff, expected_wdiff) # wdiff should still be available after running wdiff_text with a valid diff. - self.assertTrue(base._wdiff_available) + self.assertTrue(port._wdiff_available) actual.close() expected.close() @@ -151,7 +154,7 @@ class PortTest(unittest.TestCase): self.assertRaises(ScriptError, port._run_wdiff, "/does/not/exist", "/does/not/exist2") self.assertRaises(ScriptError, port.wdiff_text, "/does/not/exist", "/does/not/exist2") # wdiff will still be available after running wdiff_text with invalid paths. - self.assertTrue(base._wdiff_available) + self.assertTrue(port._wdiff_available) base._wdiff_available = True # If wdiff does not exist _run_wdiff should throw an OSError. @@ -161,8 +164,7 @@ class PortTest(unittest.TestCase): # wdiff_text should not throw an error if wdiff does not exist. self.assertEqual(port.wdiff_text("foo", "bar"), "") # However wdiff should not be available after running wdiff_text if wdiff is missing. - self.assertFalse(base._wdiff_available) - base._wdiff_available = True + self.assertFalse(port._wdiff_available) def test_diff_text(self): port = base.Port() @@ -226,6 +228,63 @@ class PortTest(unittest.TestCase): self.assertTrue('canvas' in dirs) self.assertTrue('css2.1' in dirs) + def test_filename_to_uri(self): + + port = base.Port() + layout_test_dir = port.layout_tests_dir() + test_file = os.path.join(layout_test_dir, "foo", "bar.html") + + # On Windows, absolute paths are of the form "c:\foo.txt". However, + # all current browsers (except for Opera) normalize file URLs by + # prepending an additional "/" as if the absolute path was + # "/c:/foo.txt". This means that all file URLs end up with "file:///" + # at the beginning. + if sys.platform == 'win32': + prefix = "file:///" + path = test_file.replace("\\", "/") + else: + prefix = "file://" + path = test_file + + self.assertEqual(port.filename_to_uri(test_file), + abspath_to_uri(test_file)) + + def test_get_option__set(self): + options, args = optparse.OptionParser().parse_args() + options.foo = 'bar' + port = base.Port(options=options) + self.assertEqual(port.get_option('foo'), 'bar') + + def test_get_option__unset(self): + port = base.Port() + self.assertEqual(port.get_option('foo'), None) + + def test_get_option__default(self): + port = base.Port() + self.assertEqual(port.get_option('foo', 'bar'), 'bar') + + def test_set_option_default__unset(self): + port = base.Port() + port.set_option_default('foo', 'bar') + self.assertEqual(port.get_option('foo'), 'bar') + + def test_set_option_default__set(self): + options, args = optparse.OptionParser().parse_args() + options.foo = 'bar' + port = base.Port(options=options) + # This call should have no effect. + port.set_option_default('foo', 'new_bar') + self.assertEqual(port.get_option('foo'), 'bar') + + def test_name__unset(self): + port = base.Port() + self.assertEqual(port.name(), None) + + def test_name__set(self): + port = base.Port(port_name='foo') + self.assertEqual(port.name(), 'foo') + + class VirtualTest(unittest.TestCase): """Tests that various methods expected to be virtual are.""" def assertVirtual(self, method, *args, **kwargs): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py index a72627a..4d17b51 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py @@ -43,6 +43,10 @@ import tempfile import time import webbrowser +from webkitpy.common.system.executive import Executive +from webkitpy.common.system.path import cygpath +from webkitpy.layout_tests.layout_package import test_expectations + import base import http_server @@ -84,11 +88,6 @@ class ChromiumPort(base.Port): def __init__(self, **kwargs): base.Port.__init__(self, **kwargs) - if 'options' in kwargs: - options = kwargs['options'] - if (options and (not hasattr(options, 'configuration') or - options.configuration is None)): - options.configuration = self.default_configuration() self._chromium_base_dir = None def baseline_path(self): @@ -100,9 +99,9 @@ class ChromiumPort(base.Port): dump_render_tree_binary_path = self._path_to_driver() result = check_file_exists(dump_render_tree_binary_path, 'test driver') and result - if result and self._options.build: + if result and self.get_option('build'): result = self._check_driver_build_up_to_date( - self._options.configuration) + self.get_option('configuration')) else: _log.error('') @@ -111,10 +110,14 @@ class ChromiumPort(base.Port): result = check_file_exists(helper_path, 'layout test helper') and result - if self._options.pixel_tests: + if self.get_option('pixel_tests'): result = self.check_image_diff( 'To override, invoke with --no-pixel-tests') and result + # It's okay if pretty patch isn't available, but we will at + # least log a message. + self.check_pretty_patch() + return result def check_sys_deps(self, needs_http): @@ -134,28 +137,43 @@ class ChromiumPort(base.Port): def diff_image(self, expected_contents, actual_contents, diff_filename=None, tolerance=0): executable = self._path_to_image_diff() - expected_tmpfile = tempfile.NamedTemporaryFile() - expected_tmpfile.write(expected_contents) - actual_tmpfile = tempfile.NamedTemporaryFile() - actual_tmpfile.write(actual_contents) + + tempdir = tempfile.mkdtemp() + expected_filename = os.path.join(tempdir, "expected.png") + with open(expected_filename, 'w+b') as file: + file.write(expected_contents) + actual_filename = os.path.join(tempdir, "actual.png") + with open(actual_filename, 'w+b') as file: + file.write(actual_contents) + if diff_filename: - cmd = [executable, '--diff', expected_tmpfile.name, - actual_tmpfile.name, diff_filename] + cmd = [executable, '--diff', expected_filename, + actual_filename, diff_filename] else: - cmd = [executable, expected_tmpfile.name, actual_tmpfile.name] + cmd = [executable, expected_filename, actual_filename] result = True try: - if self._executive.run_command(cmd, return_exit_code=True) == 0: - return False + exit_code = self._executive.run_command(cmd, return_exit_code=True) + if exit_code == 0: + # The images are the same. + result = False + elif exit_code != 1: + _log.error("image diff returned an exit code of " + + str(exit_code)) + # Returning False here causes the script to think that we + # successfully created the diff even though we didn't. If + # we return True, we think that the images match but the hashes + # don't match. + # FIXME: Figure out why image_diff returns other values. + result = False except OSError, e: if e.errno == errno.ENOENT or e.errno == errno.EACCES: _compare_available = False else: raise e finally: - expected_tmpfile.close() - actual_tmpfile.close() + shutil.rmtree(tempdir, ignore_errors=True) return result def driver_name(self): @@ -182,10 +200,11 @@ class ChromiumPort(base.Port): def results_directory(self): try: return self.path_from_chromium_base('webkit', - self._options.configuration, self._options.results_directory) + self.get_option('configuration'), + self.get_option('results_directory')) except AssertionError: - return self._build_path(self._options.configuration, - self._options.results_directory) + return self._build_path(self.get_option('configuration'), + self.get_option('results_directory')) def setup_test_run(self): # Delete the disk cache if any to ensure a clean test run. @@ -239,7 +258,7 @@ class ChromiumPort(base.Port): # FIXME: This drt_overrides handling should be removed when we switch # from tes_shell to DRT. drt_overrides = '' - if self._options and self._options.use_drt: + if self.get_option('use_drt'): drt_overrides_path = self.path_from_webkit_base('LayoutTests', 'platform', 'chromium', 'drt_expectations.txt') if os.path.exists(drt_overrides_path): @@ -325,11 +344,18 @@ class ChromiumPort(base.Port): platform = self.name() return self.path_from_webkit_base('LayoutTests', 'platform', platform) + def _convert_path(self, path): + """Handles filename conversion for subprocess command line args.""" + # See note above in diff_image() for why we need this. + if sys.platform == 'cygwin': + return cygpath(path) + return path + def _path_to_image_diff(self): binary_name = 'image_diff' - if self._options.use_drt: + if self.get_option('use_drt'): binary_name = 'ImageDiff' - return self._build_path(self._options.configuration, binary_name) + return self._build_path(self.get_option('configuration'), binary_name) class ChromiumDriver(base.Driver): @@ -344,29 +370,31 @@ class ChromiumDriver(base.Driver): def _driver_args(self): driver_args = [] if self._image_path: - driver_args.append("--pixel-tests=" + self._image_path) + # See note above in diff_image() for why we need _convert_path(). + driver_args.append("--pixel-tests=" + + self._port._convert_path(self._image_path)) - if self._options.use_drt: + if self._port.get_option('use_drt'): driver_args.append('--test-shell') else: driver_args.append('--layout-tests') - if self._options.startup_dialog: + if self._port.get_option('startup_dialog'): driver_args.append('--testshell-startup-dialog') - if self._options.gp_fault_error_box: + if self._port.get_option('gp_fault_error_box'): driver_args.append('--gp-fault-error-box') - if self._options.accelerated_compositing: + if self._port.get_option('accelerated_compositing'): driver_args.append('--enable-accelerated-compositing') - if self._options.accelerated_2d_canvas: + if self._port.get_option('accelerated_2d_canvas'): driver_args.append('--enable-accelerated-2d-canvas') return driver_args def start(self): # FIXME: Should be an error to call this method twice. - cmd = self._command_wrapper(self._options.wrapper) + cmd = self._command_wrapper(self._port.get_option('wrapper')) cmd.append(self._port._path_to_driver()) cmd += self._driver_args() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py index 80602d9..95c716e 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py @@ -66,7 +66,7 @@ def get(**kwargs): def _set_gpu_options(options): if options: if options.accelerated_compositing is None: - options.accelerated_composting = True + options.accelerated_compositing = True if options.accelerated_2d_canvas is None: options.accelerated_2d_canvas = True @@ -90,7 +90,8 @@ class ChromiumGpuLinuxPort(chromium_linux.ChromiumLinuxPort): chromium_linux.ChromiumLinuxPort.__init__(self, **kwargs) def baseline_search_path(self): - return ([self._webkit_baseline_path('chromium-gpu-linux')] + + # Mimic the Linux -> Win expectations fallback in the ordinary Chromium port. + return (map(self._webkit_baseline_path, ['chromium-gpu-linux', 'chromium-gpu-win', 'chromium-gpu']) + chromium_linux.ChromiumLinuxPort.baseline_search_path(self)) def path_to_test_expectations_file(self): @@ -108,7 +109,7 @@ class ChromiumGpuMacPort(chromium_mac.ChromiumMacPort): chromium_mac.ChromiumMacPort.__init__(self, **kwargs) def baseline_search_path(self): - return ([self._webkit_baseline_path('chromium-gpu-mac')] + + return (map(self._webkit_baseline_path, ['chromium-gpu-mac', 'chromium-gpu']) + chromium_mac.ChromiumMacPort.baseline_search_path(self)) def path_to_test_expectations_file(self): @@ -126,7 +127,7 @@ class ChromiumGpuWinPort(chromium_win.ChromiumWinPort): chromium_win.ChromiumWinPort.__init__(self, **kwargs) def baseline_search_path(self): - return ([self._webkit_baseline_path('chromium-gpu-win')] + + return (map(self._webkit_baseline_path, ['chromium-gpu-win', 'chromium-gpu']) + chromium_win.ChromiumWinPort.baseline_search_path(self)) def path_to_test_expectations_file(self): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py index 5c79a3f..7a13b1c 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py @@ -26,6 +26,8 @@ import os import unittest + +from webkitpy.tool import mocktool import chromium_gpu @@ -41,16 +43,25 @@ class ChromiumGpuTest(unittest.TestCase): def assertOverridesWorked(self, port_name): # test that we got the right port - port = chromium_gpu.get(port_name=port_name, options=None) + mock_options = mocktool.MockOptions(accelerated_compositing=None, + accelerated_2d_canvas=None) + port = chromium_gpu.get(port_name=port_name, options=mock_options) + self.assertTrue(port._options.accelerated_compositing) + self.assertTrue(port._options.accelerated_2d_canvas) # we use startswith() instead of Equal to gloss over platform versions. self.assertTrue(port.name().startswith(port_name)) - # test that it has the right directory in front of the search path. - path = port.baseline_search_path()[0] - self.assertEqual(port._webkit_baseline_path(port_name), path) + # test that it has the right directories in front of the search path. + paths = port.baseline_search_path() + self.assertEqual(port._webkit_baseline_path(port_name), paths[0]) + if port_name == 'chromium-gpu-linux': + self.assertEqual(port._webkit_baseline_path('chromium-gpu-win'), paths[1]) + self.assertEqual(port._webkit_baseline_path('chromium-gpu'), paths[2]) + else: + self.assertEqual(port._webkit_baseline_path('chromium-gpu'), paths[1]) - # test that we have the right expectations file. + # Test that we have the right expectations file. self.assertTrue('chromium-gpu' in port.path_to_test_expectations_file()) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py index 176991b..b26a6b5 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py @@ -52,7 +52,7 @@ class ChromiumLinuxPort(chromium.ChromiumPort): def check_build(self, needs_http): result = chromium.ChromiumPort.check_build(self, needs_http) if needs_http: - if self._options.use_apache: + if self.get_option('use_apache'): result = self._check_apache_install() and result else: result = self._check_lighttpd_install() and result @@ -81,7 +81,7 @@ class ChromiumLinuxPort(chromium.ChromiumPort): base = self.path_from_chromium_base() if os.path.exists(os.path.join(base, 'sconsbuild')): return os.path.join(base, 'sconsbuild', *comps) - if os.path.exists(os.path.join(base, 'out', *comps)) or not self._options.use_drt: + if os.path.exists(os.path.join(base, 'out', *comps)) or not self.get_option('use_drt'): return os.path.join(base, 'out', *comps) base = self.path_from_webkit_base() if os.path.exists(os.path.join(base, 'sconsbuild')): @@ -147,9 +147,9 @@ class ChromiumLinuxPort(chromium.ChromiumPort): def _path_to_driver(self, configuration=None): if not configuration: - configuration = self._options.configuration + configuration = self.get_option('configuration') binary_name = 'test_shell' - if self._options.use_drt: + if self.get_option('use_drt'): binary_name = 'DumpRenderTree' return self._build_path(configuration, binary_name) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py index 64016ab..d1c383c 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py @@ -73,7 +73,7 @@ class ChromiumMacPort(chromium.ChromiumPort): def driver_name(self): """name for this port's equivalent of DumpRenderTree.""" - if self._options.use_drt: + if self.get_option('use_drt'): return "DumpRenderTree" return "TestShell" @@ -100,7 +100,7 @@ class ChromiumMacPort(chromium.ChromiumPort): def _build_path(self, *comps): path = self.path_from_chromium_base('xcodebuild', *comps) - if os.path.exists(path) or not self._options.use_drt: + if os.path.exists(path) or not self.get_option('use_drt'): return path return self.path_from_webkit_base('WebKit', 'chromium', 'xcodebuild', *comps) @@ -138,15 +138,15 @@ class ChromiumMacPort(chromium.ChromiumPort): # FIXME: make |configuration| happy with case-sensitive file # systems. if not configuration: - configuration = self._options.configuration + configuration = self.get_option('configuration') return self._build_path(configuration, self.driver_name() + '.app', 'Contents', 'MacOS', self.driver_name()) def _path_to_helper(self): binary_name = 'layout_test_helper' - if self._options.use_drt: + if self.get_option('use_drt'): binary_name = 'LayoutTestHelper' - return self._build_path(self._options.configuration, binary_name) + return self._build_path(self.get_option('configuration'), binary_name) def _path_to_wdiff(self): return 'wdiff' diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py index a4a9ea6..cb45430 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py @@ -26,24 +26,22 @@ # (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 chromium -import chromium_linux -import chromium_mac -import chromium_win +import os import unittest import StringIO -import os +from webkitpy.tool import mocktool from webkitpy.thirdparty.mock import Mock +import chromium +import chromium_linux +import chromium_mac +import chromium_win class ChromiumDriverTest(unittest.TestCase): def setUp(self): mock_port = Mock() - # FIXME: The Driver should not be grabbing at port._options! - mock_port._options = Mock() - mock_port._options.wrapper = "" self.driver = chromium.ChromiumDriver(mock_port, image_path=None, options=None) def test_test_shell_command(self): @@ -106,25 +104,19 @@ class ChromiumPortTest(unittest.TestCase): return 'default' def test_path_to_image_diff(self): - class MockOptions: - def __init__(self): - self.use_drt = True - - port = ChromiumPortTest.TestLinuxPort(options=MockOptions()) + mock_options = mocktool.MockOptions(use_drt=True) + port = ChromiumPortTest.TestLinuxPort(options=mock_options) self.assertTrue(port._path_to_image_diff().endswith( '/out/default/ImageDiff'), msg=port._path_to_image_diff()) - port = ChromiumPortTest.TestMacPort(options=MockOptions()) + port = ChromiumPortTest.TestMacPort(options=mock_options) self.assertTrue(port._path_to_image_diff().endswith( '/xcodebuild/default/ImageDiff')) # FIXME: Figure out how this is going to work on Windows. #port = chromium_win.ChromiumWinPort('test-port', options=MockOptions()) def test_skipped_layout_tests(self): - class MockOptions: - def __init__(self): - self.use_drt = True - - port = ChromiumPortTest.TestLinuxPort(options=MockOptions()) + mock_options = mocktool.MockOptions(use_drt=True) + port = ChromiumPortTest.TestLinuxPort(options=mock_options) fake_test = os.path.join(port.layout_tests_dir(), "fast/js/not-good.js") @@ -138,23 +130,56 @@ DEFER LINUX WIN : fast/js/very-good.js = TIMEOUT PASS""" self.assertTrue("fast/js/not-good.js" in skipped_tests) def test_default_configuration(self): - class EmptyOptions: - def __init__(self): - pass - - options = EmptyOptions() - port = ChromiumPortTest.TestLinuxPort(options) - self.assertEquals(options.configuration, 'default') + mock_options = mocktool.MockOptions() + port = ChromiumPortTest.TestLinuxPort(options=mock_options) + self.assertEquals(mock_options.configuration, 'default') self.assertTrue(port.default_configuration_called) - class OptionsWithUnsetConfiguration: - def __init__(self): - self.configuration = None - - options = OptionsWithUnsetConfiguration() - port = ChromiumPortTest.TestLinuxPort(options) - self.assertEquals(options.configuration, 'default') + mock_options = mocktool.MockOptions(configuration=None) + port = ChromiumPortTest.TestLinuxPort(mock_options) + self.assertEquals(mock_options.configuration, 'default') self.assertTrue(port.default_configuration_called) + def test_diff_image(self): + class TestPort(ChromiumPortTest.TestLinuxPort): + def _path_to_image_diff(self): + return "/path/to/image_diff" + + class MockExecute: + def __init__(self, result): + self._result = result + + def run_command(self, + args, + cwd=None, + input=None, + error_handler=None, + return_exit_code=False, + return_stderr=True, + decode_output=False): + if return_exit_code: + return self._result + return '' + + mock_options = mocktool.MockOptions(use_drt=False) + port = ChromiumPortTest.TestLinuxPort(mock_options) + + # Images are different. + port._executive = MockExecute(0) + self.assertEquals(False, port.diff_image("EXPECTED", "ACTUAL")) + + # Images are the same. + port._executive = MockExecute(1) + self.assertEquals(True, port.diff_image("EXPECTED", "ACTUAL")) + + # There was some error running image_diff. + port._executive = MockExecute(2) + exception_raised = False + try: + port.diff_image("EXPECTED", "ACTUAL") + except ValueError, e: + exception_raised = True + self.assertFalse(exception_raised) + if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py index d2b0265..69b529a 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py @@ -55,9 +55,7 @@ class ChromiumWinPort(chromium.ChromiumPort): # python executable to run cgi program. env["CYGWIN_PATH"] = self.path_from_chromium_base( "third_party", "cygwin", "bin") - if (sys.platform == "win32" and self._options and - hasattr(self._options, "register_cygwin") and - self._options.register_cygwin): + if (sys.platform == "win32" and self.get_option('register_cygwin')): setup_mount = self.path_from_chromium_base("third_party", "cygwin", "setup_mount.bat") @@ -84,11 +82,6 @@ class ChromiumWinPort(chromium.ChromiumPort): 'build-instructions-windows') return result - 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 relative_test_filename(self, filename): path = filename[len(self.layout_tests_dir()) + 1:] return path.replace('\\', '/') @@ -118,7 +111,7 @@ class ChromiumWinPort(chromium.ChromiumPort): if os.path.exists(p): return p p = self.path_from_chromium_base('chrome', *comps) - if os.path.exists(p) or not self._options.use_drt: + if os.path.exists(p) or not self.get_option('use_drt'): return p return os.path.join(self.path_from_webkit_base(), 'WebKit', 'chromium', *comps) @@ -146,23 +139,23 @@ class ChromiumWinPort(chromium.ChromiumPort): def _path_to_driver(self, configuration=None): if not configuration: - configuration = self._options.configuration + configuration = self.get_option('configuration') binary_name = 'test_shell.exe' - if self._options.use_drt: + if self.get_option('use_drt'): binary_name = 'DumpRenderTree.exe' return self._build_path(configuration, binary_name) def _path_to_helper(self): binary_name = 'layout_test_helper.exe' - if self._options.use_drt: + if self.get_option('use_drt'): binary_name = 'LayoutTestHelper.exe' - return self._build_path(self._options.configuration, binary_name) + return self._build_path(self.get_option('configuration'), binary_name) def _path_to_image_diff(self): binary_name = 'image_diff.exe' - if self._options.use_drt: + if self.get_option('use_drt'): binary_name = 'ImageDiff.exe' - return self._build_path(self._options.configuration, binary_name) + return self._build_path(self.get_option('configuration'), binary_name) def _path_to_wdiff(self): return self.path_from_chromium_base('third_party', 'cygwin', 'bin', diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py index 648ccad..8a6af56 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/dryrun.py @@ -101,7 +101,6 @@ class DryrunDriver(base.Driver): def __init__(self, port, image_path, options, executive): self._port = port - self._options = options self._image_path = image_path self._executive = executive self._layout_tests_dir = None diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py index 81c3732..978a557 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory_unittest.py @@ -29,6 +29,8 @@ import sys import unittest +from webkitpy.tool import mocktool + import chromium_gpu import chromium_linux import chromium_mac @@ -52,21 +54,11 @@ class FactoryTest(unittest.TestCase): # FIXME: The ports themselves should expose what options they require, # instead of passing generic "options". - class WebKitOptions(object): - """Represents the minimum options for WebKit port.""" - def __init__(self): - self.pixel_tests = False - - class ChromiumOptions(WebKitOptions): - """Represents minimum options for Chromium port.""" - def __init__(self): - FactoryTest.WebKitOptions.__init__(self) - self.chromium = True - def setUp(self): self.real_sys_platform = sys.platform - self.webkit_options = FactoryTest.WebKitOptions() - self.chromium_options = FactoryTest.ChromiumOptions() + self.webkit_options = mocktool.MockOptions(pixel_tests=False) + self.chromium_options = mocktool.MockOptions(pixel_tests=False, + chromium=True) def tearDown(self): sys.platform = self.real_sys_platform diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py index bffc860..8d94bb5 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py @@ -24,6 +24,30 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from __future__ import with_statement + +import codecs +import os + + +def _test_expectations_overrides(port, super): + # The chrome ports use the regular overrides plus anything in the + # official test_expectations as well. Hopefully we don't get collisions. + chromium_overrides = super.test_expectations_overrides(port) + + # FIXME: It used to be that AssertionError would get raised by + # path_from_chromium_base() if we weren't in a Chromium checkout, but + # this changed in r60427. This should probably be changed back. + overrides_path = port.path_from_chromium_base('webkit', 'tools', + 'layout_tests', 'test_expectations_chrome.txt') + if not os.path.exists(overrides_path): + return chromium_overrides + + with codecs.open(overrides_path, "r", "utf-8") as file: + if chromium_overrides: + return chromium_overrides + file.read() + else: + return file.read() def GetGoogleChromePort(**kwargs): """Some tests have slightly different results when compiled as Google @@ -41,6 +65,11 @@ def GetGoogleChromePort(**kwargs): paths.insert(0, self._webkit_baseline_path( 'google-chrome-linux32')) return paths + + def test_expectations_overrides(self): + return _test_expectations_overrides(self, + chromium_linux.ChromiumLinuxPort) + return GoogleChromeLinux32Port(**kwargs) elif port_name == 'google-chrome-linux64': import chromium_linux @@ -52,6 +81,11 @@ def GetGoogleChromePort(**kwargs): paths.insert(0, self._webkit_baseline_path( 'google-chrome-linux64')) return paths + + def test_expectations_overrides(self): + return _test_expectations_overrides(self, + chromium_linux.ChromiumLinuxPort) + return GoogleChromeLinux64Port(**kwargs) elif port_name.startswith('google-chrome-mac'): import chromium_mac @@ -63,6 +97,11 @@ def GetGoogleChromePort(**kwargs): paths.insert(0, self._webkit_baseline_path( 'google-chrome-mac')) return paths + + def test_expectations_overrides(self): + return _test_expectations_overrides(self, + chromium_mac.ChromiumMacPort) + return GoogleChromeMacPort(**kwargs) elif port_name.startswith('google-chrome-win'): import chromium_win @@ -74,5 +113,10 @@ def GetGoogleChromePort(**kwargs): paths.insert(0, self._webkit_baseline_path( 'google-chrome-win')) return paths + + def test_expectations_overrides(self): + return _test_expectations_overrides(self, + chromium_win.ChromiumWinPort) + return GoogleChromeWinPort(**kwargs) raise NotImplementedError('unsupported port: %s' % port_name) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py index 85e9338..c4c885d 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py @@ -24,8 +24,12 @@ # (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 codecs import os import unittest + +import base_unittest +import factory import google_chrome @@ -35,6 +39,7 @@ class GetGoogleChromePortTest(unittest.TestCase): 'google-chrome-mac', 'google-chrome-win') for port in test_ports: self._verify_baseline_path(port, port) + self._verify_expectations_overrides(port) self._verify_baseline_path('google-chrome-mac', 'google-chrome-mac-leopard') self._verify_baseline_path('google-chrome-win', 'google-chrome-win-xp') @@ -45,3 +50,53 @@ class GetGoogleChromePortTest(unittest.TestCase): options=None) path = port.baseline_search_path()[0] self.assertEqual(expected_path, os.path.split(path)[1]) + + def _verify_expectations_overrides(self, port_name): + # FIXME: make this more robust when we have the Tree() abstraction. + # we should be able to test for the files existing or not, and + # be able to control the contents better. + + chromium_port = factory.get("chromium-mac") + chromium_overrides = chromium_port.test_expectations_overrides() + port = google_chrome.GetGoogleChromePort(port_name=port_name, + options=None) + + orig_exists = os.path.exists + orig_open = codecs.open + expected_string = "// hello, world\n" + + def mock_exists_chrome_not_found(path): + if 'test_expectations_chrome.txt' in path: + return False + return orig_exists(path) + + def mock_exists_chrome_found(path): + if 'test_expectations_chrome.txt' in path: + return True + return orig_exists(path) + + def mock_open(path, mode, encoding): + if 'test_expectations_chrome.txt' in path: + return base_unittest.NewStringIO(expected_string) + return orig_open(path, mode, encoding) + + try: + os.path.exists = mock_exists_chrome_not_found + chrome_overrides = port.test_expectations_overrides() + self.assertEqual(chromium_overrides, chrome_overrides) + + os.path.exists = mock_exists_chrome_found + codecs.open = mock_open + chrome_overrides = port.test_expectations_overrides() + if chromium_overrides: + self.assertEqual(chrome_overrides, + chromium_overrides + expected_string) + else: + self.assertEqual(chrome_overrides, expected_string) + finally: + os.path.exists = orig_exists + codecs.open = orig_open + + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock.py new file mode 100644 index 0000000..73200a0 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# Copyright (C) 2010 Gabor Rapcsanyi (rgabor@inf.u-szeged.hu), University of Szeged +# Copyright (C) 2010 Andras Becsi (abecsi@inf.u-szeged.hu), University of Szeged +# +# 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. +# +# THIS SOFTWARE IS PROVIDED BY UNIVERSITY OF SZEGED ``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 UNIVERSITY OF SZEGED 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 class helps to block NRWT threads when more NRWTs run +http and websocket tests in a same time.""" + +import glob +import os +import sys +import tempfile +import time + + +class HttpLock(object): + + def __init__(self, lock_path, lock_file_prefix="WebKitHttpd.lock.", + guard_lock="WebKit.lock"): + if not lock_path: + self._lock_path = tempfile.gettempdir() + self._lock_file_prefix = lock_file_prefix + self._lock_file_path_prefix = os.path.join(self._lock_path, + self._lock_file_prefix) + self._guard_lock_file = os.path.join(self._lock_path, guard_lock) + self._process_lock_file_name = "" + + def cleanup_http_lock(self): + """Delete the lock file if exists.""" + if os.path.exists(self._process_lock_file_name): + os.unlink(self._process_lock_file_name) + + def _extract_lock_number(self, lock_file_name): + """Return the lock number from lock file.""" + prefix_length = len(self._lock_file_path_prefix) + return int(lock_file_name[prefix_length:]) + + def _lock_file_list(self): + """Return the list of lock files sequentially.""" + lock_list = glob.glob(self._lock_file_path_prefix + '*') + lock_list.sort(key=self._extract_lock_number) + return lock_list + + def _next_lock_number(self): + """Return the next available lock number.""" + lock_list = self._lock_file_list() + if not lock_list: + return 0 + return self._extract_lock_number(lock_list[-1]) + 1 + + def _check_pid(self, current_pid): + """Return True if pid is alive, otherwise return False.""" + try: + os.kill(current_pid, 0) + except OSError: + return False + else: + return True + + def _curent_lock_pid(self): + """Return with the current lock pid. If the lock is not valid + it deletes the lock file.""" + lock_list = self._lock_file_list() + if not lock_list: + return + try: + current_lock_file = open(lock_list[0], 'r') + current_pid = current_lock_file.readline() + current_lock_file.close() + if not (current_pid and + sys.platform in ('darwin', 'linux2') and + self._check_pid(int(current_pid))): + os.unlink(lock_list[0]) + return + except IOError, OSError: + return + return int(current_pid) + + def _create_lock_file(self): + """The lock files are used to schedule the running test sessions in first + come first served order. The sequential guard lock ensures that the lock + numbers are sequential.""" + while(True): + try: + sequential_guard_lock = os.open(self._guard_lock_file, + os.O_CREAT | os.O_NONBLOCK | os.O_EXCL) + + self._process_lock_file_name = (self._lock_file_path_prefix + + str(self._next_lock_number())) + lock_file = open(self._process_lock_file_name, 'w') + lock_file.write(str(os.getpid())) + lock_file.close() + os.close(sequential_guard_lock) + os.unlink(self._guard_lock_file) + break + except OSError: + pass + + def wait_for_httpd_lock(self): + """Create a lock file and wait until it's turn comes.""" + self._create_lock_file() + while self._curent_lock_pid() != os.getpid(): + time.sleep(1) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock_unittest.py new file mode 100644 index 0000000..85c760a --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock_unittest.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# Copyright (C) 2010 Gabor Rapcsanyi (rgabor@inf.u-szeged.hu), University of Szeged +# +# 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. +# +# THIS SOFTWARE IS PROVIDED BY UNIVERSITY OF SZEGED ``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 UNIVERSITY OF SZEGED 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 glob +import http_lock +import os +import unittest + + +class HttpLockTest(unittest.TestCase): + + def __init__(self, testFunc): + self.http_lock_obj = http_lock.HttpLock(None, "WebKitTestHttpd.lock.", "WebKitTest.lock") + self.lock_file_path_prefix = os.path.join(self.http_lock_obj._lock_path, + self.http_lock_obj._lock_file_prefix) + self.lock_file_name = self.lock_file_path_prefix + "0" + self.guard_lock_file = self.http_lock_obj._guard_lock_file + self.clean_all_lockfile() + unittest.TestCase.__init__(self, testFunc) + + def clean_all_lockfile(self): + if os.path.exists(self.guard_lock_file): + os.unlink(self.guard_lock_file) + lock_list = glob.glob(self.lock_file_path_prefix + '*') + for file_name in lock_list: + os.unlink(file_name) + + def assertEqual(self, first, second): + if first != second: + self.clean_all_lockfile() + unittest.TestCase.assertEqual(self, first, second) + + def _check_lock_file(self): + if os.path.exists(self.lock_file_name): + pid = os.getpid() + lock_file = open(self.lock_file_name, 'r') + lock_file_pid = lock_file.readline() + lock_file.close() + self.assertEqual(pid, int(lock_file_pid)) + return True + return False + + def test_lock_lifecycle(self): + self.http_lock_obj._create_lock_file() + + self.assertEqual(True, self._check_lock_file()) + self.assertEqual(1, self.http_lock_obj._next_lock_number()) + + self.http_lock_obj.cleanup_http_lock() + + self.assertEqual(False, self._check_lock_file()) + self.assertEqual(0, self.http_lock_obj._next_lock_number()) + + def test_extract_lock_number(self,): + lock_file_list = ( + self.lock_file_path_prefix + "00", + self.lock_file_path_prefix + "9", + self.lock_file_path_prefix + "001", + self.lock_file_path_prefix + "021", + ) + + expected_number_list = (0, 9, 1, 21) + + for lock_file, expected in zip(lock_file_list, expected_number_list): + self.assertEqual(self.http_lock_obj._extract_lock_number(lock_file), expected) + + def test_lock_file_list(self): + lock_file_list = [ + self.lock_file_path_prefix + "6", + self.lock_file_path_prefix + "1", + self.lock_file_path_prefix + "4", + self.lock_file_path_prefix + "3", + ] + + expected_file_list = [ + self.lock_file_path_prefix + "1", + self.lock_file_path_prefix + "3", + self.lock_file_path_prefix + "4", + self.lock_file_path_prefix + "6", + ] + + for file_name in lock_file_list: + open(file_name, 'w') + + self.assertEqual(self.http_lock_obj._lock_file_list(), expected_file_list) + + for file_name in lock_file_list: + os.unlink(file_name) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/mac_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/mac_unittest.py index 327b19e..d383a4c 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/mac_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/mac_unittest.py @@ -35,7 +35,7 @@ import port_testcase class MacTest(port_testcase.PortTestCase): - def make_port(self, options=port_testcase.MockOptions()): + def make_port(self, options=port_testcase.mock_options): if sys.platform != 'darwin': return None port_obj = mac.MacPort(options=options) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py index 47597d6..04ada50 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/port_testcase.py @@ -32,20 +32,15 @@ import os import tempfile import unittest - -class MockOptions(object): - def __init__(self, - results_directory='layout-test-results', - use_apache=True, - configuration='Release'): - self.results_directory = results_directory - self.use_apache = use_apache - self.configuration = configuration +from webkitpy.tool import mocktool +mock_options = mocktool.MockOptions(results_directory='layout-test-results', + use_apache=True, + configuration='Release') class PortTestCase(unittest.TestCase): """Tests the WebKit port implementation.""" - def make_port(self, options=MockOptions()): + def make_port(self, options=mock_options): """Override in subclass.""" raise NotImplementedError() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py index 3b81167..3691c5a 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py @@ -215,14 +215,11 @@ class TestPort(base.Port): def name(self): return self._name - def options(self): - return self._options - def _path_to_wdiff(self): return None def results_directory(self): - return '/tmp/' + self._options.results_directory + return '/tmp/' + self.get_option('results_directory') def setup_test_run(self): pass @@ -285,7 +282,6 @@ class TestDriver(base.Driver): def __init__(self, port, image_path, options, executive): self._port = port self._image_path = image_path - self._options = options self._executive = executive self._image_written = False @@ -302,7 +298,7 @@ class TestDriver(base.Driver): if test.hang: time.sleep((float(timeoutms) * 4) / 1000.0) - if self._port.options().pixel_tests and test.actual_image: + if self._port.get_option('pixel_tests') and test.actual_image: with open(self._image_path, 'w') as file: file.write(test.actual_image) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py index ed19c09..c940f1e 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py @@ -65,9 +65,7 @@ class WebKitPort(base.Port): # FIXME: disable pixel tests until they are run by default on the # build machines. - if self._options and (not hasattr(self._options, "pixel_tests") or - self._options.pixel_tests is None): - self._options.pixel_tests = False + self.set_option_default('pixel_tests', False) def baseline_path(self): return self._webkit_baseline_path(self._name) @@ -86,7 +84,7 @@ class WebKitPort(base.Port): def _build_driver(self): exit_code = self._executive.run_command([ self.script_path("build-dumprendertree"), - self.flag_from_configuration(self._options.configuration), + self.flag_from_configuration(self.get_option('configuration')), ], return_exit_code=True) if exit_code != 0: _log.error("Failed to build DumpRenderTree") @@ -101,11 +99,11 @@ class WebKitPort(base.Port): return True def check_build(self, needs_http): - if self._options.build and not self._build_driver(): + if self.get_option('build') and not self._build_driver(): return False if not self._check_driver(): return False - if self._options.pixel_tests: + if self.get_option('pixel_tests'): if not self.check_image_diff(): return False if not self._check_port_build(): @@ -184,7 +182,7 @@ class WebKitPort(base.Port): def results_directory(self): # Results are store relative to the built products to make it easy # to have multiple copies of webkit checked out and built. - return self._build_path(self._options.results_directory) + return self._build_path(self.get_option('results_directory')) def setup_test_run(self): # This port doesn't require any specific configuration. @@ -360,7 +358,7 @@ class WebKitPort(base.Port): if not self._cached_build_root: self._cached_build_root = self._webkit_build_directory([ "--configuration", - self.flag_from_configuration(self._options.configuration), + self.flag_from_configuration(self.get_option('configuration')), ]) return os.path.join(self._cached_build_root, *comps) @@ -401,7 +399,6 @@ class WebKitDriver(base.Driver): def __init__(self, port, image_path, options, executive=Executive()): self._port = port self._image_path = image_path - self._options = options self._executive = executive self._driver_tempdir = tempfile.mkdtemp(prefix='DumpRenderTree-') @@ -414,17 +411,17 @@ class WebKitDriver(base.Driver): if self._image_path: driver_args.append('--pixel-tests') - if self._options.use_drt: - if self._options.accelerated_compositing: + if self._port.get_option('use_drt'): + if self._port.get_option('accelerated_compositing'): driver_args.append('--enable-accelerated-compositing') - if self._options.accelerated_2d_canvas: + if self._port.get_option('accelerated_2d_canvas'): driver_args.append('--enable-accelerated-2d-canvas') return driver_args def start(self): - command = self._command_wrapper(self._options.wrapper) + command = self._command_wrapper(self._port.get_option('wrapper')) command += [self._port._path_to_driver(), '-'] command += self._driver_args() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py index 7346671..926bc04 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py @@ -124,12 +124,13 @@ class PyWebSocket(http_server.Lighttpd): if self._root: self._layout_tests = os.path.abspath(self._root) self._web_socket_tests = os.path.abspath( - os.path.join(self._root, 'websocket', 'tests')) + os.path.join(self._root, 'http', 'tests', + 'websocket', 'tests')) else: try: self._layout_tests = self._port_obj.layout_tests_dir() self._web_socket_tests = os.path.join(self._layout_tests, - 'websocket', 'tests') + 'http', 'tests', 'websocket', 'tests') except: self._web_socket_tests = None @@ -164,10 +165,10 @@ class PyWebSocket(http_server.Lighttpd): pywebsocket_script = os.path.join(pywebsocket_base, 'mod_pywebsocket', 'standalone.py') start_cmd = [ - python_interp, pywebsocket_script, + python_interp, '-u', pywebsocket_script, '--server-host', '127.0.0.1', '--port', str(self._port), - '--document-root', self._layout_tests, + '--document-root', os.path.join(self._layout_tests, 'http', 'tests'), '--scan-dir', self._web_socket_tests, '--cgi-paths', '/websocket/tests', '--log-file', error_log, 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 e57ceb2..a47370d 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py @@ -57,8 +57,9 @@ import time import urllib import zipfile +from webkitpy.common.system import path from webkitpy.common.system import user -from webkitpy.common.system.executive import run_command, ScriptError +from webkitpy.common.system.executive import Executive, ScriptError import webkitpy.common.checkout.scm as scm import port @@ -81,58 +82,6 @@ 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. - - Args: - command: program and arguments. - print_output: if true, print the command results to standard output. - - Returns: - command output, return code - """ - - # Use a shell for subcommands on Windows to get a PATH search. - # FIXME: shell=True is a trail of tears, and should be removed. - use_shell = sys.platform.startswith('win') - # Note: Not thread safe: http://bugs.python.org/issue2320 - p = subprocess.Popen(command, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, shell=use_shell) - if print_output: - output_array = [] - while True: - line = p.stdout.readline() - if not line: - break - if print_output: - print line.strip('\n') - output_array.append(line) - output = ''.join(output_array) - else: - output = p.stdout.read() - p.wait() - p.stdout.close() - - 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. - - Args: - command: program and arguments. - print_output: if true, print the command results to standard output. - - Returns: - command output - """ - - output, return_code = run_shell_with_return_code(command, print_output) - return output - - def log_dashed_string(text, platform, logging_level=logging.INFO): """Log text message with dashes on both sides.""" @@ -547,7 +496,7 @@ class Rebaseliner(object): """ if is_image: - return self._port.diff_image(output1, output2) + return self._port.diff_image(output1, output2, None, 0) else: return self._port.compare_text(output1, output2) @@ -628,7 +577,6 @@ class Rebaseliner(object): base_file = get_result_file_fullpath(self._options.html_directory, baseline_filename, self._platform, 'old') - # FIXME: This assumes run_shell returns a byte array. # We should be using an explicit encoding here. with open(base_file, "wb") as file: file.write(output) @@ -642,12 +590,12 @@ class Rebaseliner(object): diff_file = get_result_file_fullpath( self._options.html_directory, baseline_filename, self._platform, 'diff') - # FIXME: This assumes run_shell returns a byte array, not unicode() with open(diff_file, 'wb') as file: file.write(output) _log.info(' Html: created baseline diff file: "%s".', diff_file) + class HtmlGenerator(object): """Class to generate rebaselining result comparison html.""" @@ -694,14 +642,20 @@ class HtmlGenerator(object): '<img style="width: 200" src="%(uri)s" /></a></td>') HTML_TR = '<tr>%s</tr>' - def __init__(self, target_port, options, platforms, rebaselining_tests): + def __init__(self, target_port, options, platforms, rebaselining_tests, + executive): self._html_directory = options.html_directory self._target_port = target_port self._platforms = platforms self._rebaselining_tests = rebaselining_tests + self._executive = executive self._html_file = os.path.join(options.html_directory, 'rebaseline.html') + def abspath_to_uri(self, filename): + """Converts an absolute path to a file: URI.""" + return path.abspath_to_uri(filename, self._executive) + def generate_html(self): """Generate html file for rebaselining result comparison.""" @@ -769,14 +723,13 @@ class HtmlGenerator(object): links = '' if os.path.exists(old_file): links += html_td_link % { - 'uri': self._target_port.filename_to_uri(old_file), + 'uri': self.abspath_to_uri(old_file), 'name': baseline_filename} else: _log.info(' No old baseline file: "%s"', old_file) links += self.HTML_TD_NOLINK % '' - links += html_td_link % {'uri': self._target_port.filename_to_uri( - new_file), + links += html_td_link % {'uri': self.abspath_to_uri(new_file), 'name': baseline_filename} diff_file = get_result_file_fullpath(self._html_directory, @@ -784,8 +737,8 @@ class HtmlGenerator(object): 'diff') _log.info(' Baseline diff file: "%s"', diff_file) if os.path.exists(diff_file): - links += html_td_link % {'uri': self._target_port.filename_to_uri( - diff_file), 'name': 'Diff'} + links += html_td_link % {'uri': self.abspath_to_uri(diff_file), + 'name': 'Diff'} else: _log.info(' No baseline diff file: "%s"', diff_file) links += self.HTML_TD_NOLINK % '' @@ -825,8 +778,7 @@ class HtmlGenerator(object): if rows: test_path = os.path.join(self._target_port.layout_tests_dir(), test) - html = self.HTML_TR_TEST % ( - self._target_port.filename_to_uri(test_path), test) + html = self.HTML_TR_TEST % (self.abspath_to_uri(test_path), test) html += self.HTML_TEST_DETAIL % ' '.join(rows) _log.debug(' html for test: %s', html) @@ -867,7 +819,7 @@ def get_host_port_object(options): return port_obj -def main(): +def main(executive=Executive()): """Main function to produce new baselines.""" option_parser = optparse.OptionParser() @@ -992,7 +944,8 @@ def main(): html_generator = HtmlGenerator(target_port_obj, options, rebaseline_platforms, - rebaselining_tests) + rebaselining_tests, + executive=executive) html_generator.generate_html() if not options.quiet: html_generator.show_html() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py index 9ba3d6b..ef33a47 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py @@ -30,10 +30,13 @@ """Unit tests for rebaseline_chromium_webkit_tests.py.""" import os +import sys import unittest +from webkitpy.tool import mocktool from webkitpy.layout_tests import port from webkitpy.layout_tests import rebaseline_chromium_webkit_tests +from webkitpy.common.system.executive import Executive, ScriptError class MockPort(object): @@ -44,11 +47,6 @@ class MockPort(object): return self.image_diff_exists -class MockOptions(object): - def __init__(self): - self.configuration = None - - def get_mock_get(config_expectations): def mock_get(port_name, options): return MockPort(config_expectations[options.configuration]) @@ -61,7 +59,8 @@ class TestGetHostPortObject(unittest.TestCase): # that Image diff is (or isn't) present in the two configs. port.get = get_mock_get({'Release': release_present, 'Debug': debug_present}) - options = MockOptions() + options = mocktool.MockOptions(configuration=None, + html_directory=None) port_obj = rebaseline_chromium_webkit_tests.get_host_port_object( options) if valid_port_obj: @@ -87,7 +86,8 @@ class TestGetHostPortObject(unittest.TestCase): class TestRebaseliner(unittest.TestCase): def make_rebaseliner(self): - options = MockOptions() + options = mocktool.MockOptions(configuration=None, + html_directory=None) host_port_obj = port.get('test', options) target_options = options target_port_obj = port.get('test', target_options) @@ -118,5 +118,32 @@ class TestRebaseliner(unittest.TestCase): self.assertFalse(rebaseliner._diff_baselines(image, image, is_image=True)) + +class TestHtmlGenerator(unittest.TestCase): + def make_generator(self, tests): + return rebaseline_chromium_webkit_tests.HtmlGenerator( + target_port=None, + options=mocktool.MockOptions(configuration=None, + html_directory='/tmp'), + platforms=['mac'], + rebaselining_tests=tests, + executive=Executive()) + + def test_generate_baseline_links(self): + orig_platform = sys.platform + orig_exists = os.path.exists + + try: + sys.platform = 'darwin' + os.path.exists = lambda x: True + generator = self.make_generator(["foo.txt"]) + links = generator._generate_baseline_links("foo", ".txt", "mac") + expected_links = '<td align=center><a href="file:///tmp/foo-expected-mac-old.txt">foo-expected.txt</a></td><td align=center><a href="file:///tmp/foo-expected-mac-new.txt">foo-expected.txt</a></td><td align=center><a href="file:///tmp/foo-expected-mac-diff.txt">Diff</a></td>' + self.assertEqual(links, expected_links) + finally: + sys.platform = orig_platform + os.path.exists = orig_exists + + if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py index e9c6d2c..9cc7895 100755 --- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # Copyright (C) 2010 Google Inc. All rights reserved. +# Copyright (C) 2010 Gabor Rapcsanyi (rgabor@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 @@ -498,6 +499,12 @@ class TestRunner: self._options.slow_time_out_ms) return TestInfo(self._port, test_file, self._options.time_out_ms) + def _test_requires_lock(self, test_file): + """Return True if the test needs to be locked when + running multiple copies of NRWTs.""" + split_path = test_file.split(os.sep) + return 'http' in split_path or 'websocket' in split_path + def _get_test_file_queue(self, test_files): """Create the thread safe queue of lists of (test filenames, test URIs) tuples. Each TestShellThread pulls a list from this queue and runs @@ -511,46 +518,47 @@ class TestRunner: The Queue of lists of TestInfo objects. """ + test_lists = [] + tests_to_http_lock = [] if (self._options.experimental_fully_parallel or self._is_single_threaded()): - filename_queue = Queue.Queue() for test_file in test_files: - filename_queue.put( - ('.', [self._get_test_info_for_file(test_file)])) - return filename_queue - - tests_by_dir = {} - for test_file in test_files: - directory = self._get_dir_for_test_file(test_file) - tests_by_dir.setdefault(directory, []) - tests_by_dir[directory].append( - self._get_test_info_for_file(test_file)) - - # Sort by the number of tests in the dir so that the ones with the - # most tests get run first in order to maximize parallelization. - # Number of tests is a good enough, but not perfect, approximation - # of how long that set of tests will take to run. We can't just use - # a PriorityQueue until we move # to Python 2.6. - test_lists = [] - http_tests = None - for directory in tests_by_dir: - test_list = tests_by_dir[directory] - # Keep the tests in alphabetical order. - # TODO: Remove once tests are fixed so they can be run in any - # order. - test_list.reverse() - test_list_tuple = (directory, test_list) - if directory == 'LayoutTests' + os.sep + 'http': - http_tests = test_list_tuple - else: + test_info = self._get_test_info_for_file(test_file) + if self._test_requires_lock(test_file): + tests_to_http_lock.append(test_info) + else: + test_lists.append((".", [test_info])) + else: + tests_by_dir = {} + for test_file in test_files: + directory = self._get_dir_for_test_file(test_file) + test_info = self._get_test_info_for_file(test_file) + if self._test_requires_lock(test_file): + tests_to_http_lock.append(test_info) + else: + tests_by_dir.setdefault(directory, []) + tests_by_dir[directory].append(test_info) + # Sort by the number of tests in the dir so that the ones with the + # most tests get run first in order to maximize parallelization. + # Number of tests is a good enough, but not perfect, approximation + # of how long that set of tests will take to run. We can't just use + # a PriorityQueue until we move to Python 2.6. + for directory in tests_by_dir: + test_list = tests_by_dir[directory] + # Keep the tests in alphabetical order. + # FIXME: Remove once tests are fixed so they can be run in any + # order. + test_list.reverse() + test_list_tuple = (directory, test_list) test_lists.append(test_list_tuple) - test_lists.sort(lambda a, b: cmp(len(b[1]), len(a[1]))) + test_lists.sort(lambda a, b: cmp(len(b[1]), len(a[1]))) # Put the http tests first. There are only a couple hundred of them, # but each http test takes a very long time to run, so sorting by the # number of tests doesn't accurately capture how long they take to run. - if http_tests: - test_lists.insert(0, http_tests) + if tests_to_http_lock: + tests_to_http_lock.reverse() + test_lists.insert(0, ("tests_to_http_lock", tests_to_http_lock)) filename_queue = Queue.Queue() for item in test_lists: @@ -687,7 +695,7 @@ class TestRunner: thread_timings.append({'name': thread.getName(), 'num_tests': thread.get_num_tests(), 'total_time': thread.get_total_time()}) - test_timings.update(thread.get_directory_timing_stats()) + test_timings.update(thread.get_test_group_timing_stats()) individual_test_timings.extend(thread.get_test_results()) return (thread_timings, test_timings, individual_test_timings) @@ -696,6 +704,10 @@ class TestRunner: """Returns whether the test runner needs an HTTP server.""" return self._contains_tests(self.HTTP_SUBDIR) + def needs_websocket(self): + """Returns whether the test runner needs a WEBSOCKET server.""" + return self._contains_tests(self.WEBSOCKET_SUBDIR) + def set_up_run(self): """Configures the system to be ready to run tests. @@ -728,14 +740,16 @@ class TestRunner: if not result_summary: return None - if self.needs_http(): - self._printer.print_update('Starting HTTP server ...') - self._port.start_http_server() + # Do not start when http locking is enabled. + if not self._options.wait_for_httpd: + if self.needs_http(): + self._printer.print_update('Starting HTTP server ...') + self._port.start_http_server() - if self._contains_tests(self.WEBSOCKET_SUBDIR): - self._printer.print_update('Starting WebSocket server ...') - self._port.start_websocket_server() - # self._websocket_secure_server.Start() + if self.needs_websocket(): + self._printer.print_update('Starting WebSocket server ...') + self._port.start_websocket_server() + # self._websocket_secure_server.Start() return result_summary @@ -826,10 +840,11 @@ class TestRunner: sys.stdout.flush() _log.debug("flushing stderr") sys.stderr.flush() - _log.debug("stopping http server") - self._port.stop_http_server() - _log.debug("stopping websocket server") - self._port.stop_websocket_server() + if not self._options.wait_for_httpd: + _log.debug("stopping http server") + self._port.stop_http_server() + _log.debug("stopping websocket server") + self._port.stop_websocket_server() _log.debug("stopping helper") self._port.stop_helper() @@ -1432,13 +1447,10 @@ def _set_up_derived_options(port_obj, 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 = port_obj.get_absolute_path( - options.results_directory) - else: - # If it's a relative path, make the output directory relative to - # Debug or Release. + if not os.path.isabs(options.results_directory): + # This normalizes the path to the build dir. + # FIXME: how this happens is not at all obvious; this is a dumb + # interface and should be cleaned up. options.results_directory = port_obj.results_directory() if not options.time_out_ms: @@ -1588,13 +1600,12 @@ def parse_args(args=None): optparse.make_option("--no-record-results", action="store_false", default=True, dest="record_results", help="Don't record the results."), + optparse.make_option("--wait-for-httpd", action="store_true", + default=False, dest="wait_for_httpd", + help="Wait for http locks."), # old-run-webkit-tests also has HTTP toggle options: # --[no-]http Run (or do not run) http tests # (default: run) - # --[no-]wait-for-httpd Wait for httpd if some other test - # session is using it already (same - # as WEBKIT_WAIT_FOR_HTTPD=1). - # (default: 0) ] test_options = [ diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py index 6fe99d6..a716cec 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py @@ -1,5 +1,6 @@ #!/usr/bin/python # Copyright (C) 2010 Google Inc. All rights reserved. +# Copyright (C) 2010 Gabor Rapcsanyi (rgabor@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 @@ -33,6 +34,7 @@ import codecs import logging import os import Queue +import shutil import sys import tempfile import thread @@ -70,7 +72,7 @@ def passing_run(args=[], port_obj=None, record_results=False, # We use the glob to test that globbing works. new_args.extend(['passes', 'http/tests', - 'websocket/tests', + 'http/tests/websocket/tests', 'failures/expected/*']) options, parsed_args = run_webkit_tests.parse_args(new_args) if port_obj is None: @@ -88,7 +90,7 @@ def logging_run(args=[], tests_included=False): if not tests_included: new_args.extend(['passes', 'http/tests', - 'websocket/tests', + 'http/tests/websocket/tests', 'failures/expected/*']) options, parsed_args = run_webkit_tests.parse_args(new_args) user = MockUser() @@ -232,10 +234,37 @@ class MainTest(unittest.TestCase): self.assertFalse(err.empty()) self.assertEqual(user.url, '/tmp/layout-test-results/results.html') + def test_results_directory_absolute(self): + # We run a configuration that should fail, to generate output, then + # look for what the output results url was. + + tmpdir = tempfile.mkdtemp() + res, out, err, user = logging_run(['--results-directory=' + tmpdir], + tests_included=True) + self.assertEqual(user.url, os.path.join(tmpdir, 'results.html')) + shutil.rmtree(tmpdir, ignore_errors=True) + + def test_results_directory_default(self): + # We run a configuration that should fail, to generate output, then + # look for what the output results url was. + + # This is the default location. + res, out, err, user = logging_run(tests_included=True) + self.assertEqual(user.url, '/tmp/layout-test-results/results.html') + + def test_results_directory_relative(self): + # We run a configuration that should fail, to generate output, then + # look for what the output results url was. + + res, out, err, user = logging_run(['--results-directory=foo'], + tests_included=True) + self.assertEqual(user.url, '/tmp/foo/results.html') + + def _mocked_open(original_open, file_list): def _wrapper(name, mode, encoding): - if name.find("-expected.") != -1 and mode == "w": + if name.find("-expected.") != -1 and mode.find("w") != -1: # we don't want to actually write new baselines, so stub these out name.replace('\\', '/') file_list.append(name) @@ -251,7 +280,10 @@ class RebaselineTest(unittest.TestCase): baseline = file + "-expected" + ext self.assertTrue(any(f.find(baseline) != -1 for f in file_list)) - def test_reset_results(self): + # FIXME: This test is failing on the bots. Also, this test touches the + # file system. Unit tests should not read or write the file system. + # https://bugs.webkit.org/show_bug.cgi?id=47879 + def disabled_test_reset_results(self): file_list = [] original_open = codecs.open try: @@ -294,6 +326,11 @@ class RebaselineTest(unittest.TestCase): codecs.open = original_open +class TestRunnerWrapper(run_webkit_tests.TestRunner): + def _get_test_info_for_file(self, test_file): + return test_file + + class TestRunnerTest(unittest.TestCase): def test_results_html(self): mock_port = Mock() @@ -314,6 +351,52 @@ class TestRunnerTest(unittest.TestCase): html = runner._results_html(["test_path"], {}, "Title", override_time="time") self.assertEqual(html, expected_html) + def queue_to_list(self, queue): + queue_list = [] + while(True): + try: + queue_list.append(queue.get_nowait()) + except Queue.Empty: + break + return queue_list + + def test_get_test_file_queue(self): + # Test that _get_test_file_queue in run_webkit_tests.TestRunner really + # put the http tests first in the queue. + runner = TestRunnerWrapper(port=Mock(), options=Mock(), printer=Mock()) + runner._options.experimental_fully_parallel = False + + test_list = [ + "LayoutTests/websocket/tests/unicode.htm", + "LayoutTests/animations/keyframes.html", + "LayoutTests/http/tests/security/view-source-no-refresh.html", + "LayoutTests/websocket/tests/websocket-protocol-ignored.html", + "LayoutTests/fast/css/display-none-inline-style-change-crash.html", + "LayoutTests/http/tests/xmlhttprequest/supported-xml-content-types.html", + "LayoutTests/dom/html/level2/html/HTMLAnchorElement03.html", + "LayoutTests/ietestcenter/Javascript/11.1.5_4-4-c-1.html", + "LayoutTests/dom/html/level2/html/HTMLAnchorElement06.html", + ] + + expected_tests_to_http_lock = set([ + 'LayoutTests/websocket/tests/unicode.htm', + 'LayoutTests/http/tests/security/view-source-no-refresh.html', + 'LayoutTests/websocket/tests/websocket-protocol-ignored.html', + 'LayoutTests/http/tests/xmlhttprequest/supported-xml-content-types.html', + ]) + + runner._options.child_processes = 1 + test_queue_for_single_thread = runner._get_test_file_queue(test_list) + runner._options.child_processes = 2 + test_queue_for_multi_thread = runner._get_test_file_queue(test_list) + + single_thread_results = self.queue_to_list(test_queue_for_single_thread) + multi_thread_results = self.queue_to_list(test_queue_for_multi_thread) + + self.assertEqual("tests_to_http_lock", single_thread_results[0][0]) + self.assertEqual(expected_tests_to_http_lock, set(single_thread_results[0][1])) + self.assertEqual("tests_to_http_lock", multi_thread_results[0][0]) + self.assertEqual(expected_tests_to_http_lock, set(multi_thread_results[0][1])) class DryrunTest(unittest.TestCase): # FIXME: it's hard to know which platforms are safe to test; the 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 1ad0fe6..0b05802 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py @@ -104,7 +104,7 @@ class ImageDiff(test_type_base.TestTypeBase): self.FILENAME_SUFFIX_EXPECTED + '.png') expected_image = port.expected_image(filename) - with codecs.open(actual_filename, 'r', None) as file: + with codecs.open(actual_filename, 'r+b', None) as file: actual_image = file.read() result = port.diff_image(expected_image, actual_image, 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 3a6e92b..dcc64a3 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 @@ -161,7 +161,10 @@ class TestTypeBase(object): def _write_into_file_at_path(self, file_path, contents, encoding): """This method assumes that byte_array is already encoded into the right format.""" - with codecs.open(file_path, "w", encoding=encoding) as file: + open_mode = 'w' + if encoding is None: + open_mode = 'w+b' + with codecs.open(file_path, open_mode, encoding=encoding) as file: file.write(contents) def write_output_files(self, filename, file_type, diff --git a/WebKitTools/Scripts/webkitpy/style/checker.py b/WebKitTools/Scripts/webkitpy/style/checker.py index f8eefa4..e0c956f 100644 --- a/WebKitTools/Scripts/webkitpy/style/checker.py +++ b/WebKitTools/Scripts/webkitpy/style/checker.py @@ -134,6 +134,7 @@ _PATH_RULES_SPECIFIER = [ ([# The GTK+ APIs use GTK+ naming style, which includes # lower-cased, underscore-separated values. # Also, GTK+ allows the use of NULL. + "WebCore/bindings/scripts/test/GObject", "WebKit/gtk/webkit/", "WebKitTools/DumpRenderTree/gtk/"], ["-readability/naming", @@ -142,6 +143,10 @@ _PATH_RULES_SPECIFIER = [ # exceptional header guards (e.g., WebCore_FWD_Debugger_h). "/ForwardingHeaders/"], ["-build/header_guard"]), + ([# assembler has lots of opcodes that use underscores, so + # we don't check for underscores in that directory. + "/JavaScriptCore/assembler/"], + ["-readability/naming"]), # For third-party Python code, keep only the following checks-- # diff --git a/WebKitTools/Scripts/webkitpy/style/checkers/cpp.py b/WebKitTools/Scripts/webkitpy/style/checkers/cpp.py index f8ebeff..7c1cb3e 100644 --- a/WebKitTools/Scripts/webkitpy/style/checkers/cpp.py +++ b/WebKitTools/Scripts/webkitpy/style/checkers/cpp.py @@ -2512,7 +2512,7 @@ def check_identifier_name_in_declaration(filename, line_number, line, error): modified_identifier = sub(r'(^|(?<=::))[ms]_', '', identifier) 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) + if (not (filename.find('JavaScriptCore') >= 0 and modified_identifier.find('op_') >= 0) and not modified_identifier.startswith('tst_') and not modified_identifier.startswith('webkit_dom_object_') and not modified_identifier.startswith('NPN_') @@ -2521,7 +2521,8 @@ def check_identifier_name_in_declaration(filename, line_number, line, error): and not modified_identifier.startswith('qt_') and not modified_identifier.startswith('cairo_') and not modified_identifier.find('::qt_') >= 0 - and not modified_identifier == "const_iterator"): + and not modified_identifier == "const_iterator" + and not modified_identifier == "vm_throw"): error(line_number, 'readability/naming', 4, identifier + " is incorrectly named. Don't use underscores in your identifier names.") # Check for variables named 'l', these are too easy to confuse with '1' in some fonts diff --git a/WebKitTools/Scripts/webkitpy/style/checkers/cpp_unittest.py b/WebKitTools/Scripts/webkitpy/style/checkers/cpp_unittest.py index 2f54305..071ce50 100644 --- a/WebKitTools/Scripts/webkitpy/style/checkers/cpp_unittest.py +++ b/WebKitTools/Scripts/webkitpy/style/checkers/cpp_unittest.py @@ -3690,6 +3690,7 @@ class WebKitStyleTest(CppStyleTestBase): # There is an exception for op code functions but only in the JavaScriptCore directory. self.assert_lint('void this_op_code(int var1, int var2)', '', 'JavaScriptCore/foo.cpp') + self.assert_lint('void op_code(int var1, int var2)', '', 'JavaScriptCore/foo.cpp') self.assert_lint('void this_op_code(int var1, int var2)', 'this_op_code' + name_underscore_error_message) # GObject requires certain magical names in class declarations. @@ -3716,6 +3717,9 @@ class WebKitStyleTest(CppStyleTestBase): # const_iterator is allowed as well. self.assert_lint('typedef VectorType::const_iterator const_iterator;', '') + # vm_throw is allowed as well. + self.assert_lint('int vm_throw;', '') + # Bitfields. self.assert_lint('unsigned _fillRule : 1;', '_fillRule' + name_underscore_error_message) diff --git a/WebKitTools/Scripts/webkitpy/style/patchreader.py b/WebKitTools/Scripts/webkitpy/style/patchreader.py index 576504a..f44839d 100644 --- a/WebKitTools/Scripts/webkitpy/style/patchreader.py +++ b/WebKitTools/Scripts/webkitpy/style/patchreader.py @@ -37,7 +37,6 @@ _log = logging.getLogger("webkitpy.style.patchreader") class PatchReader(object): - """Supports checking style in patches.""" def __init__(self, text_file_reader): @@ -53,28 +52,15 @@ class PatchReader(object): """Check style in the given patch.""" patch_files = DiffParser(patch_string.splitlines()).files - # The diff variable is a DiffFile instance. - for path, diff in patch_files.iteritems(): - line_numbers = set() - for line in diff.lines: - # When deleted line is not set, it means that - # the line is newly added (or modified). - if not line[0]: - line_numbers.add(line[1]) - - _log.debug('Found %s new or modified lines in: %s' - % (len(line_numbers), path)) + for path, diff_file in patch_files.iteritems(): + line_numbers = diff_file.added_or_modified_line_numbers() + _log.debug('Found %s new or modified lines in: %s' % (len(line_numbers), path)) - # If line_numbers is empty, the file has no new or - # modified lines. In this case, we don't check the file - # because we'll never output errors for the file. - # This optimization also prevents the program from exiting - # due to a deleted file. - if line_numbers: - self._text_file_reader.process_file(file_path=path, - line_numbers=line_numbers) - else: - # We don't check the file which contains deleted lines only - # but would like to treat it as to be processed so that - # we count up number of such files. + if not line_numbers: + # Don't check files which contain only deleted lines + # as they can never add style errors. However, mark them as + # processed so that we count up number of such files. self._text_file_reader.count_delete_only_file() + continue + + self._text_file_reader.process_file(file_path=path, line_numbers=line_numbers) diff --git a/WebKitTools/Scripts/webkitpy/style/patchreader_unittest.py b/WebKitTools/Scripts/webkitpy/style/patchreader_unittest.py index 2453c6b..b121082 100644 --- a/WebKitTools/Scripts/webkitpy/style/patchreader_unittest.py +++ b/WebKitTools/Scripts/webkitpy/style/patchreader_unittest.py @@ -78,7 +78,7 @@ index ef65bee..e3db70e 100644 # Required for Python to search this directory for module files +# New line """) - self._assert_checked([("__init__.py", set([2]))], 0) + self._assert_checked([("__init__.py", [2])], 0) def test_check_patch_with_deletion(self): self._call_check_patch("""Index: __init__.py diff --git a/WebKitTools/Scripts/webkitpy/test/main.py b/WebKitTools/Scripts/webkitpy/test/main.py index 9351768..1038d82 100644 --- a/WebKitTools/Scripts/webkitpy/test/main.py +++ b/WebKitTools/Scripts/webkitpy/test/main.py @@ -50,12 +50,12 @@ class Tester(object): return unittest_paths - def _modules_from_paths(self, webkitpy_dir, paths): + def _modules_from_paths(self, package_root, paths): """Return a list of fully-qualified module names given paths.""" - webkitpy_dir = os.path.abspath(webkitpy_dir) - webkitpy_name = os.path.split(webkitpy_dir)[1] # Equals "webkitpy". + package_path = os.path.abspath(package_root) + root_package_name = os.path.split(package_path)[1] # Equals "webkitpy". - prefix_length = len(webkitpy_dir) + prefix_length = len(package_path) modules = [] for path in paths: @@ -72,7 +72,8 @@ class Tester(object): break parts.insert(0, tail) # We now have, for example: common.config.ports_unittest - parts.insert(0, webkitpy_name) # Put "webkitpy" at the beginning. + # FIXME: This is all a hack around the fact that we always prefix webkitpy includes with "webkitpy." + parts.insert(0, root_package_name) # Put "webkitpy" at the beginning. module = ".".join(parts) modules.append(module) @@ -91,6 +92,9 @@ class Tester(object): if external_package_paths is None: external_package_paths = [] else: + # FIXME: We should consider moving webkitpy off of using "webkitpy." to prefix + # all includes. If we did that, then this would use path instead of dirname(path). + # QueueStatusServer.__init__ has a sys.path import hack due to this code. sys.path.extend(set(os.path.dirname(path) for path in external_package_paths)) if len(sys_argv) > 1 and not sys_argv[-1].startswith("-"): @@ -101,6 +105,7 @@ class Tester(object): # Otherwise, auto-detect all unit tests. + # FIXME: This should be combined with the external_package_paths code above. webkitpy_dir = os.path.dirname(webkitpy.__file__) modules = [] diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py index a347972..02e203c 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py @@ -27,19 +27,39 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from webkitpy.common.system.executive import ScriptError +from webkitpy.common.net.layouttestresults import LayoutTestResults + + +class CommitQueueTaskDelegate(object): + def run_command(self, command): + raise NotImplementedError("subclasses must implement") + + def command_passed(self, message, patch): + raise NotImplementedError("subclasses must implement") + + def command_failed(self, message, script_error, patch): + raise NotImplementedError("subclasses must implement") + + def refetch_patch(self, patch): + raise NotImplementedError("subclasses must implement") + + def layout_test_results(self): + raise NotImplementedError("subclasses must implement") + + def report_flaky_tests(self, patch, flaky_tests): + raise NotImplementedError("subclasses must implement") class CommitQueueTask(object): - def __init__(self, tool, commit_queue, patch): - self._tool = tool - self._commit_queue = commit_queue + def __init__(self, delegate, patch): + self._delegate = delegate self._patch = patch self._script_error = None def _validate(self): # Bugs might get closed, or patches might be obsoleted or r-'d while the # commit-queue is processing. - self._patch = self._tool.bugs.fetch_attachment(self._patch.id()) + self._patch = self._delegate.refetch_patch(self._patch) if self._patch.is_obsolete(): return False if self._patch.bug().is_closed(): @@ -52,12 +72,12 @@ class CommitQueueTask(object): def _run_command(self, command, success_message, failure_message): try: - self._commit_queue.run_webkit_patch(command) - self._commit_queue.command_passed(success_message, patch=self._patch) + self._delegate.run_command(command) + self._delegate.command_passed(success_message, patch=self._patch) return True except ScriptError, e: self._script_error = e - self.failure_status_id = self._commit_queue.command_failed(failure_message, script_error=self._script_error, patch=self._patch) + self.failure_status_id = self._delegate.command_failed(failure_message, script_error=self._script_error, patch=self._patch) return False def _apply(self): @@ -76,7 +96,6 @@ class CommitQueueTask(object): "build", "--no-clean", "--no-update", - "--build", "--build-style=both", "--quiet", ], @@ -88,7 +107,6 @@ class CommitQueueTask(object): "build", "--force-clean", "--no-update", - "--build", "--build-style=both", "--quiet", ], @@ -121,6 +139,12 @@ class CommitQueueTask(object): "Able to pass tests without patch", "Unable to pass tests without patch (tree is red?)") + def _failing_tests_from_last_run(self): + results = self._delegate.layout_test_results() + if not results: + return None + return results.failing_tests() + def _land(self): return self._run_command([ "land-attachment", @@ -134,6 +158,29 @@ class CommitQueueTask(object): "Landed patch", "Unable to land patch") + def _report_flaky_tests(self, flaky_tests): + self._delegate.report_flaky_tests(self._patch, flaky_tests) + + def _test_patch(self): + if self._patch.is_rollout(): + return True + if self._test(): + return True + + first_failing_tests = self._failing_tests_from_last_run() + if self._test(): + self._report_flaky_tests(first_failing_tests) + return True + + second_failing_tests = self._failing_tests_from_last_run() + if first_failing_tests != second_failing_tests: + self._report_flaky_tests(first_failing_tests + second_failing_tests) + return False + + if self._build_and_test_without_patch(): + raise self._script_error # The error from the previous ._test() run is real, report it. + return False # Tree must be red, just retry later. + def run(self): if not self._validate(): return False @@ -143,12 +190,8 @@ class CommitQueueTask(object): if not self._build_without_patch(): return False raise self._script_error - if not self._patch.is_rollout(): - if not self._test(): - if not self._test(): - if not self._build_and_test_without_patch(): - return False - raise self._script_error + if not self._test_patch(): + return False # Make sure the patch is still valid before landing (e.g., make sure # no one has set commit-queue- since we started working on the patch.) if not self._validate(): diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py index 8b46146..6fa0667 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py @@ -36,11 +36,11 @@ from webkitpy.tool.bot.commitqueuetask import * from webkitpy.tool.mocktool import MockTool -class MockCommitQueue: +class MockCommitQueue(CommitQueueTaskDelegate): def __init__(self, error_plan): self._error_plan = error_plan - def run_webkit_patch(self, command): + def run_command(self, command): log("run_webkit_patch: %s" % command) if self._error_plan: error = self._error_plan.pop(0) @@ -56,19 +56,28 @@ class MockCommitQueue: failure_message, script_error, patch.id())) return 3947 + def refetch_patch(self, patch): + return patch + + def layout_test_results(self): + return None + + def report_flaky_tests(self, patch, flaky_tests): + log("report_flaky_tests: patch='%s' flaky_tests='%s'" % (patch.id(), flaky_tests)) + class CommitQueueTaskTest(unittest.TestCase): def _run_through_task(self, commit_queue, expected_stderr, expected_exception=None): tool = MockTool(log_executive=True) patch = tool.bugs.fetch_attachment(197) - task = CommitQueueTask(tool, commit_queue, patch) + task = CommitQueueTask(commit_queue, patch) OutputCapture().assert_outputs(self, task.run, expected_stderr=expected_stderr, expected_exception=expected_exception) def test_success_case(self): commit_queue = MockCommitQueue([]) expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] command_passed: success_message='Applied patch' patch='197' -run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] command_passed: success_message='Built patch' patch='197' run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] command_passed: success_message='Passed tests' patch='197' @@ -93,9 +102,9 @@ command_failed: failure_message='Patch does not apply' script_error='MOCK apply ]) expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] command_passed: success_message='Applied patch' patch='197' -run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='197' -run_webkit_patch: ['build', '--force-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--force-clean', '--no-update', '--build-style=both', '--quiet'] command_passed: success_message='Able to build without patch' patch='197' """ self._run_through_task(commit_queue, expected_stderr, ScriptError) @@ -108,9 +117,9 @@ command_passed: success_message='Able to build without patch' patch='197' ]) expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] command_passed: success_message='Applied patch' patch='197' -run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='197' -run_webkit_patch: ['build', '--force-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--force-clean', '--no-update', '--build-style=both', '--quiet'] command_failed: failure_message='Unable to build without patch' script_error='MOCK clean build failure' patch='197' """ self._run_through_task(commit_queue, expected_stderr) @@ -123,12 +132,13 @@ command_failed: failure_message='Unable to build without patch' script_error='MO ]) expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] command_passed: success_message='Applied patch' patch='197' -run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] command_passed: success_message='Built patch' patch='197' run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] command_failed: failure_message='Patch does not pass tests' script_error='MOCK tests failure' patch='197' run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] command_passed: success_message='Passed tests' patch='197' +report_flaky_tests: patch='197' flaky_tests='None' run_webkit_patch: ['land-attachment', '--force-clean', '--ignore-builders', '--quiet', '--non-interactive', '--parent-command=commit-queue', 197] command_passed: success_message='Landed patch' patch='197' """ @@ -143,7 +153,7 @@ command_passed: success_message='Landed patch' patch='197' ]) expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] command_passed: success_message='Applied patch' patch='197' -run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] command_passed: success_message='Built patch' patch='197' run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='197' @@ -164,7 +174,7 @@ command_passed: success_message='Able to pass tests without patch' patch='197' ]) expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] command_passed: success_message='Applied patch' patch='197' -run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] command_passed: success_message='Built patch' patch='197' run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='197' @@ -184,7 +194,7 @@ command_failed: failure_message='Unable to pass tests without patch (tree is red ]) expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] command_passed: success_message='Applied patch' patch='197' -run_webkit_patch: ['build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] command_passed: success_message='Built patch' patch='197' run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] command_passed: success_message='Passed tests' patch='197' diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py b/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py index 15eaaf3..dc892a4 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py @@ -28,18 +28,15 @@ from webkitpy.common.system.deprecated_logging import log from webkitpy.common.net.bugzilla import CommitterValidator +from webkitpy.tool.grammar import pluralize class AbstractFeeder(object): def __init__(self, tool): self._tool = tool - def feed(tool): - raise NotImplementedError, "subclasses must implement" - - def update_work_items(self, item_ids): - self._tool.status_server.update_work_items(self.queue_name, item_ids) - log("Feeding %s items %s" % (self.queue_name, item_ids)) + def feed(self): + raise NotImplementedError("subclasses must implement") class CommitQueueFeeder(AbstractFeeder): @@ -49,11 +46,17 @@ class CommitQueueFeeder(AbstractFeeder): AbstractFeeder.__init__(self, tool) self.committer_validator = CommitterValidator(self._tool.bugs) + def _update_work_items(self, item_ids): + # FIXME: This is the last use of update_work_items, the commit-queue + # should move to feeding patches one at a time like the EWS does. + self._tool.status_server.update_work_items(self.queue_name, item_ids) + log("Feeding %s items %s" % (self.queue_name, item_ids)) + def feed(self): patches = self._validate_patches() patches = sorted(patches, self._patch_cmp) patch_ids = [patch.id() for patch in patches] - self.update_work_items(patch_ids) + self._update_work_items(patch_ids) def _patches_for_bug(self, bug_id): return self._tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True) @@ -71,3 +74,17 @@ class CommitQueueFeeder(AbstractFeeder): if rollout_cmp != 0: return rollout_cmp return cmp(a.attach_date(), b.attach_date()) + + +class EWSFeeder(AbstractFeeder): + def __init__(self, tool): + self._ids_sent_to_server = set() + AbstractFeeder.__init__(self, tool) + + def feed(self): + ids_needing_review = set(self._tool.bugs.queries.fetch_attachment_ids_from_review_queue()) + new_ids = ids_needing_review.difference(self._ids_sent_to_server) + log("Feeding EWS (%s, %s new)" % (pluralize("r? patch", len(ids_needing_review)), len(new_ids))) + for attachment_id in new_ids: # Order doesn't really matter for the EWS. + self._tool.status_server.submit_to_ews(attachment_id) + self._ids_sent_to_server.add(attachment_id) diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/patchcollection.py b/WebKitTools/Scripts/webkitpy/tool/bot/patchcollection.py deleted file mode 100644 index 6100cf8..0000000 --- a/WebKitTools/Scripts/webkitpy/tool/bot/patchcollection.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) 2010 Google Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following disclaimer -# in the documentation and/or other materials provided with the -# distribution. -# * Neither the name of Google Inc. nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -class PersistentPatchCollectionDelegate: - def collection_name(self): - raise NotImplementedError, "subclasses must implement" - - def fetch_potential_patch_ids(self): - raise NotImplementedError, "subclasses must implement" - - def status_server(self): - raise NotImplementedError, "subclasses must implement" - - def is_terminal_status(self, status): - raise NotImplementedError, "subclasses must implement" - - -class PersistentPatchCollection: - def __init__(self, delegate): - self._delegate = delegate - self._name = self._delegate.collection_name() - self._status = self._delegate.status_server() - self._status_cache = {} - - def _cached_status(self, patch_id): - cached = self._status_cache.get(patch_id) - if cached: - return cached - status = self._status.patch_status(self._name, patch_id) - if status and self._delegate.is_terminal_status(status): - self._status_cache[patch_id] = status - return status - - def _is_active_patch_id(self, patch_id): - """Active patches are patches waiting to be processed from this collection.""" - status = self._cached_status(patch_id) - return not status or not self._delegate.is_terminal_status(status) - - def _fetch_active_patch_ids(self): - patch_ids = self._delegate.fetch_potential_patch_ids() - return filter(lambda patch_id: self._is_active_patch_id(patch_id), patch_ids) - - def next(self): - # Note: We only fetch all the ids so we can post them back to the server. - # This will go away once we have a feeder queue and all other queues are - # just pulling their next work item from the server. - patch_ids = self._fetch_active_patch_ids() - # FIXME: We're assuming self._name is a valid queue-name. - self._status.update_work_items(self._name, patch_ids) - if not patch_ids: - return None - return patch_ids[0] diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py index 8118653..8b016e8 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py @@ -125,8 +125,8 @@ class QueueEngine: traceback.print_exc() # Don't try tell the status bot, in case telling it causes an exception. self._sleep("Exception while preparing queue") - # Never reached. - self._ensure_work_log_closed() + self._stopping("Delegate terminated queue.") + return 0 def _stopping(self, message): log("\n%s" % message) diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py index bfec401..37d8502 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py @@ -43,6 +43,7 @@ class LoggingDelegate(QueueEngineDelegate): self._test = test self._callbacks = [] self._run_before = False + self.stop_message = None expected_callbacks = [ 'queue_log_path', @@ -52,7 +53,8 @@ class LoggingDelegate(QueueEngineDelegate): 'should_proceed_with_work_item', 'work_item_log_path', 'process_work_item', - 'should_continue_work_queue' + 'should_continue_work_queue', + 'stop_work_queue', ] def record(self, method_name): @@ -95,21 +97,20 @@ class LoggingDelegate(QueueEngineDelegate): self.record("handle_unexpected_error") self._test.assertEquals(work_item, "work_item") + def stop_work_queue(self, message): + self.record("stop_work_queue") + self.stop_message = message + class RaisingDelegate(LoggingDelegate): def __init__(self, test, exception): LoggingDelegate.__init__(self, test) self._exception = exception - self.stop_message = None def process_work_item(self, work_item): self.record("process_work_item") raise self._exception - def stop_work_queue(self, message): - self.record("stop_work_queue") - self.stop_message = message - class NotSafeToProceedDelegate(LoggingDelegate): def should_proceed_with_work_item(self, work_item): @@ -132,16 +133,15 @@ class FastQueueEngine(QueueEngine): class QueueEngineTest(unittest.TestCase): def test_trivial(self): delegate = LoggingDelegate(self) - work_queue = QueueEngine("trivial-queue", delegate, threading.Event()) - work_queue.run() + self._run_engine(delegate) + self.assertEquals(delegate.stop_message, "Delegate terminated queue.") self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks) self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "queue_log_path"))) self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "work_log_path", "work_item.log"))) def test_unexpected_error(self): delegate = RaisingDelegate(self, ScriptError(exit_code=3)) - work_queue = QueueEngine("error-queue", delegate, threading.Event()) - work_queue.run() + self._run_engine(delegate) expected_callbacks = LoggingDelegate.expected_callbacks[:] work_item_index = expected_callbacks.index('process_work_item') # The unexpected error should be handled right after process_work_item starts @@ -151,11 +151,18 @@ class QueueEngineTest(unittest.TestCase): def test_handled_error(self): delegate = RaisingDelegate(self, ScriptError(exit_code=QueueEngine.handled_error_code)) - work_queue = QueueEngine("handled-error-queue", delegate, threading.Event()) - work_queue.run() + self._run_engine(delegate) self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks) - def _test_terminating_queue(self, exception, expected_message): + def _run_engine(self, delegate, engine=None, termination_message=None): + if not engine: + engine = QueueEngine("test-queue", delegate, threading.Event()) + if not termination_message: + termination_message = "Delegate terminated queue." + expected_stderr = "\n%s\n" % termination_message + OutputCapture().assert_outputs(self, engine.run, [], expected_stderr=expected_stderr) + + def _test_terminating_queue(self, exception, termination_message): work_item_index = LoggingDelegate.expected_callbacks.index('process_work_item') # The terminating error should be handled right after process_work_item. # There should be no other callbacks after stop_work_queue. @@ -163,14 +170,10 @@ class QueueEngineTest(unittest.TestCase): expected_callbacks.append("stop_work_queue") delegate = RaisingDelegate(self, exception) - work_queue = QueueEngine("terminating-queue", delegate, threading.Event()) - - output = OutputCapture() - expected_stderr = "\n%s\n" % expected_message - output.assert_outputs(self, work_queue.run, [], expected_stderr=expected_stderr) + self._run_engine(delegate, termination_message=termination_message) self.assertEquals(delegate._callbacks, expected_callbacks) - self.assertEquals(delegate.stop_message, expected_message) + self.assertEquals(delegate.stop_message, termination_message) def test_terminating_error(self): self._test_terminating_queue(KeyboardInterrupt(), "User terminated queue.") @@ -178,15 +181,10 @@ class QueueEngineTest(unittest.TestCase): def test_not_safe_to_proceed(self): delegate = NotSafeToProceedDelegate(self) - work_queue = FastQueueEngine(delegate) - work_queue.run() + self._run_engine(delegate, engine=FastQueueEngine(delegate)) expected_callbacks = LoggingDelegate.expected_callbacks[:] - next_work_item_index = expected_callbacks.index('next_work_item') - # We slice out the common part of the expected callbacks. - # We add 2 here to include should_proceed_with_work_item, which is - # a pain to search for directly because it occurs twice. - expected_callbacks = expected_callbacks[:next_work_item_index + 2] - expected_callbacks.append('should_continue_work_queue') + expected_callbacks.remove('work_item_log_path') + expected_callbacks.remove('process_work_item') self.assertEquals(delegate._callbacks, expected_callbacks) def test_now(self): diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py index a38c3cf..da506bc 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py @@ -77,55 +77,15 @@ class Sheriff(object): ]) return parse_bug_id(output) - def _rollout_reason(self, builders): - # FIXME: This should explain which layout tests failed - # however, that would require Build objects here, either passed - # in through failure_info, or through Builder.latest_build. - names = [builder.name() for builder in builders] - return "Caused builders %s to fail." % join_with_separators(names) - - def post_automatic_rollout_patch(self, commit_info, builders): - # For now we're only posting rollout patches for commit-queue patches. - commit_bot_email = "eseidel@chromium.org" - if commit_bot_email == commit_info.committer_email(): - try: - self.post_rollout_patch(commit_info.revision(), - self._rollout_reason(builders)) - except ScriptError, e: - log("Failed to create-rollout.") - - def post_blame_comment_on_bug(self, commit_info, builders, blame_list): + def post_blame_comment_on_bug(self, commit_info, builders, tests): if not commit_info.bug_id(): return comment = "%s might have broken %s" % ( view_source_url(commit_info.revision()), join_with_separators([builder.name() for builder in builders])) - if len(blame_list) > 1: - comment += "\nThe following changes are on the blame list:\n" - comment += "\n".join(map(view_source_url, blame_list)) + if tests: + comment += "\nThe following tests are not passing:\n" + comment += "\n".join(tests) self._tool.bugs.post_comment_to_bug(commit_info.bug_id(), comment, cc=self._sheriffbot.watchers) - - # FIXME: Should some of this logic be on BuildBot? - def provoke_flaky_builders(self, revisions_causing_failures): - # We force_build builders that are red but have not "failed" (i.e., - # been red twice). We do this to avoid a deadlock situation where a - # flaky test blocks the commit-queue and there aren't any other - # patches being landed to re-spin the builder. - failed_builders = sum([revisions_causing_failures[key] for - key in revisions_causing_failures.keys()], []) - failed_builder_names = \ - set([builder.name() for builder in failed_builders]) - idle_red_builder_names = \ - set([builder["name"] - for builder in self._tool.buildbot.idle_red_core_builders()]) - - # We only want to provoke these builders if they are idle and have not - # yet "failed" (i.e., been red twice) to avoid overloading the bots. - flaky_builder_names = idle_red_builder_names - failed_builder_names - - for name in flaky_builder_names: - flaky_builder = self._tool.buildbot.builder_with_name(name) - flaky_builder.force_build(username=self._sheriffbot.name, - comments="Probe for flakiness.") diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py index c375ff9..690af1f 100644 --- a/WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py @@ -47,15 +47,6 @@ class MockSheriffBot(object): class SheriffTest(unittest.TestCase): - def test_rollout_reason(self): - sheriff = Sheriff(MockTool(), MockSheriffBot()) - builders = [ - Builder("Foo", None), - Builder("Bar", None), - ] - reason = "Caused builders Foo and Bar to fail." - self.assertEquals(sheriff._rollout_reason(builders), reason) - def test_post_blame_comment_on_bug(self): def run(): sheriff = Sheriff(MockTool(), MockSheriffBot()) @@ -68,38 +59,32 @@ class SheriffTest(unittest.TestCase): commit_info.revision = lambda: 4321 # Should do nothing with no bug_id sheriff.post_blame_comment_on_bug(commit_info, builders, []) - sheriff.post_blame_comment_on_bug(commit_info, builders, [2468, 5646]) + sheriff.post_blame_comment_on_bug(commit_info, builders, ["mock-test-1", "mock-test-2"]) # Should try to post a comment to the bug, but MockTool.bugs does nothing. commit_info.bug_id = lambda: 1234 sheriff.post_blame_comment_on_bug(commit_info, builders, []) - sheriff.post_blame_comment_on_bug(commit_info, builders, [3432]) - sheriff.post_blame_comment_on_bug(commit_info, builders, [841, 5646]) + sheriff.post_blame_comment_on_bug(commit_info, builders, ["mock-test-1"]) + sheriff.post_blame_comment_on_bug(commit_info, builders, ["mock-test-1", "mock-test-2"]) - expected_stderr = u"MOCK bug comment: bug_id=1234, cc=['watcher@example.com']\n--- Begin comment ---\\http://trac.webkit.org/changeset/4321 might have broken Foo and Bar\n--- End comment ---\n\nMOCK bug comment: bug_id=1234, cc=['watcher@example.com']\n--- Begin comment ---\\http://trac.webkit.org/changeset/4321 might have broken Foo and Bar\n--- End comment ---\n\nMOCK bug comment: bug_id=1234, cc=['watcher@example.com']\n--- Begin comment ---\\http://trac.webkit.org/changeset/4321 might have broken Foo and Bar\nThe following changes are on the blame list:\nhttp://trac.webkit.org/changeset/841\nhttp://trac.webkit.org/changeset/5646\n--- End comment ---\n\n" - OutputCapture().assert_outputs(self, run, expected_stderr=expected_stderr) + expected_stderr = u"""MOCK bug comment: bug_id=1234, cc=['watcher@example.com'] +--- Begin comment --- +http://trac.webkit.org/changeset/4321 might have broken Foo and Bar +--- End comment --- - def test_provoke_flaky_builders(self): - def run(): - tool = MockTool() - tool.buildbot.light_tree_on_fire() - sheriff = Sheriff(tool, MockSheriffBot()) - revisions_causing_failures = {} - sheriff.provoke_flaky_builders(revisions_causing_failures) - expected_stderr = "MOCK: force_build: name=Builder2, username=mock-sheriff-bot, comments=Probe for flakiness.\n" - OutputCapture().assert_outputs(self, run, expected_stderr=expected_stderr) +MOCK bug comment: bug_id=1234, cc=['watcher@example.com'] +--- Begin comment --- +http://trac.webkit.org/changeset/4321 might have broken Foo and Bar +The following tests are not passing: +mock-test-1 +--- End comment --- - def test_post_blame_comment_on_bug(self): - sheriff = Sheriff(MockTool(), MockSheriffBot()) - builders = [ - Builder("Foo", None), - Builder("Bar", None), - ] - commit_info = Mock() - commit_info.bug_id = lambda: None - commit_info.revision = lambda: 4321 - commit_info.committer = lambda: None - commit_info.committer_email = lambda: "foo@example.com" - commit_info.reviewer = lambda: None - commit_info.author = lambda: None - sheriff.post_automatic_rollout_patch(commit_info, builders) +MOCK bug comment: bug_id=1234, cc=['watcher@example.com'] +--- Begin comment --- +http://trac.webkit.org/changeset/4321 might have broken Foo and Bar +The following tests are not passing: +mock-test-1 +mock-test-2 +--- End comment --- +""" + OutputCapture().assert_outputs(self, run, expected_stderr=expected_stderr) diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py b/WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py index de92cd3..adc6d81 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py @@ -33,5 +33,16 @@ from webkitpy.tool.mocktool import MockOptions, MockTool class CommandsTest(unittest.TestCase): def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=MockOptions(), tool=MockTool()): + options.blocks = True + options.cc = 'MOCK cc' + options.component = 'MOCK component' + options.confirm = True + options.email = 'MOCK email' + options.git_commit = 'MOCK git commit' + options.obsolete_patches = True + options.open_bug = True + options.port = 'MOCK port' + options.quiet = True + options.reviewer = 'MOCK reviewer' command.bind_to_tool(tool) OutputCapture().assert_outputs(self, command.execute, [options, args, tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr) diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/download.py b/WebKitTools/Scripts/webkitpy/tool/commands/download.py index 9916523..ed5c604 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/download.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/download.py @@ -43,6 +43,17 @@ from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand from webkitpy.common.system.deprecated_logging import error, log +class Clean(AbstractSequencedCommand): + name = "clean" + help_text = "Clean the working copy" + steps = [ + steps.CleanWorkingDirectory, + ] + + def _prepare_state(self, options, args, tool): + options.force_clean = True + + class Update(AbstractSequencedCommand): name = "update" help_text = "Update working copy (used internally)" @@ -61,6 +72,9 @@ class Build(AbstractSequencedCommand): steps.Build, ] + def _prepare_state(self, options, args, tool): + options.build = True + class BuildAndTest(AbstractSequencedCommand): name = "build-and-test" diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py index faddd50..6af1f64 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py @@ -57,15 +57,19 @@ class AbstractRolloutPrepCommandTest(unittest.TestCase): class DownloadCommandsTest(CommandsTest): def _default_options(self): options = MockOptions() - options.force_clean = False - options.clean = True + options.build = True + options.build_style = True options.check_builders = True - options.quiet = False + options.check_style = True + options.clean = True + options.close_bug = True + options.force_clean = False + options.force_patch = True options.non_interactive = False - options.update = True - options.build = True + options.parent_command = 'MOCK parent command' + options.quiet = False options.test = True - options.close_bug = True + options.update = True return options def test_build(self): diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py index 86e2e15..5ec468e 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py @@ -48,7 +48,6 @@ class AbstractEarlyWarningSystem(AbstractReviewQueue): self.run_webkit_patch([ "build", self.port.flag(), - "--build", "--build-style=%s" % self._build_style, "--force-clean", "--no-update", @@ -149,10 +148,6 @@ class ChromiumWindowsEWS(AbstractChromiumEWS): name = "cr-win-ews" -class ChromiumMacEWS(AbstractChromiumEWS): - name = "cr-mac-ews" - - # For platforms that we can't run inside a VM (like Mac OS X), we require # patches to be uploaded by committers, who are generally trustworthy folk. :) class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem): @@ -167,6 +162,14 @@ class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem): return AbstractEarlyWarningSystem.process_work_item(self, patch) +# FIXME: Inheriting from AbstractCommitterOnlyEWS is kinda a hack, but it +# happens to work because AbstractChromiumEWS and AbstractCommitterOnlyEWS +# provide disjoint sets of functionality, and Python is otherwise smart +# enough to handle the diamond inheritance. +class ChromiumMacEWS(AbstractChromiumEWS, AbstractCommitterOnlyEWS): + name = "cr-mac-ews" + + class MacEWS(AbstractCommitterOnlyEWS): name = "mac-ews" port_name = "mac" diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py index 3b0ea47..c400f81 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py @@ -48,9 +48,9 @@ class EarlyWarningSytemTest(QueuesTest): expected_stderr = { "begin_work_queue": self._default_begin_work_queue_stderr(ews.name, os.getcwd()), # FIXME: Use of os.getcwd() is wrong, should be scm.checkout_root "handle_unexpected_error": "Mock error message\n", - "next_work_item": "MOCK: update_work_items: %(name)s [103]\n" % string_replacemnts, - "process_work_item": "MOCK: update_status: %(name)s Pass\n" % string_replacemnts, - "handle_script_error": "MOCK: update_status: %(name)s ScriptError error message\nMOCK bug comment: bug_id=142, cc=%(watchers)s\n--- Begin comment ---\\Attachment 197 did not build on %(port)s:\nBuild output: http://dummy_url\n--- End comment ---\n\n" % string_replacemnts, + "next_work_item": "", + "process_work_item": "MOCK: update_status: %(name)s Pass\nMOCK: release_work_item: %(name)s 197\n" % string_replacemnts, + "handle_script_error": "MOCK: update_status: %(name)s ScriptError error message\nMOCK bug comment: bug_id=142, cc=%(watchers)s\n--- Begin comment ---\nAttachment 197 did not build on %(port)s:\nBuild output: http://dummy_url\n--- End comment ---\n\n" % string_replacemnts, } return expected_stderr @@ -85,3 +85,12 @@ class EarlyWarningSytemTest(QueuesTest): "handle_script_error": SystemExit, } self.assert_queue_outputs(ews, expected_stderr=expected_stderr, expected_exceptions=expected_exceptions) + + def test_chromium_mac_ews(self): + ews = ChromiumMacEWS() + expected_stderr = self._default_expected_stderr(ews) + expected_stderr["process_work_item"] = "MOCK: update_status: cr-mac-ews Error: cr-mac-ews cannot process patches from non-committers :(\n" + expected_exceptions = { + "handle_script_error": SystemExit, + } + self.assert_queue_outputs(ews, expected_stderr=expected_stderr, expected_exceptions=expected_exceptions) diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queries.py b/WebKitTools/Scripts/webkitpy/tool/commands/queries.py index c6e45aa..16ddc2c 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queries.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queries.py @@ -30,6 +30,8 @@ from optparse import make_option +import webkitpy.tool.steps as steps + from webkitpy.common.checkout.commitinfo import CommitInfo from webkitpy.common.config.committers import CommitterList from webkitpy.common.net.buildbot import BuildBot @@ -41,6 +43,21 @@ from webkitpy.common.system.deprecated_logging import log from webkitpy.layout_tests import port +class SuggestReviewers(AbstractDeclarativeCommand): + name = "suggest-reviewers" + help_text = "Suggest reviewers for a patch based on recent changes to the modified files." + + def __init__(self): + options = [ + steps.Options.git_commit, + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + def execute(self, options, args, tool): + reviewers = tool.checkout().suggested_reviewers(options.git_commit) + print "\n".join([reviewer.full_name for reviewer in reviewers]) + + class BugsToCommit(AbstractDeclarativeCommand): name = "bugs-to-commit" help_text = "List bugs in the commit-queue" @@ -162,15 +179,6 @@ class WhatBroke(AbstractDeclarativeCommand): print "All builders are passing!" -class WhoBrokeIt(AbstractDeclarativeCommand): - name = "who-broke-it" - help_text = "Print a list of revisions causing failures on %s" % BuildBot.default_host - - def execute(self, options, args, tool): - for revision, builders in self._tool.buildbot.failure_map(False).revisions_causing_failures().items(): - print "r%s appears to have broken %s" % (revision, [builder.name() for builder in builders]) - - class ResultsFor(AbstractDeclarativeCommand): name = "results-for" help_text = "Print a list of failures for the passed revision from bots on %s" % BuildBot.default_host @@ -197,17 +205,21 @@ class FailureReason(AbstractDeclarativeCommand): name = "failure-reason" help_text = "Lists revisions where individual test failures started at %s" % BuildBot.default_host - def _print_blame_information_for_transition(self, green_build, red_build, failing_tests): - regression_window = RegressionWindow(green_build, red_build) - revisions = regression_window.revisions() + def _blame_line_for_revision(self, revision): + try: + commit_info = self._tool.checkout().commit_info_for_revision(revision) + except Exception, e: + return "FAILED to fetch CommitInfo for r%s, exception: %s" % (revision, e) + if not commit_info: + return "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision + return commit_info.blame_string(self._tool.bugs) + + def _print_blame_information_for_transition(self, regression_window, failing_tests): + red_build = regression_window.failing_build() print "SUCCESS: Build %s (r%s) was the first to show failures: %s" % (red_build._number, red_build.revision(), failing_tests) print "Suspect revisions:" - for revision in revisions: - commit_info = self._tool.checkout().commit_info_for_revision(revision) - if commit_info: - print commit_info.blame_string(self._tool.bugs) - else: - print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision + for revision in regression_window.revisions(): + print self._blame_line_for_revision(revision) def _explain_failures_for_builder(self, builder, start_revision): print "Examining failures for \"%s\", starting at r%s" % (builder.name(), start_revision) @@ -244,7 +256,8 @@ class FailureReason(AbstractDeclarativeCommand): print "No change in build %s (r%s), %s unexplained failures (%s in this build)" % (build._number, build.revision(), len(results_to_explain), len(failures)) last_build_with_results = build continue - self._print_blame_information_for_transition(build, last_build_with_results, fixed_results) + regression_window = RegressionWindow(build, last_build_with_results) + self._print_blame_information_for_transition(regression_window, fixed_results) last_build_with_results = build results_to_explain -= fixed_results if results_to_explain: @@ -274,6 +287,75 @@ class FailureReason(AbstractDeclarativeCommand): return self._explain_failures_for_builder(builder, start_revision=int(start_revision)) +class FindFlakyTests(AbstractDeclarativeCommand): + name = "find-flaky-tests" + help_text = "Lists tests that often fail for a single build at %s" % BuildBot.default_host + + def _find_failures(self, builder, revision): + build = builder.build_for_revision(revision, allow_failed_lookups=True) + if not build: + print "No build for %s" % revision + return (None, None) + results = build.layout_test_results() + if not results: + print "No results build %s (r%s)" % (build._number, build.revision()) + return (None, None) + failures = set(results.failing_tests()) + if len(failures) >= 20: + # FIXME: We may need to move this logic into the LayoutTestResults class. + # The buildbot stops runs after 20 failures so we don't have full results to work with here. + print "Too many failures in build %s (r%s), ignoring." % (build._number, build.revision()) + return (None, None) + return (build, failures) + + def _increment_statistics(self, flaky_tests, flaky_test_statistics): + for test in flaky_tests: + count = flaky_test_statistics.get(test, 0) + flaky_test_statistics[test] = count + 1 + + def _print_statistics(self, statistics): + print "=== Results ===" + print "Occurances Test name" + for value, key in sorted([(value, key) for key, value in statistics.items()]): + print "%10d %s" % (value, key) + + def _walk_backwards_from(self, builder, start_revision, limit): + flaky_test_statistics = {} + all_previous_failures = set([]) + one_time_previous_failures = set([]) + previous_build = None + for i in range(limit): + revision = start_revision - i + print "Analyzing %s ... " % revision, + (build, failures) = self._find_failures(builder, revision) + if failures == None: + # Notice that we don't loop on the empty set! + continue + print "has %s failures" % len(failures) + flaky_tests = one_time_previous_failures - failures + if flaky_tests: + print "Flaky tests: %s %s" % (sorted(flaky_tests), + previous_build.results_url()) + self._increment_statistics(flaky_tests, flaky_test_statistics) + one_time_previous_failures = failures - all_previous_failures + all_previous_failures = failures + previous_build = build + self._print_statistics(flaky_test_statistics) + + def _builder_to_analyze(self): + statuses = self._tool.buildbot.builder_statuses() + choices = [status["name"] for status in statuses] + chosen_name = User.prompt_with_list("Which builder to analyze:", choices) + for status in statuses: + if status["name"] == chosen_name: + return (self._tool.buildbot.builder_with_name(chosen_name), status["built_revision"]) + + def execute(self, options, args, tool): + (builder, latest_revision) = self._builder_to_analyze() + limit = self._tool.user.prompt("How many revisions to look through? [10000] ") or 10000 + return self._walk_backwards_from(builder, latest_revision, limit=int(limit)) + + class TreeStatus(AbstractDeclarativeCommand): name = "tree-status" help_text = "Print the status of the %s buildbots" % BuildBot.default_host diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queries_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queries_unittest.py index 7dddfe7..05a4a5c 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queries_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queries_unittest.py @@ -26,12 +26,15 @@ # (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.common.net.bugzilla import Bugzilla from webkitpy.thirdparty.mock import Mock from webkitpy.tool.commands.commandtest import CommandsTest from webkitpy.tool.commands.queries import * from webkitpy.tool.mocktool import MockTool + class QueryCommandsTest(CommandsTest): def test_bugs_to_commit(self): expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\n" @@ -71,3 +74,17 @@ class QueryCommandsTest(CommandsTest): expected_stdout = "Test 'media' is not skipped by any port.\n" self.assert_execute_outputs(SkippedPorts(), ("media",), expected_stdout) + + +class FailureReasonTest(unittest.TestCase): + def test_blame_line_for_revision(self): + tool = MockTool() + command = FailureReason() + command.bind_to_tool(tool) + # This is an artificial example, mostly to test the CommitInfo lookup failure case. + self.assertEquals(command._blame_line_for_revision(None), "FAILED to fetch CommitInfo for rNone, likely missing ChangeLog") + + def raising_mock(self): + raise Exception("MESSAGE") + tool.checkout().commit_info_for_revision = raising_mock + self.assertEquals(command._blame_line_for_revision(None), "FAILED to fetch CommitInfo for rNone, exception: MESSAGE") diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queues.py b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py index 80fd2ea..7b3002a 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queues.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py @@ -27,6 +27,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from __future__ import with_statement + +import codecs import time import traceback import os @@ -36,17 +39,18 @@ from optparse import make_option from StringIO import StringIO from webkitpy.common.net.bugzilla import CommitterValidator +from webkitpy.common.net.layouttestresults import path_for_layout_test, LayoutTestResults from webkitpy.common.net.statusserver import StatusServer from webkitpy.common.system.executive import ScriptError from webkitpy.common.system.deprecated_logging import error, log from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler -from webkitpy.tool.bot.commitqueuetask import CommitQueueTask -from webkitpy.tool.bot.feeders import CommitQueueFeeder -from webkitpy.tool.bot.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate +from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate +from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate from webkitpy.tool.grammar import pluralize from webkitpy.tool.multicommandtool import Command, TryAgain + class AbstractQueue(Command, QueueEngineDelegate): watchers = [ ] @@ -78,6 +82,10 @@ class AbstractQueue(Command, QueueEngineDelegate): # because our global option code looks for the first argument which does # not begin with "-" and assumes that is the command name. webkit_patch_args += ["--status-host=%s" % self._tool.status_server.host] + if self._tool.status_server.bot_id: + webkit_patch_args += ["--bot-id=%s" % self._tool.status_server.bot_id] + if self._options.port: + webkit_patch_args += ["--port=%s" % self._options.port] webkit_patch_args.extend(args) return self._tool.executive.run_and_throw_if_fail(webkit_patch_args) @@ -94,7 +102,7 @@ class AbstractQueue(Command, QueueEngineDelegate): def begin_work_queue(self): log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root)) - if self.options.confirm: + if self._options.confirm: response = self._tool.user.prompt("Are you sure? Type \"yes\" to continue: ") if (response != "yes"): error("User declined.") @@ -106,7 +114,7 @@ class AbstractQueue(Command, QueueEngineDelegate): def should_continue_work_queue(self): self._iteration_count += 1 - return not self.options.iterations or self._iteration_count <= self.options.iterations + return not self._options.iterations or self._iteration_count <= self._options.iterations def next_work_item(self): raise NotImplementedError, "subclasses must implement" @@ -123,7 +131,7 @@ class AbstractQueue(Command, QueueEngineDelegate): # Command methods def execute(self, options, args, tool, engine=QueueEngine): - self.options = options # FIXME: This code is wrong. Command.options is a list, this assumes an Options element! + self._options = options # FIXME: This code is wrong. Command.options is a list, this assumes an Options element! self._tool = tool # FIXME: This code is wrong too! Command.bind_to_tool handles this! return engine(self.name, self, self._tool.wakeup_event).run() @@ -159,6 +167,7 @@ class FeederQueue(AbstractQueue): AbstractQueue.begin_work_queue(self) self.feeders = [ CommitQueueFeeder(self._tool), + EWSFeeder(self._tool), ] def next_work_item(self): @@ -190,6 +199,9 @@ class AbstractPatchQueue(AbstractQueue): def _fetch_next_work_item(self): return self._tool.status_server.next_work_item(self.name) + def _release_work_item(self, patch): + self._tool.status_server.release_work_item(self.name, patch) + def _did_pass(self, patch): self._update_status(self._pass_status, patch) @@ -207,7 +219,7 @@ class AbstractPatchQueue(AbstractQueue): return os.path.join(self._log_directory(), "%s.log" % patch.bug_id()) -class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): +class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate): name = "commit-queue" # AbstractPatchQueue methods @@ -229,7 +241,7 @@ class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): def process_work_item(self, patch): self._cc_watchers(patch.bug_id()) - task = CommitQueueTask(self._tool, self, patch) + task = CommitQueueTask(self, patch) try: if task.run(): self._did_pass(patch) @@ -239,10 +251,22 @@ class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): validator = CommitterValidator(self._tool.bugs) validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task.failure_status_id, e)) self._did_fail(patch) + self._release_work_item(patch) + + def _error_message_for_bug(self, status_id, script_error): + if not script_error.output: + return script_error.message_with_output() + results_link = self._tool.status_server.results_url_for_status(status_id) + return "%s\nFull output: %s" % (script_error.message_with_output(), results_link) def handle_unexpected_error(self, patch, message): self.committer_validator.reject_patch_from_commit_queue(patch.id(), message) + # CommitQueueTaskDelegate methods + + def run_command(self, command): + self.run_webkit_patch(command) + def command_passed(self, message, patch): self._update_status(message, patch=patch) @@ -250,11 +274,36 @@ class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): failure_log = self._log_from_script_error_for_upload(script_error) return self._update_status(message, patch=patch, results_file=failure_log) - def _error_message_for_bug(self, status_id, script_error): - if not script_error.output: - return script_error.message_with_output() - results_link = self._tool.status_server.results_url_for_status(status_id) - return "%s\nFull output: %s" % (script_error.message_with_output(), results_link) + # FIXME: This exists for mocking, but should instead be mocked via + # some sort of tool.filesystem() object. + def _read_file_contents(self, path): + try: + with codecs.open(path, "r", "utf-8") as open_file: + return open_file.read() + except OSError, e: # File does not exist or can't be read. + return None + + # FIXME: This may belong on the Port object. + def layout_test_results(self): + results_path = self._tool.port().layout_tests_results_path() + results_html = self._read_file_contents(results_path) + if not results_html: + return None + return LayoutTestResults.results_from_string(results_html) + + def refetch_patch(self, patch): + return self._tool.bugs.fetch_attachment(patch.id()) + + def _author_emails_for_tests(self, flaky_tests): + test_paths = map(path_for_layout_test, flaky_tests) + commit_infos = self._tool.checkout().recent_commit_infos_for_files(test_paths) + return [commit_info.author().bugzilla_email() for commit_info in commit_infos if commit_info.author()] + + def report_flaky_tests(self, patch, flaky_tests): + authors = self._author_emails_for_tests(flaky_tests) + cc_explaination = " The author(s) of the test(s) have been CCed on this bug." if authors else "" + message = "The %s encountered the following flaky tests while processing attachment %s:\n\n%s\n\nPlease file bugs against the tests.%s The commit-queue is continuing to process your patch." % (self.name, patch.id(), "\n".join(flaky_tests), cc_explaination) + self._tool.bugs.post_comment_to_bug(patch.bug_id(), message, cc=authors) # StepSequenceErrorHandler methods @@ -278,6 +327,7 @@ class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler): raise TryAgain() +# FIXME: All the Rietveld code is no longer used and should be deleted. class RietveldUploadQueue(AbstractPatchQueue, StepSequenceErrorHandler): name = "rietveld-upload-queue" @@ -323,40 +373,27 @@ class RietveldUploadQueue(AbstractPatchQueue, StepSequenceErrorHandler): cls._reject_patch(tool, state["patch"].id()) -class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler): +class AbstractReviewQueue(AbstractPatchQueue, StepSequenceErrorHandler): + """This is the base-class for the EWS queues and the style-queue.""" def __init__(self, options=None): AbstractPatchQueue.__init__(self, options) def review_patch(self, patch): - raise NotImplementedError, "subclasses must implement" - - # PersistentPatchCollectionDelegate methods - - def collection_name(self): - return self.name - - def fetch_potential_patch_ids(self): - return self._tool.bugs.queries.fetch_attachment_ids_from_review_queue() - - def status_server(self): - return self._tool.status_server - - def is_terminal_status(self, status): - return status == "Pass" or status == "Fail" or status.startswith("Error:") + raise NotImplementedError("subclasses must implement") # AbstractPatchQueue methods def begin_work_queue(self): AbstractPatchQueue.begin_work_queue(self) - self._patches = PersistentPatchCollection(self) def next_work_item(self): - patch_id = self._patches.next() - if patch_id: - return self._tool.bugs.fetch_attachment(patch_id) + patch_id = self._fetch_next_work_item() + if not patch_id: + return None + return self._tool.bugs.fetch_attachment(patch_id) def should_proceed_with_work_item(self, patch): - raise NotImplementedError, "subclasses must implement" + raise NotImplementedError("subclasses must implement") def process_work_item(self, patch): try: @@ -368,6 +405,8 @@ class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, if e.exit_code != QueueEngine.handled_error_code: self._did_fail(patch) raise e + finally: + self._release_work_item(patch) def handle_unexpected_error(self, patch, message): log(message) diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py index 029814e..b37b5dc 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py @@ -60,24 +60,31 @@ class AbstractQueueTest(CommandsTest): def test_log_directory(self): self.assertEquals(TestQueue()._log_directory(), "test-queue-logs") - def _assert_run_webkit_patch(self, run_args): + def _assert_run_webkit_patch(self, run_args, port=None): queue = TestQueue() tool = MockTool() + tool.status_server.bot_id = "gort" tool.executive = Mock() queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = port queue.run_webkit_patch(run_args) - expected_run_args = ["echo", "--status-host=example.com"] + run_args + expected_run_args = ["echo", "--status-host=example.com", "--bot-id=gort"] + if port: + expected_run_args.append("--port=%s" % port) + expected_run_args.extend(run_args) tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args) def test_run_webkit_patch(self): self._assert_run_webkit_patch([1]) self._assert_run_webkit_patch(["one", 2]) + self._assert_run_webkit_patch([1], port="mockport") def test_iteration_count(self): queue = TestQueue() - queue.options = Mock() - queue.options.iterations = 3 + queue._options = Mock() + queue._options.iterations = 3 self.assertTrue(queue.should_continue_work_queue()) self.assertTrue(queue.should_continue_work_queue()) self.assertTrue(queue.should_continue_work_queue()) @@ -85,7 +92,7 @@ class AbstractQueueTest(CommandsTest): def test_no_iteration_count(self): queue = TestQueue() - queue.options = Mock() + queue._options = Mock() self.assertTrue(queue.should_continue_work_queue()) self.assertTrue(queue.should_continue_work_queue()) self.assertTrue(queue.should_continue_work_queue()) @@ -128,6 +135,8 @@ MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Reject - If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.' MOCK: update_work_items: commit-queue [106, 197] Feeding commit-queue items [106, 197] +Feeding EWS (1 r? patch, 1 new) +MOCK: submit_to_ews: 103 """, "handle_unexpected_error": "Mock error message\n", } @@ -139,25 +148,13 @@ class AbstractPatchQueueTest(CommandsTest): queue = AbstractPatchQueue() tool = MockTool() queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = None self.assertEquals(queue._fetch_next_work_item(), None) tool.status_server = MockStatusServer(work_items=[2, 1, 3]) self.assertEquals(queue._fetch_next_work_item(), 2) -class AbstractReviewQueueTest(CommandsTest): - def test_patch_collection_delegate_methods(self): - queue = TestReviewQueue() - tool = MockTool() - queue.bind_to_tool(tool) - self.assertEquals(queue.collection_name(), "test-review-queue") - self.assertEquals(queue.fetch_potential_patch_ids(), [103]) - queue.status_server() - self.assertTrue(queue.is_terminal_status("Pass")) - self.assertTrue(queue.is_terminal_status("Fail")) - self.assertTrue(queue.is_terminal_status("Error: Your patch exploded")) - self.assertFalse(queue.is_terminal_status("Foo")) - - class NeedsUpdateSequence(StepSequence): def _run(self, tool, options, state): raise CheckoutNeedsUpdate([], 1, "", None) @@ -172,7 +169,20 @@ class AlwaysCommitQueueTool(object): class SecondThoughtsCommitQueue(CommitQueue): - def _build_and_test_patch(self, patch, first_run=True): + def __init__(self): + self._reject_patch = False + CommitQueue.__init__(self) + + def run_command(self, command): + # We want to reject the patch after the first validation, + # so wait to reject it until after some other command has run. + self._reject_patch = True + return CommitQueue.run_command(self, command) + + def refetch_patch(self, patch): + if not self._reject_patch: + return self._tool.bugs.fetch_attachment(patch.id()) + attachment_dictionary = { "id": patch.id(), "bug_id": patch.bug_id(), @@ -185,9 +195,7 @@ class SecondThoughtsCommitQueue(CommitQueue): "committer_email": "foo@bar.com", "attacher_email": "Contributer1", } - patch = Attachment(attachment_dictionary, None) - self._tool.bugs.set_override_patch(patch) - return True + return Attachment(attachment_dictionary, None) class CommitQueueTest(QueuesTest): @@ -215,6 +223,7 @@ MOCK: update_status: commit-queue Pass "process_work_item": """MOCK: update_status: commit-queue Patch does not apply MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'MOCK script error' MOCK: update_status: commit-queue Fail +MOCK: release_work_item: commit-queue 197 """, "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting patch 197 from commit-queue.' and additional comment 'Mock error message'\n", "handle_script_error": "ScriptError error message\n", @@ -236,7 +245,7 @@ MOCK: update_status: commit-queue Fail "next_work_item": "", "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] MOCK: update_status: commit-queue Applied patch -MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] MOCK: update_status: commit-queue Built patch MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] MOCK: update_status: commit-queue Passed tests @@ -259,7 +268,7 @@ MOCK: update_status: commit-queue Pass "next_work_item": "", "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--force-clean', '--non-interactive', '--quiet', 197] MOCK: update_status: commit-queue Applied patch -MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build', '--build-style=both', '--quiet'] +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=both', '--quiet'] MOCK: update_status: commit-queue Built patch MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test', '--no-clean', '--no-update', '--test', '--quiet', '--non-interactive'] MOCK: update_status: commit-queue Passed tests @@ -290,14 +299,39 @@ MOCK: update_status: commit-queue Pass def test_manual_reject_during_processing(self): queue = SecondThoughtsCommitQueue() queue.bind_to_tool(MockTool()) + queue._options = Mock() + queue._options.port = None expected_stderr = """MOCK: update_status: commit-queue Applied patch MOCK: update_status: commit-queue Built patch MOCK: update_status: commit-queue Passed tests -MOCK: update_status: commit-queue Landed patch -MOCK: update_status: commit-queue Pass +MOCK: update_status: commit-queue Retry +MOCK: release_work_item: commit-queue 197 """ OutputCapture().assert_outputs(self, queue.process_work_item, [MockPatch()], expected_stderr=expected_stderr) + def test_report_flaky_tests(self): + queue = CommitQueue() + queue.bind_to_tool(MockTool()) + expected_stderr = """MOCK bug comment: bug_id=142, cc=['abarth@webkit.org'] +--- Begin comment --- +The commit-queue encountered the following flaky tests while processing attachment 197: + +foo/bar.html +bar/baz.html + +Please file bugs against the tests. The author(s) of the test(s) have been CCed on this bug. The commit-queue is continuing to process your patch. +--- End comment --- + +""" + OutputCapture().assert_outputs(self, queue.report_flaky_tests, [MockPatch(), ["foo/bar.html", "bar/baz.html"]], expected_stderr=expected_stderr) + + def test_layout_test_results(self): + queue = CommitQueue() + queue.bind_to_tool(MockTool()) + queue._read_file_contents = lambda path: None + self.assertEquals(queue.layout_test_results(), None) + queue._read_file_contents = lambda path: "" + self.assertEquals(queue.layout_test_results(), None) class RietveldUploadQueueTest(QueuesTest): def test_rietveld_upload_queue(self): @@ -315,11 +349,11 @@ class StyleQueueTest(QueuesTest): def test_style_queue(self): expected_stderr = { "begin_work_queue": self._default_begin_work_queue_stderr("style-queue", MockSCM.fake_checkout_root), - "next_work_item": "MOCK: update_work_items: style-queue [103]\n", + "next_work_item": "", "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n", - "process_work_item": "MOCK: update_status: style-queue Pass\n", + "process_work_item": "MOCK: update_status: style-queue Pass\nMOCK: release_work_item: style-queue 197\n", "handle_unexpected_error": "Mock error message\n", - "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=142, cc=[]\n--- Begin comment ---\\Attachment 197 did not pass style-queue:\n\nScriptError error message\n\nIf any of these errors are false positives, please file a bug against check-webkit-style.\n--- End comment ---\n\n", + "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=142, cc=[]\n--- Begin comment ---\nAttachment 197 did not pass style-queue:\n\nScriptError error message\n\nIf any of these errors are false positives, please file a bug against check-webkit-style.\n--- End comment ---\n\n", } expected_exceptions = { "handle_script_error": SystemExit, diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py index 9f3583d..379d380 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py @@ -80,13 +80,18 @@ class QueuesTest(unittest.TestCase): string_replacements = {"name": name, 'checkout_dir': checkout_dir} return "CAUTION: %(name)s will discard all local changes in \"%(checkout_dir)s\"\nRunning WebKit %(name)s.\nMOCK: update_status: %(name)s Starting Queue\n" % string_replacements - def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, expected_exceptions=None, options=Mock(), tool=MockTool()): + def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, expected_exceptions=None, options=None, tool=None): + if not tool: + tool = MockTool() if not expected_stdout: expected_stdout = {} if not expected_stderr: expected_stderr = {} if not args: args = [] + if not options: + options = Mock() + options.port = None if not work_item: work_item = self.mock_work_item tool.user.prompt = lambda message: "yes" diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py index 23d013d..145f485 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py @@ -54,77 +54,47 @@ class SheriffBot(AbstractQueue, StepSequenceErrorHandler): self._irc_bot = SheriffIRCBot(self._tool, self._sheriff) self._tool.ensure_irc_connected(self._irc_bot.irc_delegate()) - def work_item_log_path(self, new_failures): - return os.path.join("%s-logs" % self.name, "%s.log" % new_failures.keys()[0]) - - def _new_failures(self, revisions_causing_failures, old_failing_svn_revisions): - # We ignore failures that might have been caused by svn_revisions that - # we've already complained about. This is conservative in the sense - # that we might be ignoring some new failures, but our experience has - # been that skipping this check causes a lot of spam for builders that - # take a long time to cycle. - old_failing_builder_names = [] - for svn_revision in old_failing_svn_revisions: - old_failing_builder_names.extend( - [builder.name() for builder in revisions_causing_failures[svn_revision]]) - - new_failures = {} - for svn_revision, builders in revisions_causing_failures.items(): - if svn_revision in old_failing_svn_revisions: - # FIXME: We should re-process the work item after some time delay. - # https://bugs.webkit.org/show_bug.cgi?id=36581 - continue - new_builders = [builder for builder in builders - if builder.name() not in old_failing_builder_names] - if new_builders: - new_failures[svn_revision] = new_builders - - return new_failures + def work_item_log_path(self, failure_map): + return None + + def _is_old_failure(self, revision): + return self._tool.status_server.svn_revision(revision) def next_work_item(self): self._irc_bot.process_pending_messages() self._update() - # We do one read from buildbot to ensure a consistent view. - revisions_causing_failures = self._tool.buildbot.failure_map().revisions_causing_failures() - - # Similarly, we read once from our the status_server. - old_failing_svn_revisions = [] - for svn_revision in revisions_causing_failures.keys(): - if self._tool.status_server.svn_revision(svn_revision): - old_failing_svn_revisions.append(svn_revision) + # FIXME: We need to figure out how to provoke_flaky_builders. - new_failures = self._new_failures(revisions_causing_failures, - old_failing_svn_revisions) + failure_map = self._tool.buildbot.failure_map() + failure_map.filter_out_old_failures(self._is_old_failure) + if failure_map.is_empty(): + return None + return failure_map - self._sheriff.provoke_flaky_builders(revisions_causing_failures) - return new_failures - - def should_proceed_with_work_item(self, new_failures): + def should_proceed_with_work_item(self, failure_map): # Currently, we don't have any reasons not to proceed with work items. return True - def process_work_item(self, new_failures): - blame_list = new_failures.keys() - for svn_revision, builders in new_failures.items(): + def process_work_item(self, failure_map): + failing_revisions = failure_map.failing_revisions() + for revision in failing_revisions: + builders = failure_map.builders_failing_for(revision) + tests = failure_map.tests_failing_for(revision) try: - commit_info = self._tool.checkout().commit_info_for_revision(svn_revision) + commit_info = self._tool.checkout().commit_info_for_revision(revision) if not commit_info: print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision continue self._sheriff.post_irc_warning(commit_info, builders) - self._sheriff.post_blame_comment_on_bug(commit_info, - builders, - blame_list) - self._sheriff.post_automatic_rollout_patch(commit_info, - builders) + self._sheriff.post_blame_comment_on_bug(commit_info, builders, tests) + finally: for builder in builders: - self._tool.status_server.update_svn_revision(svn_revision, - builder.name()) + self._tool.status_server.update_svn_revision(revision, builder.name()) return True - def handle_unexpected_error(self, new_failures, message): + def handle_unexpected_error(self, failure_map, message): log(message) # StepSequenceErrorHandler methods diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py index a63ec24..32eb016 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py @@ -30,7 +30,7 @@ import os from webkitpy.tool.commands.queuestest import QueuesTest from webkitpy.tool.commands.sheriffbot import SheriffBot -from webkitpy.tool.mocktool import MockBuilder +from webkitpy.tool.mocktool import * class SheriffBotTest(QueuesTest): @@ -38,36 +38,19 @@ class SheriffBotTest(QueuesTest): builder2 = MockBuilder("Builder2") def test_sheriff_bot(self): - mock_work_item = { - 29837: [self.builder1], - } + mock_work_item = MockFailureMap(MockTool().buildbot) expected_stderr = { "begin_work_queue": self._default_begin_work_queue_stderr("sheriff-bot", os.getcwd()), "next_work_item": "", - "process_work_item": "MOCK: irc.post: abarth, darin, eseidel: http://trac.webkit.org/changeset/29837 might have broken Builder1\nMOCK bug comment: bug_id=42, cc=['abarth@webkit.org', 'eric@webkit.org']\n--- Begin comment ---\\http://trac.webkit.org/changeset/29837 might have broken Builder1\n--- End comment ---\n\n", + "process_work_item": """MOCK: irc.post: abarth, darin, eseidel: http://trac.webkit.org/changeset/29837 might have broken Builder1 +MOCK bug comment: bug_id=42, cc=['abarth@webkit.org', 'eric@webkit.org'] +--- Begin comment --- +http://trac.webkit.org/changeset/29837 might have broken Builder1 +The following tests are not passing: +mock-test-1 +--- End comment --- + +""", "handle_unexpected_error": "Mock error message\n" } self.assert_queue_outputs(SheriffBot(), work_item=mock_work_item, expected_stderr=expected_stderr) - - revisions_causing_failures = { - 1234: [builder1], - 1235: [builder1, builder2], - } - - def test_new_failures(self): - old_failing_svn_revisions = [] - self.assertEquals(SheriffBot()._new_failures(self.revisions_causing_failures, - old_failing_svn_revisions), - self.revisions_causing_failures) - - def test_new_failures_with_old_revisions(self): - old_failing_svn_revisions = [1234] - self.assertEquals(SheriffBot()._new_failures(self.revisions_causing_failures, - old_failing_svn_revisions), - {1235: [builder2]}) - - def test_new_failures_with_old_revisions(self): - old_failing_svn_revisions = [1235] - self.assertEquals(SheriffBot()._new_failures(self.revisions_causing_failures, - old_failing_svn_revisions), - {}) diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/upload_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/upload_unittest.py index 5f3f400..0d096b6 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/upload_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/upload_unittest.py @@ -52,11 +52,12 @@ class UploadCommandsTest(CommandsTest): def test_post(self): options = MockOptions() + options.cc = None + options.check_style = True + options.comment = None options.description = "MOCK description" options.request_commit = False options.review = True - options.comment = None - options.cc = None expected_stderr = """Running check-webkit-style MOCK: user.open_url: file://... Obsoleting 2 old patches on bug 42 @@ -81,11 +82,12 @@ MOCK: user.open_url: http://example.com/42 def test_upload(self): options = MockOptions() + options.cc = None + options.check_style = True + options.comment = None options.description = "MOCK description" options.request_commit = False options.review = True - options.comment = None - options.cc = None expected_stderr = """Running check-webkit-style MOCK: user.open_url: file://... Obsoleting 2 old patches on bug 42 @@ -103,7 +105,7 @@ MOCK: user.open_url: http://example.com/42 options = Mock() options.bug_id = 42 options.comment = "MOCK comment" - expected_stderr = "Bug: <http://example.com/42> Bug with two r+'d and cq+'d patches, one of which has an invalid commit-queue setter.\nRevision: 9876\nMOCK: user.open_url: http://example.com/42\nAdding comment to Bug 42.\nMOCK bug comment: bug_id=42, cc=None\n--- Begin comment ---\\MOCK comment\n\nCommitted r9876: <http://trac.webkit.org/changeset/9876>\n--- End comment ---\n\n" + expected_stderr = "Bug: <http://example.com/42> Bug with two r+'d and cq+'d patches, one of which has an invalid commit-queue setter.\nRevision: 9876\nMOCK: user.open_url: http://example.com/42\nAdding comment to Bug 42.\nMOCK bug comment: bug_id=42, cc=None\n--- Begin comment ---\nMOCK comment\n\nCommitted r9876: <http://trac.webkit.org/changeset/9876>\n--- End comment ---\n\n" self.assert_execute_outputs(MarkBugFixed(), [], expected_stderr=expected_stderr, tool=tool, options=options) def test_edit_changelog(self): diff --git a/WebKitTools/Scripts/webkitpy/tool/main.py b/WebKitTools/Scripts/webkitpy/tool/main.py index 9531b63..ce6666e 100755 --- a/WebKitTools/Scripts/webkitpy/tool/main.py +++ b/WebKitTools/Scripts/webkitpy/tool/main.py @@ -34,6 +34,7 @@ import threading from webkitpy.common.checkout.api import Checkout from webkitpy.common.checkout.scm import default_scm +from webkitpy.common.config.ports import WebKitPort from webkitpy.common.net.bugzilla import Bugzilla from webkitpy.common.net.buildbot import BuildBot from webkitpy.common.net.rietveld import Rietveld @@ -52,15 +53,16 @@ from webkitpy.tool.commands.queues import * from webkitpy.tool.commands.sheriffbot import * from webkitpy.tool.commands.upload import * from webkitpy.tool.multicommandtool import MultiCommandTool -from webkitpy.common.system.deprecated_logging import log class WebKitPatch(MultiCommandTool): global_options = [ make_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="enable all logging"), make_option("--dry-run", action="store_true", dest="dry_run", default=False, help="do not touch remote servers"), - make_option("--status-host", action="store", dest="status_host", type="string", nargs=1, help="Hostname (e.g. localhost or commit.webkit.org) where status updates should be posted."), - make_option("--irc-password", action="store", dest="irc_password", type="string", nargs=1, help="Password to use when communicating via IRC."), + make_option("--status-host", action="store", dest="status_host", type="string", help="Hostname (e.g. localhost or commit.webkit.org) where status updates should be posted."), + make_option("--bot-id", action="store", dest="bot_id", type="string", help="Identifier for this bot (if multiple bots are running for a queue)"), + make_option("--irc-password", action="store", dest="irc_password", type="string", help="Password to use when communicating via IRC."), + make_option("--port", action="store", dest="port", default=None, help="Specify a port (e.g., mac, qt, gtk, ...)."), ] def __init__(self, path): @@ -72,6 +74,7 @@ class WebKitPatch(MultiCommandTool): self.buildbot = BuildBot() self.executive = Executive() self._irc = None + self._port = None self.user = User() self._scm = None self._checkout = None @@ -90,6 +93,9 @@ class WebKitPatch(MultiCommandTool): self._checkout = Checkout(self.scm()) return self._checkout + def port(self): + return self._port + def ensure_irc_connected(self, irc_delegate): if not self._irc: self._irc = IRCProxy(irc_delegate) @@ -123,8 +129,12 @@ class WebKitPatch(MultiCommandTool): self.codereview.dryrun = True if options.status_host: self.status_server.set_host(options.status_host) + if options.bot_id: + self.status_server.set_bot_id(options.bot_id) if options.irc_password: self.irc_password = options.irc_password + # If options.port is None, we'll get the default port for this platform. + self._port = WebKitPort.port(options.port) def should_execute_command(self, command): if command.requires_local_commits and not self.scm().supports_local_commits(): diff --git a/WebKitTools/Scripts/webkitpy/tool/mocktool.py b/WebKitTools/Scripts/webkitpy/tool/mocktool.py index 277bd08..05b30dd 100644 --- a/WebKitTools/Scripts/webkitpy/tool/mocktool.py +++ b/WebKitTools/Scripts/webkitpy/tool/mocktool.py @@ -317,7 +317,7 @@ class MockBugzilla(Mock): flag_name, flag_value, attachment_id, comment_text, additional_comment_text)) def post_comment_to_bug(self, bug_id, comment_text, cc=None): - log("MOCK bug comment: bug_id=%s, cc=%s\n--- Begin comment ---\%s\n--- End comment ---\n" % ( + log("MOCK bug comment: bug_id=%s, cc=%s\n--- Begin comment ---\n%s\n--- End comment ---\n" % ( bug_id, cc, comment_text)) def add_patch_to_bug(self, @@ -350,14 +350,24 @@ class MockBuilder(object): self._name, username, comments)) -class MockFailureMap(): +class MockFailureMap(object): def __init__(self, buildbot): self._buildbot = buildbot - def revisions_causing_failures(self): - return { - "29837": [self._buildbot.builder_with_name("Builder1")], - } + def is_empty(self): + return False + + def filter_out_old_failures(self, is_old_revision): + pass + + def failing_revisions(self): + return [29837] + + def builders_failing_for(self, revision): + return [self._buildbot.builder_with_name("Builder1")] + + def tests_failing_for(self, revision): + return ["mock-test-1"] class MockBuildBot(object): @@ -419,7 +429,7 @@ class MockSCM(Mock): # will actually be the root. Since getcwd() is wrong, use a globally fake root for now. self.checkout_root = self.fake_checkout_root - def create_patch(self, git_commit): + def create_patch(self, git_commit, changed_files=None): return "Patch1" def commit_ids_from_commitish_arguments(self, args): @@ -447,6 +457,9 @@ class MockCheckout(object): _committer_list = CommitterList() def commit_info_for_revision(self, svn_revision): + # The real Checkout would probably throw an exception, but this is the only way tests have to get None back at the moment. + if not svn_revision: + return None return CommitInfo(svn_revision, "eric@webkit.org", { "bug_id": 42, "author_name": "Adam Barth", @@ -459,7 +472,10 @@ class MockCheckout(object): def bug_id_for_revision(self, svn_revision): return 12345 - def modified_changelogs(self, git_commit): + def recent_commit_infos_for_files(self, paths): + return [self.commit_info_for_revision(32)] + + def modified_changelogs(self, git_commit, changed_files=None): # Ideally we'd return something more interesting here. The problem is # that LandDiff will try to actually read the patch from disk! return [] @@ -515,8 +531,9 @@ class MockIRC(object): class MockStatusServer(object): - def __init__(self, work_items=None): + def __init__(self, bot_id=None, work_items=None): self.host = "example.com" + self.bot_id = bot_id self._work_items = work_items or [] def patch_status(self, queue_name, patch_id): @@ -530,10 +547,16 @@ class MockStatusServer(object): return None return self._work_items[0] + def release_work_item(self, queue_name, patch): + log("MOCK: release_work_item: %s %s" % (queue_name, patch.id())) + def update_work_items(self, queue_name, work_items): self._work_items = work_items log("MOCK: update_work_items: %s %s" % (queue_name, work_items)) + def submit_to_ews(self, patch_id): + log("MOCK: submit_to_ews: %s" % (patch_id)) + def update_status(self, queue_name, status, patch=None, results_file=None): log("MOCK: update_status: %s %s" % (queue_name, status)) return 187 @@ -567,9 +590,17 @@ class MockExecute(Mock): return "MOCK output of child process" -class MockOptions(Mock): - no_squash = False - squash = False +class MockOptions(object): + """Mock implementation of optparse.Values.""" + + def __init__(self, **kwargs): + # The caller can set option values using keyword arguments. We don't + # set any values by default because we don't know how this + # object will be used. Generally speaking unit tests should + # subclass this or provider wrapper functions that set a common + # set of options. + for key, value in kwargs.items(): + self.__dict__[key] = value class MockRietveld(): @@ -630,3 +661,22 @@ class MockTool(): def path(self): return "echo" + + def port(self): + return Mock() + + +class MockBrowser(object): + params = {} + + def open(self, url): + pass + + def select_form(self, name): + pass + + def __setitem__(self, key, value): + self.params[key] = value + + def submit(self): + return Mock(file) diff --git a/WebKitTools/Scripts/webkitpy/tool/mocktool_unittest.py b/WebKitTools/Scripts/webkitpy/tool/mocktool_unittest.py new file mode 100644 index 0000000..cceaa2e --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/mocktool_unittest.py @@ -0,0 +1,59 @@ +# 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 mocktool import MockOptions + + +class MockOptionsTest(unittest.TestCase): + # MockOptions() should implement the same semantics that + # optparse.Values does. + + def test_get__set(self): + # Test that we can still set options after we construct the + # object. + options = MockOptions() + options.foo = 'bar' + self.assertEqual(options.foo, 'bar') + + def test_get__unset(self): + # Test that unset options raise an exception (regular Mock + # objects return an object and hence are different from + # optparse.Values()). + options = MockOptions() + self.assertRaises(AttributeError, lambda: options.foo) + + def test_kwarg__set(self): + # Test that keyword arguments work in the constructor. + options = MockOptions(foo='bar') + self.assertEqual(options.foo, 'bar') + + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/abstractstep.py b/WebKitTools/Scripts/webkitpy/tool/steps/abstractstep.py index 9ceb2cb..5525ea0 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/abstractstep.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/abstractstep.py @@ -36,27 +36,23 @@ class AbstractStep(object): def __init__(self, tool, options): self._tool = tool self._options = options - self._port = None + # FIXME: This should use tool.port() def _run_script(self, script_name, args=None, quiet=False, port=WebKitPort): log("Running %s" % script_name) command = [port.script_path(script_name)] if args: command.extend(args) - # FIXME: This should use self.port() self._tool.executive.run_and_throw_if_fail(command, quiet) - # FIXME: The port should live on the tool. - def port(self): - if self._port: - return self._port - self._port = WebKitPort.port(self._options.port) - return self._port + def _changed_files(self, state): + return self.cached_lookup(state, "changed_files") _well_known_keys = { - "diff": lambda self, state: self._tool.scm().create_patch(self._options.git_commit), - "changelogs": lambda self, state: self._tool.checkout().modified_changelogs(self._options.git_commit), "bug_title": lambda self, state: self._tool.bugs.fetch_bug(state["bug_id"]).title(), + "changed_files": lambda self, state: self._tool.scm().changed_files(self._options.git_commit), + "diff": lambda self, state: self._tool.scm().create_patch(self._options.git_commit, changed_files=self._changed_files(state)), + "changelogs": lambda self, state: self._tool.checkout().modified_changelogs(self._options.git_commit, changed_files=self._changed_files(state)), } def cached_lookup(self, state, key, promise=None): @@ -67,6 +63,11 @@ class AbstractStep(object): state[key] = promise(self, state) return state[key] + def did_modify_checkout(self, state): + state["diff"] = None + state["changelogs"] = None + state["changed_files"] = None + @classmethod def options(cls): return [ diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/build.py b/WebKitTools/Scripts/webkitpy/tool/steps/build.py index 456db25..0990b8b 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/build.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/build.py @@ -41,7 +41,7 @@ class Build(AbstractStep): ] def build(self, build_style): - self._tool.executive.run_and_throw_if_fail(self.port().build_webkit_command(build_style=build_style), self._options.quiet) + self._tool.executive.run_and_throw_if_fail(self._tool.port().build_webkit_command(build_style=build_style), self._options.quiet) def run(self, state): if not self._options.build: diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/editchangelog.py b/WebKitTools/Scripts/webkitpy/tool/steps/editchangelog.py index de9b4e4..4d9646f 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/editchangelog.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/editchangelog.py @@ -35,3 +35,4 @@ class EditChangeLog(AbstractStep): def run(self, state): os.chdir(self._tool.scm().checkout_root) self._tool.user.edit_changelog(self.cached_lookup(state, "changelogs")) + self.did_modify_checkout(state) diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/options.py b/WebKitTools/Scripts/webkitpy/tool/steps/options.py index 3dc1963..835fdba 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/options.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/options.py @@ -50,7 +50,6 @@ class Options(object): obsolete_patches = make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one.") open_bug = make_option("--open-bug", action="store_true", dest="open_bug", default=False, help="Opens the associated bug in a browser.") parent_command = make_option("--parent-command", action="store", dest="parent_command", default=None, help="(Internal) The command that spawned this instance.") - port = make_option("--port", action="store", dest="port", default=None, help="Specify a port (e.g., mac, qt, gtk, ...).") quiet = make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output.") request_commit = make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review.") review = make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review.") diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/preparechangelog.py b/WebKitTools/Scripts/webkitpy/tool/steps/preparechangelog.py index ce04024..099dfe3 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/preparechangelog.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/preparechangelog.py @@ -39,7 +39,6 @@ class PrepareChangeLog(AbstractStep): @classmethod def options(cls): return AbstractStep.options() + [ - Options.port, Options.quiet, Options.email, Options.git_commit, @@ -62,7 +61,7 @@ class PrepareChangeLog(AbstractStep): self._ensure_bug_url(state) return os.chdir(self._tool.scm().checkout_root) - args = [self.port().script_path("prepare-ChangeLog")] + args = [self._tool.port().script_path("prepare-ChangeLog")] if state.get("bug_id"): args.append("--bug=%s" % state["bug_id"]) if self._options.email: @@ -75,4 +74,4 @@ class PrepareChangeLog(AbstractStep): self._tool.executive.run_and_throw_if_fail(args, self._options.quiet) except ScriptError, e: error("Unable to prepare ChangeLogs.") - state["diff"] = None # We've changed the diff + self.did_modify_checkout(state) diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/runtests.py b/WebKitTools/Scripts/webkitpy/tool/steps/runtests.py index aff1fd9..dcbfc44 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/runtests.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/runtests.py @@ -37,7 +37,6 @@ class RunTests(AbstractStep): Options.test, Options.non_interactive, Options.quiet, - Options.port, ] def run(self, state): @@ -46,14 +45,17 @@ class RunTests(AbstractStep): # Run the scripting unit tests first because they're quickest. log("Running Python unit tests") - self._tool.executive.run_and_throw_if_fail(self.port().run_python_unittests_command()) + self._tool.executive.run_and_throw_if_fail(self._tool.port().run_python_unittests_command()) log("Running Perl unit tests") - self._tool.executive.run_and_throw_if_fail(self.port().run_perl_unittests_command()) - log("Running JavaScriptCore tests") - self._tool.executive.run_and_throw_if_fail(self.port().run_javascriptcore_tests_command(), quiet=True) + self._tool.executive.run_and_throw_if_fail(self._tool.port().run_perl_unittests_command()) + + javascriptcore_tests_command = self._tool.port().run_javascriptcore_tests_command() + if javascriptcore_tests_command: + log("Running JavaScriptCore tests") + self._tool.executive.run_and_throw_if_fail(javascriptcore_tests_command, quiet=True) log("Running run-webkit-tests") - args = self.port().run_webkit_tests_command() + args = self._tool.port().run_webkit_tests_command() if self._options.non_interactive: args.append("--no-launch-safari") args.append("--exit-after-n-failures=1") @@ -61,7 +63,7 @@ class RunTests(AbstractStep): # FIXME: Hack to work around https://bugs.webkit.org/show_bug.cgi?id=38912 # when running the commit-queue on a mac leopard machine since compositing # does not work reliably on Leopard due to various graphics driver/system bugs. - if self.port().name() == "Mac" and self.port().is_leopard(): + if self._tool.port().name() == "Mac" and self._tool.port().is_leopard(): tests_to_ignore = [] tests_to_ignore.append("compositing") diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/steps_unittest.py b/WebKitTools/Scripts/webkitpy/tool/steps/steps_unittest.py index 15f275a..7eb8e3a 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/steps_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/steps_unittest.py @@ -37,39 +37,49 @@ from webkitpy.tool.steps.promptforbugortitle import PromptForBugOrTitle class StepsTest(unittest.TestCase): + def _step_options(self): + options = MockOptions() + options.non_interactive = True + options.port = 'MOCK port' + options.quiet = True + options.test = True + return options + def _run_step(self, step, tool=None, options=None, state=None): if not tool: tool = MockTool() if not options: - options = MockOptions() + options = self._step_options() if not state: state = {} step(tool, options).run(state) def test_update_step(self): - options = MockOptions() + tool = MockTool() + options = self._step_options() options.update = True expected_stderr = "Updating working directory\n" - OutputCapture().assert_outputs(self, self._run_step, [Update, options], expected_stderr=expected_stderr) + OutputCapture().assert_outputs(self, self._run_step, [Update, tool, options], expected_stderr=expected_stderr) def test_prompt_for_bug_or_title_step(self): tool = MockTool() tool.user.prompt = lambda message: 42 self._run_step(PromptForBugOrTitle, tool=tool) - def test_runtests_leopard_commit_queue_hack(self): + def test_runtests_leopard_commit_queue_hack_step(self): expected_stderr = "Running Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\n" OutputCapture().assert_outputs(self, self._run_step, [RunTests], expected_stderr=expected_stderr) - def test_runtests_leopard_commit_queue_hack(self): - mock_options = MockOptions() - mock_options.non_interactive = True + def test_runtests_leopard_commit_queue_hack_command(self): + mock_options = self._step_options() step = RunTests(MockTool(log_executive=True), mock_options) # FIXME: We shouldn't use a real port-object here, but there is too much to mock at the moment. mock_port = WebKitPort() mock_port.name = lambda: "Mac" mock_port.is_leopard = lambda: True - step.port = lambda: mock_port + tool = MockTool(log_executive=True) + tool.port = lambda: mock_port + step = RunTests(tool, mock_options) expected_stderr = """Running Python unit tests MOCK run_and_throw_if_fail: ['WebKitTools/Scripts/test-webkitpy'] Running Perl unit tests diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/update.py b/WebKitTools/Scripts/webkitpy/tool/steps/update.py index 0f450f3..cd1d4d8 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/update.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/update.py @@ -36,11 +36,10 @@ class Update(AbstractStep): def options(cls): return AbstractStep.options() + [ Options.update, - Options.port, ] def run(self, state): if not self._options.update: return log("Updating working directory") - self._tool.executive.run_and_throw_if_fail(self.port().update_webkit_command(), quiet=True) + self._tool.executive.run_and_throw_if_fail(self._tool.port().update_webkit_command(), quiet=True) diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py b/WebKitTools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py index a037422..b475378 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py @@ -41,5 +41,8 @@ class UpdateChangeLogsWithReviewerTest(unittest.TestCase): def test_empty_state(self): capture = OutputCapture() - step = UpdateChangeLogsWithReviewer(MockTool(), MockOptions()) + options = MockOptions() + options.reviewer = 'MOCK reviewer' + options.git_commit = 'MOCK git commit' + step = UpdateChangeLogsWithReviewer(MockTool(), options) capture.assert_outputs(self, step.run, [{}]) |